Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ __pycache__
node_modules
/package.json
package-lock.json
*.lock
*.lock
/mcp.json
23 changes: 19 additions & 4 deletions Argcfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -e

BIN_DIR=bin
TMP_DIR="cache/tmp"
TMP_DIR="cache/__tmp__"
VENV_DIR=".venv"

LANG_CMDS=( \
Expand Down Expand Up @@ -421,9 +421,8 @@ test-demo@tool() {
# @cmd Test agents
# @alias agent:test
test@agent() {
tmp_dir="cache/tmp"
mkdir -p "$tmp_dir"
names_file="$tmp_dir/agents.txt"
mkdir -p "$TMP_DIR"
names_file="$TMP_DIR/agents.txt"
argc list@agent > "$names_file"
argc build@agent --names-file "$names_file"
test-demo@agent
Expand Down Expand Up @@ -499,6 +498,12 @@ install() {
fi
}

# @cmd Run mcp command
# @arg args~[?`_choice_mcp_args`] The mcp command and arguments
mcp() {
bash ./scripts/mcp.sh "$@"
}

# @cmd Create a boilplate tool script
# @alias tool:create
# @arg args~
Expand Down Expand Up @@ -671,6 +676,16 @@ _choice_agent_action() {
argc generate-declarations@agent "$1" --oneline | sed "$expr"
}

_choice_mcp_args() {
if [[ "$ARGC_COMPGEN" -eq 1 ]]; then
args=( "${argc__positionals[@]}" )
args[-1]="$ARGC_LAST_ARG"
argc --argc-compgen generic scripts/mcp.sh mcp "${args[@]}"
else
:;
fi
}

_die() {
echo "$*" >&2
exit 1
Expand Down
42 changes: 42 additions & 0 deletions mcp/bridge/README.md
Original file line number Diff line number Diff line change
@@ -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
195 changes: 195 additions & 0 deletions mcp/bridge/index.js
Original file line number Diff line number Diff line change
@@ -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);
22 changes: 22 additions & 0 deletions mcp/bridge/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading
Loading