@truefoundry/trueforge-core 0.2.0 → 0.3.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/core/index.d.ts +1 -0
  2. package/dist/core/index.d.ts.map +1 -1
  3. package/dist/core/index.js +7 -0
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +4 -0
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/core/llm/LLMTypes.js +2 -2
  8. package/dist/core/llm/LLMTypes.js.map +1 -1
  9. package/dist/core/llm/LLMTypes.mjs +2 -2
  10. package/dist/core/llm/LLMTypes.mjs.map +1 -1
  11. package/dist/core/llm/VercelAILLM.d.ts.map +1 -1
  12. package/dist/core/llm/VercelAILLM.js +10 -0
  13. package/dist/core/llm/VercelAILLM.js.map +1 -1
  14. package/dist/core/llm/VercelAILLM.mjs +10 -0
  15. package/dist/core/llm/VercelAILLM.mjs.map +1 -1
  16. package/dist/core/llm/toOpenAIChatMessage.d.ts +4 -3
  17. package/dist/core/llm/toOpenAIChatMessage.d.ts.map +1 -1
  18. package/dist/core/llm/toOpenAIChatMessage.js.map +1 -1
  19. package/dist/core/llm/toOpenAIChatMessage.mjs.map +1 -1
  20. package/dist/core/mcp/RemoteMCP.d.ts +2 -1
  21. package/dist/core/mcp/RemoteMCP.d.ts.map +1 -1
  22. package/dist/core/mcp/RemoteMCP.js +65 -20
  23. package/dist/core/mcp/RemoteMCP.js.map +1 -1
  24. package/dist/core/mcp/RemoteMCP.mjs +65 -20
  25. package/dist/core/mcp/RemoteMCP.mjs.map +1 -1
  26. package/dist/core/mcp/remoteMcpClient.d.ts.map +1 -1
  27. package/dist/core/mcp/remoteMcpClient.js +2 -5
  28. package/dist/core/mcp/remoteMcpClient.js.map +1 -1
  29. package/dist/core/mcp/remoteMcpClient.mjs +2 -5
  30. package/dist/core/mcp/remoteMcpClient.mjs.map +1 -1
  31. package/dist/core/runtime/AgentThread.d.ts.map +1 -1
  32. package/dist/core/runtime/AgentThread.js +7 -1
  33. package/dist/core/runtime/AgentThread.js.map +1 -1
  34. package/dist/core/runtime/AgentThread.mjs +7 -1
  35. package/dist/core/runtime/AgentThread.mjs.map +1 -1
  36. package/dist/core/sandbox/provider/TFYSandboxProvider.d.ts.map +1 -1
  37. package/dist/core/sandbox/provider/TFYSandboxProvider.js +56 -14
  38. package/dist/core/sandbox/provider/TFYSandboxProvider.js.map +1 -1
  39. package/dist/core/sandbox/provider/TFYSandboxProvider.mjs +57 -16
  40. package/dist/core/sandbox/provider/TFYSandboxProvider.mjs.map +1 -1
  41. package/dist/core/util/errorLogFields.d.ts +1 -0
  42. package/dist/core/util/errorLogFields.d.ts.map +1 -1
  43. package/dist/core/util/errorLogFields.js +33 -6
  44. package/dist/core/util/errorLogFields.js.map +1 -1
  45. package/dist/core/util/errorLogFields.mjs +33 -6
  46. package/dist/core/util/errorLogFields.mjs.map +1 -1
  47. package/dist/core/util/ssrfGuard.d.ts +10 -0
  48. package/dist/core/util/ssrfGuard.d.ts.map +1 -0
  49. package/dist/core/util/ssrfGuard.js +333 -0
  50. package/dist/core/util/ssrfGuard.js.map +1 -0
  51. package/dist/core/util/ssrfGuard.mjs +305 -0
  52. package/dist/core/util/ssrfGuard.mjs.map +1 -0
  53. package/dist/core/web-search/ParallelWebSearchProvider.d.ts.map +1 -1
  54. package/dist/core/web-search/ParallelWebSearchProvider.js +5 -1
  55. package/dist/core/web-search/ParallelWebSearchProvider.js.map +1 -1
  56. package/dist/core/web-search/ParallelWebSearchProvider.mjs +5 -1
  57. package/dist/core/web-search/ParallelWebSearchProvider.mjs.map +1 -1
  58. package/dist/request-reply/client.d.ts +2 -2
  59. package/dist/request-reply/client.d.ts.map +1 -1
  60. package/dist/request-reply/client.js.map +1 -1
  61. package/dist/request-reply/client.mjs.map +1 -1
  62. package/dist/request-reply/executor.d.ts +3 -3
  63. package/dist/request-reply/executor.d.ts.map +1 -1
  64. package/dist/request-reply/executor.js +0 -1
  65. package/dist/request-reply/executor.js.map +1 -1
  66. package/dist/request-reply/executor.mjs +0 -1
  67. package/dist/request-reply/executor.mjs.map +1 -1
  68. package/dist/request-reply/index.d.ts +1 -0
  69. package/dist/request-reply/index.d.ts.map +1 -1
  70. package/dist/request-reply/index.js.map +1 -1
  71. package/dist/request-reply/index.mjs.map +1 -1
  72. package/dist/request-reply/redisClient.d.ts +4 -0
  73. package/dist/request-reply/redisClient.d.ts.map +1 -0
  74. package/dist/request-reply/redisClient.js +19 -0
  75. package/dist/request-reply/redisClient.js.map +1 -0
  76. package/dist/request-reply/redisClient.mjs +1 -0
  77. package/dist/request-reply/redisClient.mjs.map +1 -0
  78. package/package.json +1 -1
