@truefoundry/trueforge-core 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +4 -0
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/llm/VercelAILLM.d.ts.map +1 -1
- package/dist/core/llm/VercelAILLM.js +7 -0
- package/dist/core/llm/VercelAILLM.js.map +1 -1
- package/dist/core/llm/VercelAILLM.mjs +7 -0
- package/dist/core/llm/VercelAILLM.mjs.map +1 -1
- package/dist/core/mcp/RemoteMCP.d.ts +2 -1
- package/dist/core/mcp/RemoteMCP.d.ts.map +1 -1
- package/dist/core/mcp/RemoteMCP.js +65 -20
- package/dist/core/mcp/RemoteMCP.js.map +1 -1
- package/dist/core/mcp/RemoteMCP.mjs +65 -20
- package/dist/core/mcp/RemoteMCP.mjs.map +1 -1
- package/dist/core/mcp/remoteMcpClient.d.ts.map +1 -1
- package/dist/core/mcp/remoteMcpClient.js +2 -5
- package/dist/core/mcp/remoteMcpClient.js.map +1 -1
- package/dist/core/mcp/remoteMcpClient.mjs +2 -5
- package/dist/core/mcp/remoteMcpClient.mjs.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.d.ts.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.js +19 -6
- package/dist/core/sandbox/provider/TFYSandboxProvider.js.map +1 -1
- package/dist/core/sandbox/provider/TFYSandboxProvider.mjs +19 -7
- package/dist/core/sandbox/provider/TFYSandboxProvider.mjs.map +1 -1
- package/dist/core/util/ssrfGuard.d.ts +10 -0
- package/dist/core/util/ssrfGuard.d.ts.map +1 -0
- package/dist/core/util/ssrfGuard.js +333 -0
- package/dist/core/util/ssrfGuard.js.map +1 -0
- package/dist/core/util/ssrfGuard.mjs +305 -0
- package/dist/core/util/ssrfGuard.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/mcp/RemoteMCP.ts"],"sourcesContent":["import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';\nimport type { Logger } from 'winston';\nimport { McpConnectionError } from '../errors';\nimport type { MCPServerInitInfo } from '../events/schema';\nimport type { InternalToolCallInfo } from '../llm/LLMTypes';\nimport type { AgentTracing } from '../tracing/AgentTracing';\nimport { NOOP_AGENT_TRACING } from '../tracing/NoopAgentTracing';\nimport { 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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
|
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.
|
|
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;
|
|
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(
|
|
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":"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","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(
|
|
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) {
|
|
@@ -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":";AAAA,SAAS,cAAc;AAGvB,SAAS,0BAA0B;AACnC,SAAS,qCAAqC;AAG9C,SAAS,SAAS,mBAAmB;AACrC,SAAS,OAAO,SAAS,mBAAmB;AAC5C,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;AAoB5B,IAAM,cAAc,EAAE,MAAM,wBAAwB,SAAS,QAAQ;AACrE,IAAM,wBAAkD,CAAC,mBAAmB,KAAK;AAE1E,IAAM,iCAAiC,KAAK,OAAO;AAM1D,IAAM,sBAAsB,KAAK,KAAK;AACtC,IAAM,eAAe,IAAI,MAAM,EAAE,aAAa,oBAAoB,CAAC;AACnE,IAAM,WAAsB,CAAC,KAAK,SAChC,YAAY,OAAO,QAAQ,WAAW,MAAM,IAAI,MAAM,EAAE,GAAI,MAAiB,YAAY,aAAa,CAAC;AAGlG,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,OAAO;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,8BAA8B,KAAK;AAAA,MAC5C;AAAA,MACA,OAAO;AAAA,MACP,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjD,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,mBAAmB,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,cAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;AAC9C;AAEA,SAAS,sBAAsB,WAAwC;AACrE,SAAO,qBAAqB,gCAAiC,UAAU,aAAa,OAAQ;AAC9F;AAEA,SAAS,eAAe,OAAoC;AAC1D,MAAI,iBAAiB,oBAAoB;AACvC,WAAO;AAAA,EACT;AACA,MAAI,YAAY,KAAK,GAAG;AACtB,WAAO,IAAI,mBAAmB,sCAAsC,KAAK;AAAA,MACvE,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,IAAI,mBAAmB,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,UAAU,OAAO,oBAAoB,8BAA8B;AACxG,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,YAAM;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,mBAAmB,4BAA4B,WAAW,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,QAAQ,CAAC,IAAI,GAAG;AACrH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/core/mcp/remoteMcpClient.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\n// SSE remains required during the Streamable HTTP migration (some servers still speak SSE only).\n\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { context, propagation } from '@opentelemetry/api';\nimport { 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":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TFYSandboxProvider.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/provider/TFYSandboxProvider.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAUvE,OAAO,
|
|
1
|
+
{"version":3,"file":"TFYSandboxProvider.d.ts","sourceRoot":"","sources":["../../../../src/core/sandbox/provider/TFYSandboxProvider.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAUvE,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AAWpB,4GAA4G;AAC5G,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGxD;AAWD,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,EAAE,MAAM,CAAC;IAChC,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB;AAOD,qBAAa,kBAAmB,YAAW,eAAe;IACxD,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAS;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8D;IAE7F,YAAY,OAAO,EAAE,yBAAyB,EAO7C;IAGD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAIhC;IAEF,UAAU,IAAI,OAAO,CAAC,YAAY,CAAC,CAElC;IAED,mBAAmB,IAAI,OAAO,CAAC,YAAY,CAAC,CAE3C;IAED,aAAa,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAI9C;IAED;;;;OAIG;IACG,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CASzD;YAEa,aAAa;YAoBb,QAAQ;YAiDR,WAAW;IAiBnB,YAAY,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB/E;IAEK,UAAU,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlG;IAGD,uBAAuB,IAAI,iBAAiB,CAO3C;IAED,yBAAyB,IAAI,MAAM,CAQlC;IAKD,oBAAoB,IAAI,MAAM,CAE7B;IAED,qBAAqB,IAAI,MAAM,CAE9B;IAED,iBAAiB,IAAI,MAAM,CAE1B;IAED,YAAY,IAAI,MAAM,CAErB;IAED,sBAAsB,IAAI,MAAM,CAE/B;CACF"}
|
|
@@ -47,6 +47,7 @@ var import_execEnv = require("./execEnv.js");
|
|
|
47
47
|
var import_Provider = require("./Provider.js");
|
|
48
48
|
var DEFAULT_TIMEOUT_SECONDS = 60;
|
|
49
49
|
var CLIENT_TIMEOUT_BUFFER_SECONDS = 5;
|
|
50
|
+
var FILE_UPLOAD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
50
51
|
var TFY_MCP_CLIENT_BIN = "mcp-client/bin";
|
|
51
52
|
function withMcpClientOnPath(path) {
|
|
52
53
|
const rest = path.split(":").filter((part) => part.length > 0 && part !== TFY_MCP_CLIENT_BIN);
|
|
@@ -206,13 +207,25 @@ var TFYSandboxProvider = class _TFYSandboxProvider {
|
|
|
206
207
|
return Buffer.from(result.response.result.trim(), "base64");
|
|
207
208
|
}
|
|
208
209
|
async uploadFile(params) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
(0, import_SandboxErrors.validateSandboxOwnedByTenant)({ sandboxId: params.sandboxId, tenantName: this.tenantName });
|
|
211
|
+
return import_api.context.with((0, import_core.suppressTracing)(import_api.context.active()), async () => {
|
|
212
|
+
const query = new URLSearchParams({ sandbox_id: params.sandboxId, path: params.remotePath });
|
|
213
|
+
const response = await fetch(`${this.serverUrl}/files/upload?${query.toString()}`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
216
|
+
body: params.content,
|
|
217
|
+
signal: AbortSignal.timeout(FILE_UPLOAD_TIMEOUT_MS)
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`File upload to sandbox failed: Sandbox server returned ${String(response.status)}: ${await response.text()}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const result = await response.json();
|
|
225
|
+
if (!result.success) {
|
|
226
|
+
throw new Error(`File upload to sandbox failed: ${result.error}`);
|
|
227
|
+
}
|
|
214
228
|
});
|
|
215
|
-
(0, import_Provider.ensureExecSuccess)(result);
|
|
216
229
|
}
|
|
217
230
|
// The TFY sandbox exposes a static, cluster-internal NATS WebSocket URL (no signed URLs).
|
|
218
231
|
createCodeModeTransport() {
|