160 lines
6.1 KiB
JavaScript
160 lines
6.1 KiB
JavaScript
// 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 }; } };
|
|
}
|