// Generated, do not edit. sha256:c1f8a76bc1578f782076e85f682098b660b8aa2852380e51aa99da1608e59043 import { describe, it, beforeEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createDbHarness } from '../helpers/db-node.mjs'; import { CONFIG, ENUMS } from '../../shared/config.js'; process.env.ASSETAPP_DATA_DIR = await mkdtemp(join(tmpdir(), 'assetapp-mcp-')); const harness = createDbHarness({ seedPreferences: true }); mock.module('../../server/db.js', { namedExports: harness.exports }); harness.recreate(); const { createMcpServer } = await import('../../server/mcp/protocol.mjs'); const { TOOLS, allTools } = await import('../../server/mcp/tools.mjs'); await import('../../server/edition.mjs'); let logboek; const maak = (tools = TOOLS, extra = {}) => { logboek = []; return createMcpServer({ tools, logCall: async (r) => { logboek.push(r); }, ...extra }); }; const verzoek = (method, params, id = 1) => ({ jsonrpc: '2.0', id, method, params }); beforeEach(() => { logboek = []; }); describe('mcp: elke tool declareert een scope (fase 1d)', () => { const kaal = { name: 'naamloos', title: 'x', description: 'x', inputSchema: {}, run: async () => ({ text: 'x' }) }; it('weigert te bestaan met een tool zonder scope', () => { assert.throws(() => createMcpServer({ tools: [kaal], logCall: async () => {} }), /scope/i); }); it('weigert een verzonnen scope', () => { assert.throws( () => createMcpServer({ tools: [{ ...kaal, scope: 'admin' }], logCall: async () => {} }), /scope/i); }); it('noemt de tool bij naam, zodat de fout te vinden is', () => { assert.throws( () => createMcpServer({ tools: [{ ...kaal, name: 'mijn_tool' }], logCall: async () => {} }), /mijn_tool/); }); it('accepteert elke scope uit de SSOT', () => { for (const scope of ENUMS.AI_SCOPE) { assert.doesNotThrow(() => createMcpServer({ tools: [{ ...kaal, scope }], logCall: async () => {} }), `${scope} wordt geweigerd terwijl hij in de enum staat`); } }); it('en de echte tool-lijst voldoet eraan', () => { assert.doesNotThrow(() => createMcpServer({ tools: TOOLS, logCall: async () => {} })); for (const tool of TOOLS) assert.ok(ENUMS.AI_SCOPE.includes(tool.scope), `${tool.name} mist een scope`); }); }); describe('mcp: het vangnet op de lengte', () => { const bulk = (n) => [{ ...TOOLS.find(t => t.name === 'connection_test'), run: async () => ({ text: 'x'.repeat(n), fieldsShared: [] }), }]; it('kapt een te lang antwoord af vóór het de deur uit gaat', async () => { const server = maak(bulk(CONFIG.AI.LIMITS.MAX_TEXT_CHARS * 3)); const a = await server.handle(verzoek('tools/call', { name: 'connection_test' })); assert.ok(a.result.content[0].text.length <= CONFIG.AI.LIMITS.MAX_TEXT_CHARS); assert.match(a.result.content[0].text, /Afgekapt/); }); it('logt de INGEKORTE lengte, want dat is wat er verstuurd is', async () => { const server = maak(bulk(CONFIG.AI.LIMITS.MAX_TEXT_CHARS * 3)); await server.handle(verzoek('tools/call', { name: 'connection_test' })); assert.ok(logboek[0].charactersOut <= CONFIG.AI.LIMITS.MAX_TEXT_CHARS); assert.match(logboek[0].detail, /ingekort/i); }); it('laat een normaal antwoord met rust, en zet er geen bijzonderheid bij', async () => { const server = maak(); await server.handle(verzoek('tools/call', { name: 'connection_test', arguments: { note: 'hoi' } })); assert.equal(logboek[0].outcome, 'ok'); assert.equal(logboek[0].detail ?? null, null, 'een niet-ingekort antwoord hoort geen detail te krijgen'); }); }); describe('mcp: een geweigerde aanroep is iets anders dan een mislukte', () => { it('logt een weigering van de poort als refused, niet als error', async () => { const stuk = [{ ...TOOLS.find(t => t.name === 'connection_test'), run: async () => { const e = new Error('De AI-koppeling staat uit.'); e.code = 'ai_disabled'; throw e; }, }]; const server = maak(stuk); const a = await server.handle(verzoek('tools/call', { name: 'connection_test' })); assert.equal(a.error, undefined, 'geen protocolfout: het model hoort de reden te lezen'); assert.equal(a.result.isError, true); assert.equal(logboek[0].outcome, 'refused'); }); it('logt een gewone tool-fout nog steeds als error', async () => { const stuk = [{ ...TOOLS.find(t => t.name === 'connection_test'), run: async () => { throw new Error('de bron is onbereikbaar'); }, }]; await maak(stuk).handle(verzoek('tools/call', { name: 'connection_test' })); assert.equal(logboek[0].outcome, 'error'); }); }); describe('mcp: de server kan niet zonder auditlog bestaan', () => { it('weigert aangemaakt te worden zonder logCall', () => { assert.throws(() => createMcpServer({ tools: TOOLS }), /without an audit log/); assert.throws(() => createMcpServer({ tools: TOOLS, logCall: 'niet-een-functie' }), /without an audit log/); }); it('weigert aangemaakt te worden zonder tools', () => { assert.throws(() => createMcpServer({ tools: [], logCall: async () => {} }), /without tools/); }); }); describe('mcp: initialize', () => { it('echoot de protocolversie die de client vraagt', async () => { const server = maak(); const a = await server.handle(verzoek('initialize', { protocolVersion: '2099-01-01' })); assert.equal(a.result.protocolVersion, '2099-01-01'); }); it('valt terug op de SSOT-versie als de client er geen noemt', async () => { const server = maak(); const a = await server.handle(verzoek('initialize', {})); assert.equal(a.result.protocolVersion, CONFIG.AI.PROTOCOL_VERSION); }); it('belooft alleen tools, en niets wat wij niet waar kunnen maken', async () => { const server = maak(); const a = await server.handle(verzoek('initialize', {})); assert.deepEqual(Object.keys(a.result.capabilities), ['tools']); assert.equal(a.result.serverInfo.name, CONFIG.AI.SERVER_NAME); assert.equal(a.result.serverInfo.version, CONFIG.AI.SERVER_VERSION); assert.notEqual(a.result.serverInfo.version, String(CONFIG.SCHEMA_VERSION)); }); it('onthoudt welke client er aan de andere kant zit', async () => { const server = maak(); await server.handle(verzoek('initialize', { clientInfo: { name: 'voorbeeld-client' } })); assert.equal(server.session.client, 'voorbeeld-client'); await server.handle(verzoek('tools/call', { name: 'connection_test' }, 2)); assert.equal(logboek[0].client, 'voorbeeld-client', 'en zet hem in het logboek'); }); it('antwoordt op id 0, want dat is een geldige id en geen notificatie', async () => { const server = maak(); const a = await server.handle(verzoek('initialize', { protocolVersion: '2025-06-18' }, 0)); assert.ok(a, 'er hoort een antwoord te komen'); assert.equal(a.id, 0, 'en met dezelfde id, niet met null'); const b = await server.handle(verzoek('tools/call', { name: 'connection_test' }, 0)); assert.equal(b.id, 0); }); it('overleeft een initialize zonder clientInfo', async () => { const server = maak(); const a = await server.handle(verzoek('initialize', { protocolVersion: '2025-06-18' })); assert.ok(a.result); assert.equal(server.session.client, null); }); }); describe('mcp: notificaties krijgen geen antwoord', () => { it('antwoordt niet op notifications/initialized', async () => { const server = maak(); assert.equal(await server.handle({ jsonrpc: '2.0', method: 'notifications/initialized' }), null); }); it('antwoordt niet op een onbekende notificatie', async () => { const server = maak(); assert.equal(await server.handle({ jsonrpc: '2.0', method: 'iets/onbekends' }), null); }); }); describe('mcp: tools/list', () => { it('geeft naam, titel, beschrijving en schema van elke tool', async () => { const server = maak(); const a = await server.handle(verzoek('tools/list')); assert.equal(a.result.tools.length, TOOLS.length); for (const t of a.result.tools) { assert.ok(t.name && t.description && t.inputSchema, `${t.name} is niet volledig`); assert.equal(t.run, undefined, 'de implementatie hoort niet over de lijn te gaan'); } }); it('houdt het oppervlak klein', () => { assert.equal(TOOLS.length, 18,'het oppervlak groeit alleen met een concrete use-case'); }); it('blijft binnen het tekenbudget, en dát is de maat die §6a noemt', () => { const tekens = allTools().reduce((n, t) => n + JSON.stringify({ name: t.name, title: t.title, description: t.description, inputSchema: t.inputSchema, }).length, 0); assert.ok(tekens <= 16000, `het tool-oppervlak kost ${tekens} tekens per verzoek; de grens is 16000`); }); }); describe('mcp: het gesprek per aanroep', () => { const gesprekVan = (regel) => regel.messages ?? []; it('legt twee momenten vast: wat binnenkwam en wat terugging', async () => { const server = maak(); const a = await server.handle(verzoek('tools/call', { name: 'connection_test', arguments: { note: 'hallo' } })); const m = gesprekVan(logboek[0]); assert.equal(m.length, 2); assert.deepEqual(m.map(x => x.kind), ['request', 'response']); assert.equal(m[1].text, a.result.content[0].text, 'het bewaarde antwoord is letterlijk wat de client kreeg'); assert.match(m[0].text, /hallo/, 'en de binnenkomst draagt de argumenten zoals ze binnenkwamen'); }); it('pakt de binnenkomst vóórdat de tool draait, en niet erna', async () => { const traag = [{ ...TOOLS.find(t => t.name === 'connection_test'), run: async () => { await new Promise(r => setTimeout(r, 15)); return { text: 'x', fieldsShared: [] }; }, }]; const server = maak(traag); await server.handle(verzoek('tools/call', { name: 'connection_test' })); const [binnen, uit] = gesprekVan(logboek[0]); assert.ok(binnen.occurredAt < uit.occurredAt, `binnenkomst ${binnen.occurredAt} hoort vóór antwoord ${uit.occurredAt} te liggen`); }); it('bewaart ook het gesprek van een geweigerde aanroep', async () => { const server = maak(); await server.handle(verzoek('tools/call', { name: 'lees_alle_xpubs' })); const m = gesprekVan(logboek[0]); assert.equal(m.length, 2); assert.match(m[1].text, /bestaat niet/); }); it('bewaart bij een mislukte tool de melding die terugging', async () => { const stuk = [{ ...TOOLS.find(t => t.name === 'connection_test'), run: async () => { throw new Error('de tool viel om'); }, }]; const server = maak(stuk); await server.handle(verzoek('tools/call', { name: 'connection_test' })); const m = gesprekVan(logboek[0]); assert.equal(m[1].text, 'de tool viel om'); }); it('bewaart de INGEKORTE tekst en niet de volledige', async () => { const server = maak([{ ...TOOLS.find(t => t.name === 'connection_test'), run: async () => ({ text: 'x'.repeat(CONFIG.AI.LIMITS.MAX_TEXT_CHARS * 2), fieldsShared: [] }), }]); const a = await server.handle(verzoek('tools/call', { name: 'connection_test' })); const m = gesprekVan(logboek[0]); assert.equal(m[1].text, a.result.content[0].text); assert.ok(m[1].text.length <= CONFIG.AI.LIMITS.MAX_TEXT_CHARS); }); it('schrijft geen gesprek bij een dry-run', async () => { const server = maak(); await server.dryRun('connection_test', {}); assert.equal(logboek.length, 0); }); }); describe('mcp: tools/call', () => { it('voert de tool uit, geeft tekst terug en logt de aanroep', async () => { const server = maak(); const a = await server.handle(verzoek('tools/call', { name: 'connection_test', arguments: { note: 'hallo' } })); assert.equal(a.result.isError, undefined); assert.match(a.result.content[0].text, /werkt/); assert.match(a.result.content[0].text, /hallo/, 'de notitie komt terug, zodat de gebruiker hem kan herkennen'); assert.equal(logboek.length, 1); assert.equal(logboek[0].tool, 'connection_test'); assert.equal(logboek[0].outcome, 'ok'); assert.deepEqual(logboek[0].fieldsShared, ['portfolio'], 'de naam van de portefeuille gaat mee en hoort dus gelogd te worden'); assert.equal(logboek[0].charactersOut, a.result.content[0].text.length, 'het aantal verstuurde tekens hoort te kloppen met wat er echt verstuurd is'); }); it('logt een aanroep zonder argumenten net zo goed', async () => { const server = maak(); await server.handle(verzoek('tools/call', { name: 'connection_test' })); assert.equal(logboek.length, 1); assert.deepEqual(logboek[0].arguments, {}); }); it('weigert een onbekende tool, en legt die poging vast', async () => { const server = maak(); const a = await server.handle(verzoek('tools/call', { name: 'lees_alle_xpubs' })); assert.ok(a.error, 'een onbekende tool is een protocolfout'); assert.match(a.error.message, /Onbekende tool/); assert.equal(logboek.length, 1); assert.equal(logboek[0].tool, 'lees_alle_xpubs', 'mét de naam die verzonnen is'); assert.equal(logboek[0].outcome, 'refused'); assert.equal(logboek[0].fieldsShared, undefined, 'er ging niets naar buiten'); }); it('weigert een verzoek zonder toolnaam, en legt dat vast', async () => { const server = maak(); const a = await server.handle(verzoek('tools/call', {})); assert.ok(a.error); assert.equal(logboek.length, 1); assert.equal(logboek[0].outcome, 'refused'); }); it('maakt van een tool-fout een geslaagd antwoord met isError, en logt hem als error', async () => { const echt = TOOLS.find(t => t.name === 'connection_test'); const stuk = [{ ...echt, run: async () => { throw new Error('de bron is onbereikbaar'); } }]; const server = maak(stuk); const a = await server.handle(verzoek('tools/call', { name: 'connection_test' })); assert.equal(a.error, undefined, 'geen protocolfout'); assert.equal(a.result.isError, true); assert.match(a.result.content[0].text, /onbereikbaar/); assert.equal(logboek[0].outcome, 'error'); assert.equal(logboek[0].detail, 'de bron is onbereikbaar'); }); it('laat een falend auditlog niet stil gebeuren', async () => { const kapot = { tools: TOOLS, logCall: async () => { throw new Error('schijf vol'); } }; const gooit = createMcpServer(kapot); await assert.rejects(() => gooit.handle(verzoek('tools/call', { name: 'connection_test' })), /schijf vol/); const gezien = []; const meldt = createMcpServer({ ...kapot, onLogError: (e) => gezien.push(e.message) }); const a = await meldt.handle(verzoek('tools/call', { name: 'connection_test' })); assert.ok(a.result, 'de aanroep zelf slaagt nog'); assert.deepEqual(gezien, ['schijf vol']); }); }); describe('mcp: het protocol zelf', () => { it('antwoordt op ping, ook vóór initialize', async () => { const server = maak(); const a = await server.handle(verzoek('ping')); assert.deepEqual(a.result, {}); }); it('geeft een onbekende methode de juiste JSON-RPC-code', async () => { const server = maak(); const a = await server.handle(verzoek('resources/list')); assert.equal(a.error.code, -32601, 'method not found'); }); it('maakt van ongeldige JSON een parse-fout in plaats van een crash', async () => { const server = maak(); const a = await server.handleRegel('{dit is geen json'); assert.equal(a.error.code, -32700); assert.equal(a.id, null); }); it('weigert iets dat geen JSON-RPC-object is', async () => { const server = maak(); for (const rommel of [null, 42, 'tekst', [1, 2]]) { const a = await server.handle(rommel); assert.equal(a.error.code, -32600, `${JSON.stringify(rommel)} hoort geweigerd te worden`); } }); it('geeft elke sessie een eigen id', () => { const a = maak(), b = maak(); assert.notEqual(a.session.id, b.session.id); assert.match(a.session.id, /^[0-9a-f-]{36}$/); }); it('laat de sessie niet van buiten aanpassen', async () => { const server = maak(); await server.handle(verzoek('initialize', { clientInfo: { name: 'echt' } })); server.session.client = 'gelogen'; await server.handle(verzoek('tools/call', { name: 'connection_test' }, 2)); assert.equal(logboek[0].client, 'echt'); }); });