Files
AtreidesAssetsFree/server/mcp/protocol.mjs
T

159 lines
6.1 KiB
JavaScript
Raw Permalink Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:6e59cd9bc5c34f22b316c129925de56d6e800bca08debd1308bf90cb9f449baa
import { randomUUID } from 'node:crypto';
import { CONFIG } from '../../shared/config.js';
import { AI_REFUSALS } from '../ai/gate.js';
import { limitText } from '../ai/limits.js';
import { ENUMS } from '../../shared/config.js';
const PARSE_ERROR = -32700;
const INVALID_REQUEST = -32600;
const METHOD_NOT_FOUND = -32601;
const INVALID_PARAMS = -32602;
const fout = (id, code, message) => ({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
const goed = (id, result) => ({ jsonrpc: '2.0', id, result });
export function createMcpServer({ tools, logCall, onLogError } = {}) {
if (typeof logCall !== 'function') {
throw new Error('An MCP server without an audit log is not allowed in this project.');
}
if (!Array.isArray(tools) || tools.length === 0) {
throw new Error('An MCP server without tools is meaningless.');
}
const zonderScope = tools.filter(t => !ENUMS.AI_SCOPE.includes(t?.scope));
if (zonderScope.length) {
const namen = zonderScope.map(t => t?.name ?? '(naamloos)').join(', ');
throw new Error(`Tool without a valid scope: ${namen}. Allowed is ${ENUMS.AI_SCOPE.join(', ')}.`);
}
const perNaam = new Map(tools.map(t => [t.name, t]));
const sessie = { id: randomUUID(), client: null, initialized: false };
async function log(regel, messages) {
try {
await logCall({
sessionId: sessie.id, client: sessie.client,
serverVersion: CONFIG.AI.SERVER_VERSION, ...regel,
...(messages ? { messages } : {}),
});
} catch (e) {
if (onLogError) onLogError(e);
else throw e;
}
}
const gesprek = (binnenAt, args, uit) => [
{ kind: 'request', text: JSON.stringify(args ?? {}), occurredAt: binnenAt },
{ kind: 'response', text: String(uit ?? ''), occurredAt: new Date().toISOString() },
];
async function voerUit(naam, args, droog) {
const binnenAt = new Date().toISOString();
const tool = perNaam.get(naam);
if (!tool) {
const regel = { tool: naam, arguments: args, outcome: 'refused', detail: 'Deze tool bestaat niet.' };
if (!droog) await log(regel, gesprek(binnenAt, args, regel.detail));
return { regel, text: regel.detail, onbekend: true };
}
try {
const { text, fieldsShared } = await tool.run(args, { session: sessie });
const { text: uitvoer, capped } = limitText(String(text ?? ''));
const regel = {
tool: naam, arguments: args, fieldsShared: fieldsShared ?? [],
charactersOut: uitvoer.length, outcome: 'ok',
detail: capped ? 'Antwoord liep tegen de tekengrens en is ingekort.' : undefined,
};
if (!droog) await log(regel, gesprek(binnenAt, args, uitvoer));
return { regel, text: uitvoer };
} catch (e) {
const melding = e && e.message ? e.message : String(e);
const soort = e && AI_REFUSALS.includes(e.code) ? 'refused' : 'error';
const regel = { tool: naam, arguments: args, outcome: soort, detail: melding };
if (!droog) await log(regel, gesprek(binnenAt, args, melding));
return { regel, text: melding, mislukt: true };
}
}
async function toolsCall(id, params) {
const naam = params && typeof params.name === 'string' ? params.name : null;
const args = params && params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
if (!naam) {
await log({ tool: '(geen)', outcome: 'refused', detail: 'Verzoek zonder toolnaam.' });
return fout(id, INVALID_PARAMS, 'Een tools/call-verzoek noemt geen tool.');
}
const r = await voerUit(naam, args, false);
if (r.onbekend) return fout(id, INVALID_PARAMS, `Onbekende tool '${naam}'.`);
if (r.mislukt) return goed(id, { content: [{ type: 'text', text: r.text }], isError: true });
return goed(id, { content: [{ type: 'text', text: r.text }] });
}
async function dryRun(naam, args = {}) {
const veiligeArgs = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
const r = await voerUit(String(naam ?? ''), veiligeArgs, true);
return {
...r.regel,
fieldsShared: r.regel.fieldsShared ?? [],
charactersOut: r.regel.charactersOut ?? null,
detail: r.regel.detail ?? null,
text: r.text,
};
}
async function handle(bericht) {
if (!bericht || typeof bericht !== 'object' || Array.isArray(bericht)) {
return fout(null, INVALID_REQUEST, 'Geen JSON-RPC-object.');
}
const { id, method, params } = bericht;
const isNotificatie = id === undefined || id === null;
if (typeof method !== 'string') {
return isNotificatie ? null : fout(id, INVALID_REQUEST, 'Verzoek zonder methode.');
}
switch (method) {
case 'initialize': {
sessie.client = params?.clientInfo?.name ? String(params.clientInfo.name) : null;
sessie.initialized = true;
return goed(id, {
protocolVersion: typeof params?.protocolVersion === 'string'
? params.protocolVersion : CONFIG.AI.PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: CONFIG.AI.SERVER_NAME, version: CONFIG.AI.SERVER_VERSION },
});
}
case 'notifications/initialized':
case 'initialized':
return null;
case 'ping':
return isNotificatie ? null : goed(id, {});
case 'tools/list': {
if (isNotificatie) return null;
return goed(id, {
tools: tools.map(t => ({
name: t.name,
title: t.title,
description: t.description,
inputSchema: t.inputSchema,
})),
});
}
case 'tools/call':
if (isNotificatie) return null;
return toolsCall(id, params);
default:
return isNotificatie ? null : fout(id, METHOD_NOT_FOUND, `Onbekende methode '${method}'.`);
}
}
async function handleRegel(regel) {
let bericht;
try { bericht = JSON.parse(regel); } catch { return fout(null, PARSE_ERROR, 'Ongeldige JSON.'); }
return handle(bericht);
}
return { handle, handleRegel, dryRun, get session() { return { ...sessie }; } };
}