/** * companion-stdio.js * * Exposes the four Void companion tools through the MCP stdio protocol. * The `claude` CLI spawns this file as a subprocess and communicates * over stdin/stdout using JSON-RPC. * * Usage (via claude MCP config): * VOID_AGENT_JSON='...' VOID_SPACE_ID='...' node lib/mcp/companion-stdio.js * * Exported helpers (transport-free — safe to import in tests): * listMcpTools() → array of {name, description, input_schema} * callMcpTool(name, args, ctx) → raw handler result object */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { companionRegistry } from '../ai/agent/tools/index.js'; import { buildCtxFromEnv } from './context.js'; // --------------------------------------------------------------------------- // Transport-free helpers (exported for testing) // --------------------------------------------------------------------------- /** * Returns the list of MCP tool descriptors (no handler) from the companion registry. * @returns {{ name: string, description: string, input_schema: object }[]} */ export function listMcpTools() { return companionRegistry.listTools().map(({ name, description, input_schema }) => ({ name, description, input_schema })); } /** * Calls a tool handler directly (same code path as the MCP server), bypassing transport. * @param {string} name * @param {object} args * @param {object} ctx – tool context {agent, space_id, view, actor} * @returns {Promise} raw handler result */ export async function callMcpTool(name, args, ctx) { const tool = companionRegistry.getTool(name); if (!tool) { throw new Error(`Unknown tool: ${name}`); } return tool.handler(args, ctx); } // --------------------------------------------------------------------------- // MCP server factory (used when running as a script) // --------------------------------------------------------------------------- function createServer() { const server = new Server( { name: 'void-companion', version: '1.0.0' }, { capabilities: { tools: {} } } ); // tools/list — return all companion tools with their raw JSON schemas server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: listMcpTools().map(({ name, description, input_schema }) => ({ name, description, inputSchema: input_schema // MCP protocol field name is inputSchema })) })); // tools/call — delegate to the registry handler, wrap result as text content server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args = {} } = request.params; const ctx = buildCtxFromEnv(); try { const result = await callMcpTool(name, args, ctx); const text = JSON.stringify(result); return { content: [{ type: 'text', text }], structuredContent: result }; } catch (err) { return { content: [{ type: 'text', text: err.message ?? String(err) }], isError: true }; } }); return server; } // --------------------------------------------------------------------------- // Entry point — only connect stdio transport when run as a script // --------------------------------------------------------------------------- const isMain = typeof process !== 'undefined' && process.argv[1] && import.meta.url === new URL(process.argv[1], 'file://').href; if (isMain) { const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); }