/** * Standalone MCP server that exposes OpenClaw plugin-registered tools * (e.g. memory-lancedb's memory_recall, memory_store, memory_forget) * so ACP sessions running Claude Code can use them. * * Run via: node --import tsx src/mcp/plugin-tools-serve.ts * Or: bun src/mcp/plugin-tools-serve.ts */ import { pathToFileURL } from "node:url"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; import type { OpenClawConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; import { formatErrorMessage } from "../infra/errors.js"; import { routeLogsToStderr } from "../logging/console.js"; import { resolvePluginTools } from "../plugins/tools.js"; import { VERSION } from "../version.js"; function resolveJsonSchemaForTool(tool: AnyAgentTool): Record { const params = tool.parameters; if (params && typeof params === "object" && "type" in params) { return params as Record; } // Fallback: accept any object return { type: "object", properties: {} }; } function resolveTools(config: OpenClawConfig): AnyAgentTool[] { return resolvePluginTools({ context: { config }, suppressNameConflicts: true, }); } export function createPluginToolsMcpServer( params: { config?: OpenClawConfig; tools?: AnyAgentTool[]; } = {}, ): Server { const cfg = params.config ?? loadConfig(); const tools = params.tools ?? resolveTools(cfg); const toolMap = new Map(); for (const tool of tools) { toolMap.set(tool.name, tool); } const server = new Server( { name: "openclaw-plugin-tools", version: VERSION }, { capabilities: { tools: {} } }, ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: tools.map((tool) => ({ name: tool.name, description: tool.description ?? "", inputSchema: resolveJsonSchemaForTool(tool), })), })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const tool = toolMap.get(request.params.name); if (!tool) { return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }], isError: true, }; } try { const result = await tool.execute(`mcp-${Date.now()}`, request.params.arguments ?? {}); return { content: Array.isArray(result.content) ? result.content : [{ type: "text", text: String(result.content) }], }; } catch (err) { return { content: [{ type: "text", text: `Tool error: ${formatErrorMessage(err)}` }], isError: true, }; } }); return server; } export async function servePluginToolsMcp(): Promise { // MCP stdio requires stdout to stay protocol-only. routeLogsToStderr(); const config = loadConfig(); const tools = resolveTools(config); const server = createPluginToolsMcpServer({ config, tools }); if (tools.length === 0) { process.stderr.write("plugin-tools-serve: no plugin tools found\n"); } const transport = new StdioServerTransport(); let shuttingDown = false; const shutdown = () => { if (shuttingDown) { return; } shuttingDown = true; process.stdin.off("end", shutdown); process.stdin.off("close", shutdown); process.off("SIGINT", shutdown); process.off("SIGTERM", shutdown); void server.close(); }; process.stdin.once("end", shutdown); process.stdin.once("close", shutdown); process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); await server.connect(transport); } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { servePluginToolsMcp().catch((err) => { process.stderr.write(`plugin-tools-serve: ${formatErrorMessage(err)}\n`); process.exit(1); }); }