Files
Void-Homelab/lib/mcp/companion-stdio.js

121 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}