import { DyFM_Log } from '@futdevpro/fsm-dynamo'; import { DyNTS_Mcp_CallResult, DyNTS_Mcp_ServerInfo, DyNTS_Mcp_StartOptions, DyNTS_Mcp_ToolDefinition } from '../_models/interfaces/dynts-mcp.interface'; import { DyNTS_Mcp_Adapter } from './dynts-mcp.adapter'; /** * `DyNTS_Mcp_Server_ServiceBase` (BFR-AM-003) — a **domain-agnosztikus** MCP-szerver-base. Egy bedrock-réteg * a hivatalos `@modelcontextprotocol/sdk` fölött: a server-base felel a tool-registry-ért, a * `tools/list`/`tools/call` dispatch-ért (az adaptoron át) + a transport-indításért — a SDK KIZÁRÓLAG * a `DyNTS_Mcp_Adapter` mögött él (egy jövőbeli transport-/SDK-csere NON-breaking). * * **Használat (consumer):** a consumer leszármazik (`extends DyNTS_Mcp_Server_ServiceBase`) + implementálja a * `getServerInfo()` (név/verzió) és `getTools()` (a SAJÁT tool-jai) abstract metódusokat. A bedrock * SEMMILYEN domain-tool-t NEM definiál — csak a server-base + a transport + a registry. (Pl. a FAM a * `read`/`write`/`capabilities` tool-jait itt regisztrálja, a bedrock-on kívül.) * * ```ts * class My_McpServer extends DyNTS_Mcp_Server_ServiceBase { * protected getServerInfo() { return { name: 'my-app', version: '1.0.0' }; } * protected getTools() { return [readTool, writeTool]; } * } * await new My_McpServer().start({ transport: 'stdio' }); * ``` * * **stdio-konvenció:** a stdout a JSON-RPC csatorna — a boot-üzenet + minden log a stderr-re megy (a * hívó App a `DyFM_Log`-ot stdio-módban stderr-re konfigurálja). */ export abstract class DyNTS_Mcp_Server_ServiceBase { /** Az MCP-adaptor (a regisztrált tool-ok + a transport mögötti SDK-réteg). Lazy a `build()`-ben. */ private adapter: DyNTS_Mcp_Adapter | null = null; /** * A szerver identitása (név/verzió) — a consumer adja. A SDK `serverInfo`-jaként hirdetődik. */ protected abstract getServerInfo(): DyNTS_Mcp_ServerInfo; /** * A consumer SAJÁT tool-definíciói (az advertised halmaz). A `build()` ezeket regisztrálja az * adaptoron át — a `tools/list` PONTOSAN ezeket hirdeti, a `tools/call` ezekre dispatch-el. */ protected abstract getTools(): DyNTS_Mcp_ToolDefinition[]; /** * A szerver felépítése (a consumer-tool-ok regisztrálása az adaptoron). NEM indítja a transportot * — a `start()` teszi. Külön lépés a contract-teszt kedvéért (a szerver felépül + a tool-okat * hirdeti, transport nélkül). Idempotens: ismételt hívás ugyanazt az adaptort adja vissza. */ public build(): DyNTS_Mcp_Adapter { if (this.adapter) { return this.adapter; } const adapter: DyNTS_Mcp_Adapter = new DyNTS_Mcp_Adapter(this.getServerInfo()); for (const tool of this.getTools()) { adapter.registerTool(tool); } this.adapter = adapter; return adapter; } /** * Az MCP-szerver indítása a választott transporton (default `stdio`). Felépíti a szervert (ha még * nem), majd a transportot csatlakoztatja — innentől a `tools/list` a consumer-tool-okat hirdeti, * a `tools/call` route-ol. A boot-üzenet a stderr-en. */ public async start(options?: DyNTS_Mcp_StartOptions): Promise { const transport: 'stdio' = options?.transport ?? 'stdio'; const adapter: DyNTS_Mcp_Adapter = this.build(); const info: DyNTS_Mcp_ServerInfo = this.getServerInfo(); DyFM_Log.testInfo(`[DyNTS MCP] ${transport} server starting (${info.name} v${info.version}); ` + `advertised tools: ${adapter.getAdvertisedToolNames().join(', ')}`); await this.startTransport(adapter, transport); } /** * A felépített tool-nevek (a contract/diszjunkció-teszthez). Build előtt a deklarált `getTools()` * neveit adja (transport nélkül is determinisztikus). */ public getAdvertisedToolNames(): string[] { return this.adapter ? this.adapter.getAdvertisedToolNames() : this.getTools().map((tool) => tool.name); } /** * Egy tool KÖZVETLEN hívása (a transport megkerülésével) — a contract-teszthez / REST-paritáshoz. * Felépíti a szervert (ha még nem), majd az adaptor egységes dispatch-ét futtatja (ismeretlen * tool → strukturált hiba; handler-hiba → `isError:true`). */ public async callTool(set: { name: string; arguments?: unknown }): Promise { return this.build().dispatchToolCall(set); } /** A szerver leállítása (graceful close — teszt-/shutdown-úthoz). */ public async close(): Promise { if (this.adapter) { await this.adapter.close(); } } /** * A transport-indítás dispatch-e. Külön metódus, hogy egy jövőbeli transport (http/sse) NON-breaking * módon bővíthető legyen (a `start()` + a registry változatlan). MVP: csak `stdio`. */ private async startTransport(adapter: DyNTS_Mcp_Adapter, transport: 'stdio'): Promise { switch (transport) { case 'stdio': await adapter.startStdio(); return; default: // Exhaustiveness-guard: a `DyNTS_Mcp_TransportKind` bővülésekor itt fordítási hibát kapunk. throw new Error(`Unsupported MCP transport: '${transport as string}'.`); } } }