@truefoundry/trueforge-core 0.2.0-rc.4 → 0.2.0-rc.6

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.
Files changed (93) hide show
  1. package/dist/agent-session/ITurnResourceResolver.d.ts +6 -0
  2. package/dist/agent-session/ITurnResourceResolver.d.ts.map +1 -1
  3. package/dist/agent-session/ITurnResourceResolver.js.map +1 -1
  4. package/dist/agent-session/SessionHandle.d.ts.map +1 -1
  5. package/dist/agent-session/SessionHandle.js +2 -0
  6. package/dist/agent-session/SessionHandle.js.map +1 -1
  7. package/dist/agent-session/SessionHandle.mjs +2 -0
  8. package/dist/agent-session/SessionHandle.mjs.map +1 -1
  9. package/dist/agent-session/TurnResourceResolver.d.ts +8 -0
  10. package/dist/agent-session/TurnResourceResolver.d.ts.map +1 -1
  11. package/dist/agent-session/TurnResourceResolver.js +4 -0
  12. package/dist/agent-session/TurnResourceResolver.js.map +1 -1
  13. package/dist/agent-session/TurnResourceResolver.mjs +4 -0
  14. package/dist/agent-session/TurnResourceResolver.mjs.map +1 -1
  15. package/dist/agent-session/builtinsFromSpec.d.ts +2 -0
  16. package/dist/agent-session/builtinsFromSpec.d.ts.map +1 -1
  17. package/dist/agent-session/builtinsFromSpec.js +5 -1
  18. package/dist/agent-session/builtinsFromSpec.js.map +1 -1
  19. package/dist/agent-session/builtinsFromSpec.mjs +5 -1
  20. package/dist/agent-session/builtinsFromSpec.mjs.map +1 -1
  21. package/dist/agent-session/index.d.ts +2 -2
  22. package/dist/agent-session/index.d.ts.map +1 -1
  23. package/dist/agent-session/index.js +2 -0
  24. package/dist/agent-session/index.js.map +1 -1
  25. package/dist/agent-session/index.mjs +3 -1
  26. package/dist/agent-session/index.mjs.map +1 -1
  27. package/dist/agent-session/schemas/agentSpec.d.ts +6 -0
  28. package/dist/agent-session/schemas/agentSpec.d.ts.map +1 -1
  29. package/dist/agent-session/schemas/agentSpec.js +7 -3
  30. package/dist/agent-session/schemas/agentSpec.js.map +1 -1
  31. package/dist/agent-session/schemas/agentSpec.mjs +7 -3
  32. package/dist/agent-session/schemas/agentSpec.mjs.map +1 -1
  33. package/dist/agent-session/schemas/events.d.ts +65 -2
  34. package/dist/agent-session/schemas/events.d.ts.map +1 -1
  35. package/dist/agent-session/schemas/events.js +32 -2
  36. package/dist/agent-session/schemas/events.js.map +1 -1
  37. package/dist/agent-session/schemas/events.mjs +26 -1
  38. package/dist/agent-session/schemas/events.mjs.map +1 -1
  39. package/dist/agent-session/schemas/session.d.ts +9 -0
  40. package/dist/agent-session/schemas/session.d.ts.map +1 -1
  41. package/dist/core/capabilities/builtins/WebSearch.d.ts +32 -0
  42. package/dist/core/capabilities/builtins/WebSearch.d.ts.map +1 -0
  43. package/dist/core/capabilities/builtins/WebSearch.js +137 -0
  44. package/dist/core/capabilities/builtins/WebSearch.js.map +1 -0
  45. package/dist/core/capabilities/builtins/WebSearch.mjs +96 -0
  46. package/dist/core/capabilities/builtins/WebSearch.mjs.map +1 -0
  47. package/dist/core/index.d.ts +6 -0
  48. package/dist/core/index.d.ts.map +1 -1
  49. package/dist/core/index.js +22 -0
  50. package/dist/core/index.js.map +1 -1
  51. package/dist/core/index.mjs +20 -0
  52. package/dist/core/index.mjs.map +1 -1
  53. package/dist/core/mcp/RemoteMCP.d.ts +2 -0
  54. package/dist/core/mcp/RemoteMCP.d.ts.map +1 -1
  55. package/dist/core/mcp/RemoteMCP.js +11 -1
  56. package/dist/core/mcp/RemoteMCP.js.map +1 -1
  57. package/dist/core/mcp/RemoteMCP.mjs +12 -1
  58. package/dist/core/mcp/RemoteMCP.mjs.map +1 -1
  59. package/dist/core/mcp/ToolSelectorPolicy.d.ts +1 -1
  60. package/dist/core/mcp/ToolSelectorPolicy.js +1 -1
  61. package/dist/core/mcp/ToolSelectorPolicy.js.map +1 -1
  62. package/dist/core/mcp/ToolSelectorPolicy.mjs +1 -1
  63. package/dist/core/mcp/ToolSelectorPolicy.mjs.map +1 -1
  64. package/dist/core/mcp/remoteMcpClient.d.ts +5 -0
  65. package/dist/core/mcp/remoteMcpClient.d.ts.map +1 -1
  66. package/dist/core/mcp/remoteMcpClient.js +49 -7
  67. package/dist/core/mcp/remoteMcpClient.js.map +1 -1
  68. package/dist/core/mcp/remoteMcpClient.mjs +46 -6
  69. package/dist/core/mcp/remoteMcpClient.mjs.map +1 -1
  70. package/dist/core/mcp/toolSelectors.d.ts.map +1 -1
  71. package/dist/core/mcp/toolSelectors.js +1 -1
  72. package/dist/core/mcp/toolSelectors.js.map +1 -1
  73. package/dist/core/mcp/toolSelectors.mjs +1 -1
  74. package/dist/core/mcp/toolSelectors.mjs.map +1 -1
  75. package/dist/core/web-search/ParallelWebSearchProvider.d.ts +24 -0
  76. package/dist/core/web-search/ParallelWebSearchProvider.d.ts.map +1 -0
  77. package/dist/core/web-search/ParallelWebSearchProvider.js +95 -0
  78. package/dist/core/web-search/ParallelWebSearchProvider.js.map +1 -0
  79. package/dist/core/web-search/ParallelWebSearchProvider.mjs +62 -0
  80. package/dist/core/web-search/ParallelWebSearchProvider.mjs.map +1 -0
  81. package/dist/core/web-search/WebSearchProvider.d.ts +42 -0
  82. package/dist/core/web-search/WebSearchProvider.d.ts.map +1 -0
  83. package/dist/core/web-search/WebSearchProvider.js +34 -0
  84. package/dist/core/web-search/WebSearchProvider.js.map +1 -0
  85. package/dist/core/web-search/WebSearchProvider.mjs +9 -0
  86. package/dist/core/web-search/WebSearchProvider.mjs.map +1 -0
  87. package/dist/request-reply/client.d.ts +2 -2
  88. package/dist/request-reply/client.d.ts.map +1 -1
  89. package/dist/request-reply/client.js +3 -0
  90. package/dist/request-reply/client.js.map +1 -1
  91. package/dist/request-reply/client.mjs +3 -0
  92. package/dist/request-reply/client.mjs.map +1 -1
  93. package/package.json +3 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/mcp/RemoteMCP.ts"],"sourcesContent":["import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';\nimport type { Logger } from 'winston';\nimport { McpConnectionError } from '../errors';\nimport type { MCPServerInitInfo } from '../events/schema';\nimport type { InternalToolCallInfo } from '../llm/LLMTypes';\nimport type { AgentTracing } from '../tracing/AgentTracing';\nimport { NOOP_AGENT_TRACING } from '../tracing/NoopAgentTracing';\nimport {\n type AgentToolSchema,\n type AuthRequiredResponse,\n type CallToolResolvedResponse,\n type ListToolsResolvedResponse,\n type MCPAuthRequired,\n type ToolSource,\n} from './IMCPServer';\nimport { paginateWithCursorGuard } from './pagination';\nimport {\n connectRemoteMcp,\n isSessionExpiredError,\n type RemoteMcpConnection,\n type RemoteMcpTransportType,\n} from './remoteMcpClient';\n\n/** Redacted url for trace spans: scheme + host + path only, so userinfo/query secrets never leak. */\nfunction redactUrlForTrace(url: string): string {\n try {\n const u = new URL(url);\n return `${u.protocol}//${u.host}${u.pathname}`;\n } catch {\n return '';\n }\n}\n\n/** What a headers resolver returns: the headers to send, or a signal that auth is required. */\nexport type ResolveHeadersResult = { headers: Record<string, string> } | { authRequired: MCPAuthRequired };\n\n/** Static headers, or a resolver (invoked at connect) returning headers or signalling auth-required. */\nexport type RemoteMcpHeaders = Record<string, string> | (() => Promise<ResolveHeadersResult>);\n\ntype ExecuteResult<T> = { result: T; wasInitialized: MCPServerInitInfo | undefined } | AuthRequiredResponse;\n\n/**\n * Connection/session half of a remote MCP server: owns the transport (connects itself from `url` +\n * `headers`), session id, raw tool cache, connect single-flight and session-expiry retry. Policy-free\n * — a per-agent {@link ToolSet} layers policy.\n */\nexport class RemoteMCP implements ToolSource {\n readonly name: string;\n readonly id: string;\n readonly description?: string | undefined;\n\n private readonly url: string;\n private readonly headers: RemoteMcpHeaders;\n private readonly signal: AbortSignal;\n private readonly logger: Logger;\n private readonly tracing: AgentTracing;\n private readonly requestTimeoutMs: number;\n private readonly connectTimeoutMs: number;\n // Redacted display url for trace spans (derived from url; unused when tracing is a no-op).\n private readonly traceUrl: string;\n\n private _connection?: RemoteMcpConnection | undefined;\n private isConnected = false;\n private connectPromise: Promise<MCPServerInitInfo | undefined> | undefined;\n // undefined = never connected, string = stateful session id, null = stateless/SSE.\n private sessionId: string | null | undefined;\n private resolvedTransportType?: RemoteMcpTransportType | undefined;\n private cachedTools?: AgentToolSchema[] | undefined;\n\n constructor(params: {\n name: string;\n id: string;\n description?: string | undefined;\n url: string;\n headers: RemoteMcpHeaders;\n logger: Logger;\n tracing?: AgentTracing | undefined;\n sessionId?: string | undefined;\n transportType?: RemoteMcpTransportType | undefined;\n requestTimeoutMs: number;\n connectTimeoutMs: number;\n signal: AbortSignal;\n }) {\n this.name = params.name;\n this.id = params.id;\n this.description = params.description;\n this.url = params.url;\n this.headers = params.headers;\n this.signal = params.signal;\n this.sessionId = params.sessionId;\n this.resolvedTransportType = params.transportType;\n this.requestTimeoutMs = params.requestTimeoutMs;\n this.connectTimeoutMs = params.connectTimeoutMs;\n this.logger = params.logger;\n this.tracing = params.tracing ?? NOOP_AGENT_TRACING;\n this.traceUrl = redactUrlForTrace(params.url);\n }\n\n getSessionId(): string | undefined {\n return this.sessionId ?? undefined;\n }\n\n private get connection(): RemoteMcpConnection {\n if (!this._connection) {\n throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);\n }\n return this._connection;\n }\n\n private async resetConnection(): Promise<void> {\n this.isConnected = false;\n this.sessionId = undefined;\n this.cachedTools = undefined;\n this.connectPromise = undefined;\n await this.closeAndClearConnection();\n }\n\n private async closeAndClearConnection(): Promise<void> {\n await this._connection?.close().catch(() => {\n /* no-op */\n });\n this._connection = undefined;\n }\n\n private async resolveHeaders(): Promise<ResolveHeadersResult> {\n if (typeof this.headers !== 'function') {\n return { headers: this.headers };\n }\n return await this.headers();\n }\n\n private async executeWithSessionRetry<T>(operation: () => Promise<T>): Promise<ExecuteResult<T>> {\n // Auth is re-checked on every operation, not only on the first connect: a registered server's OAuth\n // can be revoked or expire mid-request, and callers must get authRequired rather than a generic\n // upstream failure. When already connected the resolved headers are unused (connect is skipped).\n const headersResult = await this.resolveHeaders();\n if ('authRequired' in headersResult) {\n return { authRequired: headersResult.authRequired };\n }\n return this.connectAndRun(headersResult.headers, operation, true);\n }\n\n private async connectAndRun<T>(\n headers: Record<string, string>,\n operation: () => Promise<T>,\n canRetry: boolean,\n ): Promise<ExecuteResult<T>> {\n try {\n const initInfo = await this.connectIfNeeded(headers);\n return { result: await operation(), wasInitialized: initInfo };\n } catch (error) {\n if (canRetry && isSessionExpiredError(error)) {\n this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);\n await this.resetConnection();\n return this.connectAndRun(headers, operation, false);\n }\n throw error;\n }\n }\n\n private async loadTools(): Promise<{ tools: AgentToolSchema[] }> {\n return this.tracing.withRemoteMcpToolSpan(\n { method: 'tools/list', serverName: this.name, serverId: this.id, serverUrl: this.traceUrl, enabled: true },\n async span => {\n const tools = await paginateWithCursorGuard(\n async cursor => {\n const page = await this.connection.listTools(cursor);\n return { items: page.tools, nextCursor: page.nextCursor };\n },\n this.name,\n this.logger,\n );\n this.cachedTools = tools.map(t => ({ ...t, preload: true }));\n span.setNumberOfTools(this.cachedTools.length);\n return { tools: this.cachedTools };\n },\n );\n }\n\n /** Unfiltered tool list (no policy). The per-agent wrapper applies selectors on top. */\n async listTools(): Promise<ListToolsResolvedResponse | AuthRequiredResponse> {\n if (this.cachedTools) {\n return { result: { tools: this.cachedTools }, wasInitialized: undefined };\n }\n const response = await this.executeWithSessionRetry(() => this.loadTools());\n if ('authRequired' in response) {\n return response;\n }\n return { result: { tools: response.result.tools }, wasInitialized: response.wasInitialized };\n }\n\n async callTool(params: CallToolRequest['params']): Promise<CallToolResolvedResponse | AuthRequiredResponse> {\n const response = await this.executeWithSessionRetry(() =>\n this.tracing.withRemoteMcpToolSpan(\n {\n method: 'tools/call',\n serverName: this.name,\n serverId: this.id,\n serverUrl: this.traceUrl,\n toolName: params.name,\n input: JSON.stringify(params.arguments),\n enabled: true,\n },\n async span => {\n const result = await this.connection.callTool(params);\n span.setOutput(JSON.stringify(result));\n return result;\n },\n ),\n );\n if ('authRequired' in response) {\n return response;\n }\n return { result: response.result, wasInitialized: response.wasInitialized };\n }\n\n toolCallInfo(params: CallToolRequest['params'], _resolveUnderlyingTool?: boolean): Promise<InternalToolCallInfo> {\n void _resolveUnderlyingTool;\n return Promise.resolve({\n type: 'mcp',\n original_tool_name: params.name,\n mcp_server_id: this.id,\n mcp_server_name: this.name,\n is_approval_required: false,\n });\n }\n\n private async connectIfNeeded(headers: Record<string, string>): Promise<MCPServerInitInfo | undefined> {\n if (this.isConnected) {\n return undefined;\n }\n if (this.connectPromise) {\n // Wait for the in-flight connect, but only its originator emits the init metadata.\n await this.connectPromise;\n return undefined;\n }\n\n const existingSessionId = this.sessionId;\n this.connectPromise = (async (): Promise<MCPServerInitInfo | undefined> => {\n let connection: RemoteMcpConnection;\n try {\n await this.closeAndClearConnection();\n connection = await this.tracing.withRemoteMcpToolSpan(\n { method: 'initialize', serverName: this.name, serverId: this.id, serverUrl: this.traceUrl, enabled: true },\n async span => {\n const conn = await connectRemoteMcp({\n url: this.url,\n headers,\n sessionId: this.sessionId ?? undefined,\n // Hint from a prior connect\n knownTransportType: this.resolvedTransportType,\n requestTimeoutMs: this.requestTimeoutMs,\n connectTimeoutMs: this.connectTimeoutMs,\n signal: this.signal,\n onClose: () => {\n this.isConnected = false;\n },\n onError: error => {\n this.logger.error(`Error on remote MCP transport ${this.name}`, { error });\n },\n });\n span.setOutput(JSON.stringify({ transport: conn.transportType, stateful: conn.sessionId !== null }));\n return conn;\n },\n );\n } catch (error) {\n await this.closeAndClearConnection();\n throw this.toConnectError(error);\n }\n this._connection = connection;\n this.resolvedTransportType = connection.transportType;\n this.isConnected = true;\n this.sessionId = connection.sessionId;\n if (existingSessionId === this.sessionId) {\n return undefined;\n }\n return {\n name: this.name,\n id: this.id,\n session_id: this.sessionId ?? undefined,\n transport_type: this.resolvedTransportType,\n };\n })().finally(() => {\n this.connectPromise = undefined;\n });\n return this.connectPromise;\n }\n\n private toConnectError(error: unknown): McpConnectionError {\n const statusCode = error instanceof McpConnectionError ? error.statusCode : 502;\n const message = error instanceof Error ? error.message : String(error);\n return new McpConnectionError(`Failed to connect to remote MCP server '${this.name}': ${message}`, statusCode, {\n cause: error,\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/core/mcp/RemoteMCP.ts"],"sourcesContent":["import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';\nimport type { Logger } from 'winston';\nimport { McpConnectionError } from '../errors';\nimport type { MCPServerInitInfo } from '../events/schema';\nimport type { InternalToolCallInfo } from '../llm/LLMTypes';\nimport type { AgentTracing } from '../tracing/AgentTracing';\nimport { NOOP_AGENT_TRACING } from '../tracing/NoopAgentTracing';\nimport { extractErrorLogFields } from '../util/errorLogFields';\nimport {\n type AgentToolSchema,\n type AuthRequiredResponse,\n type CallToolResolvedResponse,\n type ListToolsResolvedResponse,\n type MCPAuthRequired,\n type ToolSource,\n} from './IMCPServer';\nimport { paginateWithCursorGuard } from './pagination';\nimport {\n connectRemoteMcp,\n DEFAULT_MAX_MCP_RESPONSE_BYTES,\n isSessionExpiredError,\n type RemoteMcpConnection,\n type RemoteMcpTransportType,\n} from './remoteMcpClient';\n\n/** Redacted url for trace spans: scheme + host + path only, so userinfo/query secrets never leak. */\nfunction redactUrlForTrace(url: string): string {\n try {\n const u = new URL(url);\n return `${u.protocol}//${u.host}${u.pathname}`;\n } catch {\n return '';\n }\n}\n\n/** What a headers resolver returns: the headers to send, or a signal that auth is required. */\nexport type ResolveHeadersResult = { headers: Record<string, string> } | { authRequired: MCPAuthRequired };\n\n/** Static headers, or a resolver (invoked at connect) returning headers or signalling auth-required. */\nexport type RemoteMcpHeaders = Record<string, string> | (() => Promise<ResolveHeadersResult>);\n\ntype ExecuteResult<T> = { result: T; wasInitialized: MCPServerInitInfo | undefined } | AuthRequiredResponse;\n\n/**\n * Connection/session half of a remote MCP server: owns the transport (connects itself from `url` +\n * `headers`), session id, raw tool cache, connect single-flight and session-expiry retry. Policy-free\n * — a per-agent {@link ToolSet} layers policy.\n */\nexport class RemoteMCP implements ToolSource {\n readonly name: string;\n readonly id: string;\n readonly description?: string | undefined;\n\n private readonly url: string;\n private readonly headers: RemoteMcpHeaders;\n private readonly signal: AbortSignal;\n private readonly logger: Logger;\n private readonly tracing: AgentTracing;\n private readonly requestTimeoutMs: number;\n private readonly connectTimeoutMs: number;\n private readonly maxResponseBytes: number;\n // Redacted display url for trace spans (derived from url; unused when tracing is a no-op).\n private readonly traceUrl: string;\n\n private _connection?: RemoteMcpConnection | undefined;\n private isConnected = false;\n private connectPromise: Promise<MCPServerInitInfo | undefined> | undefined;\n // undefined = never connected, string = stateful session id, null = stateless/SSE.\n private sessionId: string | null | undefined;\n private resolvedTransportType?: RemoteMcpTransportType | undefined;\n private cachedTools?: AgentToolSchema[] | undefined;\n\n constructor(params: {\n name: string;\n id: string;\n description?: string | undefined;\n url: string;\n headers: RemoteMcpHeaders;\n logger: Logger;\n tracing?: AgentTracing | undefined;\n sessionId?: string | undefined;\n transportType?: RemoteMcpTransportType | undefined;\n requestTimeoutMs: number;\n connectTimeoutMs: number;\n maxResponseBytes?: number | undefined;\n signal: AbortSignal;\n }) {\n this.name = params.name;\n this.id = params.id;\n this.description = params.description;\n this.url = params.url;\n this.headers = params.headers;\n this.signal = params.signal;\n this.sessionId = params.sessionId;\n this.resolvedTransportType = params.transportType;\n this.requestTimeoutMs = params.requestTimeoutMs;\n this.connectTimeoutMs = params.connectTimeoutMs;\n this.maxResponseBytes = params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES;\n this.logger = params.logger;\n this.tracing = params.tracing ?? NOOP_AGENT_TRACING;\n this.traceUrl = redactUrlForTrace(params.url);\n }\n\n getSessionId(): string | undefined {\n return this.sessionId ?? undefined;\n }\n\n private get connection(): RemoteMcpConnection {\n if (!this._connection) {\n throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);\n }\n return this._connection;\n }\n\n private async resetConnection(): Promise<void> {\n this.isConnected = false;\n this.sessionId = undefined;\n this.cachedTools = undefined;\n this.connectPromise = undefined;\n await this.closeAndClearConnection();\n }\n\n private async closeAndClearConnection(): Promise<void> {\n await this._connection?.close().catch(() => {\n /* no-op */\n });\n this._connection = undefined;\n }\n\n private async resolveHeaders(): Promise<ResolveHeadersResult> {\n if (typeof this.headers !== 'function') {\n return { headers: this.headers };\n }\n return await this.headers();\n }\n\n private async executeWithSessionRetry<T>(operation: () => Promise<T>): Promise<ExecuteResult<T>> {\n // Auth is re-checked on every operation, not only on the first connect: a registered server's OAuth\n // can be revoked or expire mid-request, and callers must get authRequired rather than a generic\n // upstream failure. When already connected the resolved headers are unused (connect is skipped).\n const headersResult = await this.resolveHeaders();\n if ('authRequired' in headersResult) {\n return { authRequired: headersResult.authRequired };\n }\n return this.connectAndRun(headersResult.headers, operation, true);\n }\n\n private async connectAndRun<T>(\n headers: Record<string, string>,\n operation: () => Promise<T>,\n canRetry: boolean,\n ): Promise<ExecuteResult<T>> {\n try {\n const initInfo = await this.connectIfNeeded(headers);\n return { result: await operation(), wasInitialized: initInfo };\n } catch (error) {\n if (canRetry && isSessionExpiredError(error)) {\n this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);\n await this.resetConnection();\n return this.connectAndRun(headers, operation, false);\n }\n throw error;\n }\n }\n\n private async loadTools(): Promise<{ tools: AgentToolSchema[] }> {\n return this.tracing.withRemoteMcpToolSpan(\n { method: 'tools/list', serverName: this.name, serverId: this.id, serverUrl: this.traceUrl, enabled: true },\n async span => {\n const tools = await paginateWithCursorGuard(\n async cursor => {\n const page = await this.connection.listTools(cursor);\n return { items: page.tools, nextCursor: page.nextCursor };\n },\n this.name,\n this.logger,\n );\n this.cachedTools = tools.map(t => ({ ...t, preload: true }));\n span.setNumberOfTools(this.cachedTools.length);\n return { tools: this.cachedTools };\n },\n );\n }\n\n /** Unfiltered tool list (no policy). The per-agent wrapper applies selectors on top. */\n async listTools(): Promise<ListToolsResolvedResponse | AuthRequiredResponse> {\n if (this.cachedTools) {\n return { result: { tools: this.cachedTools }, wasInitialized: undefined };\n }\n const response = await this.executeWithSessionRetry(() => this.loadTools());\n if ('authRequired' in response) {\n return response;\n }\n return { result: { tools: response.result.tools }, wasInitialized: response.wasInitialized };\n }\n\n async callTool(params: CallToolRequest['params']): Promise<CallToolResolvedResponse | AuthRequiredResponse> {\n const response = await this.executeWithSessionRetry(() =>\n this.tracing.withRemoteMcpToolSpan(\n {\n method: 'tools/call',\n serverName: this.name,\n serverId: this.id,\n serverUrl: this.traceUrl,\n toolName: params.name,\n input: JSON.stringify(params.arguments),\n enabled: true,\n },\n async span => {\n const result = await this.connection.callTool(params);\n span.setOutput(JSON.stringify(result));\n return result;\n },\n ),\n );\n if ('authRequired' in response) {\n return response;\n }\n return { result: response.result, wasInitialized: response.wasInitialized };\n }\n\n toolCallInfo(params: CallToolRequest['params'], _resolveUnderlyingTool?: boolean): Promise<InternalToolCallInfo> {\n void _resolveUnderlyingTool;\n return Promise.resolve({\n type: 'mcp',\n original_tool_name: params.name,\n mcp_server_id: this.id,\n mcp_server_name: this.name,\n is_approval_required: false,\n });\n }\n\n private async connectIfNeeded(headers: Record<string, string>): Promise<MCPServerInitInfo | undefined> {\n if (this.isConnected) {\n return undefined;\n }\n if (this.connectPromise) {\n // Wait for the in-flight connect, but only its originator emits the init metadata.\n await this.connectPromise;\n return undefined;\n }\n\n const existingSessionId = this.sessionId;\n this.connectPromise = (async (): Promise<MCPServerInitInfo | undefined> => {\n let connection: RemoteMcpConnection;\n try {\n await this.closeAndClearConnection();\n connection = await this.tracing.withRemoteMcpToolSpan(\n { method: 'initialize', serverName: this.name, serverId: this.id, serverUrl: this.traceUrl, enabled: true },\n async span => {\n const conn = await connectRemoteMcp({\n url: this.url,\n headers,\n sessionId: this.sessionId ?? undefined,\n // Hint from a prior connect\n knownTransportType: this.resolvedTransportType,\n requestTimeoutMs: this.requestTimeoutMs,\n connectTimeoutMs: this.connectTimeoutMs,\n maxResponseBytes: this.maxResponseBytes,\n signal: this.signal,\n onClose: () => {\n this.isConnected = false;\n },\n onError: error => {\n const fields = extractErrorLogFields(error);\n const msg = `Error on remote MCP transport ${this.name}`;\n if (fields.error.includes('Body Timeout')) {\n this.logger.warn(msg, fields);\n } else {\n this.logger.error(msg, fields);\n }\n },\n });\n span.setOutput(JSON.stringify({ transport: conn.transportType, stateful: conn.sessionId !== null }));\n return conn;\n },\n );\n } catch (error) {\n await this.closeAndClearConnection();\n throw this.toConnectError(error);\n }\n this._connection = connection;\n this.resolvedTransportType = connection.transportType;\n this.isConnected = true;\n this.sessionId = connection.sessionId;\n if (existingSessionId === this.sessionId) {\n return undefined;\n }\n return {\n name: this.name,\n id: this.id,\n session_id: this.sessionId ?? undefined,\n transport_type: this.resolvedTransportType,\n };\n })().finally(() => {\n this.connectPromise = undefined;\n });\n return this.connectPromise;\n }\n\n private toConnectError(error: unknown): McpConnectionError {\n const statusCode = error instanceof McpConnectionError ? error.statusCode : 502;\n const message = error instanceof Error ? error.message : String(error);\n return new McpConnectionError(`Failed to connect to remote MCP server '${this.name}': ${message}`, statusCode, {\n cause: error,\n });\n }\n}\n"],"mappings":";AAEA,SAAS,0BAA0B;AAInC,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,OAOO;AACP,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAGP,SAAS,kBAAkB,KAAqB;AAC9C,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,WAAO,GAAG,EAAE,QAAQ,KAAK,EAAE,IAAI,GAAG,EAAE,QAAQ;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,IAAM,YAAN,MAAsC;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EAET;AAAA,EACA,cAAc;AAAA,EACd;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAcT;AACD,SAAK,OAAO,OAAO;AACnB,SAAK,KAAK,OAAO;AACjB,SAAK,cAAc,OAAO;AAC1B,SAAK,MAAM,OAAO;AAClB,SAAK,UAAU,OAAO;AACtB,SAAK,SAAS,OAAO;AACrB,SAAK,YAAY,OAAO;AACxB,SAAK,wBAAwB,OAAO;AACpC,SAAK,mBAAmB,OAAO;AAC/B,SAAK,mBAAmB,OAAO;AAC/B,SAAK,mBAAmB,OAAO,oBAAoB;AACnD,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU,OAAO,WAAW;AACjC,SAAK,WAAW,kBAAkB,OAAO,GAAG;AAAA,EAC9C;AAAA,EAEA,eAAmC;AACjC,WAAO,KAAK,aAAa;AAAA,EAC3B;AAAA,EAEA,IAAY,aAAkC;AAC5C,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,eAAe,KAAK,IAAI,oDAAoD;AAAA,IAC9F;AACA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,kBAAiC;AAC7C,SAAK,cAAc;AACnB,SAAK,YAAY;AACjB,SAAK,cAAc;AACnB,SAAK,iBAAiB;AACtB,UAAM,KAAK,wBAAwB;AAAA,EACrC;AAAA,EAEA,MAAc,0BAAyC;AACrD,UAAM,KAAK,aAAa,MAAM,EAAE,MAAM,MAAM;AAAA,IAE5C,CAAC;AACD,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,MAAc,iBAAgD;AAC5D,QAAI,OAAO,KAAK,YAAY,YAAY;AACtC,aAAO,EAAE,SAAS,KAAK,QAAQ;AAAA,IACjC;AACA,WAAO,MAAM,KAAK,QAAQ;AAAA,EAC5B;AAAA,EAEA,MAAc,wBAA2B,WAAwD;AAI/F,UAAM,gBAAgB,MAAM,KAAK,eAAe;AAChD,QAAI,kBAAkB,eAAe;AACnC,aAAO,EAAE,cAAc,cAAc,aAAa;AAAA,IACpD;AACA,WAAO,KAAK,cAAc,cAAc,SAAS,WAAW,IAAI;AAAA,EAClE;AAAA,EAEA,MAAc,cACZ,SACA,WACA,UAC2B;AAC3B,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,gBAAgB,OAAO;AACnD,aAAO,EAAE,QAAQ,MAAM,UAAU,GAAG,gBAAgB,SAAS;AAAA,IAC/D,SAAS,OAAO;AACd,UAAI,YAAY,sBAAsB,KAAK,GAAG;AAC5C,aAAK,OAAO,KAAK,kCAAkC,KAAK,IAAI,qBAAqB;AACjF,cAAM,KAAK,gBAAgB;AAC3B,eAAO,KAAK,cAAc,SAAS,WAAW,KAAK;AAAA,MACrD;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAc,YAAmD;AAC/D,WAAO,KAAK,QAAQ;AAAA,MAClB,EAAE,QAAQ,cAAc,YAAY,KAAK,MAAM,UAAU,KAAK,IAAI,WAAW,KAAK,UAAU,SAAS,KAAK;AAAA,MAC1G,OAAM,SAAQ;AACZ,cAAM,QAAQ,MAAM;AAAA,UAClB,OAAM,WAAU;AACd,kBAAM,OAAO,MAAM,KAAK,WAAW,UAAU,MAAM;AACnD,mBAAO,EAAE,OAAO,KAAK,OAAO,YAAY,KAAK,WAAW;AAAA,UAC1D;AAAA,UACA,KAAK;AAAA,UACL,KAAK;AAAA,QACP;AACA,aAAK,cAAc,MAAM,IAAI,QAAM,EAAE,GAAG,GAAG,SAAS,KAAK,EAAE;AAC3D,aAAK,iBAAiB,KAAK,YAAY,MAAM;AAC7C,eAAO,EAAE,OAAO,KAAK,YAAY;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,YAAuE;AAC3E,QAAI,KAAK,aAAa;AACpB,aAAO,EAAE,QAAQ,EAAE,OAAO,KAAK,YAAY,GAAG,gBAAgB,OAAU;AAAA,IAC1E;AACA,UAAM,WAAW,MAAM,KAAK,wBAAwB,MAAM,KAAK,UAAU,CAAC;AAC1E,QAAI,kBAAkB,UAAU;AAC9B,aAAO;AAAA,IACT;AACA,WAAO,EAAE,QAAQ,EAAE,OAAO,SAAS,OAAO,MAAM,GAAG,gBAAgB,SAAS,eAAe;AAAA,EAC7F;AAAA,EAEA,MAAM,SAAS,QAA6F;AAC1G,UAAM,WAAW,MAAM,KAAK;AAAA,MAAwB,MAClD,KAAK,QAAQ;AAAA,QACX;AAAA,UACE,QAAQ;AAAA,UACR,YAAY,KAAK;AAAA,UACjB,UAAU,KAAK;AAAA,UACf,WAAW,KAAK;AAAA,UAChB,UAAU,OAAO;AAAA,UACjB,OAAO,KAAK,UAAU,OAAO,SAAS;AAAA,UACtC,SAAS;AAAA,QACX;AAAA,QACA,OAAM,SAAQ;AACZ,gBAAM,SAAS,MAAM,KAAK,WAAW,SAAS,MAAM;AACpD,eAAK,UAAU,KAAK,UAAU,MAAM,CAAC;AACrC,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AACA,QAAI,kBAAkB,UAAU;AAC9B,aAAO;AAAA,IACT;AACA,WAAO,EAAE,QAAQ,SAAS,QAAQ,gBAAgB,SAAS,eAAe;AAAA,EAC5E;AAAA,EAEA,aAAa,QAAmC,wBAAiE;AAC/G,SAAK;AACL,WAAO,QAAQ,QAAQ;AAAA,MACrB,MAAM;AAAA,MACN,oBAAoB,OAAO;AAAA,MAC3B,eAAe,KAAK;AAAA,MACpB,iBAAiB,KAAK;AAAA,MACtB,sBAAsB;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,gBAAgB,SAAyE;AACrG,QAAI,KAAK,aAAa;AACpB,aAAO;AAAA,IACT;AACA,QAAI,KAAK,gBAAgB;AAEvB,YAAM,KAAK;AACX,aAAO;AAAA,IACT;AAEA,UAAM,oBAAoB,KAAK;AAC/B,SAAK,kBAAkB,YAAoD;AACzE,UAAI;AACJ,UAAI;AACF,cAAM,KAAK,wBAAwB;AACnC,qBAAa,MAAM,KAAK,QAAQ;AAAA,UAC9B,EAAE,QAAQ,cAAc,YAAY,KAAK,MAAM,UAAU,KAAK,IAAI,WAAW,KAAK,UAAU,SAAS,KAAK;AAAA,UAC1G,OAAM,SAAQ;AACZ,kBAAM,OAAO,MAAM,iBAAiB;AAAA,cAClC,KAAK,KAAK;AAAA,cACV;AAAA,cACA,WAAW,KAAK,aAAa;AAAA;AAAA,cAE7B,oBAAoB,KAAK;AAAA,cACzB,kBAAkB,KAAK;AAAA,cACvB,kBAAkB,KAAK;AAAA,cACvB,kBAAkB,KAAK;AAAA,cACvB,QAAQ,KAAK;AAAA,cACb,SAAS,MAAM;AACb,qBAAK,cAAc;AAAA,cACrB;AAAA,cACA,SAAS,WAAS;AAChB,sBAAM,SAAS,sBAAsB,KAAK;AAC1C,sBAAM,MAAM,iCAAiC,KAAK,IAAI;AACtD,oBAAI,OAAO,MAAM,SAAS,cAAc,GAAG;AACzC,uBAAK,OAAO,KAAK,KAAK,MAAM;AAAA,gBAC9B,OAAO;AACL,uBAAK,OAAO,MAAM,KAAK,MAAM;AAAA,gBAC/B;AAAA,cACF;AAAA,YACF,CAAC;AACD,iBAAK,UAAU,KAAK,UAAU,EAAE,WAAW,KAAK,eAAe,UAAU,KAAK,cAAc,KAAK,CAAC,CAAC;AACnG,mBAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd,cAAM,KAAK,wBAAwB;AACnC,cAAM,KAAK,eAAe,KAAK;AAAA,MACjC;AACA,WAAK,cAAc;AACnB,WAAK,wBAAwB,WAAW;AACxC,WAAK,cAAc;AACnB,WAAK,YAAY,WAAW;AAC5B,UAAI,sBAAsB,KAAK,WAAW;AACxC,eAAO;AAAA,MACT;AACA,aAAO;AAAA,QACL,MAAM,KAAK;AAAA,QACX,IAAI,KAAK;AAAA,QACT,YAAY,KAAK,aAAa;AAAA,QAC9B,gBAAgB,KAAK;AAAA,MACvB;AAAA,IACF,GAAG,EAAE,QAAQ,MAAM;AACjB,WAAK,iBAAiB;AAAA,IACxB,CAAC;AACD,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,eAAe,OAAoC;AACzD,UAAM,aAAa,iBAAiB,qBAAqB,MAAM,aAAa;AAC5E,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,WAAO,IAAI,mBAAmB,2CAA2C,KAAK,IAAI,MAAM,OAAO,IAAI,YAAY;AAAA,MAC7G,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;","names":[]}
