|
1 | 1 | import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js' |
2 | 2 | import { createLogger } from '@sim/logger' |
3 | 3 | import { sanitizeForLogging } from '@/lib/core/security/redaction' |
| 4 | +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' |
4 | 5 | import { getMcpSafeErrorDiagnostics } from '@/lib/mcp/error-diagnostics' |
5 | 6 |
|
6 | 7 | const logger = createLogger('McpHttpDiag') |
@@ -28,15 +29,8 @@ function diagnosticsEnabled(): boolean { |
28 | 29 | return process.env.MCP_HTTP_DIAGNOSTICS !== 'false' |
29 | 30 | } |
30 | 31 |
|
31 | | -/** Origin + path only — query values can carry `code`/`token`/`api_key`, so they're dropped. */ |
32 | | -function safeUrl(input: string | URL | Request): string { |
33 | | - const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url |
34 | | - try { |
35 | | - const u = new URL(raw) |
36 | | - return u.search ? `${u.origin}${u.pathname}?<redacted>` : `${u.origin}${u.pathname}` |
37 | | - } catch { |
38 | | - return '<unparseable-url>' |
39 | | - } |
| 32 | +function rawUrl(input: string | URL | Request): string { |
| 33 | + return typeof input === 'string' ? input : input instanceof URL ? input.href : input.url |
40 | 34 | } |
41 | 35 |
|
42 | 36 | /** |
@@ -65,7 +59,7 @@ export function withMcpHttpDiagnostics( |
65 | 59 | if (!diagnosticsEnabled()) return fetchFn |
66 | 60 |
|
67 | 61 | return async (input, init) => { |
68 | | - const url = safeUrl(input as string | URL | Request) |
| 62 | + const url = sanitizeUrlForLog(rawUrl(input as string | URL | Request)) |
69 | 63 | const method = init?.method ?? 'GET' |
70 | 64 | const reqHeaders = new Headers((init?.headers as HeadersInit | undefined) ?? undefined) |
71 | 65 | const startedAt = Date.now() |
@@ -113,8 +107,18 @@ export function withMcpHttpDiagnostics( |
113 | 107 | return res |
114 | 108 | } |
115 | 109 |
|
| 110 | + // Cancel the detached log reader when the caller aborts (e.g. the SDK's 30s |
| 111 | + // initialize timeout — exactly the hang we're tracing) so this tee branch can't |
| 112 | + // keep the response stream / connection alive after the SDK has given up. |
| 113 | + const signal = init?.signal |
116 | 114 | void (async () => { |
117 | 115 | const reader = logBranch.getReader() |
| 116 | + const cancelReader = () => void reader.cancel().catch(() => {}) |
| 117 | + if (signal?.aborted) { |
| 118 | + cancelReader() |
| 119 | + return |
| 120 | + } |
| 121 | + signal?.addEventListener('abort', cancelReader, { once: true }) |
118 | 122 | const decoder = new TextDecoder() |
119 | 123 | let chunks = 0 |
120 | 124 | let bytes = 0 |
@@ -150,6 +154,8 @@ export function withMcpHttpDiagnostics( |
150 | 154 | ms: Date.now() - startedAt, |
151 | 155 | error: getMcpSafeErrorDiagnostics(error), |
152 | 156 | }) |
| 157 | + } finally { |
| 158 | + signal?.removeEventListener('abort', cancelReader) |
153 | 159 | } |
154 | 160 | })() |
155 | 161 |
|
|
0 commit comments