@x-otto/mcp 0.1.0-alpha.19 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["logger","logger","logger"],"sources":["../src/sse.ts","../src/transport.ts","../src/mcp-client.ts","../src/mcp-tool-adapter.ts","../src/oauth/discovery.ts","../src/oauth/registration.ts","../src/oauth/token.ts","../src/oauth/token-store.ts","../src/oauth/refresh-lock.ts","../src/oauth/login.ts","../src/mcp-manager.ts","../src/dot-mcp-json.ts"],"sourcesContent":["/**\n * sse.ts — W3C/MCP 规范的 Server-Sent Events 数据解析(M6/RFC-016 一致性硬化).\n *\n * 旧 consumeStream 内联解析有三处偏离规范:\n * 1. 仅按 `\\n` 切,未剥 CRLF 的尾随 `\\r`;\n * 2. 硬要求 `data: `(带空格),漏掉 `data:{...}`(冒号后空格可选);\n * 3. 每条 `data:` 行立即派发,未按规范累积多行 data 直到空行(事件边界)再 join。\n *\n * 本解析器对齐规范 + 官方 SDK(eventsource-parser 语义):累积 `data` 字段值,\n * 遇空行派发一个事件(多行 data 以 `\\n` 连接);忽略注释行(`:` 开头)与其它字段\n * (event/id/retry);流结束时 flush 末尾未以空行收尾的事件。\n */\nexport interface SseDataParser {\n /** 喂入一段原始文本块(可跨事件/跨行边界)。 */\n feed(chunk: string): void\n /** 流结束时调用:派发任何未以空行收尾的累积事件。 */\n flush(): void\n}\n\n/** 创建一个 SSE data 解析器;每个完整事件的 data(多行已 join)经 onData 回调。 */\nexport function createSseDataParser(onData: (data: string) => void): SseDataParser {\n let buffer = ''\n let dataLines: string[] = []\n\n const dispatch = (): void => {\n if (dataLines.length === 0) {\n return\n }\n const data = dataLines.join('\\n')\n dataLines = []\n onData(data)\n }\n\n const consumeLine = (rawLine: string): void => {\n const line = rawLine.endsWith('\\r') ? rawLine.slice(0, -1) : rawLine\n if (line === '') {\n dispatch()\n return\n }\n if (line.startsWith(':')) {\n return\n }\n if (line.startsWith('data:')) {\n let value = line.slice(5)\n if (value.startsWith(' ')) {\n value = value.slice(1)\n }\n dataLines.push(value)\n }\n }\n\n return {\n feed(chunk: string): void {\n buffer += chunk\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n consumeLine(line)\n }\n },\n flush(): void {\n if (buffer.length > 0) {\n consumeLine(buffer)\n buffer = ''\n }\n dispatch()\n },\n }\n}\n","import { spawn } from 'node:child_process'\nimport { createLogger, McpError, buildAllowedEnv } from '@x-otto/shared'\nimport { JSON_RPC_VERSION } from './types'\nimport type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './types'\nimport { createSseDataParser } from './sse'\n\n/** 轻量进程句柄(避免 MCP 包直接依赖 @x-otto/runtime) */\nexport interface LightManagedProcess {\n readonly pid: number | undefined\n readonly stdin: NodeJS.WritableStream | null\n readonly stdout: NodeJS.ReadableStream | null\n readonly stderr: NodeJS.ReadableStream | null\n kill(): Promise<void>\n onError(callback: (err: Error) => void): void\n onExit(callback: () => void): void\n}\n\n/**\n * 进程 spawn 工厂:由宿主(coding/McpController)注入,内部 wrap @x-otto/runtime 的 ProcessRuntime,\n * 使 MCP 子进程纳入统一运行时(Layer 3 exit reaper 覆盖 + 可观测)。缺省时 StdioTransport 走\n * 向后兼容的私有 spawn(仍 buildAllowedEnv + detached)。\n */\nexport type ProcessSpawnFactory = (command: string, args: string[], env: Record<string, string>) => LightManagedProcess\n\nconst logger = createLogger('@x-otto/mcp:transport')\n\nconst SSE_DEFAULT_TIMEOUT_MS = 30_000\n\nconst MAX_MESSAGE_BYTES = 32 * 1024 * 1024\n\n/**\n * 官方 MCP stdio 分帧:每条 JSON-RPC 消息单行、`\\n` 分隔、正文不含内嵌换行\n * (JSON.stringify 会把字符串内换行转义为 `\\n` 字面量,故按字节 0x0A 切是安全的;\n * 多字节 UTF-8 续字节永不为 0x0A)。容忍 CRLF。返回完整行文本 + 剩余未成行字节。\n *\n * 健壮性:无换行且缓冲超 MAX_MESSAGE_BYTES → 复位(防 DoS)。\n */\nexport function decodeNewlineFrames(buffer: Buffer): { bodies: string[]; rest: Buffer } {\n const bodies: string[] = []\n let buf = buffer\n\n for (;;) {\n const nl = buf.indexOf(0x0a)\n if (nl === -1) {\n if (buf.length > MAX_MESSAGE_BYTES) {\n logger.warn('MCP newline frame exceeds limit without terminator; resetting buffer')\n buf = Buffer.alloc(0)\n }\n break\n }\n\n let line = buf.subarray(0, nl)\n if (line.length > 0 && line[line.length - 1] === 0x0d) {\n line = line.subarray(0, line.length - 1)\n }\n buf = buf.subarray(nl + 1)\n\n const text = line.toString('utf-8').trim()\n if (text) bodies.push(text)\n }\n\n return { bodies, rest: buf }\n}\n\ntype FetchLike = (input: string, init?: RequestInit) => Promise<Response>\n\nexport interface McpTransport {\n send(message: JsonRpcRequest | JsonRpcNotification): void | Promise<void>\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void\n /** 传输非预期断开(进程退出 / SSE 流结束或出错)时回调;client 据此触发自动重连。 */\n onClose(handler: (reason?: string) => void): void\n start(): Promise<void>\n close(): Promise<void>\n}\n\n/**\n * 校验 MCP server URL:协议白名单 http/https,拒绝 file:/ftp:/data:/javascript: 等。\n * **不**拦本机/私网——本地 MCP server(localhost/127.0.0.1)是常态;DNS-rebinding 对长驻配置端点\n * 风险低,且 MCP 端点由运维配置(非 LLM 输入)。仅挡协议层误用与畸形 URL。\n */\nexport function validateMcpUrl(url: string): URL {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`Invalid MCP server URL: ${url}`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`Unsupported MCP server URL protocol: ${parsed.protocol} (use http/https)`)\n }\n return parsed\n}\n\nexport class StdioTransport implements McpTransport {\n private managedProcess: LightManagedProcess | null = null\n private messageHandler: ((msg: JsonRpcResponse | JsonRpcNotification) => void) | null = null\n private closeHandler: ((reason?: string) => void) | null = null\n private closing = false\n private buffer: Buffer = Buffer.alloc(0)\n private readonly command: string\n private readonly args: string[]\n private readonly env: Record<string, string>\n private readonly processSpawnFactory: ProcessSpawnFactory | undefined\n\n constructor(\n command: string,\n args: string[] = [],\n env: Record<string, string> = {},\n processSpawnFactory?: ProcessSpawnFactory,\n ) {\n this.command = command\n this.args = args\n this.env = env\n this.processSpawnFactory = processSpawnFactory\n }\n\n async start(): Promise<void> {\n if (this.processSpawnFactory) {\n // RFC-085 M2:通过注入的 spawn factory 管理进程(将接入 ProcessRuntime)\n this.managedProcess = this.processSpawnFactory(this.command, this.args, this.env)\n } else {\n // 向后兼容:直接 spawn\n const child = spawn(this.command, this.args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n env: buildAllowedEnv(this.env),\n detached: true, // C1\n })\n this.managedProcess = {\n pid: child.pid,\n stdin: child.stdin,\n stdout: child.stdout,\n stderr: child.stderr,\n kill: async (): Promise<void> => {\n child.stdin?.end()\n return new Promise<void>((resolve) => {\n const t = setTimeout(() => { child.kill('SIGKILL'); resolve() }, 2000)\n child.on('exit', () => { clearTimeout(t); resolve() })\n child.kill('SIGTERM')\n })\n },\n onError: (callback: (err: Error) => void): void => { child.on('error', callback) },\n onExit: (callback: () => void): void => { child.on('exit', () => callback()) },\n }\n }\n\n const proc = this.managedProcess\n const stdout = proc.stdout\n if (stdout) {\n stdout.on('data', (chunk: unknown) => {\n // RFC review D6a:增量分帧——不再每次 data 事件 Buffer.concat 整个未完成缓冲\n //(O(n²) 复制);只把「残留 + chunk 中最后一个换行之前」一次性交给帧解码,\n // 尾部残留原样持有。大响应下每字节至多参与一次拼接(线性)。\n this.pushChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string))\n })\n }\n\n const stderr = proc.stderr\n if (stderr) {\n stderr.on('data', (chunk: unknown) => {\n const s = Buffer.isBuffer(chunk) ? chunk.toString('utf-8').trimEnd() : String(chunk).trimEnd()\n logger.debug('MCP stderr: %s', s)\n })\n }\n\n proc.onError((err) => {\n logger.error('MCP process error: %s', err.message)\n })\n\n proc.onExit(() => {\n logger.debug('MCP process exited')\n if (!this.closing) {\n this.closeHandler?.('process exited')\n }\n this.managedProcess = null\n })\n }\n\n send(message: JsonRpcRequest | JsonRpcNotification): void {\n const stdin = this.managedProcess?.stdin\n if (!stdin?.writable) {\n throw new Error('MCP stdio transport is not connected')\n }\n\n const json = JSON.stringify(message)\n ;(stdin as NodeJS.WritableStream).write(json + '\\n')\n }\n\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void {\n this.messageHandler = handler\n }\n\n onClose(handler: (reason?: string) => void): void {\n this.closeHandler = handler\n }\n\n async close(): Promise<void> {\n this.closing = true\n if (this.managedProcess) {\n await this.managedProcess.kill()\n this.managedProcess = null\n }\n }\n\n /**\n * RFC review D6a:增量分帧——仅把「残留 buffer + chunk 中最后一个换行之前」的部分\n * 交给 decodeNewlineFrames(一次性、线性),换行之后的未完成尾部作为新残留持有。\n *\n * 语义与旧实现全等:\n * - 帧边界 = 换行(官方 MCP stdio 分帧);\n * - 无换行且残留超 MAX_MESSAGE_BYTES → 复位(防 DoS,同 decodeNewlineFrames 语义);\n * - 解析失败只告警不中断(同一消息级容错)。\n *\n * 复杂度:旧实现每 chunk 复制「全部残留 + chunk」(残留可含已收帧前缀 → O(n²));\n * 新实现每 chunk 只复制「残留 + 完整帧段」,残留段通常为空或单帧尾部 → O(总字节)。\n */\n private pushChunk(chunk: Buffer): void {\n if (chunk.length === 0) return\n const lastNl = chunk.lastIndexOf(0x0a)\n if (lastNl === -1) {\n // 无换行:追加到残留缓冲(超限复位防护,同 decodeNewlineFrames 的 rest 语义)\n if (this.buffer.length + chunk.length > MAX_MESSAGE_BYTES) {\n logger.warn('MCP newline frame exceeds limit without terminator; resetting buffer')\n this.buffer = Buffer.alloc(0)\n return\n }\n this.buffer = this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk])\n return\n }\n\n // 完整帧段 = 残留 + chunk[0..lastNl];decode 只消费一次\n const complete =\n this.buffer.length === 0\n ? chunk.subarray(0, lastNl + 1)\n : Buffer.concat([this.buffer, chunk.subarray(0, lastNl + 1)])\n const { bodies, rest } = decodeNewlineFrames(complete)\n\n // 残留 = decode 的 rest(恒为空,防御)+ chunk 尾部(lastNl 之后,通常为空)\n const tail = chunk.subarray(lastNl + 1)\n if (rest.length > 0 && tail.length > 0) {\n this.buffer = Buffer.concat([rest, tail])\n } else if (rest.length > 0) {\n this.buffer = rest\n } else {\n this.buffer = tail\n }\n\n for (const body of bodies) {\n try {\n const message = JSON.parse(body) as JsonRpcResponse | JsonRpcNotification\n this.messageHandler?.(message)\n } catch (err) {\n logger.warn('Failed to parse MCP message: %s', (err as Error).message)\n }\n }\n }\n}\n\nexport interface SseTransportOptions {\n /** 单次请求超时(ms),与传输级 abort 组合。缺省 30s。 */\n requestTimeoutMs?: number\n /** 注入 fetch(测试用)。缺省全局 fetch。 */\n fetchImpl?: FetchLike\n /**\n * 静态请求头,随每次 GET(流)/POST(消息)发出。鉴权(`Authorization: Bearer …` /\n * `X-API-Key`)及自定义头。与内置头(Accept/Content-Type/Mcp-Session-Id)合并——**内置头优先**,\n * 防止误覆盖协议必需头。\n */\n headers?: Record<string, string>\n /**\n * 动态 Bearer token provider(OAuth 2.1)。每次请求前调用,返回的 access token\n * 注入 `Authorization: Bearer <token>` 头。**优先级最高**——覆盖静态 headers 中的 Authorization。\n * 返回 undefined 时跳过(用静态 headers 中的 Authorization)。\n */\n getAuthToken?: () => Promise<string | undefined>\n}\n\nexport class SseTransport implements McpTransport {\n private messageHandler: ((msg: JsonRpcResponse | JsonRpcNotification) => void) | null = null\n private closeHandler: ((reason?: string) => void) | null = null\n private abortController: AbortController | null = null\n private sessionId: string | null = null\n private closing = false\n private readonly baseUrl: string\n private readonly fetchImpl: FetchLike\n private readonly requestTimeoutMs: number\n private readonly headers: Record<string, string>\n private readonly getAuthToken?: () => Promise<string | undefined>\n\n constructor(url: string, options: SseTransportOptions = {}) {\n this.baseUrl = validateMcpUrl(url).href\n this.fetchImpl = options.fetchImpl ?? (globalThis.fetch as FetchLike)\n this.requestTimeoutMs = options.requestTimeoutMs ?? SSE_DEFAULT_TIMEOUT_MS\n this.headers = options.headers ?? {}\n this.getAuthToken = options.getAuthToken\n }\n\n /** 解析本次请求的 headers:静态 headers + 动态 Bearer(最高优先)+ 内置协议头覆盖。 */\n private async resolveHeaders(\n ...builtin: Record<string, string>[]\n ): Promise<Record<string, string>> {\n let merged = { ...this.headers }\n\n if (this.getAuthToken) {\n const token = await this.getAuthToken()\n if (token) {\n merged = { ...merged, Authorization: `Bearer ${token}` }\n }\n }\n\n for (const override of builtin) {\n merged = { ...merged, ...override }\n }\n return merged\n }\n\n /**\n * 认证中间层——401 时自动刷新 token 并重试一次(handle_401 → refresh → retry)。\n */\n private async fetchWithAuthRetry(\n method: 'GET' | 'POST',\n body?: string,\n signal?: AbortSignal,\n ): Promise<Response> {\n const builtin: Record<string, string> =\n method === 'GET'\n ? { Accept: 'text/event-stream' }\n : {\n 'Content-Type': 'application/json',\n ...(this.sessionId ? { 'Mcp-Session-Id': this.sessionId } : {}),\n }\n\n const headers = await this.resolveHeaders(builtin)\n const response = await this.fetchImpl(this.baseUrl, {\n method,\n headers,\n body,\n signal,\n })\n\n if (response.status === 401 && this.getAuthToken) {\n logger.debug('MCP 401 on %s %s — attempting token refresh and retry', method, this.baseUrl)\n\n const newToken = await this.getAuthToken()\n if (newToken) {\n const retryHeaders = await this.resolveHeaders(builtin)\n return this.fetchImpl(this.baseUrl, {\n method,\n headers: retryHeaders,\n body,\n signal,\n })\n }\n\n logger.warn('MCP token refresh returned no token for %s', this.baseUrl)\n }\n\n return response\n }\n\n async start(): Promise<void> {\n this.closing = false\n this.abortController = new AbortController()\n\n const response = await this.fetchWithAuthRetry('GET', undefined, this.abortController.signal)\n\n if (!response.ok) {\n if (response.status === 401) {\n // 协议层只报中性事实 + 错误码,**不**嵌宿主 CLI 文案\n // ('otto mcp login' 是 otto CLI 词汇,宿主知识)。actionable 指引由宿主据 code 渲染。\n throw new McpError(`MCP server '${this.baseUrl}' requires authentication.`, {\n code: 'MCP_UNAUTHORIZED',\n })\n }\n throw new Error(`MCP SSE connection failed: ${response.status} ${response.statusText}`)\n }\n\n const sessionId = response.headers.get('mcp-session-id')\n if (sessionId) {\n this.sessionId = sessionId\n }\n\n void this.consumeStream(response, true)\n }\n\n async send(message: JsonRpcRequest | JsonRpcNotification): Promise<void> {\n const perRequest = new AbortController()\n const timer = setTimeout(() => {\n perRequest.abort(new DOMException('MCP request timeout', 'TimeoutError'))\n }, this.requestTimeoutMs)\n timer.unref?.()\n const signal = this.abortController\n ? AbortSignal.any([this.abortController.signal, perRequest.signal])\n : perRequest.signal\n\n void this.fetchWithAuthRetry('POST', JSON.stringify(message), signal)\n .then(async (resp) => {\n clearTimeout(timer)\n if (!resp.ok) {\n if (resp.status === 401) {\n logger.warn('MCP SSE POST 401: authentication required for %s', this.baseUrl)\n // 中性 reason + 鉴权错误码(-32001);宿主据码渲染 CLI 指引。\n this.failRequest(message, -32001, `MCP server requires authentication.`)\n return\n }\n logger.warn('MCP SSE POST failed: %s', resp.status)\n this.failRequest(message, -32000, `MCP SSE POST failed: HTTP ${resp.status}`)\n return\n }\n\n const sid = resp.headers.get('mcp-session-id')\n if (sid) {\n this.sessionId = sid\n }\n\n const contentType = resp.headers.get('content-type') ?? ''\n if (contentType.includes('application/json')) {\n const body = (await resp.json()) as JsonRpcResponse | JsonRpcNotification\n this.messageHandler?.(body)\n } else if (contentType.includes('text/event-stream')) {\n void this.consumeStream(resp, false)\n }\n })\n .catch((err: Error) => {\n clearTimeout(timer)\n if (err.name !== 'AbortError') {\n logger.warn('MCP SSE send error: %s', err.message)\n this.failRequest(message, -32000, `MCP SSE send error: ${err.message}`)\n }\n })\n }\n\n private failRequest(\n message: JsonRpcRequest | JsonRpcNotification,\n code: number,\n reason: string,\n ): void {\n if ('id' in message && message.id !== undefined && message.id !== null) {\n this.messageHandler?.({ jsonrpc: JSON_RPC_VERSION, id: message.id, error: { code, message: reason } })\n }\n }\n\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void {\n this.messageHandler = handler\n }\n\n onClose(handler: (reason?: string) => void): void {\n this.closeHandler = handler\n }\n\n async close(): Promise<void> {\n this.closing = true\n this.abortController?.abort()\n this.abortController = null\n }\n\n private async consumeStream(response: Response, isPrimary: boolean): Promise<void> {\n const reader = response.body?.getReader()\n if (!reader) {\n if (isPrimary && !this.closing) {\n this.closeHandler?.('SSE stream had no readable body')\n }\n return\n }\n\n const decoder = new TextDecoder()\n const parser = createSseDataParser((data) => {\n const trimmed = data.trim()\n if (!trimmed || trimmed === '[DONE]') {\n return\n }\n try {\n const message = JSON.parse(trimmed) as JsonRpcResponse | JsonRpcNotification\n this.messageHandler?.(message)\n } catch {\n logger.debug('Ignoring non-JSON SSE data')\n }\n })\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n parser.feed(decoder.decode(value, { stream: true }))\n }\n parser.flush()\n if (isPrimary && !this.closing) {\n this.closeHandler?.('SSE stream ended')\n }\n } catch (err) {\n if ((err as Error).name !== 'AbortError') {\n logger.warn('MCP SSE stream error: %s', (err as Error).message)\n if (isPrimary && !this.closing) {\n this.closeHandler?.(`SSE stream error: ${(err as Error).message}`)\n }\n }\n } finally {\n reader.releaseLock()\n }\n }\n}\n\n/**\n * Streamable HTTP transport(MCP 2025-03-26 规范)——单端点、POST 驱动。\n *\n * 与 {@link SseTransport}(旧版 2024-11-05 HTTP+SSE:GET 开长连 SSE 流 + POST 发消息)的\n * 本质区别:**不**先 GET 开流。每条 JSON-RPC 消息由 POST 发到同一端点,服务端按需\n * 用 `application/json`(单条响应)或 `text/event-stream`(流式多条)回复,客户端据\n * `Content-Type` 分流消费。多数现代 MCP server 只实现本协议——对\n * 初始 GET 直接返回 JSON/关流,导致 SseTransport 报 \"SSE stream ended\"。\n *\n * 会话:`initialize` 响应的 `Mcp-Session-Id` 头被捕获并回填到后续 POST。通知(无 id)\n * 服务端通常回 202 空体,静默即可。start() 不建连——首个 initialize POST 才触发握手。\n */\nexport class StreamableHttpTransport implements McpTransport {\n private messageHandler: ((msg: JsonRpcResponse | JsonRpcNotification) => void) | null = null\n private closeHandler: ((reason?: string) => void) | null = null\n private abortController: AbortController | null = null\n private sessionId: string | null = null\n private closing = false\n private readonly baseUrl: string\n private readonly fetchImpl: FetchLike\n private readonly requestTimeoutMs: number\n private readonly headers: Record<string, string>\n private readonly getAuthToken?: () => Promise<string | undefined>\n\n constructor(url: string, options: SseTransportOptions = {}) {\n this.baseUrl = validateMcpUrl(url).href\n this.fetchImpl = options.fetchImpl ?? (globalThis.fetch as FetchLike)\n this.requestTimeoutMs = options.requestTimeoutMs ?? SSE_DEFAULT_TIMEOUT_MS\n this.headers = options.headers ?? {}\n this.getAuthToken = options.getAuthToken\n }\n\n private async resolveHeaders(\n ...builtin: Record<string, string>[]\n ): Promise<Record<string, string>> {\n let merged = { ...this.headers }\n if (this.getAuthToken) {\n const token = await this.getAuthToken()\n if (token) merged = { ...merged, Authorization: `Bearer ${token}` }\n }\n for (const override of builtin) merged = { ...merged, ...override }\n return merged\n }\n\n async start(): Promise<void> {\n // Streamable HTTP 无独立握手——首个 send(initialize) 即建立会话。start() 仅重置状态。\n this.closing = false\n this.abortController = new AbortController()\n }\n\n async send(message: JsonRpcRequest | JsonRpcNotification): Promise<void> {\n const perRequest = new AbortController()\n const timer = setTimeout(() => {\n perRequest.abort(new DOMException('MCP request timeout', 'TimeoutError'))\n }, this.requestTimeoutMs)\n timer.unref?.()\n const signal = this.abortController\n ? AbortSignal.any([this.abortController.signal, perRequest.signal])\n : perRequest.signal\n\n const builtin: Record<string, string> = {\n 'Content-Type': 'application/json',\n // 两种回复都可能——服务端据请求选 json(单条)或 event-stream(流式)\n Accept: 'application/json, text/event-stream',\n ...(this.sessionId ? { 'Mcp-Session-Id': this.sessionId } : {}),\n }\n const body = JSON.stringify(message)\n\n let response: Response\n try {\n response = await this.fetchImpl(this.baseUrl, {\n method: 'POST',\n headers: await this.resolveHeaders(builtin),\n body,\n signal,\n })\n if (response.status === 401 && this.getAuthToken) {\n const newToken = await this.getAuthToken()\n if (newToken) {\n response = await this.fetchImpl(this.baseUrl, {\n method: 'POST',\n headers: await this.resolveHeaders(builtin),\n body,\n signal,\n })\n }\n }\n } catch (err) {\n clearTimeout(timer)\n if ((err as Error).name !== 'AbortError' && !this.closing) {\n this.closeHandler?.(`HTTP POST failed: ${(err as Error).message}`)\n }\n return\n }\n\n // 会话 id 只在 initialize 响应下发一次,之后回填\n const sid = response.headers.get('mcp-session-id')\n if (sid) this.sessionId = sid\n\n if (!response.ok) {\n clearTimeout(timer)\n if (response.status === 401) {\n if (!this.closing) this.closeHandler?.('unauthorized')\n return\n }\n // 202 之外的非 2xx——通知场景服务端常回 202 空体,属正常\n if (response.status !== 202 && !this.closing) {\n this.closeHandler?.(`HTTP ${response.status} ${response.statusText}`)\n }\n return\n }\n\n const contentType = (response.headers.get('content-type') ?? '').toLowerCase()\n if (contentType.includes('text/event-stream')) {\n // 流式回复:SSE 帧逐条派发,读到流尾即完成(非传输断开,isPrimary=false 不触发重连)\n await this.consumeSseResponse(response)\n clearTimeout(timer)\n return\n }\n\n // 单条 JSON 回复(或 202 空体通知)\n clearTimeout(timer)\n const text = await response.text()\n if (!text.trim()) return\n try {\n const parsed = JSON.parse(text) as\n | JsonRpcResponse\n | JsonRpcNotification\n | Array<JsonRpcResponse | JsonRpcNotification>\n for (const m of Array.isArray(parsed) ? parsed : [parsed]) {\n this.messageHandler?.(m)\n }\n } catch (err) {\n logger.warn('MCP streamable-http non-JSON response body: %s', (err as Error).message)\n }\n }\n\n private async consumeSseResponse(response: Response): Promise<void> {\n const reader = response.body?.getReader()\n if (!reader) return\n const decoder = new TextDecoder()\n const parser = createSseDataParser((data) => {\n const trimmed = data.trim()\n if (!trimmed || trimmed === '[DONE]') return\n try {\n this.messageHandler?.(JSON.parse(trimmed) as JsonRpcResponse | JsonRpcNotification)\n } catch {\n logger.debug('Ignoring non-JSON SSE data')\n }\n })\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n parser.feed(decoder.decode(value, { stream: true }))\n }\n parser.flush()\n } catch (err) {\n if ((err as Error).name !== 'AbortError') {\n logger.warn('MCP streamable-http stream error: %s', (err as Error).message)\n }\n } finally {\n reader.releaseLock()\n }\n }\n\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void {\n this.messageHandler = handler\n }\n\n onClose(handler: (reason?: string) => void): void {\n this.closeHandler = handler\n }\n\n async close(): Promise<void> {\n this.closing = true\n this.abortController?.abort()\n this.abortController = null\n }\n}\n\n// transport 工厂族(createStdioTransport/createSseTransport/createHttpTransport)已删——\n// 零生产消费,mcp-client 直接 new StdioTransport/SseTransport,仅测试曾用工厂便捷构造(RFC-074 M10)。\n","import { McpError, createLogger } from '@x-otto/shared'\nimport { StdioTransport, SseTransport, StreamableHttpTransport } from './transport'\n\nimport type { McpTransport, ProcessSpawnFactory } from './transport'\nimport { JSON_RPC_VERSION } from './types'\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n JsonRpcNotification,\n McpInitializeResult,\n McpToolDefinition,\n McpToolCallParams,\n McpToolCallResult,\n McpServerConfig,\n McpClientStatus,\n McpServerCapabilities,\n McpResource,\n McpResourceContents,\n McpPrompt,\n McpPromptMessage,\n} from './types'\n\nconst logger = createLogger('@x-otto/mcp:client')\n\nconst MCP_PROTOCOL_VERSION = '2024-11-05'\n\n/**\n * RFC-165 D8/H2:断连分类——区分\"预期内的显式断开\"(`kind: 'manual'`,如\n * `McpClient.disconnect()` 被调用)与\"传输层意外关闭\"(`kind: 'crash'`,如子进程异常退出、\n * SSE 流出错、重连耗尽)。这是最小可行区分(不深入解析子进程 exit code),供上层\n * (`McpManager`/`McpController`)据此派生更细的语义(如\"用户经 /extension 禁用\" vs\n * \"server 真的崩了\"——那一层区分需要 disable 调用方上下文,留给 `McpManager`/\n * `McpController` 在 `kind: 'manual'` 基础上进一步标注,见 D8 备注)。\n */\nexport interface DisconnectInfo {\n reason?: string\n kind: 'manual' | 'crash'\n}\nconst MCP_CLIENT_NAME = 'otto-coding'\nconst MCP_CLIENT_VERSION = '0.0.1'\n\ninterface PendingRequest {\n resolve: (value: unknown) => void\n reject: (reason: Error) => void\n timer: ReturnType<typeof setTimeout>\n}\n\nexport interface McpClientOptions {\n /** 请求超时(毫秒),默认 30000 */\n requestTimeoutMs?: number\n /** 自动重连(默认 true) */\n autoReconnect?: boolean\n /** 最大重连次数(默认 3) */\n maxReconnectAttempts?: number\n /** 重连退避基数(毫秒,默认 500;第 n 次=base·2^(n-1))。测试可设小值。 */\n reconnectDelayMs?: number\n /** 注入 transport 工厂(测试用)。缺省按 config 构造 stdio/sse。 */\n transportFactory?: () => McpTransport\n /**\n * 注入进程 spawn 工厂(RFC-085 M2):stdio transport 用它把 MCP 子进程纳入 ProcessRuntime。\n * 缺省时 StdioTransport 走向后兼容私有 spawn(仍 buildAllowedEnv + detached)。\n */\n processSpawnFactory?: ProcessSpawnFactory\n /**\n * 动态 Bearer token provider(OAuth 2.1)。\n * 每次 HTTP 请求前调用,返回的 access token 注入 Authorization 头。\n * 由调用方(McpManager 或外部)注入 TokenProvider.getAccessToken。\n */\n getAuthToken?: () => Promise<string | undefined>\n}\n\nexport class McpClient {\n readonly serverName: string\n private transport: McpTransport | null = null\n private config: McpServerConfig\n private options: McpClientOptions\n\n private status: McpClientStatus = 'disconnected'\n private serverInfo: McpInitializeResult | null = null\n private capabilities: McpServerCapabilities | null = null\n private tools: McpToolDefinition[] = []\n private resources: McpResource[] = []\n private prompts: McpPrompt[] = []\n private nextRequestId = 1\n private pending = new Map<number | string, PendingRequest>()\n private closing = false\n private reconnectAttempts = 0\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null\n private toolsChangedCallback: (() => void) | null = null\n private resourcesChangedCallback: (() => void) | null = null\n private disconnectedCallback: ((info: DisconnectInfo) => void) | null = null\n /** RFC-165 D9:通用 notification 订阅——server 发出的、未被上面专用处理的 method\n * (如 `backend/state`)经此回调透传给上层,不再被 `logger.debug` 静默丢弃。\n * 对全部 notification(含已被专用 switch-case 处理的)都会调用,让消费方可按需扩展。 */\n private notificationCallback: ((method: string, params?: Record<string, unknown>) => void) | null = null\n\n constructor(serverName: string, config: McpServerConfig, options: McpClientOptions = {}) {\n this.serverName = serverName\n this.config = config\n this.options = options\n }\n\n getStatus(): McpClientStatus {\n return this.status\n }\n\n getConfig(): McpServerConfig {\n return this.config\n }\n\n getTools(): McpToolDefinition[] {\n return this.tools\n }\n\n getResources(): McpResource[] {\n return this.resources\n }\n\n getPrompts(): McpPrompt[] {\n return this.prompts\n }\n\n getCapabilities(): McpServerCapabilities | null {\n return this.capabilities\n }\n\n getServerInfo(): McpInitializeResult | null {\n return this.serverInfo\n }\n\n onToolsChanged(callback: () => void): void {\n this.toolsChangedCallback = callback\n }\n\n onResourcesChanged(callback: () => void): void {\n this.resourcesChangedCallback = callback\n }\n\n onDisconnected(callback: (info: DisconnectInfo) => void): void {\n this.disconnectedCallback = callback\n }\n\n /**\n * RFC-165 D9/H1:通用 notification 订阅——面向未被 `handleNotification` 专用 switch-case\n * 处理的 method(如插件 backend facet 推送的 `backend/state`)。已处理的 method\n * (`tools/list_changed`/`resources/list_changed`)也会触发本回调,消费方按 method 自行过滤。\n */\n onNotification(callback: (method: string, params?: Record<string, unknown>) => void): void {\n this.notificationCallback = callback\n }\n\n async connect(): Promise<void> {\n if (this.status === 'ready') {\n return\n }\n\n this.closing = false\n this.status = 'connecting'\n\n try {\n await this.establish()\n this.reconnectAttempts = 0\n this.status = 'ready'\n } catch (err) {\n this.status = 'error'\n const message = err instanceof Error ? err.message : String(err)\n logger.error('MCP server \"%s\" connection failed: %s', this.serverName, message)\n // 保留底层鉴权错误码(MCP_UNAUTHORIZED),不一律压成 MCP_CONNECT_ERROR——\n // 否则宿主无从据码渲染 actionable 指引('otto mcp login')。\n const code =\n err instanceof McpError && err.code === 'MCP_UNAUTHORIZED'\n ? 'MCP_UNAUTHORIZED'\n : 'MCP_CONNECT_ERROR'\n throw new McpError(`Failed to connect to MCP server \"${this.serverName}\": ${message}`, {\n code,\n cause: err instanceof Error ? err : undefined,\n })\n }\n }\n\n private async establish(): Promise<void> {\n const transport = this.createTransport()\n this.transport = transport\n transport.onMessage((msg) => this.handleMessage(msg))\n transport.onClose((reason) => {\n if (this.transport !== transport) {\n return\n }\n this.handleTransportClose(reason)\n })\n\n try {\n await transport.start()\n\n this.serverInfo = await this.request<McpInitializeResult>('initialize', {\n protocolVersion: MCP_PROTOCOL_VERSION,\n capabilities: { roots: { listChanged: false } },\n clientInfo: { name: MCP_CLIENT_NAME, version: MCP_CLIENT_VERSION },\n })\n\n this.capabilities = this.serverInfo.capabilities\n this.notify('notifications/initialized')\n\n logger.info(\n 'MCP server \"%s\" connected: %s (protocol %s)',\n this.serverName,\n this.serverInfo.serverInfo.name,\n this.serverInfo.protocolVersion,\n )\n\n await this.refreshTools()\n if (this.capabilities?.resources) {\n await this.refreshResources()\n }\n if (this.capabilities?.prompts) {\n await this.refreshPrompts()\n }\n } catch (err) {\n if (this.transport === transport) {\n this.transport = null\n }\n await transport.close().catch(() => undefined)\n throw err\n }\n }\n\n private handleTransportClose(reason?: string): void {\n if (this.closing) {\n return\n }\n this.failPending(\n new McpError(`MCP transport closed: ${reason ?? 'unknown'}`, { code: 'MCP_DISCONNECTED' }),\n )\n\n // B10:初次连接期(status==='connecting')transport 关闭,交给 establish 的 catch + connect() 的\n // throw 上报,此处不重连、不改状态、不回调——否则被丢弃的 client(从未入池)仍武装重连定时器,\n // 反复 spawn 子进程/重开流(孤儿重连循环)。只有已建立(ready)或重连中(reconnecting)的掉线才自愈。\n if (this.status === 'connecting') {\n return\n }\n\n const autoReconnect = this.options.autoReconnect ?? true\n const maxAttempts = this.options.maxReconnectAttempts ?? 3\n if (!autoReconnect || this.reconnectAttempts >= maxAttempts) {\n this.status = 'disconnected'\n this.transport = null\n // transport 非预期关闭(非 disconnect() 主动调用)——分类为 crash(D8/H2)。\n this.disconnectedCallback?.({ reason, kind: 'crash' })\n return\n }\n this.scheduleReconnect()\n }\n\n private scheduleReconnect(): void {\n this.reconnectAttempts++\n this.status = 'reconnecting'\n const base = this.options.reconnectDelayMs ?? 500\n const delay = base * 2 ** (this.reconnectAttempts - 1)\n logger.info(\n 'MCP server \"%s\" reconnecting (attempt %d) in %dms',\n this.serverName,\n this.reconnectAttempts,\n delay,\n )\n this.reconnectTimer = setTimeout(() => {\n void this.attemptReconnect()\n }, delay)\n }\n\n private async attemptReconnect(): Promise<void> {\n if (this.closing) {\n return\n }\n try {\n // 防御性:先 close 旧 transport(stdio 下 child 通常已 exit,但仍该做)\n if (this.transport) {\n await this.transport.close().catch(() => undefined)\n }\n this.transport = null\n await this.establish()\n this.reconnectAttempts = 0\n this.status = 'ready'\n logger.info('MCP server \"%s\" reconnected', this.serverName)\n } catch (err) {\n logger.warn(\n 'MCP server \"%s\" reconnect attempt %d failed: %s',\n this.serverName,\n this.reconnectAttempts,\n (err as Error).message,\n )\n const maxAttempts = this.options.maxReconnectAttempts ?? 3\n if (this.reconnectAttempts >= maxAttempts) {\n this.status = 'error'\n this.transport = null\n // 重连耗尽——仍是非预期关闭的延续,分类为 crash(D8/H2)。\n this.disconnectedCallback?.({\n reason: `reconnect failed after ${maxAttempts} attempts`,\n kind: 'crash',\n })\n } else {\n this.scheduleReconnect()\n }\n }\n }\n\n private failPending(error: Error): void {\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer)\n pending.reject(error)\n this.pending.delete(id)\n }\n }\n\n async refreshTools(): Promise<McpToolDefinition[]> {\n const result = await this.request<{ tools: McpToolDefinition[] }>('tools/list')\n this.tools = result.tools ?? []\n\n logger.debug(\n 'MCP server \"%s\" provides %d tools: %s',\n this.serverName,\n this.tools.length,\n this.tools.map((t) => t.name).join(', '),\n )\n\n return this.tools\n }\n\n async refreshResources(): Promise<McpResource[]> {\n try {\n const result = await this.request<{ resources: McpResource[] }>('resources/list')\n this.resources = result.resources ?? []\n\n logger.debug('MCP server \"%s\" provides %d resources', this.serverName, this.resources.length)\n } catch (err) {\n logger.debug(\n 'MCP server \"%s\" resources/list failed: %s',\n this.serverName,\n (err as Error).message,\n )\n this.resources = []\n }\n\n return this.resources\n }\n\n async readResource(uri: string): Promise<McpResourceContents[]> {\n const result = await this.request<{ contents: McpResourceContents[] }>('resources/read', {\n uri,\n })\n return result.contents ?? []\n }\n\n async refreshPrompts(): Promise<McpPrompt[]> {\n try {\n const result = await this.request<{ prompts: McpPrompt[] }>('prompts/list')\n this.prompts = result.prompts ?? []\n\n logger.debug('MCP server \"%s\" provides %d prompts', this.serverName, this.prompts.length)\n } catch (err) {\n logger.debug(\n 'MCP server \"%s\" prompts/list failed: %s',\n this.serverName,\n (err as Error).message,\n )\n this.prompts = []\n }\n\n return this.prompts\n }\n\n async getPrompt(\n name: string,\n args?: Record<string, string>,\n ): Promise<{ description?: string; messages: McpPromptMessage[] }> {\n const result = await this.request<{ description?: string; messages: McpPromptMessage[] }>(\n 'prompts/get',\n {\n name,\n arguments: args,\n },\n )\n return result\n }\n\n async callTool(params: McpToolCallParams, signal?: AbortSignal): Promise<McpToolCallResult> {\n if (this.status !== 'ready') {\n throw new McpError(`MCP server \"${this.serverName}\" is not ready (status: ${this.status})`, {\n code: 'MCP_NOT_READY',\n })\n }\n\n const result = await this.request<McpToolCallResult>(\n 'tools/call',\n params as unknown as Record<string, unknown>,\n signal,\n )\n return result\n }\n\n /**\n * RFC-165 T6:通用请求出口——镜像底层私有 `request`,但方法名不含 \"tool\" 语义\n * (避免与模型工具调用面混淆,对齐 D3 概念分离:即使是客户端内部 API 命名也不应让\n * `backend/*` 调用看起来像\"调用了一个 tool\")。供 `PanelBackendController` 发送\n * `backend/call`/`backend/cancel`/`backend/getState` 等面板专用请求。\n *\n * 刻意不要求 `status === 'ready'`(区别于 `callTool`)——D7 降级启动场景下 backend facet\n * 恒可用,但 `mcp` facet 的业务工具可能未注册,`ready` 判断不应耦合 `mcp` facet 健康度;\n * 断连场景由调用方(`PanelBackendController`)经 `McpClient.getStatus()` 感知并拒绝新请求。\n */\n async callRaw(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n return this.request<unknown>(method, params, signal)\n }\n\n async disconnect(): Promise<void> {\n this.closing = true\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n\n if (this.transport) {\n try {\n await this.transport.close()\n } catch (err) {\n logger.debug('Error closing MCP transport: %s', (err as Error).message)\n }\n this.transport = null\n }\n\n this.failPending(new McpError('MCP client disconnected', { code: 'MCP_DISCONNECTED' }))\n\n this.status = 'disconnected'\n this.tools = []\n this.resources = []\n this.prompts = []\n this.serverInfo = null\n this.capabilities = null\n // 调用方主动发起(如 McpManager.disconnect()/setServerEnabled(false))——分类为 manual(D8/H2)。\n this.disconnectedCallback?.({ reason: 'client disconnect', kind: 'manual' })\n }\n\n private createTransport(): McpTransport {\n if (this.options.transportFactory) {\n return this.options.transportFactory()\n }\n\n const { type, url, command } = this.config\n // `http`(含 type 省略但给了 url)= Streamable HTTP(2025-03-26,POST 驱动单端点);\n // `sse` = 旧版 HTTP+SSE(2024-11-05,GET 开长连流)。二者线协议不同,不可混用——\n // 现代 MCP server 普遍只实现前者,此前统一走 SseTransport 会 \"SSE stream ended\"。\n const wantsStreamableHttp = type === 'http' || (type === undefined && url !== undefined)\n const wantsSse = type === 'sse'\n const wantsStdio = type === 'stdio' || (type === undefined && command !== undefined)\n\n if (wantsStreamableHttp || wantsSse) {\n if (!url) {\n throw new McpError(`MCP server \"${this.serverName}\" transport '${type}' requires a url`, {\n code: 'MCP_CONFIG_ERROR',\n })\n }\n const transportOpts = {\n requestTimeoutMs: this.config.requestTimeoutMs ?? this.options.requestTimeoutMs,\n headers: this.config.headers,\n getAuthToken: this.options.getAuthToken,\n }\n return wantsSse\n ? new SseTransport(url, transportOpts)\n : new StreamableHttpTransport(url, transportOpts)\n }\n\n if (wantsStdio) {\n if (!command) {\n throw new McpError(`MCP server \"${this.serverName}\" transport 'stdio' requires a command`, {\n code: 'MCP_CONFIG_ERROR',\n })\n }\n return new StdioTransport(\n command,\n this.config.args ?? [],\n this.config.env ?? {},\n this.options.processSpawnFactory,\n )\n }\n\n throw new McpError(`MCP server \"${this.serverName}\" has no command or url configured`, {\n code: 'MCP_CONFIG_ERROR',\n })\n }\n\n private request<T>(\n method: string,\n params?: Record<string, unknown>,\n signal?: AbortSignal,\n ): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new McpError(`MCP request \"${method}\" aborted`, { code: 'MCP_ABORTED' }))\n return\n }\n const id = this.nextRequestId++\n const timeoutMs = this.config.requestTimeoutMs ?? this.options.requestTimeoutMs ?? 30_000\n\n let abortHandler: (() => void) | undefined\n const detach = (): void => {\n if (abortHandler && signal) signal.removeEventListener('abort', abortHandler)\n }\n\n const timer = setTimeout(() => {\n this.pending.delete(id)\n detach()\n reject(\n new McpError(`MCP request \"${method}\" timed out after ${timeoutMs}ms`, {\n code: 'MCP_TIMEOUT',\n }),\n )\n }, timeoutMs)\n\n this.pending.set(id, {\n resolve: (v: unknown) => {\n detach()\n ;(resolve as (value: unknown) => void)(v)\n },\n reject: (e: unknown) => {\n detach()\n reject(e)\n },\n timer,\n })\n\n if (signal) {\n abortHandler = () => {\n const p = this.pending.get(id)\n if (!p) return\n clearTimeout(p.timer)\n this.pending.delete(id)\n detach()\n reject(new McpError(`MCP request \"${method}\" aborted`, { code: 'MCP_ABORTED' }))\n }\n signal.addEventListener('abort', abortHandler, { once: true })\n }\n\n const message: JsonRpcRequest = {\n jsonrpc: JSON_RPC_VERSION,\n id,\n method,\n ...(params !== undefined && { params }),\n }\n\n try {\n this.transport!.send(message)\n } catch (err) {\n clearTimeout(timer)\n this.pending.delete(id)\n detach()\n reject(err)\n }\n })\n }\n\n private notify(method: string, params?: Record<string, unknown>): void {\n const message: JsonRpcNotification = {\n jsonrpc: JSON_RPC_VERSION,\n method,\n ...(params !== undefined && { params }),\n }\n\n try {\n this.transport!.send(message)\n } catch (err) {\n logger.debug('Failed to send MCP notification \"%s\": %s', method, (err as Error).message)\n }\n }\n\n private handleMessage(message: JsonRpcResponse | JsonRpcNotification): void {\n if (!('id' in message) || message.id === undefined) {\n this.handleNotification(message as JsonRpcNotification)\n return\n }\n\n const response = message as JsonRpcResponse\n const pending = this.pending.get(response.id!)\n if (!pending) {\n logger.debug('Received response for unknown request id: %s', response.id)\n return\n }\n\n clearTimeout(pending.timer)\n this.pending.delete(response.id!)\n\n if (response.error) {\n pending.reject(\n new McpError(`MCP error: ${response.error.message} (code: ${response.error.code})`, {\n code: 'MCP_SERVER_ERROR',\n }),\n )\n } else {\n pending.resolve(response.result)\n }\n }\n\n private handleNotification(notification: JsonRpcNotification): void {\n switch (notification.method) {\n case 'notifications/tools/list_changed':\n logger.info('MCP server \"%s\" notified tools list changed', this.serverName)\n void this.refreshTools().then(() => {\n this.toolsChangedCallback?.()\n })\n break\n\n case 'notifications/resources/list_changed':\n logger.info('MCP server \"%s\" notified resources list changed', this.serverName)\n void this.refreshResources().then(() => {\n this.resourcesChangedCallback?.()\n })\n break\n\n default:\n logger.debug('MCP notification: %s', notification.method)\n }\n\n // RFC-165 D9/H1:通用订阅——所有 notification(含上面已被专用 case 处理的)都转发,\n // 不再对未知 method 静默丢弃。让 backend/state 这类插件自定义 notification 能被\n // 上层(McpManager → PanelBackendController)消费。\n this.notificationCallback?.(notification.method, notification.params)\n }\n}\n\nexport function createMcpClient(\n serverName: string,\n config: McpServerConfig,\n options?: McpClientOptions,\n): McpClient {\n return new McpClient(serverName, config, options)\n}\n","import { z } from 'zod'\nimport { createLogger, probeImageDimensions, API_IMAGE_DIMENSION_LIMIT_PX, PROVIDER_IMAGE_INLINE_MAX_BYTES } from '@x-otto/shared'\nimport type { AgentToolDefinition, ToolResult } from '@x-otto/interchange'\nimport type { McpClient } from './mcp-client'\nimport type { McpToolDefinition, McpJsonSchemaProperty, McpContent } from './types'\n\nconst logger = createLogger('@x-otto/mcp:adapter')\n\nfunction enumToZod(values: unknown[]): z.ZodType | null {\n if (values.length === 0) {\n return null\n }\n if (values.every((v) => typeof v === 'string')) {\n return z.enum(values as [string, ...string[]])\n }\n const literals = values\n .filter((v): v is string | number | boolean => {\n const t = typeof v\n return t === 'string' || t === 'number' || t === 'boolean'\n })\n .map((v) => z.literal(v))\n if (literals.length === 0) {\n return null\n }\n if (literals.length === 1) {\n return literals[0]!\n }\n return z.union(literals as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]])\n}\n\n/**\n * 将 MCP JSON Schema property → Zod Schema\n */\nexport function jsonSchemaPropertyToZod(prop: McpJsonSchemaProperty): z.ZodType {\n // B11(评审建议优化):复杂 schema 显式降级——oneOf/anyOf/allOf/$ref 不支持时\n // 落到 z.unknown()(丢约束但**有日志**,此前静默退化难排查)。$ref 尽力局部解析\n // (schema 内嵌 definitions/`$defs` 引用),解析失败才降级。\n if (prop.oneOf && Array.isArray(prop.oneOf) && prop.oneOf.length > 0) {\n const subs = prop.oneOf.map((s) => jsonSchemaPropertyToZod(s as McpJsonSchemaProperty))\n return (prop.description ? z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]).describe(prop.description) : z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]))\n }\n if (prop.anyOf && Array.isArray(prop.anyOf) && prop.anyOf.length > 0) {\n const subs = prop.anyOf.map((s) => jsonSchemaPropertyToZod(s as McpJsonSchemaProperty))\n return (prop.description ? z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]).describe(prop.description) : z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]))\n }\n if (prop.allOf && Array.isArray(prop.allOf) && prop.allOf.length > 0) {\n // allOf 语义 = 全约束交叠——近似取最后一段(MCP 实践中 allOf 罕见,降级可接受但记日志)。\n console.warn('[mcp-tool-adapter] allOf schema degraded to last sub-schema (intersection unsupported)')\n return jsonSchemaPropertyToZod(prop.allOf[prop.allOf.length - 1] as McpJsonSchemaProperty)\n }\n if (typeof prop.$ref === 'string') {\n const resolved = resolveLocalRef(prop.$ref, prop)\n if (resolved) return jsonSchemaPropertyToZod(resolved)\n console.warn(`[mcp-tool-adapter] unresolvable $ref \"${prop.$ref}\" degraded to z.unknown()`)\n return z.unknown()\n }\n\n // enum 覆盖所有类型(MCP 规范允许 object/array 的 enum,此前只处理了标量)\n if (Array.isArray(prop.enum)) {\n const enumSchema = enumToZod(prop.enum)\n if (enumSchema) {\n return prop.description ? enumSchema.describe(prop.description) : enumSchema\n }\n }\n\n switch (prop.type) {\n case 'string':\n return prop.description ? z.string().describe(prop.description) : z.string()\n\n case 'number':\n case 'integer': {\n // LLM 常把快照提取的数字参数写成字符串(如 index:\"3856\")。\n // z.preprocess 在 safeParse 前强制转换,避免 expected number, received string。\n const base = prop.type === 'integer' ? z.number().int() : z.number()\n const coerced = z.preprocess((v) => (typeof v === 'string' ? Number(v) : v), base)\n return prop.description ? coerced.describe(prop.description) : coerced\n }\n\n case 'boolean':\n return prop.description ? z.boolean().describe(prop.description) : z.boolean()\n\n case 'array': {\n const itemSchema = prop.items ? jsonSchemaPropertyToZod(prop.items) : z.unknown()\n return prop.description ? z.array(itemSchema).describe(prop.description) : z.array(itemSchema)\n }\n\n case 'object': {\n if (prop.properties) {\n const shape: Record<string, z.ZodType> = {}\n const requiredSet = new Set(prop.required ?? [])\n\n for (const [key, value] of Object.entries(prop.properties)) {\n const fieldSchema = jsonSchemaPropertyToZod(value)\n shape[key] = requiredSet.has(key) ? fieldSchema : fieldSchema.optional()\n }\n\n return prop.description ? z.object(shape).describe(prop.description) : z.object(shape)\n }\n return prop.description\n ? z.record(z.string(), z.unknown()).describe(prop.description)\n : z.record(z.string(), z.unknown())\n }\n\n default: {\n // B11:未识别类型显式降级(此前静默 z.unknown(),不可诊断)\n console.warn(`[mcp-tool-adapter] unsupported JSON schema type \"${prop.type}\" degraded to z.unknown()`)\n return z.unknown()\n }\n }\n}\n\n/**\n * B11:局部 $ref 解析——`#/definitions/Name` / `#/$defs/Name` 引用当前 schema 内嵌\n * definitions/$defs。MCP 服务端惯用内嵌 definitions 做复用;跨文件/远程引用不支持\n * (显式降级,调用方已记日志)。\n */\nfunction resolveLocalRef(ref: string, prop: McpJsonSchemaProperty): McpJsonSchemaProperty | undefined {\n const m = /^#\\/(?:definitions|\\$defs)\\/([^/]+)$/.exec(ref)\n if (!m) return undefined\n const name = m[1]!.replace(/~1/g, '/').replace(/~0/g, '~')\n const defs = (prop as { definitions?: Record<string, unknown>; $defs?: Record<string, unknown> })\n const def = defs.definitions?.[name] ?? defs.$defs?.[name]\n return def as McpJsonSchemaProperty | undefined\n}\n\n/**\n * 将 MCP tool inputSchema → Zod schema\n */\nexport function mcpSchemaToZod(tool: McpToolDefinition): z.ZodType {\n const props = tool.inputSchema.properties\n if (!props || Object.keys(props).length === 0) {\n return z.object({})\n }\n\n const shape: Record<string, z.ZodType> = {}\n const requiredSet = new Set(tool.inputSchema.required ?? [])\n\n for (const [key, value] of Object.entries(props)) {\n const fieldSchema = jsonSchemaPropertyToZod(value)\n shape[key] = requiredSet.has(key) ? fieldSchema : fieldSchema.optional()\n }\n\n return z.object(shape)\n}\n\n/** MCP 工具产出图片体积硬上限——跨包单源 @x-otto/shared PROVIDER_IMAGE_INLINE_MAX_BYTES(C 组收敛)。\n * 语义:字节长度 > 上限才拒绝,恰好等于上限放行。 */\nconst MAX_MCP_IMAGE_BYTES = PROVIDER_IMAGE_INLINE_MAX_BYTES\n\nfunction base64ByteLength(b64: string): number {\n const padding = b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0\n return Math.max(0, Math.floor((b64.length * 3) / 4) - padding)\n}\n\nfunction resourceToContent(resource: {\n uri: string\n text?: string\n blob?: string\n mimeType?: string\n}): ToolResult['content'][number] {\n if (resource.text !== undefined) {\n return { type: 'text', text: resource.text }\n }\n if (resource.blob !== undefined) {\n if (resource.mimeType?.startsWith('image/') && resource.mimeType !== 'image/svg+xml') {\n const bytes = base64ByteLength(resource.blob)\n if (bytes > MAX_MCP_IMAGE_BYTES) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${bytes} bytes exceeds ${MAX_MCP_IMAGE_BYTES} byte limit — ask the tool to capture a smaller region/resolution]`,\n }\n }\n const dims = probeImageDimensions(resource.blob, resource.mimeType)\n // 2000px many-image 规则由 work-loop D6 cap(≤20 张/请求)保证不适用,\n // 此处只拦 API 必拒的 >8000px。原 2000px 会把 Retina 截图(2560px+)拒成占位文本。\n if (dims && (dims.width > API_IMAGE_DIMENSION_LIMIT_PX || dims.height > API_IMAGE_DIMENSION_LIMIT_PX)) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${dims.width}x${dims.height}px exceeds Anthropic per-image dimension hard limit of ${API_IMAGE_DIMENSION_LIMIT_PX}px — ask the tool to capture a smaller region or lower resolution]`,\n }\n }\n return {\n type: 'image',\n mime: resource.mimeType,\n source: `data:${resource.mimeType};base64,${resource.blob}`,\n }\n }\n const mimeNote = resource.mimeType ? ` (${resource.mimeType})` : ''\n return {\n type: 'text',\n text: `[Resource: ${resource.uri}${mimeNote}, ${base64ByteLength(resource.blob)} bytes binary]`,\n }\n }\n return { type: 'text', text: `[Resource: ${resource.uri}]` }\n}\n\n/**\n * 将 MCP content → ToolResult content\n */\nexport function mcpContentToToolContent(content: McpContent[]): ToolResult['content'] {\n return content.map((item) => {\n switch (item.type) {\n case 'text':\n return { type: 'text' as const, text: item.text }\n case 'image':\n if (!item.data) {\n return { type: 'text' as const, text: '[image unavailable: no data]' }\n }\n {\n const bytes = base64ByteLength(item.data)\n if (bytes > MAX_MCP_IMAGE_BYTES) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${bytes} bytes exceeds ${MAX_MCP_IMAGE_BYTES} byte limit — ask the tool to capture a smaller region/resolution]`,\n }\n }\n const dims = probeImageDimensions(item.data, item.mimeType ?? '')\n // 2000px many-image 规则由 work-loop D6 cap(≤20 张/请求)保证不适用,\n // 此处只拦 API 必拒的 >8000px。原 2000px 会把 Retina 截图(2560px+)拒成占位文本。\n if (dims && (dims.width > API_IMAGE_DIMENSION_LIMIT_PX || dims.height > API_IMAGE_DIMENSION_LIMIT_PX)) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${dims.width}x${dims.height}px exceeds Anthropic per-image dimension hard limit of ${API_IMAGE_DIMENSION_LIMIT_PX}px — ask the tool to capture a smaller region or lower resolution]`,\n }\n }\n }\n return {\n type: 'image' as const,\n mime: item.mimeType,\n source: `data:${item.mimeType};base64,${item.data}`,\n }\n case 'resource':\n return resourceToContent(item.resource)\n default:\n return { type: 'text' as const, text: JSON.stringify(item) }\n }\n })\n}\n\n/**\n * 从 MCP 工具 inputSchema 推断\"文件系统路径\"参数名(endgame review:MCP 工具此前无 pathParams →\n * permission-guard 的 path 规则(file-guard 写保护 / 凭证读侧 deny / sandbox)全不命中 →\n * `mcp__fs__read_file{path:~/.ssh/id_rsa}` 零守卫读出密钥)。声明 pathParams 后,执行器经\n * resolveToolPaths 把这些 string 实参作为路径喂给所有路径守卫。\n *\n * 名字启发式刻意**宽**(宁可多声明几个无害的——非路径串过守卫不命中即放行):含\n * path/file/dir/folder 子串,或精确 source/destination/dest/src/cwd/location。刻意**排除** uri/url\n * (多为网络地址,按 fs 路径解析会误导 sandbox 写评估)。仅取 string 类型属性。\n */\nfunction looksLikePathParam(name: string): boolean {\n const n = name.toLowerCase().replace(/[_\\s-]/g, '')\n if (['source', 'destination', 'dest', 'src', 'cwd', 'location'].includes(n)) return true\n return n.includes('path') || n.includes('file') || n.includes('dir') || n.includes('folder')\n}\n\nexport function deriveMcpPathParams(toolDef: McpToolDefinition): string[] {\n const props = toolDef.inputSchema.properties\n if (!props) return []\n const out: string[] = []\n for (const [key, prop] of Object.entries(props)) {\n const t = prop.type\n if ((t === 'string' || t === undefined) && looksLikePathParam(key)) {\n out.push(key)\n }\n }\n return out\n}\n\n/**\n * 为单个 MCP tool 创建 AgentTool 适配器\n * 工具名格式: mcp__{serverName}__{toolName}\n */\nexport function createMcpToolAdapter(\n client: McpClient,\n toolDef: McpToolDefinition,\n serverName: string,\n): AgentToolDefinition {\n const qualifiedName = `mcp__${serverName}__${toolDef.name}`\n const description = toolDef.description\n ? `[MCP: ${serverName}] ${toolDef.description}`\n : `[MCP: ${serverName}] ${toolDef.name}`\n\n const parameters = mcpSchemaToZod(toolDef)\n const pathParams = deriveMcpPathParams(toolDef)\n // MCP `annotations.readOnlyHint` → AgentTool.readonly:work-loop 据 readonly 把只读调用分入并行桶\n // 此前适配器不传 readonly → 所有外部 MCP 工具恒串行。仅 true 才置位(保守)。\n const readonly = toolDef.annotations?.readOnlyHint === true\n\n return {\n name: qualifiedName,\n description,\n parameters,\n ...(pathParams.length > 0 ? { pathParams } : {}),\n ...(readonly ? { readonly: true } : {}),\n exposure: 'deferred',\n\n async execute({ params, signal }): Promise<ToolResult> {\n logger.debug('Calling MCP tool %s on server %s', toolDef.name, serverName)\n\n const result = await client.callTool(\n {\n name: toolDef.name,\n arguments: params as Record<string, unknown>,\n },\n signal,\n )\n\n const content = mcpContentToToolContent(result.content)\n\n return {\n content: content.length > 0 ? content : [{ type: 'text', text: '(empty result)' }],\n isError: result.isError ?? false,\n details: {\n mcpServer: serverName,\n mcpTool: toolDef.name,\n },\n }\n },\n }\n}\n\n/**\n * 为一个 MCP Client 的所有工具创建适配器\n */\nexport function createMcpToolAdapters(client: McpClient, serverName: string): AgentToolDefinition[] {\n const tools = client.getTools()\n return tools.map((toolDef) => createMcpToolAdapter(client, toolDef, serverName))\n}\n","export function parseWwwAuthenticate(header: string): string | undefined {\n const parts = header.split(',').map((p) => p.trim())\n const bearer = parts.find(\n (p) => p.toLowerCase().startsWith('bearer ') || p.toLowerCase() === 'bearer',\n )\n if (!bearer) return undefined\n\n const match = bearer.match(/metadata_url\\s*=\\s*\"([^\"]+)\"/i)\n return match?.[1]\n}\n\nexport interface ProtectedResourceMetadata {\n /** 资源标识符(即 MCP server 的 canonical URL)。 */\n resource: string\n /** 授权服务器 issuer URL 列表。 */\n authorization_servers?: string[]\n /** 支持的 Bearer 方法。 */\n bearer_methods_supported?: string[]\n /** 可读的资源文档 URL。 */\n resource_documentation?: string\n}\n\nexport async function fetchProtectedResourceMetadata(\n metadataUrl: string,\n fetchImpl: typeof globalThis.fetch = fetch,\n): Promise<ProtectedResourceMetadata> {\n const res = await fetchImpl(metadataUrl, {\n headers: { Accept: 'application/json' },\n })\n if (!res.ok) {\n throw new Error(\n `Failed to fetch protected resource metadata from ${metadataUrl}: ${res.status}`,\n )\n }\n return (await res.json()) as ProtectedResourceMetadata\n}\n\nexport interface AuthorizationServerMetadata {\n issuer: string\n authorization_endpoint: string\n token_endpoint: string\n /** DCR 注册端点(RFC 7591);若无则不支持动态注册。 */\n registration_endpoint?: string\n /** 注册端点认证方式;'none'=无需 client_secret,PKCE only。 */\n registration_endpoint_auth_methods_supported?: string[]\n /** 支持的 PKCE challenge 方法(期望 ['S256'])。 */\n code_challenge_methods_supported?: string[]\n /** 授权端点支持的 response_type。 */\n response_types_supported?: string[]\n /** token 端点支持的 grant_type。 */\n grant_types_supported?: string[]\n /** 授权端点支持的 scope。 */\n scopes_supported?: string[]\n /** 令牌端点认证方式。 */\n token_endpoint_auth_methods_supported?: string[]\n /** RFC 8414 未定义的其他字段(passthrough)。 */\n [key: string]: unknown\n}\n\n/**\n * 获取 authorization server metadata。\n * 先试 RFC 8414(GET issuer/.well-known/oauth-authorization-server),\n * 失败回退 OIDC discovery(GET issuer/.well-known/openid-configuration),\n * 只取与 OAuth 授权码流相关的字段。\n */\nexport async function fetchAuthorizationServerMetadata(\n issuer: string,\n fetchImpl: typeof globalThis.fetch = fetch,\n): Promise<AuthorizationServerMetadata> {\n const rfc8414Url = `${issuer.replace(/\\/$/, '')}/.well-known/oauth-authorization-server`\n const oidcUrl = `${issuer.replace(/\\/$/, '')}/.well-known/openid-configuration`\n\n const tryFetch = async (url: string): Promise<AuthorizationServerMetadata> => {\n const res = await fetchImpl(url, {\n headers: { Accept: 'application/json' },\n })\n if (!res.ok) {\n throw new Error(`Metadata fetch failed: ${url} → ${res.status}`)\n }\n return (await res.json()) as AuthorizationServerMetadata\n }\n\n try {\n return await tryFetch(rfc8414Url)\n } catch {\n return tryFetch(oidcUrl)\n }\n}\n\nexport interface OAuthDiscoveryResult {\n protectedResource: ProtectedResourceMetadata\n authorizationServer: AuthorizationServerMetadata\n /** 选定的授权服务器 issuer。 */\n issuer: string\n}\n\n/**\n * 完整 OAuth 发现:\n * 1. 构造 metadata URL(优先 401 WWW-Authenticate,否则 assume <server-url>/.well-known/oauth-protected-resource)\n * 2. 获取 protected resource metadata\n * 3. 选择 authorization server(取 metadata 列表第一个,或 issuer)\n * 4. 获取 authorization server metadata\n */\nexport async function discoverOAuth(\n serverUrl: string,\n options: {\n /** 401 响应的 WWW-Authenticate 头(可选,有一等公民优先)。 */\n wwwAuthenticate?: string\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n } = {},\n): Promise<OAuthDiscoveryResult> {\n const fetchImpl = options.fetch ?? fetch\n const metadataUrl =\n parseWwwAuthenticate(options.wwwAuthenticate ?? '') ??\n `${serverUrl.replace(/\\/$/, '')}/.well-known/oauth-protected-resource`\n\n const protectedResource = await fetchProtectedResourceMetadata(metadataUrl, fetchImpl)\n\n const issuer = protectedResource.authorization_servers?.[0] ?? new URL(metadataUrl).origin\n\n const authorizationServer = await fetchAuthorizationServerMetadata(issuer, fetchImpl)\n\n return { protectedResource, authorizationServer, issuer }\n}\n","export interface DCRRequest {\n /** 可读的客户端名称。 */\n client_name?: string\n /** 授权码回调 redirect URI 列表。 */\n redirect_uris: string[]\n /** 授权端点请求的 grant type。 */\n grant_types?: string[]\n /** token 端点认证方式;PKCE 用 'none'。 */\n token_endpoint_auth_method?: string\n}\n\nexport interface DCRResponse {\n client_id: string\n client_secret?: string\n /** 注册完成后的 access token(供后续管理此客户端)。 */\n registration_access_token?: string\n /** 管理此注册的 endpoint URL。 */\n registration_client_uri?: string\n /** RFC 7591 未定义字段(passthrough)。 */\n [key: string]: unknown\n}\n\nexport interface RegisterClientOptions {\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n /** 注册端点的初始 access token(少数 AS 要求 bearer 认证)。 */\n initialAccessToken?: string\n}\n\n/**\n * 向 authorization server 注册 OAuth 客户端(DCR)。\n * POST <registration_endpoint>,请求体 JSON,响应 JSON。\n */\nexport async function registerClient(\n registrationEndpoint: string,\n params: DCRRequest,\n options: RegisterClientOptions = {},\n): Promise<DCRResponse> {\n const fetchImpl = options.fetch ?? fetch\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n }\n if (options.initialAccessToken) {\n headers['Authorization'] = `Bearer ${options.initialAccessToken}`\n }\n\n const res = await fetchImpl(registrationEndpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n client_name: params.client_name ?? 'otto MCP Client',\n redirect_uris: params.redirect_uris,\n grant_types: params.grant_types ?? ['authorization_code', 'refresh_token'],\n token_endpoint_auth_method: params.token_endpoint_auth_method ?? 'none',\n }),\n })\n\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(\n `DCR registration failed at ${registrationEndpoint}: ${res.status}${body ? ` — ${body}` : ''}`,\n )\n }\n\n return (await res.json()) as DCRResponse\n}\n","import { isExpiredAt } from '@x-otto/credentials'\nimport { OAuthError } from '@x-otto/shared'\n\nexport interface AuthorizationCodeExchangeParams {\n /** 授权码(来自回调)。 */\n code: string\n /** 回调 redirect_uri(必须与 authorize 请求一致)。 */\n redirectUri: string\n /** PKCE code_verifier。 */\n codeVerifier: string\n /** RFC 8707 resource indicator(指向 MCP server)。 */\n resource?: string\n /** DCR 获得的 client_id。 */\n clientId: string\n}\n\nexport interface RefreshTokenParams {\n refreshToken: string\n /** RFC 8707 resource indicator。 */\n resource?: string\n clientId?: string\n}\n\nexport interface TokenResponse {\n access_token: string\n token_type: string\n expires_in?: number\n refresh_token?: string\n /** token 签发时的时间戳(ms),用于计算过期。 */\n issued_at?: number\n /** RFC 8707 审计。 */\n resource?: string\n}\n\nexport interface OAuthCredentials {\n accessToken: string\n refreshToken?: string\n expiresAt?: number\n resource?: string\n /** DCR client_id(持久化供后续 refresh)。 */\n clientId?: string\n}\n\nexport interface TokenExchangeOptions {\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n}\n\n/**\n * 用授权码交换 token(PKCE)。\n * POST <token_endpoint>(application/x-www-form-urlencoded)。\n */\nexport async function exchangeCode(\n tokenEndpoint: string,\n params: AuthorizationCodeExchangeParams,\n options: TokenExchangeOptions = {},\n): Promise<OAuthCredentials> {\n const fetchImpl = options.fetch ?? fetch\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code: params.code,\n redirect_uri: params.redirectUri,\n code_verifier: params.codeVerifier,\n client_id: params.clientId,\n })\n if (params.resource) {\n body.set('resource', params.resource)\n }\n\n const res = await fetchImpl(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },\n body: body.toString(),\n })\n\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new OAuthError(`Token exchange failed: ${res.status}${errBody ? ` — ${errBody}` : ''}`, {\n code: 'OAUTH_TOKEN_EXCHANGE_FAILED',\n })\n }\n\n const data: TokenResponse = (await res.json()) as TokenResponse\n return toCredentials(data)\n}\n\n/**\n * 用 refresh_token 刷新 access token。\n * POST <token_endpoint>(application/x-www-form-urlencoded)。\n */\nexport async function refreshAccessToken(\n tokenEndpoint: string,\n params: RefreshTokenParams,\n options: TokenExchangeOptions = {},\n): Promise<OAuthCredentials> {\n const fetchImpl = options.fetch ?? fetch\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: params.refreshToken,\n })\n if (params.clientId) {\n body.set('client_id', params.clientId)\n }\n if (params.resource) {\n body.set('resource', params.resource)\n }\n\n const res = await fetchImpl(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },\n body: body.toString(),\n })\n\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n // P1 策略层:解析 OAuth2 标准错误响应体({\"error\":\"invalid_grant\",...}),\n // 附加 errorType 到抛出的错误上——供 RefreshStrategy.shouldFailFast/cacheFailure\n // 判定是否永久失败(invalid_grant = refresh_token 已过期/被撤销/已轮换)。\n const oauthErrorType = parseOAuthErrorType(errBody)\n throw new OAuthError(\n `Token refresh failed: ${res.status}${errBody ? ` — ${errBody}` : ''}`,\n { code: 'OAUTH_REFRESH_FAILED', ...(oauthErrorType ? { errorType: oauthErrorType } : {}) },\n )\n }\n\n const data: TokenResponse = (await res.json()) as TokenResponse\n return toCredentials(data, params.refreshToken)\n}\n\nfunction toCredentials(data: TokenResponse, fallbackRefreshToken?: string): OAuthCredentials {\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? fallbackRefreshToken,\n expiresAt: data.expires_in\n ? (data.issued_at ?? Date.now()) + data.expires_in * 1000\n : undefined,\n resource: data.resource,\n }\n}\n\n/**\n * 解析 OAuth2 标准错误响应体中的 error 字段(RFC 6749 §5.2)。\n * 服务端返回 `{\"error\":\"invalid_grant\"}` 时,refresh_token 已被永久拒绝——\n * 同一凭据快照后续刷新必失败,可 fail-fast 不再竞锁/打网络。\n */\nfunction parseOAuthErrorType(body: string): string | undefined {\n try {\n const obj = JSON.parse(body) as Record<string, unknown>\n return typeof obj.error === 'string' ? obj.error : undefined\n } catch {\n return undefined\n }\n}\n\n/** 检查 access token 是否已过期(提前 60 秒视为过期,避免边界竞态)。 */\n/**\n * RFC-374 M1:过期判断委托 @x-otto/credentials 叶包(skew 单源)。\n * 公开 API 形状不变(`isTokenExpired(credentials, skewSeconds=60)`),\n * 实现改为叶包 `isExpiredAt`(ms 口径)——mcp 域只负责取 `expiresAt` 字段。\n */\nexport function isTokenExpired(credentials: OAuthCredentials, skewSeconds = 60): boolean {\n return isExpiredAt(credentials.expiresAt, skewSeconds * 1000)\n}\n","import { createHash } from 'node:crypto'\nimport { readdir, readFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { deleteFileIfExists, ensurePrivateDir, readJsonFile, writeJsonAtomic } from '@x-otto/credentials'\nimport { createOverlayStore } from '@x-otto/credentials'\nimport { isShadowModeEnabled } from '@x-otto/env'\n\nimport type { OAuthCredentials } from './token'\n\nexport interface TokenStore {\n /** 加载某 server 的凭据;不存在返回 undefined。 */\n load(serverName: string): Promise<OAuthCredentials | undefined>\n /** 保存某 server 的凭据。 */\n save(serverName: string, credentials: OAuthCredentials): Promise<void>\n /** 删除某 server 的凭据(logout)。 */\n delete(serverName: string): Promise<void>\n /** 列出所有有凭据的 server name。 */\n list(): Promise<string[]>\n /**\n * 跨进程刷新锁目录(与凭据文件同目录)。仅文件 store 提供;内存/overlay store 为\n * undefined(后者影子态禁止写锁文件)。业务层据此决定 refresh 是否走跨进程锁。\n */\n readonly refreshLockDir?: string\n /**\n * 是否为影子(overlay)态 store。影子态 store 读穿透磁盘、写只进内存——\n * 过期 token 不可刷新(轮换型 refresh_token 会作废磁盘旧 token,致全部普通会话\n * 掉登录)。业务层据此在 refresh 入口拦截(RFC-345 §D2b,对齐 ai auth-store 的\n * overlay 拒刷语义)。内存 store 非 shadow(无磁盘可破坏,刷新安全)。\n */\n readonly shadow?: boolean\n}\n\nexport interface FileTokenStoreOptions {\n /** 存储目录,默认 `~/.otto/mcp/credentials`。 */\n baseDir?: string\n}\n\nexport function createFileTokenStore(options: FileTokenStoreOptions = {}): TokenStore {\n const baseDir = options.baseDir ?? join(homedir(), '.otto', 'mcp', 'credentials')\n\n const filePath = (serverName: string): string => join(baseDir, `${sanitizeServerName(serverName)}.json`)\n\n return {\n refreshLockDir: baseDir,\n\n async load(serverName) {\n // RFC-374 M1:读盘纹理委托叶包 readJsonFile(ENOENT/损坏 → fallback,EACCES rethrow)。\n return readJsonFile<OAuthCredentials | undefined>(filePath(serverName), undefined)\n },\n\n async save(serverName, credentials) {\n // RFC-374 M1:原子写盘纹理委托叶包 writeJsonAtomic——临时文件 + chmod 0o600 +\n // rename 原子替换,凭据从落盘第一刻起就不可被其他本地用户读取,读者永远\n // 不会观测到半写状态(对齐 @x-otto/ai auth-store saveSerial 的同一语义)。\n await ensurePrivateDir(baseDir)\n await writeJsonAtomic(filePath(serverName), { ...credentials, serverName })\n },\n\n async delete(serverName) {\n await deleteFileIfExists(filePath(serverName))\n },\n\n async list() {\n try {\n const entries = await readdir(baseDir)\n const names: string[] = []\n for (const f of entries) {\n if (!f.endsWith('.json')) continue\n try {\n const raw = await readFile(join(baseDir, f), 'utf-8')\n const original = readServerName(raw)\n names.push(original ?? f.slice(0, -5))\n } catch {\n names.push(f.slice(0, -5))\n }\n }\n return names\n } catch (err) {\n const code = (err as NodeJS.ErrnoException)?.code\n if (code === 'ENOENT') return []\n throw err\n }\n },\n }\n}\n\nexport function createMemoryTokenStore(): TokenStore {\n const store = new Map<string, OAuthCredentials>()\n return {\n async load(serverName) {\n return store.get(serverName)\n },\n async save(serverName, credentials) {\n store.set(serverName, credentials)\n },\n async delete(serverName) {\n store.delete(serverName)\n },\n async list() {\n return [...store.keys()]\n },\n }\n}\n\n/**\n * RFC-345 影子模式 overlay(RFC-374 M1:实现委托 @x-otto/credentials 通用 overlay):\n * 读穿透到 base(继承磁盘既有凭据),写/删只进内存 overlay。进程退出后磁盘凭据\n * 逐字节不变。语义细节(load 墓碑优先/save 只进内存/delete 记墓碑/list 合并)\n * 由叶包 createOverlayStore 保证,本函数只做 TokenStore 形状适配 + shadow 标记。\n *\n * shadow: true + refreshLockDir: undefined —— 显式声明影子态语义(此前靠 overlay\n * 丢失 base 的 refreshLockDir 属性实现,是隐性开关,未来给 overlay 透传 refreshLockDir\n * 会静默改变影子态锁语义)。\n */\nexport function createOverlayTokenStore(base: TokenStore): TokenStore {\n const overlay = createOverlayStore<OAuthCredentials>(base)\n return {\n load: overlay.load,\n save: overlay.save,\n delete: overlay.delete,\n list: overlay.list,\n shadow: true,\n refreshLockDir: undefined,\n }\n}\n\n/**\n * MCP OAuth 凭据 store 的 §D1 白名单构造入口:影子态返回 overlay(读磁盘、写内存),\n * 否则返回纯文件 store。业务层(mcp-manager / login)统一走此函数,不自行判断影子态。\n */\nexport function createDefaultTokenStore(options: FileTokenStoreOptions = {}): TokenStore {\n const fileStore = createFileTokenStore(options)\n return isShadowModeEnabled() ? createOverlayTokenStore(fileStore) : fileStore\n}\n\n/**\n * 防止路径穿越 + 冲突:sanitize + 前 8 位 SHA256 哈希避免 sanitize 碰撞。\n * 同时作为跨进程刷新锁的 key(与凭据文件同源命名,避免 serverName 中的路径分隔符\n * 破坏锁文件路径)。\n */\nexport function sanitizeServerName(name: string): string {\n const safe = name.replace(/[^a-zA-Z0-9._-]/g, '_')\n const hash = createHash('sha256').update(name).digest('hex').slice(0, 8)\n return `${safe}_${hash}`\n}\n\nfunction readServerName(raw: string): string | undefined {\n try {\n const obj = JSON.parse(raw) as Record<string, unknown>\n return typeof obj.serverName === 'string' ? obj.serverName : undefined\n } catch {\n return undefined\n }\n}\n","/**\n * refresh-lock.ts —— MCP OAuth 跨进程刷新互斥锁(RFC-374 M1 迁移后:域配置薄层)。\n *\n * 背景与 ai 的 `oauth-refresh-coordinator.ts` 同源:本机可能同时运行多个 otto 进程\n * (CLI、feishu-use 桥接等),共享同一份 MCP 凭据目录 `~/.otto/mcp/credentials`。\n * 轮换型 refresh_token 在多进程几乎同时用同一个旧 token 刷新时会双花——服务端只认\n * 先到者,其余进程刷新失败并非凭据过期而是被抢先轮换。\n *\n * RFC-374 M1:通用锁原语与刷新协调骨架已下沉 `@x-otto/credentials` 叶包\n * (createRefreshLock / refreshUnderCrossProcessLock),本文件只保留 MCP 域的\n * 三样配置:锁目录(凭据文件同目录)、key 命名(sanitizeServerName)、锁文件前缀\n * `mcp-refresh`(**上线后不可变**——换前缀 = 新旧进程各锁各的文件 = 等于没锁,\n * 与 ai 的 `auth-refresh` 前缀同一条滚动升级硬约束)。\n *\n * 公开 API 形状不变(acquireRefreshLock/releaseRefreshLock/waitForRefreshLockRelease/\n * refreshWithCrossProcessLock/REFRESH_LOCK_TIMEOUT_MS/RefreshWithLockPorts),\n * 既有消费方(login.ts)与测试零改动。\n */\nimport {\n REFRESH_LOCK_TIMEOUT_MS,\n createRefreshLock,\n refreshUnderCrossProcessLock,\n type RefreshStrategy,\n} from '@x-otto/credentials'\n\nimport { isTokenExpired, refreshAccessToken } from './token'\nimport { sanitizeServerName } from './token-store'\n\nimport type { OAuthCredentials, RefreshTokenParams, TokenExchangeOptions } from './token'\n\nexport type { RefreshStrategy }\n\n/** 锁文件名前缀(`.mcp-refresh-<key>.lock`)。MCP 是全新锁域,无滚动升级历史约束。 */\nconst LOCK_PREFIX = 'mcp-refresh'\n\n/** 陈旧锁清理阈值 = 等待释放超时(与 ai 的规则9一致,两者必须相等)。 */\nexport { REFRESH_LOCK_TIMEOUT_MS }\n\n/** 尝试获取某 server 的跨进程刷新锁。成功 true;被占用/异常 false。 */\nexport async function acquireRefreshLock(\n credentialsDir: string,\n serverName: string,\n timeoutMs: number = REFRESH_LOCK_TIMEOUT_MS,\n): Promise<boolean> {\n return createRefreshLock(credentialsDir, LOCK_PREFIX).acquire(sanitizeServerName(serverName), timeoutMs)\n}\n\n/** 释放某 server 的跨进程刷新锁(幂等,并停止心跳)。 */\nexport async function releaseRefreshLock(credentialsDir: string, serverName: string): Promise<void> {\n return createRefreshLock(credentialsDir, LOCK_PREFIX).release(sanitizeServerName(serverName))\n}\n\n/** 等待另一进程持有的刷新锁释放,超时返回 false(调用方走读盘兜底)。 */\nexport async function waitForRefreshLockRelease(\n credentialsDir: string,\n serverName: string,\n timeoutMs: number = REFRESH_LOCK_TIMEOUT_MS,\n): Promise<boolean> {\n return createRefreshLock(credentialsDir, LOCK_PREFIX).waitForRelease(\n sanitizeServerName(serverName),\n timeoutMs,\n )\n}\n\n/** 刷新协调器读写端口:锁内需要重读/落盘,但不应直接持有 TokenStore 实例。 */\nexport interface RefreshWithLockPorts {\n /** 读取当前磁盘/存储上的凭据(可能已被另一进程刷新)。 */\n load(): Promise<OAuthCredentials | undefined>\n /** 保存真正刷新成功的新凭据。 */\n save(credentials: OAuthCredentials): Promise<void>\n}\n\n/**\n * 跨进程互斥刷新:同一时刻同一 server 全机器只有一个进程真正发起 `refreshAccessToken`\n * 网络请求,其余进程等待后直接读盘复用结果。语义(拿锁重读/等待读盘/抛错/finally 释放)\n * 由叶包 refreshUnderCrossProcessLock 保证,本函数做 MCP 域参数装配。\n *\n * P1 扩展:可选 `strategy` 透传到叶包骨架,启用永久失败缓存 / 磁盘恢复 / 可观测性。\n * 缺省(未提供 strategy)时与原有行为完全一致。\n */\nexport async function refreshWithCrossProcessLock(\n credentialsDir: string,\n serverName: string,\n tokenEndpoint: string,\n params: RefreshTokenParams,\n options: TokenExchangeOptions = {},\n ports: RefreshWithLockPorts,\n strategy?: RefreshStrategy<OAuthCredentials>,\n): Promise<OAuthCredentials> {\n const lock = createRefreshLock(credentialsDir, LOCK_PREFIX)\n return refreshUnderCrossProcessLock<OAuthCredentials>({\n lock,\n key: sanitizeServerName(serverName),\n load: ports.load,\n isFresh: (c) => !isTokenExpired(c),\n refresh: () => refreshAccessToken(tokenEndpoint, params, options),\n save: ports.save,\n strategy,\n })\n}\n","import { spawn } from 'node:child_process'\nimport { createPkcePair, generateState, startLoopbackServer, TypedEventEmitter } from '@x-otto/shared'\n\nimport { discoverOAuth } from './discovery'\nimport { registerClient } from './registration'\nimport { exchangeCode, refreshAccessToken, isTokenExpired } from './token'\nimport { createDefaultTokenStore } from './token-store'\nimport { refreshWithCrossProcessLock } from './refresh-lock'\nimport type { RefreshStrategy } from './refresh-lock'\n\nimport type { AuthorizationServerMetadata } from './discovery'\nimport type { OAuthCredentials, RefreshTokenParams } from './token'\nimport type { TokenStore } from './token-store'\n\nexport interface McpOAuthLoginOptions {\n /** MCP server URL。 */\n serverUrl: string\n /** MCP server 名称(用于 token store 键控)。 */\n serverName: string\n /** 401 响应的 WWW-Authenticate 头(可选,优先用于发现)。 */\n wwwAuthenticate?: string\n /** 手动指定 client_id(跳过 DCR)。 */\n clientId?: string\n /** 授权服务器元数据(跳过发现,适用于已知 AS)。 */\n authorizationServer?: AuthorizationServerMetadata\n /** token store(默认文件存储 ~/.otto/mcp/credentials/)。 */\n tokenStore?: TokenStore\n /** 打开浏览器的函数(默认 `open`命令)。注入用于测试。 */\n openBrowser?: (url: string) => Promise<void>\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n /** 取消信号。 */\n signal?: AbortSignal\n}\n\nexport interface McpOAuthLoginResult {\n credentials: OAuthCredentials\n /** 是否经 DCR 注册了新的 client_id。 */\n registered: boolean\n /** DCR 获得的 client_id(持久化供后续刷新)。 */\n clientId: string\n}\n\n/**\n * 执行完整 OAuth 2.1 交互式登录(PKCE + DCR)。\n *\n * 流程(修正终局 review):\n * 1. 发现 OAuth 端点(或使用传入的已知 metadata)\n * 2. 生成 PKCE pair + state,**先**启动 loopback server(获取真实 redirect_uri)\n * 3. DCR 注册客户端(使用真实 redirect_uri;或使用传入的 client_id)\n * 4. 构造 authorize URL,打开浏览器\n * 5. 等待浏览器回调送达授权码\n * 6. 用 PKCE code_verifier 交换 token\n * 7. 保存凭据到 token store(含 client_id)\n */\nexport async function login(options: McpOAuthLoginOptions): Promise<McpOAuthLoginResult> {\n const {\n serverUrl,\n serverName,\n wwwAuthenticate,\n clientId: explicitClientId,\n authorizationServer: knownAs,\n tokenStore = createDefaultTokenStore(),\n openBrowser = defaultOpenBrowser,\n fetch: fetchImpl = fetch,\n signal,\n } = options\n\n if (signal?.aborted) {\n throw new Error('Login cancelled')\n }\n\n const as =\n knownAs ??\n (await discoverOAuth(serverUrl, { wwwAuthenticate, fetch: fetchImpl })).authorizationServer\n\n if (!as.token_endpoint) {\n throw new Error('Authorization server has no token_endpoint')\n }\n\n const pkce = createPkcePair()\n const state = generateState()\n\n const loopback = await startLoopbackServer({\n ports: [1455, 1457, 1459],\n expectedState: state,\n signal,\n })\n const redirectUri = loopback.redirectUri\n\n try {\n let clientId = explicitClientId\n let registered = false\n if (!clientId && as.registration_endpoint) {\n const reg = await registerClient(\n as.registration_endpoint,\n { redirect_uris: [redirectUri] },\n { fetch: fetchImpl },\n )\n clientId = reg.client_id\n registered = true\n }\n if (!clientId) {\n throw new Error(\n 'No client_id available. Configure client_id in MCP server settings or ensure the ' +\n 'authorization server supports Dynamic Client Registration (registration_endpoint).',\n )\n }\n\n const authParams = new URLSearchParams({\n response_type: 'code',\n client_id: clientId,\n redirect_uri: redirectUri,\n code_challenge: pkce.challenge,\n code_challenge_method: pkce.method,\n state,\n })\n // RFC 8707 resource 指示器:authorize 与 token 交换必须一致(token 交换无条件携带\n // resource=serverUrl),否则受众限定不对称——统一无条件携带 canonical serverUrl。\n authParams.set('resource', serverUrl)\n const authUrl = `${as.authorization_endpoint}?${authParams.toString()}`\n\n await openBrowser(authUrl)\n\n const callback = await loopback.waitForCallback()\n\n const credentials = await exchangeCode(\n as.token_endpoint,\n {\n code: callback.code,\n redirectUri,\n codeVerifier: pkce.verifier,\n resource: serverUrl,\n clientId,\n },\n { fetch: fetchImpl },\n )\n\n await tokenStore.save(serverName, { ...credentials, clientId })\n\n return { credentials, registered, clientId }\n } finally {\n await loopback.close()\n }\n}\n\n/** 凭据状态描述(对齐 ai AuthStore.getAuthSource/hasCredential,S1 补齐)。 */\nexport interface CredentialStatus {\n /** 是否有凭据(store 中存在)。 */\n configured: boolean\n /** 凭据来源类型:'oauth' = 有 access/refresh token。缺省 = 无凭据。 */\n source?: 'oauth'\n /** 凭据是否即将过期(isTokenExpired 判定)。 */\n expiringSoon?: boolean\n /** 是否影子态(影子态不可刷新)。 */\n shadow?: boolean\n}\n\n/** 凭据变更事件(对齐 ai AuthStore 'credential.changed')。 */\nexport interface TokenProviderEvents {\n /** 凭据发生变化(login/logout/refresh 成功,或外部进程写入被感知)。 */\n 'credential.changed': (payload: { serverName: string; source: 'self' | 'external' | 'refresh' }) => void\n}\n\nexport interface TokenProvider {\n /** 获取 access token;过期自动刷新。不可恢复的失败抛 McpUnauthorized 错误。 */\n getAccessToken(): Promise<string | undefined>\n /** 强制重新登录(login → 保存 → 返回 token)。 */\n login(options: McpOAuthLoginOptions): Promise<string>\n /** 登出(删除存储的凭据)。 */\n logout(): Promise<void>\n /** S1 补齐:只读探查凭据状态(不触发刷新/网络)。对齐 ai getAuthSource/hasCredential。 */\n describe(): Promise<CredentialStatus>\n /** S1 补齐:凭据变更事件(对齐 ai 'credential.changed')。 */\n on(event: 'credential.changed', listener: TokenProviderEvents['credential.changed']): this\n}\n\nexport interface TokenProviderOptions {\n serverName: string\n serverUrl: string\n tokenStore?: TokenStore\n /** client_id(跳过 DCR)。 */\n clientId?: string\n /** 已知的 AS metadata(跳过发现,也用于跳过刷新时的 rediscovery)。 */\n authorizationServer?: AuthorizationServerMetadata\n fetch?: typeof globalThis.fetch\n}\n\n/**\n * 创建 MCP token provider:加载 → 检查刷新 → 提供 access token。\n * token 过期时自动 refresh;refresh 失败 → getAccessToken 返回 undefined\n * (提示用户运行 `otto mcp login`)。\n */\nexport function createTokenProvider(options: TokenProviderOptions): TokenProvider {\n const {\n serverName,\n serverUrl,\n tokenStore = createDefaultTokenStore(),\n clientId,\n authorizationServer,\n fetch: fetchImpl,\n } = options\n\n let credentials: OAuthCredentials | undefined\n let cachedAs = authorizationServer ?? undefined\n\n // clientId 合并保持与原有 refresh 路径语义一致:仅当 options.clientId 存在时才回填。\n const withClientId = (creds: OAuthCredentials): OAuthCredentials =>\n clientId ? { ...creds, clientId } : creds\n\n // S1 补齐:事件面——TypedEventEmitter 提供 .on/.emit,与 TokenProvider 接口合并。\n const emitter = new TypedEventEmitter<TokenProviderEvents>()\n\n const provider: TokenProvider = {\n async getAccessToken() {\n if (!credentials) {\n credentials = await tokenStore.load(serverName)\n }\n if (!credentials) return undefined\n\n if (isTokenExpired(credentials) && credentials.refreshToken) {\n // RFC-345 §D2b(终局 review P2 修复):影子态 store 不可刷新——\n // refresh_token 是轮换型的 provider(如 Anthropic),影子进程刷新会作废\n // 磁盘上的旧 token,且新 token 只进内存 overlay → 进程退出后蒸发 →\n // 全部普通会话掉登录(本仓已有同类真实事故,见 ai auth-store overlay 防御)。\n // 返回 undefined 提示用户重新登录,不发起网络刷新。\n if (tokenStore.shadow) {\n return undefined\n }\n try {\n if (!cachedAs) {\n cachedAs = (await discoverOAuth(serverUrl, { fetch: fetchImpl })).authorizationServer\n }\n const params: RefreshTokenParams = {\n refreshToken: credentials.refreshToken,\n resource: serverUrl,\n clientId: credentials.clientId ?? clientId,\n }\n const lockDir = tokenStore.refreshLockDir\n if (lockDir) {\n credentials = await refreshWithCrossProcessLock(\n lockDir,\n serverName,\n cachedAs.token_endpoint,\n params,\n { fetch: fetchImpl },\n {\n load: () => tokenStore.load(serverName),\n save: (creds) => tokenStore.save(serverName, withClientId(creds)),\n },\n // P1 策略层:永久失败缓存 + 磁盘恢复 + 可观测性\n // (对齐 ai OAuthRefreshCoordinator 的三层防御,mcp 此前完全缺失)\n createRefreshStrategy(serverName, tokenStore, withClientId),\n )\n credentials = withClientId(credentials)\n emitter.emit('credential.changed', { serverName, source: 'refresh' })\n } else {\n credentials = await refreshAccessToken(cachedAs.token_endpoint, params, {\n fetch: fetchImpl,\n })\n credentials = withClientId(credentials)\n await tokenStore.save(serverName, credentials)\n emitter.emit('credential.changed', { serverName, source: 'refresh' })\n }\n } catch {\n return undefined\n }\n }\n\n return credentials.accessToken\n },\n\n async login(options) {\n const result = await login({\n ...options,\n tokenStore,\n clientId,\n authorizationServer: cachedAs,\n })\n credentials = result.credentials\n cachedAs ??= options.authorizationServer\n emitter.emit('credential.changed', { serverName, source: 'self' })\n return credentials.accessToken\n },\n\n async logout() {\n credentials = undefined\n cachedAs = undefined\n await tokenStore.delete(serverName)\n emitter.emit('credential.changed', { serverName, source: 'self' })\n },\n\n async describe() {\n const creds = credentials ?? await tokenStore.load(serverName)\n if (!creds) {\n return { configured: false, shadow: tokenStore.shadow }\n }\n return {\n configured: true,\n source: 'oauth' as const,\n expiringSoon: isTokenExpired(creds),\n shadow: tokenStore.shadow,\n }\n },\n\n on(event, listener) {\n emitter.on(event, listener)\n return this\n },\n }\n\n return provider\n}\n\n/**\n * P1 策略层:为 mcp 的跨进程刷新构建永久失败缓存 + 磁盘恢复策略(对齐 ai\n * OAuthRefreshCoordinator 的三层防御)。\n *\n * - shouldFailFast:同一 refreshToken 快照此前被 invalid_grant 永久拒绝 → 不竞锁/不打网络。\n * - recoverFromFailure:重读 tokenStore(绕过内存 cache)——另一进程可能已抢先刷新。\n * - cacheFailure:服务端返回 invalid_grant 时缓存(key=refreshToken 快照)。\n * - onStep:恢复步骤可观测性(旁路日志,不影响控制流)。\n *\n * 策略状态(permanentFailures Map)随 createTokenProvider 闭包生命周期——provider\n * 被 GC 时策略状态随之释放。与 ai 的差异:ai 的协调器是长驻类(permanentFailures\n * 是实例字段),mcp 的 TokenProvider 是闭包——但 mcp 的 provider 生命周期与\n * MCP 连接绑定,不需要跨连接持久化失败缓存(重新连接 = 重新 login = 新凭据)。\n */\nfunction createRefreshStrategy(\n serverName: string,\n tokenStore: TokenStore,\n withClientId: (creds: OAuthCredentials) => OAuthCredentials,\n): RefreshStrategy<OAuthCredentials> {\n const permanentFailures = new Map<string, { refreshToken: string; error: unknown }>()\n\n return {\n shouldFailFast(latest) {\n if (!latest.refreshToken) return false\n const cached = permanentFailures.get(serverName)\n return !!cached && cached.refreshToken === latest.refreshToken\n },\n\n async recoverFromFailure(latest) {\n // 重读 store(绕过内存 cache)——另一进程可能已抢先刷新并落盘\n const disk = await tokenStore.load(serverName)\n if (!disk || !disk.refreshToken) return undefined\n // 磁盘凭据的 refreshToken 与当前旧值相同 → 不是另一进程刷新的结果\n if (disk.refreshToken === latest.refreshToken) return undefined\n // 磁盘有新凭据 → 清永久失败缓存(旧快照已失效)\n permanentFailures.delete(serverName)\n return withClientId(disk)\n },\n\n cacheFailure(error, latest) {\n const errorType = (error as { errorType?: string })?.errorType\n if (errorType === 'invalid_grant' && latest.refreshToken) {\n permanentFailures.set(serverName, {\n refreshToken: latest.refreshToken,\n error,\n })\n }\n },\n\n onStep(step, detail) {\n // 旁路可观测性——当前只 console.debug,未来可接线到日志体系\n if (process.env['OTTO_DEBUG'] && step) {\n console.debug(`[mcp:refresh] ${serverName} ${step}${detail ? `: ${detail}` : ''}`)\n }\n },\n }\n}\nasync function defaultOpenBrowser(url: string): Promise<void> {\n // Windows:`start` 是 cmd.exe 内建命令、非可执行文件,`spawn('start', ...)` 会 ENOENT。\n // 必须经 `cmd /c start`;空字符串是 start 的 title 占位(防 url 被当标题)。\n // darwin=open / linux=xdg-open 是真实可执行文件,直接 spawn。\n const isWin = process.platform === 'win32'\n const cmd = isWin ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open'\n const args = isWin ? ['/c', 'start', '', url] : [url]\n\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { stdio: 'ignore', detached: true })\n child.on('error', (err) => {\n reject(new Error(`Failed to open browser: ${err.message}`))\n })\n child.on('close', (code) => {\n if (code === 0 || code === null) {\n resolve()\n } else {\n reject(new Error(`Browser command exited with code ${code}`))\n }\n })\n child.unref()\n })\n}\n","import { createLogger, McpError, TypedEventEmitter, traceStartupPhase } from '@x-otto/shared'\nimport { McpClient, createMcpClient } from './mcp-client'\nimport type { McpTransport, ProcessSpawnFactory } from './transport'\nimport { createMcpToolAdapters } from './mcp-tool-adapter'\nimport { createTokenProvider, createDefaultTokenStore } from './oauth'\nimport type {\n McpServerConfig,\n McpServerInfo,\n McpClientStatus,\n McpResource,\n McpPrompt,\n McpEvents,\n} from './types'\nimport type { AgentToolDefinition } from '@x-otto/interchange'\n\nconst logger = createLogger('@x-otto/mcp:manager')\n\nexport interface McpManagerOptions {\n /** 每个 MCP 请求超时 */\n requestTimeoutMs?: number\n /** 连接失败时是否静默跳过(默认 true) */\n skipOnError?: boolean\n /** 自动重连(默认 true) */\n autoReconnect?: boolean\n /** 最大重连次数(默认 3) */\n maxReconnectAttempts?: number\n /**\n * RFC-230:重连退避基准延迟(ms,指数退避,默认 500)。**只做全局配置**,不支持\n * per-server 覆盖——`McpServerConfig` 没有对应字段(与 `requestTimeoutMs`/\n * `maxReconnectAttempts` 不同,那两者有 per-server 覆盖能力)。若未来有真实需求,\n * 可另立 TODO 补 `McpServerConfig.reconnectDelayMs` + settings schema 字段。\n */\n reconnectDelayMs?: number\n /** 进程 spawn 工厂(RFC-085 M2):注入后 stdio MCP 子进程纳入 ProcessRuntime 统一管理。 */\n processSpawnFactory?: ProcessSpawnFactory\n /**\n * 注入 transport 工厂(RFC-368 M2 测试用)。缺省时按 config 构造 stdio/sse。\n * 仅用于测试注入 FakeTransport;不可用于生产环境。\n */\n transportFactory?: () => McpTransport\n}\n\nexport class McpManager extends TypedEventEmitter<McpEvents> {\n private clients = new Map<string, McpClient>()\n private options: McpManagerOptions\n private disabledServers: Set<string>\n constructor(options: McpManagerOptions = {}, disabledServers: string[] = []) {\n super()\n this.options = options\n this.disabledServers = new Set(disabledServers)\n }\n\n /**\n * 返回值(RFC-150 D1 追加,向后兼容——原调用点若不消费返回值零影响):本轮 connectAll 的\n * 汇总统计,供宿主层(McpController.start())非阻塞路径完成后驱动 toast/日志通知,\n * 不需要重新遍历/监听事件才能知道\"这轮到底连上几个\"。\n */\n async connectAll(\n servers: Record<string, McpServerConfig>,\n ): Promise<{ connected: number; failed: number }> {\n const entries = Object.entries(servers)\n if (entries.length === 0) {\n return { connected: 0, failed: 0 }\n }\n\n logger.info('Connecting to %d MCP servers...', entries.length)\n\n // RFC-150 D7/M0:per-server 打点——connectAll 内部并行,但整体常被上层 await(RFC-150 D1\n // 拟改为非阻塞);per-server 耗时是定位\"哪个 server 拖累整体\"的直接证据(§1.1 时间线)。\n const results = await Promise.allSettled(\n entries.map(([name, config]) =>\n traceStartupPhase(`mcp:${name} connect`, () => this.connect(name, config)),\n ),\n )\n\n let connected = 0\n let failed = 0\n for (const result of results) {\n if (result.status === 'fulfilled') {\n connected++\n } else {\n failed++\n }\n }\n\n logger.info('MCP servers: %d connected, %d failed', connected, failed)\n return { connected, failed }\n }\n\n async connect(name: string, config: McpServerConfig): Promise<void> {\n if (this.disabledServers.has(name)) {\n logger.info('MCP server \"%s\" is disabled, skipping', name)\n return\n }\n\n if (this.clients.has(name)) {\n await this.disconnect(name)\n }\n\n let getAuthToken: (() => Promise<string | undefined>) | undefined\n if (config.url && config.type !== 'stdio') {\n const tokenStore = createDefaultTokenStore()\n const existing = await tokenStore.load(name)\n if (existing) {\n const provider = createTokenProvider({\n serverName: name,\n serverUrl: config.url,\n tokenStore,\n clientId: existing.clientId,\n })\n getAuthToken = () => provider.getAccessToken()\n }\n }\n\n const client = createMcpClient(name, config, {\n requestTimeoutMs: config.requestTimeoutMs ?? this.options.requestTimeoutMs,\n autoReconnect: config.autoReconnect ?? this.options.autoReconnect,\n maxReconnectAttempts: config.maxReconnectAttempts ?? this.options.maxReconnectAttempts,\n // RFC-230:只读全局 this.options.reconnectDelayMs——不读 config.reconnectDelayMs,\n // 该 per-server 字段不存在也不新增(见 McpManagerOptions.reconnectDelayMs 注释)。\n reconnectDelayMs: this.options.reconnectDelayMs,\n getAuthToken,\n processSpawnFactory: this.options.processSpawnFactory,\n transportFactory: this.options.transportFactory,\n })\n\n client.onToolsChanged(() => {\n logger.info('MCP server \"%s\" tools changed', name)\n this.emit('tools.changed', { serverName: name, tools: client.getTools() })\n })\n\n client.onResourcesChanged(() => {\n logger.info('MCP server \"%s\" resources changed', name)\n this.emit('resources.changed', { serverName: name })\n })\n\n client.onDisconnected((info) => {\n logger.info('MCP server \"%s\" disconnected (%s): %s', name, info.kind, info.reason ?? 'unknown')\n this.emit('server.disconnected', { name, reason: info.reason, kind: info.kind })\n })\n\n // RFC-165 D9/H1:转发通用 notification 为 server.notification 事件,供\n // PanelBackendController(M2)等上层消费方订阅(如 backend/state 推送)。\n client.onNotification((method, params) => {\n this.emit('server.notification', { name, method, params })\n })\n\n try {\n await client.connect()\n this.clients.set(name, client)\n this.emit('server.connected', { name, tools: client.getTools().length })\n } catch (err) {\n const message = (err as Error).message\n // 透传错误码,供宿主据码渲染 actionable 指引(如 MCP_UNAUTHORIZED → 'otto mcp login')。\n const code = err instanceof McpError ? err.code : undefined\n this.emit('server.error', { name, error: message, code })\n\n if (this.options.skipOnError !== false) {\n logger.warn('MCP server \"%s\" connection failed, skipping: %s', name, message)\n } else {\n throw err\n }\n }\n }\n\n /**\n * RFC-368 M2:断开连接。`server.disconnected` 事件由 McpClient 的生命周期边界作为唯一\n * 发布者——manager 的 `onDisconnected` 回调已在 `connect()` 中注册转发,此处不再重复 emit,\n * 保证每个连接代次恰好发布一次断开事件(手动断开与自然关闭均遵循此契约)。\n */\n async disconnect(name: string): Promise<void> {\n const client = this.clients.get(name)\n if (client) {\n await client.disconnect()\n this.clients.delete(name)\n // server.disconnected 已由 client 的 disconnectedCallback → manager 回调转发,\n // 此处不再直接 emit,避免同一次断开触发两次事件(RFC-368 M2/D3)。\n }\n }\n\n async disconnectAll(): Promise<void> {\n const names = [...this.clients.keys()]\n await Promise.allSettled(names.map((name) => this.disconnect(name)))\n }\n\n getAllTools(): AgentToolDefinition[] {\n const tools: AgentToolDefinition[] = []\n for (const [name, client] of this.clients) {\n if (client.getStatus() === 'ready') {\n tools.push(...createMcpToolAdapters(client, name))\n }\n }\n return tools\n }\n\n getAllResources(): Array<McpResource & { serverName: string }> {\n const resources: Array<McpResource & { serverName: string }> = []\n for (const [name, client] of this.clients) {\n if (client.getStatus() === 'ready') {\n for (const resource of client.getResources()) {\n resources.push({ ...resource, serverName: name })\n }\n }\n }\n return resources\n }\n\n getAllPrompts(): Array<McpPrompt & { serverName: string }> {\n const prompts: Array<McpPrompt & { serverName: string }> = []\n for (const [name, client] of this.clients) {\n if (client.getStatus() === 'ready') {\n for (const prompt of client.getPrompts()) {\n prompts.push({ ...prompt, serverName: name })\n }\n }\n }\n return prompts\n }\n\n getClient(name: string): McpClient | undefined {\n return this.clients.get(name)\n }\n\n getServerInfos(): McpServerInfo[] {\n const infos: McpServerInfo[] = []\n\n for (const [name, client] of this.clients) {\n infos.push({\n name,\n config: client.getConfig(),\n status: client.getStatus(),\n tools: client.getTools(),\n resources: client.getResources(),\n prompts: client.getPrompts(),\n capabilities: client.getCapabilities() ?? undefined,\n error: client.getStatus() === 'error' ? 'Connection failed' : undefined,\n })\n }\n\n for (const name of this.disabledServers) {\n if (!this.clients.has(name)) {\n infos.push({\n name,\n status: 'disconnected' as McpClientStatus,\n tools: [],\n })\n }\n }\n\n return infos\n }\n\n get connectedCount(): number {\n let count = 0\n for (const client of this.clients.values()) {\n if (client.getStatus() === 'ready') {\n count++\n }\n }\n return count\n }\n\n get toolCount(): number {\n let count = 0\n for (const client of this.clients.values()) {\n if (client.getStatus() === 'ready') {\n count += client.getTools().length\n }\n }\n return count\n }\n}\n","/**\n * dot-mcp-json.ts — `.mcp.json` 文件格式解析器(RFC-421 D2,原 claude-compat).\n *\n * `.mcp.json` 形如 `{ \"mcp_servers\": { \"<name>\": { command/args/env/url/type/headers } } }`,\n * 字段与 `McpServerConfig` 基本同构。支持双键:otto 统一键 `mcp_servers`(优先)+\n * Claude Code 原生驼峰键 `mcpServers`(回退,读第三方文件)。\n * 支持 `${VAR}` / `${VAR:-default}` 环境变量展开。\n *\n * 归属本包(`@x-otto/mcp`):`.mcp.json` 是 `McpServerConfig` 的文件格式,解析器与类型同域。\n * 原在 `@x-otto/claude-compat`,造成 `coding/plugin/mcp.ts` 反向依赖 compat(RFC-421 P0-1)。\n * 信任门逻辑不在此处——由各消费方自行叠加(compat 的 `resolveClaudeMcpServers` 叠加\n * Claude 信任门;plugin 的 `resolvePluginMcpServers` 叠加插件信任门)。\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\nimport type { McpServerConfig } from './types'\n\n/** 展开 `${VAR}` 与 `${VAR:-default}`(未设或空 → default/空串),遵循 shell `:-` 语义。 */\nfunction expandEnv(value: string): string {\n return value.replace(\n /\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}/g,\n (_m, name: string, def: string | undefined) => {\n const v = process.env[name]\n return v !== undefined && v !== '' ? v : (def ?? '')\n },\n )\n}\n\nfunction expandStringArray(arr: unknown): string[] | undefined {\n if (!Array.isArray(arr)) return undefined\n return arr.filter((x): x is string => typeof x === 'string').map(expandEnv)\n}\n\nfunction expandStringRecord(rec: unknown): Record<string, string> | undefined {\n if (!rec || typeof rec !== 'object') return undefined\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(rec as Record<string, unknown>)) {\n if (typeof v === 'string') out[k] = expandEnv(v)\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/** 把一条 `.mcp.json` server 原始项映射成 McpServerConfig(仅已知字段,含 env 展开)。 */\nfunction mapServerEntry(raw: unknown): McpServerConfig | null {\n if (!raw || typeof raw !== 'object') return null\n const r = raw as Record<string, unknown>\n const config: McpServerConfig = {}\n if (typeof r['type'] === 'string') config.type = r['type'] as McpServerConfig['type']\n if (typeof r['command'] === 'string') config.command = expandEnv(r['command'])\n const args = expandStringArray(r['args'])\n if (args) config.args = args\n const env = expandStringRecord(r['env'])\n if (env) config.env = env\n if (typeof r['url'] === 'string') config.url = expandEnv(r['url'])\n const headers = expandStringRecord(r['headers'])\n if (headers) config.headers = headers\n if (typeof r['managed'] === 'boolean') config.managed = r['managed'] as boolean\n if (!config.command && !config.url) return null\n return config\n}\n\n/**\n * 解析给定目录的 `.mcp.json`(`mcp_servers` 优先,回退 `mcpServers`)。\n * 不做信任门判断;缺失/损坏 → {}。信任门由消费方叠加。\n */\nexport function parseDotMcpJson(dir: string): Record<string, McpServerConfig> {\n const path = join(resolve(dir), '.mcp.json')\n if (!existsSync(path)) return {}\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(path, 'utf-8'))\n } catch {\n return {}\n }\n const rawObj = raw as { mcp_servers?: unknown; mcpServers?: unknown }\n const servers = rawObj?.mcp_servers ?? rawObj?.mcpServers\n if (!servers || typeof servers !== 'object') return {}\n\n const out: Record<string, McpServerConfig> = {}\n for (const [name, entry] of Object.entries(servers as Record<string, unknown>)) {\n const mapped = mapServerEntry(entry)\n if (mapped) out[name] = mapped\n }\n return out\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["logger","logger","logger"],"sources":["../src/sse.ts","../src/types.ts","../src/transport.ts","../src/mcp-client.ts","../src/mcp-tool-adapter.ts","../src/oauth/discovery.ts","../src/oauth/registration.ts","../src/oauth/token.ts","../src/oauth/token-store.ts","../src/oauth/refresh-lock.ts","../src/oauth/login.ts","../src/mcp-manager.ts","../src/dot-mcp-json.ts"],"sourcesContent":["/**\n * sse.ts — W3C/MCP 规范的 Server-Sent Events 数据解析(M6/RFC-016 一致性硬化).\n *\n * 旧 consumeStream 内联解析有三处偏离规范:\n * 1. 仅按 `\\n` 切,未剥 CRLF 的尾随 `\\r`;\n * 2. 硬要求 `data: `(带空格),漏掉 `data:{...}`(冒号后空格可选);\n * 3. 每条 `data:` 行立即派发,未按规范累积多行 data 直到空行(事件边界)再 join。\n *\n * 本解析器对齐规范 + 官方 SDK(eventsource-parser 语义):累积 `data` 字段值,\n * 遇空行派发一个事件(多行 data 以 `\\n` 连接);忽略注释行(`:` 开头)与其它字段\n * (event/id/retry);流结束时 flush 末尾未以空行收尾的事件。\n */\nexport interface SseDataParser {\n /** 喂入一段原始文本块(可跨事件/跨行边界)。 */\n feed(chunk: string): void\n /** 流结束时调用:派发任何未以空行收尾的累积事件。 */\n flush(): void\n}\n\n/**\n * 创建一个 SSE 事件解析器;每个完整事件的 data(多行已 join)经 onData 回调。\n *\n * 第二个参数是事件名(`event:` 字段值,缺省 `'message'`)——MCP 2024-11-05 HTTP+SSE\n * 规范要求服务端连接时先发一个 `event: endpoint` 事件告知 POST 端点,其 data 是 URI\n * 而非 JSON,必须按事件名分流而不能当普通消息帧解析(否则被当成\"非 JSON 帧\"忽略)。\n * 只关心 data 的调用方可继续用单参回调,签名向后兼容。\n */\nexport function createSseDataParser(onData: (data: string, event: string) => void): SseDataParser {\n let buffer = ''\n let dataLines: string[] = []\n let eventName = ''\n\n const dispatch = (): void => {\n if (dataLines.length === 0) {\n eventName = ''\n return\n }\n const data = dataLines.join('\\n')\n const event = eventName || 'message'\n dataLines = []\n eventName = ''\n onData(data, event)\n }\n\n const consumeLine = (rawLine: string): void => {\n const line = rawLine.endsWith('\\r') ? rawLine.slice(0, -1) : rawLine\n if (line === '') {\n dispatch()\n return\n }\n if (line.startsWith(':')) {\n return\n }\n if (line.startsWith('data:')) {\n let value = line.slice(5)\n if (value.startsWith(' ')) {\n value = value.slice(1)\n }\n dataLines.push(value)\n return\n }\n if (line.startsWith('event:')) {\n let value = line.slice(6)\n if (value.startsWith(' ')) {\n value = value.slice(1)\n }\n eventName = value\n }\n }\n\n return {\n feed(chunk: string): void {\n buffer += chunk\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n consumeLine(line)\n }\n },\n flush(): void {\n if (buffer.length > 0) {\n consumeLine(buffer)\n buffer = ''\n }\n dispatch()\n },\n }\n}\n","/** JSON-RPC 2.0 协议版本字面量——单一真源,避免散落各处的裸字符串漂移。 */\nexport const JSON_RPC_VERSION = '2.0' as const\n\nexport interface JsonRpcRequest {\n jsonrpc: typeof JSON_RPC_VERSION\n id: number | string\n method: string\n params?: Record<string, unknown>\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: typeof JSON_RPC_VERSION\n id: number | string | null\n result?: unknown\n error?: JsonRpcError\n}\n\nexport interface JsonRpcNotification {\n jsonrpc: typeof JSON_RPC_VERSION\n method: string\n params?: Record<string, unknown>\n}\n\nexport interface JsonRpcError {\n code: number\n message: string\n data?: unknown\n}\n\n/**\n * 最小 JSON-RPC 2.0 形状校验——**语法门,不是语义门**。\n *\n * 动机:网关(尤其带登录态的企业网关)在鉴权失败时常回 `HTTP 200 + application/json`\n * 承载**业务**错误体(`{code:40100,message:'login expired'}`)。这类响应 `JSON.parse` 成功,\n * 若直接喂给 messageHandler,会因「没有 id」被当成 notification 静默丢弃 → 在途的\n * initialize 无人 resolve,一路挂到 requestTimeoutMs(线上 30s)才报超时,且错误信息\n * (\"timed out\")完全掩盖真实原因(登录态过期)。官方 @modelcontextprotocol/sdk 对同一\n * 响应走 schema 校验,毫秒级失败。\n *\n * 判定规则(对齐 JSON-RPC 2.0 §4/§5,刻意保持最小):\n * - 必须是非数组对象,且 `jsonrpc === '2.0'`;\n * - response:有 `id`(number|string|null)且有 `result` 或 `error` 之一;\n * - notification:有 string 型 `method`、无 `id`。\n *\n * 刻意**不**校验 result 的业务结构(那是 initialize/tools 各自的事),只挡\"根本不是\n * JSON-RPC 消息\"的帧,避免过度收紧误伤合法 server。\n */\nexport function isJsonRpcMessage(value: unknown): value is JsonRpcResponse | JsonRpcNotification {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const msg = value as Record<string, unknown>\n if (msg.jsonrpc !== JSON_RPC_VERSION) return false\n\n const hasId = 'id' in msg && msg.id !== undefined\n if (hasId) {\n const id = msg.id\n if (typeof id !== 'number' && typeof id !== 'string' && id !== null) return false\n // response 必须携带 result 或 error 之一;server→client 的**请求**(有 id + method)\n // 也是合法 JSON-RPC 形状,放行交由上层处理(当前 client 不处理,但不该在语法层拒掉)。\n return 'result' in msg || 'error' in msg || typeof msg.method === 'string'\n }\n return typeof msg.method === 'string'\n}\n\nexport interface McpServerCapabilities {\n tools?: { listChanged?: boolean }\n resources?: { subscribe?: boolean; listChanged?: boolean }\n prompts?: { listChanged?: boolean }\n}\n\nexport interface McpClientCapabilities {\n roots?: { listChanged?: boolean }\n sampling?: Record<string, unknown>\n}\n\nexport interface McpInitializeParams {\n protocolVersion: string\n capabilities: McpClientCapabilities\n clientInfo: { name: string; version: string }\n}\n\nexport interface McpInitializeResult {\n protocolVersion: string\n capabilities: McpServerCapabilities\n serverInfo: { name: string; version?: string }\n}\n\n/**\n * MCP 工具行为注解(MCP spec `Tool.annotations`)。仅作模型/调度提示,非安全保证。\n * 服务端在 tools/list 携带;client 透传原始 JSON,故运行时本就存在,此处补类型以供消费。\n */\nexport interface McpToolAnnotations {\n title?: string\n /** 只读工具:不改变环境,可与其他只读调用并发执行。 */\n readOnlyHint?: boolean\n destructiveHint?: boolean\n idempotentHint?: boolean\n openWorldHint?: boolean\n}\n\nexport interface McpToolDefinition {\n name: string\n description?: string\n inputSchema: McpJsonSchema\n annotations?: McpToolAnnotations\n}\n\nexport interface McpJsonSchema {\n type: string\n properties?: Record<string, McpJsonSchemaProperty>\n required?: string[]\n additionalProperties?: boolean\n [key: string]: unknown\n}\n\nexport interface McpJsonSchemaProperty {\n type: string\n description?: string\n enum?: unknown[]\n default?: unknown\n items?: McpJsonSchemaProperty\n properties?: Record<string, McpJsonSchemaProperty>\n required?: string[]\n [key: string]: unknown\n}\n\nexport interface McpToolCallParams {\n name: string\n arguments?: Record<string, unknown>\n}\n\nexport interface McpToolCallResult {\n content: McpContent[]\n isError?: boolean\n}\n\nexport type McpContent = McpTextContent | McpImageContent | McpResourceContent\n\nexport interface McpTextContent {\n type: 'text'\n text: string\n}\n\nexport interface McpImageContent {\n type: 'image'\n data: string\n mimeType: string\n}\n\nexport interface McpResourceContent {\n type: 'resource'\n resource: {\n uri: string\n text?: string\n blob?: string\n mimeType?: string\n }\n}\n\nexport interface McpResource {\n uri: string\n name: string\n description?: string\n mimeType?: string\n}\n\nexport interface McpResourceTemplate {\n uriTemplate: string\n name: string\n description?: string\n mimeType?: string\n}\n\nexport interface McpResourceContents {\n uri: string\n mimeType?: string\n text?: string\n blob?: string\n}\n\nexport interface McpPrompt {\n name: string\n description?: string\n arguments?: McpPromptArgument[]\n}\n\nexport interface McpPromptArgument {\n name: string\n description?: string\n required?: boolean\n}\n\nexport interface McpPromptMessage {\n role: 'user' | 'assistant'\n content: McpTextContent | McpImageContent | McpResourceContent\n}\n\nexport interface McpServerConfig {\n /**\n * 传输类型(显式判别)。缺省按 url/command 推断(向后兼容):有 url→http,有 command→stdio。\n * 'http'='sse'(同一 streamable HTTP transport,'http' 为首选名)。\n */\n type?: McpTransportType\n /** stdio 模式:命令 */\n command?: string\n /** stdio 模式:命令参数 */\n args?: string[]\n /** stdio 模式:环境变量 */\n env?: Record<string, string>\n /** streamable HTTP 模式:URL */\n url?: string\n /**\n * HTTP 模式静态请求头,随每次 GET(流)/POST(消息)发出。\n * 用于鉴权(`Authorization: Bearer …` / `X-API-Key: …`)及自定义头。来自 `.otto/config.json`\n * 的 `mcp_servers`(JSON 安全)。stdio 模式忽略。\n */\n headers?: Record<string, string>\n /** 请求超时(毫秒),覆盖 manager 级默认值 */\n requestTimeoutMs?: number\n /** 自动重连(默认 true) */\n autoReconnect?: boolean\n /** 最大重连次数(默认 3) */\n maxReconnectAttempts?: number\n /**\n * 是否由所在层管理(RFC-081)。\n * 仅 plugin 层有效。标记后用户不可在 /extension 面板中单独 disable/delete,\n * 只能通过卸载插件来移除该 MCP server。\n */\n managed?: boolean\n}\n\nexport type McpTransportType = 'stdio' | 'sse' | 'http'\n\nexport type McpClientStatus = 'disconnected' | 'connecting' | 'ready' | 'error' | 'reconnecting'\n\nexport interface McpServerInfo {\n name: string\n /** 禁用 server(无 client 实例)无从取真配置 → optional;活 client 经 getConfig() 填真值。 */\n config?: McpServerConfig\n status: McpClientStatus\n tools: McpToolDefinition[]\n resources?: McpResource[]\n prompts?: McpPrompt[]\n capabilities?: McpServerCapabilities\n error?: string\n}\n\nexport interface McpEvents {\n 'server.connected': (info: { name: string; tools: number }) => void\n /**\n * RFC-165 D8/H2:`kind` 区分预期内的显式断开(`'manual'`,如用户经 /extension 禁用、\n * McpController.stop() 主动断开)与传输层意外关闭(`'crash'`,子进程异常退出/重连耗尽)。\n * 消费方(如 `usePanelBackend()` 的 `status` 派生逻辑)据此区分 `disabled` 语境与\n * `crashed` 语境——`'manual'` 断开若同时命中\"用户刚调用了 setServerEnabled(false)\"这个\n * 应用层上下文,上层(`McpController`)可进一步归类为 `disabled`;本事件本身只提供\n * \"是否预期内\"这一层区分,不深入解析应用层意图。\n */\n 'server.disconnected': (info: { name: string; reason?: string; kind: 'manual' | 'crash' }) => void\n 'server.error': (info: { name: string; error: string; code?: string }) => void\n 'server.reconnecting': (info: { name: string; attempt: number }) => void\n 'tools.changed': (info: { serverName: string; tools: McpToolDefinition[] }) => void\n 'resources.changed': (info: { serverName: string }) => void\n /**\n * RFC-165 D9/H1:通用 notification 转发——未被 `tools.changed`/`resources.changed`\n * 语义化事件覆盖的 server→client notification(如插件 backend facet 的 `backend/state`)\n * 经此事件透传给上层消费方(`PanelBackendController` 等)。也会收到已被语义化事件覆盖的\n * notification(如 `notifications/tools/list_changed`),消费方按 `method` 自行过滤。\n */\n 'server.notification': (info: { name: string; method: string; params?: Record<string, unknown> }) => void\n}\n","import { spawn } from 'node:child_process'\nimport { createLogger, McpError, buildAllowedEnv } from '@x-otto/shared'\nimport { JSON_RPC_VERSION, isJsonRpcMessage } from './types'\nimport type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './types'\nimport { createSseDataParser } from './sse'\n\n/** 轻量进程句柄(避免 MCP 包直接依赖 @x-otto/runtime) */\nexport interface LightManagedProcess {\n readonly pid: number | undefined\n readonly stdin: NodeJS.WritableStream | null\n readonly stdout: NodeJS.ReadableStream | null\n readonly stderr: NodeJS.ReadableStream | null\n kill(): Promise<void>\n onError(callback: (err: Error) => void): void\n onExit(callback: () => void): void\n}\n\n/**\n * 进程 spawn 工厂:由宿主(coding/McpController)注入,内部 wrap @x-otto/runtime 的 ProcessRuntime,\n * 使 MCP 子进程纳入统一运行时(Layer 3 exit reaper 覆盖 + 可观测)。缺省时 StdioTransport 走\n * 向后兼容的私有 spawn(仍 buildAllowedEnv + detached)。\n */\nexport type ProcessSpawnFactory = (command: string, args: string[], env: Record<string, string>) => LightManagedProcess\n\nconst logger = createLogger('@x-otto/mcp:transport')\n\nconst SSE_DEFAULT_TIMEOUT_MS = 30_000\n\nconst MAX_MESSAGE_BYTES = 32 * 1024 * 1024\n\n/** 传输层错误码(JSON-RPC 保留区 -32000 段,服务端实现自定义)。 */\nconst TRANSPORT_ERROR = -32000\nconst TRANSPORT_UNAUTHORIZED = -32001\n/** 响应体不是合法 JSON-RPC 消息(网关业务错误体 / HTML 登录页等)。 */\nconst TRANSPORT_INVALID_PAYLOAD = -32002\n\n/** 截断网关原始错误体用于诊断——避免把整页 HTML 灌进日志/错误消息。 */\nfunction summarizePayload(text: string, limit = 200): string {\n const flat = text.replace(/\\s+/g, ' ').trim()\n return flat.length > limit ? `${flat.slice(0, limit)}…` : flat\n}\n\n/**\n * 官方 MCP stdio 分帧:每条 JSON-RPC 消息单行、`\\n` 分隔、正文不含内嵌换行\n * (JSON.stringify 会把字符串内换行转义为 `\\n` 字面量,故按字节 0x0A 切是安全的;\n * 多字节 UTF-8 续字节永不为 0x0A)。容忍 CRLF。返回完整行文本 + 剩余未成行字节。\n *\n * 健壮性:无换行且缓冲超 MAX_MESSAGE_BYTES → 复位(防 DoS)。\n */\nexport function decodeNewlineFrames(buffer: Buffer): { bodies: string[]; rest: Buffer } {\n const bodies: string[] = []\n let buf = buffer\n\n for (;;) {\n const nl = buf.indexOf(0x0a)\n if (nl === -1) {\n if (buf.length > MAX_MESSAGE_BYTES) {\n logger.warn('MCP newline frame exceeds limit without terminator; resetting buffer')\n buf = Buffer.alloc(0)\n }\n break\n }\n\n let line = buf.subarray(0, nl)\n if (line.length > 0 && line[line.length - 1] === 0x0d) {\n line = line.subarray(0, line.length - 1)\n }\n buf = buf.subarray(nl + 1)\n\n const text = line.toString('utf-8').trim()\n if (text) bodies.push(text)\n }\n\n return { bodies, rest: buf }\n}\n\ntype FetchLike = (input: string, init?: RequestInit) => Promise<Response>\n\nexport interface McpTransport {\n send(message: JsonRpcRequest | JsonRpcNotification): void | Promise<void>\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void\n /** 传输非预期断开(进程退出 / SSE 流结束或出错)时回调;client 据此触发自动重连。 */\n onClose(handler: (reason?: string) => void): void\n start(): Promise<void>\n close(): Promise<void>\n /**\n * 可选:注入\"当前是否有在途请求\"的探针。HTTP+SSE 传输用它判定\"流里只有坏帧\"是真故障\n * (有请求在干等)还是良性噪声(响应走 POST、流里只是厂商保活帧)。\n * 不实现该方法的传输(stdio / streamable HTTP)不受影响。\n */\n setPendingProbe?(probe: () => boolean): void\n}\n\n/**\n * 校验 MCP server URL:协议白名单 http/https,拒绝 file:/ftp:/data:/javascript: 等。\n * **不**拦本机/私网——本地 MCP server(localhost/127.0.0.1)是常态;DNS-rebinding 对长驻配置端点\n * 风险低,且 MCP 端点由运维配置(非 LLM 输入)。仅挡协议层误用与畸形 URL。\n */\nexport function validateMcpUrl(url: string): URL {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`Invalid MCP server URL: ${url}`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`Unsupported MCP server URL protocol: ${parsed.protocol} (use http/https)`)\n }\n return parsed\n}\n\nexport class StdioTransport implements McpTransport {\n private managedProcess: LightManagedProcess | null = null\n private messageHandler: ((msg: JsonRpcResponse | JsonRpcNotification) => void) | null = null\n private closeHandler: ((reason?: string) => void) | null = null\n private closing = false\n private buffer: Buffer = Buffer.alloc(0)\n private readonly command: string\n private readonly args: string[]\n private readonly env: Record<string, string>\n private readonly processSpawnFactory: ProcessSpawnFactory | undefined\n\n constructor(\n command: string,\n args: string[] = [],\n env: Record<string, string> = {},\n processSpawnFactory?: ProcessSpawnFactory,\n ) {\n this.command = command\n this.args = args\n this.env = env\n this.processSpawnFactory = processSpawnFactory\n }\n\n async start(): Promise<void> {\n if (this.processSpawnFactory) {\n // RFC-085 M2:通过注入的 spawn factory 管理进程(将接入 ProcessRuntime)\n this.managedProcess = this.processSpawnFactory(this.command, this.args, this.env)\n } else {\n // 向后兼容:直接 spawn\n const child = spawn(this.command, this.args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n env: buildAllowedEnv(this.env),\n detached: true, // C1\n })\n this.managedProcess = {\n pid: child.pid,\n stdin: child.stdin,\n stdout: child.stdout,\n stderr: child.stderr,\n kill: async (): Promise<void> => {\n child.stdin?.end()\n return new Promise<void>((resolve) => {\n const t = setTimeout(() => { child.kill('SIGKILL'); resolve() }, 2000)\n child.on('exit', () => { clearTimeout(t); resolve() })\n child.kill('SIGTERM')\n })\n },\n onError: (callback: (err: Error) => void): void => { child.on('error', callback) },\n onExit: (callback: () => void): void => { child.on('exit', () => callback()) },\n }\n }\n\n const proc = this.managedProcess\n const stdout = proc.stdout\n if (stdout) {\n stdout.on('data', (chunk: unknown) => {\n // RFC review D6a:增量分帧——不再每次 data 事件 Buffer.concat 整个未完成缓冲\n //(O(n²) 复制);只把「残留 + chunk 中最后一个换行之前」一次性交给帧解码,\n // 尾部残留原样持有。大响应下每字节至多参与一次拼接(线性)。\n this.pushChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string))\n })\n }\n\n const stderr = proc.stderr\n if (stderr) {\n stderr.on('data', (chunk: unknown) => {\n const s = Buffer.isBuffer(chunk) ? chunk.toString('utf-8').trimEnd() : String(chunk).trimEnd()\n logger.debug('MCP stderr: %s', s)\n })\n }\n\n proc.onError((err) => {\n logger.error('MCP process error: %s', err.message)\n })\n\n proc.onExit(() => {\n logger.debug('MCP process exited')\n if (!this.closing) {\n this.closeHandler?.('process exited')\n }\n this.managedProcess = null\n })\n }\n\n send(message: JsonRpcRequest | JsonRpcNotification): void {\n const stdin = this.managedProcess?.stdin\n if (!stdin?.writable) {\n throw new Error('MCP stdio transport is not connected')\n }\n\n const json = JSON.stringify(message)\n ;(stdin as NodeJS.WritableStream).write(json + '\\n')\n }\n\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void {\n this.messageHandler = handler\n }\n\n onClose(handler: (reason?: string) => void): void {\n this.closeHandler = handler\n }\n\n async close(): Promise<void> {\n this.closing = true\n if (this.managedProcess) {\n await this.managedProcess.kill()\n this.managedProcess = null\n }\n }\n\n /**\n * RFC review D6a:增量分帧——仅把「残留 buffer + chunk 中最后一个换行之前」的部分\n * 交给 decodeNewlineFrames(一次性、线性),换行之后的未完成尾部作为新残留持有。\n *\n * 语义与旧实现全等:\n * - 帧边界 = 换行(官方 MCP stdio 分帧);\n * - 无换行且残留超 MAX_MESSAGE_BYTES → 复位(防 DoS,同 decodeNewlineFrames 语义);\n * - 解析失败只告警不中断(同一消息级容错)。\n *\n * 复杂度:旧实现每 chunk 复制「全部残留 + chunk」(残留可含已收帧前缀 → O(n²));\n * 新实现每 chunk 只复制「残留 + 完整帧段」,残留段通常为空或单帧尾部 → O(总字节)。\n */\n private pushChunk(chunk: Buffer): void {\n if (chunk.length === 0) return\n const lastNl = chunk.lastIndexOf(0x0a)\n if (lastNl === -1) {\n // 无换行:追加到残留缓冲(超限复位防护,同 decodeNewlineFrames 的 rest 语义)\n if (this.buffer.length + chunk.length > MAX_MESSAGE_BYTES) {\n logger.warn('MCP newline frame exceeds limit without terminator; resetting buffer')\n this.buffer = Buffer.alloc(0)\n return\n }\n this.buffer = this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk])\n return\n }\n\n // 完整帧段 = 残留 + chunk[0..lastNl];decode 只消费一次\n const complete =\n this.buffer.length === 0\n ? chunk.subarray(0, lastNl + 1)\n : Buffer.concat([this.buffer, chunk.subarray(0, lastNl + 1)])\n const { bodies, rest } = decodeNewlineFrames(complete)\n\n // 残留 = decode 的 rest(恒为空,防御)+ chunk 尾部(lastNl 之后,通常为空)\n const tail = chunk.subarray(lastNl + 1)\n if (rest.length > 0 && tail.length > 0) {\n this.buffer = Buffer.concat([rest, tail])\n } else if (rest.length > 0) {\n this.buffer = rest\n } else {\n this.buffer = tail\n }\n\n for (const body of bodies) {\n try {\n const message = JSON.parse(body) as JsonRpcResponse | JsonRpcNotification\n this.messageHandler?.(message)\n } catch (err) {\n logger.warn('Failed to parse MCP message: %s', (err as Error).message)\n }\n }\n }\n}\n\nexport interface SseTransportOptions {\n /** 单次请求超时(ms),与传输级 abort 组合。缺省 30s。 */\n requestTimeoutMs?: number\n /** 注入 fetch(测试用)。缺省全局 fetch。 */\n fetchImpl?: FetchLike\n /**\n * 静态请求头,随每次 GET(流)/POST(消息)发出。鉴权(`Authorization: Bearer …` /\n * `X-API-Key`)及自定义头。与内置头(Accept/Content-Type/Mcp-Session-Id)合并——**内置头优先**,\n * 防止误覆盖协议必需头。\n */\n headers?: Record<string, string>\n /**\n * 动态 Bearer token provider(OAuth 2.1)。每次请求前调用,返回的 access token\n * 注入 `Authorization: Bearer <token>` 头。**优先级最高**——覆盖静态 headers 中的 Authorization。\n * 返回 undefined 时跳过(用静态 headers 中的 Authorization)。\n */\n getAuthToken?: () => Promise<string | undefined>\n}\n\nexport class SseTransport implements McpTransport {\n private messageHandler: ((msg: JsonRpcResponse | JsonRpcNotification) => void) | null = null\n private closeHandler: ((reason?: string) => void) | null = null\n private abortController: AbortController | null = null\n private sessionId: string | null = null\n private closing = false\n private readonly baseUrl: string\n private readonly fetchImpl: FetchLike\n private readonly requestTimeoutMs: number\n private readonly headers: Record<string, string>\n private readonly getAuthToken?: () => Promise<string | undefined>\n /**\n * 规范(MCP 2024-11-05 HTTP+SSE)要求:连接建立后服务端先发 `event: endpoint`,data 为\n * 客户端**发消息专用**的 URI,\"All subsequent client messages MUST be sent as HTTP POST\n * requests to this endpoint\"。此前实现不消费该事件、POST 一律打到 baseUrl——对 SSE 端点与\n * POST 端点分离的网关(规范的标准形态)直接连不上,且 endpoint 事件的 data 因不是 JSON\n * 被当\"非 JSON 帧\"忽略。此处持有解析后的绝对 URL(相对 URI 以 baseUrl 为基准解析)。\n */\n private messageEndpoint: string | null = null\n /** endpoint 事件先于首个 send 到达的等待闩:首个 send 最多等它一小会儿。 */\n private endpointWaiters: Array<() => void> = []\n /**\n * endpoint 归属已确定(收到事件,或主流已结束/出错证明不会再来)。置位后 send 不再等待——\n * 不发 endpoint 事件的服务端(SSE 与 POST 同路径)必须零额外延迟地回落 baseUrl。\n */\n private endpointSettled = false\n /** 由 McpClient 注入:当前是否有在途请求在等应答(见 armViolationTimer 第 ④ 条件)。 */\n private hasPendingRequests?: () => boolean\n\n constructor(url: string, options: SseTransportOptions = {}) {\n this.baseUrl = validateMcpUrl(url).href\n this.fetchImpl = options.fetchImpl ?? (globalThis.fetch as FetchLike)\n this.requestTimeoutMs = options.requestTimeoutMs ?? SSE_DEFAULT_TIMEOUT_MS\n this.headers = options.headers ?? {}\n this.getAuthToken = options.getAuthToken\n }\n\n /**\n * POST 目标:优先用服务端经 `endpoint` 事件下发的地址;未下发则回落 baseUrl\n * (兼容那些把 SSE 与 POST 合并在同一路径、不发 endpoint 事件的实现)。\n */\n private get postUrl(): string {\n return this.messageEndpoint ?? this.baseUrl\n }\n\n /** 记录服务端下发的消息端点;相对 URI 以 baseUrl 解析,非法值忽略并告警。 */\n private setMessageEndpoint(raw: string): void {\n const value = raw.trim()\n if (!value) return\n let resolved: URL\n try {\n resolved = new URL(value, this.baseUrl)\n } catch {\n logger.warn('MCP SSE endpoint event carried an unparsable URI: %s', summarizePayload(value))\n return\n }\n if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:') {\n logger.warn('MCP SSE endpoint event used unsupported protocol: %s', resolved.protocol)\n return\n }\n this.messageEndpoint = resolved.href\n logger.debug('MCP SSE message endpoint set to %s', resolved.href)\n this.settleEndpoint()\n }\n\n /** 解除所有等待者:endpoint 已确定,或已确定不会再到(主流结束/出错/关闭)。 */\n private settleEndpoint(): void {\n this.endpointSettled = true\n const waiters = this.endpointWaiters\n this.endpointWaiters = []\n for (const resume of waiters) resume()\n }\n\n /**\n * 首个 send 可能早于 endpoint 事件到达(GET 响应头一回来 start() 就 resolve,流体是异步的)。\n * 等待窗口很短——等不到就按回落地址(baseUrl)发,绝不把请求拖到超时。\n *\n * 窗口必须**远小于**坏帧协议违例的宽限期:不发 endpoint 事件的服务端(SSE 与 POST 同\n * 路径)很常见,若在此空等得太久,请求迟迟发不出去,反而会被违例判定当成\"久等无应答\"\n * 而误杀健康连接(实测复现过)。200ms 足够覆盖\"事件已在飞行途中\"的正常情形。\n */\n private async awaitMessageEndpoint(): Promise<void> {\n // 没有活跃的主流(未 start 或已 close)→ 不可能收到 endpoint 事件,立即回落,不空等。\n if (this.messageEndpoint || this.endpointSettled || this.closing || !this.abortController) {\n return\n }\n const budget = Math.min(this.requestTimeoutMs, 200)\n await new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n // 窗口到期仍无 endpoint 事件 → 判定该服务端不发此事件(SSE 与 POST 同路径),\n // **永久**置位回落。否则长连不结束时 endpointSettled 永不置位,之后每个请求都要\n // 重新空等一次这个窗口(实测每次固定多付 ~200ms)。\n this.settleEndpoint()\n resolve()\n }, budget)\n timer.unref?.()\n const resume = (): void => {\n clearTimeout(timer)\n resolve()\n }\n this.endpointWaiters.push(resume)\n })\n }\n\n /** 解析本次请求的 headers:静态 headers + 动态 Bearer(最高优先)+ 内置协议头覆盖。 */\n private async resolveHeaders(\n ...builtin: Record<string, string>[]\n ): Promise<Record<string, string>> {\n let merged = { ...this.headers }\n\n if (this.getAuthToken) {\n const token = await this.getAuthToken()\n if (token) {\n merged = { ...merged, Authorization: `Bearer ${token}` }\n }\n }\n\n for (const override of builtin) {\n merged = { ...merged, ...override }\n }\n return merged\n }\n\n /**\n * 认证中间层——401 时自动刷新 token 并重试一次(handle_401 → refresh → retry)。\n */\n private async fetchWithAuthRetry(\n method: 'GET' | 'POST',\n body?: string,\n signal?: AbortSignal,\n ): Promise<Response> {\n const builtin: Record<string, string> =\n method === 'GET'\n ? { Accept: 'text/event-stream' }\n : {\n 'Content-Type': 'application/json',\n ...(this.sessionId ? { 'Mcp-Session-Id': this.sessionId } : {}),\n }\n\n // GET 走 SSE 端点(baseUrl);POST 走服务端下发的消息端点(规范要求),未下发则回落 baseUrl。\n const target = method === 'GET' ? this.baseUrl : this.postUrl\n const headers = await this.resolveHeaders(builtin)\n const response = await this.fetchImpl(target, {\n method,\n headers,\n body,\n signal,\n })\n\n if (response.status === 401 && this.getAuthToken) {\n logger.debug('MCP 401 on %s %s — attempting token refresh and retry', method, target)\n\n const newToken = await this.getAuthToken()\n if (newToken) {\n const retryHeaders = await this.resolveHeaders(builtin)\n return this.fetchImpl(target, {\n method,\n headers: retryHeaders,\n body,\n signal,\n })\n }\n\n logger.warn('MCP token refresh returned no token for %s', target)\n }\n\n return response\n }\n\n async start(): Promise<void> {\n this.closing = false\n this.messageEndpoint = null\n this.endpointWaiters = []\n this.endpointSettled = false\n this.abortController = new AbortController()\n\n const response = await this.fetchWithAuthRetry('GET', undefined, this.abortController.signal)\n\n if (!response.ok) {\n if (response.status === 401) {\n // 协议层只报中性事实 + 错误码,**不**嵌宿主 CLI 文案\n // ('otto mcp login' 是 otto CLI 词汇,宿主知识)。actionable 指引由宿主据 code 渲染。\n throw new McpError(`MCP server '${this.baseUrl}' requires authentication.`, {\n code: 'MCP_UNAUTHORIZED',\n })\n }\n throw new Error(`MCP SSE connection failed: ${response.status} ${response.statusText}`)\n }\n\n const sessionId = response.headers.get('mcp-session-id')\n if (sessionId) {\n this.sessionId = sessionId\n }\n\n void this.consumeStream(response, true)\n }\n\n async send(message: JsonRpcRequest | JsonRpcNotification): Promise<void> {\n const perRequest = new AbortController()\n const timer = setTimeout(() => {\n perRequest.abort(new DOMException('MCP request timeout', 'TimeoutError'))\n }, this.requestTimeoutMs)\n timer.unref?.()\n const signal = this.abortController\n ? AbortSignal.any([this.abortController.signal, perRequest.signal])\n : perRequest.signal\n\n // 首个消息可能早于 endpoint 事件;短暂等待后按 postUrl 发(等不到则回落 baseUrl)。\n await this.awaitMessageEndpoint()\n\n void this.fetchWithAuthRetry('POST', JSON.stringify(message), signal)\n .then(async (resp) => {\n clearTimeout(timer)\n if (!resp.ok) {\n if (resp.status === 401) {\n logger.warn('MCP SSE POST 401: authentication required for %s', this.baseUrl)\n // 中性 reason + 鉴权错误码(-32001);宿主据码渲染 CLI 指引。\n this.failRequest(message, TRANSPORT_UNAUTHORIZED, `MCP server requires authentication.`)\n return\n }\n logger.warn('MCP SSE POST failed: %s', resp.status)\n this.failRequest(message, TRANSPORT_ERROR, `MCP SSE POST failed: HTTP ${resp.status}`)\n return\n }\n\n const sid = resp.headers.get('mcp-session-id')\n if (sid) {\n this.sessionId = sid\n }\n\n const contentType = resp.headers.get('content-type') ?? ''\n if (contentType.includes('application/json')) {\n // 与 streamable HTTP 同理:JSON.parse 成功不代表是 JSON-RPC 消息。网关登录态过期\n // 常回 200 + 业务错误体,旧实现把它当 notification 丢掉 → 在途请求挂到超时。\n const text = await resp.text()\n if (!text.trim()) return\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n logger.warn('MCP SSE POST returned a non-JSON body: %s', summarizePayload(text))\n this.failRequest(\n message,\n TRANSPORT_INVALID_PAYLOAD,\n `MCP server returned a non-JSON body: ${summarizePayload(text)}`,\n )\n return\n }\n const batch = Array.isArray(parsed) ? parsed : [parsed]\n if (batch.some((m) => !isJsonRpcMessage(m))) {\n logger.warn('MCP SSE POST returned a non-JSON-RPC payload: %s', summarizePayload(text))\n this.failRequest(\n message,\n TRANSPORT_INVALID_PAYLOAD,\n `MCP server returned a non-JSON-RPC payload: ${summarizePayload(text)}`,\n )\n return\n }\n for (const m of batch) {\n this.messageHandler?.(m as JsonRpcResponse | JsonRpcNotification)\n }\n } else if (contentType.includes('text/event-stream')) {\n void this.consumeStream(resp, false)\n }\n })\n .catch((err: Error) => {\n clearTimeout(timer)\n if (err.name !== 'AbortError') {\n logger.warn('MCP SSE send error: %s', err.message)\n this.failRequest(message, TRANSPORT_ERROR, `MCP SSE send error: ${err.message}`)\n }\n })\n }\n\n private failRequest(\n message: JsonRpcRequest | JsonRpcNotification,\n code: number,\n reason: string,\n ): void {\n if ('id' in message && message.id !== undefined && message.id !== null) {\n this.messageHandler?.({ jsonrpc: JSON_RPC_VERSION, id: message.id, error: { code, message: reason } })\n }\n }\n\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void {\n this.messageHandler = handler\n }\n\n onClose(handler: (reason?: string) => void): void {\n this.closeHandler = handler\n }\n\n setPendingProbe(probe: () => boolean): void {\n this.hasPendingRequests = probe\n }\n\n async close(): Promise<void> {\n this.closing = true\n // 解除任何在等 endpoint 事件的 send,避免关闭后仍有请求空等宽限期。\n this.settleEndpoint()\n this.abortController?.abort()\n this.abortController = null\n }\n\n private async consumeStream(response: Response, isPrimary: boolean): Promise<void> {\n const reader = response.body?.getReader()\n if (!reader) {\n if (isPrimary) this.settleEndpoint()\n if (isPrimary && !this.closing) {\n this.closeHandler?.('SSE stream had no readable body')\n }\n return\n }\n\n const decoder = new TextDecoder()\n // 坏帧不再只打 debug:计数 + 留样。主流(GET)读到流尾若积累了坏帧,把它们作为断开\n // 原因带给 closeHandler——之前网关回鉴权错误帧时这里完全静默,请求一路挂到超时。\n let badFrames = 0\n let firstBadSample = ''\n let validFrames = 0\n let violationTimer: ReturnType<typeof setTimeout> | null = null\n\n const clearViolationTimer = (): void => {\n if (violationTimer) {\n clearTimeout(violationTimer)\n violationTimer = null\n }\n }\n\n /**\n * 协议违例判定(刻意保守,防误杀)。触发需**同时**满足:\n * ① 这条主流从未产出合法 MCP 消息;② 已出现坏帧;③ 宽限期内仍无合法消息;\n * ④ **当前确有在途请求在等应答**(`pending` 非空)。\n *\n * 第 ④ 条是关键防线:HTTP+SSE 传输下响应既可能经 SSE 流回,也可能经 POST 响应体回\n * (本实现两条路径都支持)。若只看\"这条流有没有合法帧\",那么\"响应全走 POST + 流里\n * 只有厂商保活帧(如 `{\"type\":\"ping\"}`,JSON 合法但非 JSON-RPC)\"的健康服务端会被\n * 冤杀——实测确认过这一误杀。加上\"有请求真的在等\"才判违例,正好只覆盖\n * \"网关在用错误体应答、请求永远等不到\"的真实故障。\n */\n const armViolationTimer = (): void => {\n if (!isPrimary || violationTimer || validFrames > 0) return\n // 探针未注入(裸用 transport,无 McpClient)→ 无从判断\"是否真有人在等\",\n // 不武装定时器:既不误杀,也不留下永远重排的空转定时器。\n if (!this.hasPendingRequests) return\n // 宽限期按请求预算的 1/4 派生并封顶 2s:线上 30s 预算 → 2s 出局(相对 30s 可忽略),\n // 短预算场景按比例缩小,保证\"快速失败\"在任何配置下都明显早于请求超时本身。\n const grace = Math.min(Math.max(this.requestTimeoutMs / 4, 250), 2_000)\n // 重排上界:覆盖到 requestTimeoutMs 为止即可——再往后请求本身会超时,判不判违例\n // 都已无意义。有界循环避免长连坏帧流留下永远空转的定时器。\n const deadline = Date.now() + this.requestTimeoutMs\n const check = (): void => {\n violationTimer = null\n // 复查:宽限期内可能已收到合法帧,或已关闭 → 撤销判定。\n if (validFrames > 0 || this.closing) return\n // 还没有\"久等不到应答\"的请求 → 可能响应正走 POST 路径,尚不能判故障,稍后再查。\n if (!this.hasPendingRequests?.()) {\n if (Date.now() >= deadline) return\n violationTimer = setTimeout(check, grace)\n violationTimer.unref?.()\n return\n }\n logger.warn(\n 'MCP SSE stream produced %d unusable frame(s) and no valid MCP message; treating as protocol failure',\n badFrames,\n )\n this.closeHandler?.(\n `SSE stream carried ${badFrames} unusable frame(s) and no valid MCP message; first: ${firstBadSample}`,\n )\n }\n violationTimer = setTimeout(check, grace)\n violationTimer.unref?.()\n }\n\n const parser = createSseDataParser((data, event) => {\n const trimmed = data.trim()\n if (!trimmed || trimmed === '[DONE]') {\n return\n }\n // 规范事件:服务端下发消息 POST 端点。data 是 URI 而非 JSON,必须按事件名分流。\n if (event === 'endpoint') {\n this.setMessageEndpoint(trimmed)\n return\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(trimmed)\n } catch {\n badFrames++\n if (!firstBadSample) firstBadSample = summarizePayload(trimmed)\n logger.warn('MCP SSE non-JSON frame ignored: %s', summarizePayload(trimmed))\n armViolationTimer()\n return\n }\n if (!isJsonRpcMessage(parsed)) {\n badFrames++\n if (!firstBadSample) firstBadSample = summarizePayload(trimmed)\n logger.warn('MCP SSE non-JSON-RPC frame ignored: %s', summarizePayload(trimmed))\n armViolationTimer()\n return\n }\n validFrames++\n clearViolationTimer()\n this.messageHandler?.(parsed as JsonRpcResponse | JsonRpcNotification)\n })\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n parser.feed(decoder.decode(value, { stream: true }))\n }\n parser.flush()\n if (isPrimary && !this.closing) {\n this.closeHandler?.(\n badFrames > 0\n ? `SSE stream ended after ${badFrames} unusable frame(s); first: ${firstBadSample}`\n : 'SSE stream ended',\n )\n }\n } catch (err) {\n if ((err as Error).name !== 'AbortError') {\n logger.warn('MCP SSE stream error: %s', (err as Error).message)\n if (isPrimary && !this.closing) {\n this.closeHandler?.(`SSE stream error: ${(err as Error).message}`)\n }\n }\n } finally {\n clearViolationTimer()\n // 主流已终结 → endpoint 事件不可能再到,解除等待者(不发 endpoint 的服务端零延迟回落)。\n if (isPrimary) this.settleEndpoint()\n reader.releaseLock()\n }\n }\n}\n\n/**\n * Streamable HTTP transport(MCP 2025-03-26 规范)——单端点、POST 驱动。\n *\n * 与 {@link SseTransport}(旧版 2024-11-05 HTTP+SSE:GET 开长连 SSE 流 + POST 发消息)的\n * 本质区别:**不**先 GET 开流。每条 JSON-RPC 消息由 POST 发到同一端点,服务端按需\n * 用 `application/json`(单条响应)或 `text/event-stream`(流式多条)回复,客户端据\n * `Content-Type` 分流消费。多数现代 MCP server 只实现本协议——对\n * 初始 GET 直接返回 JSON/关流,导致 SseTransport 报 \"SSE stream ended\"。\n *\n * 会话:`initialize` 响应的 `Mcp-Session-Id` 头被捕获并回填到后续 POST。通知(无 id)\n * 服务端通常回 202 空体,静默即可。start() 不建连——首个 initialize POST 才触发握手。\n */\nexport class StreamableHttpTransport implements McpTransport {\n private messageHandler: ((msg: JsonRpcResponse | JsonRpcNotification) => void) | null = null\n private closeHandler: ((reason?: string) => void) | null = null\n private abortController: AbortController | null = null\n private sessionId: string | null = null\n private closing = false\n private readonly baseUrl: string\n private readonly fetchImpl: FetchLike\n private readonly requestTimeoutMs: number\n private readonly headers: Record<string, string>\n private readonly getAuthToken?: () => Promise<string | undefined>\n\n constructor(url: string, options: SseTransportOptions = {}) {\n this.baseUrl = validateMcpUrl(url).href\n this.fetchImpl = options.fetchImpl ?? (globalThis.fetch as FetchLike)\n this.requestTimeoutMs = options.requestTimeoutMs ?? SSE_DEFAULT_TIMEOUT_MS\n this.headers = options.headers ?? {}\n this.getAuthToken = options.getAuthToken\n }\n\n private async resolveHeaders(\n ...builtin: Record<string, string>[]\n ): Promise<Record<string, string>> {\n let merged = { ...this.headers }\n if (this.getAuthToken) {\n const token = await this.getAuthToken()\n if (token) merged = { ...merged, Authorization: `Bearer ${token}` }\n }\n for (const override of builtin) merged = { ...merged, ...override }\n return merged\n }\n\n async start(): Promise<void> {\n // Streamable HTTP 无独立握手——首个 send(initialize) 即建立会话。start() 仅重置状态。\n this.closing = false\n this.abortController = new AbortController()\n }\n\n async send(message: JsonRpcRequest | JsonRpcNotification): Promise<void> {\n const perRequest = new AbortController()\n const timer = setTimeout(() => {\n perRequest.abort(new DOMException('MCP request timeout', 'TimeoutError'))\n }, this.requestTimeoutMs)\n timer.unref?.()\n const signal = this.abortController\n ? AbortSignal.any([this.abortController.signal, perRequest.signal])\n : perRequest.signal\n\n const builtin: Record<string, string> = {\n 'Content-Type': 'application/json',\n // 两种回复都可能——服务端据请求选 json(单条)或 event-stream(流式)\n Accept: 'application/json, text/event-stream',\n ...(this.sessionId ? { 'Mcp-Session-Id': this.sessionId } : {}),\n }\n const body = JSON.stringify(message)\n\n let response: Response\n try {\n response = await this.fetchImpl(this.baseUrl, {\n method: 'POST',\n headers: await this.resolveHeaders(builtin),\n body,\n signal,\n })\n if (response.status === 401 && this.getAuthToken) {\n const newToken = await this.getAuthToken()\n if (newToken) {\n response = await this.fetchImpl(this.baseUrl, {\n method: 'POST',\n headers: await this.resolveHeaders(builtin),\n body,\n signal,\n })\n }\n }\n } catch (err) {\n clearTimeout(timer)\n if ((err as Error).name !== 'AbortError' && !this.closing) {\n this.closeHandler?.(`HTTP POST failed: ${(err as Error).message}`)\n }\n return\n }\n\n // 会话 id 只在 initialize 响应下发一次,之后回填\n const sid = response.headers.get('mcp-session-id')\n if (sid) this.sessionId = sid\n\n if (!response.ok) {\n clearTimeout(timer)\n if (response.status === 401) {\n if (!this.closing) this.closeHandler?.('unauthorized')\n return\n }\n // 202 之外的非 2xx——通知场景服务端常回 202 空体,属正常\n if (response.status !== 202 && !this.closing) {\n this.closeHandler?.(`HTTP ${response.status} ${response.statusText}`)\n }\n return\n }\n\n const contentType = (response.headers.get('content-type') ?? '').toLowerCase()\n if (contentType.includes('text/event-stream')) {\n // 流式回复:SSE 帧逐条派发,读到流尾即完成(非传输断开,isPrimary=false 不触发重连)\n await this.consumeSseResponse(response, message)\n clearTimeout(timer)\n return\n }\n\n // 单条 JSON 回复(或 202 空体通知)\n clearTimeout(timer)\n const text = await response.text()\n if (!text.trim()) return\n\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch (err) {\n logger.warn('MCP streamable-http non-JSON response body: %s', (err as Error).message)\n this.failRequest(\n message,\n TRANSPORT_INVALID_PAYLOAD,\n `MCP server returned a non-JSON body: ${summarizePayload(text)}`,\n )\n return\n }\n\n // JSON.parse 成功 ≠ 是 JSON-RPC 消息。网关登录态过期常回 HTTP 200 + 业务错误体\n // (`{code:40100,message:'login expired'}`)——它没有 id,旧实现会当 notification 丢掉,\n // 在途 initialize 无人 resolve,一直挂到 requestTimeoutMs。此处形状校验不通过即\n // **立刻 reject 在途请求**,把网关原始错误体带回给调用方(毫秒级失败 + 可诊断)。\n const batch = Array.isArray(parsed) ? parsed : [parsed]\n const invalid = batch.filter((m) => !isJsonRpcMessage(m))\n if (invalid.length > 0) {\n logger.warn(\n 'MCP streamable-http response is not a JSON-RPC message (%d frame(s)): %s',\n invalid.length,\n summarizePayload(text),\n )\n this.failRequest(\n message,\n TRANSPORT_INVALID_PAYLOAD,\n `MCP server returned a non-JSON-RPC payload: ${summarizePayload(text)}`,\n )\n return\n }\n\n for (const m of batch) {\n this.messageHandler?.(m as JsonRpcResponse | JsonRpcNotification)\n }\n }\n\n /**\n * 把传输层故障投递为在途请求的 JSON-RPC error 响应——让 `McpClient.request` 的 pending\n * 立即 reject,而不是干等 requestTimeoutMs。通知(无 id)无在途请求,静默即可。\n */\n private failRequest(\n message: JsonRpcRequest | JsonRpcNotification,\n code: number,\n reason: string,\n ): void {\n if ('id' in message && message.id !== undefined && message.id !== null) {\n this.messageHandler?.({\n jsonrpc: JSON_RPC_VERSION,\n id: message.id,\n error: { code, message: reason },\n })\n }\n }\n\n private async consumeSseResponse(\n response: Response,\n request: JsonRpcRequest | JsonRpcNotification,\n ): Promise<void> {\n const reader = response.body?.getReader()\n if (!reader) return\n const decoder = new TextDecoder()\n // 坏帧不再静默:计数 + 留样,流结束时若在途请求仍未被任何合法帧应答,据此 reject\n // (否则同样挂到 requestTimeoutMs)。\n let badFrames = 0\n let firstBadSample = ''\n const requestId = 'id' in request ? request.id : undefined\n let answered = requestId === undefined || requestId === null\n const parser = createSseDataParser((data) => {\n const trimmed = data.trim()\n if (!trimmed || trimmed === '[DONE]') return\n let parsed: unknown\n try {\n parsed = JSON.parse(trimmed)\n } catch {\n badFrames++\n if (!firstBadSample) firstBadSample = summarizePayload(trimmed)\n logger.warn('MCP streamable-http non-JSON SSE frame: %s', summarizePayload(trimmed))\n return\n }\n if (!isJsonRpcMessage(parsed)) {\n badFrames++\n if (!firstBadSample) firstBadSample = summarizePayload(trimmed)\n logger.warn('MCP streamable-http non-JSON-RPC SSE frame: %s', summarizePayload(trimmed))\n return\n }\n // 只有 id 匹配本次请求的响应帧才算「已应答」——服务端在同一流里推的 progress\n // 通知(无 id)或其它请求的响应都不能解除本请求的挂起判定。\n const frameId = (parsed as JsonRpcResponse).id\n if (!answered && frameId !== undefined && frameId === requestId) answered = true\n this.messageHandler?.(parsed as JsonRpcResponse | JsonRpcNotification)\n })\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n parser.feed(decoder.decode(value, { stream: true }))\n }\n parser.flush()\n // 流已走完而本次请求始终没等到匹配 id 的响应 → 不再干等超时,立即 reject。\n // 坏帧样本随错误带回(网关鉴权错误体通常就在里面),让失败可诊断。\n if (!answered) {\n this.failRequest(\n request,\n badFrames > 0 ? TRANSPORT_INVALID_PAYLOAD : TRANSPORT_ERROR,\n badFrames > 0\n ? `MCP stream ended after ${badFrames} unusable frame(s) without a response; first: ${firstBadSample}`\n : 'MCP stream ended without a response to this request',\n )\n }\n } catch (err) {\n if ((err as Error).name !== 'AbortError') {\n logger.warn('MCP streamable-http stream error: %s', (err as Error).message)\n this.failRequest(\n request,\n TRANSPORT_ERROR,\n `MCP stream error: ${(err as Error).message}`,\n )\n }\n } finally {\n reader.releaseLock()\n }\n }\n\n onMessage(handler: (message: JsonRpcResponse | JsonRpcNotification) => void): void {\n this.messageHandler = handler\n }\n\n onClose(handler: (reason?: string) => void): void {\n this.closeHandler = handler\n }\n\n async close(): Promise<void> {\n this.closing = true\n this.abortController?.abort()\n this.abortController = null\n }\n}\n\n// transport 工厂族(createStdioTransport/createSseTransport/createHttpTransport)已删——\n// 零生产消费,mcp-client 直接 new StdioTransport/SseTransport,仅测试曾用工厂便捷构造(RFC-074 M10)。\n","import { McpError, createLogger } from '@x-otto/shared'\nimport { StdioTransport, SseTransport, StreamableHttpTransport } from './transport'\n\nimport type { McpTransport, ProcessSpawnFactory } from './transport'\nimport { JSON_RPC_VERSION } from './types'\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n JsonRpcNotification,\n McpInitializeResult,\n McpToolDefinition,\n McpToolCallParams,\n McpToolCallResult,\n McpServerConfig,\n McpClientStatus,\n McpServerCapabilities,\n McpResource,\n McpResourceContents,\n McpPrompt,\n McpPromptMessage,\n} from './types'\n\nconst logger = createLogger('@x-otto/mcp:client')\n\nconst MCP_PROTOCOL_VERSION = '2024-11-05'\n\n/**\n * 连接阶段默认预算(ms)。取值理由:正常 MCP server(含 stdio 子进程冷启动)握手在 1-3s\n * 量级,10s 留足余量;而挂起的 server 被限制在 10s 内出局,不再拖满 requestTimeoutMs(30s)。\n */\nconst DEFAULT_CONNECT_TIMEOUT_MS = 10_000\n\n/**\n * RFC-165 D8/H2:断连分类——区分\"预期内的显式断开\"(`kind: 'manual'`,如\n * `McpClient.disconnect()` 被调用)与\"传输层意外关闭\"(`kind: 'crash'`,如子进程异常退出、\n * SSE 流出错、重连耗尽)。这是最小可行区分(不深入解析子进程 exit code),供上层\n * (`McpManager`/`McpController`)据此派生更细的语义(如\"用户经 /extension 禁用\" vs\n * \"server 真的崩了\"——那一层区分需要 disable 调用方上下文,留给 `McpManager`/\n * `McpController` 在 `kind: 'manual'` 基础上进一步标注,见 D8 备注)。\n */\nexport interface DisconnectInfo {\n reason?: string\n kind: 'manual' | 'crash'\n}\nconst MCP_CLIENT_NAME = 'otto-coding'\nconst MCP_CLIENT_VERSION = '0.0.1'\n\ninterface PendingRequest {\n resolve: (value: unknown) => void\n reject: (reason: Error) => void\n timer: ReturnType<typeof setTimeout>\n /** 发出时刻(ms)。用于判定\"等了很久仍无应答\",见 hasStalePending。 */\n sentAt: number\n}\n\n/**\n * 判定\"在途请求已等太久\"的阈值(ms)。取值需明显大于正常往返(本地/同城网关在\n * 毫秒~百毫秒量级),又远小于 requestTimeoutMs(线上 30s),使协议违例能快速出局。\n */\nconst STALE_PENDING_MS = 1_500\n\nexport interface McpClientOptions {\n /** 请求超时(毫秒),默认 30000 */\n requestTimeoutMs?: number\n /**\n * 连接阶段超时(毫秒),默认 10000。覆盖 `connect()` 全程(transport.start + initialize\n * + tools/list + 可选的 resources/prompts),超时即中止本次连接并抛 MCP_CONNECT_TIMEOUT。\n *\n * 与 requestTimeoutMs 分离:连接阶段卡死会拖住宿主装配(connectAll 被 await),而单次\n * 业务请求慢是可接受的。设 0 表示不启用连接阶段超时(回到旧行为)。\n */\n connectTimeoutMs?: number\n /** 自动重连(默认 true) */\n autoReconnect?: boolean\n /** 最大重连次数(默认 3) */\n maxReconnectAttempts?: number\n /** 重连退避基数(毫秒,默认 500;第 n 次=base·2^(n-1))。测试可设小值。 */\n reconnectDelayMs?: number\n /** 注入 transport 工厂(测试用)。缺省按 config 构造 stdio/sse。 */\n transportFactory?: () => McpTransport\n /**\n * 注入进程 spawn 工厂(RFC-085 M2):stdio transport 用它把 MCP 子进程纳入 ProcessRuntime。\n * 缺省时 StdioTransport 走向后兼容私有 spawn(仍 buildAllowedEnv + detached)。\n */\n processSpawnFactory?: ProcessSpawnFactory\n /**\n * 动态 Bearer token provider(OAuth 2.1)。\n * 每次 HTTP 请求前调用,返回的 access token 注入 Authorization 头。\n * 由调用方(McpManager 或外部)注入 TokenProvider.getAccessToken。\n */\n getAuthToken?: () => Promise<string | undefined>\n}\n\nexport class McpClient {\n readonly serverName: string\n private transport: McpTransport | null = null\n private config: McpServerConfig\n private options: McpClientOptions\n\n private status: McpClientStatus = 'disconnected'\n private serverInfo: McpInitializeResult | null = null\n private capabilities: McpServerCapabilities | null = null\n private tools: McpToolDefinition[] = []\n private resources: McpResource[] = []\n private prompts: McpPrompt[] = []\n private nextRequestId = 1\n private pending = new Map<number | string, PendingRequest>()\n private closing = false\n private reconnectAttempts = 0\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null\n private toolsChangedCallback: (() => void) | null = null\n private resourcesChangedCallback: (() => void) | null = null\n private disconnectedCallback: ((info: DisconnectInfo) => void) | null = null\n /** RFC-165 D9:通用 notification 订阅——server 发出的、未被上面专用处理的 method\n * (如 `backend/state`)经此回调透传给上层,不再被 `logger.debug` 静默丢弃。\n * 对全部 notification(含已被专用 switch-case 处理的)都会调用,让消费方可按需扩展。 */\n private notificationCallback: ((method: string, params?: Record<string, unknown>) => void) | null = null\n\n constructor(serverName: string, config: McpServerConfig, options: McpClientOptions = {}) {\n this.serverName = serverName\n this.config = config\n this.options = options\n }\n\n getStatus(): McpClientStatus {\n return this.status\n }\n\n getConfig(): McpServerConfig {\n return this.config\n }\n\n getTools(): McpToolDefinition[] {\n return this.tools\n }\n\n getResources(): McpResource[] {\n return this.resources\n }\n\n getPrompts(): McpPrompt[] {\n return this.prompts\n }\n\n getCapabilities(): McpServerCapabilities | null {\n return this.capabilities\n }\n\n getServerInfo(): McpInitializeResult | null {\n return this.serverInfo\n }\n\n onToolsChanged(callback: () => void): void {\n this.toolsChangedCallback = callback\n }\n\n onResourcesChanged(callback: () => void): void {\n this.resourcesChangedCallback = callback\n }\n\n onDisconnected(callback: (info: DisconnectInfo) => void): void {\n this.disconnectedCallback = callback\n }\n\n /**\n * RFC-165 D9/H1:通用 notification 订阅——面向未被 `handleNotification` 专用 switch-case\n * 处理的 method(如插件 backend facet 推送的 `backend/state`)。已处理的 method\n * (`tools/list_changed`/`resources/list_changed`)也会触发本回调,消费方按 method 自行过滤。\n */\n onNotification(callback: (method: string, params?: Record<string, unknown>) => void): void {\n this.notificationCallback = callback\n }\n\n async connect(): Promise<void> {\n if (this.status === 'ready') {\n return\n }\n\n this.closing = false\n this.status = 'connecting'\n\n try {\n await this.establishWithTimeout()\n this.reconnectAttempts = 0\n this.status = 'ready'\n } catch (err) {\n this.status = 'error'\n const message = err instanceof Error ? err.message : String(err)\n logger.error('MCP server \"%s\" connection failed: %s', this.serverName, message)\n // 保留底层鉴权错误码(MCP_UNAUTHORIZED),不一律压成 MCP_CONNECT_ERROR——\n // 否则宿主无从据码渲染 actionable 指引('otto mcp login')。\n const preserved =\n err instanceof McpError &&\n (err.code === 'MCP_UNAUTHORIZED' || err.code === 'MCP_CONNECT_TIMEOUT')\n const code = preserved ? (err as McpError).code! : 'MCP_CONNECT_ERROR'\n throw new McpError(`Failed to connect to MCP server \"${this.serverName}\": ${message}`, {\n code,\n cause: err instanceof Error ? err : undefined,\n })\n }\n }\n\n /**\n * 连接阶段总预算——把 `establish()`(transport.start + initialize + tools/list + 可选\n * resources/prompts)整体包在一个短超时里。\n *\n * 为什么不是仅靠 requestTimeoutMs:那是**每个请求各自**的预算,且线上放宽到 30s;\n * 一个挂起的 server 会让 `connectAll` 的 `Promise.allSettled` 等满 30s,把宿主装配\n * (实测 0.4s)拖成 30s+。连接阶段单独给短预算,超时即放弃该 server,其余照常。\n *\n * 超时后必须 `transport.close()`——否则已建的连接/子进程会在后台继续跑(泄漏),\n * 且其迟到的响应会打到一个已被判失败的 client 上。\n */\n private async establishWithTimeout(): Promise<void> {\n const budget = this.options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS\n if (!budget || budget <= 0) {\n await this.establish()\n return\n }\n\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new McpError(\n `MCP server \"${this.serverName}\" did not finish connecting within ${budget}ms`,\n { code: 'MCP_CONNECT_TIMEOUT' },\n ),\n )\n }, budget)\n timer.unref?.()\n })\n\n try {\n await Promise.race([this.establish(), timeout])\n } catch (err) {\n // 超时分支:establish() 仍在途,它自己的 catch 不会被等到 → 此处主动收尾 transport,\n // 防止挂起的连接/子进程泄漏。\n const transport = this.transport\n if (transport) {\n this.transport = null\n await transport.close().catch(() => undefined)\n }\n this.failPending(\n err instanceof Error ? err : new Error(String(err)),\n )\n throw err\n } finally {\n clearTimeout(timer)\n }\n }\n\n private async establish(): Promise<void> {\n const transport = this.createTransport()\n this.transport = transport\n // 传输层据此区分\"坏帧 + 真有请求在干等\"(故障)与\"坏帧但无人等待\"(保活噪声)。\n // 关键:只统计**已等待超过一个宽限窗口**的请求——刚发出的 initialize 正常也在途,\n // 用它当判据会把\"响应稍后经 POST 返回\"的健康服务端误杀(实测确认过)。\n transport.setPendingProbe?.(() => this.hasStalePending())\n transport.onMessage((msg) => this.handleMessage(msg))\n transport.onClose((reason) => {\n if (this.transport !== transport) {\n return\n }\n this.handleTransportClose(reason)\n })\n\n try {\n await transport.start()\n\n this.serverInfo = await this.request<McpInitializeResult>('initialize', {\n protocolVersion: MCP_PROTOCOL_VERSION,\n capabilities: { roots: { listChanged: false } },\n clientInfo: { name: MCP_CLIENT_NAME, version: MCP_CLIENT_VERSION },\n })\n\n this.capabilities = this.serverInfo.capabilities\n this.notify('notifications/initialized')\n\n logger.info(\n 'MCP server \"%s\" connected: %s (protocol %s)',\n this.serverName,\n this.serverInfo.serverInfo.name,\n this.serverInfo.protocolVersion,\n )\n\n await this.refreshTools()\n if (this.capabilities?.resources) {\n await this.refreshResources()\n }\n if (this.capabilities?.prompts) {\n await this.refreshPrompts()\n }\n } catch (err) {\n if (this.transport === transport) {\n this.transport = null\n }\n await transport.close().catch(() => undefined)\n throw err\n }\n }\n\n private handleTransportClose(reason?: string): void {\n if (this.closing) {\n return\n }\n this.failPending(\n new McpError(`MCP transport closed: ${reason ?? 'unknown'}`, { code: 'MCP_DISCONNECTED' }),\n )\n\n // B10:初次连接期(status==='connecting')transport 关闭,交给 establish 的 catch + connect() 的\n // throw 上报,此处不重连、不改状态、不回调——否则被丢弃的 client(从未入池)仍武装重连定时器,\n // 反复 spawn 子进程/重开流(孤儿重连循环)。只有已建立(ready)或重连中(reconnecting)的掉线才自愈。\n if (this.status === 'connecting') {\n return\n }\n\n const autoReconnect = this.options.autoReconnect ?? true\n const maxAttempts = this.options.maxReconnectAttempts ?? 3\n if (!autoReconnect || this.reconnectAttempts >= maxAttempts) {\n this.status = 'disconnected'\n this.transport = null\n // transport 非预期关闭(非 disconnect() 主动调用)——分类为 crash(D8/H2)。\n this.disconnectedCallback?.({ reason, kind: 'crash' })\n return\n }\n this.scheduleReconnect()\n }\n\n private scheduleReconnect(): void {\n this.reconnectAttempts++\n this.status = 'reconnecting'\n const base = this.options.reconnectDelayMs ?? 500\n const delay = base * 2 ** (this.reconnectAttempts - 1)\n logger.info(\n 'MCP server \"%s\" reconnecting (attempt %d) in %dms',\n this.serverName,\n this.reconnectAttempts,\n delay,\n )\n this.reconnectTimer = setTimeout(() => {\n void this.attemptReconnect()\n }, delay)\n }\n\n private async attemptReconnect(): Promise<void> {\n if (this.closing) {\n return\n }\n try {\n // 防御性:先 close 旧 transport(stdio 下 child 通常已 exit,但仍该做)\n if (this.transport) {\n await this.transport.close().catch(() => undefined)\n }\n this.transport = null\n // 重连同样受连接阶段预算约束——否则挂起的 server 会让每次重连卡满 requestTimeoutMs,\n // 退避序列被拉长数十倍,自愈变成长时间僵死。\n await this.establishWithTimeout()\n this.reconnectAttempts = 0\n this.status = 'ready'\n logger.info('MCP server \"%s\" reconnected', this.serverName)\n } catch (err) {\n logger.warn(\n 'MCP server \"%s\" reconnect attempt %d failed: %s',\n this.serverName,\n this.reconnectAttempts,\n (err as Error).message,\n )\n const maxAttempts = this.options.maxReconnectAttempts ?? 3\n if (this.reconnectAttempts >= maxAttempts) {\n this.status = 'error'\n this.transport = null\n // 重连耗尽——仍是非预期关闭的延续,分类为 crash(D8/H2)。\n this.disconnectedCallback?.({\n reason: `reconnect failed after ${maxAttempts} attempts`,\n kind: 'crash',\n })\n } else {\n this.scheduleReconnect()\n }\n }\n }\n\n /**\n * 是否存在\"已等待超过 STALE_PENDING_MS 仍无应答\"的在途请求。\n *\n * 传输层用它作为\"流里只有坏帧\"是否属真故障的判据:刚发出的请求正常也在途(响应可能\n * 稍后经 POST 响应体返回),只有**久等不到**才说明网关在用错误体应答而非讲 MCP。\n */\n private hasStalePending(): boolean {\n const now = Date.now()\n for (const pending of this.pending.values()) {\n if (now - pending.sentAt >= STALE_PENDING_MS) return true\n }\n return false\n }\n\n private failPending(error: Error): void {\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer)\n pending.reject(error)\n this.pending.delete(id)\n }\n }\n\n async refreshTools(): Promise<McpToolDefinition[]> {\n const result = await this.request<{ tools: McpToolDefinition[] }>('tools/list')\n this.tools = result.tools ?? []\n\n logger.debug(\n 'MCP server \"%s\" provides %d tools: %s',\n this.serverName,\n this.tools.length,\n this.tools.map((t) => t.name).join(', '),\n )\n\n return this.tools\n }\n\n async refreshResources(): Promise<McpResource[]> {\n try {\n const result = await this.request<{ resources: McpResource[] }>('resources/list')\n this.resources = result.resources ?? []\n\n logger.debug('MCP server \"%s\" provides %d resources', this.serverName, this.resources.length)\n } catch (err) {\n logger.debug(\n 'MCP server \"%s\" resources/list failed: %s',\n this.serverName,\n (err as Error).message,\n )\n this.resources = []\n }\n\n return this.resources\n }\n\n async readResource(uri: string): Promise<McpResourceContents[]> {\n const result = await this.request<{ contents: McpResourceContents[] }>('resources/read', {\n uri,\n })\n return result.contents ?? []\n }\n\n async refreshPrompts(): Promise<McpPrompt[]> {\n try {\n const result = await this.request<{ prompts: McpPrompt[] }>('prompts/list')\n this.prompts = result.prompts ?? []\n\n logger.debug('MCP server \"%s\" provides %d prompts', this.serverName, this.prompts.length)\n } catch (err) {\n logger.debug(\n 'MCP server \"%s\" prompts/list failed: %s',\n this.serverName,\n (err as Error).message,\n )\n this.prompts = []\n }\n\n return this.prompts\n }\n\n async getPrompt(\n name: string,\n args?: Record<string, string>,\n ): Promise<{ description?: string; messages: McpPromptMessage[] }> {\n const result = await this.request<{ description?: string; messages: McpPromptMessage[] }>(\n 'prompts/get',\n {\n name,\n arguments: args,\n },\n )\n return result\n }\n\n async callTool(params: McpToolCallParams, signal?: AbortSignal): Promise<McpToolCallResult> {\n if (this.status !== 'ready') {\n throw new McpError(`MCP server \"${this.serverName}\" is not ready (status: ${this.status})`, {\n code: 'MCP_NOT_READY',\n })\n }\n\n const result = await this.request<McpToolCallResult>(\n 'tools/call',\n params as unknown as Record<string, unknown>,\n signal,\n )\n return result\n }\n\n /**\n * RFC-165 T6:通用请求出口——镜像底层私有 `request`,但方法名不含 \"tool\" 语义\n * (避免与模型工具调用面混淆,对齐 D3 概念分离:即使是客户端内部 API 命名也不应让\n * `backend/*` 调用看起来像\"调用了一个 tool\")。供 `PanelBackendController` 发送\n * `backend/call`/`backend/cancel`/`backend/getState` 等面板专用请求。\n *\n * 刻意不要求 `status === 'ready'`(区别于 `callTool`)——D7 降级启动场景下 backend facet\n * 恒可用,但 `mcp` facet 的业务工具可能未注册,`ready` 判断不应耦合 `mcp` facet 健康度;\n * 断连场景由调用方(`PanelBackendController`)经 `McpClient.getStatus()` 感知并拒绝新请求。\n */\n async callRaw(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n return this.request<unknown>(method, params, signal)\n }\n\n async disconnect(): Promise<void> {\n this.closing = true\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n\n if (this.transport) {\n try {\n await this.transport.close()\n } catch (err) {\n logger.debug('Error closing MCP transport: %s', (err as Error).message)\n }\n this.transport = null\n }\n\n this.failPending(new McpError('MCP client disconnected', { code: 'MCP_DISCONNECTED' }))\n\n this.status = 'disconnected'\n this.tools = []\n this.resources = []\n this.prompts = []\n this.serverInfo = null\n this.capabilities = null\n // 调用方主动发起(如 McpManager.disconnect()/setServerEnabled(false))——分类为 manual(D8/H2)。\n this.disconnectedCallback?.({ reason: 'client disconnect', kind: 'manual' })\n }\n\n private createTransport(): McpTransport {\n if (this.options.transportFactory) {\n return this.options.transportFactory()\n }\n\n const { type, url, command } = this.config\n // `http`(含 type 省略但给了 url)= Streamable HTTP(2025-03-26,POST 驱动单端点);\n // `sse` = 旧版 HTTP+SSE(2024-11-05,GET 开长连流)。二者线协议不同,不可混用——\n // 现代 MCP server 普遍只实现前者,此前统一走 SseTransport 会 \"SSE stream ended\"。\n const wantsStreamableHttp = type === 'http' || (type === undefined && url !== undefined)\n const wantsSse = type === 'sse'\n const wantsStdio = type === 'stdio' || (type === undefined && command !== undefined)\n\n if (wantsStreamableHttp || wantsSse) {\n if (!url) {\n throw new McpError(`MCP server \"${this.serverName}\" transport '${type}' requires a url`, {\n code: 'MCP_CONFIG_ERROR',\n })\n }\n const transportOpts = {\n requestTimeoutMs: this.config.requestTimeoutMs ?? this.options.requestTimeoutMs,\n headers: this.config.headers,\n getAuthToken: this.options.getAuthToken,\n }\n return wantsSse\n ? new SseTransport(url, transportOpts)\n : new StreamableHttpTransport(url, transportOpts)\n }\n\n if (wantsStdio) {\n if (!command) {\n throw new McpError(`MCP server \"${this.serverName}\" transport 'stdio' requires a command`, {\n code: 'MCP_CONFIG_ERROR',\n })\n }\n return new StdioTransport(\n command,\n this.config.args ?? [],\n this.config.env ?? {},\n this.options.processSpawnFactory,\n )\n }\n\n throw new McpError(`MCP server \"${this.serverName}\" has no command or url configured`, {\n code: 'MCP_CONFIG_ERROR',\n })\n }\n\n private request<T>(\n method: string,\n params?: Record<string, unknown>,\n signal?: AbortSignal,\n ): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new McpError(`MCP request \"${method}\" aborted`, { code: 'MCP_ABORTED' }))\n return\n }\n const id = this.nextRequestId++\n const timeoutMs = this.config.requestTimeoutMs ?? this.options.requestTimeoutMs ?? 30_000\n\n let abortHandler: (() => void) | undefined\n const detach = (): void => {\n if (abortHandler && signal) signal.removeEventListener('abort', abortHandler)\n }\n\n const timer = setTimeout(() => {\n this.pending.delete(id)\n detach()\n reject(\n new McpError(`MCP request \"${method}\" timed out after ${timeoutMs}ms`, {\n code: 'MCP_TIMEOUT',\n }),\n )\n }, timeoutMs)\n\n this.pending.set(id, {\n sentAt: Date.now(),\n resolve: (v: unknown) => {\n detach()\n ;(resolve as (value: unknown) => void)(v)\n },\n reject: (e: unknown) => {\n detach()\n reject(e)\n },\n timer,\n })\n\n if (signal) {\n abortHandler = () => {\n const p = this.pending.get(id)\n if (!p) return\n clearTimeout(p.timer)\n this.pending.delete(id)\n detach()\n reject(new McpError(`MCP request \"${method}\" aborted`, { code: 'MCP_ABORTED' }))\n }\n signal.addEventListener('abort', abortHandler, { once: true })\n }\n\n const message: JsonRpcRequest = {\n jsonrpc: JSON_RPC_VERSION,\n id,\n method,\n ...(params !== undefined && { params }),\n }\n\n try {\n this.transport!.send(message)\n } catch (err) {\n clearTimeout(timer)\n this.pending.delete(id)\n detach()\n reject(err)\n }\n })\n }\n\n private notify(method: string, params?: Record<string, unknown>): void {\n const message: JsonRpcNotification = {\n jsonrpc: JSON_RPC_VERSION,\n method,\n ...(params !== undefined && { params }),\n }\n\n try {\n this.transport!.send(message)\n } catch (err) {\n logger.debug('Failed to send MCP notification \"%s\": %s', method, (err as Error).message)\n }\n }\n\n private handleMessage(message: JsonRpcResponse | JsonRpcNotification): void {\n if (!('id' in message) || message.id === undefined) {\n this.handleNotification(message as JsonRpcNotification)\n return\n }\n\n const response = message as JsonRpcResponse\n const pending = this.pending.get(response.id!)\n if (!pending) {\n logger.debug('Received response for unknown request id: %s', response.id)\n return\n }\n\n clearTimeout(pending.timer)\n this.pending.delete(response.id!)\n\n if (response.error) {\n pending.reject(\n new McpError(`MCP error: ${response.error.message} (code: ${response.error.code})`, {\n code: 'MCP_SERVER_ERROR',\n }),\n )\n } else {\n pending.resolve(response.result)\n }\n }\n\n private handleNotification(notification: JsonRpcNotification): void {\n switch (notification.method) {\n case 'notifications/tools/list_changed':\n logger.info('MCP server \"%s\" notified tools list changed', this.serverName)\n void this.refreshTools().then(() => {\n this.toolsChangedCallback?.()\n })\n break\n\n case 'notifications/resources/list_changed':\n logger.info('MCP server \"%s\" notified resources list changed', this.serverName)\n void this.refreshResources().then(() => {\n this.resourcesChangedCallback?.()\n })\n break\n\n default:\n logger.debug('MCP notification: %s', notification.method)\n }\n\n // RFC-165 D9/H1:通用订阅——所有 notification(含上面已被专用 case 处理的)都转发,\n // 不再对未知 method 静默丢弃。让 backend/state 这类插件自定义 notification 能被\n // 上层(McpManager → PanelBackendController)消费。\n this.notificationCallback?.(notification.method, notification.params)\n }\n}\n\nexport function createMcpClient(\n serverName: string,\n config: McpServerConfig,\n options?: McpClientOptions,\n): McpClient {\n return new McpClient(serverName, config, options)\n}\n","import { z } from 'zod'\nimport { createLogger, probeImageDimensions, API_IMAGE_DIMENSION_LIMIT_PX, PROVIDER_IMAGE_INLINE_MAX_BYTES } from '@x-otto/shared'\nimport type { AgentToolDefinition, ToolResult } from '@x-otto/interchange'\nimport type { McpClient } from './mcp-client'\nimport type { McpToolDefinition, McpJsonSchemaProperty, McpContent } from './types'\n\nconst logger = createLogger('@x-otto/mcp:adapter')\n\nfunction enumToZod(values: unknown[]): z.ZodType | null {\n if (values.length === 0) {\n return null\n }\n if (values.every((v) => typeof v === 'string')) {\n return z.enum(values as [string, ...string[]])\n }\n const literals = values\n .filter((v): v is string | number | boolean => {\n const t = typeof v\n return t === 'string' || t === 'number' || t === 'boolean'\n })\n .map((v) => z.literal(v))\n if (literals.length === 0) {\n return null\n }\n if (literals.length === 1) {\n return literals[0]!\n }\n return z.union(literals as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]])\n}\n\n/**\n * 将 MCP JSON Schema property → Zod Schema\n */\nexport function jsonSchemaPropertyToZod(prop: McpJsonSchemaProperty): z.ZodType {\n // B11(评审建议优化):复杂 schema 显式降级——oneOf/anyOf/allOf/$ref 不支持时\n // 落到 z.unknown()(丢约束但**有日志**,此前静默退化难排查)。$ref 尽力局部解析\n // (schema 内嵌 definitions/`$defs` 引用),解析失败才降级。\n if (prop.oneOf && Array.isArray(prop.oneOf) && prop.oneOf.length > 0) {\n const subs = prop.oneOf.map((s) => jsonSchemaPropertyToZod(s as McpJsonSchemaProperty))\n return (prop.description ? z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]).describe(prop.description) : z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]))\n }\n if (prop.anyOf && Array.isArray(prop.anyOf) && prop.anyOf.length > 0) {\n const subs = prop.anyOf.map((s) => jsonSchemaPropertyToZod(s as McpJsonSchemaProperty))\n return (prop.description ? z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]).describe(prop.description) : z.union(subs as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]]))\n }\n if (prop.allOf && Array.isArray(prop.allOf) && prop.allOf.length > 0) {\n // allOf 语义 = 全约束交叠——近似取最后一段(MCP 实践中 allOf 罕见,降级可接受但记日志)。\n console.warn('[mcp-tool-adapter] allOf schema degraded to last sub-schema (intersection unsupported)')\n return jsonSchemaPropertyToZod(prop.allOf[prop.allOf.length - 1] as McpJsonSchemaProperty)\n }\n if (typeof prop.$ref === 'string') {\n const resolved = resolveLocalRef(prop.$ref, prop)\n if (resolved) return jsonSchemaPropertyToZod(resolved)\n console.warn(`[mcp-tool-adapter] unresolvable $ref \"${prop.$ref}\" degraded to z.unknown()`)\n return z.unknown()\n }\n\n // enum 覆盖所有类型(MCP 规范允许 object/array 的 enum,此前只处理了标量)\n if (Array.isArray(prop.enum)) {\n const enumSchema = enumToZod(prop.enum)\n if (enumSchema) {\n return prop.description ? enumSchema.describe(prop.description) : enumSchema\n }\n }\n\n switch (prop.type) {\n case 'string':\n return prop.description ? z.string().describe(prop.description) : z.string()\n\n case 'number':\n case 'integer': {\n // LLM 常把快照提取的数字参数写成字符串(如 index:\"3856\")。\n // z.preprocess 在 safeParse 前强制转换,避免 expected number, received string。\n const base = prop.type === 'integer' ? z.number().int() : z.number()\n const coerced = z.preprocess((v) => (typeof v === 'string' ? Number(v) : v), base)\n return prop.description ? coerced.describe(prop.description) : coerced\n }\n\n case 'boolean':\n return prop.description ? z.boolean().describe(prop.description) : z.boolean()\n\n case 'array': {\n const itemSchema = prop.items ? jsonSchemaPropertyToZod(prop.items) : z.unknown()\n return prop.description ? z.array(itemSchema).describe(prop.description) : z.array(itemSchema)\n }\n\n case 'object': {\n if (prop.properties) {\n const shape: Record<string, z.ZodType> = {}\n const requiredSet = new Set(prop.required ?? [])\n\n for (const [key, value] of Object.entries(prop.properties)) {\n const fieldSchema = jsonSchemaPropertyToZod(value)\n shape[key] = requiredSet.has(key) ? fieldSchema : fieldSchema.optional()\n }\n\n return prop.description ? z.object(shape).describe(prop.description) : z.object(shape)\n }\n return prop.description\n ? z.record(z.string(), z.unknown()).describe(prop.description)\n : z.record(z.string(), z.unknown())\n }\n\n default: {\n // B11:未识别类型显式降级(此前静默 z.unknown(),不可诊断)\n console.warn(`[mcp-tool-adapter] unsupported JSON schema type \"${prop.type}\" degraded to z.unknown()`)\n return z.unknown()\n }\n }\n}\n\n/**\n * B11:局部 $ref 解析——`#/definitions/Name` / `#/$defs/Name` 引用当前 schema 内嵌\n * definitions/$defs。MCP 服务端惯用内嵌 definitions 做复用;跨文件/远程引用不支持\n * (显式降级,调用方已记日志)。\n */\nfunction resolveLocalRef(ref: string, prop: McpJsonSchemaProperty): McpJsonSchemaProperty | undefined {\n const m = /^#\\/(?:definitions|\\$defs)\\/([^/]+)$/.exec(ref)\n if (!m) return undefined\n const name = m[1]!.replace(/~1/g, '/').replace(/~0/g, '~')\n const defs = (prop as { definitions?: Record<string, unknown>; $defs?: Record<string, unknown> })\n const def = defs.definitions?.[name] ?? defs.$defs?.[name]\n return def as McpJsonSchemaProperty | undefined\n}\n\n/**\n * 将 MCP tool inputSchema → Zod schema\n */\nexport function mcpSchemaToZod(tool: McpToolDefinition): z.ZodType {\n const props = tool.inputSchema.properties\n if (!props || Object.keys(props).length === 0) {\n return z.object({})\n }\n\n const shape: Record<string, z.ZodType> = {}\n const requiredSet = new Set(tool.inputSchema.required ?? [])\n\n for (const [key, value] of Object.entries(props)) {\n const fieldSchema = jsonSchemaPropertyToZod(value)\n shape[key] = requiredSet.has(key) ? fieldSchema : fieldSchema.optional()\n }\n\n return z.object(shape)\n}\n\n/** MCP 工具产出图片体积硬上限——跨包单源 @x-otto/shared PROVIDER_IMAGE_INLINE_MAX_BYTES(C 组收敛)。\n * 语义:字节长度 > 上限才拒绝,恰好等于上限放行。 */\nconst MAX_MCP_IMAGE_BYTES = PROVIDER_IMAGE_INLINE_MAX_BYTES\n\nfunction base64ByteLength(b64: string): number {\n const padding = b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0\n return Math.max(0, Math.floor((b64.length * 3) / 4) - padding)\n}\n\nfunction resourceToContent(resource: {\n uri: string\n text?: string\n blob?: string\n mimeType?: string\n}): ToolResult['content'][number] {\n if (resource.text !== undefined) {\n return { type: 'text', text: resource.text }\n }\n if (resource.blob !== undefined) {\n if (resource.mimeType?.startsWith('image/') && resource.mimeType !== 'image/svg+xml') {\n const bytes = base64ByteLength(resource.blob)\n if (bytes > MAX_MCP_IMAGE_BYTES) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${bytes} bytes exceeds ${MAX_MCP_IMAGE_BYTES} byte limit — ask the tool to capture a smaller region/resolution]`,\n }\n }\n const dims = probeImageDimensions(resource.blob, resource.mimeType)\n // 2000px many-image 规则由 work-loop D6 cap(≤20 张/请求)保证不适用,\n // 此处只拦 API 必拒的 >8000px。原 2000px 会把 Retina 截图(2560px+)拒成占位文本。\n if (dims && (dims.width > API_IMAGE_DIMENSION_LIMIT_PX || dims.height > API_IMAGE_DIMENSION_LIMIT_PX)) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${dims.width}x${dims.height}px exceeds Anthropic per-image dimension hard limit of ${API_IMAGE_DIMENSION_LIMIT_PX}px — ask the tool to capture a smaller region or lower resolution]`,\n }\n }\n return {\n type: 'image',\n mime: resource.mimeType,\n source: `data:${resource.mimeType};base64,${resource.blob}`,\n }\n }\n const mimeNote = resource.mimeType ? ` (${resource.mimeType})` : ''\n return {\n type: 'text',\n text: `[Resource: ${resource.uri}${mimeNote}, ${base64ByteLength(resource.blob)} bytes binary]`,\n }\n }\n return { type: 'text', text: `[Resource: ${resource.uri}]` }\n}\n\n/**\n * 将 MCP content → ToolResult content\n */\nexport function mcpContentToToolContent(content: McpContent[]): ToolResult['content'] {\n return content.map((item) => {\n switch (item.type) {\n case 'text':\n return { type: 'text' as const, text: item.text }\n case 'image':\n if (!item.data) {\n return { type: 'text' as const, text: '[image unavailable: no data]' }\n }\n {\n const bytes = base64ByteLength(item.data)\n if (bytes > MAX_MCP_IMAGE_BYTES) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${bytes} bytes exceeds ${MAX_MCP_IMAGE_BYTES} byte limit — ask the tool to capture a smaller region/resolution]`,\n }\n }\n const dims = probeImageDimensions(item.data, item.mimeType ?? '')\n // 2000px many-image 规则由 work-loop D6 cap(≤20 张/请求)保证不适用,\n // 此处只拦 API 必拒的 >8000px。原 2000px 会把 Retina 截图(2560px+)拒成占位文本。\n if (dims && (dims.width > API_IMAGE_DIMENSION_LIMIT_PX || dims.height > API_IMAGE_DIMENSION_LIMIT_PX)) {\n return {\n type: 'text' as const,\n text: `[image omitted: ${dims.width}x${dims.height}px exceeds Anthropic per-image dimension hard limit of ${API_IMAGE_DIMENSION_LIMIT_PX}px — ask the tool to capture a smaller region or lower resolution]`,\n }\n }\n }\n return {\n type: 'image' as const,\n mime: item.mimeType,\n source: `data:${item.mimeType};base64,${item.data}`,\n }\n case 'resource':\n return resourceToContent(item.resource)\n default:\n return { type: 'text' as const, text: JSON.stringify(item) }\n }\n })\n}\n\n/**\n * 从 MCP 工具 inputSchema 推断\"文件系统路径\"参数名(endgame review:MCP 工具此前无 pathParams →\n * permission-guard 的 path 规则(file-guard 写保护 / 凭证读侧 deny / sandbox)全不命中 →\n * `mcp__fs__read_file{path:~/.ssh/id_rsa}` 零守卫读出密钥)。声明 pathParams 后,执行器经\n * resolveToolPaths 把这些 string 实参作为路径喂给所有路径守卫。\n *\n * 名字启发式刻意**宽**(宁可多声明几个无害的——非路径串过守卫不命中即放行):含\n * path/file/dir/folder 子串,或精确 source/destination/dest/src/cwd/location。刻意**排除** uri/url\n * (多为网络地址,按 fs 路径解析会误导 sandbox 写评估)。仅取 string 类型属性。\n */\nfunction looksLikePathParam(name: string): boolean {\n const n = name.toLowerCase().replace(/[_\\s-]/g, '')\n if (['source', 'destination', 'dest', 'src', 'cwd', 'location'].includes(n)) return true\n return n.includes('path') || n.includes('file') || n.includes('dir') || n.includes('folder')\n}\n\nexport function deriveMcpPathParams(toolDef: McpToolDefinition): string[] {\n const props = toolDef.inputSchema.properties\n if (!props) return []\n const out: string[] = []\n for (const [key, prop] of Object.entries(props)) {\n const t = prop.type\n if ((t === 'string' || t === undefined) && looksLikePathParam(key)) {\n out.push(key)\n }\n }\n return out\n}\n\n/**\n * 为单个 MCP tool 创建 AgentTool 适配器\n * 工具名格式: mcp__{serverName}__{toolName}\n */\nexport function createMcpToolAdapter(\n client: McpClient,\n toolDef: McpToolDefinition,\n serverName: string,\n): AgentToolDefinition {\n const qualifiedName = `mcp__${serverName}__${toolDef.name}`\n const description = toolDef.description\n ? `[MCP: ${serverName}] ${toolDef.description}`\n : `[MCP: ${serverName}] ${toolDef.name}`\n\n const parameters = mcpSchemaToZod(toolDef)\n const pathParams = deriveMcpPathParams(toolDef)\n // MCP `annotations.readOnlyHint` → AgentTool.readonly:work-loop 据 readonly 把只读调用分入并行桶\n // 此前适配器不传 readonly → 所有外部 MCP 工具恒串行。仅 true 才置位(保守)。\n const readonly = toolDef.annotations?.readOnlyHint === true\n\n return {\n name: qualifiedName,\n description,\n parameters,\n ...(pathParams.length > 0 ? { pathParams } : {}),\n ...(readonly ? { readonly: true } : {}),\n exposure: 'deferred',\n\n async execute({ params, signal }): Promise<ToolResult> {\n logger.debug('Calling MCP tool %s on server %s', toolDef.name, serverName)\n\n const result = await client.callTool(\n {\n name: toolDef.name,\n arguments: params as Record<string, unknown>,\n },\n signal,\n )\n\n const content = mcpContentToToolContent(result.content)\n\n return {\n content: content.length > 0 ? content : [{ type: 'text', text: '(empty result)' }],\n isError: result.isError ?? false,\n details: {\n mcpServer: serverName,\n mcpTool: toolDef.name,\n },\n }\n },\n }\n}\n\n/**\n * 为一个 MCP Client 的所有工具创建适配器\n */\nexport function createMcpToolAdapters(client: McpClient, serverName: string): AgentToolDefinition[] {\n const tools = client.getTools()\n return tools.map((toolDef) => createMcpToolAdapter(client, toolDef, serverName))\n}\n","export function parseWwwAuthenticate(header: string): string | undefined {\n const parts = header.split(',').map((p) => p.trim())\n const bearer = parts.find(\n (p) => p.toLowerCase().startsWith('bearer ') || p.toLowerCase() === 'bearer',\n )\n if (!bearer) return undefined\n\n const match = bearer.match(/metadata_url\\s*=\\s*\"([^\"]+)\"/i)\n return match?.[1]\n}\n\nexport interface ProtectedResourceMetadata {\n /** 资源标识符(即 MCP server 的 canonical URL)。 */\n resource: string\n /** 授权服务器 issuer URL 列表。 */\n authorization_servers?: string[]\n /** 支持的 Bearer 方法。 */\n bearer_methods_supported?: string[]\n /** 可读的资源文档 URL。 */\n resource_documentation?: string\n}\n\nexport async function fetchProtectedResourceMetadata(\n metadataUrl: string,\n fetchImpl: typeof globalThis.fetch = fetch,\n): Promise<ProtectedResourceMetadata> {\n const res = await fetchImpl(metadataUrl, {\n headers: { Accept: 'application/json' },\n })\n if (!res.ok) {\n throw new Error(\n `Failed to fetch protected resource metadata from ${metadataUrl}: ${res.status}`,\n )\n }\n return (await res.json()) as ProtectedResourceMetadata\n}\n\nexport interface AuthorizationServerMetadata {\n issuer: string\n authorization_endpoint: string\n token_endpoint: string\n /** DCR 注册端点(RFC 7591);若无则不支持动态注册。 */\n registration_endpoint?: string\n /** 注册端点认证方式;'none'=无需 client_secret,PKCE only。 */\n registration_endpoint_auth_methods_supported?: string[]\n /** 支持的 PKCE challenge 方法(期望 ['S256'])。 */\n code_challenge_methods_supported?: string[]\n /** 授权端点支持的 response_type。 */\n response_types_supported?: string[]\n /** token 端点支持的 grant_type。 */\n grant_types_supported?: string[]\n /** 授权端点支持的 scope。 */\n scopes_supported?: string[]\n /** 令牌端点认证方式。 */\n token_endpoint_auth_methods_supported?: string[]\n /** RFC 8414 未定义的其他字段(passthrough)。 */\n [key: string]: unknown\n}\n\n/**\n * 获取 authorization server metadata。\n * 先试 RFC 8414(GET issuer/.well-known/oauth-authorization-server),\n * 失败回退 OIDC discovery(GET issuer/.well-known/openid-configuration),\n * 只取与 OAuth 授权码流相关的字段。\n */\nexport async function fetchAuthorizationServerMetadata(\n issuer: string,\n fetchImpl: typeof globalThis.fetch = fetch,\n): Promise<AuthorizationServerMetadata> {\n const rfc8414Url = `${issuer.replace(/\\/$/, '')}/.well-known/oauth-authorization-server`\n const oidcUrl = `${issuer.replace(/\\/$/, '')}/.well-known/openid-configuration`\n\n const tryFetch = async (url: string): Promise<AuthorizationServerMetadata> => {\n const res = await fetchImpl(url, {\n headers: { Accept: 'application/json' },\n })\n if (!res.ok) {\n throw new Error(`Metadata fetch failed: ${url} → ${res.status}`)\n }\n return (await res.json()) as AuthorizationServerMetadata\n }\n\n try {\n return await tryFetch(rfc8414Url)\n } catch {\n return tryFetch(oidcUrl)\n }\n}\n\nexport interface OAuthDiscoveryResult {\n protectedResource: ProtectedResourceMetadata\n authorizationServer: AuthorizationServerMetadata\n /** 选定的授权服务器 issuer。 */\n issuer: string\n}\n\n/**\n * 完整 OAuth 发现:\n * 1. 构造 metadata URL(优先 401 WWW-Authenticate,否则 assume <server-url>/.well-known/oauth-protected-resource)\n * 2. 获取 protected resource metadata\n * 3. 选择 authorization server(取 metadata 列表第一个,或 issuer)\n * 4. 获取 authorization server metadata\n */\nexport async function discoverOAuth(\n serverUrl: string,\n options: {\n /** 401 响应的 WWW-Authenticate 头(可选,有一等公民优先)。 */\n wwwAuthenticate?: string\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n } = {},\n): Promise<OAuthDiscoveryResult> {\n const fetchImpl = options.fetch ?? fetch\n const metadataUrl =\n parseWwwAuthenticate(options.wwwAuthenticate ?? '') ??\n `${serverUrl.replace(/\\/$/, '')}/.well-known/oauth-protected-resource`\n\n const protectedResource = await fetchProtectedResourceMetadata(metadataUrl, fetchImpl)\n\n const issuer = protectedResource.authorization_servers?.[0] ?? new URL(metadataUrl).origin\n\n const authorizationServer = await fetchAuthorizationServerMetadata(issuer, fetchImpl)\n\n return { protectedResource, authorizationServer, issuer }\n}\n","export interface DCRRequest {\n /** 可读的客户端名称。 */\n client_name?: string\n /** 授权码回调 redirect URI 列表。 */\n redirect_uris: string[]\n /** 授权端点请求的 grant type。 */\n grant_types?: string[]\n /** token 端点认证方式;PKCE 用 'none'。 */\n token_endpoint_auth_method?: string\n}\n\nexport interface DCRResponse {\n client_id: string\n client_secret?: string\n /** 注册完成后的 access token(供后续管理此客户端)。 */\n registration_access_token?: string\n /** 管理此注册的 endpoint URL。 */\n registration_client_uri?: string\n /** RFC 7591 未定义字段(passthrough)。 */\n [key: string]: unknown\n}\n\nexport interface RegisterClientOptions {\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n /** 注册端点的初始 access token(少数 AS 要求 bearer 认证)。 */\n initialAccessToken?: string\n}\n\n/**\n * 向 authorization server 注册 OAuth 客户端(DCR)。\n * POST <registration_endpoint>,请求体 JSON,响应 JSON。\n */\nexport async function registerClient(\n registrationEndpoint: string,\n params: DCRRequest,\n options: RegisterClientOptions = {},\n): Promise<DCRResponse> {\n const fetchImpl = options.fetch ?? fetch\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n }\n if (options.initialAccessToken) {\n headers['Authorization'] = `Bearer ${options.initialAccessToken}`\n }\n\n const res = await fetchImpl(registrationEndpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n client_name: params.client_name ?? 'otto MCP Client',\n redirect_uris: params.redirect_uris,\n grant_types: params.grant_types ?? ['authorization_code', 'refresh_token'],\n token_endpoint_auth_method: params.token_endpoint_auth_method ?? 'none',\n }),\n })\n\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new Error(\n `DCR registration failed at ${registrationEndpoint}: ${res.status}${body ? ` — ${body}` : ''}`,\n )\n }\n\n return (await res.json()) as DCRResponse\n}\n","import { isExpiredAt } from '@x-otto/credentials'\nimport { OAuthError } from '@x-otto/shared'\n\nexport interface AuthorizationCodeExchangeParams {\n /** 授权码(来自回调)。 */\n code: string\n /** 回调 redirect_uri(必须与 authorize 请求一致)。 */\n redirectUri: string\n /** PKCE code_verifier。 */\n codeVerifier: string\n /** RFC 8707 resource indicator(指向 MCP server)。 */\n resource?: string\n /** DCR 获得的 client_id。 */\n clientId: string\n}\n\nexport interface RefreshTokenParams {\n refreshToken: string\n /** RFC 8707 resource indicator。 */\n resource?: string\n clientId?: string\n}\n\nexport interface TokenResponse {\n access_token: string\n token_type: string\n expires_in?: number\n refresh_token?: string\n /** token 签发时的时间戳(ms),用于计算过期。 */\n issued_at?: number\n /** RFC 8707 审计。 */\n resource?: string\n}\n\nexport interface OAuthCredentials {\n accessToken: string\n refreshToken?: string\n expiresAt?: number\n resource?: string\n /** DCR client_id(持久化供后续 refresh)。 */\n clientId?: string\n}\n\nexport interface TokenExchangeOptions {\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n}\n\n/**\n * 用授权码交换 token(PKCE)。\n * POST <token_endpoint>(application/x-www-form-urlencoded)。\n */\nexport async function exchangeCode(\n tokenEndpoint: string,\n params: AuthorizationCodeExchangeParams,\n options: TokenExchangeOptions = {},\n): Promise<OAuthCredentials> {\n const fetchImpl = options.fetch ?? fetch\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code: params.code,\n redirect_uri: params.redirectUri,\n code_verifier: params.codeVerifier,\n client_id: params.clientId,\n })\n if (params.resource) {\n body.set('resource', params.resource)\n }\n\n const res = await fetchImpl(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },\n body: body.toString(),\n })\n\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new OAuthError(`Token exchange failed: ${res.status}${errBody ? ` — ${errBody}` : ''}`, {\n code: 'OAUTH_TOKEN_EXCHANGE_FAILED',\n })\n }\n\n const data: TokenResponse = (await res.json()) as TokenResponse\n return toCredentials(data)\n}\n\n/**\n * 用 refresh_token 刷新 access token。\n * POST <token_endpoint>(application/x-www-form-urlencoded)。\n */\nexport async function refreshAccessToken(\n tokenEndpoint: string,\n params: RefreshTokenParams,\n options: TokenExchangeOptions = {},\n): Promise<OAuthCredentials> {\n const fetchImpl = options.fetch ?? fetch\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: params.refreshToken,\n })\n if (params.clientId) {\n body.set('client_id', params.clientId)\n }\n if (params.resource) {\n body.set('resource', params.resource)\n }\n\n const res = await fetchImpl(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },\n body: body.toString(),\n })\n\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n // P1 策略层:解析 OAuth2 标准错误响应体({\"error\":\"invalid_grant\",...}),\n // 附加 errorType 到抛出的错误上——供 RefreshStrategy.shouldFailFast/cacheFailure\n // 判定是否永久失败(invalid_grant = refresh_token 已过期/被撤销/已轮换)。\n const oauthErrorType = parseOAuthErrorType(errBody)\n throw new OAuthError(\n `Token refresh failed: ${res.status}${errBody ? ` — ${errBody}` : ''}`,\n { code: 'OAUTH_REFRESH_FAILED', ...(oauthErrorType ? { errorType: oauthErrorType } : {}) },\n )\n }\n\n const data: TokenResponse = (await res.json()) as TokenResponse\n return toCredentials(data, params.refreshToken)\n}\n\nfunction toCredentials(data: TokenResponse, fallbackRefreshToken?: string): OAuthCredentials {\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? fallbackRefreshToken,\n expiresAt: data.expires_in\n ? (data.issued_at ?? Date.now()) + data.expires_in * 1000\n : undefined,\n resource: data.resource,\n }\n}\n\n/**\n * 解析 OAuth2 标准错误响应体中的 error 字段(RFC 6749 §5.2)。\n * 服务端返回 `{\"error\":\"invalid_grant\"}` 时,refresh_token 已被永久拒绝——\n * 同一凭据快照后续刷新必失败,可 fail-fast 不再竞锁/打网络。\n */\nfunction parseOAuthErrorType(body: string): string | undefined {\n try {\n const obj = JSON.parse(body) as Record<string, unknown>\n return typeof obj.error === 'string' ? obj.error : undefined\n } catch {\n return undefined\n }\n}\n\n/** 检查 access token 是否已过期(提前 60 秒视为过期,避免边界竞态)。 */\n/**\n * RFC-374 M1:过期判断委托 @x-otto/credentials 叶包(skew 单源)。\n * 公开 API 形状不变(`isTokenExpired(credentials, skewSeconds=60)`),\n * 实现改为叶包 `isExpiredAt`(ms 口径)——mcp 域只负责取 `expiresAt` 字段。\n */\nexport function isTokenExpired(credentials: OAuthCredentials, skewSeconds = 60): boolean {\n return isExpiredAt(credentials.expiresAt, skewSeconds * 1000)\n}\n","import { createHash } from 'node:crypto'\nimport { readdir, readFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { deleteFileIfExists, ensurePrivateDir, readJsonFile, writeJsonAtomic } from '@x-otto/credentials'\nimport { createOverlayStore } from '@x-otto/credentials'\nimport { isShadowModeEnabled } from '@x-otto/env'\n\nimport type { OAuthCredentials } from './token'\n\nexport interface TokenStore {\n /** 加载某 server 的凭据;不存在返回 undefined。 */\n load(serverName: string): Promise<OAuthCredentials | undefined>\n /** 保存某 server 的凭据。 */\n save(serverName: string, credentials: OAuthCredentials): Promise<void>\n /** 删除某 server 的凭据(logout)。 */\n delete(serverName: string): Promise<void>\n /** 列出所有有凭据的 server name。 */\n list(): Promise<string[]>\n /**\n * 跨进程刷新锁目录(与凭据文件同目录)。仅文件 store 提供;内存/overlay store 为\n * undefined(后者影子态禁止写锁文件)。业务层据此决定 refresh 是否走跨进程锁。\n */\n readonly refreshLockDir?: string\n /**\n * 是否为影子(overlay)态 store。影子态 store 读穿透磁盘、写只进内存——\n * 过期 token 不可刷新(轮换型 refresh_token 会作废磁盘旧 token,致全部普通会话\n * 掉登录)。业务层据此在 refresh 入口拦截(RFC-345 §D2b,对齐 ai auth-store 的\n * overlay 拒刷语义)。内存 store 非 shadow(无磁盘可破坏,刷新安全)。\n */\n readonly shadow?: boolean\n}\n\nexport interface FileTokenStoreOptions {\n /** 存储目录,默认 `~/.otto/mcp/credentials`。 */\n baseDir?: string\n}\n\nexport function createFileTokenStore(options: FileTokenStoreOptions = {}): TokenStore {\n const baseDir = options.baseDir ?? join(homedir(), '.otto', 'mcp', 'credentials')\n\n const filePath = (serverName: string): string => join(baseDir, `${sanitizeServerName(serverName)}.json`)\n\n return {\n refreshLockDir: baseDir,\n\n async load(serverName) {\n // RFC-374 M1:读盘纹理委托叶包 readJsonFile(ENOENT/损坏 → fallback,EACCES rethrow)。\n return readJsonFile<OAuthCredentials | undefined>(filePath(serverName), undefined)\n },\n\n async save(serverName, credentials) {\n // RFC-374 M1:原子写盘纹理委托叶包 writeJsonAtomic——临时文件 + chmod 0o600 +\n // rename 原子替换,凭据从落盘第一刻起就不可被其他本地用户读取,读者永远\n // 不会观测到半写状态(对齐 @x-otto/ai auth-store saveSerial 的同一语义)。\n await ensurePrivateDir(baseDir)\n await writeJsonAtomic(filePath(serverName), { ...credentials, serverName })\n },\n\n async delete(serverName) {\n await deleteFileIfExists(filePath(serverName))\n },\n\n async list() {\n try {\n const entries = await readdir(baseDir)\n const names: string[] = []\n for (const f of entries) {\n if (!f.endsWith('.json')) continue\n try {\n const raw = await readFile(join(baseDir, f), 'utf-8')\n const original = readServerName(raw)\n names.push(original ?? f.slice(0, -5))\n } catch {\n names.push(f.slice(0, -5))\n }\n }\n return names\n } catch (err) {\n const code = (err as NodeJS.ErrnoException)?.code\n if (code === 'ENOENT') return []\n throw err\n }\n },\n }\n}\n\nexport function createMemoryTokenStore(): TokenStore {\n const store = new Map<string, OAuthCredentials>()\n return {\n async load(serverName) {\n return store.get(serverName)\n },\n async save(serverName, credentials) {\n store.set(serverName, credentials)\n },\n async delete(serverName) {\n store.delete(serverName)\n },\n async list() {\n return [...store.keys()]\n },\n }\n}\n\n/**\n * RFC-345 影子模式 overlay(RFC-374 M1:实现委托 @x-otto/credentials 通用 overlay):\n * 读穿透到 base(继承磁盘既有凭据),写/删只进内存 overlay。进程退出后磁盘凭据\n * 逐字节不变。语义细节(load 墓碑优先/save 只进内存/delete 记墓碑/list 合并)\n * 由叶包 createOverlayStore 保证,本函数只做 TokenStore 形状适配 + shadow 标记。\n *\n * shadow: true + refreshLockDir: undefined —— 显式声明影子态语义(此前靠 overlay\n * 丢失 base 的 refreshLockDir 属性实现,是隐性开关,未来给 overlay 透传 refreshLockDir\n * 会静默改变影子态锁语义)。\n */\nexport function createOverlayTokenStore(base: TokenStore): TokenStore {\n const overlay = createOverlayStore<OAuthCredentials>(base)\n return {\n load: overlay.load,\n save: overlay.save,\n delete: overlay.delete,\n list: overlay.list,\n shadow: true,\n refreshLockDir: undefined,\n }\n}\n\n/**\n * MCP OAuth 凭据 store 的 §D1 白名单构造入口:影子态返回 overlay(读磁盘、写内存),\n * 否则返回纯文件 store。业务层(mcp-manager / login)统一走此函数,不自行判断影子态。\n */\nexport function createDefaultTokenStore(options: FileTokenStoreOptions = {}): TokenStore {\n const fileStore = createFileTokenStore(options)\n return isShadowModeEnabled() ? createOverlayTokenStore(fileStore) : fileStore\n}\n\n/**\n * 防止路径穿越 + 冲突:sanitize + 前 8 位 SHA256 哈希避免 sanitize 碰撞。\n * 同时作为跨进程刷新锁的 key(与凭据文件同源命名,避免 serverName 中的路径分隔符\n * 破坏锁文件路径)。\n */\nexport function sanitizeServerName(name: string): string {\n const safe = name.replace(/[^a-zA-Z0-9._-]/g, '_')\n const hash = createHash('sha256').update(name).digest('hex').slice(0, 8)\n return `${safe}_${hash}`\n}\n\nfunction readServerName(raw: string): string | undefined {\n try {\n const obj = JSON.parse(raw) as Record<string, unknown>\n return typeof obj.serverName === 'string' ? obj.serverName : undefined\n } catch {\n return undefined\n }\n}\n","/**\n * refresh-lock.ts —— MCP OAuth 跨进程刷新互斥锁(RFC-374 M1 迁移后:域配置薄层)。\n *\n * 背景与 ai 的 `oauth-refresh-coordinator.ts` 同源:本机可能同时运行多个 otto 进程\n * (CLI、feishu-use 桥接等),共享同一份 MCP 凭据目录 `~/.otto/mcp/credentials`。\n * 轮换型 refresh_token 在多进程几乎同时用同一个旧 token 刷新时会双花——服务端只认\n * 先到者,其余进程刷新失败并非凭据过期而是被抢先轮换。\n *\n * RFC-374 M1:通用锁原语与刷新协调骨架已下沉 `@x-otto/credentials` 叶包\n * (createRefreshLock / refreshUnderCrossProcessLock),本文件只保留 MCP 域的\n * 三样配置:锁目录(凭据文件同目录)、key 命名(sanitizeServerName)、锁文件前缀\n * `mcp-refresh`(**上线后不可变**——换前缀 = 新旧进程各锁各的文件 = 等于没锁,\n * 与 ai 的 `auth-refresh` 前缀同一条滚动升级硬约束)。\n *\n * 公开 API 形状不变(acquireRefreshLock/releaseRefreshLock/waitForRefreshLockRelease/\n * refreshWithCrossProcessLock/REFRESH_LOCK_TIMEOUT_MS/RefreshWithLockPorts),\n * 既有消费方(login.ts)与测试零改动。\n */\nimport {\n REFRESH_LOCK_TIMEOUT_MS,\n createRefreshLock,\n refreshUnderCrossProcessLock,\n type RefreshStrategy,\n} from '@x-otto/credentials'\n\nimport { isTokenExpired, refreshAccessToken } from './token'\nimport { sanitizeServerName } from './token-store'\n\nimport type { OAuthCredentials, RefreshTokenParams, TokenExchangeOptions } from './token'\n\nexport type { RefreshStrategy }\n\n/** 锁文件名前缀(`.mcp-refresh-<key>.lock`)。MCP 是全新锁域,无滚动升级历史约束。 */\nconst LOCK_PREFIX = 'mcp-refresh'\n\n/** 陈旧锁清理阈值 = 等待释放超时(与 ai 的规则9一致,两者必须相等)。 */\nexport { REFRESH_LOCK_TIMEOUT_MS }\n\n/** 尝试获取某 server 的跨进程刷新锁。成功 true;被占用/异常 false。 */\nexport async function acquireRefreshLock(\n credentialsDir: string,\n serverName: string,\n timeoutMs: number = REFRESH_LOCK_TIMEOUT_MS,\n): Promise<boolean> {\n return createRefreshLock(credentialsDir, LOCK_PREFIX).acquire(sanitizeServerName(serverName), timeoutMs)\n}\n\n/** 释放某 server 的跨进程刷新锁(幂等,并停止心跳)。 */\nexport async function releaseRefreshLock(credentialsDir: string, serverName: string): Promise<void> {\n return createRefreshLock(credentialsDir, LOCK_PREFIX).release(sanitizeServerName(serverName))\n}\n\n/** 等待另一进程持有的刷新锁释放,超时返回 false(调用方走读盘兜底)。 */\nexport async function waitForRefreshLockRelease(\n credentialsDir: string,\n serverName: string,\n timeoutMs: number = REFRESH_LOCK_TIMEOUT_MS,\n): Promise<boolean> {\n return createRefreshLock(credentialsDir, LOCK_PREFIX).waitForRelease(\n sanitizeServerName(serverName),\n timeoutMs,\n )\n}\n\n/** 刷新协调器读写端口:锁内需要重读/落盘,但不应直接持有 TokenStore 实例。 */\nexport interface RefreshWithLockPorts {\n /** 读取当前磁盘/存储上的凭据(可能已被另一进程刷新)。 */\n load(): Promise<OAuthCredentials | undefined>\n /** 保存真正刷新成功的新凭据。 */\n save(credentials: OAuthCredentials): Promise<void>\n}\n\n/**\n * 跨进程互斥刷新:同一时刻同一 server 全机器只有一个进程真正发起 `refreshAccessToken`\n * 网络请求,其余进程等待后直接读盘复用结果。语义(拿锁重读/等待读盘/抛错/finally 释放)\n * 由叶包 refreshUnderCrossProcessLock 保证,本函数做 MCP 域参数装配。\n *\n * P1 扩展:可选 `strategy` 透传到叶包骨架,启用永久失败缓存 / 磁盘恢复 / 可观测性。\n * 缺省(未提供 strategy)时与原有行为完全一致。\n */\nexport async function refreshWithCrossProcessLock(\n credentialsDir: string,\n serverName: string,\n tokenEndpoint: string,\n params: RefreshTokenParams,\n options: TokenExchangeOptions = {},\n ports: RefreshWithLockPorts,\n strategy?: RefreshStrategy<OAuthCredentials>,\n): Promise<OAuthCredentials> {\n const lock = createRefreshLock(credentialsDir, LOCK_PREFIX)\n return refreshUnderCrossProcessLock<OAuthCredentials>({\n lock,\n key: sanitizeServerName(serverName),\n load: ports.load,\n isFresh: (c) => !isTokenExpired(c),\n refresh: () => refreshAccessToken(tokenEndpoint, params, options),\n save: ports.save,\n strategy,\n })\n}\n","import { spawn } from 'node:child_process'\nimport { createPkcePair, generateState, startLoopbackServer, TypedEventEmitter } from '@x-otto/shared'\n\nimport { discoverOAuth } from './discovery'\nimport { registerClient } from './registration'\nimport { exchangeCode, refreshAccessToken, isTokenExpired } from './token'\nimport { createDefaultTokenStore } from './token-store'\nimport { refreshWithCrossProcessLock } from './refresh-lock'\nimport type { RefreshStrategy } from './refresh-lock'\n\nimport type { AuthorizationServerMetadata } from './discovery'\nimport type { OAuthCredentials, RefreshTokenParams } from './token'\nimport type { TokenStore } from './token-store'\n\nexport interface McpOAuthLoginOptions {\n /** MCP server URL。 */\n serverUrl: string\n /** MCP server 名称(用于 token store 键控)。 */\n serverName: string\n /** 401 响应的 WWW-Authenticate 头(可选,优先用于发现)。 */\n wwwAuthenticate?: string\n /** 手动指定 client_id(跳过 DCR)。 */\n clientId?: string\n /** 授权服务器元数据(跳过发现,适用于已知 AS)。 */\n authorizationServer?: AuthorizationServerMetadata\n /** token store(默认文件存储 ~/.otto/mcp/credentials/)。 */\n tokenStore?: TokenStore\n /** 打开浏览器的函数(默认 `open`命令)。注入用于测试。 */\n openBrowser?: (url: string) => Promise<void>\n /** 可注入 fetch(测试)。 */\n fetch?: typeof globalThis.fetch\n /** 取消信号。 */\n signal?: AbortSignal\n}\n\nexport interface McpOAuthLoginResult {\n credentials: OAuthCredentials\n /** 是否经 DCR 注册了新的 client_id。 */\n registered: boolean\n /** DCR 获得的 client_id(持久化供后续刷新)。 */\n clientId: string\n}\n\n/**\n * 执行完整 OAuth 2.1 交互式登录(PKCE + DCR)。\n *\n * 流程(修正终局 review):\n * 1. 发现 OAuth 端点(或使用传入的已知 metadata)\n * 2. 生成 PKCE pair + state,**先**启动 loopback server(获取真实 redirect_uri)\n * 3. DCR 注册客户端(使用真实 redirect_uri;或使用传入的 client_id)\n * 4. 构造 authorize URL,打开浏览器\n * 5. 等待浏览器回调送达授权码\n * 6. 用 PKCE code_verifier 交换 token\n * 7. 保存凭据到 token store(含 client_id)\n */\nexport async function login(options: McpOAuthLoginOptions): Promise<McpOAuthLoginResult> {\n const {\n serverUrl,\n serverName,\n wwwAuthenticate,\n clientId: explicitClientId,\n authorizationServer: knownAs,\n tokenStore = createDefaultTokenStore(),\n openBrowser = defaultOpenBrowser,\n fetch: fetchImpl = fetch,\n signal,\n } = options\n\n if (signal?.aborted) {\n throw new Error('Login cancelled')\n }\n\n const as =\n knownAs ??\n (await discoverOAuth(serverUrl, { wwwAuthenticate, fetch: fetchImpl })).authorizationServer\n\n if (!as.token_endpoint) {\n throw new Error('Authorization server has no token_endpoint')\n }\n\n const pkce = createPkcePair()\n const state = generateState()\n\n const loopback = await startLoopbackServer({\n ports: [1455, 1457, 1459],\n expectedState: state,\n signal,\n })\n const redirectUri = loopback.redirectUri\n\n try {\n let clientId = explicitClientId\n let registered = false\n if (!clientId && as.registration_endpoint) {\n const reg = await registerClient(\n as.registration_endpoint,\n { redirect_uris: [redirectUri] },\n { fetch: fetchImpl },\n )\n clientId = reg.client_id\n registered = true\n }\n if (!clientId) {\n throw new Error(\n 'No client_id available. Configure client_id in MCP server settings or ensure the ' +\n 'authorization server supports Dynamic Client Registration (registration_endpoint).',\n )\n }\n\n const authParams = new URLSearchParams({\n response_type: 'code',\n client_id: clientId,\n redirect_uri: redirectUri,\n code_challenge: pkce.challenge,\n code_challenge_method: pkce.method,\n state,\n })\n // RFC 8707 resource 指示器:authorize 与 token 交换必须一致(token 交换无条件携带\n // resource=serverUrl),否则受众限定不对称——统一无条件携带 canonical serverUrl。\n authParams.set('resource', serverUrl)\n const authUrl = `${as.authorization_endpoint}?${authParams.toString()}`\n\n await openBrowser(authUrl)\n\n const callback = await loopback.waitForCallback()\n\n const credentials = await exchangeCode(\n as.token_endpoint,\n {\n code: callback.code,\n redirectUri,\n codeVerifier: pkce.verifier,\n resource: serverUrl,\n clientId,\n },\n { fetch: fetchImpl },\n )\n\n await tokenStore.save(serverName, { ...credentials, clientId })\n\n return { credentials, registered, clientId }\n } finally {\n await loopback.close()\n }\n}\n\n/** 凭据状态描述(对齐 ai AuthStore.getAuthSource/hasCredential,S1 补齐)。 */\nexport interface CredentialStatus {\n /** 是否有凭据(store 中存在)。 */\n configured: boolean\n /** 凭据来源类型:'oauth' = 有 access/refresh token。缺省 = 无凭据。 */\n source?: 'oauth'\n /** 凭据是否即将过期(isTokenExpired 判定)。 */\n expiringSoon?: boolean\n /** 是否影子态(影子态不可刷新)。 */\n shadow?: boolean\n}\n\n/** 凭据变更事件(对齐 ai AuthStore 'credential.changed')。 */\nexport interface TokenProviderEvents {\n /** 凭据发生变化(login/logout/refresh 成功,或外部进程写入被感知)。 */\n 'credential.changed': (payload: { serverName: string; source: 'self' | 'external' | 'refresh' }) => void\n}\n\nexport interface TokenProvider {\n /** 获取 access token;过期自动刷新。不可恢复的失败抛 McpUnauthorized 错误。 */\n getAccessToken(): Promise<string | undefined>\n /** 强制重新登录(login → 保存 → 返回 token)。 */\n login(options: McpOAuthLoginOptions): Promise<string>\n /** 登出(删除存储的凭据)。 */\n logout(): Promise<void>\n /** S1 补齐:只读探查凭据状态(不触发刷新/网络)。对齐 ai getAuthSource/hasCredential。 */\n describe(): Promise<CredentialStatus>\n /** S1 补齐:凭据变更事件(对齐 ai 'credential.changed')。 */\n on(event: 'credential.changed', listener: TokenProviderEvents['credential.changed']): this\n}\n\nexport interface TokenProviderOptions {\n serverName: string\n serverUrl: string\n tokenStore?: TokenStore\n /** client_id(跳过 DCR)。 */\n clientId?: string\n /** 已知的 AS metadata(跳过发现,也用于跳过刷新时的 rediscovery)。 */\n authorizationServer?: AuthorizationServerMetadata\n fetch?: typeof globalThis.fetch\n}\n\n/**\n * 创建 MCP token provider:加载 → 检查刷新 → 提供 access token。\n * token 过期时自动 refresh;refresh 失败 → getAccessToken 返回 undefined\n * (提示用户运行 `otto mcp login`)。\n */\nexport function createTokenProvider(options: TokenProviderOptions): TokenProvider {\n const {\n serverName,\n serverUrl,\n tokenStore = createDefaultTokenStore(),\n clientId,\n authorizationServer,\n fetch: fetchImpl,\n } = options\n\n let credentials: OAuthCredentials | undefined\n let cachedAs = authorizationServer ?? undefined\n\n // clientId 合并保持与原有 refresh 路径语义一致:仅当 options.clientId 存在时才回填。\n const withClientId = (creds: OAuthCredentials): OAuthCredentials =>\n clientId ? { ...creds, clientId } : creds\n\n // S1 补齐:事件面——TypedEventEmitter 提供 .on/.emit,与 TokenProvider 接口合并。\n const emitter = new TypedEventEmitter<TokenProviderEvents>()\n\n const provider: TokenProvider = {\n async getAccessToken() {\n if (!credentials) {\n credentials = await tokenStore.load(serverName)\n }\n if (!credentials) return undefined\n\n if (isTokenExpired(credentials) && credentials.refreshToken) {\n // RFC-345 §D2b(终局 review P2 修复):影子态 store 不可刷新——\n // refresh_token 是轮换型的 provider(如 Anthropic),影子进程刷新会作废\n // 磁盘上的旧 token,且新 token 只进内存 overlay → 进程退出后蒸发 →\n // 全部普通会话掉登录(本仓已有同类真实事故,见 ai auth-store overlay 防御)。\n // 返回 undefined 提示用户重新登录,不发起网络刷新。\n if (tokenStore.shadow) {\n return undefined\n }\n try {\n if (!cachedAs) {\n cachedAs = (await discoverOAuth(serverUrl, { fetch: fetchImpl })).authorizationServer\n }\n const params: RefreshTokenParams = {\n refreshToken: credentials.refreshToken,\n resource: serverUrl,\n clientId: credentials.clientId ?? clientId,\n }\n const lockDir = tokenStore.refreshLockDir\n if (lockDir) {\n credentials = await refreshWithCrossProcessLock(\n lockDir,\n serverName,\n cachedAs.token_endpoint,\n params,\n { fetch: fetchImpl },\n {\n load: () => tokenStore.load(serverName),\n save: (creds) => tokenStore.save(serverName, withClientId(creds)),\n },\n // P1 策略层:永久失败缓存 + 磁盘恢复 + 可观测性\n // (对齐 ai OAuthRefreshCoordinator 的三层防御,mcp 此前完全缺失)\n createRefreshStrategy(serverName, tokenStore, withClientId),\n )\n credentials = withClientId(credentials)\n emitter.emit('credential.changed', { serverName, source: 'refresh' })\n } else {\n credentials = await refreshAccessToken(cachedAs.token_endpoint, params, {\n fetch: fetchImpl,\n })\n credentials = withClientId(credentials)\n await tokenStore.save(serverName, credentials)\n emitter.emit('credential.changed', { serverName, source: 'refresh' })\n }\n } catch {\n return undefined\n }\n }\n\n return credentials.accessToken\n },\n\n async login(options) {\n const result = await login({\n ...options,\n tokenStore,\n clientId,\n authorizationServer: cachedAs,\n })\n credentials = result.credentials\n cachedAs ??= options.authorizationServer\n emitter.emit('credential.changed', { serverName, source: 'self' })\n return credentials.accessToken\n },\n\n async logout() {\n credentials = undefined\n cachedAs = undefined\n await tokenStore.delete(serverName)\n emitter.emit('credential.changed', { serverName, source: 'self' })\n },\n\n async describe() {\n const creds = credentials ?? await tokenStore.load(serverName)\n if (!creds) {\n return { configured: false, shadow: tokenStore.shadow }\n }\n return {\n configured: true,\n source: 'oauth' as const,\n expiringSoon: isTokenExpired(creds),\n shadow: tokenStore.shadow,\n }\n },\n\n on(event, listener) {\n emitter.on(event, listener)\n return this\n },\n }\n\n return provider\n}\n\n/**\n * P1 策略层:为 mcp 的跨进程刷新构建永久失败缓存 + 磁盘恢复策略(对齐 ai\n * OAuthRefreshCoordinator 的三层防御)。\n *\n * - shouldFailFast:同一 refreshToken 快照此前被 invalid_grant 永久拒绝 → 不竞锁/不打网络。\n * - recoverFromFailure:重读 tokenStore(绕过内存 cache)——另一进程可能已抢先刷新。\n * - cacheFailure:服务端返回 invalid_grant 时缓存(key=refreshToken 快照)。\n * - onStep:恢复步骤可观测性(旁路日志,不影响控制流)。\n *\n * 策略状态(permanentFailures Map)随 createTokenProvider 闭包生命周期——provider\n * 被 GC 时策略状态随之释放。与 ai 的差异:ai 的协调器是长驻类(permanentFailures\n * 是实例字段),mcp 的 TokenProvider 是闭包——但 mcp 的 provider 生命周期与\n * MCP 连接绑定,不需要跨连接持久化失败缓存(重新连接 = 重新 login = 新凭据)。\n */\nfunction createRefreshStrategy(\n serverName: string,\n tokenStore: TokenStore,\n withClientId: (creds: OAuthCredentials) => OAuthCredentials,\n): RefreshStrategy<OAuthCredentials> {\n const permanentFailures = new Map<string, { refreshToken: string; error: unknown }>()\n\n return {\n shouldFailFast(latest) {\n if (!latest.refreshToken) return false\n const cached = permanentFailures.get(serverName)\n return !!cached && cached.refreshToken === latest.refreshToken\n },\n\n async recoverFromFailure(latest) {\n // 重读 store(绕过内存 cache)——另一进程可能已抢先刷新并落盘\n const disk = await tokenStore.load(serverName)\n if (!disk || !disk.refreshToken) return undefined\n // 磁盘凭据的 refreshToken 与当前旧值相同 → 不是另一进程刷新的结果\n if (disk.refreshToken === latest.refreshToken) return undefined\n // 磁盘有新凭据 → 清永久失败缓存(旧快照已失效)\n permanentFailures.delete(serverName)\n return withClientId(disk)\n },\n\n cacheFailure(error, latest) {\n const errorType = (error as { errorType?: string })?.errorType\n if (errorType === 'invalid_grant' && latest.refreshToken) {\n permanentFailures.set(serverName, {\n refreshToken: latest.refreshToken,\n error,\n })\n }\n },\n\n onStep(step, detail) {\n // 旁路可观测性——当前只 console.debug,未来可接线到日志体系\n if (process.env['OTTO_DEBUG'] && step) {\n console.debug(`[mcp:refresh] ${serverName} ${step}${detail ? `: ${detail}` : ''}`)\n }\n },\n }\n}\nasync function defaultOpenBrowser(url: string): Promise<void> {\n // Windows:`start` 是 cmd.exe 内建命令、非可执行文件,`spawn('start', ...)` 会 ENOENT。\n // 必须经 `cmd /c start`;空字符串是 start 的 title 占位(防 url 被当标题)。\n // darwin=open / linux=xdg-open 是真实可执行文件,直接 spawn。\n const isWin = process.platform === 'win32'\n const cmd = isWin ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open'\n const args = isWin ? ['/c', 'start', '', url] : [url]\n\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { stdio: 'ignore', detached: true })\n child.on('error', (err) => {\n reject(new Error(`Failed to open browser: ${err.message}`))\n })\n child.on('close', (code) => {\n if (code === 0 || code === null) {\n resolve()\n } else {\n reject(new Error(`Browser command exited with code ${code}`))\n }\n })\n child.unref()\n })\n}\n","import { createLogger, McpError, TypedEventEmitter, traceStartupPhase } from '@x-otto/shared'\nimport { McpClient, createMcpClient } from './mcp-client'\nimport type { McpTransport, ProcessSpawnFactory } from './transport'\nimport { createMcpToolAdapters } from './mcp-tool-adapter'\nimport { createTokenProvider, createDefaultTokenStore } from './oauth'\nimport type {\n McpServerConfig,\n McpServerInfo,\n McpClientStatus,\n McpResource,\n McpPrompt,\n McpEvents,\n} from './types'\nimport type { AgentToolDefinition } from '@x-otto/interchange'\n\nconst logger = createLogger('@x-otto/mcp:manager')\n\nexport interface McpManagerOptions {\n /** 每个 MCP 请求超时 */\n requestTimeoutMs?: number\n /**\n * 连接阶段超时(ms,默认 10_000)。与 requestTimeoutMs 分离——见\n * `McpClientOptions.connectTimeoutMs`。只做全局配置(同 reconnectDelayMs),\n * `McpServerConfig` 暂不提供 per-server 覆盖。\n */\n connectTimeoutMs?: number\n /** 连接失败时是否静默跳过(默认 true) */\n skipOnError?: boolean\n /** 自动重连(默认 true) */\n autoReconnect?: boolean\n /** 最大重连次数(默认 3) */\n maxReconnectAttempts?: number\n /**\n * RFC-230:重连退避基准延迟(ms,指数退避,默认 500)。**只做全局配置**,不支持\n * per-server 覆盖——`McpServerConfig` 没有对应字段(与 `requestTimeoutMs`/\n * `maxReconnectAttempts` 不同,那两者有 per-server 覆盖能力)。若未来有真实需求,\n * 可另立 TODO 补 `McpServerConfig.reconnectDelayMs` + settings schema 字段。\n */\n reconnectDelayMs?: number\n /** 进程 spawn 工厂(RFC-085 M2):注入后 stdio MCP 子进程纳入 ProcessRuntime 统一管理。 */\n processSpawnFactory?: ProcessSpawnFactory\n /**\n * 注入 transport 工厂(RFC-368 M2 测试用)。缺省时按 config 构造 stdio/sse。\n * 仅用于测试注入 FakeTransport;不可用于生产环境。\n */\n transportFactory?: () => McpTransport\n}\n\nexport class McpManager extends TypedEventEmitter<McpEvents> {\n private clients = new Map<string, McpClient>()\n private options: McpManagerOptions\n private disabledServers: Set<string>\n constructor(options: McpManagerOptions = {}, disabledServers: string[] = []) {\n super()\n this.options = options\n this.disabledServers = new Set(disabledServers)\n }\n\n /**\n * 返回值(RFC-150 D1 追加,向后兼容——原调用点若不消费返回值零影响):本轮 connectAll 的\n * 汇总统计,供宿主层(McpController.start())非阻塞路径完成后驱动 toast/日志通知,\n * 不需要重新遍历/监听事件才能知道\"这轮到底连上几个\"。\n */\n async connectAll(\n servers: Record<string, McpServerConfig>,\n ): Promise<{ connected: number; failed: number }> {\n const entries = Object.entries(servers)\n if (entries.length === 0) {\n return { connected: 0, failed: 0 }\n }\n\n logger.info('Connecting to %d MCP servers...', entries.length)\n\n // RFC-150 D7/M0:per-server 打点——connectAll 内部并行,但整体常被上层 await(RFC-150 D1\n // 拟改为非阻塞);per-server 耗时是定位\"哪个 server 拖累整体\"的直接证据(§1.1 时间线)。\n const results = await Promise.allSettled(\n entries.map(([name, config]) =>\n traceStartupPhase(`mcp:${name} connect`, () => this.connect(name, config)),\n ),\n )\n\n // 统计必须看**实际结果**而不是 Promise 状态:`skipOnError !== false`(生产默认,见\n // McpController.ensureManager)时 `connect()` 把连接失败吞掉并正常 resolve,\n // 单看 fulfilled 会把失败的 server 记成 connected——宿主据此弹的 toast 会报\n // \"N connected\" 而实际一个都没连上。以 client 是否入池且 ready 为准。\n let connected = 0\n let failed = 0\n for (const [index, result] of results.entries()) {\n const name = entries[index]![0]\n // 显式禁用的 server 被 connect() 直接跳过(不入池),既不算连上也不算失败。\n if (this.disabledServers.has(name)) continue\n const client = this.clients.get(name)\n if (result.status === 'fulfilled' && client?.getStatus() === 'ready') {\n connected++\n } else {\n failed++\n }\n }\n\n logger.info('MCP servers: %d connected, %d failed', connected, failed)\n return { connected, failed }\n }\n\n async connect(name: string, config: McpServerConfig): Promise<void> {\n if (this.disabledServers.has(name)) {\n logger.info('MCP server \"%s\" is disabled, skipping', name)\n return\n }\n\n if (this.clients.has(name)) {\n await this.disconnect(name)\n }\n\n let getAuthToken: (() => Promise<string | undefined>) | undefined\n if (config.url && config.type !== 'stdio') {\n const tokenStore = createDefaultTokenStore()\n const existing = await tokenStore.load(name)\n if (existing) {\n const provider = createTokenProvider({\n serverName: name,\n serverUrl: config.url,\n tokenStore,\n clientId: existing.clientId,\n })\n getAuthToken = () => provider.getAccessToken()\n }\n }\n\n const client = createMcpClient(name, config, {\n requestTimeoutMs: config.requestTimeoutMs ?? this.options.requestTimeoutMs,\n autoReconnect: config.autoReconnect ?? this.options.autoReconnect,\n maxReconnectAttempts: config.maxReconnectAttempts ?? this.options.maxReconnectAttempts,\n // RFC-230:只读全局 this.options.reconnectDelayMs——不读 config.reconnectDelayMs,\n // 该 per-server 字段不存在也不新增(见 McpManagerOptions.reconnectDelayMs 注释)。\n reconnectDelayMs: this.options.reconnectDelayMs,\n connectTimeoutMs: this.options.connectTimeoutMs,\n getAuthToken,\n processSpawnFactory: this.options.processSpawnFactory,\n transportFactory: this.options.transportFactory,\n })\n\n client.onToolsChanged(() => {\n logger.info('MCP server \"%s\" tools changed', name)\n this.emit('tools.changed', { serverName: name, tools: client.getTools() })\n })\n\n client.onResourcesChanged(() => {\n logger.info('MCP server \"%s\" resources changed', name)\n this.emit('resources.changed', { serverName: name })\n })\n\n client.onDisconnected((info) => {\n logger.info('MCP server \"%s\" disconnected (%s): %s', name, info.kind, info.reason ?? 'unknown')\n this.emit('server.disconnected', { name, reason: info.reason, kind: info.kind })\n })\n\n // RFC-165 D9/H1:转发通用 notification 为 server.notification 事件,供\n // PanelBackendController(M2)等上层消费方订阅(如 backend/state 推送)。\n client.onNotification((method, params) => {\n this.emit('server.notification', { name, method, params })\n })\n\n try {\n await client.connect()\n this.clients.set(name, client)\n this.emit('server.connected', { name, tools: client.getTools().length })\n } catch (err) {\n const message = (err as Error).message\n // 透传错误码,供宿主据码渲染 actionable 指引(如 MCP_UNAUTHORIZED → 'otto mcp login')。\n const code = err instanceof McpError ? err.code : undefined\n this.emit('server.error', { name, error: message, code })\n\n if (this.options.skipOnError !== false) {\n logger.warn('MCP server \"%s\" connection failed, skipping: %s', name, message)\n } else {\n throw err\n }\n }\n }\n\n /**\n * RFC-368 M2:断开连接。`server.disconnected` 事件由 McpClient 的生命周期边界作为唯一\n * 发布者——manager 的 `onDisconnected` 回调已在 `connect()` 中注册转发,此处不再重复 emit,\n * 保证每个连接代次恰好发布一次断开事件(手动断开与自然关闭均遵循此契约)。\n */\n async disconnect(name: string): Promise<void> {\n const client = this.clients.get(name)\n if (client) {\n await client.disconnect()\n this.clients.delete(name)\n // server.disconnected 已由 client 的 disconnectedCallback → manager 回调转发,\n // 此处不再直接 emit,避免同一次断开触发两次事件(RFC-368 M2/D3)。\n }\n }\n\n async disconnectAll(): Promise<void> {\n const names = [...this.clients.keys()]\n await Promise.allSettled(names.map((name) => this.disconnect(name)))\n }\n\n getAllTools(): AgentToolDefinition[] {\n const tools: AgentToolDefinition[] = []\n for (const [name, client] of this.clients) {\n if (client.getStatus() === 'ready') {\n tools.push(...createMcpToolAdapters(client, name))\n }\n }\n return tools\n }\n\n getAllResources(): Array<McpResource & { serverName: string }> {\n const resources: Array<McpResource & { serverName: string }> = []\n for (const [name, client] of this.clients) {\n if (client.getStatus() === 'ready') {\n for (const resource of client.getResources()) {\n resources.push({ ...resource, serverName: name })\n }\n }\n }\n return resources\n }\n\n getAllPrompts(): Array<McpPrompt & { serverName: string }> {\n const prompts: Array<McpPrompt & { serverName: string }> = []\n for (const [name, client] of this.clients) {\n if (client.getStatus() === 'ready') {\n for (const prompt of client.getPrompts()) {\n prompts.push({ ...prompt, serverName: name })\n }\n }\n }\n return prompts\n }\n\n getClient(name: string): McpClient | undefined {\n return this.clients.get(name)\n }\n\n getServerInfos(): McpServerInfo[] {\n const infos: McpServerInfo[] = []\n\n for (const [name, client] of this.clients) {\n infos.push({\n name,\n config: client.getConfig(),\n status: client.getStatus(),\n tools: client.getTools(),\n resources: client.getResources(),\n prompts: client.getPrompts(),\n capabilities: client.getCapabilities() ?? undefined,\n error: client.getStatus() === 'error' ? 'Connection failed' : undefined,\n })\n }\n\n for (const name of this.disabledServers) {\n if (!this.clients.has(name)) {\n infos.push({\n name,\n status: 'disconnected' as McpClientStatus,\n tools: [],\n })\n }\n }\n\n return infos\n }\n\n get connectedCount(): number {\n let count = 0\n for (const client of this.clients.values()) {\n if (client.getStatus() === 'ready') {\n count++\n }\n }\n return count\n }\n\n get toolCount(): number {\n let count = 0\n for (const client of this.clients.values()) {\n if (client.getStatus() === 'ready') {\n count += client.getTools().length\n }\n }\n return count\n }\n}\n","/**\n * dot-mcp-json.ts — `.mcp.json` 文件格式解析器(RFC-421 D2,原 claude-compat).\n *\n * `.mcp.json` 形如 `{ \"mcp_servers\": { \"<name>\": { command/args/env/url/type/headers } } }`,\n * 字段与 `McpServerConfig` 基本同构。支持双键:otto 统一键 `mcp_servers`(优先)+\n * Claude Code 原生驼峰键 `mcpServers`(回退,读第三方文件)。\n * 支持 `${VAR}` / `${VAR:-default}` 环境变量展开。\n *\n * 归属本包(`@x-otto/mcp`):`.mcp.json` 是 `McpServerConfig` 的文件格式,解析器与类型同域。\n * 原在 `@x-otto/claude-compat`,造成 `coding/plugin/mcp.ts` 反向依赖 compat(RFC-421 P0-1)。\n * 信任门逻辑不在此处——由各消费方自行叠加(compat 的 `resolveClaudeMcpServers` 叠加\n * Claude 信任门;plugin 的 `resolvePluginMcpServers` 叠加插件信任门)。\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\nimport type { McpServerConfig } from './types'\n\n/** 展开 `${VAR}` 与 `${VAR:-default}`(未设或空 → default/空串),遵循 shell `:-` 语义。 */\nfunction expandEnv(value: string): string {\n return value.replace(\n /\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}/g,\n (_m, name: string, def: string | undefined) => {\n const v = process.env[name]\n return v !== undefined && v !== '' ? v : (def ?? '')\n },\n )\n}\n\nfunction expandStringArray(arr: unknown): string[] | undefined {\n if (!Array.isArray(arr)) return undefined\n return arr.filter((x): x is string => typeof x === 'string').map(expandEnv)\n}\n\nfunction expandStringRecord(rec: unknown): Record<string, string> | undefined {\n if (!rec || typeof rec !== 'object') return undefined\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(rec as Record<string, unknown>)) {\n if (typeof v === 'string') out[k] = expandEnv(v)\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/** 把一条 `.mcp.json` server 原始项映射成 McpServerConfig(仅已知字段,含 env 展开)。 */\nfunction mapServerEntry(raw: unknown): McpServerConfig | null {\n if (!raw || typeof raw !== 'object') return null\n const r = raw as Record<string, unknown>\n const config: McpServerConfig = {}\n if (typeof r['type'] === 'string') config.type = r['type'] as McpServerConfig['type']\n if (typeof r['command'] === 'string') config.command = expandEnv(r['command'])\n const args = expandStringArray(r['args'])\n if (args) config.args = args\n const env = expandStringRecord(r['env'])\n if (env) config.env = env\n if (typeof r['url'] === 'string') config.url = expandEnv(r['url'])\n const headers = expandStringRecord(r['headers'])\n if (headers) config.headers = headers\n if (typeof r['managed'] === 'boolean') config.managed = r['managed'] as boolean\n if (!config.command && !config.url) return null\n return config\n}\n\n/**\n * 解析给定目录的 `.mcp.json`(`mcp_servers` 优先,回退 `mcpServers`)。\n * 不做信任门判断;缺失/损坏 → {}。信任门由消费方叠加。\n */\nexport function parseDotMcpJson(dir: string): Record<string, McpServerConfig> {\n const path = join(resolve(dir), '.mcp.json')\n if (!existsSync(path)) return {}\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(path, 'utf-8'))\n } catch {\n return {}\n }\n const rawObj = raw as { mcp_servers?: unknown; mcpServers?: unknown }\n const servers = rawObj?.mcp_servers ?? rawObj?.mcpServers\n if (!servers || typeof servers !== 'object') return {}\n\n const out: Record<string, McpServerConfig> = {}\n for (const [name, entry] of Object.entries(servers as Record<string, unknown>)) {\n const mapped = mapServerEntry(entry)\n if (mapped) out[name] = mapped\n }\n return out\n}\n"],"mappings":"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"}