Files
2026-03-25 17:38:59 -07:00

97 lines
3.0 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Persistent MCP bridge server for code-mode benchmarks.
*
* Spawns chrome-devtools-mcp as a child process and maintains a single
* persistent MCP session. Exposes a simple HTTP API:
* POST /call { name, args } → { result }
* GET /tools → [{ name, description }]
* GET /health → "ok"
*
* Usage:
* npx tsx browser-code/bridge.ts & # start in background
* # ... agent scripts call http://127.0.0.1:9223/call
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { createServer } from "node:http";
const PORT = parseInt(process.env.BROWSER_CODE_BRIDGE_PORT ?? "9223", 10);
async function main() {
// Connect to chrome-devtools-mcp via stdio
const transport = new StdioClientTransport({
command: "npx",
args: ["-y", "chrome-devtools-mcp@latest", "--headless", "--isolated"],
});
const client = new Client({ name: "browser-code-bridge", version: "1.0.0" });
await client.connect(transport);
console.error(`[bridge] Connected to chrome-devtools-mcp`);
const server = createServer(async (req, res) => {
// CORS headers (not needed but harmless)
res.setHeader("Content-Type", "application/json");
if (req.method === "GET" && req.url === "/health") {
res.end(JSON.stringify({ status: "ok" }));
return;
}
if (req.method === "GET" && req.url === "/tools") {
try {
const result = await client.listTools();
const tools = result.tools.map((t) => ({
name: t.name,
description: t.description,
}));
res.end(JSON.stringify(tools));
} catch (err: any) {
res.statusCode = 500;
res.end(JSON.stringify({ error: err.message }));
}
return;
}
if (req.method === "POST" && req.url === "/call") {
let body = "";
for await (const chunk of req) body += chunk;
try {
const { name, args } = JSON.parse(body);
const result = await client.callTool({ name, arguments: args ?? {} });
const parts: string[] = [];
for (const block of result.content as Array<{ type: string; text?: string }>) {
if (block.type === "text" && block.text) parts.push(block.text);
}
res.end(JSON.stringify({ result: parts.join("\n") }));
} catch (err: any) {
res.statusCode = 500;
res.end(JSON.stringify({ error: err.message }));
}
return;
}
res.statusCode = 404;
res.end(JSON.stringify({ error: "not found" }));
});
server.listen(PORT, "127.0.0.1", () => {
console.error(`[bridge] Listening on http://127.0.0.1:${PORT}`);
// Signal readiness to parent
console.log("READY");
});
// Graceful shutdown
process.on("SIGTERM", async () => {
server.close();
await client.close();
await transport.close();
process.exit(0);
});
}
main().catch((err) => {
console.error(`[bridge] Fatal: ${err}`);
process.exit(1);
});