From 4d506aabf737ceecca3ab75fea2fb170d22a8c51 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sat, 4 Apr 2026 10:31:29 +0800 Subject: [PATCH] Reduce remaining file, LSP, and XAA debug detail --- src/services/api/filesApi.ts | 70 +++++++++++++++++++------- src/services/lsp/passiveFeedback.ts | 55 ++++++++++++++++---- src/services/mcp/xaa.ts | 75 +++++++++++++++++----------- src/services/mcp/xaaIdpLogin.ts | 23 +++------ src/tools/BriefTool/upload.ts | 59 +++++++++++++++++++--- src/tools/McpAuthTool/McpAuthTool.ts | 19 +++---- 6 files changed, 211 insertions(+), 90 deletions(-) diff --git a/src/services/api/filesApi.ts b/src/services/api/filesApi.ts index cb9a03b..7c4d55f 100644 --- a/src/services/api/filesApi.ts +++ b/src/services/api/filesApi.ts @@ -14,9 +14,10 @@ import * as path from 'path' import { count } from '../../utils/array.js' import { getCwd } from '../../utils/cwd.js' import { logForDebugging } from '../../utils/debug.js' -import { errorMessage } from '../../utils/errors.js' +import { errorMessage, getErrnoCode } from '../../utils/errors.js' import { logError } from '../../utils/log.js' import { sleep } from '../../utils/sleep.js' +import { jsonStringify } from '../../utils/slowOperations.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, @@ -45,6 +46,37 @@ function logDebug(message: string): void { logForDebugging(`[files-api] ${message}`) } +function summarizeFilesApiError(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 + } 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) +} + /** * File specification parsed from CLI args * Format: --file=: @@ -108,9 +140,7 @@ async function retryWithBackoff( } lastError = result.error || `${operation} failed` - logDebug( - `${operation} attempt ${attempt}/${MAX_RETRIES} failed: ${lastError}`, - ) + logDebug(`${operation} attempt ${attempt}/${MAX_RETRIES} failed`) if (attempt < MAX_RETRIES) { const delayMs = BASE_DELAY_MS * Math.pow(2, attempt - 1) @@ -142,7 +172,7 @@ export async function downloadFile( 'anthropic-beta': FILES_API_BETA_HEADER, } - logDebug(`Downloading file ${fileId} from ${url}`) + logDebug(`Downloading file ${fileId} from configured Files API endpoint`) return retryWithBackoff(`Download file ${fileId}`, async () => { try { @@ -191,9 +221,7 @@ export function buildDownloadPath( ): string | null { const normalized = path.normalize(relativePath) if (normalized.startsWith('..')) { - logDebugError( - `Invalid file path: ${relativePath}. Path must not traverse above workspace`, - ) + logDebugError('Invalid file path rejected: path traversal is not allowed') return null } @@ -243,7 +271,7 @@ export async function downloadAndSaveFile( // Write the file await fs.writeFile(fullPath, content) - logDebug(`Saved file ${fileId} to ${fullPath} (${content.length} bytes)`) + logDebug(`Saved file ${fileId} (${content.length} bytes)`) return { fileId, @@ -252,10 +280,16 @@ export async function downloadAndSaveFile( bytesWritten: content.length, } } catch (error) { - logDebugError(`Failed to download file ${fileId}: ${errorMessage(error)}`) - if (error instanceof Error) { - logError(error) - } + logDebugError( + `Failed to download file ${fileId}: ${summarizeFilesApiError(error)}`, + ) + logError( + new Error( + `Files API download failed for ${fileId}: ${summarizeFilesApiError( + error, + )}`, + ), + ) return { fileId, @@ -390,7 +424,7 @@ export async function uploadFile( 'anthropic-beta': FILES_API_BETA_HEADER, } - logDebug(`Uploading file ${filePath} as ${relativePath}`) + logDebug('Uploading file to configured Files API endpoint') // Read file content first (outside retry loop since it's not a network operation) let content: Buffer @@ -455,7 +489,7 @@ export async function uploadFile( const body = Buffer.concat(bodyParts) try { - return await retryWithBackoff(`Upload file ${relativePath}`, async () => { + return await retryWithBackoff('Upload session file', async () => { try { const response = await axios.post(url, body, { headers: { @@ -476,7 +510,7 @@ export async function uploadFile( error: 'Upload succeeded but no file ID returned', } } - logDebug(`Uploaded file ${filePath} -> ${fileId} (${fileSize} bytes)`) + logDebug(`Uploaded file (${fileSize} bytes)`) return { done: true, value: { @@ -735,9 +769,7 @@ export function parseFileSpecs(fileSpecs: string[]): File[] { const relativePath = spec.substring(colonIndex + 1) if (!fileId || !relativePath) { - logDebugError( - `Invalid file spec: ${spec}. Both file_id and path are required`, - ) + logDebugError('Invalid file spec: missing file_id or relative path') continue } diff --git a/src/services/lsp/passiveFeedback.ts b/src/services/lsp/passiveFeedback.ts index 7109808..c9a5613 100644 --- a/src/services/lsp/passiveFeedback.ts +++ b/src/services/lsp/passiveFeedback.ts @@ -8,6 +8,34 @@ import type { DiagnosticFile } from '../diagnosticTracking.js' import { registerPendingLSPDiagnostic } from './LSPDiagnosticRegistry.js' import type { LSPServerManager } from './LSPServerManager.js' +function summarizeLspErrorForDebug(error: unknown): string { + const err = toError(error) + return jsonStringify({ + errorType: err.constructor.name, + errorName: err.name, + hasMessage: err.message.length > 0, + }) +} + +function summarizeDiagnosticParamsForDebug(params: unknown): string { + if (!params || typeof params !== 'object') { + return jsonStringify({ + paramsType: typeof params, + hasValue: params !== undefined && params !== null, + }) + } + + const paramRecord = params as Record + const diagnostics = paramRecord.diagnostics + return jsonStringify({ + keys: Object.keys(paramRecord) + .sort() + .slice(0, 10), + hasUri: typeof paramRecord.uri === 'string', + diagnosticsCount: Array.isArray(diagnostics) ? diagnostics.length : 0, + }) +} + /** * Map LSP severity to Claude diagnostic severity * @@ -54,7 +82,9 @@ export function formatDiagnosticsForAttachment( const err = toError(error) logError(err) logForDebugging( - `Failed to convert URI to file path: ${params.uri}. Error: ${err.message}. Using original URI as fallback.`, + `Failed to convert diagnostic URI to file path; using original URI fallback (${summarizeLspErrorForDebug( + err, + )})`, ) // Gracefully fallback to original URI - LSP servers may send malformed URIs uri = params.uri @@ -177,14 +207,16 @@ export function registerLSPNotificationHandlers( ) logError(err) logForDebugging( - `Invalid diagnostic params from ${serverName}: ${jsonStringify(params)}`, + `Invalid diagnostic params from ${serverName}: ${summarizeDiagnosticParamsForDebug( + params, + )}`, ) return } const diagnosticParams = params as PublishDiagnosticsParams logForDebugging( - `Received diagnostics from ${serverName}: ${diagnosticParams.diagnostics.length} diagnostic(s) for ${diagnosticParams.uri}`, + `Received diagnostics from ${serverName}: ${diagnosticParams.diagnostics.length} diagnostic(s)`, ) // Convert LSP diagnostics to Claude format (can throw on invalid URIs) @@ -199,7 +231,7 @@ export function registerLSPNotificationHandlers( firstFile.diagnostics.length === 0 ) { logForDebugging( - `Skipping empty diagnostics from ${serverName} for ${diagnosticParams.uri}`, + `Skipping empty diagnostics from ${serverName}`, ) return } @@ -223,9 +255,8 @@ export function registerLSPNotificationHandlers( logError(err) logForDebugging( `Error registering LSP diagnostics from ${serverName}: ` + - `URI: ${diagnosticParams.uri}, ` + `Diagnostic count: ${firstFile.diagnostics.length}, ` + - `Error: ${err.message}`, + `Error: ${summarizeLspErrorForDebug(err)}`, ) // Track consecutive failures and warn after 3+ @@ -234,7 +265,7 @@ export function registerLSPNotificationHandlers( lastError: '', } failures.count++ - failures.lastError = err.message + failures.lastError = summarizeLspErrorForDebug(err) diagnosticFailures.set(serverName, failures) if (failures.count >= 3) { @@ -251,7 +282,9 @@ export function registerLSPNotificationHandlers( const err = toError(error) logError(err) logForDebugging( - `Unexpected error processing diagnostics from ${serverName}: ${err.message}`, + `Unexpected error processing diagnostics from ${serverName}: ${summarizeLspErrorForDebug( + err, + )}`, ) // Track consecutive failures and warn after 3+ @@ -260,7 +293,7 @@ export function registerLSPNotificationHandlers( lastError: '', } failures.count++ - failures.lastError = err.message + failures.lastError = summarizeLspErrorForDebug(err) diagnosticFailures.set(serverName, failures) if (failures.count >= 3) { @@ -284,13 +317,13 @@ export function registerLSPNotificationHandlers( registrationErrors.push({ serverName, - error: err.message, + error: summarizeLspErrorForDebug(err), }) logError(err) logForDebugging( `Failed to register diagnostics handler for ${serverName}: ` + - `Error: ${err.message}`, + `Error: ${summarizeLspErrorForDebug(err)}`, ) } } diff --git a/src/services/mcp/xaa.ts b/src/services/mcp/xaa.ts index 26f5845..beb1fe4 100644 --- a/src/services/mcp/xaa.ts +++ b/src/services/mcp/xaa.ts @@ -96,6 +96,24 @@ function redactTokens(raw: unknown): string { return s.replace(SENSITIVE_TOKEN_RE, (_, k) => `"${k}":"[REDACTED]"`) } +function summarizeXaaPayload(raw: unknown): string { + if (typeof raw === 'string') { + return `text(${raw.length} chars)` + } + if (Array.isArray(raw)) { + return `array(${raw.length})` + } + if (raw && typeof raw === 'object') { + return jsonStringify({ + payloadType: 'object', + keys: Object.keys(raw as Record) + .sort() + .slice(0, 10), + }) + } + return typeof raw +} + // ─── Zod Schemas ──────────────────────────────────────────────────────────── const TokenExchangeResponseSchema = lazySchema(() => @@ -145,7 +163,7 @@ export async function discoverProtectedResource( ) } catch (e) { throw new Error( - `XAA: PRM discovery failed: ${e instanceof Error ? e.message : String(e)}`, + `XAA: PRM discovery failed (${e instanceof Error ? e.name : typeof e})`, ) } if (!prm.resource || !prm.authorization_servers?.[0]) { @@ -154,9 +172,7 @@ export async function discoverProtectedResource( ) } if (normalizeUrl(prm.resource) !== normalizeUrl(serverUrl)) { - throw new Error( - `XAA: PRM discovery failed: PRM resource mismatch: expected ${serverUrl}, got ${prm.resource}`, - ) + throw new Error('XAA: PRM discovery failed: PRM resource mismatch') } return { resource: prm.resource, @@ -183,22 +199,16 @@ export async function discoverAuthorizationServer( fetchFn: opts?.fetchFn ?? defaultFetch, }) if (!meta?.issuer || !meta.token_endpoint) { - throw new Error( - `XAA: AS metadata discovery failed: no valid metadata at ${asUrl}`, - ) + throw new Error('XAA: AS metadata discovery failed: no valid metadata') } if (normalizeUrl(meta.issuer) !== normalizeUrl(asUrl)) { - throw new Error( - `XAA: AS metadata discovery failed: issuer mismatch: expected ${asUrl}, got ${meta.issuer}`, - ) + throw new Error('XAA: AS metadata discovery failed: issuer mismatch') } // RFC 8414 §3.3 / RFC 9728 §3 require HTTPS. A PRM-advertised http:// AS // that self-consistently reports an http:// issuer would pass the mismatch // check above, then we'd POST id_token + client_secret over plaintext. if (new URL(meta.token_endpoint).protocol !== 'https:') { - throw new Error( - `XAA: refusing non-HTTPS token endpoint: ${meta.token_endpoint}`, - ) + throw new Error('XAA: refusing non-HTTPS token endpoint') } return { issuer: meta.issuer, @@ -263,7 +273,7 @@ export async function requestJwtAuthorizationGrant(opts: { body: params, }) if (!res.ok) { - const body = redactTokens(await res.text()).slice(0, 200) + const body = summarizeXaaPayload(redactTokens(await res.text())) // 4xx → id_token rejected (invalid_grant etc.), clear cache. // 5xx → IdP outage, id_token may still be valid, preserve it. const shouldClear = res.status < 500 @@ -278,21 +288,25 @@ export async function requestJwtAuthorizationGrant(opts: { } catch { // Transient network condition (captive portal, proxy) — don't clear id_token. throw new XaaTokenExchangeError( - `XAA: token exchange returned non-JSON (captive portal?) at ${opts.tokenEndpoint}`, + 'XAA: token exchange returned non-JSON response (captive portal?)', false, ) } const exchangeParsed = TokenExchangeResponseSchema().safeParse(rawExchange) if (!exchangeParsed.success) { throw new XaaTokenExchangeError( - `XAA: token exchange response did not match expected shape: ${redactTokens(rawExchange)}`, + `XAA: token exchange response did not match expected shape: ${summarizeXaaPayload( + redactTokens(rawExchange), + )}`, true, ) } const result = exchangeParsed.data if (!result.access_token) { throw new XaaTokenExchangeError( - `XAA: token exchange response missing access_token: ${redactTokens(result)}`, + `XAA: token exchange response missing access_token: ${summarizeXaaPayload( + redactTokens(result), + )}`, true, ) } @@ -373,7 +387,7 @@ export async function exchangeJwtAuthGrant(opts: { body: params, }) if (!res.ok) { - const body = redactTokens(await res.text()).slice(0, 200) + const body = summarizeXaaPayload(redactTokens(await res.text())) throw new Error(`XAA: jwt-bearer grant failed: HTTP ${res.status}: ${body}`) } let rawTokens: unknown @@ -381,13 +395,15 @@ export async function exchangeJwtAuthGrant(opts: { rawTokens = await res.json() } catch { throw new Error( - `XAA: jwt-bearer grant returned non-JSON (captive portal?) at ${opts.tokenEndpoint}`, + 'XAA: jwt-bearer grant returned non-JSON response (captive portal?)', ) } const tokensParsed = JwtBearerResponseSchema().safeParse(rawTokens) if (!tokensParsed.success) { throw new Error( - `XAA: jwt-bearer response did not match expected shape: ${redactTokens(rawTokens)}`, + `XAA: jwt-bearer response did not match expected shape: ${summarizeXaaPayload( + redactTokens(rawTokens), + )}`, ) } return tokensParsed.data @@ -431,11 +447,14 @@ export async function performCrossAppAccess( ): Promise { const fetchFn = makeXaaFetch(abortSignal) - logMCPDebug(serverName, `XAA: discovering PRM for ${serverUrl}`) + logMCPDebug(serverName, 'XAA: discovering protected resource metadata') const prm = await discoverProtectedResource(serverUrl, { fetchFn }) logMCPDebug( serverName, - `XAA: discovered resource=${prm.resource} ASes=[${prm.authorization_servers.join(', ')}]`, + `XAA: discovered protected resource metadata ${jsonStringify({ + hasResource: Boolean(prm.resource), + authorizationServerCount: prm.authorization_servers.length, + })}`, ) // Try each advertised AS in order. grant_types_supported is OPTIONAL per @@ -449,16 +468,16 @@ export async function performCrossAppAccess( candidate = await discoverAuthorizationServer(asUrl, { fetchFn }) } catch (e) { if (abortSignal?.aborted) throw e - asErrors.push(`${asUrl}: ${e instanceof Error ? e.message : String(e)}`) + asErrors.push( + `authorization server discovery failed (${e instanceof Error ? e.name : typeof e})`, + ) continue } if ( candidate.grant_types_supported && !candidate.grant_types_supported.includes(JWT_BEARER_GRANT) ) { - asErrors.push( - `${asUrl}: does not advertise jwt-bearer grant (supported: ${candidate.grant_types_supported.join(', ')})`, - ) + asErrors.push('authorization server does not advertise jwt-bearer grant') continue } asMeta = candidate @@ -466,7 +485,7 @@ export async function performCrossAppAccess( } if (!asMeta) { throw new Error( - `XAA: no authorization server supports jwt-bearer. Tried: ${asErrors.join('; ')}`, + `XAA: no authorization server supports jwt-bearer (${asErrors.length} candidates tried)`, ) } // Pick auth method from what the AS advertises. We handle @@ -481,7 +500,7 @@ export async function performCrossAppAccess( : 'client_secret_basic' logMCPDebug( serverName, - `XAA: AS issuer=${asMeta.issuer} token_endpoint=${asMeta.token_endpoint} auth_method=${authMethod}`, + `XAA: selected authorization server (auth_method=${authMethod})`, ) logMCPDebug(serverName, `XAA: exchanging id_token for ID-JAG at IdP`) diff --git a/src/services/mcp/xaaIdpLogin.ts b/src/services/mcp/xaaIdpLogin.ts index f1325c9..bcbe932 100644 --- a/src/services/mcp/xaaIdpLogin.ts +++ b/src/services/mcp/xaaIdpLogin.ts @@ -210,9 +210,7 @@ export async function discoverOidc( signal: AbortSignal.timeout(IDP_REQUEST_TIMEOUT_MS), }) if (!res.ok) { - throw new Error( - `XAA IdP: OIDC discovery failed: HTTP ${res.status} at ${url}`, - ) + throw new Error(`XAA IdP: OIDC discovery failed (HTTP ${res.status})`) } // Captive portals and proxy auth pages return 200 with HTML. res.json() // throws a raw SyntaxError before safeParse can give a useful message. @@ -221,17 +219,15 @@ export async function discoverOidc( body = await res.json() } catch { throw new Error( - `XAA IdP: OIDC discovery returned non-JSON at ${url} (captive portal or proxy?)`, + 'XAA IdP: OIDC discovery returned non-JSON response (captive portal or proxy?)', ) } const parsed = OpenIdProviderDiscoveryMetadataSchema.safeParse(body) if (!parsed.success) { - throw new Error(`XAA IdP: invalid OIDC metadata: ${parsed.error.message}`) + throw new Error('XAA IdP: invalid OIDC metadata') } if (new URL(parsed.data.token_endpoint).protocol !== 'https:') { - throw new Error( - `XAA IdP: refusing non-HTTPS token endpoint: ${parsed.data.token_endpoint}`, - ) + throw new Error('XAA IdP: refusing non-HTTPS token endpoint') } return parsed.data } @@ -373,7 +369,7 @@ function waitForCallback( ), ) } else { - rejectOnce(new Error(`XAA IdP: callback server failed: ${err.message}`)) + rejectOnce(new Error('XAA IdP: callback server failed')) } }) @@ -405,11 +401,11 @@ export async function acquireIdpIdToken( const cached = getCachedIdpIdToken(idpIssuer) if (cached) { - logMCPDebug('xaa', `Using cached id_token for ${idpIssuer}`) + logMCPDebug('xaa', 'Using cached id_token for configured IdP') return cached } - logMCPDebug('xaa', `No cached id_token for ${idpIssuer}; starting OIDC login`) + logMCPDebug('xaa', 'No cached id_token for configured IdP; starting OIDC login') const metadata = await discoverOidc(idpIssuer) const port = opts.callbackPort ?? (await findAvailablePort()) @@ -478,10 +474,7 @@ export async function acquireIdpIdToken( : Date.now() + (tokens.expires_in ?? 3600) * 1000 saveIdpIdToken(idpIssuer, tokens.id_token, expiresAt) - logMCPDebug( - 'xaa', - `Cached id_token for ${idpIssuer} (expires ${new Date(expiresAt).toISOString()})`, - ) + logMCPDebug('xaa', 'Cached id_token for configured IdP') return tokens.id_token } diff --git a/src/tools/BriefTool/upload.ts b/src/tools/BriefTool/upload.ts index 306e6f4..2b771e1 100644 --- a/src/tools/BriefTool/upload.ts +++ b/src/tools/BriefTool/upload.ts @@ -57,6 +57,47 @@ function debug(msg: string): void { logForDebugging(`[brief:upload] ${msg}`) } +function summarizeUploadError(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 + } else { + summary.errorType = typeof error + summary.hasValue = error !== undefined && error !== null + } + + 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) +} + +function summarizeUploadResponse(data: unknown): string { + if (data === undefined) return 'undefined' + if (data === null) return 'null' + if (Array.isArray(data)) return `array(${data.length})` + if (typeof data === 'object') { + return jsonStringify({ + responseType: 'object', + keys: Object.keys(data as Record) + .sort() + .slice(0, 10), + }) + } + return typeof data +} + /** * Base URL for uploads. Must match the host the token is valid for. * @@ -100,7 +141,9 @@ export async function uploadBriefAttachment( if (!ctx.replBridgeEnabled) return undefined if (size > MAX_UPLOAD_BYTES) { - debug(`skip ${fullPath}: ${size} bytes exceeds ${MAX_UPLOAD_BYTES} limit`) + debug( + `skip attachment upload: ${size} bytes exceeds ${MAX_UPLOAD_BYTES} limit`, + ) return undefined } @@ -114,7 +157,7 @@ export async function uploadBriefAttachment( try { content = await readFile(fullPath) } catch (e) { - debug(`read failed for ${fullPath}: ${e}`) + debug(`read failed before upload: ${summarizeUploadError(e)}`) return undefined } @@ -150,23 +193,23 @@ export async function uploadBriefAttachment( if (response.status !== 201) { debug( - `upload failed for ${fullPath}: status=${response.status} body=${jsonStringify(response.data).slice(0, 200)}`, + `upload failed: status=${response.status} response=${summarizeUploadResponse( + response.data, + )}`, ) return undefined } const parsed = uploadResponseSchema().safeParse(response.data) if (!parsed.success) { - debug( - `unexpected response shape for ${fullPath}: ${parsed.error.message}`, - ) + debug('unexpected upload response shape') return undefined } - debug(`uploaded ${fullPath} → ${parsed.data.file_uuid} (${size} bytes)`) + debug(`uploaded attachment (${size} bytes)`) return parsed.data.file_uuid } catch (e) { - debug(`upload threw for ${fullPath}: ${e}`) + debug(`upload threw: ${summarizeUploadError(e)}`) return undefined } } diff --git a/src/tools/McpAuthTool/McpAuthTool.ts b/src/tools/McpAuthTool/McpAuthTool.ts index 7ea9679..2ecbb93 100644 --- a/src/tools/McpAuthTool/McpAuthTool.ts +++ b/src/tools/McpAuthTool/McpAuthTool.ts @@ -15,7 +15,6 @@ import type { ScopedMcpServerConfig, } from '../../services/mcp/types.js' import type { Tool } from '../../Tool.js' -import { errorMessage } from '../../utils/errors.js' import { lazySchema } from '../../utils/lazySchema.js' import { logMCPDebug, logMCPError } from '../../utils/log.js' import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js' @@ -29,9 +28,11 @@ export type McpAuthOutput = { authUrl?: string } -function getConfigUrl(config: ScopedMcpServerConfig): string | undefined { - if ('url' in config) return config.url - return undefined +function summarizeMcpAuthToolError(error: unknown): string { + if (error instanceof Error) { + return `${error.name} (hasMessage=${error.message.length > 0})` + } + return `non-Error (${typeof error})` } /** @@ -50,12 +51,10 @@ export function createMcpAuthTool( serverName: string, config: ScopedMcpServerConfig, ): Tool { - const url = getConfigUrl(config) const transport = config.type ?? 'stdio' - const location = url ? `${transport} at ${url}` : transport const description = - `The \`${serverName}\` MCP server (${location}) is installed but requires authentication. ` + + `The \`${serverName}\` MCP server (${transport}) is installed but requires authentication. ` + `Call this tool to start the OAuth flow — you'll receive an authorization URL to share with the user. ` + `Once the user completes authorization in their browser, the server's real tools will become available automatically.` @@ -167,7 +166,9 @@ export function createMcpAuthTool( .catch(err => { logMCPError( serverName, - `OAuth flow failed after tool-triggered start: ${errorMessage(err)}`, + `OAuth flow failed after tool-triggered start: ${summarizeMcpAuthToolError( + err, + )}`, ) }) @@ -199,7 +200,7 @@ export function createMcpAuthTool( return { data: { status: 'error' as const, - message: `Failed to start OAuth flow for ${serverName}: ${errorMessage(err)}. Ask the user to run /mcp and authenticate manually.`, + message: `Failed to start OAuth flow for ${serverName}. Ask the user to run /mcp and authenticate manually.`, }, } }