Files
Void-Homelab/lib/mcp/companion-stdio.js
root a3eb5a58f0 feat(security): seed Yerin agent + registry-selectable MCP server
- migration 011_yerin.sql: seed read-only 'yerin' agent ({read:true}, kind claude,
  model NULL = server default; switch to local Ollama via agents.model anytime)
- companion-stdio.js: select the toolset from VOID_TOOL_REGISTRY ('security' →
  Yerin's securityRegistry; default → Dross's companionRegistry)
- tests/mcp/registry_select.test.js

Remaining for Yerin (left for review): an entry point (route or cron) + persona
prompt — see docs/yerin-security-agent.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 00:17:53 +10:00

119 lines
4.1 KiB
JavaScript
Raw 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 { 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).
const REGISTRIES = { companion: companionRegistry, security: securityRegistry };
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);
}