@graphorin/mcp 0.5.0 → 0.6.0
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/CHANGELOG.md +45 -0
- package/README.md +13 -13
- package/dist/client/adapt-result.js +22 -10
- package/dist/client/adapt-result.js.map +1 -1
- package/dist/client/client-handlers.js +11 -2
- package/dist/client/client-handlers.js.map +1 -1
- package/dist/client/client.js +110 -27
- package/dist/client/client.js.map +1 -1
- package/dist/client/inbound-filters.js +8 -3
- package/dist/client/inbound-filters.js.map +1 -1
- package/dist/client/mcp-resource-reader.d.ts +10 -0
- package/dist/client/mcp-resource-reader.d.ts.map +1 -1
- package/dist/client/mcp-resource-reader.js +16 -5
- package/dist/client/mcp-resource-reader.js.map +1 -1
- package/dist/client/pinning.js +2 -2
- package/dist/client/pinning.js.map +1 -1
- package/dist/client/to-tools.js.map +1 -1
- package/dist/client/transport-factory.js +1 -1
- package/dist/client/transport-factory.js.map +1 -1
- package/dist/client/types.d.ts +58 -10
- package/dist/client/types.d.ts.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/helpers/validate-config.js +1 -1
- package/dist/helpers/validate-config.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/oauth/bridge.d.ts +1 -1
- package/dist/oauth/bridge.js.map +1 -1
- package/dist/registry/json-schema.js +36 -7
- package/dist/registry/json-schema.js.map +1 -1
- package/dist/transport/types.d.ts +4 -4
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["collisionStrategy: CollisionStrategy","result: Awaited<ReturnType<typeof sdkClient.listTools>>","result: Awaited<ReturnType<typeof sdkClient.listResources>>","result: Awaited<ReturnType<typeof sdkClient.listPrompts>>","result: Awaited<ReturnType<typeof sdkClient.callTool>>","result: Awaited<ReturnType<typeof sdkClient.readResource>>","result: Awaited<ReturnType<typeof sdkClient.getPrompt>>","lastToolFingerprints: ReadonlyMap<string, string> | undefined","clientApi: MCPClient"],"sources":["../../src/client/client.ts"],"sourcesContent":["/**\n * `createMCPClient(...)` — the entry point for opening a typed MCP\n * client connection.\n *\n * The returned {@link MCPClient}:\n *\n * - Wraps the `@modelcontextprotocol/sdk` `Client` instance and the\n * selected SDK transport.\n * - Exposes `listTools` / `listResources` / `listPrompts` /\n * `callTool` / `readResource` / `getPrompt` / `close` plus the\n * strategy-aware `toTools(...)` adapter.\n * - Emits one INFO-log per server when the connected transport is\n * the deprecated SSE transport, on the resolved resumable\n * capability, and when the structured-content + outputSchema\n * round-trip first succeeds.\n *\n * @packageDocumentation\n */\n\nimport type { Tool } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { CollisionStrategy } from '@graphorin/tools/registry';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';\nimport {\n MCPCallTimeoutError,\n MCPCancelledError,\n MCPConnectionError,\n MCPInvalidConfigError,\n MCPProtocolError,\n MCPToolNotFoundError,\n MCPToolPinningError,\n} from '../errors/index.js';\nimport { deriveServerIdentity } from '../helpers/identity.js';\nimport { validateMCPServerConfig } from '../helpers/validate-config.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport { computeClientCapabilities, registerClientRequestHandlers } from './client-handlers.js';\nimport { adaptMCPTools } from './to-tools.js';\nimport { buildTransport, type TransportAuthSource } from './transport-factory.js';\nimport type {\n CreateMCPClientOptions,\n MCPCallToolResult,\n MCPClient,\n MCPContentPart,\n MCPPromptDefinition,\n MCPPromptMessage,\n MCPResourceContent,\n MCPResourceDefinition,\n MCPToolDefinition,\n MCPToToolsOptions,\n} from './types.js';\n\nconst DEFAULT_CLIENT_NAME = 'graphorin-mcp-client';\nconst DEFAULT_CLIENT_VERSION = '0.5.0';\n\n/**\n * Process-scoped dedup flag for the deprecated SSE transport WARN.\n * Once set, subsequent {@link createMCPClient} calls with the SSE\n * transport do not re-emit the warning. Tests reset via\n * {@link _resetSseWarnDedupForTesting}.\n */\nlet sseWarnEmittedThisProcess = false;\n\n/**\n * Reset the SSE WARN dedup flag. Used by tests.\n *\n * @experimental\n */\nexport function _resetSseWarnDedupForTesting(): void {\n sseWarnEmittedThisProcess = false;\n}\n\n/**\n * Open a typed MCP client connection.\n *\n * @stable\n */\nexport async function createMCPClient(options: CreateMCPClientOptions): Promise<MCPClient> {\n validateMCPServerConfig({ transport: options.transport });\n\n // Mutually exclusive (documented on `CreateMCPClientOptions`): a live\n // OAuth provider and a static pre-shared token cannot both drive the\n // outbound `Authorization` header.\n if (options.authProvider !== undefined && options.bearerToken !== undefined) {\n throw new MCPInvalidConfigError(\n '`authProvider` and `bearerToken` are mutually exclusive; supply at most one.',\n { metadata: { transport: options.transport.kind } },\n );\n }\n const auth = resolveTransportAuth(options);\n if (auth !== undefined && options.transport.kind === 'stdio') {\n throw new MCPInvalidConfigError(\n 'authProvider / bearerToken require an HTTP transport (streamable-http or sse); the stdio transport carries no Authorization header.',\n { metadata: { transport: 'stdio' } },\n );\n }\n\n if (\n options.transport.kind === 'sse' &&\n options.suppressDeprecatedTransportWarning !== true &&\n !sseWarnEmittedThisProcess\n ) {\n sseWarnEmittedThisProcess = true;\n if (options.logger !== undefined) {\n options.logger(\n 'warn',\n 'MCP SSE transport is deprecated; migrate the server to the streamable-http transport when possible.',\n );\n }\n incrementCounter('mcp.transport.deprecated.warn.total', { transport: 'sse' });\n }\n\n const built = buildTransport(options.transport, auth === undefined ? undefined : { auth });\n return createMCPClientFromSdkTransport({\n transport: built.transport,\n transportConfig: options.transport,\n ...(options.collisionStrategy === undefined\n ? {}\n : { collisionStrategy: options.collisionStrategy }),\n ...(options.priority === undefined ? {} : { priority: options.priority }),\n ...(options.serverInfoName === undefined ? {} : { serverInfoName: options.serverInfoName }),\n ...(options.logger === undefined ? {} : { logger: options.logger }),\n ...(options.clientName === undefined ? {} : { clientName: options.clientName }),\n ...(options.clientVersion === undefined ? {} : { clientVersion: options.clientVersion }),\n ...(options.elicitation === undefined ? {} : { elicitation: options.elicitation }),\n ...(options.sampling === undefined ? {} : { sampling: options.sampling }),\n });\n}\n\n/**\n * Internal factory that takes a pre-built SDK `Transport`. Exposed\n * for the test seam — production code uses {@link createMCPClient}.\n *\n * @internal\n */\nexport interface CreateMCPClientFromSdkTransportOptions {\n readonly transport: Transport;\n readonly transportConfig: import('../transport/types.js').MCPTransportConfig;\n readonly collisionStrategy?: CollisionStrategy;\n readonly priority?: number;\n readonly serverInfoName?: string;\n readonly logger?: CreateMCPClientOptions['logger'];\n readonly clientName?: string;\n readonly clientVersion?: string;\n readonly elicitation?: CreateMCPClientOptions['elicitation'];\n readonly sampling?: CreateMCPClientOptions['sampling'];\n}\n\n/**\n * Build an {@link MCPClient} from a pre-built SDK transport. The\n * production {@link createMCPClient} delegates here after building\n * the SDK transport from a {@link MCPTransportConfig}.\n *\n * @internal\n */\nexport async function createMCPClientFromSdkTransport(\n options: CreateMCPClientFromSdkTransportOptions,\n): Promise<MCPClient> {\n const sdkClient = new Client({\n name: options.clientName ?? DEFAULT_CLIENT_NAME,\n version: options.clientVersion ?? DEFAULT_CLIENT_VERSION,\n });\n\n // WI-13 (P2-2): advertise + register the server-initiated request\n // handlers (elicitation / sampling) before connecting, so they are in\n // place when the session starts. Both are gated — capabilities are\n // advertised only when the operator supplied the matching callback.\n const clientCapabilities = computeClientCapabilities({\n elicitation: options.elicitation,\n sampling: options.sampling,\n });\n if (clientCapabilities !== undefined) {\n sdkClient.registerCapabilities(clientCapabilities);\n }\n const serverIdRef = { current: 'unknown' };\n registerClientRequestHandlers(sdkClient, {\n ...(options.elicitation === undefined ? {} : { elicitation: options.elicitation }),\n ...(options.sampling === undefined ? {} : { sampling: options.sampling }),\n serverIdRef,\n });\n // MC-6: surface server-side catalogue churn — at minimum an audit\n // counter + log line; operators re-run toTools() to refresh and\n // re-sanitize the catalogue (which also re-runs the drift diff).\n sdkClient.setNotificationHandler(ToolListChangedNotificationSchema, async () => {\n incrementCounter('mcp.tools.list-changed.total', {\n server: serverIdRef.current ?? 'unknown',\n });\n options.logger?.(\n 'warn',\n 'mcp.tools.list_changed received — re-run toTools() to refresh + re-sanitize the catalogue',\n { server: serverIdRef.current },\n );\n });\n\n try {\n await sdkClient.connect(options.transport);\n } catch (cause) {\n throw new MCPConnectionError(\n `MCP transport could not be established: ${(cause as Error).message ?? String(cause)}`,\n {\n metadata: { transport: options.transportConfig.kind },\n cause,\n },\n );\n }\n\n const serverInfo = sdkClient.getServerVersion() ?? {\n name: 'unknown',\n version: '0.0.0',\n };\n const serverIdentity = deriveServerIdentity(\n options.transportConfig,\n options.serverInfoName ?? serverInfo.name,\n );\n // Backfill the server id so the client-side request handlers\n // (registered before connect) label their counters with it.\n serverIdRef.current = serverIdentity.id;\n const collisionStrategy: CollisionStrategy = options.collisionStrategy ?? 'auto-prefix';\n\n // MC-1: the client-side eventStore option was removed — per the\n // Streamable HTTP spec event replay is the SERVER's responsibility,\n // and the SDK transport auto-reconnects with Last-Event-ID on its\n // own. Warn legacy callers instead of silently ignoring the option.\n if ((options as { readonly eventStore?: unknown }).eventStore !== undefined) {\n options.logger?.(\n 'warn',\n \"the client 'eventStore' option was removed (MC-1): event replay is a server responsibility; the SDK transport auto-reconnects with Last-Event-ID. Remove the option.\",\n { server: serverIdentity.id },\n );\n }\n // MC-9: a session id means stateful routing, NOT a replay guarantee.\n const sessionIdPresent =\n isStreamableHttp(options.transport) &&\n (options.transport as StreamableHTTPClientTransport).sessionId !== undefined;\n const resumable = sessionIdPresent;\n if (options.logger !== undefined) {\n options.logger('info', 'mcp.session.session-id.resolved', {\n server: serverIdentity.id,\n value: sessionIdPresent,\n source: sessionIdPresent ? 'session-id-present' : 'transport-default',\n });\n }\n let structuredContentSeenLogged = false;\n\n async function listTools(opts?: {\n signal?: AbortSignal;\n }): Promise<ReadonlyArray<MCPToolDefinition>> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n let result: Awaited<ReturnType<typeof sdkClient.listTools>>;\n try {\n result = await sdkClient.listTools({}, requestOptions);\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n const tools = (result.tools ?? []) as ReadonlyArray<{\n name: string;\n description?: string;\n inputSchema: Readonly<Record<string, unknown>>;\n outputSchema?: Readonly<Record<string, unknown>>;\n title?: string;\n }>;\n if (!structuredContentSeenLogged && tools.some((t) => t.outputSchema !== undefined)) {\n structuredContentSeenLogged = true;\n if (options.logger !== undefined) {\n options.logger('info', 'mcp.server.structured-content.detected', {\n server: serverIdentity.id,\n });\n }\n }\n return Object.freeze(\n tools.map((t) =>\n Object.freeze({\n name: t.name,\n description: t.description ?? '',\n inputSchema: t.inputSchema,\n ...(t.outputSchema === undefined ? {} : { outputSchema: t.outputSchema }),\n ...(t.title === undefined ? {} : { title: t.title }),\n }),\n ),\n );\n }\n\n async function listResources(opts?: {\n signal?: AbortSignal;\n }): Promise<ReadonlyArray<MCPResourceDefinition>> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n let result: Awaited<ReturnType<typeof sdkClient.listResources>>;\n try {\n result = await sdkClient.listResources({}, requestOptions);\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n const items = (result.resources ?? []) as ReadonlyArray<{\n uri: string;\n name?: string;\n description?: string;\n mimeType?: string;\n }>;\n return Object.freeze(\n items.map((r) =>\n Object.freeze({\n uri: r.uri,\n ...(r.name === undefined ? {} : { name: r.name }),\n ...(r.description === undefined ? {} : { description: r.description }),\n ...(r.mimeType === undefined ? {} : { mimeType: r.mimeType }),\n }),\n ),\n );\n }\n\n async function listPrompts(opts?: {\n signal?: AbortSignal;\n }): Promise<ReadonlyArray<MCPPromptDefinition>> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n let result: Awaited<ReturnType<typeof sdkClient.listPrompts>>;\n try {\n result = await sdkClient.listPrompts({}, requestOptions);\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n const items = (result.prompts ?? []) as ReadonlyArray<{\n name: string;\n description?: string;\n arguments?: ReadonlyArray<{ name: string; description?: string; required?: boolean }>;\n }>;\n return Object.freeze(\n items.map((p) =>\n Object.freeze({\n name: p.name,\n ...(p.description === undefined ? {} : { description: p.description }),\n ...(p.arguments === undefined\n ? {}\n : {\n arguments: Object.freeze(\n p.arguments.map((a) =>\n Object.freeze({\n name: a.name,\n ...(a.description === undefined ? {} : { description: a.description }),\n ...(a.required === undefined ? {} : { required: a.required }),\n }),\n ),\n ),\n }),\n }),\n ),\n );\n }\n\n async function callTool(\n name: string,\n args: unknown,\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<MCPCallToolResult> {\n // MC-3: `timeoutMs` maps onto the SDK's RequestOptions — both the\n // per-attempt and the total ceiling, so progress notifications\n // cannot extend past the caller's budget.\n const requestOptions = {\n ...(opts?.signal === undefined ? {} : { signal: opts.signal }),\n ...(opts?.timeoutMs === undefined\n ? {}\n : { timeout: opts.timeoutMs, maxTotalTimeout: opts.timeoutMs }),\n };\n incrementCounter('mcp.call.invoked.total', { server: serverIdentity.id, tool: name });\n let result: Awaited<ReturnType<typeof sdkClient.callTool>>;\n try {\n result = await sdkClient.callTool(\n { name, arguments: args as Record<string, unknown> },\n undefined,\n requestOptions,\n );\n } catch (cause) {\n const mapped = mapSdkError(cause, { tool: name });\n if (mapped instanceof MCPCancelledError) {\n incrementCounter('mcp.call.cancelled.total', { server: serverIdentity.id, tool: name });\n } else {\n incrementCounter('mcp.call.failed.total', { server: serverIdentity.id, tool: name });\n }\n throw mapped;\n }\n const content = (result.content ?? []) as ReadonlyArray<MCPContentPart>;\n return Object.freeze({\n content: Object.freeze([...content]),\n ...(result.structuredContent === undefined\n ? {}\n : { structuredContent: result.structuredContent as Readonly<Record<string, unknown>> }),\n ...(result.isError === undefined ? {} : { isError: Boolean(result.isError) }),\n });\n }\n\n async function readResource(\n uri: string,\n opts?: { signal?: AbortSignal },\n ): Promise<MCPResourceContent> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n let result: Awaited<ReturnType<typeof sdkClient.readResource>>;\n try {\n result = await sdkClient.readResource({ uri }, requestOptions);\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n const first = ((result.contents ?? []) as ReadonlyArray<MCPResourceContent>)[0];\n if (first === undefined) {\n throw new MCPProtocolError(`MCP server returned no contents for resource '${uri}'.`, {\n metadata: { server: serverIdentity.id },\n });\n }\n return Object.freeze(first);\n }\n\n async function getPrompt(\n name: string,\n args?: unknown,\n opts?: { signal?: AbortSignal },\n ): Promise<{ readonly messages: ReadonlyArray<MCPPromptMessage> }> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n let result: Awaited<ReturnType<typeof sdkClient.getPrompt>>;\n try {\n result = await sdkClient.getPrompt(\n {\n name,\n ...(args === undefined ? {} : { arguments: args as Record<string, string> }),\n },\n requestOptions,\n );\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n const messages = (result.messages ?? []).map(\n (m): MCPPromptMessage =>\n Object.freeze({\n role: m.role,\n content: m.content as MCPContentPart,\n }),\n );\n return Object.freeze({ messages: Object.freeze(messages) });\n }\n\n let lastToolFingerprints: ReadonlyMap<string, string> | undefined;\n\n async function toTools(toolsOpts?: MCPToToolsOptions): Promise<ReadonlyArray<Tool>> {\n const catalogue = await listTools();\n const adapted = adaptMCPTools({\n client: clientApi,\n serverIdentity,\n catalogue,\n ...(toolsOpts === undefined ? {} : { options: toolsOpts }),\n ...(options.logger === undefined ? {} : { logger: options.logger }),\n });\n // MC-6: cross-snapshot drift — a definition changing behind an\n // already-seen name within this client's lifetime is audited.\n if (lastToolFingerprints !== undefined) {\n for (const [name, hash] of adapted.fingerprints) {\n const previous = lastToolFingerprints.get(name);\n if (previous !== undefined && previous !== hash) {\n incrementCounter('mcp.tools.changed.total', {\n server: serverIdentity.id,\n tool: name,\n });\n options.logger?.('warn', 'mcp.tools.changed: definition drifted between snapshots', {\n server: serverIdentity.id,\n tool: name,\n previous,\n current: hash,\n });\n }\n }\n }\n lastToolFingerprints = adapted.fingerprints;\n // MC-6: operator pins from a previously approved snapshot — the\n // rug-pull (approve-then-swap across restarts) posture.\n const pins = toolsOpts?.pinnedFingerprints;\n if (pins !== undefined) {\n for (const [name, pinned] of Object.entries(pins)) {\n const current = adapted.fingerprints.get(name);\n if (current !== undefined && current !== pinned) {\n if (toolsOpts?.onPinMismatch === 'reject') {\n throw new MCPToolPinningError(\n `MCP tool '${name}' no longer matches its pinned definition fingerprint — the server changed the definition behind an approved name.`,\n { metadata: { server: serverIdentity.id, tool: name } },\n );\n }\n incrementCounter('mcp.tools.pin-mismatch.total', {\n server: serverIdentity.id,\n tool: name,\n });\n options.logger?.('warn', 'mcp.tools.pin-mismatch: pinned fingerprint diverged', {\n server: serverIdentity.id,\n tool: name,\n });\n }\n }\n }\n return adapted.tools;\n }\n\n async function close(): Promise<void> {\n try {\n await sdkClient.close();\n } catch (cause) {\n // Treat double-close as a no-op; surface other failures.\n if (cause instanceof Error && cause.message.toLowerCase().includes('already')) return;\n throw new MCPConnectionError('MCP transport could not be closed cleanly.', {\n metadata: { transport: options.transportConfig.kind, server: serverIdentity.id },\n cause,\n });\n }\n }\n\n const clientApi: MCPClient = Object.freeze({\n id: serverIdentity.id,\n serverInfo,\n serverIdentity,\n collisionStrategy,\n ...(options.priority === undefined ? {} : { priority: options.priority }),\n sessionIdPresent,\n resumable,\n listTools,\n listResources,\n listPrompts,\n callTool,\n readResource,\n getPrompt,\n toTools,\n close,\n });\n return clientApi;\n}\n\n/**\n * Resolve the live {@link TransportAuthSource} for the outbound\n * `Authorization` header from the mutually-exclusive `authProvider` /\n * `bearerToken` options. `authProvider.resolveHeader()` already returns\n * the full header value (`Bearer …`); a static `bearerToken` is wrapped\n * into a constant `Bearer`-prefixed resolver. Returns `undefined` when\n * neither is supplied (no header injection).\n */\nfunction resolveTransportAuth(options: CreateMCPClientOptions): TransportAuthSource | undefined {\n const provider = options.authProvider;\n if (provider !== undefined) {\n return { resolveHeader: () => provider.resolveHeader() };\n }\n if (options.bearerToken !== undefined) {\n const token = options.bearerToken;\n const header = /^bearer\\s/i.test(token) ? token : `Bearer ${token}`;\n return { resolveHeader: () => header };\n }\n return undefined;\n}\n\nfunction mapSdkError(cause: unknown, ctx: { readonly tool?: string }): Error {\n const metadata = ctx.tool === undefined ? {} : { tool: ctx.tool };\n if (cause instanceof Error) {\n const name = cause.name;\n const message = cause.message ?? '';\n if (name === 'AbortError' || /aborted|cancell/i.test(message)) {\n return new MCPCancelledError('MCP request was cancelled.', { metadata, cause });\n }\n // MC-3: the SDK reports request-timeout as a plain McpError — map it\n // onto the advertised typed class instead of MCPProtocolError.\n if (/request timed out|timed out/i.test(message)) {\n return new MCPCallTimeoutError(\n `MCP request timed out${ctx.tool ? ` (tool: ${ctx.tool})` : ''}.`,\n {\n metadata,\n cause,\n },\n );\n }\n if (/unknown\\s+tool|tool\\s+not\\s+found|method\\s+not\\s+found/i.test(message)) {\n return new MCPToolNotFoundError(`MCP tool not found${ctx.tool ? `: ${ctx.tool}` : ''}.`, {\n metadata,\n cause,\n });\n }\n return new MCPProtocolError(message.length === 0 ? cause.toString() : message, {\n metadata,\n cause,\n });\n }\n return new MCPProtocolError(`MCP request failed: ${String(cause)}`, { metadata, cause });\n}\n\nfunction isStreamableHttp(transport: unknown): transport is StreamableHTTPClientTransport {\n return (\n typeof transport === 'object' &&\n transport !== null &&\n transport.constructor !== undefined &&\n transport.constructor.name === 'StreamableHTTPClientTransport'\n );\n}\n\nexport type { ServerIdentity };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["collisionStrategy: CollisionStrategy","tools: SdkTool[]","cursor: string | undefined","result: Awaited<ReturnType<typeof sdkClient.listTools>>","items: SdkResource[]","result: Awaited<ReturnType<typeof sdkClient.listResources>>","items: SdkPrompt[]","result: Awaited<ReturnType<typeof sdkClient.listPrompts>>","result: Awaited<ReturnType<typeof sdkClient.callTool>>","result: Awaited<ReturnType<typeof sdkClient.readResource>>","result: Awaited<ReturnType<typeof sdkClient.getPrompt>>","lastToolFingerprints: ReadonlyMap<string, string> | undefined","recorded: Record<string, string>","clientApi: MCPClient"],"sources":["../../src/client/client.ts"],"sourcesContent":["/**\n * `createMCPClient(...)` - the entry point for opening a typed MCP\n * client connection.\n *\n * The returned {@link MCPClient}:\n *\n * - Wraps the `@modelcontextprotocol/sdk` `Client` instance and the\n * selected SDK transport.\n * - Exposes `listTools` / `listResources` / `listPrompts` /\n * `callTool` / `readResource` / `getPrompt` / `close` plus the\n * strategy-aware `toTools(...)` adapter.\n * - Emits one INFO-log per server when the connected transport is\n * the deprecated SSE transport, on the resolved resumable\n * capability, and when the structured-content + outputSchema\n * round-trip first succeeds.\n *\n * @packageDocumentation\n */\n\nimport type { Tool } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { CollisionStrategy } from '@graphorin/tools/registry';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';\nimport {\n MCPCallTimeoutError,\n MCPCancelledError,\n MCPConnectionError,\n MCPInvalidConfigError,\n MCPProtocolError,\n MCPToolNotFoundError,\n MCPToolPinningError,\n} from '../errors/index.js';\nimport { deriveServerIdentity } from '../helpers/identity.js';\nimport { validateMCPServerConfig } from '../helpers/validate-config.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport { computeClientCapabilities, registerClientRequestHandlers } from './client-handlers.js';\nimport { adaptMCPTools } from './to-tools.js';\nimport { buildTransport, type TransportAuthSource } from './transport-factory.js';\nimport type {\n CreateMCPClientOptions,\n MCPCallToolResult,\n MCPClient,\n MCPContentPart,\n MCPPromptDefinition,\n MCPPromptMessage,\n MCPResourceContent,\n MCPResourceDefinition,\n MCPToolDefinition,\n MCPToToolsOptions,\n} from './types.js';\n\nconst DEFAULT_CLIENT_NAME = 'graphorin-mcp-client';\nconst DEFAULT_CLIENT_VERSION = '0.6.0';\n\n/**\n * Process-scoped dedup flag for the deprecated SSE transport WARN.\n * Once set, subsequent {@link createMCPClient} calls with the SSE\n * transport do not re-emit the warning. Tests reset via\n * {@link _resetSseWarnDedupForTesting}.\n */\nlet sseWarnEmittedThisProcess = false;\n\n/**\n * Reset the SSE WARN dedup flag. Used by tests.\n *\n * @experimental\n */\nexport function _resetSseWarnDedupForTesting(): void {\n sseWarnEmittedThisProcess = false;\n}\n\n/**\n * Open a typed MCP client connection.\n *\n * @stable\n */\nexport async function createMCPClient(options: CreateMCPClientOptions): Promise<MCPClient> {\n validateMCPServerConfig({ transport: options.transport });\n\n // Mutually exclusive (documented on `CreateMCPClientOptions`): a live\n // OAuth provider and a static pre-shared token cannot both drive the\n // outbound `Authorization` header.\n if (options.authProvider !== undefined && options.bearerToken !== undefined) {\n throw new MCPInvalidConfigError(\n '`authProvider` and `bearerToken` are mutually exclusive; supply at most one.',\n { metadata: { transport: options.transport.kind } },\n );\n }\n const auth = resolveTransportAuth(options);\n if (auth !== undefined && options.transport.kind === 'stdio') {\n throw new MCPInvalidConfigError(\n 'authProvider / bearerToken require an HTTP transport (streamable-http or sse); the stdio transport carries no Authorization header.',\n { metadata: { transport: 'stdio' } },\n );\n }\n\n if (\n options.transport.kind === 'sse' &&\n options.suppressDeprecatedTransportWarning !== true &&\n !sseWarnEmittedThisProcess\n ) {\n sseWarnEmittedThisProcess = true;\n if (options.logger !== undefined) {\n options.logger(\n 'warn',\n 'MCP SSE transport is deprecated; migrate the server to the streamable-http transport when possible.',\n );\n }\n incrementCounter('mcp.transport.deprecated.warn.total', { transport: 'sse' });\n }\n\n const built = buildTransport(options.transport, auth === undefined ? undefined : { auth });\n return createMCPClientFromSdkTransport({\n transport: built.transport,\n transportConfig: options.transport,\n ...(options.collisionStrategy === undefined\n ? {}\n : { collisionStrategy: options.collisionStrategy }),\n ...(options.priority === undefined ? {} : { priority: options.priority }),\n ...(options.serverInfoName === undefined ? {} : { serverInfoName: options.serverInfoName }),\n ...(options.logger === undefined ? {} : { logger: options.logger }),\n ...(options.clientName === undefined ? {} : { clientName: options.clientName }),\n ...(options.clientVersion === undefined ? {} : { clientVersion: options.clientVersion }),\n ...(options.elicitation === undefined ? {} : { elicitation: options.elicitation }),\n ...(options.sampling === undefined ? {} : { sampling: options.sampling }),\n ...(options.onTransportClose === undefined\n ? {}\n : { onTransportClose: options.onTransportClose }),\n ...(options.onTransportError === undefined\n ? {}\n : { onTransportError: options.onTransportError }),\n });\n}\n\n/**\n * Internal factory that takes a pre-built SDK `Transport`. Exposed\n * for the test seam - production code uses {@link createMCPClient}.\n *\n * @internal\n */\nexport interface CreateMCPClientFromSdkTransportOptions {\n readonly transport: Transport;\n readonly transportConfig: import('../transport/types.js').MCPTransportConfig;\n readonly collisionStrategy?: CollisionStrategy;\n readonly priority?: number;\n readonly serverInfoName?: string;\n readonly logger?: CreateMCPClientOptions['logger'];\n readonly clientName?: string;\n readonly clientVersion?: string;\n readonly elicitation?: CreateMCPClientOptions['elicitation'];\n readonly sampling?: CreateMCPClientOptions['sampling'];\n readonly onTransportClose?: CreateMCPClientOptions['onTransportClose'];\n readonly onTransportError?: CreateMCPClientOptions['onTransportError'];\n}\n\n/**\n * Build an {@link MCPClient} from a pre-built SDK transport. The\n * production {@link createMCPClient} delegates here after building\n * the SDK transport from a {@link MCPTransportConfig}.\n *\n * @internal\n */\nexport async function createMCPClientFromSdkTransport(\n options: CreateMCPClientFromSdkTransportOptions,\n): Promise<MCPClient> {\n const sdkClient = new Client({\n name: options.clientName ?? DEFAULT_CLIENT_NAME,\n version: options.clientVersion ?? DEFAULT_CLIENT_VERSION,\n });\n\n // WI-13 (P2-2): advertise + register the server-initiated request\n // handlers (elicitation / sampling) before connecting, so they are in\n // place when the session starts. Both are gated - capabilities are\n // advertised only when the operator supplied the matching callback.\n const clientCapabilities = computeClientCapabilities({\n elicitation: options.elicitation,\n sampling: options.sampling,\n });\n if (clientCapabilities !== undefined) {\n sdkClient.registerCapabilities(clientCapabilities);\n }\n const serverIdRef = { current: 'unknown' };\n registerClientRequestHandlers(sdkClient, {\n ...(options.elicitation === undefined ? {} : { elicitation: options.elicitation }),\n ...(options.sampling === undefined ? {} : { sampling: options.sampling }),\n serverIdRef,\n });\n // MC-6: surface server-side catalogue churn - at minimum an audit\n // counter + log line; operators re-run toTools() to refresh and\n // re-sanitize the catalogue (which also re-runs the drift diff).\n sdkClient.setNotificationHandler(ToolListChangedNotificationSchema, async () => {\n incrementCounter('mcp.tools.list-changed.total', {\n server: serverIdRef.current ?? 'unknown',\n });\n options.logger?.(\n 'warn',\n 'mcp.tools.list_changed received - re-run toTools() to refresh + re-sanitize the catalogue',\n { server: serverIdRef.current },\n );\n });\n\n try {\n await sdkClient.connect(options.transport);\n } catch (cause) {\n throw new MCPConnectionError(\n `MCP transport could not be established: ${(cause as Error).message ?? String(cause)}`,\n {\n metadata: { transport: options.transportConfig.kind },\n cause,\n },\n );\n }\n\n const serverInfo = sdkClient.getServerVersion() ?? {\n name: 'unknown',\n version: '0.0.0',\n };\n const serverIdentity = deriveServerIdentity(\n options.transportConfig,\n options.serverInfoName ?? serverInfo.name,\n );\n // Backfill the server id so the client-side request handlers\n // (registered before connect) label their counters with it.\n serverIdRef.current = serverIdentity.id;\n // mcp-skills-10: surface transport lifecycle. Without these a dead\n // stdio child / dropped HTTP session is observable only as\n // MCPProtocolErrors on subsequent calls. The SDK Protocol base\n // exposes assignable onclose/onerror callbacks (the transport's own\n // handlers are managed by the SDK - never overwrite those).\n sdkClient.onclose = () => {\n incrementCounter('mcp.transport.closed.total', { server: serverIdentity.id });\n options.logger?.('warn', 'MCP transport closed - rebuild the client to reconnect', {\n server: serverIdentity.id,\n });\n options.onTransportClose?.({ server: serverIdentity.id });\n };\n sdkClient.onerror = (error: Error) => {\n incrementCounter('mcp.transport.error.total', { server: serverIdentity.id });\n options.onTransportError?.(error, { server: serverIdentity.id });\n };\n const collisionStrategy: CollisionStrategy = options.collisionStrategy ?? 'auto-prefix';\n\n // MC-1: the client-side eventStore option was removed - per the\n // Streamable HTTP spec event replay is the SERVER's responsibility,\n // and the SDK transport auto-reconnects with Last-Event-ID on its\n // own. Warn legacy callers instead of silently ignoring the option.\n if ((options as { readonly eventStore?: unknown }).eventStore !== undefined) {\n options.logger?.(\n 'warn',\n \"the client 'eventStore' option was removed (MC-1): event replay is a server responsibility; the SDK transport auto-reconnects with Last-Event-ID. Remove the option.\",\n { server: serverIdentity.id },\n );\n }\n // MC-9: a session id means stateful routing, NOT a replay guarantee.\n const sessionIdPresent =\n isStreamableHttp(options.transport) &&\n (options.transport as StreamableHTTPClientTransport).sessionId !== undefined;\n const resumable = sessionIdPresent;\n if (options.logger !== undefined) {\n options.logger('info', 'mcp.session.session-id.resolved', {\n server: serverIdentity.id,\n value: sessionIdPresent,\n source: sessionIdPresent ? 'session-id-present' : 'transport-default',\n });\n }\n let structuredContentSeenLogged = false;\n\n /**\n * mcp-skills-02: MCP list operations are cursor-paginated (protocol\n * 2024-11-05+) and the SDK does NOT auto-paginate - a single call\n * returns page 1 only. Ignoring `nextCursor` silently truncated the\n * catalogue: tools beyond the first page never reached `toTools()`,\n * defer-loading thresholds counted a partial catalogue, pin\n * fingerprints covered a partial catalogue, and resources/prompts were\n * under-listed. Every list surface now drains the cursor chain, with a\n * defensive page cap against buggy/adversarial servers that never\n * terminate it.\n */\n const MAX_LIST_PAGES = 100;\n\n function warnListPageCap(what: 'tools' | 'resources' | 'prompts', collected: number): void {\n options.logger?.(\n 'warn',\n `mcp.list.${what}.page-cap-reached: server kept returning nextCursor after ${MAX_LIST_PAGES} pages; the ${what} catalogue is truncated at ${collected} entries.`,\n { server: serverIdentity.id },\n );\n }\n\n async function listTools(opts?: {\n signal?: AbortSignal;\n }): Promise<ReadonlyArray<MCPToolDefinition>> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n type SdkTool = {\n name: string;\n description?: string;\n inputSchema: Readonly<Record<string, unknown>>;\n outputSchema?: Readonly<Record<string, unknown>>;\n title?: string;\n };\n const tools: SdkTool[] = [];\n let cursor: string | undefined;\n for (let page = 0; ; page++) {\n if (page >= MAX_LIST_PAGES) {\n warnListPageCap('tools', tools.length);\n break;\n }\n let result: Awaited<ReturnType<typeof sdkClient.listTools>>;\n try {\n result = await sdkClient.listTools(cursor === undefined ? {} : { cursor }, requestOptions);\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n tools.push(...((result.tools ?? []) as ReadonlyArray<SdkTool>));\n cursor = (result as { nextCursor?: string }).nextCursor;\n if (cursor === undefined) break;\n }\n if (!structuredContentSeenLogged && tools.some((t) => t.outputSchema !== undefined)) {\n structuredContentSeenLogged = true;\n if (options.logger !== undefined) {\n options.logger('info', 'mcp.server.structured-content.detected', {\n server: serverIdentity.id,\n });\n }\n }\n return Object.freeze(\n tools.map((t) =>\n Object.freeze({\n name: t.name,\n description: t.description ?? '',\n inputSchema: t.inputSchema,\n ...(t.outputSchema === undefined ? {} : { outputSchema: t.outputSchema }),\n ...(t.title === undefined ? {} : { title: t.title }),\n }),\n ),\n );\n }\n\n async function listResources(opts?: {\n signal?: AbortSignal;\n }): Promise<ReadonlyArray<MCPResourceDefinition>> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n type SdkResource = {\n uri: string;\n name?: string;\n description?: string;\n mimeType?: string;\n };\n const items: SdkResource[] = [];\n let cursor: string | undefined;\n for (let page = 0; ; page++) {\n if (page >= MAX_LIST_PAGES) {\n warnListPageCap('resources', items.length);\n break;\n }\n let result: Awaited<ReturnType<typeof sdkClient.listResources>>;\n try {\n result = await sdkClient.listResources(\n cursor === undefined ? {} : { cursor },\n requestOptions,\n );\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n items.push(...((result.resources ?? []) as ReadonlyArray<SdkResource>));\n cursor = (result as { nextCursor?: string }).nextCursor;\n if (cursor === undefined) break;\n }\n return Object.freeze(\n items.map((r) =>\n Object.freeze({\n uri: r.uri,\n ...(r.name === undefined ? {} : { name: r.name }),\n ...(r.description === undefined ? {} : { description: r.description }),\n ...(r.mimeType === undefined ? {} : { mimeType: r.mimeType }),\n }),\n ),\n );\n }\n\n async function listPrompts(opts?: {\n signal?: AbortSignal;\n }): Promise<ReadonlyArray<MCPPromptDefinition>> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n type SdkPrompt = {\n name: string;\n description?: string;\n arguments?: ReadonlyArray<{ name: string; description?: string; required?: boolean }>;\n };\n const items: SdkPrompt[] = [];\n let cursor: string | undefined;\n for (let page = 0; ; page++) {\n if (page >= MAX_LIST_PAGES) {\n warnListPageCap('prompts', items.length);\n break;\n }\n let result: Awaited<ReturnType<typeof sdkClient.listPrompts>>;\n try {\n result = await sdkClient.listPrompts(\n cursor === undefined ? {} : { cursor },\n requestOptions,\n );\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n items.push(...((result.prompts ?? []) as ReadonlyArray<SdkPrompt>));\n cursor = (result as { nextCursor?: string }).nextCursor;\n if (cursor === undefined) break;\n }\n return Object.freeze(\n items.map((p) =>\n Object.freeze({\n name: p.name,\n ...(p.description === undefined ? {} : { description: p.description }),\n ...(p.arguments === undefined\n ? {}\n : {\n arguments: Object.freeze(\n p.arguments.map((a) =>\n Object.freeze({\n name: a.name,\n ...(a.description === undefined ? {} : { description: a.description }),\n ...(a.required === undefined ? {} : { required: a.required }),\n }),\n ),\n ),\n }),\n }),\n ),\n );\n }\n\n async function callTool(\n name: string,\n args: unknown,\n opts?: { signal?: AbortSignal; timeoutMs?: number },\n ): Promise<MCPCallToolResult> {\n // MC-3: `timeoutMs` maps onto the SDK's RequestOptions - both the\n // per-attempt and the total ceiling, so progress notifications\n // cannot extend past the caller's budget.\n const requestOptions = {\n ...(opts?.signal === undefined ? {} : { signal: opts.signal }),\n ...(opts?.timeoutMs === undefined\n ? {}\n : { timeout: opts.timeoutMs, maxTotalTimeout: opts.timeoutMs }),\n };\n incrementCounter('mcp.call.invoked.total', { server: serverIdentity.id, tool: name });\n let result: Awaited<ReturnType<typeof sdkClient.callTool>>;\n try {\n result = await sdkClient.callTool(\n { name, arguments: args as Record<string, unknown> },\n undefined,\n requestOptions,\n );\n } catch (cause) {\n const mapped = mapSdkError(cause, { tool: name });\n if (mapped instanceof MCPCancelledError) {\n incrementCounter('mcp.call.cancelled.total', { server: serverIdentity.id, tool: name });\n } else {\n incrementCounter('mcp.call.failed.total', { server: serverIdentity.id, tool: name });\n }\n throw mapped;\n }\n const content = (result.content ?? []) as ReadonlyArray<MCPContentPart>;\n return Object.freeze({\n content: Object.freeze([...content]),\n ...(result.structuredContent === undefined\n ? {}\n : { structuredContent: result.structuredContent as Readonly<Record<string, unknown>> }),\n ...(result.isError === undefined ? {} : { isError: Boolean(result.isError) }),\n });\n }\n\n async function readResourceContents(\n uri: string,\n opts?: { signal?: AbortSignal },\n ): Promise<ReadonlyArray<MCPResourceContent>> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n let result: Awaited<ReturnType<typeof sdkClient.readResource>>;\n try {\n result = await sdkClient.readResource({ uri }, requestOptions);\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n const contents = (result.contents ?? []) as ReadonlyArray<MCPResourceContent>;\n if (contents.length === 0) {\n throw new MCPProtocolError(`MCP server returned no contents for resource '${uri}'.`, {\n metadata: { server: serverIdentity.id },\n });\n }\n return Object.freeze(contents.map((c) => Object.freeze(c)));\n }\n\n async function readResource(\n uri: string,\n opts?: { signal?: AbortSignal },\n ): Promise<MCPResourceContent> {\n const contents = await readResourceContents(uri, opts);\n // mcp-skills-11: the `resources/read` result is an ARRAY precisely\n // because one URI can yield multiple contents. The single-content\n // convenience keeps its shape, but a silent truncation is now a\n // visible one.\n if (contents.length > 1) {\n incrementCounter('mcp.resource.multi-content-truncated.total', {\n server: serverIdentity.id,\n });\n options.logger?.(\n 'warn',\n `resource '${uri}' returned ${contents.length} content items; readResource() surfaces only the first - use readResourceContents() for all of them`,\n { server: serverIdentity.id },\n );\n }\n return contents[0] as MCPResourceContent;\n }\n\n async function getPrompt(\n name: string,\n args?: unknown,\n opts?: { signal?: AbortSignal },\n ): Promise<{ readonly messages: ReadonlyArray<MCPPromptMessage> }> {\n const requestOptions = opts?.signal === undefined ? {} : { signal: opts.signal };\n let result: Awaited<ReturnType<typeof sdkClient.getPrompt>>;\n try {\n result = await sdkClient.getPrompt(\n {\n name,\n ...(args === undefined ? {} : { arguments: args as Record<string, string> }),\n },\n requestOptions,\n );\n } catch (cause) {\n throw mapSdkError(cause, {});\n }\n const messages = (result.messages ?? []).map(\n (m): MCPPromptMessage =>\n Object.freeze({\n role: m.role,\n content: m.content as MCPContentPart,\n }),\n );\n return Object.freeze({ messages: Object.freeze(messages) });\n }\n\n let lastToolFingerprints: ReadonlyMap<string, string> | undefined;\n\n async function toTools(toolsOpts?: MCPToToolsOptions): Promise<ReadonlyArray<Tool>> {\n const catalogue = await listTools();\n const adapted = adaptMCPTools({\n client: clientApi,\n serverIdentity,\n catalogue,\n ...(toolsOpts === undefined ? {} : { options: toolsOpts }),\n ...(options.logger === undefined ? {} : { logger: options.logger }),\n });\n // MC-6: cross-snapshot drift - a definition changing behind an\n // already-seen name within this client's lifetime is audited.\n if (lastToolFingerprints !== undefined) {\n for (const [name, hash] of adapted.fingerprints) {\n const previous = lastToolFingerprints.get(name);\n if (previous !== undefined && previous !== hash) {\n incrementCounter('mcp.tools.changed.total', {\n server: serverIdentity.id,\n tool: name,\n });\n options.logger?.('warn', 'mcp.tools.changed: definition drifted between snapshots', {\n server: serverIdentity.id,\n tool: name,\n previous,\n current: hash,\n });\n }\n }\n }\n lastToolFingerprints = adapted.fingerprints;\n // MC-6: operator pins from a previously approved snapshot - the\n // rug-pull (approve-then-swap across restarts) posture. C6 extends it\n // with durable trust-on-first-use via `pinStore`: the first snapshot\n // is RECORDED, later snapshots are COMPARED, and a store-backed\n // mismatch defaults to 'reject' (a persisted first approval is an\n // explicit trust decision).\n let pins = toolsOpts?.pinnedFingerprints;\n let mismatchAction = toolsOpts?.onPinMismatch ?? 'warn';\n const pinStore = toolsOpts?.pinStore;\n if (pins === undefined && pinStore !== undefined) {\n const stored = await pinStore.get(serverIdentity.id);\n if (stored === undefined) {\n const recorded: Record<string, string> = {};\n for (const [name, hash] of adapted.fingerprints) recorded[name] = hash;\n await pinStore.set(serverIdentity.id, recorded);\n incrementCounter('mcp.tools.pins-recorded.total', { server: serverIdentity.id });\n options.logger?.('info', 'mcp.tools.pins-recorded: first-use fingerprints stored', {\n server: serverIdentity.id,\n tools: Object.keys(recorded).length,\n });\n } else {\n pins = stored;\n mismatchAction = toolsOpts?.onPinMismatch ?? 'reject';\n }\n }\n if (pins !== undefined) {\n for (const [name, pinned] of Object.entries(pins)) {\n const current = adapted.fingerprints.get(name);\n if (current !== undefined && current !== pinned) {\n if (mismatchAction === 'reject') {\n throw new MCPToolPinningError(\n `MCP tool '${name}' no longer matches its pinned definition fingerprint - the server changed the definition behind an approved name.`,\n { metadata: { server: serverIdentity.id, tool: name } },\n );\n }\n incrementCounter('mcp.tools.pin-mismatch.total', {\n server: serverIdentity.id,\n tool: name,\n });\n options.logger?.('warn', 'mcp.tools.pin-mismatch: pinned fingerprint diverged', {\n server: serverIdentity.id,\n tool: name,\n });\n }\n }\n }\n return adapted.tools;\n }\n\n async function close(): Promise<void> {\n try {\n await sdkClient.close();\n } catch (cause) {\n // Treat double-close as a no-op; surface other failures.\n if (cause instanceof Error && cause.message.toLowerCase().includes('already')) return;\n throw new MCPConnectionError('MCP transport could not be closed cleanly.', {\n metadata: { transport: options.transportConfig.kind, server: serverIdentity.id },\n cause,\n });\n }\n }\n\n const clientApi: MCPClient = Object.freeze({\n id: serverIdentity.id,\n serverInfo,\n serverIdentity,\n collisionStrategy,\n ...(options.priority === undefined ? {} : { priority: options.priority }),\n sessionIdPresent,\n resumable,\n listTools,\n listResources,\n listPrompts,\n callTool,\n readResource,\n readResourceContents,\n getPrompt,\n toTools,\n close,\n });\n return clientApi;\n}\n\n/**\n * Resolve the live {@link TransportAuthSource} for the outbound\n * `Authorization` header from the mutually-exclusive `authProvider` /\n * `bearerToken` options. `authProvider.resolveHeader()` already returns\n * the full header value (`Bearer …`); a static `bearerToken` is wrapped\n * into a constant `Bearer`-prefixed resolver. Returns `undefined` when\n * neither is supplied (no header injection).\n */\nfunction resolveTransportAuth(options: CreateMCPClientOptions): TransportAuthSource | undefined {\n const provider = options.authProvider;\n if (provider !== undefined) {\n return { resolveHeader: () => provider.resolveHeader() };\n }\n if (options.bearerToken !== undefined) {\n const token = options.bearerToken;\n const header = /^bearer\\s/i.test(token) ? token : `Bearer ${token}`;\n return { resolveHeader: () => header };\n }\n return undefined;\n}\n\nfunction mapSdkError(cause: unknown, ctx: { readonly tool?: string }): Error {\n const metadata = ctx.tool === undefined ? {} : { tool: ctx.tool };\n if (cause instanceof Error) {\n const name = cause.name;\n const message = cause.message ?? '';\n if (name === 'AbortError' || /aborted|cancell/i.test(message)) {\n return new MCPCancelledError('MCP request was cancelled.', { metadata, cause });\n }\n // MC-3: the SDK reports request-timeout as a plain McpError - map it\n // onto the advertised typed class instead of MCPProtocolError.\n if (/request timed out|timed out/i.test(message)) {\n return new MCPCallTimeoutError(\n `MCP request timed out${ctx.tool ? ` (tool: ${ctx.tool})` : ''}.`,\n {\n metadata,\n cause,\n },\n );\n }\n if (/unknown\\s+tool|tool\\s+not\\s+found|method\\s+not\\s+found/i.test(message)) {\n return new MCPToolNotFoundError(`MCP tool not found${ctx.tool ? `: ${ctx.tool}` : ''}.`, {\n metadata,\n cause,\n });\n }\n return new MCPProtocolError(message.length === 0 ? cause.toString() : message, {\n metadata,\n cause,\n });\n }\n return new MCPProtocolError(`MCP request failed: ${String(cause)}`, { metadata, cause });\n}\n\nfunction isStreamableHttp(transport: unknown): transport is StreamableHTTPClientTransport {\n return (\n typeof transport === 'object' &&\n transport !== null &&\n transport.constructor !== undefined &&\n transport.constructor.name === 'StreamableHTTPClientTransport'\n );\n}\n\nexport type { ServerIdentity };\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { applyInboundSanitization } from "@graphorin/tools/inbound";
|
|
|
4
4
|
//#region src/client/inbound-filters.ts
|
|
5
5
|
/**
|
|
6
6
|
* Process-scoped dedup keys for the `pass-through` override WARN. The
|
|
7
|
-
* spec mandates exactly-one WARN per server identity per process
|
|
7
|
+
* spec mandates exactly-one WARN per server identity per process - the
|
|
8
8
|
* Set retains the keys for the lifetime of the process. Tests reset via
|
|
9
9
|
* {@link import('./to-tools.js')._resetMcpAdapterDedupForTesting}.
|
|
10
10
|
*/
|
|
@@ -50,14 +50,19 @@ function warnOnPassthroughOverride(args) {
|
|
|
50
50
|
*/
|
|
51
51
|
function sanitizeDescription(args) {
|
|
52
52
|
const stripPolicy = args.inboundSanitization === "pass-through" ? "pass-through" : "detect-and-strip";
|
|
53
|
-
|
|
53
|
+
const outcome = applyInboundSanitization({
|
|
54
54
|
body: args.description,
|
|
55
55
|
policy: stripPolicy,
|
|
56
56
|
trustClass: "mcp-derived",
|
|
57
57
|
toolName: args.toolName,
|
|
58
58
|
contentOrigin: `mcp:tool-description:${args.serverIdentity.id}`,
|
|
59
59
|
failClosed: false
|
|
60
|
-
})
|
|
60
|
+
});
|
|
61
|
+
if (outcome.patternsHit.length > 0) incrementCounter("mcp.tool-description.injection-flagged.total", {
|
|
62
|
+
server: args.serverIdentity.id,
|
|
63
|
+
tool: args.toolName
|
|
64
|
+
});
|
|
65
|
+
return outcome.body;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbound-filters.js","names":["stripPolicy: InboundSanitizationPolicy"],"sources":["../../src/client/inbound-filters.ts"],"sourcesContent":["/**\n * Inbound prompt-injection filtering for the `toTools()` adapter\n * (extracted from `to-tools.ts` per F-MCP-001).\n *\n * Owns: the per-server inbound-sanitization default, the once-per-server\n * `pass-through` override WARN, and the tool-description strip applied at\n * registration time. The trust class is pinned to `'mcp-derived'` for\n * every produced tool so the agent runtime's per-step preamble fires\n * regardless of the body-level policy chosen here.\n *\n * @packageDocumentation\n */\n\nimport type { InboundSanitizationPolicy } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { applyInboundSanitization } from '@graphorin/tools/inbound';\nimport type { ServerIdentity } from '../transport/types.js';\n\n/** Operator-supplied structured logger (mirrors the client logger shape). */\ntype AdapterLogger = (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n) => void;\n\n/**\n * Process-scoped dedup keys for the `pass-through` override WARN. The\n * spec mandates exactly-one WARN per server identity per process
|
|
1
|
+
{"version":3,"file":"inbound-filters.js","names":["stripPolicy: InboundSanitizationPolicy"],"sources":["../../src/client/inbound-filters.ts"],"sourcesContent":["/**\n * Inbound prompt-injection filtering for the `toTools()` adapter\n * (extracted from `to-tools.ts` per F-MCP-001).\n *\n * Owns: the per-server inbound-sanitization default, the once-per-server\n * `pass-through` override WARN, and the tool-description strip applied at\n * registration time. The trust class is pinned to `'mcp-derived'` for\n * every produced tool so the agent runtime's per-step preamble fires\n * regardless of the body-level policy chosen here.\n *\n * @packageDocumentation\n */\n\nimport type { InboundSanitizationPolicy } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { applyInboundSanitization } from '@graphorin/tools/inbound';\nimport type { ServerIdentity } from '../transport/types.js';\n\n/** Operator-supplied structured logger (mirrors the client logger shape). */\ntype AdapterLogger = (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n) => void;\n\n/**\n * Process-scoped dedup keys for the `pass-through` override WARN. The\n * spec mandates exactly-one WARN per server identity per process - the\n * Set retains the keys for the lifetime of the process. Tests reset via\n * {@link import('./to-tools.js')._resetMcpAdapterDedupForTesting}.\n */\nconst passthroughWarnSeen = new Set<string>();\n\n/**\n * Reset the inbound-filter dedup set. Used by\n * {@link import('./to-tools.js')._resetMcpAdapterDedupForTesting}.\n *\n * @experimental\n */\nexport function _resetInboundFiltersDedupForTesting(): void {\n passthroughWarnSeen.clear();\n}\n\n/**\n * Resolve the effective per-server inbound-sanitization policy. MCP\n * tools default to the strictest body-level policy.\n */\nexport function resolveInboundPolicy(\n override: InboundSanitizationPolicy | undefined,\n): InboundSanitizationPolicy {\n return override ?? 'detect-and-strip-and-wrap';\n}\n\n/**\n * WARN-once per server when the operator opts out of body-level\n * sanitization. The trust class stays `'mcp-derived'` regardless so the\n * per-step preamble in the agent runtime still fires; the WARN exists so\n * the audit log records the operator's deliberate choice.\n */\nexport function warnOnPassthroughOverride(args: {\n readonly resolvedInbound: InboundSanitizationPolicy;\n readonly serverIdentity: ServerIdentity;\n readonly logger?: AdapterLogger;\n}): void {\n if (args.resolvedInbound !== 'pass-through') return;\n if (passthroughWarnSeen.has(args.serverIdentity.id)) return;\n passthroughWarnSeen.add(args.serverIdentity.id);\n incrementCounter('mcp.inbound.sanitization.passthrough-override.warn.total', {\n server: args.serverIdentity.id,\n });\n if (args.logger !== undefined) {\n args.logger('warn', 'mcp.inbound.sanitization.passthrough-override', {\n server: args.serverIdentity.id,\n policy: 'pass-through',\n note: \"Body-level prompt-injection sanitization is disabled for this MCP server; the trust class remains 'mcp-derived' so the per-step preamble still fires. The WARN cannot be silenced (deliberate operator-friction).\",\n });\n }\n}\n\n/**\n * Strip imperative payloads from a tool description before it enters the\n * per-step tool catalogue. The description is never wrapped in the\n * `<<<untrusted_content>>>` envelope (the wrap is reserved for tool\n * result bodies emitted into the conversation history).\n */\nexport function sanitizeDescription(args: {\n readonly description: string;\n readonly inboundSanitization: InboundSanitizationPolicy;\n readonly toolName: string;\n readonly serverIdentity: ServerIdentity;\n}): string {\n const stripPolicy: InboundSanitizationPolicy =\n args.inboundSanitization === 'pass-through' ? 'pass-through' : 'detect-and-strip';\n const outcome = applyInboundSanitization({\n body: args.description,\n policy: stripPolicy,\n trustClass: 'mcp-derived',\n toolName: args.toolName,\n contentOrigin: `mcp:tool-description:${args.serverIdentity.id}`,\n failClosed: false,\n });\n // C6: tool-description poisoning is a registration-time SIGNAL, not\n // just a silent strip - count it so operators see which server ships\n // imperative-laden descriptions (Invariant Labs tool-poisoning class).\n if (outcome.patternsHit.length > 0) {\n incrementCounter('mcp.tool-description.injection-flagged.total', {\n server: args.serverIdentity.id,\n tool: args.toolName,\n });\n }\n return outcome.body;\n}\n"],"mappings":";;;;;;;;;;AA+BA,MAAM,sCAAsB,IAAI,KAAa;;;;;;;AAQ7C,SAAgB,sCAA4C;AAC1D,qBAAoB,OAAO;;;;;;AAO7B,SAAgB,qBACd,UAC2B;AAC3B,QAAO,YAAY;;;;;;;;AASrB,SAAgB,0BAA0B,MAIjC;AACP,KAAI,KAAK,oBAAoB,eAAgB;AAC7C,KAAI,oBAAoB,IAAI,KAAK,eAAe,GAAG,CAAE;AACrD,qBAAoB,IAAI,KAAK,eAAe,GAAG;AAC/C,kBAAiB,4DAA4D,EAC3E,QAAQ,KAAK,eAAe,IAC7B,CAAC;AACF,KAAI,KAAK,WAAW,OAClB,MAAK,OAAO,QAAQ,iDAAiD;EACnE,QAAQ,KAAK,eAAe;EAC5B,QAAQ;EACR,MAAM;EACP,CAAC;;;;;;;;AAUN,SAAgB,oBAAoB,MAKzB;CACT,MAAMA,cACJ,KAAK,wBAAwB,iBAAiB,iBAAiB;CACjE,MAAM,UAAU,yBAAyB;EACvC,MAAM,KAAK;EACX,QAAQ;EACR,YAAY;EACZ,UAAU,KAAK;EACf,eAAe,wBAAwB,KAAK,eAAe;EAC3D,YAAY;EACb,CAAC;AAIF,KAAI,QAAQ,YAAY,SAAS,EAC/B,kBAAiB,gDAAgD;EAC/D,QAAQ,KAAK,eAAe;EAC5B,MAAM,KAAK;EACZ,CAAC;AAEJ,QAAO,QAAQ"}
|
|
@@ -9,6 +9,16 @@ interface McpResourceReaderOptions {
|
|
|
9
9
|
readonly clients: ReadonlyArray<MCPClient>;
|
|
10
10
|
/** Default `maxBytes` when `read(...)` is called without one. Default `65536`. */
|
|
11
11
|
readonly defaultMaxBytes?: number;
|
|
12
|
+
/**
|
|
13
|
+
* mcp-skills-06: allow a BARE (unscoped) resource URI to be tried
|
|
14
|
+
* against every configured client. Default `false` - handles minted
|
|
15
|
+
* by the adapter are scoped (`mcp:<serverId>:<uri>`) and resolve
|
|
16
|
+
* ONLY against their originating server, so a malicious server's
|
|
17
|
+
* link (or a prompt-injected model) cannot fetch a resource from a
|
|
18
|
+
* different, more-trusted server (the cross-server confused-deputy
|
|
19
|
+
* hop). Enable only when you accept that risk for legacy handles.
|
|
20
|
+
*/
|
|
21
|
+
readonly allowCrossServer?: boolean;
|
|
12
22
|
}
|
|
13
23
|
/**
|
|
14
24
|
* Build a {@link ResultReader} that resolves MCP resource URIs through
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-resource-reader.d.ts","names":[],"sources":["../../src/client/mcp-resource-reader.ts"],"sourcesContent":[],"mappings":";;;;;;UA4BiB,wBAAA;;oBAEG,cAAc
|
|
1
|
+
{"version":3,"file":"mcp-resource-reader.d.ts","names":[],"sources":["../../src/client/mcp-resource-reader.ts"],"sourcesContent":[],"mappings":";;;;;;UA4BiB,wBAAA;;oBAEG,cAAc;;;;;;;;;;;;;;;;;;;;iBAwBlB,uBAAA,OAA8B,2BAA2B"}
|
|
@@ -2,14 +2,14 @@ import { incrementCounter } from "@graphorin/tools/audit";
|
|
|
2
2
|
|
|
3
3
|
//#region src/client/mcp-resource-reader.ts
|
|
4
4
|
/**
|
|
5
|
-
* {@link createMcpResourceReader}
|
|
5
|
+
* {@link createMcpResourceReader} - resolve MCP `resource_link` handles
|
|
6
6
|
* on demand (WI-13 / P2-2, ties to WI-10 result handles).
|
|
7
7
|
*
|
|
8
8
|
* The `toTools()` adapter surfaces an MCP `resource_link` as a preview +
|
|
9
9
|
* the resource `uri` (the {@link import('@graphorin/tools/result').ResultReader}
|
|
10
10
|
* handle) instead of inlining the body. This reader resolves that handle
|
|
11
11
|
* via {@link MCPClient.readResource} when the model later calls the
|
|
12
|
-
* built-in `read_result` tool
|
|
12
|
+
* built-in `read_result` tool - so a large MCP resource never inflates
|
|
13
13
|
* context until the model actually asks for it.
|
|
14
14
|
*
|
|
15
15
|
* Compose it with the agent's spill-file reader (the agent tries each
|
|
@@ -23,6 +23,8 @@ import { incrementCounter } from "@graphorin/tools/audit";
|
|
|
23
23
|
*
|
|
24
24
|
* @packageDocumentation
|
|
25
25
|
*/
|
|
26
|
+
/** Scoped-handle grammar minted by the tool adapter: `mcp:<serverId>:<uri>`. */
|
|
27
|
+
const SCOPED_HANDLE = /^mcp:([^:]+):(.+)$/;
|
|
26
28
|
/**
|
|
27
29
|
* Build a {@link ResultReader} that resolves MCP resource URIs through
|
|
28
30
|
* one or more connected {@link MCPClient}s.
|
|
@@ -32,10 +34,19 @@ import { incrementCounter } from "@graphorin/tools/audit";
|
|
|
32
34
|
function createMcpResourceReader(opts) {
|
|
33
35
|
const clients = [...opts.clients];
|
|
34
36
|
const defaultMaxBytes = opts.defaultMaxBytes ?? 65536;
|
|
35
|
-
return { async read(
|
|
37
|
+
return { async read(handle, range) {
|
|
36
38
|
if (clients.length === 0) throw new Error("createMcpResourceReader: no MCP clients configured to resolve resource handles.");
|
|
39
|
+
const scoped = SCOPED_HANDLE.exec(handle);
|
|
40
|
+
let uri = handle;
|
|
41
|
+
let candidates = clients;
|
|
42
|
+
if (scoped !== null) {
|
|
43
|
+
const serverId = scoped[1] ?? "";
|
|
44
|
+
uri = scoped[2] ?? "";
|
|
45
|
+
candidates = clients.filter((c) => c.serverIdentity.id === serverId);
|
|
46
|
+
if (candidates.length === 0) throw new Error(`createMcpResourceReader: no configured client matches the handle's server '${serverId}' - a handle only resolves against its originating server.`);
|
|
47
|
+
} else if (opts.allowCrossServer !== true) throw new Error(`createMcpResourceReader: refusing to resolve the unscoped resource URI ${JSON.stringify(handle)} against every configured server (cross-server confused-deputy risk). Use the scoped handle from the tool result ('mcp:<serverId>:<uri>'), or opt in with allowCrossServer: true.`);
|
|
37
48
|
let lastError;
|
|
38
|
-
for (const client of
|
|
49
|
+
for (const client of candidates) {
|
|
39
50
|
let content;
|
|
40
51
|
try {
|
|
41
52
|
content = await client.readResource(uri);
|
|
@@ -52,7 +63,7 @@ function createMcpResourceReader(opts) {
|
|
|
52
63
|
}
|
|
53
64
|
/**
|
|
54
65
|
* Raw resource bytes (MC-10): text resources as UTF-8, blob resources
|
|
55
|
-
* DECODED from base64
|
|
66
|
+
* DECODED from base64 - slicing/totalBytes operate on real payload
|
|
56
67
|
* bytes, never on the ~33%-inflated base64 string (whose arbitrary
|
|
57
68
|
* cuts also break base64 quads).
|
|
58
69
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-resource-reader.js","names":["lastError: unknown","content: MCPResourceContent","end"],"sources":["../../src/client/mcp-resource-reader.ts"],"sourcesContent":["/**\n * {@link createMcpResourceReader} — resolve MCP `resource_link` handles\n * on demand (WI-13 / P2-2, ties to WI-10 result handles).\n *\n * The `toTools()` adapter surfaces an MCP `resource_link` as a preview +\n * the resource `uri` (the {@link import('@graphorin/tools/result').ResultReader}\n * handle) instead of inlining the body. This reader resolves that handle\n * via {@link MCPClient.readResource} when the model later calls the\n * built-in `read_result` tool — so a large MCP resource never inflates\n * context until the model actually asks for it.\n *\n * Compose it with the agent's spill-file reader (the agent tries each\n * configured reader in order): the spill reader claims\n * `graphorin-spill:` handles, this reader resolves the rest. With more\n * than one MCP client, the reader tries each until one server resolves\n * the URI.\n *\n * Network note (R4): this reader performs no I/O until `read(...)` is\n * called, and then only over a connection the operator already opened.\n *\n * @packageDocumentation\n */\n\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { ResultReader, ResultReadOutcome, ResultReadRange } from '@graphorin/tools/result';\nimport type { MCPClient, MCPResourceContent } from './types.js';\n\n/** Configuration for {@link createMcpResourceReader}. */\nexport interface McpResourceReaderOptions {\n /** Clients consulted (in order) to resolve a resource URI. */\n readonly clients: ReadonlyArray<MCPClient>;\n /** Default `maxBytes` when `read(...)` is called without one. Default `65536`. */\n readonly defaultMaxBytes?: number;\n}\n\n/**\n * Build a {@link ResultReader} that resolves MCP resource URIs through\n * one or more connected {@link MCPClient}s.\n *\n * @stable\n */\nexport function createMcpResourceReader(opts: McpResourceReaderOptions): ResultReader {\n const clients = [...opts.clients];\n const defaultMaxBytes = opts.defaultMaxBytes ?? 65_536;\n return {\n async read(uri, range): Promise<ResultReadOutcome> {\n if (clients.length === 0) {\n throw new Error(\n 'createMcpResourceReader: no MCP clients configured to resolve resource handles.',\n );\n }\n let lastError: unknown;\n for (const client of clients) {\n let content: MCPResourceContent;\n try {\n content = await client.readResource(uri);\n } catch (err) {\n lastError = err;\n continue;\n }\n incrementCounter('mcp.resource-link.resolved.total', {\n server: client.serverIdentity.id,\n });\n return sliceResource(content, range, defaultMaxBytes);\n }\n const suffix = lastError instanceof Error ? ` Last error: ${lastError.message}` : '';\n throw new Error(\n `createMcpResourceReader: no configured MCP server resolved resource ${JSON.stringify(uri)}.${suffix}`,\n );\n },\n };\n}\n\n/**\n * Raw resource bytes (MC-10): text resources as UTF-8, blob resources\n * DECODED from base64 — slicing/totalBytes operate on real payload\n * bytes, never on the ~33%-inflated base64 string (whose arbitrary\n * cuts also break base64 quads).\n */\nfunction resourceBytes(content: MCPResourceContent): Buffer {\n if (content.text !== undefined) return Buffer.from(content.text, 'utf8');\n if (content.blob !== undefined) return Buffer.from(content.blob, 'base64');\n return Buffer.alloc(0);\n}\n\n/**\n * Apply a {@link ResultReadRange} to the fully-fetched resource body,\n * mirroring the spill-file reader's slicing semantics (line mode wins;\n * `maxBytes` caps the returned slice) so `read_result` pages uniformly.\n */\nfunction sliceResource(\n content: MCPResourceContent,\n range: ResultReadRange | undefined,\n defaultMaxBytes: number,\n): ResultReadOutcome {\n const buf = resourceBytes(content);\n const full = buf.toString('utf8');\n const totalBytes = buf.byteLength;\n const cap = Math.max(0, range?.maxBytes ?? defaultMaxBytes);\n\n if (range?.startLine !== undefined || range?.endLine !== undefined) {\n const lines = full.split('\\n');\n const start = Math.max(1, range.startLine ?? 1);\n const end = Math.min(lines.length, range.endLine ?? lines.length);\n const selected = start <= end ? lines.slice(start - 1, end).join('\\n') : '';\n const capped = Buffer.byteLength(selected, 'utf8') > cap;\n const out = capBytes(selected, cap);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition — the\n // executor re-applies inbound sanitization + dataflow provenance\n // by this class when read_result relays it.\n producerTrustClass: 'mcp-derived' as const,\n content: out,\n bytes: Buffer.byteLength(out, 'utf8'),\n totalBytes,\n eof: end >= lines.length && !capped,\n });\n }\n\n const offset = clamp(range?.offset ?? 0, 0, totalBytes);\n const requested = range?.length ?? totalBytes - offset;\n const rawEnd = clamp(offset + Math.max(0, requested), offset, totalBytes);\n const end = Math.min(rawEnd, offset + cap);\n const slice = buf.subarray(offset, end);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition.\n producerTrustClass: 'mcp-derived' as const,\n content: slice.toString('utf8'),\n bytes: slice.byteLength,\n totalBytes,\n eof: end >= totalBytes,\n });\n}\n\nfunction clamp(value: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, value));\n}\n\n/** Truncate `s` to at most `maxBytes` UTF-8 bytes (tolerating a split trailing char). */\nfunction capBytes(s: string, maxBytes: number): string {\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n return Buffer.from(s, 'utf8').subarray(0, maxBytes).toString('utf8');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,wBAAwB,MAA8C;CACpF,MAAM,UAAU,CAAC,GAAG,KAAK,QAAQ;CACjC,MAAM,kBAAkB,KAAK,mBAAmB;AAChD,QAAO,EACL,MAAM,KAAK,KAAK,OAAmC;AACjD,MAAI,QAAQ,WAAW,EACrB,OAAM,IAAI,MACR,kFACD;EAEH,IAAIA;AACJ,OAAK,MAAM,UAAU,SAAS;GAC5B,IAAIC;AACJ,OAAI;AACF,cAAU,MAAM,OAAO,aAAa,IAAI;YACjC,KAAK;AACZ,gBAAY;AACZ;;AAEF,oBAAiB,oCAAoC,EACnD,QAAQ,OAAO,eAAe,IAC/B,CAAC;AACF,UAAO,cAAc,SAAS,OAAO,gBAAgB;;EAEvD,MAAM,SAAS,qBAAqB,QAAQ,gBAAgB,UAAU,YAAY;AAClF,QAAM,IAAI,MACR,uEAAuE,KAAK,UAAU,IAAI,CAAC,GAAG,SAC/F;IAEJ;;;;;;;;AASH,SAAS,cAAc,SAAqC;AAC1D,KAAI,QAAQ,SAAS,OAAW,QAAO,OAAO,KAAK,QAAQ,MAAM,OAAO;AACxE,KAAI,QAAQ,SAAS,OAAW,QAAO,OAAO,KAAK,QAAQ,MAAM,SAAS;AAC1E,QAAO,OAAO,MAAM,EAAE;;;;;;;AAQxB,SAAS,cACP,SACA,OACA,iBACmB;CACnB,MAAM,MAAM,cAAc,QAAQ;CAClC,MAAM,OAAO,IAAI,SAAS,OAAO;CACjC,MAAM,aAAa,IAAI;CACvB,MAAM,MAAM,KAAK,IAAI,GAAG,OAAO,YAAY,gBAAgB;AAE3D,KAAI,OAAO,cAAc,UAAa,OAAO,YAAY,QAAW;EAClE,MAAM,QAAQ,KAAK,MAAM,KAAK;EAC9B,MAAM,QAAQ,KAAK,IAAI,GAAG,MAAM,aAAa,EAAE;EAC/C,MAAMC,QAAM,KAAK,IAAI,MAAM,QAAQ,MAAM,WAAW,MAAM,OAAO;EACjE,MAAM,WAAW,SAASA,QAAM,MAAM,MAAM,QAAQ,GAAGA,MAAI,CAAC,KAAK,KAAK,GAAG;EACzE,MAAM,SAAS,OAAO,WAAW,UAAU,OAAO,GAAG;EACrD,MAAM,MAAM,SAAS,UAAU,IAAI;AACnC,SAAO,OAAO,OAAO;GAInB,oBAAoB;GACpB,SAAS;GACT,OAAO,OAAO,WAAW,KAAK,OAAO;GACrC;GACA,KAAKA,SAAO,MAAM,UAAU,CAAC;GAC9B,CAAC;;CAGJ,MAAM,SAAS,MAAM,OAAO,UAAU,GAAG,GAAG,WAAW;CACvD,MAAM,YAAY,OAAO,UAAU,aAAa;CAChD,MAAM,SAAS,MAAM,SAAS,KAAK,IAAI,GAAG,UAAU,EAAE,QAAQ,WAAW;CACzE,MAAM,MAAM,KAAK,IAAI,QAAQ,SAAS,IAAI;CAC1C,MAAM,QAAQ,IAAI,SAAS,QAAQ,IAAI;AACvC,QAAO,OAAO,OAAO;EAEnB,oBAAoB;EACpB,SAAS,MAAM,SAAS,OAAO;EAC/B,OAAO,MAAM;EACb;EACA,KAAK,OAAO;EACb,CAAC;;AAGJ,SAAS,MAAM,OAAe,IAAY,IAAoB;AAC5D,QAAO,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC;;;AAI1C,SAAS,SAAS,GAAW,UAA0B;AACrD,KAAI,OAAO,WAAW,GAAG,OAAO,IAAI,SAAU,QAAO;AACrD,QAAO,OAAO,KAAK,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,SAAS,OAAO"}
|
|
1
|
+
{"version":3,"file":"mcp-resource-reader.js","names":["lastError: unknown","content: MCPResourceContent","end"],"sources":["../../src/client/mcp-resource-reader.ts"],"sourcesContent":["/**\n * {@link createMcpResourceReader} - resolve MCP `resource_link` handles\n * on demand (WI-13 / P2-2, ties to WI-10 result handles).\n *\n * The `toTools()` adapter surfaces an MCP `resource_link` as a preview +\n * the resource `uri` (the {@link import('@graphorin/tools/result').ResultReader}\n * handle) instead of inlining the body. This reader resolves that handle\n * via {@link MCPClient.readResource} when the model later calls the\n * built-in `read_result` tool - so a large MCP resource never inflates\n * context until the model actually asks for it.\n *\n * Compose it with the agent's spill-file reader (the agent tries each\n * configured reader in order): the spill reader claims\n * `graphorin-spill:` handles, this reader resolves the rest. With more\n * than one MCP client, the reader tries each until one server resolves\n * the URI.\n *\n * Network note (R4): this reader performs no I/O until `read(...)` is\n * called, and then only over a connection the operator already opened.\n *\n * @packageDocumentation\n */\n\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { ResultReader, ResultReadOutcome, ResultReadRange } from '@graphorin/tools/result';\nimport type { MCPClient, MCPResourceContent } from './types.js';\n\n/** Configuration for {@link createMcpResourceReader}. */\nexport interface McpResourceReaderOptions {\n /** Clients consulted (in order) to resolve a resource URI. */\n readonly clients: ReadonlyArray<MCPClient>;\n /** Default `maxBytes` when `read(...)` is called without one. Default `65536`. */\n readonly defaultMaxBytes?: number;\n /**\n * mcp-skills-06: allow a BARE (unscoped) resource URI to be tried\n * against every configured client. Default `false` - handles minted\n * by the adapter are scoped (`mcp:<serverId>:<uri>`) and resolve\n * ONLY against their originating server, so a malicious server's\n * link (or a prompt-injected model) cannot fetch a resource from a\n * different, more-trusted server (the cross-server confused-deputy\n * hop). Enable only when you accept that risk for legacy handles.\n */\n readonly allowCrossServer?: boolean;\n}\n\n/** Scoped-handle grammar minted by the tool adapter: `mcp:<serverId>:<uri>`. */\nconst SCOPED_HANDLE = /^mcp:([^:]+):(.+)$/;\n\n/**\n * Build a {@link ResultReader} that resolves MCP resource URIs through\n * one or more connected {@link MCPClient}s.\n *\n * @stable\n */\nexport function createMcpResourceReader(opts: McpResourceReaderOptions): ResultReader {\n const clients = [...opts.clients];\n const defaultMaxBytes = opts.defaultMaxBytes ?? 65_536;\n return {\n async read(handle, range): Promise<ResultReadOutcome> {\n if (clients.length === 0) {\n throw new Error(\n 'createMcpResourceReader: no MCP clients configured to resolve resource handles.',\n );\n }\n // mcp-skills-06: a scoped handle (`mcp:<serverId>:<uri>`) resolves\n // ONLY against its originating server. The try-every-client loop\n // survives solely for bare URIs behind the explicit\n // `allowCrossServer` opt-in.\n const scoped = SCOPED_HANDLE.exec(handle);\n let uri = handle;\n let candidates = clients;\n if (scoped !== null) {\n const serverId = scoped[1] ?? '';\n uri = scoped[2] ?? '';\n candidates = clients.filter((c) => c.serverIdentity.id === serverId);\n if (candidates.length === 0) {\n throw new Error(\n `createMcpResourceReader: no configured client matches the handle's server ` +\n `'${serverId}' - a handle only resolves against its originating server.`,\n );\n }\n } else if (opts.allowCrossServer !== true) {\n throw new Error(\n `createMcpResourceReader: refusing to resolve the unscoped resource URI ` +\n `${JSON.stringify(handle)} against every configured server (cross-server ` +\n `confused-deputy risk). Use the scoped handle from the tool result ` +\n `('mcp:<serverId>:<uri>'), or opt in with allowCrossServer: true.`,\n );\n }\n let lastError: unknown;\n for (const client of candidates) {\n let content: MCPResourceContent;\n try {\n content = await client.readResource(uri);\n } catch (err) {\n lastError = err;\n continue;\n }\n incrementCounter('mcp.resource-link.resolved.total', {\n server: client.serverIdentity.id,\n });\n return sliceResource(content, range, defaultMaxBytes);\n }\n const suffix = lastError instanceof Error ? ` Last error: ${lastError.message}` : '';\n throw new Error(\n `createMcpResourceReader: no configured MCP server resolved resource ${JSON.stringify(uri)}.${suffix}`,\n );\n },\n };\n}\n\n/**\n * Raw resource bytes (MC-10): text resources as UTF-8, blob resources\n * DECODED from base64 - slicing/totalBytes operate on real payload\n * bytes, never on the ~33%-inflated base64 string (whose arbitrary\n * cuts also break base64 quads).\n */\nfunction resourceBytes(content: MCPResourceContent): Buffer {\n if (content.text !== undefined) return Buffer.from(content.text, 'utf8');\n if (content.blob !== undefined) return Buffer.from(content.blob, 'base64');\n return Buffer.alloc(0);\n}\n\n/**\n * Apply a {@link ResultReadRange} to the fully-fetched resource body,\n * mirroring the spill-file reader's slicing semantics (line mode wins;\n * `maxBytes` caps the returned slice) so `read_result` pages uniformly.\n */\nfunction sliceResource(\n content: MCPResourceContent,\n range: ResultReadRange | undefined,\n defaultMaxBytes: number,\n): ResultReadOutcome {\n const buf = resourceBytes(content);\n const full = buf.toString('utf8');\n const totalBytes = buf.byteLength;\n const cap = Math.max(0, range?.maxBytes ?? defaultMaxBytes);\n\n if (range?.startLine !== undefined || range?.endLine !== undefined) {\n const lines = full.split('\\n');\n const start = Math.max(1, range.startLine ?? 1);\n const end = Math.min(lines.length, range.endLine ?? lines.length);\n const selected = start <= end ? lines.slice(start - 1, end).join('\\n') : '';\n const capped = Buffer.byteLength(selected, 'utf8') > cap;\n const out = capBytes(selected, cap);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition - the\n // executor re-applies inbound sanitization + dataflow provenance\n // by this class when read_result relays it.\n producerTrustClass: 'mcp-derived' as const,\n content: out,\n bytes: Buffer.byteLength(out, 'utf8'),\n totalBytes,\n eof: end >= lines.length && !capped,\n });\n }\n\n const offset = clamp(range?.offset ?? 0, 0, totalBytes);\n const requested = range?.length ?? totalBytes - offset;\n const rawEnd = clamp(offset + Math.max(0, requested), offset, totalBytes);\n const end = Math.min(rawEnd, offset + cap);\n const slice = buf.subarray(offset, end);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition.\n producerTrustClass: 'mcp-derived' as const,\n content: slice.toString('utf8'),\n bytes: slice.byteLength,\n totalBytes,\n eof: end >= totalBytes,\n });\n}\n\nfunction clamp(value: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, value));\n}\n\n/** Truncate `s` to at most `maxBytes` UTF-8 bytes (tolerating a split trailing char). */\nfunction capBytes(s: string, maxBytes: number): string {\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n return Buffer.from(s, 'utf8').subarray(0, maxBytes).toString('utf8');\n}\n"],"mappings":"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"}
|
package/dist/client/pinning.js
CHANGED
|
@@ -3,8 +3,8 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
//#region src/client/pinning.ts
|
|
4
4
|
/**
|
|
5
5
|
* Tool-definition pinning (MC-6): stable fingerprints over MCP tool
|
|
6
|
-
* definitions so approve-then-swap rug-pulls
|
|
7
|
-
* tool's description/schema behind an already-approved name
|
|
6
|
+
* definitions so approve-then-swap rug-pulls - a server changing a
|
|
7
|
+
* tool's description/schema behind an already-approved name - are
|
|
8
8
|
* detectable across snapshots and process restarts.
|
|
9
9
|
*
|
|
10
10
|
* The fingerprint is a sha256 over a key-sorted canonical render of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinning.js","names":[],"sources":["../../src/client/pinning.ts"],"sourcesContent":["/**\n * Tool-definition pinning (MC-6): stable fingerprints over MCP tool\n * definitions so approve-then-swap rug-pulls
|
|
1
|
+
{"version":3,"file":"pinning.js","names":[],"sources":["../../src/client/pinning.ts"],"sourcesContent":["/**\n * Tool-definition pinning (MC-6): stable fingerprints over MCP tool\n * definitions so approve-then-swap rug-pulls - a server changing a\n * tool's description/schema behind an already-approved name - are\n * detectable across snapshots and process restarts.\n *\n * The fingerprint is a sha256 over a key-sorted canonical render of\n * `{ name, description, inputSchema, outputSchema, title }`. Operators\n * persist the stamped `__definitionHash` (or read it from the audit\n * trail) and pass it back via `toTools({ pinnedFingerprints })`.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { MCPToolDefinition } from './types.js';\n\n/** Deterministic JSON render: object keys sorted recursively. */\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map((v) => stableStringify(v)).join(',')}]`;\n }\n if (typeof value === 'object' && value !== null) {\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);\n return `{${entries.join(',')}}`;\n }\n return JSON.stringify(value) ?? 'null';\n}\n\n/**\n * Stable sha256 fingerprint of one MCP tool definition (MC-6).\n *\n * @stable\n */\nexport function computeToolDefinitionHash(def: MCPToolDefinition): string {\n const canonical = stableStringify({\n name: def.name,\n description: def.description,\n inputSchema: def.inputSchema,\n ...(def.outputSchema !== undefined ? { outputSchema: def.outputSchema } : {}),\n ...(def.title !== undefined ? { title: def.title } : {}),\n });\n return createHash('sha256').update(canonical).digest('hex');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,SAAS,gBAAgB,OAAwB;AAC/C,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,IAAI,MAAM,KAAK,MAAM,gBAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;AAE5D,KAAI,OAAO,UAAU,YAAY,UAAU,KAKzC,QAAO,IAJS,OAAO,QAAQ,MAAiC,CAC7D,QAAQ,GAAG,OAAO,MAAM,OAAU,CAClC,MAAM,CAAC,IAAI,CAAC,OAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,EAAG,CAChD,KAAK,CAAC,GAAG,OAAO,GAAG,KAAK,UAAU,EAAE,CAAC,GAAG,gBAAgB,EAAE,GAAG,CAC7C,KAAK,IAAI,CAAC;AAE/B,QAAO,KAAK,UAAU,MAAM,IAAI;;;;;;;AAQlC,SAAgB,0BAA0B,KAAgC;CACxE,MAAM,YAAY,gBAAgB;EAChC,MAAM,IAAI;EACV,aAAa,IAAI;EACjB,aAAa,IAAI;EACjB,GAAI,IAAI,iBAAiB,SAAY,EAAE,cAAc,IAAI,cAAc,GAAG,EAAE;EAC5E,GAAI,IAAI,UAAU,SAAY,EAAE,OAAO,IAAI,OAAO,GAAG,EAAE;EACxD,CAAC;AACF,QAAO,WAAW,SAAS,CAAC,OAAO,UAAU,CAAC,OAAO,MAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-tools.js","names":["tools: Tool[]"],"sources":["../../src/client/to-tools.ts"],"sourcesContent":["/**\n * `toTools()` adapter — bridges MCP tool descriptors into the\n * Graphorin tool registry.\n *\n * The adapter orchestrates three extracted concerns (F-MCP-001):\n *\n * 1. Filters / namespaces the `listTools()` output, then resolves the\n * per-server effective `defer_loading` flag — see\n * {@link import('./defer-loading.js').resolveDeferLoading}.\n * 2. Resolves the per-server inbound prompt-injection policy and strips\n * imperative payloads from each description — see\n * {@link import('./inbound-filters.js')}.\n * 3. Converts each MCP tool into a strongly-typed Graphorin `Tool` whose\n * `execute(...)` calls back into {@link MCPClient.callTool} and adapts\n * the result — see {@link import('./adapt-result.js').adaptCallResult}.\n *\n * The trust class is pinned to `'mcp-derived'` so the agent runtime's\n * per-step preamble fires regardless of the body-level policy.\n *\n * @packageDocumentation\n */\n\nimport type { ContentChunk, InboundSanitizationPolicy, Tool, ToolReturn } from '@graphorin/core';\nimport { buildJsonSchemaValidator, type JsonSchemaLike } from '../registry/json-schema.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport { adaptCallResult } from './adapt-result.js';\nimport {\n _resetDeferLoadingDedupForTesting,\n DEFAULT_DEFER_LOADING_THRESHOLD,\n resolveDeferLoading,\n} from './defer-loading.js';\nimport {\n _resetInboundFiltersDedupForTesting,\n resolveInboundPolicy,\n sanitizeDescription,\n warnOnPassthroughOverride,\n} from './inbound-filters.js';\nimport { computeToolDefinitionHash } from './pinning.js';\nimport type { MCPClient, MCPToolDefinition, MCPToToolsOptions } from './types.js';\n\n// Re-exported for backward compatibility: callers (and tests) import\n// these symbols directly from `./to-tools.js` and via the client barrel.\nexport { adaptCallResult } from './adapt-result.js';\nexport { DEFAULT_DEFER_LOADING_THRESHOLD } from './defer-loading.js';\n\n/**\n * Reset every process-scoped dedup set owned by the adapter modules.\n * Used by tests.\n *\n * @experimental\n */\nexport function _resetMcpAdapterDedupForTesting(): void {\n _resetDeferLoadingDedupForTesting();\n _resetInboundFiltersDedupForTesting();\n}\n\n/** Result returned by {@link adaptMCPTools}. */\nexport interface AdaptedToolsResult {\n readonly tools: ReadonlyArray<Tool>;\n /** MC-6: sha256 definition fingerprint per MCP tool name. */\n readonly fingerprints: ReadonlyMap<string, string>;\n readonly autoDeferralFired: boolean;\n readonly resolvedDeferLoading: boolean;\n readonly resolvedInboundSanitization: InboundSanitizationPolicy;\n readonly toolCount: number;\n readonly deferralThreshold: number;\n}\n\n/**\n * Build the {@link Tool} array for the supplied MCP tool catalogue.\n *\n * @stable\n */\nexport function adaptMCPTools(args: {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly catalogue: ReadonlyArray<MCPToolDefinition>;\n readonly options?: MCPToToolsOptions;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n}): AdaptedToolsResult {\n const opts = args.options ?? {};\n const fingerprints = new Map<string, string>();\n const filter = opts.filter;\n const namespace = (opts.namespace ?? '').trim();\n const filtered = filter === undefined ? args.catalogue : args.catalogue.filter((t) => filter(t));\n const total = filtered.length;\n const threshold = opts.deferLoadingThreshold ?? DEFAULT_DEFER_LOADING_THRESHOLD;\n\n const { autoDeferralFired, resolvedDeferLoading } = resolveDeferLoading({\n serverIdentity: args.serverIdentity,\n toolNames: filtered.map((t) => t.name),\n explicitDefer: opts.defer_loading,\n threshold,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const resolvedInbound = resolveInboundPolicy(opts.inboundSanitization);\n warnOnPassthroughOverride({\n resolvedInbound,\n serverIdentity: args.serverIdentity,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const tools: Tool[] = [];\n for (const definition of filtered) {\n const namespacedName =\n namespace.length === 0 ? definition.name : `${namespace}.${definition.name}`;\n const sideEffectClass = opts.sideEffectClassByTool?.[namespacedName] ?? 'external-stateful';\n const preferredModel = opts.preferredModelByTool?.[namespacedName];\n const inputValidator = buildJsonSchemaValidator(definition.inputSchema as JsonSchemaLike);\n const outputValidator =\n definition.outputSchema === undefined\n ? undefined\n : buildJsonSchemaValidator(definition.outputSchema as JsonSchemaLike);\n const definitionHash = computeToolDefinitionHash(definition);\n fingerprints.set(definition.name, definitionHash);\n tools.push(\n buildAdaptedTool({\n client: args.client,\n serverIdentity: args.serverIdentity,\n definitionHash,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n mcpToolName: definition.name,\n graphorinToolName: namespacedName,\n description:\n definition.description.length === 0 ? `${definition.name} (MCP)` : definition.description,\n inputSchema: inputValidator,\n outputSchema: outputValidator,\n defer_loading: resolvedDeferLoading,\n inboundSanitization: resolvedInbound,\n sideEffectClass,\n ...(opts.maxResultTokens === undefined ? {} : { maxResultTokens: opts.maxResultTokens }),\n ...(opts.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: opts.truncationStrategy }),\n ...(opts.callTimeoutMs === undefined ? {} : { callTimeoutMs: opts.callTimeoutMs }),\n ...(preferredModel === undefined ? {} : { preferredModel }),\n }),\n );\n }\n\n return Object.freeze({\n tools: Object.freeze(tools),\n fingerprints,\n autoDeferralFired,\n resolvedDeferLoading,\n resolvedInboundSanitization: resolvedInbound,\n toolCount: total,\n deferralThreshold: threshold,\n });\n}\n\ninterface BuildAdaptedToolArgs {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly mcpToolName: string;\n readonly graphorinToolName: string;\n readonly description: string;\n readonly inputSchema: import('@graphorin/core').ZodLikeSchema<unknown>;\n readonly outputSchema?: import('@graphorin/core').ZodLikeSchema<unknown> | undefined;\n readonly defer_loading: boolean;\n readonly inboundSanitization: InboundSanitizationPolicy;\n readonly sideEffectClass: import('@graphorin/core').SideEffectClass;\n readonly maxResultTokens?: number;\n readonly truncationStrategy?: import('@graphorin/core').TruncationStrategy;\n /** Per-call timeout forwarded to `client.callTool` (MC-3/MC-5). */\n readonly callTimeoutMs?: number;\n /** MC-6: sha256 fingerprint of the producing MCP definition. */\n readonly definitionHash: string;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n readonly preferredModel?:\n | import('@graphorin/core').ModelHint\n | import('@graphorin/core').ModelSpec;\n}\n\nfunction buildAdaptedTool(args: BuildAdaptedToolArgs): Tool<unknown, unknown, unknown> {\n const sanitizedDescription = sanitizeDescription({\n description: args.description,\n inboundSanitization: args.inboundSanitization,\n toolName: args.graphorinToolName,\n serverIdentity: args.serverIdentity,\n });\n\n const tool = {\n name: args.graphorinToolName,\n description: sanitizedDescription,\n inputSchema: args.inputSchema,\n ...(args.outputSchema === undefined ? {} : { outputSchema: args.outputSchema }),\n defer_loading: args.defer_loading,\n inboundSanitization: args.inboundSanitization,\n sideEffectClass: args.sideEffectClass,\n sandboxPolicy: 'sandboxed' as const,\n ...(args.maxResultTokens === undefined ? {} : { maxResultTokens: args.maxResultTokens }),\n ...(args.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: args.truncationStrategy }),\n ...(args.preferredModel === undefined ? {} : { preferredModel: args.preferredModel }),\n async execute(\n input: unknown,\n ctx?: import('@graphorin/core').ToolExecutionContext<unknown>,\n ): Promise<ToolReturn<unknown>> {\n // MC-5: the agent's per-call AbortSignal reaches the wire — an\n // aborted run sends `notifications/cancelled` instead of orphaning\n // the JSON-RPC request on the server. MC-3: the per-server call\n // timeout rides along.\n const result = await args.client.callTool(args.mcpToolName, input, {\n ...(ctx?.signal !== undefined ? { signal: ctx.signal } : {}),\n ...(args.callTimeoutMs !== undefined ? { timeoutMs: args.callTimeoutMs } : {}),\n });\n return adaptCallResult({\n result,\n outputSchema: args.outputSchema,\n serverIdentity: args.serverIdentity,\n toolName: args.graphorinToolName,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n });\n },\n } satisfies Tool<unknown, unknown, unknown>;\n // MC-7: pre-stamp the provenance so the agent's inferToolSource —\n // which honours an existing stamp — classifies tools passed via\n // `config.tools` as 'mcp-derived' (untrusted for the WI-12 dataflow\n // policy) instead of first-party. Zero operator boilerplate.\n return Object.assign(tool, {\n __source: { kind: 'mcp', serverIdentity: args.serverIdentity.id } as const,\n // MC-6: operators persist this fingerprint to pin the approved\n // definition (`toTools({ pinnedFingerprints })`).\n __definitionHash: args.definitionHash,\n });\n}\n\n/** Unused export kept for ergonomic test access. */\nexport type AdaptedToolChunkBuffer = ReadonlyArray<ContentChunk>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"to-tools.js","names":["tools: Tool[]"],"sources":["../../src/client/to-tools.ts"],"sourcesContent":["/**\n * `toTools()` adapter - bridges MCP tool descriptors into the\n * Graphorin tool registry.\n *\n * The adapter orchestrates three extracted concerns (F-MCP-001):\n *\n * 1. Filters / namespaces the `listTools()` output, then resolves the\n * per-server effective `defer_loading` flag - see\n * {@link import('./defer-loading.js').resolveDeferLoading}.\n * 2. Resolves the per-server inbound prompt-injection policy and strips\n * imperative payloads from each description - see\n * {@link import('./inbound-filters.js')}.\n * 3. Converts each MCP tool into a strongly-typed Graphorin `Tool` whose\n * `execute(...)` calls back into {@link MCPClient.callTool} and adapts\n * the result - see {@link import('./adapt-result.js').adaptCallResult}.\n *\n * The trust class is pinned to `'mcp-derived'` so the agent runtime's\n * per-step preamble fires regardless of the body-level policy.\n *\n * @packageDocumentation\n */\n\nimport type { ContentChunk, InboundSanitizationPolicy, Tool, ToolReturn } from '@graphorin/core';\nimport { buildJsonSchemaValidator, type JsonSchemaLike } from '../registry/json-schema.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport { adaptCallResult } from './adapt-result.js';\nimport {\n _resetDeferLoadingDedupForTesting,\n DEFAULT_DEFER_LOADING_THRESHOLD,\n resolveDeferLoading,\n} from './defer-loading.js';\nimport {\n _resetInboundFiltersDedupForTesting,\n resolveInboundPolicy,\n sanitizeDescription,\n warnOnPassthroughOverride,\n} from './inbound-filters.js';\nimport { computeToolDefinitionHash } from './pinning.js';\nimport type { MCPClient, MCPToolDefinition, MCPToToolsOptions } from './types.js';\n\n// Re-exported for backward compatibility: callers (and tests) import\n// these symbols directly from `./to-tools.js` and via the client barrel.\nexport { adaptCallResult } from './adapt-result.js';\nexport { DEFAULT_DEFER_LOADING_THRESHOLD } from './defer-loading.js';\n\n/**\n * Reset every process-scoped dedup set owned by the adapter modules.\n * Used by tests.\n *\n * @experimental\n */\nexport function _resetMcpAdapterDedupForTesting(): void {\n _resetDeferLoadingDedupForTesting();\n _resetInboundFiltersDedupForTesting();\n}\n\n/** Result returned by {@link adaptMCPTools}. */\nexport interface AdaptedToolsResult {\n readonly tools: ReadonlyArray<Tool>;\n /** MC-6: sha256 definition fingerprint per MCP tool name. */\n readonly fingerprints: ReadonlyMap<string, string>;\n readonly autoDeferralFired: boolean;\n readonly resolvedDeferLoading: boolean;\n readonly resolvedInboundSanitization: InboundSanitizationPolicy;\n readonly toolCount: number;\n readonly deferralThreshold: number;\n}\n\n/**\n * Build the {@link Tool} array for the supplied MCP tool catalogue.\n *\n * @stable\n */\nexport function adaptMCPTools(args: {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly catalogue: ReadonlyArray<MCPToolDefinition>;\n readonly options?: MCPToToolsOptions;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n}): AdaptedToolsResult {\n const opts = args.options ?? {};\n const fingerprints = new Map<string, string>();\n const filter = opts.filter;\n const namespace = (opts.namespace ?? '').trim();\n const filtered = filter === undefined ? args.catalogue : args.catalogue.filter((t) => filter(t));\n const total = filtered.length;\n const threshold = opts.deferLoadingThreshold ?? DEFAULT_DEFER_LOADING_THRESHOLD;\n\n const { autoDeferralFired, resolvedDeferLoading } = resolveDeferLoading({\n serverIdentity: args.serverIdentity,\n toolNames: filtered.map((t) => t.name),\n explicitDefer: opts.defer_loading,\n threshold,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const resolvedInbound = resolveInboundPolicy(opts.inboundSanitization);\n warnOnPassthroughOverride({\n resolvedInbound,\n serverIdentity: args.serverIdentity,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const tools: Tool[] = [];\n for (const definition of filtered) {\n const namespacedName =\n namespace.length === 0 ? definition.name : `${namespace}.${definition.name}`;\n const sideEffectClass = opts.sideEffectClassByTool?.[namespacedName] ?? 'external-stateful';\n const preferredModel = opts.preferredModelByTool?.[namespacedName];\n const inputValidator = buildJsonSchemaValidator(definition.inputSchema as JsonSchemaLike);\n const outputValidator =\n definition.outputSchema === undefined\n ? undefined\n : buildJsonSchemaValidator(definition.outputSchema as JsonSchemaLike);\n const definitionHash = computeToolDefinitionHash(definition);\n fingerprints.set(definition.name, definitionHash);\n tools.push(\n buildAdaptedTool({\n client: args.client,\n serverIdentity: args.serverIdentity,\n definitionHash,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n mcpToolName: definition.name,\n graphorinToolName: namespacedName,\n description:\n definition.description.length === 0 ? `${definition.name} (MCP)` : definition.description,\n inputSchema: inputValidator,\n outputSchema: outputValidator,\n defer_loading: resolvedDeferLoading,\n inboundSanitization: resolvedInbound,\n sideEffectClass,\n ...(opts.maxResultTokens === undefined ? {} : { maxResultTokens: opts.maxResultTokens }),\n ...(opts.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: opts.truncationStrategy }),\n ...(opts.callTimeoutMs === undefined ? {} : { callTimeoutMs: opts.callTimeoutMs }),\n ...(preferredModel === undefined ? {} : { preferredModel }),\n }),\n );\n }\n\n return Object.freeze({\n tools: Object.freeze(tools),\n fingerprints,\n autoDeferralFired,\n resolvedDeferLoading,\n resolvedInboundSanitization: resolvedInbound,\n toolCount: total,\n deferralThreshold: threshold,\n });\n}\n\ninterface BuildAdaptedToolArgs {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly mcpToolName: string;\n readonly graphorinToolName: string;\n readonly description: string;\n readonly inputSchema: import('@graphorin/core').ZodLikeSchema<unknown>;\n readonly outputSchema?: import('@graphorin/core').ZodLikeSchema<unknown> | undefined;\n readonly defer_loading: boolean;\n readonly inboundSanitization: InboundSanitizationPolicy;\n readonly sideEffectClass: import('@graphorin/core').SideEffectClass;\n readonly maxResultTokens?: number;\n readonly truncationStrategy?: import('@graphorin/core').TruncationStrategy;\n /** Per-call timeout forwarded to `client.callTool` (MC-3/MC-5). */\n readonly callTimeoutMs?: number;\n /** MC-6: sha256 fingerprint of the producing MCP definition. */\n readonly definitionHash: string;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n readonly preferredModel?:\n | import('@graphorin/core').ModelHint\n | import('@graphorin/core').ModelSpec;\n}\n\nfunction buildAdaptedTool(args: BuildAdaptedToolArgs): Tool<unknown, unknown, unknown> {\n const sanitizedDescription = sanitizeDescription({\n description: args.description,\n inboundSanitization: args.inboundSanitization,\n toolName: args.graphorinToolName,\n serverIdentity: args.serverIdentity,\n });\n\n const tool = {\n name: args.graphorinToolName,\n description: sanitizedDescription,\n inputSchema: args.inputSchema,\n ...(args.outputSchema === undefined ? {} : { outputSchema: args.outputSchema }),\n defer_loading: args.defer_loading,\n inboundSanitization: args.inboundSanitization,\n sideEffectClass: args.sideEffectClass,\n sandboxPolicy: 'sandboxed' as const,\n ...(args.maxResultTokens === undefined ? {} : { maxResultTokens: args.maxResultTokens }),\n ...(args.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: args.truncationStrategy }),\n ...(args.preferredModel === undefined ? {} : { preferredModel: args.preferredModel }),\n async execute(\n input: unknown,\n ctx?: import('@graphorin/core').ToolExecutionContext<unknown>,\n ): Promise<ToolReturn<unknown>> {\n // MC-5: the agent's per-call AbortSignal reaches the wire - an\n // aborted run sends `notifications/cancelled` instead of orphaning\n // the JSON-RPC request on the server. MC-3: the per-server call\n // timeout rides along.\n const result = await args.client.callTool(args.mcpToolName, input, {\n ...(ctx?.signal !== undefined ? { signal: ctx.signal } : {}),\n ...(args.callTimeoutMs !== undefined ? { timeoutMs: args.callTimeoutMs } : {}),\n });\n return adaptCallResult({\n result,\n outputSchema: args.outputSchema,\n serverIdentity: args.serverIdentity,\n toolName: args.graphorinToolName,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n });\n },\n } satisfies Tool<unknown, unknown, unknown>;\n // MC-7: pre-stamp the provenance so the agent's inferToolSource -\n // which honours an existing stamp - classifies tools passed via\n // `config.tools` as 'mcp-derived' (untrusted for the WI-12 dataflow\n // policy) instead of first-party. Zero operator boilerplate.\n return Object.assign(tool, {\n __source: { kind: 'mcp', serverIdentity: args.serverIdentity.id } as const,\n // MC-6: operators persist this fingerprint to pin the approved\n // definition (`toTools({ pinnedFingerprints })`).\n __definitionHash: args.definitionHash,\n });\n}\n\n/** Unused export kept for ergonomic test access. */\nexport type AdaptedToolChunkBuffer = ReadonlyArray<ContentChunk>;\n"],"mappings":"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"}
|
|
@@ -37,7 +37,7 @@ function authWrappedFetch(baseFetch, auth) {
|
|
|
37
37
|
*
|
|
38
38
|
* When an `auth` source is supplied (HTTP transports only) the factory
|
|
39
39
|
* installs a per-request fetch-wrapper that re-resolves the
|
|
40
|
-
* `Authorization` header on every outgoing call
|
|
40
|
+
* `Authorization` header on every outgoing call - this is the seam that
|
|
41
41
|
* makes a long-lived agent session survive OAuth token expiry without
|
|
42
42
|
* re-creating the client.
|
|
43
43
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport-factory.js","names":["stdio: Transport","transport: Transport"],"sources":["../../src/client/transport-factory.ts"],"sourcesContent":["/**\n * Factory that materialises a `@modelcontextprotocol/sdk` `Transport`\n * instance from a {@link MCPTransportConfig}.\n *\n * The factory is small on purpose: every MCP-spec evolution (new\n * transport, transport-specific option, transport-level header\n * convention) lands here so the higher-level {@link createMCPClient}\n * stays untouched.\n *\n * @packageDocumentation\n */\n\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\n\nimport type { MCPTransportConfig } from '../transport/types.js';\n\n/**\n * Outcome of the transport build, including the SDK transport\n * instance and the helper hooks the client needs to forward to the\n * SDK.\n */\nexport interface BuiltTransport {\n readonly transport: Transport;\n readonly disposeBeforeStart?: () => Promise<void>;\n}\n\n/**\n * Live source of the outbound `Authorization` header for the HTTP\n * transports. The bridge's `OAuthAuthorizationProvider` satisfies this\n * structurally (its `resolveHeader()` already returns the full\n * `Bearer <token>` value); a static `bearerToken` is wrapped into a\n * constant resolver by {@link createMCPClient}.\n *\n * @internal\n */\nexport interface TransportAuthSource {\n /** Resolve the full `Authorization` header value (e.g. `Bearer abc`). */\n resolveHeader(): Promise<string> | string;\n}\n\n/**\n * Wrap a `fetch` implementation so the live `Authorization` header is\n * re-resolved and injected on **every** outgoing request. The wrapper\n * always overwrites a caller-supplied `Authorization` header so the\n * provider stays the single source of truth (refresh-ahead from\n * {@link createOAuthAuthorizationProvider} fires per request).\n */\nfunction authWrappedFetch(baseFetch: typeof fetch, auth: TransportAuthSource): typeof fetch {\n return (async (input: Parameters<typeof fetch>[0], init?: RequestInit): Promise<Response> => {\n const header = await auth.resolveHeader();\n const headers = new Headers(init?.headers);\n headers.set('Authorization', header);\n return baseFetch(input, { ...init, headers });\n }) as typeof fetch;\n}\n\n/**\n * Build a SDK `Transport` from a {@link MCPTransportConfig}.\n *\n * When an `auth` source is supplied (HTTP transports only) the factory\n * installs a per-request fetch-wrapper that re-resolves the\n * `Authorization` header on every outgoing call
|
|
1
|
+
{"version":3,"file":"transport-factory.js","names":["stdio: Transport","transport: Transport"],"sources":["../../src/client/transport-factory.ts"],"sourcesContent":["/**\n * Factory that materialises a `@modelcontextprotocol/sdk` `Transport`\n * instance from a {@link MCPTransportConfig}.\n *\n * The factory is small on purpose: every MCP-spec evolution (new\n * transport, transport-specific option, transport-level header\n * convention) lands here so the higher-level {@link createMCPClient}\n * stays untouched.\n *\n * @packageDocumentation\n */\n\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\n\nimport type { MCPTransportConfig } from '../transport/types.js';\n\n/**\n * Outcome of the transport build, including the SDK transport\n * instance and the helper hooks the client needs to forward to the\n * SDK.\n */\nexport interface BuiltTransport {\n readonly transport: Transport;\n readonly disposeBeforeStart?: () => Promise<void>;\n}\n\n/**\n * Live source of the outbound `Authorization` header for the HTTP\n * transports. The bridge's `OAuthAuthorizationProvider` satisfies this\n * structurally (its `resolveHeader()` already returns the full\n * `Bearer <token>` value); a static `bearerToken` is wrapped into a\n * constant resolver by {@link createMCPClient}.\n *\n * @internal\n */\nexport interface TransportAuthSource {\n /** Resolve the full `Authorization` header value (e.g. `Bearer abc`). */\n resolveHeader(): Promise<string> | string;\n}\n\n/**\n * Wrap a `fetch` implementation so the live `Authorization` header is\n * re-resolved and injected on **every** outgoing request. The wrapper\n * always overwrites a caller-supplied `Authorization` header so the\n * provider stays the single source of truth (refresh-ahead from\n * {@link createOAuthAuthorizationProvider} fires per request).\n */\nfunction authWrappedFetch(baseFetch: typeof fetch, auth: TransportAuthSource): typeof fetch {\n return (async (input: Parameters<typeof fetch>[0], init?: RequestInit): Promise<Response> => {\n const header = await auth.resolveHeader();\n const headers = new Headers(init?.headers);\n headers.set('Authorization', header);\n return baseFetch(input, { ...init, headers });\n }) as typeof fetch;\n}\n\n/**\n * Build a SDK `Transport` from a {@link MCPTransportConfig}.\n *\n * When an `auth` source is supplied (HTTP transports only) the factory\n * installs a per-request fetch-wrapper that re-resolves the\n * `Authorization` header on every outgoing call - this is the seam that\n * makes a long-lived agent session survive OAuth token expiry without\n * re-creating the client.\n *\n * @stable\n */\nexport function buildTransport(\n config: MCPTransportConfig,\n options?: { readonly auth?: TransportAuthSource },\n): BuiltTransport {\n switch (config.kind) {\n case 'stdio': {\n const stdio: Transport = new StdioClientTransport({\n command: config.command,\n ...(config.args === undefined ? {} : { args: [...config.args] }),\n ...(config.env === undefined ? {} : { env: { ...config.env } }),\n ...(config.cwd === undefined ? {} : { cwd: config.cwd }),\n ...(config.stderr === undefined ? {} : { stderr: config.stderr }),\n });\n return Object.freeze({ transport: stdio });\n }\n case 'streamable-http': {\n const url = typeof config.url === 'string' ? new URL(config.url) : config.url;\n const headers = { ...(config.headers ?? {}) };\n const baseFetch = config.fetch ?? fetch;\n const fetchImpl =\n options?.auth === undefined ? config.fetch : authWrappedFetch(baseFetch, options.auth);\n // The SDK class's `sessionId: string | undefined` getter is\n // structurally narrower than `Transport.sessionId?: string` in\n // strict mode; route through `unknown` to keep the public\n // surface clean.\n const sdkTransport = new StreamableHTTPClientTransport(url, {\n ...(fetchImpl === undefined ? {} : { fetch: fetchImpl }),\n requestInit: { headers },\n ...(config.sessionId === undefined ? {} : { sessionId: config.sessionId }),\n });\n const transport = sdkTransport as unknown as Transport;\n return Object.freeze({ transport });\n }\n case 'sse': {\n const url = typeof config.url === 'string' ? new URL(config.url) : config.url;\n const headers = { ...(config.headers ?? {}) };\n const baseFetch = config.fetch ?? fetch;\n const fetchImpl =\n options?.auth === undefined ? config.fetch : authWrappedFetch(baseFetch, options.auth);\n const transport: Transport = new SSEClientTransport(url, {\n ...(fetchImpl === undefined ? {} : { fetch: fetchImpl }),\n requestInit: { headers },\n });\n return Object.freeze({ transport });\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAS,iBAAiB,WAAyB,MAAyC;AAC1F,SAAQ,OAAO,OAAoC,SAA0C;EAC3F,MAAM,SAAS,MAAM,KAAK,eAAe;EACzC,MAAM,UAAU,IAAI,QAAQ,MAAM,QAAQ;AAC1C,UAAQ,IAAI,iBAAiB,OAAO;AACpC,SAAO,UAAU,OAAO;GAAE,GAAG;GAAM;GAAS,CAAC;;;;;;;;;;;;;;AAejD,SAAgB,eACd,QACA,SACgB;AAChB,SAAQ,OAAO,MAAf;EACE,KAAK,SAAS;GACZ,MAAMA,QAAmB,IAAI,qBAAqB;IAChD,SAAS,OAAO;IAChB,GAAI,OAAO,SAAS,SAAY,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,OAAO,KAAK,EAAE;IAC/D,GAAI,OAAO,QAAQ,SAAY,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,OAAO,KAAK,EAAE;IAC9D,GAAI,OAAO,QAAQ,SAAY,EAAE,GAAG,EAAE,KAAK,OAAO,KAAK;IACvD,GAAI,OAAO,WAAW,SAAY,EAAE,GAAG,EAAE,QAAQ,OAAO,QAAQ;IACjE,CAAC;AACF,UAAO,OAAO,OAAO,EAAE,WAAW,OAAO,CAAC;;EAE5C,KAAK,mBAAmB;GACtB,MAAM,MAAM,OAAO,OAAO,QAAQ,WAAW,IAAI,IAAI,OAAO,IAAI,GAAG,OAAO;GAC1E,MAAM,UAAU,EAAE,GAAI,OAAO,WAAW,EAAE,EAAG;GAC7C,MAAM,YAAY,OAAO,SAAS;GAClC,MAAM,YACJ,SAAS,SAAS,SAAY,OAAO,QAAQ,iBAAiB,WAAW,QAAQ,KAAK;GAUxF,MAAM,YALe,IAAI,8BAA8B,KAAK;IAC1D,GAAI,cAAc,SAAY,EAAE,GAAG,EAAE,OAAO,WAAW;IACvD,aAAa,EAAE,SAAS;IACxB,GAAI,OAAO,cAAc,SAAY,EAAE,GAAG,EAAE,WAAW,OAAO,WAAW;IAC1E,CAAC;AAEF,UAAO,OAAO,OAAO,EAAE,WAAW,CAAC;;EAErC,KAAK,OAAO;GACV,MAAM,MAAM,OAAO,OAAO,QAAQ,WAAW,IAAI,IAAI,OAAO,IAAI,GAAG,OAAO;GAC1E,MAAM,UAAU,EAAE,GAAI,OAAO,WAAW,EAAE,EAAG;GAC7C,MAAM,YAAY,OAAO,SAAS;GAClC,MAAM,YACJ,SAAS,SAAS,SAAY,OAAO,QAAQ,iBAAiB,WAAW,QAAQ,KAAK;GACxF,MAAMC,YAAuB,IAAI,mBAAmB,KAAK;IACvD,GAAI,cAAc,SAAY,EAAE,GAAG,EAAE,OAAO,WAAW;IACvD,aAAa,EAAE,SAAS;IACzB,CAAC;AACF,UAAO,OAAO,OAAO,EAAE,WAAW,CAAC"}
|