// A `@modelcontextprotocol/sdk` ESM-only csomag; a deep subpath-importjai (server/index, server/stdio, // types) KÖTELEZŐEN `.js`-suffixet igényelnek (az SDK `exports`-map így oldja fel) — ezért a `no-js-import` // szabály ITT indokoltan kikapcsolva (C2, FAM-REV bedrock-fix). Suffix-strip → resolve-hiba. /* eslint-disable @futdevpro/dynamo/no-js-import */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, CallToolResult, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; /* eslint-enable @futdevpro/dynamo/no-js-import */ import { DyFM_Error } from '@futdevpro/fsm-dynamo'; import { DyNTS_Mcp_CallResult, DyNTS_Mcp_ServerInfo, DyNTS_Mcp_ToolDefinition, DyNTS_Mcp_ToolOutcome } from '../_models/interfaces/dynts-mcp.interface'; /** * `DyNTS_Mcp_Adapter` (BFR-AM-003) — a hivatalos `@modelcontextprotocol/sdk` köré épített **vékony * adaptor-réteg**. EZ az EGYETLEN hely a `/mcp` submodule-ban, ahol a SDK-típusok megjelennek — a * `DyNTS_Mcp_Server_ServiceBase` + a consumer tool-jai SDK-mentesek, így egy jövőbeli transport-/SDK-csere * NON-breaking (csak ez az osztály cserélődik). * * Felelősség (3 dolog, semmi domain-logika): * 1. EGY `registerTool` choke-pont (a tool-name → def map), * 2. a `tools/list` + `tools/call` JSON-RPC handler bekötése a SDK-szerverre, * 3. egységes hiba-fordítás: a tool-handler dobott hibája (vagy `isError:true` outcome-ja) * strukturált `CallToolResult { isError:true }`-tá fordul — SOHA nem néma, SOHA nem crash. * * **stdio-konvenció:** a stdout a JSON-RPC csatorna — az adaptor maga SEMMIT nem ír a stdout-ra a * transport-payload-on kívül (minden log/diag a hívó App felelőssége, stderr-re). */ export class DyNTS_Mcp_Adapter { /** A SDK MCP-szerver (a transport-agnosztikus protocol-réteg). */ private readonly server: Server; /** A regisztrált tool-definíciók `name` → def map-je (a `tools/call` dispatch-hez). */ private readonly tools: Map = new Map(); constructor(set: DyNTS_Mcp_ServerInfo) { this.server = new Server( { name: set.name, version: set.version }, { capabilities: { tools: {} } }, ); this.installHandlers(); } /** * Az EGYETLEN tool-regisztrációs choke-pont. A regisztrált tool-ok hirdetődnek a `tools/list`-ben * és dispatch-elhetők a `tools/call`-on. Az azonos nevű ismételt regisztráció felülír (last-wins). */ public registerTool(definition: DyNTS_Mcp_ToolDefinition): void { this.tools.set(definition.name, definition); } /** A hirdetett (regisztrált) tool-nevek (a contract/diszjunkció-teszthez). */ public getAdvertisedToolNames(): string[] { return Array.from(this.tools.keys()); } /** * A stdio transport csatlakoztatása + a szerver indítása. A `connect` után a `tools/list` a * regisztrált tool-okat hirdeti. A boot-üzenetek a stderr-en (a hívó App felel). */ public async startStdio(): Promise { const transport: StdioServerTransport = new StdioServerTransport(); await this.server.connect(transport); } /** A szerver leállítása (graceful close — teszt-/shutdown-úthoz). */ public async close(): Promise { await this.server.close(); } /** * Egy tool-hívás KÖZVETLEN dispatch-elése (a transport-megkerülésével) — a contract/registry-teszt * ezt hívja a stdio-szerver indítása nélkül. Ugyanaz a logika fut, mint a SDK `tools/call`-on: * ismeretlen tool → strukturált hiba; handler-hiba → `isError:true` fordítás. */ public async dispatchToolCall(set: { name: string; arguments?: unknown }): Promise { const definition: DyNTS_Mcp_ToolDefinition | undefined = this.tools.get(set.name); if (!definition) { return this.errorResult(`Unknown tool: '${set.name}'. Advertised tools: ` + `${this.getAdvertisedToolNames().join(', ')}.`); } try { const outcome: DyNTS_Mcp_ToolOutcome = await definition.handler(set.arguments); return this.toCallResult(outcome); } catch (error) { return this.errorResult(this.describeError(error), error); } } // ========================================================================= // SDK request-handler bekötés (tools/list + tools/call) // ========================================================================= /** A `tools/list` és a `tools/call` JSON-RPC handler bekötése a SDK-szerverre. */ private installHandlers(): void { // tools/list — PONTOSAN a regisztrált (advertised) tool-okat hirdeti. this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: Array.from(this.tools.values()).map((definition) => ({ name: definition.name, description: definition.description, inputSchema: definition.inputSchema, })), })); // tools/call — a megfelelő handler futtatása + egységes hiba-fordítás (a közös dispatch-en át). // A `DyNTS_Mcp_CallResult` a SDK `CallToolResult` egy al-shape-je (text-content + isError) — a // SDK-cast KIZÁRÓLAG itt, az adaptor-boundary-n él (a SDK izolálva marad). this.server.setRequestHandler(CallToolRequestSchema, async (request): Promise => (await this.dispatchToolCall({ name: request.params.name, arguments: request.params.arguments, })) as CallToolResult); } // ========================================================================= // CallToolResult fordítás (a transport-payload — text-JSON; egységes hiba-fordítás) // ========================================================================= /** Egy sikeres (vagy strukturált-hiba) handler-outcome → `CallToolResult` (text-JSON content). */ private toCallResult(outcome: DyNTS_Mcp_ToolOutcome): DyNTS_Mcp_CallResult { return { content: [{ type: 'text', text: this.stringify(outcome.data) }], isError: outcome.isError === true, }; } /** Egy hiba → `CallToolResult { isError:true }` strukturált hiba-payload-dal (soha nem néma). */ private errorResult(message: string, error?: unknown): DyNTS_Mcp_CallResult { const errorCode: string | undefined = error instanceof DyFM_Error ? DyFM_Error.getErrorCode(error) : undefined; const payload: object = { ok: false, error: { errorCode: errorCode, message: message } }; return { content: [{ type: 'text', text: this.stringify(payload) }], isError: true }; } /** Egy hiba ember-olvasható üzenete (DyFM_Error → message; Error → message; egyéb → fallback). */ private describeError(error: unknown): string { if (error instanceof DyFM_Error) { return DyFM_Error.getErrorMessage(error) || 'Unknown DyFM error.'; } if (error instanceof Error) { return error.message; } return 'Unknown error during tool execution.'; } /** JSON-stringify (a transport text-content payload-ja); szerializálás-hiba → strukturált hiba. */ private stringify(data: unknown): string { try { return JSON.stringify(data); } catch { return JSON.stringify({ ok: false, error: { message: 'The response is not JSON-serializable.' } }); } } }