Skip to content

Commit 9fb9ac5

Browse files
authored
diag(mcp): per-request phase logging on the streamable-HTTP transport (#5819)
* diag(mcp): per-request phase logging on the streamable-HTTP transport Mirrors the OAuth guarded-fetch logging that isolated the /oauth/start body stall. Logs each transport request (host+method), the response headers with TTFB, and failures — so a first-connect stall shows whether it hangs before response headers (connect/request) or after (the SDK's stream read), which isolates the client-side first-connect stall we haven't yet root-caused. * diag(mcp): include error name/code in transport failure log Distinguishes abort vs connect-refused vs TLS vs timeout in the failure-phase data.
1 parent 051c86f commit 9fb9ac5

1 file changed

Lines changed: 41 additions & 1 deletion

File tree

apps/sim/lib/mcp/pinned-fetch.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { validateMcpServerSsrf } from '@/lib/mcp/domain-check'
66
import { McpError } from '@/lib/mcp/types'
77

88
const logger = createLogger('McpOauthFetch')
9+
const transportLogger = createLogger('McpTransportFetch')
910

1011
/** Pinned fetch for the live MCP transport, plus a handle to release its sockets. */
1112
export interface PinnedMcpFetch {
@@ -25,7 +26,46 @@ export interface PinnedMcpFetch {
2526
*/
2627
export function createPinnedMcpFetch(resolvedIP: string): PinnedMcpFetch {
2728
const { fetch: pinnedFetch, dispatcher } = createPinnedFetchWithDispatcher(resolvedIP)
28-
return { fetch: pinnedFetch, close: () => dispatcher.destroy() }
29+
// Per-request phase logging: a stalled transport request (e.g. a first `initialize` that hangs
30+
// to the client timeout) shows whether it stalls BEFORE response headers ("request" with no
31+
// "response headers" = connect/request stall) or AFTER ("response headers" then the SDK's
32+
// stream read stalls). Isolates the client-side first-connect stall.
33+
const instrumentedFetch: typeof fetch = async (input, init) => {
34+
const method = init?.method ?? (input instanceof Request ? input.method : 'GET')
35+
const target =
36+
typeof input === 'string'
37+
? input
38+
: input instanceof URL
39+
? input.href
40+
: input instanceof Request
41+
? input.url
42+
: String(input)
43+
const host = URL.canParse(target) ? new URL(target).host : target
44+
const startedAt = Date.now()
45+
transportLogger.info('MCP transport request', { host, method })
46+
try {
47+
const response = await pinnedFetch(input, init)
48+
transportLogger.info('MCP transport response headers', {
49+
host,
50+
method,
51+
status: response.status,
52+
ttfbMs: Date.now() - startedAt,
53+
})
54+
return response
55+
} catch (error) {
56+
const e = error as { name?: string; code?: string; cause?: { name?: string; code?: string } }
57+
transportLogger.warn('MCP transport request failed', {
58+
host,
59+
method,
60+
ms: Date.now() - startedAt,
61+
errorName: e?.name,
62+
errorCode: e?.code ?? e?.cause?.code,
63+
causeName: e?.cause?.name,
64+
})
65+
throw error
66+
}
67+
}
68+
return { fetch: instrumentedFetch, close: () => dispatcher.destroy() }
2969
}
3070

3171
/**

0 commit comments

Comments
 (0)