diff --git a/src/services/mcp/auth.ts b/src/services/mcp/auth.ts index ffc1870..5edd215 100644 --- a/src/services/mcp/auth.ts +++ b/src/services/mcp/auth.ts @@ -118,6 +118,54 @@ function summarizeHeadersForDebug( } } +function extractHttpStatusFromErrorMessage(message: string): number | undefined { + const statusMatch = message.match(/^HTTP (\d{3}):/) + if (!statusMatch) { + return undefined + } + return Number(statusMatch[1]) +} + +function summarizeOAuthErrorForDebug(error: unknown): string { + const summary: Record = {} + + if (error instanceof Error) { + summary.errorType = error.constructor.name + summary.errorName = error.name + summary.hasMessage = error.message.length > 0 + + const httpStatus = extractHttpStatusFromErrorMessage(error.message) + if (httpStatus !== undefined) { + summary.httpStatus = httpStatus + } + + if (error instanceof OAuthError) { + summary.oauthErrorCode = error.errorCode + } + } else { + summary.errorType = typeof error + summary.hasValue = error !== undefined && error !== null + } + + const errno = getErrnoCode(error) + if (errno) { + summary.errno = errno + } + + if (axios.isAxiosError(error)) { + summary.errorType = 'AxiosError' + if (error.code) { + summary.axiosCode = error.code + } + if (typeof error.response?.status === 'number') { + summary.httpStatus = error.response.status + } + summary.hasResponseData = error.response?.data !== undefined + } + + return jsonStringify(summary) +} + /** * Some OAuth servers (notably Slack) return HTTP 200 for all responses, * signaling errors via the JSON body instead. The SDK's executeTokenRequest @@ -289,7 +337,9 @@ async function fetchAuthServerMetadata( // to the legacy path-aware retry. logMCPDebug( serverName, - `RFC 9728 discovery failed, falling back: ${errorMessage(err)}`, + `RFC 9728 discovery failed, falling back: ${summarizeOAuthErrorForDebug( + err, + )}`, ) } @@ -511,7 +561,7 @@ export async function revokeServerTokens( : 'client_secret_basic' logMCPDebug( serverName, - `Revoking tokens via ${revocationEndpointStr} (${authMethod})`, + `Revoking tokens via discovered OAuth revocation endpoint (${authMethod})`, ) // Revoke refresh token first (more important - prevents future access token generation) @@ -531,7 +581,9 @@ export async function revokeServerTokens( // Log but continue logMCPDebug( serverName, - `Failed to revoke refresh token: ${errorMessage(error)}`, + `Failed to revoke refresh token: ${summarizeOAuthErrorForDebug( + error, + )}`, ) } } @@ -552,7 +604,9 @@ export async function revokeServerTokens( } catch (error: unknown) { logMCPDebug( serverName, - `Failed to revoke access token: ${errorMessage(error)}`, + `Failed to revoke access token: ${summarizeOAuthErrorForDebug( + error, + )}`, ) } } @@ -560,7 +614,10 @@ export async function revokeServerTokens( } } catch (error: unknown) { // Log error but don't throw - revocation is best-effort - logMCPDebug(serverName, `Failed to revoke tokens: ${errorMessage(error)}`) + logMCPDebug( + serverName, + `Failed to revoke tokens: ${summarizeOAuthErrorForDebug(error)}`, + ) } } else { logMCPDebug(serverName, 'No tokens to revoke') @@ -914,10 +971,7 @@ export async function performMCPOAuthFlow( try { resourceMetadataUrl = new URL(cachedResourceMetadataUrl) } catch { - logMCPDebug( - serverName, - `Invalid cached resourceMetadataUrl: ${cachedResourceMetadataUrl}`, - ) + logMCPDebug(serverName, 'Invalid cached resource metadata URL') } } const wwwAuthParams: WWWAuthenticateParams = { @@ -987,7 +1041,7 @@ export async function performMCPOAuthFlow( } catch (error) { logMCPDebug( serverName, - `Failed to fetch OAuth metadata: ${errorMessage(error)}`, + `Failed to fetch OAuth metadata: ${summarizeOAuthErrorForDebug(error)}`, ) } @@ -1184,7 +1238,10 @@ export async function performMCPOAuthFlow( ) } } catch (error) { - logMCPDebug(serverName, `SDK auth error: ${errorMessage(error)}`) + logMCPDebug( + serverName, + `SDK auth error: ${summarizeOAuthErrorForDebug(error)}`, + ) cleanup() rejectOnce(new Error(`SDK auth failed: ${errorMessage(error)}`)) } @@ -1258,7 +1315,7 @@ export async function performMCPOAuthFlow( } catch (error) { logMCPDebug( serverName, - `Error during auth completion: ${errorMessage(error)}`, + `Error during auth completion: ${summarizeOAuthErrorForDebug(error)}`, ) // Determine failure reason for attribution telemetry. The try block covers @@ -1300,9 +1357,9 @@ export async function performMCPOAuthFlow( // SDK does not attach HTTP status as a property, but the fallback ServerError // embeds it in the message as "HTTP {status}:" when the response body was // unparseable. Best-effort extraction. - const statusMatch = error.message.match(/^HTTP (\d{3}):/) - if (statusMatch) { - httpStatus = Number(statusMatch[1]) + const parsedStatus = extractHttpStatusFromErrorMessage(error.message) + if (parsedStatus !== undefined) { + httpStatus = parsedStatus } // If client not found, clear the stored client ID and suggest retry if ( @@ -1608,7 +1665,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider { } catch (e) { logMCPDebug( this.serverName, - `XAA silent exchange failed: ${errorMessage(e)}`, + `XAA silent exchange failed: ${summarizeOAuthErrorForDebug(e)}`, ) } // Fall through. Either id_token isn't cached (xaaRefresh returned @@ -1681,7 +1738,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider { } catch (error) { logMCPDebug( this.serverName, - `Token refresh error: ${errorMessage(error)}`, + `Token refresh error: ${summarizeOAuthErrorForDebug(error)}`, ) } } @@ -1796,7 +1853,9 @@ export class ClaudeAuthProvider implements OAuthClientProvider { } catch (e) { logMCPDebug( this.serverName, - `XAA: OIDC discovery failed in silent refresh: ${errorMessage(e)}`, + `XAA: OIDC discovery failed in silent refresh: ${summarizeOAuthErrorForDebug( + e, + )}`, ) return undefined } @@ -2055,7 +2114,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider { if (metadataUrl) { logMCPDebug( this.serverName, - `Fetching metadata from configured URL: ${metadataUrl}`, + 'Fetching metadata from configured override URL', ) try { const metadata = await fetchAuthServerMetadata( @@ -2073,7 +2132,9 @@ export class ClaudeAuthProvider implements OAuthClientProvider { } catch (error) { logMCPDebug( this.serverName, - `Failed to fetch from configured metadata URL: ${errorMessage(error)}`, + `Failed to fetch from configured metadata URL: ${summarizeOAuthErrorForDebug( + error, + )}`, ) } } @@ -2225,7 +2286,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider { } else if (cached?.authorizationServerUrl) { logMCPDebug( this.serverName, - `Re-discovering metadata from persisted auth server URL: ${cached.authorizationServerUrl}`, + 'Re-discovering metadata from persisted auth server URL', ) metadata = await discoverAuthorizationServerMetadata( cached.authorizationServerUrl, @@ -2281,10 +2342,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider { // Invalid grant means the refresh token itself is invalid/revoked/expired. // But another process may have already refreshed successfully — check first. if (error instanceof InvalidGrantError) { - logMCPDebug( - this.serverName, - `Token refresh failed with invalid_grant: ${error.message}`, - ) + logMCPDebug(this.serverName, 'Token refresh failed with invalid_grant') clearKeychainCache() const storage = getSecureStorage() const data = storage.read() @@ -2331,7 +2389,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider { if (!isRetryable || attempt >= MAX_ATTEMPTS) { logMCPDebug( this.serverName, - `Token refresh failed: ${errorMessage(error)}`, + `Token refresh failed: ${summarizeOAuthErrorForDebug(error)}`, ) emitRefreshEvent( 'failure', diff --git a/src/services/mcp/client.ts b/src/services/mcp/client.ts index 306be0a..2f41227 100644 --- a/src/services/mcp/client.ts +++ b/src/services/mcp/client.ts @@ -377,6 +377,49 @@ function summarizeStderrForDebug(stderrOutput: string): string { return `Server stderr captured (${trimmed.length} chars, ${lineCount} lines)` } +function summarizeMcpErrorForDebug(error: unknown): string { + const summary: Record = {} + + if (error instanceof Error) { + summary.errorType = error.constructor.name + summary.errorName = error.name + summary.hasMessage = error.message.length > 0 + summary.hasStack = Boolean(error.stack) + + const errorObj = error as Error & { + code?: unknown + errno?: unknown + syscall?: unknown + status?: unknown + cause?: unknown + } + + if (typeof errorObj.code === 'string' || typeof errorObj.code === 'number') { + summary.code = errorObj.code + } + if ( + typeof errorObj.errno === 'string' || + typeof errorObj.errno === 'number' + ) { + summary.errno = errorObj.errno + } + if (typeof errorObj.syscall === 'string') { + summary.syscall = errorObj.syscall + } + if (typeof errorObj.status === 'number') { + summary.status = errorObj.status + } + if (errorObj.cause !== undefined) { + summary.hasCause = true + } + } else { + summary.errorType = typeof error + summary.hasValue = error !== undefined && error !== null + } + + return jsonStringify(summary) +} + /** * Shared handler for sse/http/claudeai-proxy auth failures during connect: * emits tengu_mcp_server_needs_auth, caches the needs-auth entry, and returns @@ -1077,20 +1120,22 @@ export const connectToServer = memoize( ) try { const testUrl = new URL(serverRef.url) - logMCPDebug( - name, - `Parsed URL: host=${testUrl.hostname}, port=${testUrl.port || 'default'}, protocol=${testUrl.protocol}`, - ) + logMCPDebug(name, 'Parsed HTTP endpoint for preflight checks') // Log DNS resolution attempt if ( testUrl.hostname === '127.0.0.1' || testUrl.hostname === 'localhost' ) { - logMCPDebug(name, `Using loopback address: ${testUrl.hostname}`) + logMCPDebug(name, 'Using loopback HTTP endpoint') } } catch (urlError) { - logMCPDebug(name, `Failed to parse URL: ${urlError}`) + logMCPDebug( + name, + `Failed to parse HTTP endpoint for preflight: ${summarizeMcpErrorForDebug( + urlError, + )}`, + ) } } @@ -1142,30 +1187,29 @@ export const connectToServer = memoize( if (serverRef.type === 'sse' && error instanceof Error) { logMCPDebug( name, - `SSE Connection failed after ${elapsed}ms: ${jsonStringify({ - url: serverRef.url, - error: error.message, - errorType: error.constructor.name, - stack: error.stack, - })}`, + `SSE connection failed after ${elapsed}ms: ${summarizeMcpErrorForDebug( + error, + )}`, + ) + logMCPError( + name, + `SSE connection failed: ${summarizeMcpErrorForDebug(error)}`, ) - logMCPError(name, error) if (error instanceof UnauthorizedError) { return handleRemoteAuthFailure(name, serverRef, 'sse') } } else if (serverRef.type === 'http' && error instanceof Error) { - const errorObj = error as Error & { - cause?: unknown - code?: string - errno?: string | number - syscall?: string - } logMCPDebug( name, - `HTTP Connection failed after ${elapsed}ms: ${error.message} (code: ${errorObj.code || 'none'}, errno: ${errorObj.errno || 'none'})`, + `HTTP connection failed after ${elapsed}ms: ${summarizeMcpErrorForDebug( + error, + )}`, + ) + logMCPError( + name, + `HTTP connection failed: ${summarizeMcpErrorForDebug(error)}`, ) - logMCPError(name, error) if (error instanceof UnauthorizedError) { return handleRemoteAuthFailure(name, serverRef, 'http') @@ -1176,9 +1220,16 @@ export const connectToServer = memoize( ) { logMCPDebug( name, - `claude.ai proxy connection failed after ${elapsed}ms: ${error.message}`, + `claude.ai proxy connection failed after ${elapsed}ms: ${summarizeMcpErrorForDebug( + error, + )}`, + ) + logMCPError( + name, + `claude.ai proxy connection failed: ${summarizeMcpErrorForDebug( + error, + )}`, ) - logMCPError(name, error) // StreamableHTTPError has a `code` property with the HTTP status const errorCode = (error as Error & { code?: number }).code @@ -1257,7 +1308,9 @@ export const connectToServer = memoize( } catch (error) { logMCPError( name, - `Failed to send ide_connected notification: ${error}`, + `Failed to send ide_connected notification: ${summarizeMcpErrorForDebug( + error, + )}`, ) } } @@ -1291,7 +1344,10 @@ export const connectToServer = memoize( hasTriggeredClose = true logMCPDebug(name, `Closing transport (${reason})`) void client.close().catch(e => { - logMCPDebug(name, `Error during close: ${errorMessage(e)}`) + logMCPDebug( + name, + `Error during close: ${summarizeMcpErrorForDebug(e)}`, + ) }) } @@ -1355,7 +1411,10 @@ export const connectToServer = memoize( `Failed to spawn process - check command and permissions`, ) } else { - logMCPDebug(name, `Connection error: ${error.message}`) + logMCPDebug( + name, + `Connection error: ${summarizeMcpErrorForDebug(error)}`, + ) } } @@ -1456,12 +1515,20 @@ export const connectToServer = memoize( try { await inProcessServer.close() } catch (error) { - logMCPDebug(name, `Error closing in-process server: ${error}`) + logMCPDebug( + name, + `Error closing in-process server: ${summarizeMcpErrorForDebug( + error, + )}`, + ) } try { await client.close() } catch (error) { - logMCPDebug(name, `Error closing client: ${error}`) + logMCPDebug( + name, + `Error closing client: ${summarizeMcpErrorForDebug(error)}`, + ) } return } @@ -1487,7 +1554,10 @@ export const connectToServer = memoize( try { process.kill(childPid, 'SIGINT') } catch (error) { - logMCPDebug(name, `Error sending SIGINT: ${error}`) + logMCPDebug( + name, + `Error sending SIGINT: ${summarizeMcpErrorForDebug(error)}`, + ) return } @@ -1541,7 +1611,12 @@ export const connectToServer = memoize( try { process.kill(childPid, 'SIGTERM') } catch (termError) { - logMCPDebug(name, `Error sending SIGTERM: ${termError}`) + logMCPDebug( + name, + `Error sending SIGTERM: ${summarizeMcpErrorForDebug( + termError, + )}`, + ) resolved = true clearInterval(checkInterval) clearTimeout(failsafeTimeout) @@ -1574,7 +1649,9 @@ export const connectToServer = memoize( } catch (killError) { logMCPDebug( name, - `Error sending SIGKILL: ${killError}`, + `Error sending SIGKILL: ${summarizeMcpErrorForDebug( + killError, + )}`, ) } } catch { @@ -1606,7 +1683,12 @@ export const connectToServer = memoize( }) } } catch (processError) { - logMCPDebug(name, `Error terminating process: ${processError}`) + logMCPDebug( + name, + `Error terminating process: ${summarizeMcpErrorForDebug( + processError, + )}`, + ) } } @@ -1614,7 +1696,10 @@ export const connectToServer = memoize( try { await client.close() } catch (error) { - logMCPDebug(name, `Error closing client: ${error}`) + logMCPDebug( + name, + `Error closing client: ${summarizeMcpErrorForDebug(error)}`, + ) } } @@ -1671,9 +1756,14 @@ export const connectToServer = memoize( }) logMCPDebug( name, - `Connection failed after ${connectionDurationMs}ms: ${errorMessage(error)}`, + `Connection failed after ${connectionDurationMs}ms: ${summarizeMcpErrorForDebug( + error, + )}`, + ) + logMCPError( + name, + `Connection failed: ${summarizeMcpErrorForDebug(error)}`, ) - logMCPError(name, `Connection failed: ${errorMessage(error)}`) if (inProcessServer) { inProcessServer.close().catch(() => {}) @@ -2038,7 +2128,10 @@ export const fetchToolsForClient = memoizeWithLRU( }) .filter(isIncludedMcpTool) } catch (error) { - logMCPError(client.name, `Failed to fetch tools: ${errorMessage(error)}`) + logMCPError( + client.name, + `Failed to fetch tools: ${summarizeMcpErrorForDebug(error)}`, + ) return [] } }, @@ -2070,7 +2163,7 @@ export const fetchResourcesForClient = memoizeWithLRU( } catch (error) { logMCPError( client.name, - `Failed to fetch resources: ${errorMessage(error)}`, + `Failed to fetch resources: ${summarizeMcpErrorForDebug(error)}`, ) return [] } @@ -2136,7 +2229,9 @@ export const fetchCommandsForClient = memoizeWithLRU( } catch (error) { logMCPError( client.name, - `Error running command '${prompt.name}': ${errorMessage(error)}`, + `Error running command '${prompt.name}': ${summarizeMcpErrorForDebug( + error, + )}`, ) throw error } @@ -2146,7 +2241,7 @@ export const fetchCommandsForClient = memoizeWithLRU( } catch (error) { logMCPError( client.name, - `Failed to fetch commands: ${errorMessage(error)}`, + `Failed to fetch commands: ${summarizeMcpErrorForDebug(error)}`, ) return [] } @@ -2247,7 +2342,10 @@ export async function reconnectMcpServerImpl( } } catch (error) { // Handle errors gracefully - connection might have closed during fetch - logMCPError(name, `Error during reconnection: ${errorMessage(error)}`) + logMCPError( + name, + `Error during reconnection: ${summarizeMcpErrorForDebug(error)}`, + ) // Return with failed status return { @@ -2422,7 +2520,9 @@ export async function getMcpToolsCommandsAndResources( // Handle errors gracefully - connection might have closed during fetch logMCPError( name, - `Error fetching tools/commands/resources: ${errorMessage(error)}`, + `Error fetching tools/commands/resources: ${summarizeMcpErrorForDebug( + error, + )}`, ) // Still update with the client but no tools/commands @@ -2509,7 +2609,7 @@ export function prefetchAllMcpResources( }, mcpConfigs).catch(error => { logMCPError( 'prefetchAllMcpResources', - `Failed to get MCP resources: ${errorMessage(error)}`, + `Failed to get MCP resources: ${summarizeMcpErrorForDebug(error)}`, ) // Still resolve with empty results void resolve({ @@ -3371,7 +3471,12 @@ export async function setupSdkMcpClients( } } catch (error) { // If connection fails, return failed server - logMCPError(name, `Failed to connect SDK MCP server: ${error}`) + logMCPError( + name, + `Failed to connect SDK MCP server: ${summarizeMcpErrorForDebug( + error, + )}`, + ) return { client: { type: 'failed' as const, diff --git a/src/services/mcp/config.ts b/src/services/mcp/config.ts index c5cee9c..c3c8e8d 100644 --- a/src/services/mcp/config.ts +++ b/src/services/mcp/config.ts @@ -1397,6 +1397,7 @@ export function parseMcpConfigFromFilePath(params: { configContent = fs.readFileSync(filePath, { encoding: 'utf8' }) } catch (error: unknown) { const code = getErrnoCode(error) + const fileName = parse(filePath).base if (code === 'ENOENT') { return { config: null, @@ -1415,7 +1416,7 @@ export function parseMcpConfigFromFilePath(params: { } } logForDebugging( - `MCP config read error for ${filePath} (scope=${scope}): ${error}`, + `MCP config read error (scope=${scope}, file=${fileName}, errno=${code ?? 'none'}, errorType=${error instanceof Error ? error.name : typeof error})`, { level: 'error' }, ) return { @@ -1439,7 +1440,7 @@ export function parseMcpConfigFromFilePath(params: { if (!parsedJson) { logForDebugging( - `MCP config is not valid JSON: ${filePath} (scope=${scope}, length=${configContent.length}, first100=${jsonStringify(configContent.slice(0, 100))})`, + `MCP config is not valid JSON (scope=${scope}, file=${parse(filePath).base}, length=${configContent.length})`, { level: 'error' }, ) return { diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index 6d775d6..b47a143 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -1344,7 +1344,11 @@ class Project { setRemoteIngressUrl(url: string): void { this.remoteIngressUrl = url - logForDebugging(`Remote persistence enabled with URL: ${url}`) + logForDebugging( + url + ? 'Remote persistence enabled (remote ingress configured)' + : 'Remote persistence disabled', + ) if (url) { // If using CCR, don't delay messages by any more than 10ms. this.FLUSH_INTERVAL_MS = REMOTE_FLUSH_INTERVAL_MS