Files
gnome-jarvis/vendor/agent-harness/agent/mcp.js
T
2026-09-11 13:41:22 -04:00

154 lines
4.4 KiB
JavaScript

/**
* 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 };