121 lines
4.2 KiB
JavaScript
121 lines
4.2 KiB
JavaScript
/**
|
||
* 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 { securityRegistry } from '../ai/agent/tools/security/index.js';
|
||
import { blueRegistry } from '../ai/agent/tools/blue/index.js';
|
||
import { buildCtxFromEnv } from './context.js';
|
||
|
||
// Which toolset this stdio server exposes, selected at launch via
|
||
// VOID_TOOL_REGISTRY (default: Dross's companion tools; 'security' = Yerin's;
|
||
// 'blue' = Little Blue's action tools).
|
||
const REGISTRIES = { companion: companionRegistry, security: securityRegistry, blue: blueRegistry };
|
||
function resolveRegistry(env = process.env) {
|
||
return REGISTRIES[env.VOID_TOOL_REGISTRY] || companionRegistry;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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(env = process.env) {
|
||
return resolveRegistry(env).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, env = process.env) {
|
||
const tool = resolveRegistry(env).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);
|
||
}
|