import { DyNTS_Mcp_Server_ServiceBase } from './dynts-mcp-server.service-base'; import { DyNTS_Mcp_CallResult, DyNTS_Mcp_ServerInfo, DyNTS_Mcp_ToolDefinition } from '../_models/interfaces/dynts-mcp.interface'; /** * CONTRACT — `DyNTS_Mcp_Server_ServiceBase` tool-registry + dispatch (BFR-AM-003). A transportot NEM nyitjuk * meg (nincs valódi stdio-szerver) — a `build()` + `callTool()` közvetlen dispatch-ét teszteljük, * ami UGYANAZ a logika, mint a SDK `tools/call`-ja: * - N tool regisztrálása → `getAdvertisedToolNames` PONTOSAN ezeket listázza, * - `callTool` a megfelelő handler-re dispatch-el (a `name` szerint), * - ismeretlen tool → strukturált `isError:true` (NEM néma, NEM crash), * - handler-dobott hiba → `isError:true` fordítás (a payload tartalmazza a message-et), * - `isError:true` outcome → `isError:true` call-result. */ describe('| DyNTS_Mcp_Server_ServiceBase (tool-registry + dispatch, BFR-AM-003)', () => { /** A handler-hívások nyoma (melyik tool, milyen args) — a dispatch-helyesség igazolásához. */ let calls: { name: string; args: unknown }[]; /** Egy teszt-tool-definíció gyára (a handler `data`-ba teszi a tool-nevet + az args-ot). */ const makeTool = (name: string): DyNTS_Mcp_ToolDefinition => ({ name: name, description: `Test tool ${name}.`, inputSchema: { type: 'object', properties: { x: { type: 'string' } } }, handler: async (args: unknown) => { calls.push({ name: name, args: args }); return { data: { tool: name, echo: args } }; }, }); /** A teszt-szerver: 3 tool + egy szándékosan dobó tool. */ class TestServer extends DyNTS_Mcp_Server_ServiceBase { protected getServerInfo(): DyNTS_Mcp_ServerInfo { return { name: 'test-mcp', version: '0.0.1-spec' }; } protected getTools(): DyNTS_Mcp_ToolDefinition[] { return [ makeTool('alpha'), makeTool('beta'), { name: 'boom', description: 'Always throws.', inputSchema: { type: 'object' }, handler: async () => { throw new Error('kaboom from handler'); }, }, { name: 'structuredError', description: 'Returns a structured error outcome.', inputSchema: { type: 'object' }, handler: async () => ({ data: { reason: 'nope' }, isError: true }), }, ]; } } let server: TestServer; beforeEach(() => { calls = []; server = new TestServer(); }); /** A `content[0].text` JSON-payload kiparse-olása (a transport text-JSON shape-je). */ const payloadOf = (result: DyNTS_Mcp_CallResult): { [key: string]: unknown } => JSON.parse(result.content[0].text) as { [key: string]: unknown }; describe('| registry — getTools → advertised', () => { it('| a deklarált tool-okat hirdeti build ELŐTT is (determinisztikus)', () => { expect(server.getAdvertisedToolNames().sort()) .toEqual([ 'alpha', 'beta', 'boom', 'structuredError' ]); }); it('| build() után UGYANAZOK a hirdetett tool-ok (idempotens build)', () => { const adapter1 = server.build(); const adapter2 = server.build(); expect(adapter1).toBe(adapter2); expect(adapter1.getAdvertisedToolNames().sort()) .toEqual([ 'alpha', 'beta', 'boom', 'structuredError' ]); }); }); describe('| dispatch — callTool a megfelelő handler-re route-ol', () => { it('| alpha → az alpha handler fut (a data a tool-nevet + az args-ot adja)', async () => { const result: DyNTS_Mcp_CallResult = await server.callTool({ name: 'alpha', arguments: { x: '1' } }); expect(result.isError).toBe(false); expect(calls).toEqual([{ name: 'alpha', args: { x: '1' } }]); expect(payloadOf(result)).toEqual({ tool: 'alpha', echo: { x: '1' } }); }); it('| beta → a beta handler fut (a dispatch a `name` szerint választ)', async () => { await server.callTool({ name: 'beta', arguments: { x: '2' } }); expect(calls).toEqual([{ name: 'beta', args: { x: '2' } }]); }); }); describe('| strukturált hiba (no-silent-failure)', () => { it('| ismeretlen tool → isError:true + advertised-lista a message-ben (NEM néma/crash)', async () => { const result: DyNTS_Mcp_CallResult = await server.callTool({ name: 'does_not_exist' }); expect(result.isError).toBe(true); const payload = payloadOf(result) as { ok: boolean; error: { message: string } }; expect(payload.ok).toBe(false); expect(payload.error.message).toContain('does_not_exist'); expect(payload.error.message).toContain('alpha'); // egyetlen ismeretlen tool sem futtatott handlert. expect(calls.length).toBe(0); }); it('| handler-dobott hiba → isError:true + a hiba message-e a payload-ban (NEM crash)', async () => { const result: DyNTS_Mcp_CallResult = await server.callTool({ name: 'boom' }); expect(result.isError).toBe(true); const payload = payloadOf(result) as { ok: boolean; error: { message: string } }; expect(payload.ok).toBe(false); expect(payload.error.message).toContain('kaboom from handler'); }); it('| isError:true outcome → isError:true call-result (a strukturált hiba átmegy)', async () => { const result: DyNTS_Mcp_CallResult = await server.callTool({ name: 'structuredError' }); expect(result.isError).toBe(true); expect(payloadOf(result)).toEqual({ reason: 'nope' }); }); }); describe('| start — transport-választás', () => { it('| ismeretlen transport → dob (exhaustiveness-guard); stdio nem nyílik a spec-ben', async () => { let thrown: unknown; try { // Szándékosan érvénytelen transport (a típus-szűkítés megkerülésével) — a guard-ágat teszteli, // valódi stdio-szervert NEM indítunk. await server.start({ transport: 'http' as never }); } catch (error) { thrown = error; } expect(thrown).toBeDefined(); expect((thrown as Error).message).toContain('Unsupported MCP transport'); }); }); });