{"version":3,"file":"sdk-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/mcp-foundation/sdk-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,KAAK,EAGX,mBAAmB,EAEnB,gBAAgB,EAEhB,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,MAAM,gBAAgB,CAAC;AAexB,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAmDD,qBAAa,aAAa;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,UAAU,CAAC,CAAsB;IACzC,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,OAAO,CAAC,CAAoC;IACpD,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAClC,OAAO,CAAC,GAAG,CAAC,CAAS;IACrB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,WAAW,CAAC,CAAiB;IACrC,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,YAAY,CAAC,CAA0B;IAC/C,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,WAAW,CAAgB;IAEnC,YAAY,OAAO,EAAE,oBAAoB,EAIxC;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAEK,OAAO,CAAC,UAAU,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuE5D;IAEK,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CA8BhC;IAEK,SAAS,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAW9C;IAEK,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAmCxD;IAED,QAAQ,IAAI,gBAAgB,CAiB3B;IAED,YAAY,IAAI,OAAO,CAQtB;IAED,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,iBAAiB;YAWX,YAAY;IAQ1B,OAAO,CAAC,aAAa;CAYrB","sourcesContent":["/**\n * MCP Client Foundation — official SDK adapter (2.13.0).\n *\n * This is the ONLY module that imports the MCP TypeScript SDK runtime and\n * transports. It owns a single stdio session: spawn, protocol negotiation,\n * tool discovery, tool invocation, cancellation/timeout, stderr isolation, and\n * clean shutdown. Everything crossing out of this file is Jensen-shaped (see\n * mcp-types.ts).\n *\n * Negotiation policy: `versionNegotiation: { mode: \"auto\" }` — probe with the\n * modern `server/discover` advertisement first, then fall back to the legacy\n * `initialize` handshake for 2025-era servers. The negotiated era/version are\n * captured and surfaced as observable Jensen session metadata; Jensen domains\n * never branch on them.\n */\n\nimport { type CallToolResult, Client, type Tool } from \"@modelcontextprotocol/client\";\nimport { StdioClientTransport, type StdioServerParameters } from \"@modelcontextprotocol/client/stdio\";\nimport { APP_NAME, VERSION } from \"../../config.js\";\nimport { classifyMcpConnectError, classifyMcpError, McpClientError } from \"./mcp-error.js\";\nimport type {\n\tMcpContentItem,\n\tMcpProtocolEra,\n\tMcpServerDefinition,\n\tMcpServerInfo,\n\tMcpServerSession,\n\tMcpSessionState,\n\tMcpToolCall,\n\tMcpToolDescriptor,\n\tMcpToolResult,\n} from \"./mcp-types.js\";\nimport { DEFAULT_MCP_REQUEST_TIMEOUT_MS, safeServerCommand } from \"./server-config.js\";\n\n/** Bounded stderr diagnostics: last N lines, each truncated. */\nconst STDERR_MAX_LINES = 200;\nconst STDERR_MAX_LINE_CHARS = 2_000;\n\n/**\n * Bound for the `server/discover` probe under `mode: \"auto\"`. On stdio a silent\n * server is treated as legacy, so this must be much shorter than the standard\n * request timeout or a spawn-per-invocation CLI would stall against a legacy\n * server that never answers unknown pre-`initialize` requests.\n */\nconst NEGOTIATION_PROBE_TIMEOUT_MS = 5_000;\n\nexport interface McpSdkAdapterOptions {\n\tsessionId: string;\n\tserverId: string;\n\tnow?: () => number;\n}\n\nfunction messageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction mapIdentity(serverVersion: { name: string; version: string } | undefined): McpServerInfo | undefined {\n\tif (!serverVersion) return undefined;\n\treturn { name: serverVersion.name, version: serverVersion.version };\n}\n\nfunction mapContent(content: readonly unknown[]): McpContentItem[] {\n\treturn content.map((item) => {\n\t\tif (typeof item !== \"object\" || item === null) {\n\t\t\treturn { type: \"unknown\", value: item } as McpContentItem;\n\t\t}\n\t\tconst record = item as Record<string, unknown>;\n\t\treturn { ...record } as McpContentItem;\n\t});\n}\n\nfunction mapTool(tool: Tool, serverId: string): McpToolDescriptor {\n\treturn {\n\t\tname: tool.name,\n\t\tdescription: tool.description,\n\t\tinputSchema: tool.inputSchema,\n\t\toutputSchema: tool.outputSchema,\n\t\tannotations: tool.annotations,\n\t\texecution: tool.execution,\n\t\ttitle: tool.title,\n\t\tmeta: tool._meta,\n\t\tserverId,\n\t};\n}\n\n/** Normalize the SDK era string into the stable Jensen vocabulary. */\nfunction normalizeEra(era: \"modern\" | \"legacy\" | undefined): McpProtocolEra {\n\tif (era === \"modern\" || era === \"legacy\") return era;\n\treturn \"unknown\";\n}\n\n/** Extract a human-facing message from a tool-error result's content. */\nfunction toolErrorMessage(content: McpContentItem[]): string | undefined {\n\tfor (const item of content) {\n\t\tif (item.type === \"text\" && typeof item.text === \"string\" && item.text.trim().length > 0) {\n\t\t\treturn item.text.trim();\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport class McpSdkAdapter {\n\tprivate readonly sessionId: string;\n\tprivate readonly serverId: string;\n\tprivate readonly now: () => number;\n\tprivate definition?: McpServerDefinition;\n\tprivate client?: Client;\n\tprivate state: McpSessionState = \"DISCONNECTED\";\n\tprivate failure?: { code: string; message: string };\n\tprivate connectedAtMs?: number;\n\tprivate disconnectedAtMs?: number;\n\tprivate pid?: number;\n\tprivate protocolVersion?: string;\n\tprivate protocolEra?: McpProtocolEra;\n\tprivate identity?: McpServerInfo;\n\tprivate capabilities?: Record<string, unknown>;\n\tprivate instructions?: string;\n\tprivate toolListChanged = false;\n\tprivate stderrPending = \"\";\n\tprivate stderrLines: string[] = [];\n\n\tconstructor(options: McpSdkAdapterOptions) {\n\t\tthis.sessionId = options.sessionId;\n\t\tthis.serverId = options.serverId;\n\t\tthis.now = options.now ?? Date.now;\n\t}\n\n\tget isConnected(): boolean {\n\t\treturn this.state === \"CONNECTED\";\n\t}\n\n\tasync connect(definition: McpServerDefinition): Promise<void> {\n\t\tif (this.client) throw new McpClientError(\"MCP_PROTOCOL_ERROR\", \"adapter already connected\");\n\t\tthis.definition = definition;\n\t\tthis.state = \"CONNECTING\";\n\t\tthis.failure = undefined;\n\n\t\tconst serverParams: StdioServerParameters = {\n\t\t\tcommand: definition.command,\n\t\t\targs: definition.args ? [...definition.args] : [],\n\t\t\tenv: definition.env ? { ...definition.env } : undefined,\n\t\t\tcwd: definition.cwd,\n\t\t\tstderr: \"pipe\",\n\t\t};\n\n\t\tconst transport = new StdioClientTransport(serverParams);\n\n\t\t// Capture stderr separately BEFORE start() so early diagnostics are not lost.\n\t\tconst stderrStream = transport.stderr;\n\t\tif (stderrStream) {\n\t\t\tstderrStream.on(\"data\", (chunk: Buffer) => this.captureStderr(chunk));\n\t\t}\n\n\t\tconst client = new Client(\n\t\t\t{ name: APP_NAME, version: VERSION },\n\t\t\t{\n\t\t\t\tcapabilities: {},\n\t\t\t\tversionNegotiation: {\n\t\t\t\t\tmode: \"auto\",\n\t\t\t\t\tprobe: { timeoutMs: NEGOTIATION_PROBE_TIMEOUT_MS },\n\t\t\t\t},\n\t\t\t\tlistChanged: {\n\t\t\t\t\ttools: {\n\t\t\t\t\t\tautoRefresh: true,\n\t\t\t\t\t\tdebounceMs: 0,\n\t\t\t\t\t\tonChanged: (error) => this.onToolsChanged(error),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\n\t\tclient.onclose = () => this.onTransportClosed();\n\t\tclient.onerror = (error) => {\n\t\t\t// Non-fatal out-of-band diagnostics are recorded but do not mutate state.\n\t\t\tthis.failure = this.failure ?? { code: \"MCP_PROTOCOL_ERROR\", message: messageOf(error) };\n\t\t};\n\n\t\tthis.client = client;\n\n\t\ttry {\n\t\t\tawait client.connect(transport, { timeout: definition.startupTimeoutMs });\n\t\t} catch (error) {\n\t\t\tconst code = classifyMcpConnectError(error);\n\t\t\tthis.state = \"FAILED\";\n\t\t\tthis.failure = { code, message: messageOf(error) };\n\t\t\tawait this.forceCleanup();\n\t\t\tthrow new McpClientError(code, `failed to connect to MCP server \"${definition.id}\": ${messageOf(error)}`, {\n\t\t\t\tserverId: definition.id,\n\t\t\t\tsessionId: this.sessionId,\n\t\t\t\tdetail: safeServerCommand(definition),\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\n\t\tthis.pid = transport.pid ?? undefined;\n\t\tthis.connectedAtMs = this.now();\n\t\tthis.state = \"CONNECTED\";\n\t\tthis.protocolVersion = client.getNegotiatedProtocolVersion() ?? undefined;\n\t\tthis.protocolEra = normalizeEra(client.getProtocolEra());\n\t\tthis.identity = mapIdentity(client.getServerVersion());\n\t\tthis.capabilities = client.getServerCapabilities() as Record<string, unknown> | undefined;\n\t\tthis.instructions = client.getInstructions();\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (this.state === \"DISCONNECTED\" || this.state === \"DISCONNECTING\") return;\n\t\tconst client = this.client;\n\t\tif (!client) {\n\t\t\tthis.state = \"DISCONNECTED\";\n\t\t\treturn;\n\t\t}\n\n\t\tthis.state = \"DISCONNECTING\";\n\t\ttry {\n\t\t\tawait client.close();\n\t\t} catch (error) {\n\t\t\tthis.state = \"FAILED\";\n\t\t\tthis.failure = { code: \"MCP_SHUTDOWN_FAILED\", message: messageOf(error) };\n\t\t\tthrow new McpClientError(\n\t\t\t\t\"MCP_SHUTDOWN_FAILED\",\n\t\t\t\t`failed to disconnect from MCP server \"${this.serverId}\": ${messageOf(error)}`,\n\t\t\t\t{\n\t\t\t\t\tserverId: this.serverId,\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tcause: error,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\n\t\t// The transport close event normally fires synchronously; normalize defensively.\n\t\tif (this.state === \"DISCONNECTING\") {\n\t\t\tthis.state = \"DISCONNECTED\";\n\t\t\tthis.disconnectedAtMs = this.now();\n\t\t}\n\t}\n\n\tasync listTools(): Promise<McpToolDescriptor[]> {\n\t\tthis.assertConnected(\"listTools\");\n\t\tconst client = this.client!;\n\n\t\t// v2 auto-aggregates every page when called without a cursor.\n\t\tconst page = await client.listTools();\n\t\tthis.toolListChanged = false;\n\n\t\tconst descriptors = page.tools.map((tool) => mapTool(tool, this.serverId));\n\t\tdescriptors.sort((a, b) => a.name.localeCompare(b.name) || a.serverId.localeCompare(b.serverId));\n\t\treturn descriptors;\n\t}\n\n\tasync callTool(call: McpToolCall): Promise<McpToolResult> {\n\t\tthis.assertConnected(\"callTool\");\n\t\tconst client = this.client!;\n\t\tconst definition = this.definition!;\n\t\tconst invokedAtMs = this.now();\n\t\tconst timeoutMs = call.timeoutMs ?? definition.requestTimeoutMs ?? DEFAULT_MCP_REQUEST_TIMEOUT_MS;\n\n\t\ttry {\n\t\t\tconst result = await client.callTool(\n\t\t\t\t{ name: call.toolName, arguments: { ...call.arguments } },\n\t\t\t\t{ timeout: timeoutMs, signal: call.signal },\n\t\t\t);\n\t\t\tconst completedAtMs = this.now();\n\t\t\treturn this.toToolResult(call.toolName, result, invokedAtMs, completedAtMs);\n\t\t} catch (error) {\n\t\t\tconst completedAtMs = this.now();\n\t\t\t// The SDK wraps an explicit client abort into a RequestTimeout-style error on\n\t\t\t// its shared cancel path, so detect the user's signal directly to keep\n\t\t\t// cancellation distinct from a genuine request timeout.\n\t\t\tconst code = call.signal?.aborted\n\t\t\t\t? \"MCP_REQUEST_CANCELLED\"\n\t\t\t\t: classifyMcpError(error, { toolName: call.toolName });\n\t\t\treturn {\n\t\t\t\tsessionId: this.sessionId,\n\t\t\t\tserverId: this.serverId,\n\t\t\t\ttoolName: call.toolName,\n\t\t\t\tstatus: this.resultStatusForCode(code),\n\t\t\t\tisError: false,\n\t\t\t\tcontent: [],\n\t\t\t\terrorCode: code,\n\t\t\t\terrorMessage: messageOf(error),\n\t\t\t\tinvokedAtMs,\n\t\t\t\tcompletedAtMs,\n\t\t\t};\n\t\t}\n\t}\n\n\tsnapshot(): McpServerSession {\n\t\treturn {\n\t\t\tsessionId: this.sessionId,\n\t\t\tserverId: this.serverId,\n\t\t\tstate: this.state,\n\t\t\tidentity: this.identity,\n\t\t\tcapabilities: this.capabilities,\n\t\t\tprotocolVersion: this.protocolVersion,\n\t\t\tprotocolEra: this.protocolEra,\n\t\t\tinstructions: this.instructions,\n\t\t\tconnectedAtMs: this.connectedAtMs,\n\t\t\tdisconnectedAtMs: this.disconnectedAtMs,\n\t\t\tfailure: this.failure,\n\t\t\tstderrTail: this.stderrLines.slice(-STDERR_MAX_LINES),\n\t\t\tpid: this.pid,\n\t\t\ttoolListChanged: this.toolListChanged,\n\t\t};\n\t}\n\n\tprocessAlive(): boolean {\n\t\tif (this.pid === undefined) return false;\n\t\ttry {\n\t\t\tprocess.kill(this.pid, 0);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate toToolResult(\n\t\ttoolName: string,\n\t\traw: CallToolResult | { toolResult: unknown },\n\t\tinvokedAtMs: number,\n\t\tcompletedAtMs: number,\n\t): McpToolResult {\n\t\tconst content = \"content\" in raw && Array.isArray(raw.content) ? mapContent(raw.content) : [];\n\t\tconst structuredContent = \"structuredContent\" in raw ? raw.structuredContent : undefined;\n\t\tconst isError = \"isError\" in raw ? raw.isError === true : false;\n\t\tconst status = isError ? \"tool-error\" : \"success\";\n\t\tconst errorMessage = isError ? (toolErrorMessage(content) ?? \"tool reported an error\") : undefined;\n\n\t\treturn {\n\t\t\tsessionId: this.sessionId,\n\t\t\tserverId: this.serverId,\n\t\t\ttoolName,\n\t\t\tstatus,\n\t\t\tisError,\n\t\t\tcontent,\n\t\t\tstructuredContent,\n\t\t\terrorCode: isError ? \"MCP_TOOL_CALL_FAILED\" : undefined,\n\t\t\terrorMessage,\n\t\t\tinvokedAtMs,\n\t\t\tcompletedAtMs,\n\t\t};\n\t}\n\n\tprivate resultStatusForCode(code: string): McpToolResult[\"status\"] {\n\t\tswitch (code) {\n\t\t\tcase \"MCP_REQUEST_TIMEOUT\":\n\t\t\t\treturn \"timeout\";\n\t\t\tcase \"MCP_REQUEST_CANCELLED\":\n\t\t\t\treturn \"cancelled\";\n\t\t\tcase \"MCP_CONNECTION_LOST\":\n\t\t\t\treturn \"connection-lost\";\n\t\t\tcase \"MCP_TOOL_NOT_FOUND\":\n\t\t\t\treturn \"failed\";\n\t\t\tdefault:\n\t\t\t\treturn \"failed\";\n\t\t}\n\t}\n\n\tprivate assertConnected(operation: string): void {\n\t\tif (this.state !== \"CONNECTED\" || !this.client) {\n\t\t\tconst code: \"MCP_SESSION_INVALID\" | \"MCP_SESSION_NOT_FOUND\" =\n\t\t\t\tthis.state === \"DISCONNECTED\" ? \"MCP_SESSION_NOT_FOUND\" : \"MCP_SESSION_INVALID\";\n\t\t\tthrow new McpClientError(\n\t\t\t\tcode,\n\t\t\t\t`cannot ${operation}: MCP session \"${this.sessionId}\" is not connected (${this.state})`,\n\t\t\t\t{\n\t\t\t\t\tserverId: this.serverId,\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate onToolsChanged(error: Error | null): void {\n\t\tthis.toolListChanged = true;\n\t\tif (error) {\n\t\t\tthis.failure = this.failure ?? {\n\t\t\t\tcode: \"MCP_PROTOCOL_ERROR\",\n\t\t\t\tmessage: `tool list change failed: ${messageOf(error)}`,\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate onTransportClosed(): void {\n\t\tif (this.state === \"DISCONNECTING\") {\n\t\t\tthis.state = \"DISCONNECTED\";\n\t\t\tthis.disconnectedAtMs = this.now();\n\t\t\treturn;\n\t\t}\n\t\tthis.state = \"FAILED\";\n\t\tthis.failure = this.failure ?? { code: \"MCP_CONNECTION_LOST\", message: \"MCP connection closed\" };\n\t\tthis.disconnectedAtMs = this.now();\n\t}\n\n\tprivate async forceCleanup(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client?.close();\n\t\t} catch {\n\t\t\t// Best-effort during failure paths; the failure is already authoritative.\n\t\t}\n\t}\n\n\tprivate captureStderr(chunk: Buffer): void {\n\t\tconst text = chunk.toString(\"utf8\");\n\t\tthis.stderrPending += text;\n\t\tconst parts = this.stderrPending.split(\"\\n\");\n\t\tthis.stderrPending = parts.pop() ?? \"\";\n\t\tfor (const line of parts) {\n\t\t\tthis.stderrLines.push(line.slice(0, STDERR_MAX_LINE_CHARS));\n\t\t}\n\t\tif (this.stderrLines.length > STDERR_MAX_LINES) {\n\t\t\tthis.stderrLines = this.stderrLines.slice(-STDERR_MAX_LINES);\n\t\t}\n\t}\n}\n"]}