#!/usr/bin/env node /** * OpenClaw MCP proxy server (stdio transport). * * Reads OpenClaw tool definitions from a JSON file (TOOL_DEFS_FILE env var), * exposes them to Claude Code as MCP tools, and executes them by calling back * to the bridge's /mcp/execute HTTP endpoint (BRIDGE_EXECUTE_URL env var). * * Started per-session by sdk-adapter.ts via --mcp-config. */ import fs from "node:fs"; import readline from "node:readline"; // ── Load tool definitions ───────────────────────────────────────────────────── function loadToolDefs() { const path = process.env.TOOL_DEFS_FILE; if (!path) return []; try { return JSON.parse(fs.readFileSync(path, "utf8")); } catch { return []; } } // ── MCP stdio transport (newline-delimited JSON-RPC 2.0) ────────────────────── const rl = readline.createInterface({ input: process.stdin, terminal: false }); function send(msg) { process.stdout.write(JSON.stringify(msg) + "\n"); } function sendResult(id, result) { send({ jsonrpc: "2.0", id, result }); } function sendError(id, code, message) { send({ jsonrpc: "2.0", id, error: { code, message } }); } // ── Tool execution via bridge HTTP ──────────────────────────────────────────── async function executeTool(name, args) { const url = process.env.BRIDGE_EXECUTE_URL; const apiKey = process.env.BRIDGE_API_KEY ?? ""; const workspace = process.env.WORKSPACE ?? ""; const agentId = process.env.AGENT_ID ?? ""; if (!url) return `[mcp-proxy] BRIDGE_EXECUTE_URL not configured`; try { const resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, // Include workspace and agentId so bridge can build the correct tool execution context body: JSON.stringify({ tool: name, args, workspace, agentId }), }); const data = await resp.json(); if (data.error) return `[tool error] ${data.error}`; return data.result ?? "(no result)"; } catch (err) { return `[mcp-proxy fetch error] ${String(err)}`; } } // ── Request dispatcher ──────────────────────────────────────────────────────── let toolDefs = []; async function handleRequest(msg) { const id = msg.id ?? null; const method = msg.method; const params = msg.params ?? {}; if (method === "initialize") { toolDefs = loadToolDefs(); sendResult(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "openclaw-mcp-proxy", version: "0.1.0" }, }); return; } if (method === "notifications/initialized") { return; // no response for notifications } if (method === "tools/list") { const tools = toolDefs.map((t) => { const originName = t.function.name; const baseDesc = t.function.description ?? ""; const aliasNote = `[openclaw tool: ${originName}] If any skill or instruction refers to "${originName}", this is the tool to call.`; const description = baseDesc ? `${baseDesc}\n${aliasNote}` : aliasNote; return { name: originName, description, inputSchema: t.function.parameters ?? { type: "object", properties: {} }, }; }); sendResult(id, { tools }); return; } if (method === "tools/call") { const toolName = params.name; const toolArgs = params.arguments ?? {}; try { const result = await executeTool(toolName, toolArgs); sendResult(id, { content: [{ type: "text", text: result }], isError: false, }); } catch (err) { sendResult(id, { content: [{ type: "text", text: `[mcp-proxy] ${String(err)}` }], isError: true, }); } return; } sendError(id, -32601, `Method not found: ${method}`); } // ── Main loop ───────────────────────────────────────────────────────────────── rl.on("line", async (line) => { const trimmed = line.trim(); if (!trimmed) return; let msg; try { msg = JSON.parse(trimmed); } catch { sendError(null, -32700, "Parse error"); return; } await handleRequest(msg); });