/** * MCP tool registry: HTTP JSON-RPC (initialize + tools/list + tools/call). * Stdio is trusted-origin only and is not spawned in this wave. */ const rpc = require('./mcp-rpc.js'); const truncate = require('./truncate.js'); const servers = new Map(); let rpcId = 1; function nextId() { rpcId += 1; return rpcId; } function register(spec, opts) { if (!spec || !spec.id) throw new Error('mcp id required'); const transport = spec.transport || 'http'; if (transport === 'stdio') { const approved = opts && (opts.alwaysApprove || opts._alwaysApprove); if (!approved) throw new Error('stdio MCP requires a trusted origin (always-approve)'); throw new Error('stdio MCP is opt-in and not connected until explicitly implemented'); } if (transport === 'http' && spec.url) { require('../lib/net.js').assertPublicHttpUrl(spec.url); } const rec = { id: spec.id, name: spec.name || spec.id, transport, url: spec.url || null, command: spec.command || null, tools: Array.isArray(spec.tools) ? spec.tools : [], handshakeError: null, handshakeReported: false, ready: null, }; servers.set(spec.id, rec); if (transport === 'http' && spec.url) { rec.ready = handshake(rec).catch((err) => { rec.handshakeError = err && err.message ? err.message : String(err); }); return rec.ready.then(() => list()); } return Promise.resolve(list()); } async function handshake(rec) { await jsonRpc(rec.url, 'initialize', { protocolVersion: '2024-11-05', capabilities: { tools: {} }, clientInfo: { name: 'agent-harness', version: '0.1.0' }, }); try { await jsonRpc(rec.url, 'notifications/initialized', {}); } catch (_) {} const listed = await jsonRpc(rec.url, 'tools/list', {}); rec.tools = rpc.normalizeTools(listed); rec.handshakeError = null; return rec.tools; } async function jsonRpc(url, method, params) { const net = require('../lib/net.js'); net.assertPublicHttpUrl(url); const payload = { jsonrpc: '2.0', id: nextId(), method, params: params || {} }; const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', }, body: JSON.stringify(payload), }); const text = await res.text(); const body = rpc.extractJson(text); return rpc.unwrapResult(body); } function unregister(id) { servers.delete(id); return list(); } function list() { return Array.from(servers.values()).map((s) => ({ id: s.id, name: s.name, transport: s.transport, toolCount: (s.tools || []).length, tools: (s.tools || []).map((t) => t.name || t), handshakeError: s.handshakeError || null, })); } function search(query) { const q = String(query || '').toLowerCase(); const hits = []; for (const s of servers.values()) { for (const t of s.tools || []) { const name = typeof t === 'string' ? t : t.name; const desc = typeof t === 'object' ? t.description || '' : ''; const wire = s.id + '__' + name; if (!q || wire.toLowerCase().includes(q) || desc.toLowerCase().includes(q)) { hits.push({ name: wire, server: s.id, description: desc }); } } } return hits; } async function call(wireName, args) { const idx = String(wireName || '').indexOf('__'); if (idx < 0) throw new Error('expected server__tool name'); const serverId = wireName.slice(0, idx); const tool = wireName.slice(idx + 2); const s = servers.get(serverId); if (!s) throw new Error('unknown MCP server: ' + serverId); if (s.ready) { try { await s.ready; } catch (_) {} } if (s.handshakeError) throw new Error('MCP handshake failed: ' + s.handshakeError); if (s.transport === 'http' && s.url) { const result = await jsonRpc(s.url, 'tools/call', { name: tool, arguments: args || {} }); return truncate.truncateWithMarker(typeof result === 'string' ? result : JSON.stringify(result), 12000); } throw new Error('MCP transport not connected: ' + s.transport); } function handshakeReminders() { const out = []; for (const s of servers.values()) { if (s.handshakeError && !s.handshakeReported) { s.handshakeReported = true; out.push('MCP ' + s.id + ' handshake failed: ' + s.handshakeError); } } return out; } function count() { return servers.size; } function reset() { servers.clear(); rpcId = 1; } module.exports = { register, unregister, list, search, call, count, handshakeReminders, reset };