feat(mcp): stdio MCP server exposing the four companion tools
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
110
lib/mcp/companion-stdio.js
Normal file
110
lib/mcp/companion-stdio.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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<object>} 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);
|
||||
}
|
||||
Reference in New Issue
Block a user