diff options
Diffstat (limited to 'mcp')
| -rw-r--r-- | mcp/bridge/README.md | 42 | ||||
| -rw-r--r-- | mcp/bridge/index.js | 195 | ||||
| -rw-r--r-- | mcp/bridge/package.json | 22 |
3 files changed, 259 insertions, 0 deletions
diff --git a/mcp/bridge/README.md b/mcp/bridge/README.md new file mode 100644 index 0000000..bffa30c --- /dev/null +++ b/mcp/bridge/README.md @@ -0,0 +1,42 @@ +# MCP-Bridge + +Let MCP tools be used by LLM functions. + +## Get Started + +1. Create a `mpc.json` at `<llm-functions-dir>`. + +```json +{ + "mcpServers": { + "sqlite": { + "command": "uvx", + "args": [ + "mcp-server-sqlite", + "--db-path", + "/tmp/foo.db" + ] + }, + "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. The tool identifier will be `server_toolname` to avoid clashes. + +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
\ No newline at end of file diff --git a/mcp/bridge/index.js b/mcp/bridge/index.js new file mode 100644 index 0000000..c978737 --- /dev/null +++ b/mcp/bridge/index.js @@ -0,0 +1,195 @@ +#!/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 transport = new StdioClientTransport({ + ...serverConfig, + }); + 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: `${normalizeToolName(`${id}_${name}`)}`, + description, + parameters: inputSchema, + }, + 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 normalizeToolName(name) { + return name.toLowerCase().replace(/-/g, "_"); +} + +runBridge() + .then(stop => { + if (stop) { + process.on('SIGINT', stop); + process.on('SIGTERM', stop); + } + }) + .catch(console.error);
\ No newline at end of file diff --git a/mcp/bridge/package.json b/mcp/bridge/package.json new file mode 100644 index 0000000..0e78df9 --- /dev/null +++ b/mcp/bridge/package.json @@ -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" + } +} |
