Files
llm-functions/mcp/bridge/index.js

197 lines
4.8 KiB
JavaScript

#!/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,
},
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);