aboutsummaryrefslogtreecommitdiffstats
path: root/mcp/bridge
diff options
context:
space:
mode:
authorsigoden <sigoden@gmail.com>2024-12-11 20:46:17 +0800
committerGitHub <noreply@github.com>2024-12-11 20:46:17 +0800
commit20d1ec47f9970caa119c3715a1c0c7a69e5aa65f (patch)
tree76b0d3585a40ce2b269fa50b54786aa865641920 /mcp/bridge
parentc58abcbaf89f27e5e3806f4309880a1eac2b7095 (diff)
downloadllm-functions-docker-20d1ec47f9970caa119c3715a1c0c7a69e5aa65f.tar.gz
feat: support MCP bridge (#140)
Diffstat (limited to 'mcp/bridge')
-rw-r--r--mcp/bridge/README.md42
-rw-r--r--mcp/bridge/index.js195
-rw-r--r--mcp/bridge/package.json22
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"
+ }
+}