@@ -58,6 +58,8 @@ var RemoteMCP = class {
58
58
  sessionId;
59
59
  resolvedTransportType;
60
60
  cachedTools;
61
+ inflight = 0;
62
+ pendingClose;
61
63
  constructor(params) {
62
64
  this.name = params.name;
63
65
  this.id = params.id;
@@ -77,13 +79,29 @@ var RemoteMCP = class {
77
79
  getSessionId() {
78
80
  return this.sessionId ?? void 0;
79
81
  }
80
- get connection() {
81
- if (!this._connection) {
82
- throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);
82
+ // Concurrent callTool/listTools share one transport. Session-expired retry must not close it
83
+ // while a sibling is still using it — close() aborts the sibling with "Connection closed",
84
+ // which is not treated as session-expired, so that call is never retried.
85
+ //
86
+ // Example: A (long) and B share socket S. B gets session-expired.
87
+ // Detach S (pendingClose), B retries on a new socket, A finishes on S,
88
+ // last caller closes pendingClose when inflight hits 0.
89
+ //
90
+ // connectAndRun:
91
+ // conn = this._connection // capture; op does not re-read this._connection
92
+ // inflight++
93
+ // try: return op(conn)
94
+ // catch sessionExpired:
95
+ // detach conn // _connection = undefined; do not close if inflight > 0
96
+ // pendingClose = conn
97
+ // reconnect and retry once
98
+ // finally:
99
+ // inflight--
100
+ // if inflight == 0: close(pendingClose)
101
+ async resetConnection(expired) {
102
+ if (expired !== void 0 && this._connection !== expired) {
103
+ return;
83
104
  }
84
- return this._connection;
85
- }
86
- async resetConnection() {
87
105
  this.isConnected = false;
88
106
  this.sessionId = void 0;
89
107
  this.cachedTools = void 0;
@@ -91,9 +109,17 @@ var RemoteMCP = class {
91
109
  await this.closeAndClearConnection();
92
110
  }
93
111
  async closeAndClearConnection() {
94
- await this._connection?.close().catch(() => {
95
- });
112
+ const connection = this._connection;
96
113
  this._connection = void 0;
114
+ if (!connection) {
115
+ return;
116
+ }
117
+ if (this.inflight > 0) {
118
+ this.pendingClose = connection;
119
+ return;
120
+ }
121
+ await connection.close().catch(() => {
122
+ });
97
123
  }
98
124
  async resolveHeaders() {
99
125
  if (typeof this.headers !== "function") {
@@ -109,25 +135,42 @@ var RemoteMCP = class {
109
135
  return this.connectAndRun(headersResult.headers, operation, true);
110
136
  }
111
137
  async connectAndRun(headers, operation, canRetry) {
138
+ let used;
112
139
  try {
113
140
  const initInfo = await this.connectIfNeeded(headers);
114
- return { result: await operation(), wasInitialized: initInfo };
141
+ const connection = this._connection;
142
+ if (!connection) {
143
+ throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);
144
+ }
145
+ used = connection;
146
+ this.inflight += 1;
147
+ return { result: await operation(connection), wasInitialized: initInfo };
115
148
  } catch (error) {
116
- if (canRetry && (0, import_remoteMcpClient.isSessionExpiredError)(error)) {
117
- this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);
118
- await this.resetConnection();
119
- return this.connectAndRun(headers, operation, false);
149
+ if (!(canRetry && (0, import_remoteMcpClient.isSessionExpiredError)(error))) {
150
+ throw error;
151
+ }
152
+ this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);
153
+ await this.resetConnection(used);
154
+ } finally {
155
+ if (used) {
156
+ this.inflight -= 1;
157
+ if (this.inflight === 0 && this.pendingClose) {
158
+ const stale = this.pendingClose;
159
+ this.pendingClose = void 0;
160
+ await stale.close().catch(() => {
161
+ });
162
+ }
120
163
  }
121
- throw error;
122
164
  }
165
+ return this.connectAndRun(headers, operation, false);
123
166
  }
124
- async loadTools() {
167
+ async loadTools(connection) {
125
168
  return this.tracing.withRemoteMcpToolSpan(
126
169
  { method: "tools/list", serverName: this.name, serverId: this.id, serverUrl: this.traceUrl, enabled: true },
127
170
  async (span) => {
128
171
  const tools = await (0, import_pagination.paginateWithCursorGuard)(
129
172
  async (cursor) => {
130
- const page = await this.connection.listTools(cursor);
173
+ const page = await connection.listTools(cursor);
131
174
  return { items: page.tools, nextCursor: page.nextCursor };
132
175
  },
133
176
  this.name,
@@ -144,7 +187,7 @@ var RemoteMCP = class {
144
187
  if (this.cachedTools) {
145
188
  return { result: { tools: this.cachedTools }, wasInitialized: void 0 };
146
189
  }
147
- const response = await this.executeWithSessionRetry(() => this.loadTools());
190
+ const response = await this.executeWithSessionRetry((connection) => this.loadTools(connection));
148
191
  if ("authRequired" in response) {
149
192
  return response;
150
193
  }
@@ -152,7 +195,7 @@ var RemoteMCP = class {
152
195
  }
153
196
  async callTool(params) {
154
197
  const response = await this.executeWithSessionRetry(
155
- () => this.tracing.withRemoteMcpToolSpan(
198
+ (connection) => this.tracing.withRemoteMcpToolSpan(
156
199
  {
157
200
  method: "tools/call",
158
201
  serverName: this.name,
@@ -163,7 +206,7 @@ var RemoteMCP = class {
163
206
  enabled: true
164
207
  },
165
208
  async (span) => {
166
- const result = await this.connection.callTool(params);
209
+ const result = await connection.callTool(params);
167
210
  span.setOutput(JSON.stringify(result));
168
211
  return result;
169
212
  }
@@ -211,7 +254,9 @@ var RemoteMCP = class {
211
254
  maxResponseBytes: this.maxResponseBytes,
212
255
  signal: this.signal,
213
256
  onClose: () => {
214
- this.isConnected = false;
257
+ if (connection !== void 0 && this._connection === connection) {
258
+ this.isConnected = false;
259
+ }
215
260
  },
216
261
  onError: (error) => {
217
262
  const fields = (0, import_errorLogFields.extractErrorLogFields)(error);
@@ -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 { 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":"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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 private inflight = 0;\n private pendingClose: RemoteMcpConnection | 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 // Concurrent callTool/listTools share one transport. Session-expired retry must not close it\n // while a sibling is still using it — close() aborts the sibling with \"Connection closed\",\n // which is not treated as session-expired, so that call is never retried.\n //\n // Example: A (long) and B share socket S. B gets session-expired.\n // Detach S (pendingClose), B retries on a new socket, A finishes on S,\n // last caller closes pendingClose when inflight hits 0.\n //\n // connectAndRun:\n // conn = this._connection // capture; op does not re-read this._connection\n // inflight++\n // try: return op(conn)\n // catch sessionExpired:\n // detach conn // _connection = undefined; do not close if inflight > 0\n // pendingClose = conn\n // reconnect and retry once\n // finally:\n // inflight--\n // if inflight == 0: close(pendingClose)\n private async resetConnection(expired?: RemoteMcpConnection): Promise<void> {\n if (expired !== undefined && this._connection !== expired) {\n return;\n }\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 const connection = this._connection;\n this._connection = undefined;\n if (!connection) {\n return;\n }\n if (this.inflight > 0) {\n // One leftover socket; a second session-expiry while the first is still pending can leak it.\n this.pendingClose = connection;\n return;\n }\n await connection.close().catch(() => {\n /* no-op */\n });\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>(\n operation: (connection: RemoteMcpConnection) => Promise<T>,\n ): 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: (connection: RemoteMcpConnection) => Promise<T>,\n canRetry: boolean,\n ): Promise<ExecuteResult<T>> {\n let used: RemoteMcpConnection | undefined;\n try {\n const initInfo = await this.connectIfNeeded(headers);\n const connection = this._connection;\n if (!connection) {\n throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);\n }\n used = connection;\n this.inflight += 1;\n return { result: await operation(connection), wasInitialized: initInfo };\n } catch (error) {\n if (!(canRetry && isSessionExpiredError(error))) {\n throw error;\n }\n this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);\n await this.resetConnection(used);\n } finally {\n if (used) {\n this.inflight -= 1;\n if (this.inflight === 0 && this.pendingClose) {\n const stale = this.pendingClose;\n this.pendingClose = undefined;\n await stale.close().catch(() => {\n /* no-op */\n });\n }\n }\n }\n return this.connectAndRun(headers, operation, false);\n }\n\n private async loadTools(connection: RemoteMcpConnection): 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 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(connection => this.loadTools(connection));\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(connection =>\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 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 | undefined;\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 if (connection !== undefined && this._connection === connection) {\n this.isConnected = false;\n }\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":"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","names":[]}
@@ -38,6 +38,8 @@ var RemoteMCP = class {
38
38
  sessionId;
39
39
  resolvedTransportType;
40
40
  cachedTools;
41
+ inflight = 0;
42
+ pendingClose;
41
43
  constructor(params) {
42
44
  this.name = params.name;
43
45
  this.id = params.id;
@@ -57,13 +59,29 @@ var RemoteMCP = class {
57
59
  getSessionId() {
58
60
  return this.sessionId ?? void 0;
59
61
  }
60
- get connection() {
61
- if (!this._connection) {
62
- throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);
62
+ // Concurrent callTool/listTools share one transport. Session-expired retry must not close it
63
+ // while a sibling is still using it — close() aborts the sibling with "Connection closed",
64
+ // which is not treated as session-expired, so that call is never retried.
65
+ //
66
+ // Example: A (long) and B share socket S. B gets session-expired.
67
+ // Detach S (pendingClose), B retries on a new socket, A finishes on S,
68
+ // last caller closes pendingClose when inflight hits 0.
69
+ //
70
+ // connectAndRun:
71
+ // conn = this._connection // capture; op does not re-read this._connection
72
+ // inflight++
73
+ // try: return op(conn)
74
+ // catch sessionExpired:
75
+ // detach conn // _connection = undefined; do not close if inflight > 0
76
+ // pendingClose = conn
77
+ // reconnect and retry once
78
+ // finally:
79
+ // inflight--
80
+ // if inflight == 0: close(pendingClose)
81
+ async resetConnection(expired) {
82
+ if (expired !== void 0 && this._connection !== expired) {
83
+ return;
63
84
  }
64
- return this._connection;
65
- }
66
- async resetConnection() {
67
85
  this.isConnected = false;
68
86
  this.sessionId = void 0;
69
87
  this.cachedTools = void 0;
@@ -71,9 +89,17 @@ var RemoteMCP = class {
71
89
  await this.closeAndClearConnection();
72
90
  }
73
91
  async closeAndClearConnection() {
74
- await this._connection?.close().catch(() => {
75
- });
92
+ const connection = this._connection;
76
93
  this._connection = void 0;
94
+ if (!connection) {
95
+ return;
96
+ }
97
+ if (this.inflight > 0) {
98
+ this.pendingClose = connection;
99
+ return;
100
+ }
101
+ await connection.close().catch(() => {
102
+ });
77
103
  }
78
104
  async resolveHeaders() {
79
105
  if (typeof this.headers !== "function") {
@@ -89,25 +115,42 @@ var RemoteMCP = class {
89
115
  return this.connectAndRun(headersResult.headers, operation, true);
90
116
  }
91
117
  async connectAndRun(headers, operation, canRetry) {
118
+ let used;
92
119
  try {
93
120
  const initInfo = await this.connectIfNeeded(headers);
94
- return { result: await operation(), wasInitialized: initInfo };
121
+ const connection = this._connection;
122
+ if (!connection) {
123
+ throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);
124
+ }
125
+ used = connection;
126
+ this.inflight += 1;
127
+ return { result: await operation(connection), wasInitialized: initInfo };
95
128
  } catch (error) {
96
- if (canRetry && isSessionExpiredError(error)) {
97
- this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);
98
- await this.resetConnection();
99
- return this.connectAndRun(headers, operation, false);
129
+ if (!(canRetry && isSessionExpiredError(error))) {
130
+ throw error;
131
+ }
132
+ this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);
133
+ await this.resetConnection(used);
134
+ } finally {
135
+ if (used) {
136
+ this.inflight -= 1;
137
+ if (this.inflight === 0 && this.pendingClose) {
138
+ const stale = this.pendingClose;
139
+ this.pendingClose = void 0;
140
+ await stale.close().catch(() => {
141
+ });
142
+ }
100
143
  }
101
- throw error;
102
144
  }
145
+ return this.connectAndRun(headers, operation, false);
103
146
  }
104
- async loadTools() {
147
+ async loadTools(connection) {
105
148
  return this.tracing.withRemoteMcpToolSpan(
106
149
  { method: "tools/list", serverName: this.name, serverId: this.id, serverUrl: this.traceUrl, enabled: true },
107
150
  async (span) => {
108
151
  const tools = await paginateWithCursorGuard(
109
152
  async (cursor) => {
110
- const page = await this.connection.listTools(cursor);
153
+ const page = await connection.listTools(cursor);
111
154
  return { items: page.tools, nextCursor: page.nextCursor };
112
155
  },
113
156
  this.name,
@@ -124,7 +167,7 @@ var RemoteMCP = class {
124
167
  if (this.cachedTools) {
125
168
  return { result: { tools: this.cachedTools }, wasInitialized: void 0 };
126
169
  }
127
- const response = await this.executeWithSessionRetry(() => this.loadTools());
170
+ const response = await this.executeWithSessionRetry((connection) => this.loadTools(connection));
128
171
  if ("authRequired" in response) {
129
172
  return response;
130
173
  }
@@ -132,7 +175,7 @@ var RemoteMCP = class {
132
175
  }
133
176
  async callTool(params) {
134
177
  const response = await this.executeWithSessionRetry(
135
- () => this.tracing.withRemoteMcpToolSpan(
178
+ (connection) => this.tracing.withRemoteMcpToolSpan(
136
179
  {
137
180
  method: "tools/call",
138
181
  serverName: this.name,
@@ -143,7 +186,7 @@ var RemoteMCP = class {
143
186
  enabled: true
144
187
  },
145
188
  async (span) => {
146
- const result = await this.connection.callTool(params);
189
+ const result = await connection.callTool(params);
147
190
  span.setOutput(JSON.stringify(result));
148
191
  return result;
149
192
  }
@@ -191,7 +234,9 @@ var RemoteMCP = class {
191
234
  maxResponseBytes: this.maxResponseBytes,
192
235
  signal: this.signal,
193
236
  onClose: () => {
194
- this.isConnected = false;
237
+ if (connection !== void 0 && this._connection === connection) {
238
+ this.isConnected = false;
239
+ }
195
240
  },
196
241
  onError: (error) => {
197
242
  const fields = extractErrorLogFields(error);
@@ -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 { 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":"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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 private inflight = 0;\n private pendingClose: RemoteMcpConnection | 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 // Concurrent callTool/listTools share one transport. Session-expired retry must not close it\n // while a sibling is still using it — close() aborts the sibling with \"Connection closed\",\n // which is not treated as session-expired, so that call is never retried.\n //\n // Example: A (long) and B share socket S. B gets session-expired.\n // Detach S (pendingClose), B retries on a new socket, A finishes on S,\n // last caller closes pendingClose when inflight hits 0.\n //\n // connectAndRun:\n // conn = this._connection // capture; op does not re-read this._connection\n // inflight++\n // try: return op(conn)\n // catch sessionExpired:\n // detach conn // _connection = undefined; do not close if inflight > 0\n // pendingClose = conn\n // reconnect and retry once\n // finally:\n // inflight--\n // if inflight == 0: close(pendingClose)\n private async resetConnection(expired?: RemoteMcpConnection): Promise<void> {\n if (expired !== undefined && this._connection !== expired) {\n return;\n }\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 const connection = this._connection;\n this._connection = undefined;\n if (!connection) {\n return;\n }\n if (this.inflight > 0) {\n // One leftover socket; a second session-expiry while the first is still pending can leak it.\n this.pendingClose = connection;\n return;\n }\n await connection.close().catch(() => {\n /* no-op */\n });\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>(\n operation: (connection: RemoteMcpConnection) => Promise<T>,\n ): 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: (connection: RemoteMcpConnection) => Promise<T>,\n canRetry: boolean,\n ): Promise<ExecuteResult<T>> {\n let used: RemoteMcpConnection | undefined;\n try {\n const initInfo = await this.connectIfNeeded(headers);\n const connection = this._connection;\n if (!connection) {\n throw new Error(`Remote MCP '${this.name}' not connected - connectIfNeeded() must run first`);\n }\n used = connection;\n this.inflight += 1;\n return { result: await operation(connection), wasInitialized: initInfo };\n } catch (error) {\n if (!(canRetry && isSessionExpiredError(error))) {\n throw error;\n }\n this.logger.info(`Session expired for remote MCP ${this.name}, reinitializing...`);\n await this.resetConnection(used);\n } finally {\n if (used) {\n this.inflight -= 1;\n if (this.inflight === 0 && this.pendingClose) {\n const stale = this.pendingClose;\n this.pendingClose = undefined;\n await stale.close().catch(() => {\n /* no-op */\n });\n }\n }\n }\n return this.connectAndRun(headers, operation, false);\n }\n\n private async loadTools(connection: RemoteMcpConnection): 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 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(connection => this.loadTools(connection));\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(connection =>\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 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 | undefined;\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 if (connection !== undefined && this._connection === connection) {\n this.isConnected = false;\n }\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":"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","names":[]}
@@ -1 +1 @@
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"}
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;AAE/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"}
@@ -30,15 +30,12 @@ var import_client = require("@modelcontextprotocol/sdk/client/index.js");
30
30
  var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
31
31
  var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
32
32
  var import_api = require("@opentelemetry/api");
33
- var import_undici = require("undici");
34
33
  var import_errors = require("../errors.js");
35
34
  var import_promiseUtils = require("../util/promiseUtils.js");
35
+ var import_ssrfGuard = require("../util/ssrfGuard.js");
36
36
  var CLIENT_INFO = { name: "tfy-agent-mcp-client", version: "1.0.0" };
37
37
  var TRANSPORT_PROBE_ORDER = ["streamable-http", "sse"];
38
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
39
  function withMaxResponseBytes(fetchFn, maxBytes) {
43
40
  return async (url, init) => {
44
41
  const response = await fetchFn(url, init);
@@ -148,7 +145,7 @@ function buildConnection(client, transport, transportType, headers, requestOptio
148
145
  async function connectRemoteMcp(params) {
149
146
  const url = new URL(params.url);
150
147
  const requestOptions = { signal: params.signal };
151
- const fetchFn = withMaxResponseBytes(mcpFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);
148
+ const fetchFn = withMaxResponseBytes(import_ssrfGuard.mcpSsrfFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);
152
149
  const candidates = params.knownTransportType ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter((t) => t !== params.knownTransportType)] : TRANSPORT_PROBE_ORDER;
153
150
  const failures = [];
154
151
  for (const transportType of candidates) {
@@ -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 { 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"]}
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 { McpConnectionError } from '../errors';\nimport { withTimeout } from '../util/promiseUtils';\nimport { mcpSsrfFetch } from '../util/ssrfGuard';\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/** 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(mcpSsrfFetch, 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAuB;AAGvB,iBAAmC;AACnC,4BAA8C;AAG9C,iBAAqC;AACrC,oBAAmC;AACnC,0BAA4B;AAC5B,uBAA6B;AAoB7B,IAAM,cAAc,EAAE,MAAM,wBAAwB,SAAS,QAAQ;AACrE,IAAM,wBAAkD,CAAC,mBAAmB,KAAK;AAE1E,IAAM,iCAAiC,KAAK,OAAO;AAGnD,SAAS,qBAAqB,SAAoB,UAA6B;AACpF,SAAO,OAAO,KAAK,SAAS;AAC1B,UAAM,WAAW,MAAM,QAAQ,KAAK,IAAI;AACxC,UAAM,YACH,MAAM,UAAU,OAAO,YAAY,MAAM,UACzC,SAAS,QAAQ,IAAI,cAAc,KAAK,IAAI,YAAY,EAAE,SAAS,mBAAmB;AACzF,QAAI,YAAY,CAAC,SAAS,MAAM;AAC9B,aAAO;AAAA,IACT;AACA,QAAI,OAAO;AACX,WAAO,IAAI;AAAA,MACT,SAAS,KAAK;AAAA,QACZ,IAAI,gBAAwC;AAAA,UAC1C,UAAU,OAAO,YAAY;AAC3B,oBAAQ,MAAM;AACd,gBAAI,OAAO,UAAU;AACnB,yBAAW,MAAM,IAAI,MAAM,6BAA6B,OAAO,QAAQ,CAAC,QAAQ,CAAC;AACjF;AAAA,YACF;AACA,uBAAW,QAAQ,KAAK;AAAA,UAC1B;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MACA,EAAE,QAAQ,SAAS,QAAQ,YAAY,SAAS,YAAY,SAAS,SAAS,QAAQ;AAAA,IACxF;AAAA,EACF;AACF;AAEA,IAAM,uBAAN,cAAmC,qBAAO;AAAA,EACxC,YAA6B,kBAA0B;AACrD,UAAM,aAAa,EAAE,cAAc,CAAC,EAAE,CAAC;AADZ;AAAA,EAE7B;AAAA,EAF6B;AAAA;AAAA;AAAA,EAMpB,QAAQ,KAAU,QAAa,SAA6B;AAEnE,WAAO,MAAM,QAAQ,KAAK,QAAQ,EAAE,GAAG,SAAS,SAAS,KAAK,iBAAiB,CAAC;AAAA,EAClF;AACF;AAEA,SAAS,gBACP,MACA,KACA,SACA,SACA,WACc;AACd,QAAM,cAAc,EAAE,QAAQ;AAC9B,MAAI,SAAS,mBAAmB;AAC9B,WAAO,IAAI,oDAA8B,KAAK;AAAA,MAC5C;AAAA,MACA,OAAO;AAAA,MACP,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjD,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,8BAAmB,KAAK,EAAE,aAAa,OAAO,QAAQ,CAAC;AACpE;AAEO,SAAS,sBAAsB,OAAyB;AAC7D,MAAI,EAAE,iBAAiB,QAAQ;AAC7B,WAAO;AAAA,EACT;AACA,MAAI,UAAU,SAAS,MAAM,SAAS,KAAK;AACzC,WAAO;AAAA,EACT;AACA,SAAO,MAAM,QAAQ,SAAS,UAAU,KAAK,MAAM,QAAQ,YAAY,EAAE,SAAS,SAAS;AAC7F;AAEA,SAAS,YAAY,OAAyB;AAC5C,MAAI,EAAE,iBAAiB,QAAQ;AAC7B,WAAO;AAAA,EACT;AACA,MAAI,UAAU,SAAS,MAAM,SAAS,KAAK;AACzC,WAAO;AAAA,EACT;AACA,SAAO,MAAM,QAAQ,SAAS,UAAU;AAC1C;AAGA,SAAS,kBAAkB,SAAuC;AAChE,yBAAY,OAAO,mBAAQ,OAAO,GAAG,OAAO;AAC9C;AAEA,SAAS,sBAAsB,WAAwC;AACrE,SAAO,qBAAqB,sDAAiC,UAAU,aAAa,OAAQ;AAC9F;AAEA,SAAS,eAAe,OAAoC;AAC1D,MAAI,iBAAiB,kCAAoB;AACvC,WAAO;AAAA,EACT;AACA,MAAI,YAAY,KAAK,GAAG;AACtB,WAAO,IAAI,iCAAmB,sCAAsC,KAAK;AAAA,MACvE,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,IAAI,iCAAmB,SAAS,KAAK,EAAE,OAAO,MAAM,CAAC;AAC9D;AAEA,SAAS,gBACP,QACA,WACA,eACA,SACA,gBACqB;AACrB,SAAO;AAAA,IACL;AAAA,IACA,WAAW,sBAAsB,SAAS;AAAA,IAC1C,WAAW,OAAO,WAAoB;AACpC,wBAAkB,OAAO;AACzB,YAAM,WAAW,MAAM,OAAO,UAAU,SAAS,EAAE,OAAO,IAAI,QAAW,cAAc;AACvF,aAAO;AAAA,QACL,OAAO,SAAS;AAAA,QAChB,YAAY,SAAS;AAAA,MACvB;AAAA,IACF;AAAA,IACA,UAAU,OAAO,eAAmE;AAClF,wBAAkB,OAAO;AACzB,aAAQ,MAAM,OAAO,SAAS,YAAY,QAAW,cAAc;AAAA,IACrE;AAAA,IACA,OAAO,YAA2B;AAChC,YAAM,OAAO,MAAM,EAAE,MAAM,MAAM;AAAA,MAEjC,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAUA,eAAsB,iBAAiB,QAWN;AAC/B,QAAM,MAAM,IAAI,IAAI,OAAO,GAAG;AAC9B,QAAM,iBAAiB,EAAE,QAAQ,OAAO,OAAO;AAC/C,QAAM,UAAU,qBAAqB,+BAAc,OAAO,oBAAoB,8BAA8B;AAC5G,QAAM,aAAa,OAAO,qBACtB,CAAC,OAAO,oBAAoB,GAAG,sBAAsB,OAAO,OAAK,MAAM,OAAO,kBAAkB,CAAC,IACjG;AACJ,QAAM,WAAmE,CAAC;AAE1E,aAAW,iBAAiB,YAAY;AACtC,UAAM,YAAY,gBAAgB,eAAe,KAAK,OAAO,SAAS,SAAS,OAAO,SAAS;AAC/F,UAAM,SAAS,IAAI,qBAAqB,OAAO,gBAAgB;AAK/D,UAAM,UAAU,IAAI,gBAAgB;AACpC,UAAM,iBAAiB,EAAE,QAAQ,YAAY,IAAI,CAAC,OAAO,QAAQ,QAAQ,MAAM,CAAC,EAAE;AAClF,QAAI;AACF,wBAAkB,OAAO,OAAO;AAChC,gBAAM;AAAA;AAAA;AAAA,QAGJ,OAAO,QAAQ,WAA+C,cAAc;AAAA,QAC5E,OAAO;AAAA,QACP;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM;AACd,YAAM,OAAO,MAAM,EAAE,MAAM,MAAM;AAAA,MAEjC,CAAC;AACD,UAAI,YAAY,KAAK,GAAG;AACtB,cAAM,eAAe,KAAK;AAAA,MAC5B;AAGA,UAAI,OAAO,aAAa,sBAAsB,KAAK,GAAG;AACpD,cAAM,eAAe,KAAK;AAAA,MAC5B;AACA,eAAS,KAAK,EAAE,WAAW,eAAe,OAAO,iBAAiB,QAAQ,MAAM,QAAQ,KAAK,IAAI,OAAO,KAAK,EAAE,CAAC;AAChH;AAAA,IACF;AAIA,WAAO,UAAU,MAAM,OAAO,UAAU;AACxC,WAAO,UAAU,WAAS,OAAO,UAAU,KAAK;AAChD,WAAO,gBAAgB,QAAQ,WAAW,eAAe,OAAO,SAAS,cAAc;AAAA,EACzF;AAEA,QAAM,IAAI,iCAAmB,4BAA4B,WAAW,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,QAAQ,CAAC,IAAI,GAAG;AACrH;","names":[]}
@@ -3,15 +3,12 @@ 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";
7
6
  import { McpConnectionError } from "../errors.mjs";
8
7
  import { withTimeout } from "../util/promiseUtils.mjs";
8
+ import { mcpSsrfFetch } from "../util/ssrfGuard.mjs";
9
9
  var CLIENT_INFO = { name: "tfy-agent-mcp-client", version: "1.0.0" };
10
10
  var TRANSPORT_PROBE_ORDER = ["streamable-http", "sse"];
11
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
12
  function withMaxResponseBytes(fetchFn, maxBytes) {
16
13
  return async (url, init) => {
17
14
  const response = await fetchFn(url, init);
@@ -121,7 +118,7 @@ function buildConnection(client, transport, transportType, headers, requestOptio
121
118
  async function connectRemoteMcp(params) {
122
119
  const url = new URL(params.url);
123
120
  const requestOptions = { signal: params.signal };
124
- const fetchFn = withMaxResponseBytes(mcpFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);
121
+ const fetchFn = withMaxResponseBytes(mcpSsrfFetch, params.maxResponseBytes ?? DEFAULT_MAX_MCP_RESPONSE_BYTES);
125
122
  const candidates = params.knownTransportType ? [params.knownTransportType, ...TRANSPORT_PROBE_ORDER.filter((t) => t !== params.knownTransportType)] : TRANSPORT_PROBE_ORDER;
126
123
  const failures = [];
127
124
  for (const transportType of candidates) {