import { Socket, createServer } from 'node:net'; import { startEvidenceContextServer } from './evidenceContextServer'; type JsonRpcId = string | number | null; type JsonRpcRequest = { jsonrpc?: unknown; id?: unknown; method?: unknown; params?: unknown; }; type JsonRpcResponse = { jsonrpc: '2.0'; id: JsonRpcId; result?: unknown; error?: { code: number; message: string; }; }; const MCP_PROTOCOL_VERSION = '2024-11-05'; const toJsonRpcId = (value: unknown): JsonRpcId => { if (typeof value === 'string' || typeof value === 'number' || value === null) { return value; } return null; }; const sendMessage = (message: JsonRpcResponse): void => { process.stdout.write(`${JSON.stringify(message)}\n`); }; const sendResult = (id: JsonRpcId, result: unknown): void => { sendMessage({ jsonrpc: '2.0', id, result, }); }; const sendError = (id: JsonRpcId, code: number, message: string): void => { sendMessage({ jsonrpc: '2.0', id, error: { code, message, }, }); }; const isPortInUse = async (host: string, port: number): Promise => await new Promise((resolve) => { const socket = new Socket(); socket.setTimeout(600); socket.once('connect', () => { socket.destroy(); resolve(true); }); socket.once('timeout', () => { socket.destroy(); resolve(false); }); socket.once('error', () => { socket.destroy(); resolve(false); }); socket.connect(port, host); }); const findEphemeralPort = async (host: string): Promise => await new Promise((resolve, reject) => { const probe = createServer(); probe.once('error', reject); probe.listen(0, host, () => { const address = probe.address(); const port = address && typeof address === 'object' ? address.port : 0; probe.close(() => resolve(port)); }); }); const fetchJson = async (url: string): Promise => { const response = await fetch(url); const text = await response.text(); if (text.trim().length === 0) { return {}; } return JSON.parse(text) as unknown; }; const startOrReuseEvidenceHttp = async (): Promise<{ host: string; port: number; route: string; }> => { const host = process.env.PUMUKI_EVIDENCE_HOST ?? '127.0.0.1'; const route = process.env.PUMUKI_EVIDENCE_ROUTE ?? '/ai-evidence'; const parsedPort = Number.parseInt(process.env.PUMUKI_EVIDENCE_PORT ?? '', 10); const preferredPort = Number.isFinite(parsedPort) ? parsedPort : 7341; const requestedPort = preferredPort > 0 ? preferredPort : await findEphemeralPort(host); const healthUrl = `http://${host}:${requestedPort}/health`; try { const health = (await fetchJson(healthUrl)) as { status?: string }; if (health.status === 'ok') { return { host, port: requestedPort, route }; } } catch (error) { void error; } const portInUse = await isPortInUse(host, requestedPort); const resolvedPort = portInUse ? await findEphemeralPort(host) : requestedPort; startEvidenceContextServer({ host, port: resolvedPort, route, repoRoot: process.cwd(), }); return { host, port: resolvedPort, route, }; }; const run = async (): Promise => { const started = await startOrReuseEvidenceHttp(); const baseUrl = `http://${started.host}:${started.port}`; const route = started.route.startsWith('/') ? started.route : `/${started.route}`; let textBuffer = ''; const resourcesCatalog = [ { uri: 'evidence://status', path: '/status', description: 'Evidence status payload', }, { uri: 'evidence://summary', path: `${route}/summary`, description: 'Evidence summary payload', }, { uri: 'evidence://snapshot', path: `${route}/snapshot`, description: 'Evidence snapshot payload', }, { uri: 'evidence://findings', path: `${route}/findings`, description: 'Evidence findings payload', }, ] as const; const toolsCatalog = [ { name: 'evidence_status', path: '/status', description: 'Read evidence status payload.', }, { name: 'evidence_summary', path: `${route}/summary`, description: 'Read evidence summary payload.', }, { name: 'evidence_snapshot', path: `${route}/snapshot`, description: 'Read evidence snapshot payload.', }, { name: 'evidence_findings', path: `${route}/findings`, description: 'Read evidence findings payload.', }, { name: 'evidence_rulesets', path: `${route}/rulesets`, description: 'Read evidence rulesets payload.', }, { name: 'evidence_platforms', path: `${route}/platforms`, description: 'Read evidence platforms payload.', }, ] as const; const handleRequest = async (request: JsonRpcRequest): Promise => { if (request.jsonrpc !== '2.0') { sendError(toJsonRpcId(request.id), -32600, 'Invalid JSON-RPC version.'); return; } const id = toJsonRpcId(request.id); const method = typeof request.method === 'string' ? request.method : ''; if (method === 'notifications/initialized') { return; } if (method === 'initialize') { sendResult(id, { protocolVersion: MCP_PROTOCOL_VERSION, serverInfo: { name: 'pumuki-evidence-stdio', version: '1.0.0', }, capabilities: { tools: { listChanged: true, }, resources: { listChanged: true, }, }, }); return; } if (method === 'ping') { sendResult(id, {}); return; } if (method === 'tools/list') { sendResult(id, { tools: toolsCatalog.map((entry) => ({ name: entry.name, description: entry.description, inputSchema: { type: 'object', properties: {}, additionalProperties: true, }, })), }); return; } if (method === 'tools/call') { const params = typeof request.params === 'object' && request.params !== null ? (request.params as { name?: unknown; arguments?: unknown }) : {}; const name = typeof params.name === 'string' ? params.name : ''; const tool = toolsCatalog.find((entry) => entry.name === name); if (!tool) { sendError(id, -32602, `Unknown tool: ${name}`); return; } const payload = await fetchJson(`${baseUrl}${tool.path}`); sendResult(id, { content: [ { type: 'text', text: JSON.stringify(payload), }, ], isError: false, }); return; } if (method === 'resources/list') { sendResult(id, { resources: resourcesCatalog.map((entry) => ({ uri: entry.uri, name: entry.uri, description: entry.description, mimeType: 'application/json', })), }); return; } if (method === 'resources/read') { const params = typeof request.params === 'object' && request.params !== null ? (request.params as { uri?: unknown }) : {}; const uri = typeof params.uri === 'string' ? params.uri : ''; const resource = resourcesCatalog.find((entry) => entry.uri === uri); if (!resource) { sendError(id, -32602, `Unknown resource URI: ${uri}`); return; } const payload = await fetchJson(`${baseUrl}${resource.path}`); sendResult(id, { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(payload), }, ], }); return; } sendError(id, -32601, `Method not found: ${method}`); }; const processBuffer = (): void => { while (true) { const lineEnd = textBuffer.indexOf('\n'); if (lineEnd === -1) { return; } const rawLine = textBuffer.slice(0, lineEnd).trim(); textBuffer = textBuffer.slice(lineEnd + 1); if (rawLine.length === 0) { continue; } let payload: JsonRpcRequest; try { payload = JSON.parse(rawLine) as JsonRpcRequest; } catch { sendError(null, -32700, 'Parse error'); continue; } void handleRequest(payload).catch((error) => { const id = toJsonRpcId(payload.id); const message = error instanceof Error ? error.message : 'Internal error'; sendError(id, -32603, message); }); } }; process.stdin.on('data', (chunk) => { textBuffer += chunk.toString('utf8'); processBuffer(); }); }; void run().catch((error) => { const message = error instanceof Error ? error.message : 'Unknown MCP stdio bridge error'; process.stderr.write(`[pumuki-mcp-evidence-stdio] ${message}\n`); process.exit(1); });