@@ -44,7 +44,7 @@ export declare class ToolSelectorPolicy {
44
44
  allowedNamesForSandbox(): string[] | undefined;
45
45
  /**
46
46
  * Whether the tool requires human approval. Honors `requireApprovalForTools` exactly: omit /
47
- * undefined defaults to write+destructive (constructor); an explicit list — including empty —
47
+ * undefined defaults to `@destructive` (constructor); an explicit list — including empty —
48
48
  * is exact.
49
49
  */
50
50
  requiresApproval(toolName: string, annotations: ToolAnnotations | undefined): boolean;
@@ -96,7 +96,7 @@ var ToolSelectorPolicy = class {
96
96
  }
97
97
  /**
98
98
  * Whether the tool requires human approval. Honors `requireApprovalForTools` exactly: omit /
99
- * undefined defaults to write+destructive (constructor); an explicit list — including empty —
99
+ * undefined defaults to `@destructive` (constructor); an explicit list — including empty —
100
100
  * is exact.
101
101
  */
102
102
  requiresApproval(toolName, annotations) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/mcp/ToolSelectorPolicy.ts"],"sourcesContent":["import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolSchema } from './IMCPServer';\nimport {\n DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS,\n isToolAllowed,\n literalToolNames,\n selectorsIncludeAll,\n toolMatchesAnySelector,\n toolRequiresApproval,\n} from './toolSelectors';\n\n/**\n * Per-server tool selectors. Enable/disable/preload tags: `@all`, `@read-only`.\n * Approval tags: `@all`, `@write`, `@destructive`.\n */\nexport interface ToolSelectorConfig {\n enableTools: string[];\n disableTools: string[];\n preloadTools: string[];\n requireApprovalForTools?: string[] | undefined;\n}\n\n/**\n * Encapsulates enable/disable/preload/approval selector policy for a single MCP server. Pure: it\n * never throws or does IO — callers decide how to surface a disallowed/missing tool so each can\n * raise its own error type.\n */\nexport class ToolSelectorPolicy {\n readonly preload: boolean;\n readonly hasPreloadedTools: boolean;\n\n private readonly enableTools: string[];\n private readonly disableTools: string[];\n private readonly preloadTools: string[];\n private readonly requireApprovalForTools: string[];\n // Tool names that survived enable/disable filtering on the most recent filterAndAnnotate() call.\n // Used by the sandbox allow-list and as the fast path for callTool gating.\n private resolvedAllowedToolNames: string[] | undefined;\n\n constructor(params: { selectors: ToolSelectorConfig; preload: boolean }) {\n this.enableTools = params.selectors.enableTools;\n this.disableTools = params.selectors.disableTools;\n this.preloadTools = params.selectors.preloadTools;\n this.requireApprovalForTools = params.selectors.requireApprovalForTools ?? DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS;\n this.preload = params.preload;\n this.hasPreloadedTools = params.preload || this.preloadTools.length > 0;\n }\n\n /** Literal enable_tools names that are not present on the server (caller throws if non-empty). */\n missingEnableLiterals(tools: AgentToolSchema[]): string[] {\n const availableNames = new Set(tools.map(t => t.name));\n return literalToolNames(this.enableTools).filter(name => !availableNames.has(name));\n }\n\n /** Filter by enable/disable, record the resolved allow-list, and annotate per-tool preload. */\n filterAndAnnotate(tools: AgentToolSchema[]): AgentToolSchema[] {\n const filtered = tools.filter(tool =>\n isToolAllowed({\n toolName: tool.name,\n annotations: tool.annotations,\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n }),\n );\n this.resolvedAllowedToolNames = filtered.map(t => t.name);\n return filtered.map(tool => ({ ...tool, preload: this.isToolPreloaded(tool.name, tool.annotations) }));\n }\n\n private isToolPreloaded(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n // Fully-preloaded server: every allowed tool is loaded into context.\n if (this.preload) {\n return true;\n }\n // Otherwise only the tools matching `preload_tools` stay eager.\n return toolMatchesAnySelector(toolName, annotations, this.preloadTools);\n }\n\n /**\n * Whether a tool may be called. Uses the resolved allow-list once listTools() has run; before that\n * falls back to matching against the raw selectors, resolving annotations lazily only when needed.\n */\n async isAllowed(toolName: string, resolveAnnotations: () => Promise<ToolAnnotations | undefined>): Promise<boolean> {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames.includes(toolName);\n }\n return isToolAllowed({\n toolName,\n annotations: await resolveAnnotations(),\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n });\n }\n\n /**\n * Best-effort allow-list for the sandbox. After a listTools() call returns the concrete names;\n * before that, `@all` with no disables means \"unrestricted\" (undefined), else the literal names.\n */\n allowedNamesForSandbox(): string[] | undefined {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames;\n }\n if (selectorsIncludeAll(this.enableTools) && this.disableTools.length === 0) {\n return undefined;\n }\n return literalToolNames(this.enableTools);\n }\n\n /**\n * Whether the tool requires human approval. Honors `requireApprovalForTools` exactly: omit /\n * undefined defaults to write+destructive (constructor); an explicit list — including empty —\n * is exact.\n */\n requiresApproval(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n return toolRequiresApproval(toolName, annotations, this.requireApprovalForTools);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,2BAOO;AAkBA,IAAM,qBAAN,MAAyB;AAAA,EACrB;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGT;AAAA,EAER,YAAY,QAA6D;AACvE,SAAK,cAAc,OAAO,UAAU;AACpC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,0BAA0B,OAAO,UAAU,2BAA2B;AAC3E,SAAK,UAAU,OAAO;AACtB,SAAK,oBAAoB,OAAO,WAAW,KAAK,aAAa,SAAS;AAAA,EACxE;AAAA;AAAA,EAGA,sBAAsB,OAAoC;AACxD,UAAM,iBAAiB,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AACrD,eAAO,uCAAiB,KAAK,WAAW,EAAE,OAAO,UAAQ,CAAC,eAAe,IAAI,IAAI,CAAC;AAAA,EACpF;AAAA;AAAA,EAGA,kBAAkB,OAA6C;AAC7D,UAAM,WAAW,MAAM;AAAA,MAAO,cAC5B,oCAAc;AAAA,QACZ,UAAU,KAAK;AAAA,QACf,aAAa,KAAK;AAAA,QAClB,iBAAiB,KAAK;AAAA,QACtB,kBAAkB,KAAK;AAAA,MACzB,CAAC;AAAA,IACH;AACA,SAAK,2BAA2B,SAAS,IAAI,OAAK,EAAE,IAAI;AACxD,WAAO,SAAS,IAAI,WAAS,EAAE,GAAG,MAAM,SAAS,KAAK,gBAAgB,KAAK,MAAM,KAAK,WAAW,EAAE,EAAE;AAAA,EACvG;AAAA,EAEQ,gBAAgB,UAAkB,aAAmD;AAE3F,QAAI,KAAK,SAAS;AAChB,aAAO;AAAA,IACT;AAEA,eAAO,6CAAuB,UAAU,aAAa,KAAK,YAAY;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAU,UAAkB,oBAAkF;AAClH,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK,yBAAyB,SAAS,QAAQ;AAAA,IACxD;AACA,eAAO,oCAAc;AAAA,MACnB;AAAA,MACA,aAAa,MAAM,mBAAmB;AAAA,MACtC,iBAAiB,KAAK;AAAA,MACtB,kBAAkB,KAAK;AAAA,IACzB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,yBAA+C;AAC7C,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK;AAAA,IACd;AACA,YAAI,0CAAoB,KAAK,WAAW,KAAK,KAAK,aAAa,WAAW,GAAG;AAC3E,aAAO;AAAA,IACT;AACA,eAAO,uCAAiB,KAAK,WAAW;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,UAAkB,aAAmD;AACpF,eAAO,2CAAqB,UAAU,aAAa,KAAK,uBAAuB;AAAA,EACjF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/core/mcp/ToolSelectorPolicy.ts"],"sourcesContent":["import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolSchema } from './IMCPServer';\nimport {\n DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS,\n isToolAllowed,\n literalToolNames,\n selectorsIncludeAll,\n toolMatchesAnySelector,\n toolRequiresApproval,\n} from './toolSelectors';\n\n/**\n * Per-server tool selectors. Enable/disable/preload tags: `@all`, `@read-only`.\n * Approval tags: `@all`, `@write`, `@destructive`.\n */\nexport interface ToolSelectorConfig {\n enableTools: string[];\n disableTools: string[];\n preloadTools: string[];\n requireApprovalForTools?: string[] | undefined;\n}\n\n/**\n * Encapsulates enable/disable/preload/approval selector policy for a single MCP server. Pure: it\n * never throws or does IO — callers decide how to surface a disallowed/missing tool so each can\n * raise its own error type.\n */\nexport class ToolSelectorPolicy {\n readonly preload: boolean;\n readonly hasPreloadedTools: boolean;\n\n private readonly enableTools: string[];\n private readonly disableTools: string[];\n private readonly preloadTools: string[];\n private readonly requireApprovalForTools: string[];\n // Tool names that survived enable/disable filtering on the most recent filterAndAnnotate() call.\n // Used by the sandbox allow-list and as the fast path for callTool gating.\n private resolvedAllowedToolNames: string[] | undefined;\n\n constructor(params: { selectors: ToolSelectorConfig; preload: boolean }) {\n this.enableTools = params.selectors.enableTools;\n this.disableTools = params.selectors.disableTools;\n this.preloadTools = params.selectors.preloadTools;\n this.requireApprovalForTools = params.selectors.requireApprovalForTools ?? DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS;\n this.preload = params.preload;\n this.hasPreloadedTools = params.preload || this.preloadTools.length > 0;\n }\n\n /** Literal enable_tools names that are not present on the server (caller throws if non-empty). */\n missingEnableLiterals(tools: AgentToolSchema[]): string[] {\n const availableNames = new Set(tools.map(t => t.name));\n return literalToolNames(this.enableTools).filter(name => !availableNames.has(name));\n }\n\n /** Filter by enable/disable, record the resolved allow-list, and annotate per-tool preload. */\n filterAndAnnotate(tools: AgentToolSchema[]): AgentToolSchema[] {\n const filtered = tools.filter(tool =>\n isToolAllowed({\n toolName: tool.name,\n annotations: tool.annotations,\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n }),\n );\n this.resolvedAllowedToolNames = filtered.map(t => t.name);\n return filtered.map(tool => ({ ...tool, preload: this.isToolPreloaded(tool.name, tool.annotations) }));\n }\n\n private isToolPreloaded(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n // Fully-preloaded server: every allowed tool is loaded into context.\n if (this.preload) {\n return true;\n }\n // Otherwise only the tools matching `preload_tools` stay eager.\n return toolMatchesAnySelector(toolName, annotations, this.preloadTools);\n }\n\n /**\n * Whether a tool may be called. Uses the resolved allow-list once listTools() has run; before that\n * falls back to matching against the raw selectors, resolving annotations lazily only when needed.\n */\n async isAllowed(toolName: string, resolveAnnotations: () => Promise<ToolAnnotations | undefined>): Promise<boolean> {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames.includes(toolName);\n }\n return isToolAllowed({\n toolName,\n annotations: await resolveAnnotations(),\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n });\n }\n\n /**\n * Best-effort allow-list for the sandbox. After a listTools() call returns the concrete names;\n * before that, `@all` with no disables means \"unrestricted\" (undefined), else the literal names.\n */\n allowedNamesForSandbox(): string[] | undefined {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames;\n }\n if (selectorsIncludeAll(this.enableTools) && this.disableTools.length === 0) {\n return undefined;\n }\n return literalToolNames(this.enableTools);\n }\n\n /**\n * Whether the tool requires human approval. Honors `requireApprovalForTools` exactly: omit /\n * undefined defaults to `@destructive` (constructor); an explicit list — including empty —\n * is exact.\n */\n requiresApproval(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n return toolRequiresApproval(toolName, annotations, this.requireApprovalForTools);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,2BAOO;AAkBA,IAAM,qBAAN,MAAyB;AAAA,EACrB;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGT;AAAA,EAER,YAAY,QAA6D;AACvE,SAAK,cAAc,OAAO,UAAU;AACpC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,0BAA0B,OAAO,UAAU,2BAA2B;AAC3E,SAAK,UAAU,OAAO;AACtB,SAAK,oBAAoB,OAAO,WAAW,KAAK,aAAa,SAAS;AAAA,EACxE;AAAA;AAAA,EAGA,sBAAsB,OAAoC;AACxD,UAAM,iBAAiB,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AACrD,eAAO,uCAAiB,KAAK,WAAW,EAAE,OAAO,UAAQ,CAAC,eAAe,IAAI,IAAI,CAAC;AAAA,EACpF;AAAA;AAAA,EAGA,kBAAkB,OAA6C;AAC7D,UAAM,WAAW,MAAM;AAAA,MAAO,cAC5B,oCAAc;AAAA,QACZ,UAAU,KAAK;AAAA,QACf,aAAa,KAAK;AAAA,QAClB,iBAAiB,KAAK;AAAA,QACtB,kBAAkB,KAAK;AAAA,MACzB,CAAC;AAAA,IACH;AACA,SAAK,2BAA2B,SAAS,IAAI,OAAK,EAAE,IAAI;AACxD,WAAO,SAAS,IAAI,WAAS,EAAE,GAAG,MAAM,SAAS,KAAK,gBAAgB,KAAK,MAAM,KAAK,WAAW,EAAE,EAAE;AAAA,EACvG;AAAA,EAEQ,gBAAgB,UAAkB,aAAmD;AAE3F,QAAI,KAAK,SAAS;AAChB,aAAO;AAAA,IACT;AAEA,eAAO,6CAAuB,UAAU,aAAa,KAAK,YAAY;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAU,UAAkB,oBAAkF;AAClH,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK,yBAAyB,SAAS,QAAQ;AAAA,IACxD;AACA,eAAO,oCAAc;AAAA,MACnB;AAAA,MACA,aAAa,MAAM,mBAAmB;AAAA,MACtC,iBAAiB,KAAK;AAAA,MACtB,kBAAkB,KAAK;AAAA,IACzB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,yBAA+C;AAC7C,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK;AAAA,IACd;AACA,YAAI,0CAAoB,KAAK,WAAW,KAAK,KAAK,aAAa,WAAW,GAAG;AAC3E,aAAO;AAAA,IACT;AACA,eAAO,uCAAiB,KAAK,WAAW;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,UAAkB,aAAmD;AACpF,eAAO,2CAAqB,UAAU,aAAa,KAAK,uBAAuB;AAAA,EACjF;AACF;","names":[]}
@@ -79,7 +79,7 @@ var ToolSelectorPolicy = class {
79
79
  }
80
80
  /**
81
81
  * Whether the tool requires human approval. Honors `requireApprovalForTools` exactly: omit /
82
- * undefined defaults to write+destructive (constructor); an explicit list — including empty —
82
+ * undefined defaults to `@destructive` (constructor); an explicit list — including empty —
83
83
  * is exact.
84
84
  */
85
85
  requiresApproval(toolName, annotations) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/mcp/ToolSelectorPolicy.ts"],"sourcesContent":["import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolSchema } from './IMCPServer';\nimport {\n DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS,\n isToolAllowed,\n literalToolNames,\n selectorsIncludeAll,\n toolMatchesAnySelector,\n toolRequiresApproval,\n} from './toolSelectors';\n\n/**\n * Per-server tool selectors. Enable/disable/preload tags: `@all`, `@read-only`.\n * Approval tags: `@all`, `@write`, `@destructive`.\n */\nexport interface ToolSelectorConfig {\n enableTools: string[];\n disableTools: string[];\n preloadTools: string[];\n requireApprovalForTools?: string[] | undefined;\n}\n\n/**\n * Encapsulates enable/disable/preload/approval selector policy for a single MCP server. Pure: it\n * never throws or does IO — callers decide how to surface a disallowed/missing tool so each can\n * raise its own error type.\n */\nexport class ToolSelectorPolicy {\n readonly preload: boolean;\n readonly hasPreloadedTools: boolean;\n\n private readonly enableTools: string[];\n private readonly disableTools: string[];\n private readonly preloadTools: string[];\n private readonly requireApprovalForTools: string[];\n // Tool names that survived enable/disable filtering on the most recent filterAndAnnotate() call.\n // Used by the sandbox allow-list and as the fast path for callTool gating.\n private resolvedAllowedToolNames: string[] | undefined;\n\n constructor(params: { selectors: ToolSelectorConfig; preload: boolean }) {\n this.enableTools = params.selectors.enableTools;\n this.disableTools = params.selectors.disableTools;\n this.preloadTools = params.selectors.preloadTools;\n this.requireApprovalForTools = params.selectors.requireApprovalForTools ?? DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS;\n this.preload = params.preload;\n this.hasPreloadedTools = params.preload || this.preloadTools.length > 0;\n }\n\n /** Literal enable_tools names that are not present on the server (caller throws if non-empty). */\n missingEnableLiterals(tools: AgentToolSchema[]): string[] {\n const availableNames = new Set(tools.map(t => t.name));\n return literalToolNames(this.enableTools).filter(name => !availableNames.has(name));\n }\n\n /** Filter by enable/disable, record the resolved allow-list, and annotate per-tool preload. */\n filterAndAnnotate(tools: AgentToolSchema[]): AgentToolSchema[] {\n const filtered = tools.filter(tool =>\n isToolAllowed({\n toolName: tool.name,\n annotations: tool.annotations,\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n }),\n );\n this.resolvedAllowedToolNames = filtered.map(t => t.name);\n return filtered.map(tool => ({ ...tool, preload: this.isToolPreloaded(tool.name, tool.annotations) }));\n }\n\n private isToolPreloaded(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n // Fully-preloaded server: every allowed tool is loaded into context.\n if (this.preload) {\n return true;\n }\n // Otherwise only the tools matching `preload_tools` stay eager.\n return toolMatchesAnySelector(toolName, annotations, this.preloadTools);\n }\n\n /**\n * Whether a tool may be called. Uses the resolved allow-list once listTools() has run; before that\n * falls back to matching against the raw selectors, resolving annotations lazily only when needed.\n */\n async isAllowed(toolName: string, resolveAnnotations: () => Promise<ToolAnnotations | undefined>): Promise<boolean> {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames.includes(toolName);\n }\n return isToolAllowed({\n toolName,\n annotations: await resolveAnnotations(),\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n });\n }\n\n /**\n * Best-effort allow-list for the sandbox. After a listTools() call returns the concrete names;\n * before that, `@all` with no disables means \"unrestricted\" (undefined), else the literal names.\n */\n allowedNamesForSandbox(): string[] | undefined {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames;\n }\n if (selectorsIncludeAll(this.enableTools) && this.disableTools.length === 0) {\n return undefined;\n }\n return literalToolNames(this.enableTools);\n }\n\n /**\n * Whether the tool requires human approval. Honors `requireApprovalForTools` exactly: omit /\n * undefined defaults to write+destructive (constructor); an explicit list — including empty —\n * is exact.\n */\n requiresApproval(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n return toolRequiresApproval(toolName, annotations, this.requireApprovalForTools);\n }\n}\n"],"mappings":";AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAkBA,IAAM,qBAAN,MAAyB;AAAA,EACrB;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGT;AAAA,EAER,YAAY,QAA6D;AACvE,SAAK,cAAc,OAAO,UAAU;AACpC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,0BAA0B,OAAO,UAAU,2BAA2B;AAC3E,SAAK,UAAU,OAAO;AACtB,SAAK,oBAAoB,OAAO,WAAW,KAAK,aAAa,SAAS;AAAA,EACxE;AAAA;AAAA,EAGA,sBAAsB,OAAoC;AACxD,UAAM,iBAAiB,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AACrD,WAAO,iBAAiB,KAAK,WAAW,EAAE,OAAO,UAAQ,CAAC,eAAe,IAAI,IAAI,CAAC;AAAA,EACpF;AAAA;AAAA,EAGA,kBAAkB,OAA6C;AAC7D,UAAM,WAAW,MAAM;AAAA,MAAO,UAC5B,cAAc;AAAA,QACZ,UAAU,KAAK;AAAA,QACf,aAAa,KAAK;AAAA,QAClB,iBAAiB,KAAK;AAAA,QACtB,kBAAkB,KAAK;AAAA,MACzB,CAAC;AAAA,IACH;AACA,SAAK,2BAA2B,SAAS,IAAI,OAAK,EAAE,IAAI;AACxD,WAAO,SAAS,IAAI,WAAS,EAAE,GAAG,MAAM,SAAS,KAAK,gBAAgB,KAAK,MAAM,KAAK,WAAW,EAAE,EAAE;AAAA,EACvG;AAAA,EAEQ,gBAAgB,UAAkB,aAAmD;AAE3F,QAAI,KAAK,SAAS;AAChB,aAAO;AAAA,IACT;AAEA,WAAO,uBAAuB,UAAU,aAAa,KAAK,YAAY;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAU,UAAkB,oBAAkF;AAClH,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK,yBAAyB,SAAS,QAAQ;AAAA,IACxD;AACA,WAAO,cAAc;AAAA,MACnB;AAAA,MACA,aAAa,MAAM,mBAAmB;AAAA,MACtC,iBAAiB,KAAK;AAAA,MACtB,kBAAkB,KAAK;AAAA,IACzB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,yBAA+C;AAC7C,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK;AAAA,IACd;AACA,QAAI,oBAAoB,KAAK,WAAW,KAAK,KAAK,aAAa,WAAW,GAAG;AAC3E,aAAO;AAAA,IACT;AACA,WAAO,iBAAiB,KAAK,WAAW;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,UAAkB,aAAmD;AACpF,WAAO,qBAAqB,UAAU,aAAa,KAAK,uBAAuB;AAAA,EACjF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/core/mcp/ToolSelectorPolicy.ts"],"sourcesContent":["import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';\nimport type { AgentToolSchema } from './IMCPServer';\nimport {\n DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS,\n isToolAllowed,\n literalToolNames,\n selectorsIncludeAll,\n toolMatchesAnySelector,\n toolRequiresApproval,\n} from './toolSelectors';\n\n/**\n * Per-server tool selectors. Enable/disable/preload tags: `@all`, `@read-only`.\n * Approval tags: `@all`, `@write`, `@destructive`.\n */\nexport interface ToolSelectorConfig {\n enableTools: string[];\n disableTools: string[];\n preloadTools: string[];\n requireApprovalForTools?: string[] | undefined;\n}\n\n/**\n * Encapsulates enable/disable/preload/approval selector policy for a single MCP server. Pure: it\n * never throws or does IO — callers decide how to surface a disallowed/missing tool so each can\n * raise its own error type.\n */\nexport class ToolSelectorPolicy {\n readonly preload: boolean;\n readonly hasPreloadedTools: boolean;\n\n private readonly enableTools: string[];\n private readonly disableTools: string[];\n private readonly preloadTools: string[];\n private readonly requireApprovalForTools: string[];\n // Tool names that survived enable/disable filtering on the most recent filterAndAnnotate() call.\n // Used by the sandbox allow-list and as the fast path for callTool gating.\n private resolvedAllowedToolNames: string[] | undefined;\n\n constructor(params: { selectors: ToolSelectorConfig; preload: boolean }) {\n this.enableTools = params.selectors.enableTools;\n this.disableTools = params.selectors.disableTools;\n this.preloadTools = params.selectors.preloadTools;\n this.requireApprovalForTools = params.selectors.requireApprovalForTools ?? DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS;\n this.preload = params.preload;\n this.hasPreloadedTools = params.preload || this.preloadTools.length > 0;\n }\n\n /** Literal enable_tools names that are not present on the server (caller throws if non-empty). */\n missingEnableLiterals(tools: AgentToolSchema[]): string[] {\n const availableNames = new Set(tools.map(t => t.name));\n return literalToolNames(this.enableTools).filter(name => !availableNames.has(name));\n }\n\n /** Filter by enable/disable, record the resolved allow-list, and annotate per-tool preload. */\n filterAndAnnotate(tools: AgentToolSchema[]): AgentToolSchema[] {\n const filtered = tools.filter(tool =>\n isToolAllowed({\n toolName: tool.name,\n annotations: tool.annotations,\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n }),\n );\n this.resolvedAllowedToolNames = filtered.map(t => t.name);\n return filtered.map(tool => ({ ...tool, preload: this.isToolPreloaded(tool.name, tool.annotations) }));\n }\n\n private isToolPreloaded(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n // Fully-preloaded server: every allowed tool is loaded into context.\n if (this.preload) {\n return true;\n }\n // Otherwise only the tools matching `preload_tools` stay eager.\n return toolMatchesAnySelector(toolName, annotations, this.preloadTools);\n }\n\n /**\n * Whether a tool may be called. Uses the resolved allow-list once listTools() has run; before that\n * falls back to matching against the raw selectors, resolving annotations lazily only when needed.\n */\n async isAllowed(toolName: string, resolveAnnotations: () => Promise<ToolAnnotations | undefined>): Promise<boolean> {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames.includes(toolName);\n }\n return isToolAllowed({\n toolName,\n annotations: await resolveAnnotations(),\n enableSelectors: this.enableTools,\n disableSelectors: this.disableTools,\n });\n }\n\n /**\n * Best-effort allow-list for the sandbox. After a listTools() call returns the concrete names;\n * before that, `@all` with no disables means \"unrestricted\" (undefined), else the literal names.\n */\n allowedNamesForSandbox(): string[] | undefined {\n if (this.resolvedAllowedToolNames) {\n return this.resolvedAllowedToolNames;\n }\n if (selectorsIncludeAll(this.enableTools) && this.disableTools.length === 0) {\n return undefined;\n }\n return literalToolNames(this.enableTools);\n }\n\n /**\n * Whether the tool requires human approval. Honors `requireApprovalForTools` exactly: omit /\n * undefined defaults to `@destructive` (constructor); an explicit list — including empty —\n * is exact.\n */\n requiresApproval(toolName: string, annotations: ToolAnnotations | undefined): boolean {\n return toolRequiresApproval(toolName, annotations, this.requireApprovalForTools);\n }\n}\n"],"mappings":";AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAkBA,IAAM,qBAAN,MAAyB;AAAA,EACrB;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGT;AAAA,EAER,YAAY,QAA6D;AACvE,SAAK,cAAc,OAAO,UAAU;AACpC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,eAAe,OAAO,UAAU;AACrC,SAAK,0BAA0B,OAAO,UAAU,2BAA2B;AAC3E,SAAK,UAAU,OAAO;AACtB,SAAK,oBAAoB,OAAO,WAAW,KAAK,aAAa,SAAS;AAAA,EACxE;AAAA;AAAA,EAGA,sBAAsB,OAAoC;AACxD,UAAM,iBAAiB,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AACrD,WAAO,iBAAiB,KAAK,WAAW,EAAE,OAAO,UAAQ,CAAC,eAAe,IAAI,IAAI,CAAC;AAAA,EACpF;AAAA;AAAA,EAGA,kBAAkB,OAA6C;AAC7D,UAAM,WAAW,MAAM;AAAA,MAAO,UAC5B,cAAc;AAAA,QACZ,UAAU,KAAK;AAAA,QACf,aAAa,KAAK;AAAA,QAClB,iBAAiB,KAAK;AAAA,QACtB,kBAAkB,KAAK;AAAA,MACzB,CAAC;AAAA,IACH;AACA,SAAK,2BAA2B,SAAS,IAAI,OAAK,EAAE,IAAI;AACxD,WAAO,SAAS,IAAI,WAAS,EAAE,GAAG,MAAM,SAAS,KAAK,gBAAgB,KAAK,MAAM,KAAK,WAAW,EAAE,EAAE;AAAA,EACvG;AAAA,EAEQ,gBAAgB,UAAkB,aAAmD;AAE3F,QAAI,KAAK,SAAS;AAChB,aAAO;AAAA,IACT;AAEA,WAAO,uBAAuB,UAAU,aAAa,KAAK,YAAY;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAU,UAAkB,oBAAkF;AAClH,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK,yBAAyB,SAAS,QAAQ;AAAA,IACxD;AACA,WAAO,cAAc;AAAA,MACnB;AAAA,MACA,aAAa,MAAM,mBAAmB;AAAA,MACtC,iBAAiB,KAAK;AAAA,MACtB,kBAAkB,KAAK;AAAA,IACzB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,yBAA+C;AAC7C,QAAI,KAAK,0BAA0B;AACjC,aAAO,KAAK;AAAA,IACd;AACA,QAAI,oBAAoB,KAAK,WAAW,KAAK,KAAK,aAAa,WAAW,GAAG;AAC3E,aAAO;AAAA,IACT;AACA,WAAO,iBAAiB,KAAK,WAAW;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,UAAkB,aAAmD;AACpF,WAAO,qBAAqB,UAAU,aAAa,KAAK,uBAAuB;AAAA,EACjF;AACF;","names":[]}
@@ -1,3 +1,4 @@
1
+ import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
1
2
  import type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
3
  import type { ToolSchema } from './IMCPServer';
3
4
  /** Networking for remote (url-based) MCP servers, kept separate so it can be mocked in tests. */
@@ -13,6 +14,9 @@ export interface RemoteMcpConnection {
13
14
  callTool(params: CallToolRequest['params']): Promise<CallToolResult>;
14
15
  close(): Promise<void>;
15
16
  }
17
+ export declare const DEFAULT_MAX_MCP_RESPONSE_BYTES: number;
18
+ /** GET SSE is long-lived and uncapped; every other body aborts at `maxBytes`. */
19
+ export declare function withMaxResponseBytes(fetchFn: FetchLike, maxBytes: number): FetchLike;
16
20
  export declare function isSessionExpiredError(error: unknown): boolean;
17
21
  /**
18
22
  * Connect to a remote MCP server, keeping the first transport that connects. `sessionId` is passed to
@@ -29,6 +33,7 @@ export declare function connectRemoteMcp(params: {
29
33
  knownTransportType?: RemoteMcpTransportType | undefined;
30
34
  requestTimeoutMs: number;
31
35
  connectTimeoutMs: number;
36
+ maxResponseBytes?: number | undefined;
32
37
  signal: AbortSignal;
33
38
  onClose?: (() => void) | undefined;
34
39
  onError?: ((error: Error) => void) | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"remoteMcpClient.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/remoteMcpClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAI1F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,iGAAiG;AAEjG,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,KAAK,CAAC;AAE/D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,aAAa,EAAE,sBAAsB,CAAC;IAC/C,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;IAC9F,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAoCD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQ7D;AAgED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,kBAAkB,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;IACxD,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAChD,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA4C/B"}
1
+ {"version":3,"file":"remoteMcpClient.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/remoteMcpClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAK1F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,iGAAiG;AAEjG,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,KAAK,CAAC;AAE/D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,aAAa,EAAE,sBAAsB,CAAC;IAC/C,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;IAC9F,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AASD,eAAO,MAAM,8BAA8B,QAAmB,CAAC;AAW/D,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,CA0BpF;AAkCD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQ7D;AAgED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,kBAAkB,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;IACxD,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAChD,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAoD/B"}
@@ -20,18 +20,50 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/core/mcp/remoteMcpClient.ts
21
21
  var remoteMcpClient_exports = {};
22
22
  __export(remoteMcpClient_exports, {
23
+ DEFAULT_MAX_MCP_RESPONSE_BYTES: () => DEFAULT_MAX_MCP_RESPONSE_BYTES,
23
24
  connectRemoteMcp: () => connectRemoteMcp,
24
- isSessionExpiredError: () => isSessionExpiredError
25
+ isSessionExpiredError: () => isSessionExpiredError,
26
+ withMaxResponseBytes: () => withMaxResponseBytes
25
27
  });
26
28
  module.exports = __toCommonJS(remoteMcpClient_exports);
27
29
  var import_client = require("@modelcontextprotocol/sdk/client/index.js");
28
30
  var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
29
31
  var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
30
32
  var import_api = require("@opentelemetry/api");
33
+ var import_undici = require("undici");
31
34
  var import_errors = require("../errors.js");
32
35
  var import_promiseUtils = require("../util/promiseUtils.js");
33
36
  var CLIENT_INFO = { name: "tfy-agent-mcp-client", version: "1.0.0" };
34
37
  var TRANSPORT_PROBE_ORDER = ["streamable-http", "sse"];
38
+ var DEFAULT_MAX_MCP_RESPONSE_BYTES = 50 * 1024 * 1024;
39
+ var MCP_BODY_TIMEOUT_MS = 30 * 60 * 1e3;
40
+ var mcpHttpAgent = new import_undici.Agent({ bodyTimeout: MCP_BODY_TIMEOUT_MS });
41
+ var mcpFetch = (url, init) => (0, import_undici.fetch)(typeof url === "string" ? url : url.href, { ...init, dispatcher: mcpHttpAgent });
42
+ function withMaxResponseBytes(fetchFn, maxBytes) {
43
+ return async (url, init) => {
44
+ const response = await fetchFn(url, init);
45
+ const isGetSse = (init?.method ?? "GET").toUpperCase() === "GET" && (response.headers.get("content-type") ?? "").toLowerCase().includes("text/event-stream");
46
+ if (isGetSse || !response.body) {
47
+ return response;
48
+ }
49
+ let seen = 0;
50
+ return new Response(
51
+ response.body.pipeThrough(
52
+ new TransformStream({
53
+ transform(chunk, controller) {
54
+ seen += chunk.byteLength;
55
+ if (seen > maxBytes) {
56
+ controller.error(new Error(`MCP response exceeded max ${String(maxBytes)} bytes`));
57
+ return;
58
+ }
59
+ controller.enqueue(chunk);
60
+ }
61
+ })
62
+ ),
63
+ { status: response.status, statusText: response.statusText, headers: response.headers }
64
+ );
65
+ };
66
+ }
35
67
  var McpClientWithTimeout = class extends import_client.Client {
36
68
  constructor(requestTimeoutMs) {
37
69
  super(CLIENT_INFO, { capabilities: {} });
@@ -44,12 +76,16 @@ var McpClientWithTimeout = class extends import_client.Client {
44
76
  return super.request(req, schema, { ...options, timeout: this.requestTimeoutMs });
45
77
  }
46
78
  };
47
- function createTransport(type, url, headers, sessionId) {
79
+ function createTransport(type, url, headers, fetchFn, sessionId) {
48
80
  const requestInit = { headers };
49
81
  if (type === "streamable-http") {
50
- return new import_streamableHttp.StreamableHTTPClientTransport(url, { requestInit, ...sessionId !== void 0 ? { sessionId } : {} });
82
+ return new import_streamableHttp.StreamableHTTPClientTransport(url, {
83
+ requestInit,
84
+ fetch: fetchFn,
85
+ ...sessionId !== void 0 ? { sessionId } : {}
86
+ });
51
87
  }
52
- return new import_sse.SSEClientTransport(url, { requestInit });
88
+ return new import_sse.SSEClientTransport(url, { requestInit, fetch: fetchFn });
53
89
  }
54
90
  function isSessionExpiredError(error) {
55
91
  if (!(error instanceof Error)) {
@@ -112,21 +148,25 @@ function buildConnection(client, transport, transportType, headers, requestOptio
112
148
  async function connectRemoteMcp(params) {
113
149
  const url = new URL(params.url);
114
150
  const requestOptions = { signal: params.signal };
151
+ const fetchFn = withMaxResponseBytes(mcpFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);
115
152
  const candidates = params.knownTransportType ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter((t) => t !== params.knownTransportType)] : TRANSPORT_PROBE_ORDER;
116
153
  const failures = [];
117
154
  for (const transportType of candidates) {
118
- const transport = createTransport(transportType, url, params.headers, params.sessionId);
155
+ const transport = createTransport(transportType, url, params.headers, fetchFn, params.sessionId);
119
156
  const client = new McpClientWithTimeout(params.requestTimeoutMs);
157
+ const timeout = new AbortController();
158
+ const connectOptions = { signal: AbortSignal.any([params.signal, timeout.signal]) };
120
159
  try {
121
160
  stampTraceHeaders(params.headers);
122
161
  await (0, import_promiseUtils.withTimeout)(
123
162
  // Concrete transports use sessionId: string|undefined; Transport uses an optional
124
163
  // property — exactOptionalPropertyTypes rejects assignability without this cast.
125
- client.connect(transport, requestOptions),
164
+ client.connect(transport, connectOptions),
126
165
  params.connectTimeoutMs,
127
166
  transportType
128
167
  );
129
168
  } catch (error) {
169
+ timeout.abort();
130
170
  await client.close().catch(() => {
131
171
  });
132
172
  if (isAuthError(error)) {
@@ -146,7 +186,9 @@ async function connectRemoteMcp(params) {
146
186
  }
147
187
  // Annotate the CommonJS export names for ESM import in node:
148
188
  0 && (module.exports = {
189
+ DEFAULT_MAX_MCP_RESPONSE_BYTES,
149
190
  connectRemoteMcp,
150
- isSessionExpiredError
191
+ isSessionExpiredError,
192
+ withMaxResponseBytes
151
193
  });
152
194
  //# sourceMappingURL=remoteMcpClient.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/mcp/remoteMcpClient.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\n// SSE remains required during the Streamable HTTP migration (some servers still speak SSE only).\n\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { context, propagation } from '@opentelemetry/api';\nimport { McpConnectionError } from '../errors';\nimport { withTimeout } from '../util/promiseUtils';\nimport type { ToolSchema } from './IMCPServer';\n\n/** Networking for remote (url-based) MCP servers, kept separate so it can be mocked in tests. */\n\nexport type RemoteMcpTransportType = 'streamable-http' | 'sse';\n\nexport interface RemoteMcpConnection {\n readonly transportType: RemoteMcpTransportType;\n /** Session id for a stateful server, or null for a stateless one. */\n readonly sessionId: string | null;\n listTools(cursor?: string): Promise<{ tools: ToolSchema[]; nextCursor?: string | undefined }>;\n callTool(params: CallToolRequest['params']): Promise<CallToolResult>;\n close(): Promise<void>;\n}\n\n// SSE transport type kept for dual-probe support during migration.\n// eslint-disable-next-line @typescript-eslint/no-deprecated -- see TRANSPORT_PROBE_ORDER\ntype McpTransport = StreamableHTTPClientTransport | SSEClientTransport;\n\nconst CLIENT_INFO = { name: 'tfy-agent-mcp-client', version: '1.0.0' } as const;\nconst TRANSPORT_PROBE_ORDER: RemoteMcpTransportType[] = ['streamable-http', 'sse'];\n\nclass McpClientWithTimeout extends Client {\n constructor(private readonly requestTimeoutMs: number) {\n super(CLIENT_INFO, { capabilities: {} });\n }\n\n // SDK Client.request is loosely typed; forward with an explicit timeout.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MCP SDK request typing\n override request(req: any, schema: any, options?: any): Promise<any> {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- MCP SDK request typing\n return super.request(req, schema, { ...options, timeout: this.requestTimeoutMs });\n }\n}\n\nfunction createTransport(\n type: RemoteMcpTransportType,\n url: URL,\n headers: Record<string, string>,\n sessionId?: string,\n): McpTransport {\n const requestInit = { headers };\n if (type === 'streamable-http') {\n return new StreamableHTTPClientTransport(url, { requestInit, ...(sessionId !== undefined ? { sessionId } : {}) });\n }\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- dual-transport probe; see TRANSPORT_PROBE_ORDER\n return new SSEClientTransport(url, { requestInit });\n}\n\nexport function isSessionExpiredError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 404) {\n return true;\n }\n return error.message.includes('HTTP 404') || error.message.toLowerCase().includes('session');\n}\n\nfunction isAuthError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 401) {\n return true;\n }\n return error.message.includes('HTTP 401');\n}\n\n/** Inject the active otel trace context so each request propagates its span. */\nfunction stampTraceHeaders(headers: Record<string, string>): void {\n propagation.inject(context.active(), headers);\n}\n\nfunction getTransportSessionId(transport: McpTransport): string | null {\n return transport instanceof StreamableHTTPClientTransport ? (transport.sessionId ?? null) : null;\n}\n\nfunction toConnectError(error: unknown): McpConnectionError {\n if (error instanceof McpConnectionError) {\n return error;\n }\n if (isAuthError(error)) {\n return new McpConnectionError('upstream returned 401 Unauthorized', 401, {\n cause: error,\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new McpConnectionError(message, 502, { cause: error });\n}\n\nfunction buildConnection(\n client: McpClientWithTimeout,\n transport: McpTransport,\n transportType: RemoteMcpTransportType,\n headers: Record<string, string>,\n requestOptions: { signal: AbortSignal },\n): RemoteMcpConnection {\n return {\n transportType,\n sessionId: getTransportSessionId(transport),\n listTools: async (cursor?: string) => {\n stampTraceHeaders(headers);\n const response = await client.listTools(cursor ? { cursor } : undefined, requestOptions);\n return {\n tools: response.tools,\n nextCursor: response.nextCursor,\n };\n },\n callTool: async (callParams: CallToolRequest['params']): Promise<CallToolResult> => {\n stampTraceHeaders(headers);\n return (await client.callTool(callParams, undefined, requestOptions)) as CallToolResult;\n },\n close: async (): Promise<void> => {\n await client.close().catch(() => {\n /* no-op */\n });\n },\n };\n}\n\n/**\n * Connect to a remote MCP server, keeping the first transport that connects. `sessionId` is passed to\n * every attempt so a stateful session resumes in place instead of opening a throwaway one.\n *\n * `knownTransportType` is a hint (from a prior connect / persisted state): it's tried first for a fast\n * path, but on failure we still fall back to probing the remaining transports, so a stale or wrong\n * hint (server switched transports, bad resume data) self-heals instead of failing every turn.\n */\nexport async function connectRemoteMcp(params: {\n url: string;\n headers: Record<string, string>;\n sessionId?: string | undefined;\n knownTransportType?: RemoteMcpTransportType | undefined;\n requestTimeoutMs: number;\n connectTimeoutMs: number;\n signal: AbortSignal;\n onClose?: (() => void) | undefined;\n onError?: ((error: Error) => void) | undefined;\n}): Promise<RemoteMcpConnection> {\n const url = new URL(params.url);\n const requestOptions = { signal: params.signal };\n const candidates = params.knownTransportType\n ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter(t => t !== params.knownTransportType)]\n : TRANSPORT_PROBE_ORDER;\n const failures: { transport: RemoteMcpTransportType; error: string }[] = [];\n\n for (const transportType of candidates) {\n const transport = createTransport(transportType, url, params.headers, params.sessionId);\n const client = new McpClientWithTimeout(params.requestTimeoutMs);\n try {\n stampTraceHeaders(params.headers);\n await withTimeout(\n // Concrete transports use sessionId: string|undefined; Transport uses an optional\n // property — exactOptionalPropertyTypes rejects assignability without this cast.\n client.connect(transport as Parameters<Client['connect']>[0], requestOptions),\n params.connectTimeoutMs,\n transportType,\n );\n } catch (error) {\n await client.close().catch(() => {\n /* no-op */\n });\n if (isAuthError(error)) {\n throw toConnectError(error);\n }\n // A session-expired error means the transport is right but the session is stale: surface it so\n // the caller reconnects fresh instead of falling through to a different transport.\n if (params.sessionId && isSessionExpiredError(error)) {\n throw toConnectError(error);\n }\n failures.push({ transport: transportType, error: error instanceof Error ? error.message.trim() : String(error) });\n continue;\n }\n\n // Set on the client (not the transport) so the SDK's own onclose/onerror cleanup still runs; the\n // SDK invokes these from inside it. Only wired on the kept connection, so failed attempts stay quiet.\n client.onclose = () => params.onClose?.();\n client.onerror = error => params.onError?.(error);\n return buildConnection(client, transport, transportType, params.headers, requestOptions);\n }\n\n throw new McpConnectionError(`failed to connect (tried ${candidates.join(', ')}): ${JSON.stringify(failures)}`, 502);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/core/mcp/remoteMcpClient.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\n// SSE remains required during the Streamable HTTP migration (some servers still speak SSE only).\n\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { context, propagation } from '@opentelemetry/api';\nimport { Agent, fetch as undiciFetch } from 'undici';\nimport { McpConnectionError } from '../errors';\nimport { withTimeout } from '../util/promiseUtils';\nimport type { ToolSchema } from './IMCPServer';\n\n/** Networking for remote (url-based) MCP servers, kept separate so it can be mocked in tests. */\n\nexport type RemoteMcpTransportType = 'streamable-http' | 'sse';\n\nexport interface RemoteMcpConnection {\n readonly transportType: RemoteMcpTransportType;\n /** Session id for a stateful server, or null for a stateless one. */\n readonly sessionId: string | null;\n listTools(cursor?: string): Promise<{ tools: ToolSchema[]; nextCursor?: string | undefined }>;\n callTool(params: CallToolRequest['params']): Promise<CallToolResult>;\n close(): Promise<void>;\n}\n\n// SSE transport type kept for dual-probe support during migration.\n// eslint-disable-next-line @typescript-eslint/no-deprecated -- see TRANSPORT_PROBE_ORDER\ntype McpTransport = StreamableHTTPClientTransport | SSEClientTransport;\n\nconst CLIENT_INFO = { name: 'tfy-agent-mcp-client', version: '1.0.0' } as const;\nconst TRANSPORT_PROBE_ORDER: RemoteMcpTransportType[] = ['streamable-http', 'sse'];\n\nexport const DEFAULT_MAX_MCP_RESPONSE_BYTES = 50 * 1024 * 1024;\n\n// MCP SSE/streamable-HTTP keeps a long-lived response open that is often idle between tool calls.\n// Node fetch (undici) defaults bodyTimeout to 300s of silence, then kills the stream with\n// `Body Timeout Error` — we reconnect and the ~5m cycle repeats in logs. 30m matches the\n// Gateway idle-body window; MCP request deadlines still come from requestTimeoutMs.\nconst MCP_BODY_TIMEOUT_MS = 30 * 60 * 1000;\nconst mcpHttpAgent = new Agent({ bodyTimeout: MCP_BODY_TIMEOUT_MS });\nconst mcpFetch: FetchLike = (url, init) =>\n undiciFetch(typeof url === 'string' ? url : url.href, { ...(init as object), dispatcher: mcpHttpAgent });\n\n/** GET SSE is long-lived and uncapped; every other body aborts at `maxBytes`. */\nexport function withMaxResponseBytes(fetchFn: FetchLike, maxBytes: number): FetchLike {\n return async (url, init) => {\n const response = await fetchFn(url, init);\n const isGetSse =\n (init?.method ?? 'GET').toUpperCase() === 'GET' &&\n (response.headers.get('content-type') ?? '').toLowerCase().includes('text/event-stream');\n if (isGetSse || !response.body) {\n return response;\n }\n let seen = 0;\n return new Response(\n response.body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n seen += chunk.byteLength;\n if (seen > maxBytes) {\n controller.error(new Error(`MCP response exceeded max ${String(maxBytes)} bytes`));\n return;\n }\n controller.enqueue(chunk);\n },\n }),\n ),\n { status: response.status, statusText: response.statusText, headers: response.headers },\n );\n };\n}\n\nclass McpClientWithTimeout extends Client {\n constructor(private readonly requestTimeoutMs: number) {\n super(CLIENT_INFO, { capabilities: {} });\n }\n\n // SDK Client.request is loosely typed; forward with an explicit timeout.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MCP SDK request typing\n override request(req: any, schema: any, options?: any): Promise<any> {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- MCP SDK request typing\n return super.request(req, schema, { ...options, timeout: this.requestTimeoutMs });\n }\n}\n\nfunction createTransport(\n type: RemoteMcpTransportType,\n url: URL,\n headers: Record<string, string>,\n fetchFn: FetchLike,\n sessionId?: string,\n): McpTransport {\n const requestInit = { headers };\n if (type === 'streamable-http') {\n return new StreamableHTTPClientTransport(url, {\n requestInit,\n fetch: fetchFn,\n ...(sessionId !== undefined ? { sessionId } : {}),\n });\n }\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- dual-transport probe; see TRANSPORT_PROBE_ORDER\n return new SSEClientTransport(url, { requestInit, fetch: fetchFn });\n}\n\nexport function isSessionExpiredError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 404) {\n return true;\n }\n return error.message.includes('HTTP 404') || error.message.toLowerCase().includes('session');\n}\n\nfunction isAuthError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 401) {\n return true;\n }\n return error.message.includes('HTTP 401');\n}\n\n/** Inject the active otel trace context so each request propagates its span. */\nfunction stampTraceHeaders(headers: Record<string, string>): void {\n propagation.inject(context.active(), headers);\n}\n\nfunction getTransportSessionId(transport: McpTransport): string | null {\n return transport instanceof StreamableHTTPClientTransport ? (transport.sessionId ?? null) : null;\n}\n\nfunction toConnectError(error: unknown): McpConnectionError {\n if (error instanceof McpConnectionError) {\n return error;\n }\n if (isAuthError(error)) {\n return new McpConnectionError('upstream returned 401 Unauthorized', 401, {\n cause: error,\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new McpConnectionError(message, 502, { cause: error });\n}\n\nfunction buildConnection(\n client: McpClientWithTimeout,\n transport: McpTransport,\n transportType: RemoteMcpTransportType,\n headers: Record<string, string>,\n requestOptions: { signal: AbortSignal },\n): RemoteMcpConnection {\n return {\n transportType,\n sessionId: getTransportSessionId(transport),\n listTools: async (cursor?: string) => {\n stampTraceHeaders(headers);\n const response = await client.listTools(cursor ? { cursor } : undefined, requestOptions);\n return {\n tools: response.tools,\n nextCursor: response.nextCursor,\n };\n },\n callTool: async (callParams: CallToolRequest['params']): Promise<CallToolResult> => {\n stampTraceHeaders(headers);\n return (await client.callTool(callParams, undefined, requestOptions)) as CallToolResult;\n },\n close: async (): Promise<void> => {\n await client.close().catch(() => {\n /* no-op */\n });\n },\n };\n}\n\n/**\n * Connect to a remote MCP server, keeping the first transport that connects. `sessionId` is passed to\n * every attempt so a stateful session resumes in place instead of opening a throwaway one.\n *\n * `knownTransportType` is a hint (from a prior connect / persisted state): it's tried first for a fast\n * path, but on failure we still fall back to probing the remaining transports, so a stale or wrong\n * hint (server switched transports, bad resume data) self-heals instead of failing every turn.\n */\nexport async function connectRemoteMcp(params: {\n url: string;\n headers: Record<string, string>;\n sessionId?: string | undefined;\n knownTransportType?: RemoteMcpTransportType | undefined;\n requestTimeoutMs: number;\n connectTimeoutMs: number;\n maxResponseBytes?: number | undefined;\n signal: AbortSignal;\n onClose?: (() => void) | undefined;\n onError?: ((error: Error) => void) | undefined;\n}): Promise<RemoteMcpConnection> {\n const url = new URL(params.url);\n const requestOptions = { signal: params.signal };\n const fetchFn = withMaxResponseBytes(mcpFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);\n const candidates = params.knownTransportType\n ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter(t => t !== params.knownTransportType)]\n : TRANSPORT_PROBE_ORDER;\n const failures: { transport: RemoteMcpTransportType; error: string }[] = [];\n\n for (const transportType of candidates) {\n const transport = createTransport(transportType, url, params.headers, fetchFn, params.sessionId);\n const client = new McpClientWithTimeout(params.requestTimeoutMs);\n // withTimeout races client.connect() and does not abort it, so timed-out connects can leak\n // sockets until GC. Abort this controller on timeout so the handshake is cancelled.\n // AbortSignal.timeout cannot be cleared, and the SDK keeps the signal on initialize, so it\n // would still fire connectTimeoutMs later and cancel the live client.\n const timeout = new AbortController();\n const connectOptions = { signal: AbortSignal.any([params.signal, timeout.signal]) };\n try {\n stampTraceHeaders(params.headers);\n await withTimeout(\n // Concrete transports use sessionId: string|undefined; Transport uses an optional\n // property — exactOptionalPropertyTypes rejects assignability without this cast.\n client.connect(transport as Parameters<Client['connect']>[0], connectOptions),\n params.connectTimeoutMs,\n transportType,\n );\n } catch (error) {\n timeout.abort();\n await client.close().catch(() => {\n /* no-op */\n });\n if (isAuthError(error)) {\n throw toConnectError(error);\n }\n // A session-expired error means the transport is right but the session is stale: surface it so\n // the caller reconnects fresh instead of falling through to a different transport.\n if (params.sessionId && isSessionExpiredError(error)) {\n throw toConnectError(error);\n }\n failures.push({ transport: transportType, error: error instanceof Error ? error.message.trim() : String(error) });\n continue;\n }\n\n // Set on the client (not the transport) so the SDK's own onclose/onerror cleanup still runs; the\n // SDK invokes these from inside it. Only wired on the kept connection, so failed attempts stay quiet.\n client.onclose = () => params.onClose?.();\n client.onerror = error => params.onError?.(error);\n return buildConnection(client, transport, transportType, params.headers, requestOptions);\n }\n\n throw new McpConnectionError(`failed to connect (tried ${candidates.join(', ')}): ${JSON.stringify(failures)}`, 502);\n}\n"],"mappings":"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","names":["undiciFetch"]}
@@ -3,10 +3,40 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
4
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
5
  import { context, propagation } from "@opentelemetry/api";
6
+ import { Agent, fetch as undiciFetch } from "undici";
6
7
  import { McpConnectionError } from "../errors.mjs";
7
8
  import { withTimeout } from "../util/promiseUtils.mjs";
8
9
  var CLIENT_INFO = { name: "tfy-agent-mcp-client", version: "1.0.0" };
9
10
  var TRANSPORT_PROBE_ORDER = ["streamable-http", "sse"];
11
+ var DEFAULT_MAX_MCP_RESPONSE_BYTES = 50 * 1024 * 1024;
12
+ var MCP_BODY_TIMEOUT_MS = 30 * 60 * 1e3;
13
+ var mcpHttpAgent = new Agent({ bodyTimeout: MCP_BODY_TIMEOUT_MS });
14
+ var mcpFetch = (url, init) => undiciFetch(typeof url === "string" ? url : url.href, { ...init, dispatcher: mcpHttpAgent });
15
+ function withMaxResponseBytes(fetchFn, maxBytes) {
16
+ return async (url, init) => {
17
+ const response = await fetchFn(url, init);
18
+ const isGetSse = (init?.method ?? "GET").toUpperCase() === "GET" && (response.headers.get("content-type") ?? "").toLowerCase().includes("text/event-stream");
19
+ if (isGetSse || !response.body) {
20
+ return response;
21
+ }
22
+ let seen = 0;
23
+ return new Response(
24
+ response.body.pipeThrough(
25
+ new TransformStream({
26
+ transform(chunk, controller) {
27
+ seen += chunk.byteLength;
28
+ if (seen > maxBytes) {
29
+ controller.error(new Error(`MCP response exceeded max ${String(maxBytes)} bytes`));
30
+ return;
31
+ }
32
+ controller.enqueue(chunk);
33
+ }
34
+ })
35
+ ),
36
+ { status: response.status, statusText: response.statusText, headers: response.headers }
37
+ );
38
+ };
39
+ }
10
40
  var McpClientWithTimeout = class extends Client {
11
41
  constructor(requestTimeoutMs) {
12
42
  super(CLIENT_INFO, { capabilities: {} });
@@ -19,12 +49,16 @@ var McpClientWithTimeout = class extends Client {
19
49
  return super.request(req, schema, { ...options, timeout: this.requestTimeoutMs });
20
50
  }
21
51
  };
22
- function createTransport(type, url, headers, sessionId) {
52
+ function createTransport(type, url, headers, fetchFn, sessionId) {
23
53
  const requestInit = { headers };
24
54
  if (type === "streamable-http") {
25
- return new StreamableHTTPClientTransport(url, { requestInit, ...sessionId !== void 0 ? { sessionId } : {} });
55
+ return new StreamableHTTPClientTransport(url, {
56
+ requestInit,
57
+ fetch: fetchFn,
58
+ ...sessionId !== void 0 ? { sessionId } : {}
59
+ });
26
60
  }
27
- return new SSEClientTransport(url, { requestInit });
61
+ return new SSEClientTransport(url, { requestInit, fetch: fetchFn });
28
62
  }
29
63
  function isSessionExpiredError(error) {
30
64
  if (!(error instanceof Error)) {
@@ -87,21 +121,25 @@ function buildConnection(client, transport, transportType, headers, requestOptio
87
121
  async function connectRemoteMcp(params) {
88
122
  const url = new URL(params.url);
89
123
  const requestOptions = { signal: params.signal };
124
+ const fetchFn = withMaxResponseBytes(mcpFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);
90
125
  const candidates = params.knownTransportType ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter((t) => t !== params.knownTransportType)] : TRANSPORT_PROBE_ORDER;
91
126
  const failures = [];
92
127
  for (const transportType of candidates) {
93
- const transport = createTransport(transportType, url, params.headers, params.sessionId);
128
+ const transport = createTransport(transportType, url, params.headers, fetchFn, params.sessionId);
94
129
  const client = new McpClientWithTimeout(params.requestTimeoutMs);
130
+ const timeout = new AbortController();
131
+ const connectOptions = { signal: AbortSignal.any([params.signal, timeout.signal]) };
95
132
  try {
96
133
  stampTraceHeaders(params.headers);
97
134
  await withTimeout(
98
135
  // Concrete transports use sessionId: string|undefined; Transport uses an optional
99
136
  // property — exactOptionalPropertyTypes rejects assignability without this cast.
100
- client.connect(transport, requestOptions),
137
+ client.connect(transport, connectOptions),
101
138
  params.connectTimeoutMs,
102
139
  transportType
103
140
  );
104
141
  } catch (error) {
142
+ timeout.abort();
105
143
  await client.close().catch(() => {
106
144
  });
107
145
  if (isAuthError(error)) {
@@ -120,7 +158,9 @@ async function connectRemoteMcp(params) {
120
158
  throw new McpConnectionError(`failed to connect (tried ${candidates.join(", ")}): ${JSON.stringify(failures)}`, 502);
121
159
  }
122
160
  export {
161
+ DEFAULT_MAX_MCP_RESPONSE_BYTES,
123
162
  connectRemoteMcp,
124
- isSessionExpiredError
163
+ isSessionExpiredError,
164
+ withMaxResponseBytes
125
165
  };
126
166
  //# sourceMappingURL=remoteMcpClient.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/mcp/remoteMcpClient.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\n// SSE remains required during the Streamable HTTP migration (some servers still speak SSE only).\n\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { context, propagation } from '@opentelemetry/api';\nimport { McpConnectionError } from '../errors';\nimport { withTimeout } from '../util/promiseUtils';\nimport type { ToolSchema } from './IMCPServer';\n\n/** Networking for remote (url-based) MCP servers, kept separate so it can be mocked in tests. */\n\nexport type RemoteMcpTransportType = 'streamable-http' | 'sse';\n\nexport interface RemoteMcpConnection {\n readonly transportType: RemoteMcpTransportType;\n /** Session id for a stateful server, or null for a stateless one. */\n readonly sessionId: string | null;\n listTools(cursor?: string): Promise<{ tools: ToolSchema[]; nextCursor?: string | undefined }>;\n callTool(params: CallToolRequest['params']): Promise<CallToolResult>;\n close(): Promise<void>;\n}\n\n// SSE transport type kept for dual-probe support during migration.\n// eslint-disable-next-line @typescript-eslint/no-deprecated -- see TRANSPORT_PROBE_ORDER\ntype McpTransport = StreamableHTTPClientTransport | SSEClientTransport;\n\nconst CLIENT_INFO = { name: 'tfy-agent-mcp-client', version: '1.0.0' } as const;\nconst TRANSPORT_PROBE_ORDER: RemoteMcpTransportType[] = ['streamable-http', 'sse'];\n\nclass McpClientWithTimeout extends Client {\n constructor(private readonly requestTimeoutMs: number) {\n super(CLIENT_INFO, { capabilities: {} });\n }\n\n // SDK Client.request is loosely typed; forward with an explicit timeout.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MCP SDK request typing\n override request(req: any, schema: any, options?: any): Promise<any> {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- MCP SDK request typing\n return super.request(req, schema, { ...options, timeout: this.requestTimeoutMs });\n }\n}\n\nfunction createTransport(\n type: RemoteMcpTransportType,\n url: URL,\n headers: Record<string, string>,\n sessionId?: string,\n): McpTransport {\n const requestInit = { headers };\n if (type === 'streamable-http') {\n return new StreamableHTTPClientTransport(url, { requestInit, ...(sessionId !== undefined ? { sessionId } : {}) });\n }\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- dual-transport probe; see TRANSPORT_PROBE_ORDER\n return new SSEClientTransport(url, { requestInit });\n}\n\nexport function isSessionExpiredError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 404) {\n return true;\n }\n return error.message.includes('HTTP 404') || error.message.toLowerCase().includes('session');\n}\n\nfunction isAuthError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 401) {\n return true;\n }\n return error.message.includes('HTTP 401');\n}\n\n/** Inject the active otel trace context so each request propagates its span. */\nfunction stampTraceHeaders(headers: Record<string, string>): void {\n propagation.inject(context.active(), headers);\n}\n\nfunction getTransportSessionId(transport: McpTransport): string | null {\n return transport instanceof StreamableHTTPClientTransport ? (transport.sessionId ?? null) : null;\n}\n\nfunction toConnectError(error: unknown): McpConnectionError {\n if (error instanceof McpConnectionError) {\n return error;\n }\n if (isAuthError(error)) {\n return new McpConnectionError('upstream returned 401 Unauthorized', 401, {\n cause: error,\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new McpConnectionError(message, 502, { cause: error });\n}\n\nfunction buildConnection(\n client: McpClientWithTimeout,\n transport: McpTransport,\n transportType: RemoteMcpTransportType,\n headers: Record<string, string>,\n requestOptions: { signal: AbortSignal },\n): RemoteMcpConnection {\n return {\n transportType,\n sessionId: getTransportSessionId(transport),\n listTools: async (cursor?: string) => {\n stampTraceHeaders(headers);\n const response = await client.listTools(cursor ? { cursor } : undefined, requestOptions);\n return {\n tools: response.tools,\n nextCursor: response.nextCursor,\n };\n },\n callTool: async (callParams: CallToolRequest['params']): Promise<CallToolResult> => {\n stampTraceHeaders(headers);\n return (await client.callTool(callParams, undefined, requestOptions)) as CallToolResult;\n },\n close: async (): Promise<void> => {\n await client.close().catch(() => {\n /* no-op */\n });\n },\n };\n}\n\n/**\n * Connect to a remote MCP server, keeping the first transport that connects. `sessionId` is passed to\n * every attempt so a stateful session resumes in place instead of opening a throwaway one.\n *\n * `knownTransportType` is a hint (from a prior connect / persisted state): it's tried first for a fast\n * path, but on failure we still fall back to probing the remaining transports, so a stale or wrong\n * hint (server switched transports, bad resume data) self-heals instead of failing every turn.\n */\nexport async function connectRemoteMcp(params: {\n url: string;\n headers: Record<string, string>;\n sessionId?: string | undefined;\n knownTransportType?: RemoteMcpTransportType | undefined;\n requestTimeoutMs: number;\n connectTimeoutMs: number;\n signal: AbortSignal;\n onClose?: (() => void) | undefined;\n onError?: ((error: Error) => void) | undefined;\n}): Promise<RemoteMcpConnection> {\n const url = new URL(params.url);\n const requestOptions = { signal: params.signal };\n const candidates = params.knownTransportType\n ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter(t => t !== params.knownTransportType)]\n : TRANSPORT_PROBE_ORDER;\n const failures: { transport: RemoteMcpTransportType; error: string }[] = [];\n\n for (const transportType of candidates) {\n const transport = createTransport(transportType, url, params.headers, params.sessionId);\n const client = new McpClientWithTimeout(params.requestTimeoutMs);\n try {\n stampTraceHeaders(params.headers);\n await withTimeout(\n // Concrete transports use sessionId: string|undefined; Transport uses an optional\n // property — exactOptionalPropertyTypes rejects assignability without this cast.\n client.connect(transport as Parameters<Client['connect']>[0], requestOptions),\n params.connectTimeoutMs,\n transportType,\n );\n } catch (error) {\n await client.close().catch(() => {\n /* no-op */\n });\n if (isAuthError(error)) {\n throw toConnectError(error);\n }\n // A session-expired error means the transport is right but the session is stale: surface it so\n // the caller reconnects fresh instead of falling through to a different transport.\n if (params.sessionId && isSessionExpiredError(error)) {\n throw toConnectError(error);\n }\n failures.push({ transport: transportType, error: error instanceof Error ? error.message.trim() : String(error) });\n continue;\n }\n\n // Set on the client (not the transport) so the SDK's own onclose/onerror cleanup still runs; the\n // SDK invokes these from inside it. Only wired on the kept connection, so failed attempts stay quiet.\n client.onclose = () => params.onClose?.();\n client.onerror = error => params.onError?.(error);\n return buildConnection(client, transport, transportType, params.headers, requestOptions);\n }\n\n throw new McpConnectionError(`failed to connect (tried ${candidates.join(', ')}): ${JSON.stringify(failures)}`, 502);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/core/mcp/remoteMcpClient.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\n// SSE remains required during the Streamable HTTP migration (some servers still speak SSE only).\n\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { context, propagation } from '@opentelemetry/api';\nimport { Agent, fetch as undiciFetch } from 'undici';\nimport { McpConnectionError } from '../errors';\nimport { withTimeout } from '../util/promiseUtils';\nimport type { ToolSchema } from './IMCPServer';\n\n/** Networking for remote (url-based) MCP servers, kept separate so it can be mocked in tests. */\n\nexport type RemoteMcpTransportType = 'streamable-http' | 'sse';\n\nexport interface RemoteMcpConnection {\n readonly transportType: RemoteMcpTransportType;\n /** Session id for a stateful server, or null for a stateless one. */\n readonly sessionId: string | null;\n listTools(cursor?: string): Promise<{ tools: ToolSchema[]; nextCursor?: string | undefined }>;\n callTool(params: CallToolRequest['params']): Promise<CallToolResult>;\n close(): Promise<void>;\n}\n\n// SSE transport type kept for dual-probe support during migration.\n// eslint-disable-next-line @typescript-eslint/no-deprecated -- see TRANSPORT_PROBE_ORDER\ntype McpTransport = StreamableHTTPClientTransport | SSEClientTransport;\n\nconst CLIENT_INFO = { name: 'tfy-agent-mcp-client', version: '1.0.0' } as const;\nconst TRANSPORT_PROBE_ORDER: RemoteMcpTransportType[] = ['streamable-http', 'sse'];\n\nexport const DEFAULT_MAX_MCP_RESPONSE_BYTES = 50 * 1024 * 1024;\n\n// MCP SSE/streamable-HTTP keeps a long-lived response open that is often idle between tool calls.\n// Node fetch (undici) defaults bodyTimeout to 300s of silence, then kills the stream with\n// `Body Timeout Error` — we reconnect and the ~5m cycle repeats in logs. 30m matches the\n// Gateway idle-body window; MCP request deadlines still come from requestTimeoutMs.\nconst MCP_BODY_TIMEOUT_MS = 30 * 60 * 1000;\nconst mcpHttpAgent = new Agent({ bodyTimeout: MCP_BODY_TIMEOUT_MS });\nconst mcpFetch: FetchLike = (url, init) =>\n undiciFetch(typeof url === 'string' ? url : url.href, { ...(init as object), dispatcher: mcpHttpAgent });\n\n/** GET SSE is long-lived and uncapped; every other body aborts at `maxBytes`. */\nexport function withMaxResponseBytes(fetchFn: FetchLike, maxBytes: number): FetchLike {\n return async (url, init) => {\n const response = await fetchFn(url, init);\n const isGetSse =\n (init?.method ?? 'GET').toUpperCase() === 'GET' &&\n (response.headers.get('content-type') ?? '').toLowerCase().includes('text/event-stream');\n if (isGetSse || !response.body) {\n return response;\n }\n let seen = 0;\n return new Response(\n response.body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n seen += chunk.byteLength;\n if (seen > maxBytes) {\n controller.error(new Error(`MCP response exceeded max ${String(maxBytes)} bytes`));\n return;\n }\n controller.enqueue(chunk);\n },\n }),\n ),\n { status: response.status, statusText: response.statusText, headers: response.headers },\n );\n };\n}\n\nclass McpClientWithTimeout extends Client {\n constructor(private readonly requestTimeoutMs: number) {\n super(CLIENT_INFO, { capabilities: {} });\n }\n\n // SDK Client.request is loosely typed; forward with an explicit timeout.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MCP SDK request typing\n override request(req: any, schema: any, options?: any): Promise<any> {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- MCP SDK request typing\n return super.request(req, schema, { ...options, timeout: this.requestTimeoutMs });\n }\n}\n\nfunction createTransport(\n type: RemoteMcpTransportType,\n url: URL,\n headers: Record<string, string>,\n fetchFn: FetchLike,\n sessionId?: string,\n): McpTransport {\n const requestInit = { headers };\n if (type === 'streamable-http') {\n return new StreamableHTTPClientTransport(url, {\n requestInit,\n fetch: fetchFn,\n ...(sessionId !== undefined ? { sessionId } : {}),\n });\n }\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- dual-transport probe; see TRANSPORT_PROBE_ORDER\n return new SSEClientTransport(url, { requestInit, fetch: fetchFn });\n}\n\nexport function isSessionExpiredError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 404) {\n return true;\n }\n return error.message.includes('HTTP 404') || error.message.toLowerCase().includes('session');\n}\n\nfunction isAuthError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n if ('code' in error && error.code === 401) {\n return true;\n }\n return error.message.includes('HTTP 401');\n}\n\n/** Inject the active otel trace context so each request propagates its span. */\nfunction stampTraceHeaders(headers: Record<string, string>): void {\n propagation.inject(context.active(), headers);\n}\n\nfunction getTransportSessionId(transport: McpTransport): string | null {\n return transport instanceof StreamableHTTPClientTransport ? (transport.sessionId ?? null) : null;\n}\n\nfunction toConnectError(error: unknown): McpConnectionError {\n if (error instanceof McpConnectionError) {\n return error;\n }\n if (isAuthError(error)) {\n return new McpConnectionError('upstream returned 401 Unauthorized', 401, {\n cause: error,\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new McpConnectionError(message, 502, { cause: error });\n}\n\nfunction buildConnection(\n client: McpClientWithTimeout,\n transport: McpTransport,\n transportType: RemoteMcpTransportType,\n headers: Record<string, string>,\n requestOptions: { signal: AbortSignal },\n): RemoteMcpConnection {\n return {\n transportType,\n sessionId: getTransportSessionId(transport),\n listTools: async (cursor?: string) => {\n stampTraceHeaders(headers);\n const response = await client.listTools(cursor ? { cursor } : undefined, requestOptions);\n return {\n tools: response.tools,\n nextCursor: response.nextCursor,\n };\n },\n callTool: async (callParams: CallToolRequest['params']): Promise<CallToolResult> => {\n stampTraceHeaders(headers);\n return (await client.callTool(callParams, undefined, requestOptions)) as CallToolResult;\n },\n close: async (): Promise<void> => {\n await client.close().catch(() => {\n /* no-op */\n });\n },\n };\n}\n\n/**\n * Connect to a remote MCP server, keeping the first transport that connects. `sessionId` is passed to\n * every attempt so a stateful session resumes in place instead of opening a throwaway one.\n *\n * `knownTransportType` is a hint (from a prior connect / persisted state): it's tried first for a fast\n * path, but on failure we still fall back to probing the remaining transports, so a stale or wrong\n * hint (server switched transports, bad resume data) self-heals instead of failing every turn.\n */\nexport async function connectRemoteMcp(params: {\n url: string;\n headers: Record<string, string>;\n sessionId?: string | undefined;\n knownTransportType?: RemoteMcpTransportType | undefined;\n requestTimeoutMs: number;\n connectTimeoutMs: number;\n maxResponseBytes?: number | undefined;\n signal: AbortSignal;\n onClose?: (() => void) | undefined;\n onError?: ((error: Error) => void) | undefined;\n}): Promise<RemoteMcpConnection> {\n const url = new URL(params.url);\n const requestOptions = { signal: params.signal };\n const fetchFn = withMaxResponseBytes(mcpFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);\n const candidates = params.knownTransportType\n ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter(t => t !== params.knownTransportType)]\n : TRANSPORT_PROBE_ORDER;\n const failures: { transport: RemoteMcpTransportType; error: string }[] = [];\n\n for (const transportType of candidates) {\n const transport = createTransport(transportType, url, params.headers, fetchFn, params.sessionId);\n const client = new McpClientWithTimeout(params.requestTimeoutMs);\n // withTimeout races client.connect() and does not abort it, so timed-out connects can leak\n // sockets until GC. Abort this controller on timeout so the handshake is cancelled.\n // AbortSignal.timeout cannot be cleared, and the SDK keeps the signal on initialize, so it\n // would still fire connectTimeoutMs later and cancel the live client.\n const timeout = new AbortController();\n const connectOptions = { signal: AbortSignal.any([params.signal, timeout.signal]) };\n try {\n stampTraceHeaders(params.headers);\n await withTimeout(\n // Concrete transports use sessionId: string|undefined; Transport uses an optional\n // property — exactOptionalPropertyTypes rejects assignability without this cast.\n client.connect(transport as Parameters<Client['connect']>[0], connectOptions),\n params.connectTimeoutMs,\n transportType,\n );\n } catch (error) {\n timeout.abort();\n await client.close().catch(() => {\n /* no-op */\n });\n if (isAuthError(error)) {\n throw toConnectError(error);\n }\n // A session-expired error means the transport is right but the session is stale: surface it so\n // the caller reconnects fresh instead of falling through to a different transport.\n if (params.sessionId && isSessionExpiredError(error)) {\n throw toConnectError(error);\n }\n failures.push({ transport: transportType, error: error instanceof Error ? error.message.trim() : String(error) });\n continue;\n }\n\n // Set on the client (not the transport) so the SDK's own onclose/onerror cleanup still runs; the\n // SDK invokes these from inside it. Only wired on the kept connection, so failed attempts stay quiet.\n client.onclose = () => params.onClose?.();\n client.onerror = error => params.onError?.(error);\n return buildConnection(client, transport, transportType, params.headers, requestOptions);\n }\n\n throw new McpConnectionError(`failed to connect (tried ${candidates.join(', ')}): ${JSON.stringify(failures)}`, 502);\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"toolSelectors.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/toolSelectors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAkB1E,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,iCAA8C,CAAC;AAE/E,oDAAoD;AACpD,eAAO,MAAM,oCAAoC,6CAAgE,CAAC;AAElH,eAAO,MAAM,oBAAoB,EAAE,MAAM,EAAmB,CAAC;AAC7D,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAAO,CAAC;AAClD,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAAO,CAAC;AAClD,eAAO,MAAM,kCAAkC,EAAE,MAAM,EAA2C,CAAC;AA+BnG,mEAAmE;AACnE,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,eAAe,GAAG,SAAS,EACxC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GACvC,OAAO,CAeT;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,eAAe,GAAG,SAAS,EACxC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GACvC,OAAO,CAKT;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,eAAe,GAAG,SAAS,CAAC;IACzC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,GAAG,OAAO,CASV;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAKnF;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAErF"}
1
+ {"version":3,"file":"toolSelectors.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/toolSelectors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAkB1E,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,iCAA8C,CAAC;AAE/E,oDAAoD;AACpD,eAAO,MAAM,oCAAoC,6CAAgE,CAAC;AAElH,eAAO,MAAM,oBAAoB,EAAE,MAAM,EAAmB,CAAC;AAC7D,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAAO,CAAC;AAClD,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAAO,CAAC;AAClD,eAAO,MAAM,kCAAkC,EAAE,MAAM,EAA2B,CAAC;AA+BnF,mEAAmE;AACnE,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,eAAe,GAAG,SAAS,EACxC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GACvC,OAAO,CAeT;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,eAAe,GAAG,SAAS,EACxC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GACvC,OAAO,CAKT;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,eAAe,GAAG,SAAS,CAAC;IACzC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,GAAG,OAAO,CASV;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAKnF;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAErF"}
@@ -43,7 +43,7 @@ var REQUIRE_APPROVAL_TOOLS_SELECTOR_TAGS = [TOOL_TAG_ALL, TOOL_TAG_WRITE, TOOL_T
43
43
  var DEFAULT_ENABLE_TOOLS = [TOOL_TAG_ALL];
44
44
  var DEFAULT_DISABLE_TOOLS = [];
45
45
  var DEFAULT_PRELOAD_TOOLS = [];
46
- var DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS = [TOOL_TAG_WRITE, TOOL_TAG_DESTRUCTIVE];
46
+ var DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS = [TOOL_TAG_DESTRUCTIVE];
47
47
  function isToolTag(selector) {
48
48
  return TOOL_TAGS.includes(selector);
49
49
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/mcp/toolSelectors.ts"],"sourcesContent":["import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';\n\n/**\n * MCP server tool selectors are either a special tag or a literal tool name.\n *\n * @all — every tool on the server\n * @read-only — readOnlyHint === true\n * @write — readOnlyHint === false and not destructive\n * @destructive — destructiveHint === true\n */\nconst TOOL_TAG_ALL = '@all';\nconst TOOL_TAG_READ_ONLY = '@read-only';\nconst TOOL_TAG_WRITE = '@write';\nconst TOOL_TAG_DESTRUCTIVE = '@destructive';\n\nconst TOOL_TAGS = [TOOL_TAG_ALL, TOOL_TAG_READ_ONLY, TOOL_TAG_WRITE, TOOL_TAG_DESTRUCTIVE] as const;\ntype ToolTag = (typeof TOOL_TAGS)[number];\n\n/** Tags allowed on `enable_tools`, `disable_tools`, and `preload_tools`. */\nexport const TOOLS_SELECTOR_TAGS = [TOOL_TAG_ALL, TOOL_TAG_READ_ONLY] as const;\n\n/** Tags allowed on `require_approval_for_tools`. */\nexport const REQUIRE_APPROVAL_TOOLS_SELECTOR_TAGS = [TOOL_TAG_ALL, TOOL_TAG_WRITE, TOOL_TAG_DESTRUCTIVE] as const;\n\nexport const DEFAULT_ENABLE_TOOLS: string[] = [TOOL_TAG_ALL];\nexport const DEFAULT_DISABLE_TOOLS: string[] = [];\nexport const DEFAULT_PRELOAD_TOOLS: string[] = [];\nexport const DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS: string[] = [TOOL_TAG_WRITE, TOOL_TAG_DESTRUCTIVE];\n\nfunction isToolTag(selector: string): selector is ToolTag {\n return TOOL_TAGS.includes(selector as ToolTag);\n}\n\nfunction isReadOnly(annotations?: ToolAnnotations): boolean {\n return annotations?.readOnlyHint === true;\n}\n\nfunction isWrite(annotations?: ToolAnnotations): boolean {\n return annotations?.readOnlyHint === false && annotations.destructiveHint !== true;\n}\n\nfunction isDestructive(annotations?: ToolAnnotations): boolean {\n return annotations?.destructiveHint === true;\n}\n\nfunction toolMatchesTag(tag: ToolTag, annotations: ToolAnnotations | undefined): boolean {\n switch (tag) {\n case TOOL_TAG_ALL:\n return true;\n case TOOL_TAG_READ_ONLY:\n return isReadOnly(annotations);\n case TOOL_TAG_WRITE:\n return isWrite(annotations);\n case TOOL_TAG_DESTRUCTIVE:\n return isDestructive(annotations);\n }\n}\n\n/** True if the tool matches any selector (tag or literal name). */\nexport function toolMatchesAnySelector(\n toolName: string,\n annotations: ToolAnnotations | undefined,\n selectors: readonly string[] | undefined,\n): boolean {\n if (!selectors?.length) {\n return false;\n }\n\n for (const selector of selectors) {\n if (isToolTag(selector)) {\n if (toolMatchesTag(selector, annotations)) {\n return true;\n }\n } else if (selector === toolName) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Whether the tool requires human approval before execution.\n * Matches `@write`, `@destructive`, `@all`, or explicit tool names against annotations.\n * Unannotated tools are exempt unless named in `require_approval_for_tools` or covered by `@all`.\n */\nexport function toolRequiresApproval(\n toolName: string,\n annotations: ToolAnnotations | undefined,\n selectors: readonly string[] | undefined,\n): boolean {\n if (!selectors?.length) {\n return false;\n }\n return toolMatchesAnySelector(toolName, annotations, selectors);\n}\n\n/** True if the tool is enabled and not disabled. */\nexport function isToolAllowed(params: {\n toolName: string;\n annotations: ToolAnnotations | undefined;\n enableSelectors: readonly string[];\n disableSelectors: readonly string[];\n}): boolean {\n const { toolName, annotations, enableSelectors, disableSelectors } = params;\n if (!toolMatchesAnySelector(toolName, annotations, enableSelectors)) {\n return false;\n }\n if (toolMatchesAnySelector(toolName, annotations, disableSelectors)) {\n return false;\n }\n return true;\n}\n\n/** Non-tag names from a selector list (for upfront validation / sandbox allow-lists). */\nexport function literalToolNames(selectors: readonly string[] | undefined): string[] {\n if (!selectors) {\n return [];\n }\n return selectors.filter(s => !isToolTag(s));\n}\n\nexport function selectorsIncludeAll(selectors: readonly string[] | undefined): boolean {\n return !!selectors?.includes(TOOL_TAG_ALL);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,IAAM,eAAe;AACrB,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB;AACvB,IAAM,uBAAuB;AAE7B,IAAM,YAAY,CAAC,cAAc,oBAAoB,gBAAgB,oBAAoB;AAIlF,IAAM,sBAAsB,CAAC,cAAc,kBAAkB;AAG7D,IAAM,uCAAuC,CAAC,cAAc,gBAAgB,oBAAoB;AAEhG,IAAM,uBAAiC,CAAC,YAAY;AACpD,IAAM,wBAAkC,CAAC;AACzC,IAAM,wBAAkC,CAAC;AACzC,IAAM,qCAA+C,CAAC,gBAAgB,oBAAoB;AAEjG,SAAS,UAAU,UAAuC;AACxD,SAAO,UAAU,SAAS,QAAmB;AAC/C;AAEA,SAAS,WAAW,aAAwC;AAC1D,SAAO,aAAa,iBAAiB;AACvC;AAEA,SAAS,QAAQ,aAAwC;AACvD,SAAO,aAAa,iBAAiB,SAAS,YAAY,oBAAoB;AAChF;AAEA,SAAS,cAAc,aAAwC;AAC7D,SAAO,aAAa,oBAAoB;AAC1C;AAEA,SAAS,eAAe,KAAc,aAAmD;AACvF,UAAQ,KAAK;AAAA,IACX,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,WAAW,WAAW;AAAA,IAC/B,KAAK;AACH,aAAO,QAAQ,WAAW;AAAA,IAC5B,KAAK;AACH,aAAO,cAAc,WAAW;AAAA,EACpC;AACF;AAGO,SAAS,uBACd,UACA,aACA,WACS;AACT,MAAI,CAAC,WAAW,QAAQ;AACtB,WAAO;AAAA,EACT;AAEA,aAAW,YAAY,WAAW;AAChC,QAAI,UAAU,QAAQ,GAAG;AACvB,UAAI,eAAe,UAAU,WAAW,GAAG;AACzC,eAAO;AAAA,MACT;AAAA,IACF,WAAW,aAAa,UAAU;AAChC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,qBACd,UACA,aACA,WACS;AACT,MAAI,CAAC,WAAW,QAAQ;AACtB,WAAO;AAAA,EACT;AACA,SAAO,uBAAuB,UAAU,aAAa,SAAS;AAChE;AAGO,SAAS,cAAc,QAKlB;AACV,QAAM,EAAE,UAAU,aAAa,iBAAiB,iBAAiB,IAAI;AACrE,MAAI,CAAC,uBAAuB,UAAU,aAAa,eAAe,GAAG;AACnE,WAAO;AAAA,EACT;AACA,MAAI,uBAAuB,UAAU,aAAa,gBAAgB,GAAG;AACnE,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,SAAS,iBAAiB,WAAoD;AACnF,MAAI,CAAC,WAAW;AACd,WAAO,CAAC;AAAA,EACV;AACA,SAAO,UAAU,OAAO,OAAK,CAAC,UAAU,CAAC,CAAC;AAC5C;AAEO,SAAS,oBAAoB,WAAmD;AACrF,SAAO,CAAC,CAAC,WAAW,SAAS,YAAY;AAC3C;","names":[]}
1
+ {"version":3,"sources":["../../../src/core/mcp/toolSelectors.ts"],"sourcesContent":["import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';\n\n/**\n * MCP server tool selectors are either a special tag or a literal tool name.\n *\n * @all — every tool on the server\n * @read-only — readOnlyHint === true\n * @write — readOnlyHint === false and not destructive\n * @destructive — destructiveHint === true\n */\nconst TOOL_TAG_ALL = '@all';\nconst TOOL_TAG_READ_ONLY = '@read-only';\nconst TOOL_TAG_WRITE = '@write';\nconst TOOL_TAG_DESTRUCTIVE = '@destructive';\n\nconst TOOL_TAGS = [TOOL_TAG_ALL, TOOL_TAG_READ_ONLY, TOOL_TAG_WRITE, TOOL_TAG_DESTRUCTIVE] as const;\ntype ToolTag = (typeof TOOL_TAGS)[number];\n\n/** Tags allowed on `enable_tools`, `disable_tools`, and `preload_tools`. */\nexport const TOOLS_SELECTOR_TAGS = [TOOL_TAG_ALL, TOOL_TAG_READ_ONLY] as const;\n\n/** Tags allowed on `require_approval_for_tools`. */\nexport const REQUIRE_APPROVAL_TOOLS_SELECTOR_TAGS = [TOOL_TAG_ALL, TOOL_TAG_WRITE, TOOL_TAG_DESTRUCTIVE] as const;\n\nexport const DEFAULT_ENABLE_TOOLS: string[] = [TOOL_TAG_ALL];\nexport const DEFAULT_DISABLE_TOOLS: string[] = [];\nexport const DEFAULT_PRELOAD_TOOLS: string[] = [];\nexport const DEFAULT_REQUIRE_APPROVAL_FOR_TOOLS: string[] = [TOOL_TAG_DESTRUCTIVE];\n\nfunction isToolTag(selector: string): selector is ToolTag {\n return TOOL_TAGS.includes(selector as ToolTag);\n}\n\nfunction isReadOnly(annotations?: ToolAnnotations): boolean {\n return annotations?.readOnlyHint === true;\n}\n\nfunction isWrite(annotations?: ToolAnnotations): boolean {\n return annotations?.readOnlyHint === false && annotations.destructiveHint !== true;\n}\n\nfunction isDestructive(annotations?: ToolAnnotations): boolean {\n return annotations?.destructiveHint === true;\n}\n\nfunction toolMatchesTag(tag: ToolTag, annotations: ToolAnnotations | undefined): boolean {\n switch (tag) {\n case TOOL_TAG_ALL:\n return true;\n case TOOL_TAG_READ_ONLY:\n return isReadOnly(annotations);\n case TOOL_TAG_WRITE:\n return isWrite(annotations);\n case TOOL_TAG_DESTRUCTIVE:\n return isDestructive(annotations);\n }\n}\n\n/** True if the tool matches any selector (tag or literal name). */\nexport function toolMatchesAnySelector(\n toolName: string,\n annotations: ToolAnnotations | undefined,\n selectors: readonly string[] | undefined,\n): boolean {\n if (!selectors?.length) {\n return false;\n }\n\n for (const selector of selectors) {\n if (isToolTag(selector)) {\n if (toolMatchesTag(selector, annotations)) {\n return true;\n }\n } else if (selector === toolName) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Whether the tool requires human approval before execution.\n * Matches `@write`, `@destructive`, `@all`, or explicit tool names against annotations.\n * Unannotated tools are exempt unless named in `require_approval_for_tools` or covered by `@all`.\n */\nexport function toolRequiresApproval(\n toolName: string,\n annotations: ToolAnnotations | undefined,\n selectors: readonly string[] | undefined,\n): boolean {\n if (!selectors?.length) {\n return false;\n }\n return toolMatchesAnySelector(toolName, annotations, selectors);\n}\n\n/** True if the tool is enabled and not disabled. */\nexport function isToolAllowed(params: {\n toolName: string;\n annotations: ToolAnnotations | undefined;\n enableSelectors: readonly string[];\n disableSelectors: readonly string[];\n}): boolean {\n const { toolName, annotations, enableSelectors, disableSelectors } = params;\n if (!toolMatchesAnySelector(toolName, annotations, enableSelectors)) {\n return false;\n }\n if (toolMatchesAnySelector(toolName, annotations, disableSelectors)) {\n return false;\n }\n return true;\n}\n\n/** Non-tag names from a selector list (for upfront validation / sandbox allow-lists). */\nexport function literalToolNames(selectors: readonly string[] | undefined): string[] {\n if (!selectors) {\n return [];\n }\n return selectors.filter(s => !isToolTag(s));\n}\n\nexport function selectorsIncludeAll(selectors: readonly string[] | undefined): boolean {\n return !!selectors?.includes(TOOL_TAG_ALL);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,IAAM,eAAe;AACrB,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB;AACvB,IAAM,uBAAuB;AAE7B,IAAM,YAAY,CAAC,cAAc,oBAAoB,gBAAgB,oBAAoB;AAIlF,IAAM,sBAAsB,CAAC,cAAc,kBAAkB;AAG7D,IAAM,uCAAuC,CAAC,cAAc,gBAAgB,oBAAoB;AAEhG,IAAM,uBAAiC,CAAC,YAAY;AACpD,IAAM,wBAAkC,CAAC;AACzC,IAAM,wBAAkC,CAAC;AACzC,IAAM,qCAA+C,CAAC,oBAAoB;AAEjF,SAAS,UAAU,UAAuC;AACxD,SAAO,UAAU,SAAS,QAAmB;AAC/C;AAEA,SAAS,WAAW,aAAwC;AAC1D,SAAO,aAAa,iBAAiB;AACvC;AAEA,SAAS,QAAQ,aAAwC;AACvD,SAAO,aAAa,iBAAiB,SAAS,YAAY,oBAAoB;AAChF;AAEA,SAAS,cAAc,aAAwC;AAC7D,SAAO,aAAa,oBAAoB;AAC1C;AAEA,SAAS,eAAe,KAAc,aAAmD;AACvF,UAAQ,KAAK;AAAA,IACX,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,WAAW,WAAW;AAAA,IAC/B,KAAK;AACH,aAAO,QAAQ,WAAW;AAAA,IAC5B,KAAK;AACH,aAAO,cAAc,WAAW;AAAA,EACpC;AACF;AAGO,SAAS,uBACd,UACA,aACA,WACS;AACT,MAAI,CAAC,WAAW,QAAQ;AACtB,WAAO;AAAA,EACT;AAEA,aAAW,YAAY,WAAW;AAChC,QAAI,UAAU,QAAQ,GAAG;AACvB,UAAI,eAAe,UAAU,WAAW,GAAG;AACzC,eAAO;AAAA,MACT;AAAA,IACF,WAAW,aAAa,UAAU;AAChC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,qBACd,UACA,aACA,WACS;AACT,MAAI,CAAC,WAAW,QAAQ;AACtB,WAAO;AAAA,EACT;AACA,SAAO,uBAAuB,UAAU,aAAa,SAAS;AAChE;AAGO,SAAS,cAAc,QAKlB;AACV,QAAM,EAAE,UAAU,aAAa,iBAAiB,iBAAiB,IAAI;AACrE,MAAI,CAAC,uBAAuB,UAAU,aAAa,eAAe,GAAG;AACnE,WAAO;AAAA,EACT;AACA,MAAI,uBAAuB,UAAU,aAAa,gBAAgB,GAAG;AACnE,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,SAAS,iBAAiB,WAAoD;AACnF,MAAI,CAAC,WAAW;AACd,WAAO,CAAC;AAAA,EACV;AACA,SAAO,UAAU,OAAO,OAAK,CAAC,UAAU,CAAC,CAAC;AAC5C;AAEO,SAAS,oBAAoB,WAAmD;AACrF,SAAO,CAAC,CAAC,WAAW,SAAS,YAAY;AAC3C;","names":[]}