Reduce remaining MCP and persistence debug detail

This commit is contained in:
2026-04-04 09:52:38 +08:00
parent 02f22d80bd
commit 010ded8476
4 changed files with 240 additions and 72 deletions

View File

@@ -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<string, boolean | number | string> = {}
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',

View File

@@ -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<string, boolean | number | string> = {}
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,

View File

@@ -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 {

View File

@@ -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