refactor: restructure repo layout
This commit is contained in:
55
llm-functions/mcp/bridge/README.md
Normal file
55
llm-functions/mcp/bridge/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# MCP-Bridge
|
||||
|
||||
Let external MCP tools be used by LLM-Functions.
|
||||
|
||||
## Get Started
|
||||
|
||||
### 1. Create a `mcp.json` at `<llm-functions-dir>`.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlite": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-server-sqlite",
|
||||
"--db-path",
|
||||
"/tmp/foo.db"
|
||||
]
|
||||
},
|
||||
"git": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-server-git",
|
||||
"--repository",
|
||||
"path/to/git/repo"
|
||||
],
|
||||
"prefix": false
|
||||
},
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-github"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> MCP-Bridge will launch the server and register all the tools listed by the server.
|
||||
|
||||
> To avoid name clashes, The server automatically prefix tool names with `<server>_`. You can disable this behavior by add `prefix: false` to server configuration.
|
||||
|
||||
### 2. Run the bridge server, build mcp tool binaries, update functions.json, all with:
|
||||
|
||||
```
|
||||
argc mcp start
|
||||
```
|
||||
|
||||
> Run `argc mcp stop` to stop the bridge server, recover functions.json.
|
||||
|
||||
> Run `argc mcp logs` to check the server's logs.
|
||||
198
llm-functions/mcp/bridge/index.js
Normal file
198
llm-functions/mcp/bridge/index.js
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import express from "express";
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.MCP_BRIDGE_PORT || 8808;
|
||||
|
||||
let [rootDir] = process.argv.slice(2);
|
||||
|
||||
if (!rootDir) {
|
||||
console.error("Usage: mcp-bridge <llm-functions-dir>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let mcpServers = {};
|
||||
const mcpJsonPath = path.join(rootDir, "mcp.json");
|
||||
try {
|
||||
const data = await fs.promises.readFile(mcpJsonPath, "utf8");
|
||||
mcpServers = JSON.parse(data)?.mcpServers;
|
||||
} catch {
|
||||
console.error(`Failed to read json at '${mcpJsonPath}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function startMcpServer(id, serverConfig) {
|
||||
console.log(`Starting ${id} server...`);
|
||||
const capabilities = { tools: {} };
|
||||
const { prefix = true, ...rest } = serverConfig;
|
||||
const transport = new StdioClientTransport({
|
||||
...rest,
|
||||
});
|
||||
const client = new Client(
|
||||
{ name: id, version: "1.0.0" },
|
||||
{ capabilities }
|
||||
);
|
||||
await client.connect(transport);
|
||||
const { tools: toolDefinitions } = await client.listTools()
|
||||
const tools = toolDefinitions.map(
|
||||
({ name, description, inputSchema }) =>
|
||||
({
|
||||
spec: {
|
||||
name: `${formatToolName(id, name, prefix)}`,
|
||||
description,
|
||||
parameters: inputSchema,
|
||||
mcp: id,
|
||||
},
|
||||
impl: async args => {
|
||||
const res = await client.callTool({
|
||||
name: name,
|
||||
arguments: args,
|
||||
});
|
||||
const content = res.content;
|
||||
let text = arrayify(content)?.map(c => {
|
||||
switch (c.type) {
|
||||
case "text":
|
||||
return c.text || ""
|
||||
case "image":
|
||||
return c.data
|
||||
case "resource":
|
||||
return c.resource?.uri || ""
|
||||
default:
|
||||
return c
|
||||
}
|
||||
}).join("\n");
|
||||
if (res.isError) {
|
||||
text = `Tool Error\n${text}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
})
|
||||
);
|
||||
return {
|
||||
tools,
|
||||
[Symbol.asyncDispose]: async () => {
|
||||
try {
|
||||
console.log(`Closing ${id} server...`);
|
||||
await client.close();
|
||||
await transport.close();
|
||||
} catch { }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function runBridge() {
|
||||
let hasError = false;
|
||||
let runningMcpServers = await Promise.all(
|
||||
Object.entries(mcpServers).map(
|
||||
async ([name, serverConfig]) => {
|
||||
try {
|
||||
return await startMcpServer(name, serverConfig)
|
||||
} catch (err) {
|
||||
hasError = true;
|
||||
console.error(`Failed to start ${name} server; ${err.message}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
runningMcpServers = runningMcpServers.filter(s => !!s);
|
||||
const stopMcpServers = () => Promise.all(runningMcpServers.map(s => s[Symbol.asyncDispose]()));
|
||||
if (hasError) {
|
||||
await stopMcpServers();
|
||||
return;
|
||||
}
|
||||
|
||||
const definitions = runningMcpServers.flatMap(s => s.tools.map(t => t.spec));
|
||||
const runTool = async (name, args) => {
|
||||
for (const server of runningMcpServers) {
|
||||
const tool = server.tools.find(t => t.spec.name === name);
|
||||
if (tool) {
|
||||
return tool.impl(args);
|
||||
}
|
||||
}
|
||||
return `Not found tool '${name}'`;
|
||||
};
|
||||
|
||||
app.use((err, _req, res, _next) => {
|
||||
res.status(500).send(err?.message || err);
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/", (_req, res) => {
|
||||
res.send(`# MCP Bridge API
|
||||
|
||||
- POST /tools/:name
|
||||
\`\`\`
|
||||
curl -X POST http://localhost:8808/tools/filesystem_write_file \\
|
||||
-H 'content-type: application/json' \\
|
||||
-d '{"path": "/tmp/file1", "content": "hello world"}'
|
||||
\`\`\`
|
||||
- GET /tools
|
||||
\`\`\`
|
||||
curl http://localhost:8808/tools
|
||||
\`\`\`
|
||||
`);
|
||||
});
|
||||
|
||||
app.get("/tools", (_req, res) => {
|
||||
res.json(definitions);
|
||||
});
|
||||
|
||||
app.post("/tools/:name", async (req, res) => {
|
||||
try {
|
||||
const output = await runTool(req.params.name, req.body);
|
||||
res.send(output);
|
||||
} catch (err) {
|
||||
res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/pid", (_req, res) => {
|
||||
res.send(process.pid.toString());
|
||||
});
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
res.send("OK");
|
||||
});
|
||||
|
||||
app.use((_req, res, _next) => {
|
||||
res.status(404).send("Not found");
|
||||
});
|
||||
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
return async () => {
|
||||
server.close(() => console.log("Http server closed"));
|
||||
await stopMcpServers();
|
||||
};
|
||||
}
|
||||
|
||||
function arrayify(a) {
|
||||
let r;
|
||||
if (a === undefined) r = [];
|
||||
else if (Array.isArray(a)) r = a.slice(0);
|
||||
else r = [a];
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
function formatToolName(serverName, toolName, prefix) {
|
||||
const name = prefix ? `${serverName}_${toolName}` : toolName;
|
||||
return name.toLowerCase().replace(/-/g, "_");
|
||||
}
|
||||
|
||||
runBridge()
|
||||
.then(stop => {
|
||||
if (stop) {
|
||||
process.on('SIGINT', stop);
|
||||
process.on('SIGTERM', stop);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
22
llm-functions/mcp/bridge/package.json
Normal file
22
llm-functions/mcp/bridge/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "mcp-bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "Let MCP tools be used by LLM functions",
|
||||
"license": "MIT",
|
||||
"author": "sigoden <sigoden@gmail.com>",
|
||||
"homepage": "https://github.com/sigoden/llm-functions/tree/main/mcp/bridge",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sigoden/llm-functions.git",
|
||||
"directory": "mcp/bridge"
|
||||
},
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-bridge": "index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.3",
|
||||
"express": "^4.21.2"
|
||||
}
|
||||
}
|
||||
40
llm-functions/mcp/server/README.md
Normal file
40
llm-functions/mcp/server/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# MCP-Server
|
||||
|
||||
Let LLM-functions tools/agents be used through the Model Context Protocol.
|
||||
|
||||
## Serve tools
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"tools": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"mcp-llm-functions",
|
||||
"<llm-functions-dir>"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Serve the agent
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"<agent-name>": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"mcp-llm-functions",
|
||||
"<llm-functions-dir>"
|
||||
"<agent-name>",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `AGENT_TOOLS_ONLY`: Set to `true` or `1` to ignore shared tools and display only agent tools.
|
||||
129
llm-functions/mcp/server/index.js
Executable file
129
llm-functions/mcp/server/index.js
Executable file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { spawn } from "node:child_process";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
let [rootDir, agentName] = process.argv.slice(2);
|
||||
if (!rootDir) {
|
||||
console.error("Usage: mcp-llm-functions <llm-functions-dir> [<agent-name>]");
|
||||
process.exit(1);
|
||||
}
|
||||
rootDir = path.resolve(rootDir);
|
||||
|
||||
let functionsJsonPath = path.join(rootDir, "functions.json");
|
||||
if (agentName) {
|
||||
functionsJsonPath = path.join(rootDir, "agents", agentName, "functions.json");
|
||||
}
|
||||
let functions = [];
|
||||
try {
|
||||
const data = await fs.promises.readFile(functionsJsonPath, "utf8");
|
||||
functions = JSON.parse(data);
|
||||
} catch {
|
||||
console.error(`Failed to read functions at '${functionsJsonPath}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
const agentToolsOnly = process.env["AGENT_TOOLS_ONLY"] === "true" || process.env["AGENT_TOOLS_ONLY"] === "1";
|
||||
functions = functions.filter(f => {
|
||||
if (f.mcp) {
|
||||
return false;
|
||||
}
|
||||
if (agentToolsOnly) {
|
||||
return f.agent;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const env = Object.assign({}, process.env, {
|
||||
PATH: `${path.join(rootDir, "bin")}:${process.env.PATH}`
|
||||
});
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: `llm-functions/${agentName || "common-tools"}`,
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: functions.map((f) => ({
|
||||
name: f.name,
|
||||
description: f.description,
|
||||
inputSchema: f.parameters,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const functionObj = functions.find((f) => f.name === request.params.name);
|
||||
if (!functionObj) {
|
||||
throw new Error(`Unknown tool '${request.params.name}'`);
|
||||
}
|
||||
let command = request.params.name;
|
||||
let args = [JSON.stringify(request.params.arguments || {})];
|
||||
if (agentName && functionObj.agent) {
|
||||
args.unshift(command);
|
||||
command = agentName;
|
||||
}
|
||||
const tmpFile = path.join(os.tmpdir(), `mcp-llm-functions-${process.pid}-eval-${uuid()}`);
|
||||
const { exitCode, stderr } = await runCommand(command, args, { ...env, LLM_OUTPUT: tmpFile });
|
||||
if (exitCode === 0) {
|
||||
let output = '';
|
||||
try {
|
||||
output = await fs.promises.readFile(tmpFile, "utf8");
|
||||
} catch { };
|
||||
return {
|
||||
content: [{ type: "text", text: output }],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: stderr }],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function runCommand(command, args, env) {
|
||||
return new Promise(resolve => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
env,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (exitCode) => {
|
||||
resolve({ exitCode, stderr });
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ exitCode: 1, stderr: `Command execution failed: ${err.message}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
runServer().catch(console.error);
|
||||
24
llm-functions/mcp/server/package.json
Normal file
24
llm-functions/mcp/server/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "mcp-llm-functions",
|
||||
"version": "1.2.0",
|
||||
"description": "Let LLM-functions tools/agents be used through the Model Context Protocol",
|
||||
"license": "MIT",
|
||||
"author": "sigoden <sigoden@gmail.com>",
|
||||
"homepage": "https://github.com/sigoden/llm-functions/tree/main/mcp/server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sigoden/llm-functions.git",
|
||||
"directory": "mcp/server"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-llm-functions": "index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.1.0",
|
||||
"uuid": "^11.0.3"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user