Trim teammate prompt UI and MCP debug logs

This commit is contained in:
2026-04-04 09:19:52 +08:00
parent f06a2c2740
commit 832035a087
4 changed files with 143 additions and 77 deletions

File diff suppressed because one or more lines are too long

View File

@@ -124,6 +124,31 @@ function redactSensitiveUrlParams(url: string): string {
}
}
function summarizeHeadersForDebug(
headers: Record<string, string> | undefined,
): {
headerCount: number
headerNames: string[]
hasAuthorization: boolean
} {
if (!headers) {
return {
headerCount: 0,
headerNames: [],
hasAuthorization: false,
}
}
const headerNames = Object.keys(headers).sort()
return {
headerCount: headerNames.length,
headerNames,
hasAuthorization: headerNames.some(
headerName => headerName.toLowerCase() === 'authorization',
),
}
}
/**
* Some OAuth servers (notably Slack) return HTTP 200 for all responses,
* signaling errors via the JSON body instead. The SDK's executeTokenRequest
@@ -696,14 +721,11 @@ async function performMCPXaaAuth(
const haveKeys = Object.keys(
getSecureStorage().read()?.mcpOAuthClientConfig ?? {},
)
const headersForLogging = Object.fromEntries(
Object.entries(serverConfig.headers ?? {}).map(([k, v]) =>
k.toLowerCase() === 'authorization' ? [k, '[REDACTED]'] : [k, v],
),
)
logMCPDebug(
serverName,
`XAA: secret lookup miss. wanted=${wantedKey} have=[${haveKeys.join(', ')}] configHeaders=${jsonStringify(headersForLogging)}`,
`XAA: secret lookup miss. wanted=${wantedKey} availableKeys=${haveKeys.length} configHeaderSummary=${jsonStringify(
summarizeHeadersForDebug(serverConfig.headers),
)}`,
)
throw new Error(
`XAA: AS client secret not found for '${serverName}'. Re-add with --client-secret.`,
@@ -988,7 +1010,9 @@ export async function performMCPOAuthFlow(
provider.setMetadata(metadata)
logMCPDebug(
serverName,
`Fetched OAuth metadata with scope: ${getScopeFromMetadata(metadata) || 'NONE'}`,
`Fetched OAuth metadata (hasScope=${Boolean(
getScopeFromMetadata(metadata),
)})`,
)
}
} catch (error) {
@@ -1170,8 +1194,10 @@ export async function performMCPOAuthFlow(
server.listen(port, '127.0.0.1', async () => {
try {
logMCPDebug(serverName, `Starting SDK auth`)
logMCPDebug(serverName, `Server URL: ${serverConfig.url}`)
logMCPDebug(
serverName,
`Starting SDK auth (transport=${serverConfig.type})`,
)
// First call to start the auth flow - should redirect
// Pass the scope and resource_metadata from WWW-Authenticate header if available
@@ -1189,7 +1215,7 @@ export async function performMCPOAuthFlow(
)
}
} catch (error) {
logMCPDebug(serverName, `SDK auth error: ${error}`)
logMCPDebug(serverName, `SDK auth error: ${errorMessage(error)}`)
cleanup()
rejectOnce(new Error(`SDK auth failed: ${errorMessage(error)}`))
}
@@ -1235,9 +1261,13 @@ export async function performMCPOAuthFlow(
if (savedTokens) {
logMCPDebug(
serverName,
`Token access_token length: ${savedTokens.access_token?.length}`,
`Token summary after auth: ${jsonStringify({
hasAccessToken: Boolean(savedTokens.access_token),
hasRefreshToken: Boolean(savedTokens.refresh_token),
expiresInSec: savedTokens.expires_in,
hasScope: Boolean(savedTokens.scope),
})}`,
)
logMCPDebug(serverName, `Token expires_in: ${savedTokens.expires_in}`)
}
logEvent('tengu_mcp_oauth_flow_success', {
@@ -1257,7 +1287,10 @@ export async function performMCPOAuthFlow(
throw new Error('Unexpected auth result: ' + result)
}
} catch (error) {
logMCPDebug(serverName, `Error during auth completion: ${error}`)
logMCPDebug(
serverName,
`Error during auth completion: ${errorMessage(error)}`,
)
// Determine failure reason for attribution telemetry. The try block covers
// port acquisition, the callback server, the redirect flow, and token
@@ -1429,7 +1462,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
metadata.scope = metadataScope
logMCPDebug(
this.serverName,
`Using scope from metadata: ${metadata.scope}`,
'Using scope from metadata',
)
}
@@ -1445,7 +1478,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
get clientMetadataUrl(): string | undefined {
const override = process.env.MCP_OAUTH_CLIENT_METADATA_URL
if (override) {
logMCPDebug(this.serverName, `Using CIMD URL from env: ${override}`)
logMCPDebug(this.serverName, 'Using CIMD URL from env override')
return override
}
return MCP_CLIENT_METADATA_URL
@@ -1467,7 +1500,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
*/
markStepUpPending(scope: string): void {
this._pendingStepUpScope = scope
logMCPDebug(this.serverName, `Marked step-up pending: ${scope}`)
logMCPDebug(this.serverName, 'Marked step-up pending')
}
async state(): Promise<string> {
@@ -1632,7 +1665,7 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
if (needsStepUp) {
logMCPDebug(
this.serverName,
`Step-up pending (${this._pendingStepUpScope}), omitting refresh_token`,
'Step-up pending, omitting refresh_token',
)
}
@@ -1693,10 +1726,15 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
token_type: 'Bearer',
}
logMCPDebug(this.serverName, `Returning tokens`)
logMCPDebug(this.serverName, `Token length: ${tokens.access_token?.length}`)
logMCPDebug(this.serverName, `Has refresh token: ${!!tokens.refresh_token}`)
logMCPDebug(this.serverName, `Expires in: ${Math.floor(expiresIn)}s`)
logMCPDebug(
this.serverName,
`Returning tokens: ${jsonStringify({
hasAccessToken: Boolean(tokens.access_token),
hasRefreshToken: Boolean(tokens.refresh_token),
hasScope: Boolean(tokens.scope),
expiresInSec: Math.floor(expiresIn),
})}`,
)
return tokens
}
@@ -1707,9 +1745,15 @@ export class ClaudeAuthProvider implements OAuthClientProvider {
const existingData = storage.read() || {}
const serverKey = getServerKey(this.serverName, this.serverConfig)
logMCPDebug(this.serverName, `Saving tokens`)
logMCPDebug(this.serverName, `Token expires in: ${tokens.expires_in}`)
logMCPDebug(this.serverName, `Has refresh token: ${!!tokens.refresh_token}`)
logMCPDebug(
this.serverName,
`Saving tokens: ${jsonStringify({
hasAccessToken: Boolean(tokens.access_token),
hasRefreshToken: Boolean(tokens.refresh_token),
hasScope: Boolean(tokens.scope),
expiresInSec: tokens.expires_in,
})}`,
)
const updatedData: SecureStorageData = {
...existingData,

View File

@@ -332,6 +332,51 @@ function mcpBaseUrlAnalytics(serverRef: ScopedMcpServerConfig): {
: {}
}
function mcpBaseUrlForDebug(serverRef: ScopedMcpServerConfig): string {
return getLoggingSafeMcpBaseUrl(serverRef) || '[unavailable]'
}
function summarizeHeadersForDebug(
headers: Record<string, string> | undefined,
): {
headerCount: number
headerNames: string[]
hasAuthorization: boolean
} {
if (!headers) {
return {
headerCount: 0,
headerNames: [],
hasAuthorization: false,
}
}
const headerNames = Object.keys(headers)
return {
headerCount: headerNames.length,
headerNames: headerNames.sort(),
hasAuthorization: headerNames.some(
headerName => headerName.toLowerCase() === 'authorization',
),
}
}
function summarizeProxyEnvForDebug(): Record<string, string | boolean> {
return {
hasNodeOptions: Boolean(process.env.NODE_OPTIONS),
uvThreadpoolSizeConfigured: Boolean(process.env.UV_THREADPOOL_SIZE),
hasHttpProxy: Boolean(process.env.HTTP_PROXY),
hasHttpsProxy: Boolean(process.env.HTTPS_PROXY),
hasNoProxy: Boolean(process.env.NO_PROXY),
}
}
function summarizeStderrForDebug(stderrOutput: string): string {
const trimmed = stderrOutput.trim()
const lineCount = trimmed === '' ? 0 : trimmed.split('\n').length
return `Server stderr captured (${trimmed.length} chars, ${lineCount} lines)`
}
/**
* Shared handler for sse/http/claudeai-proxy auth failures during connect:
* emits tengu_mcp_server_needs_auth, caches the needs-auth entry, and returns
@@ -676,7 +721,10 @@ export const connectToServer = memoize(
)
logMCPDebug(name, `SSE transport initialized, awaiting connection`)
} else if (serverRef.type === 'sse-ide') {
logMCPDebug(name, `Setting up SSE-IDE transport to ${serverRef.url}`)
logMCPDebug(
name,
`Setting up SSE-IDE transport to ${mcpBaseUrlForDebug(serverRef)}`,
)
// IDE servers don't need authentication
// TODO: Use the auth token provided in the lockfile
const proxyOptions = getProxyFetchOptions()
@@ -735,7 +783,7 @@ export const connectToServer = memoize(
} else if (serverRef.type === 'ws') {
logMCPDebug(
name,
`Initializing WebSocket transport to ${serverRef.url}`,
`Initializing WebSocket transport to ${mcpBaseUrlForDebug(serverRef)}`,
)
const combinedHeaders = await getMcpServerHeaders(name, serverRef)
@@ -749,16 +797,17 @@ export const connectToServer = memoize(
...combinedHeaders,
}
// Redact sensitive headers before logging
const wsHeadersForLogging = mapValues(wsHeaders, (value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : value,
const wsHeadersForLogging = summarizeHeadersForDebug(
mapValues(wsHeaders, (_value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : '[set]',
),
)
logMCPDebug(
name,
`WebSocket transport options: ${jsonStringify({
url: serverRef.url,
headers: wsHeadersForLogging,
url: mcpBaseUrlForDebug(serverRef),
...wsHeadersForLogging,
hasSessionAuth: !!sessionIngressToken,
})}`,
)
@@ -782,20 +831,17 @@ export const connectToServer = memoize(
}
transport = new WebSocketTransport(wsClient)
} else if (serverRef.type === 'http') {
logMCPDebug(name, `Initializing HTTP transport to ${serverRef.url}`)
logMCPDebug(
name,
`Initializing HTTP transport to ${mcpBaseUrlForDebug(serverRef)}`,
)
logMCPDebug(
name,
`Node version: ${process.version}, Platform: ${process.platform}`,
)
logMCPDebug(
name,
`Environment: ${jsonStringify({
NODE_OPTIONS: process.env.NODE_OPTIONS || 'not set',
UV_THREADPOOL_SIZE: process.env.UV_THREADPOOL_SIZE || 'default',
HTTP_PROXY: process.env.HTTP_PROXY || 'not set',
HTTPS_PROXY: process.env.HTTPS_PROXY || 'not set',
NO_PROXY: process.env.NO_PROXY || 'not set',
})}`,
`Environment: ${jsonStringify(summarizeProxyEnvForDebug())}`,
)
// Create an auth provider for this server
@@ -843,16 +889,16 @@ export const connectToServer = memoize(
const headersForLogging = transportOptions.requestInit?.headers
? mapValues(
transportOptions.requestInit.headers as Record<string, string>,
(value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : value,
(_value, key) =>
key.toLowerCase() === 'authorization' ? '[REDACTED]' : '[set]',
)
: undefined
logMCPDebug(
name,
`HTTP transport options: ${jsonStringify({
url: serverRef.url,
headers: headersForLogging,
url: mcpBaseUrlForDebug(serverRef),
...summarizeHeadersForDebug(headersForLogging),
hasAuthProvider: !!authProvider,
timeoutMs: MCP_REQUEST_TIMEOUT_MS,
})}`,
@@ -879,7 +925,7 @@ export const connectToServer = memoize(
const oauthConfig = getOauthConfig()
const proxyUrl = `${oauthConfig.MCP_PROXY_URL}${oauthConfig.MCP_PROXY_PATH.replace('{server_id}', serverRef.id)}`
logMCPDebug(name, `Using claude.ai proxy at ${proxyUrl}`)
logMCPDebug(name, `Using claude.ai proxy transport`)
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
const fetchWithAuth = createClaudeAiProxyFetch(globalThis.fetch)
@@ -1025,7 +1071,10 @@ export const connectToServer = memoize(
// For HTTP transport, try a basic connectivity test first
if (serverRef.type === 'http') {
logMCPDebug(name, `Testing basic HTTP connectivity to ${serverRef.url}`)
logMCPDebug(
name,
`Testing basic HTTP connectivity to ${mcpBaseUrlForDebug(serverRef)}`,
)
try {
const testUrl = new URL(serverRef.url)
logMCPDebug(
@@ -1079,7 +1128,7 @@ export const connectToServer = memoize(
try {
await Promise.race([connectPromise, timeoutPromise])
if (stderrOutput) {
logMCPError(name, `Server stderr: ${stderrOutput}`)
logMCPError(name, summarizeStderrForDebug(stderrOutput))
stderrOutput = '' // Release accumulated string to prevent memory growth
}
const elapsed = Date.now() - connectStartTime
@@ -1149,7 +1198,7 @@ export const connectToServer = memoize(
}
transport.close().catch(() => {})
if (stderrOutput) {
logMCPError(name, `Server stderr: ${stderrOutput}`)
logMCPError(name, summarizeStderrForDebug(stderrOutput))
}
throw error
}

View File

@@ -20,7 +20,6 @@ export type TeammateStatus = {
agentId: string
agentType?: string
model?: string
prompt?: string
status: 'running' | 'idle' | 'unknown'
color?: string
idleSince?: string // ISO timestamp from idle notification
@@ -60,7 +59,6 @@ export function getTeammateStatuses(teamName: string): TeammateStatus[] {
agentId: member.agentId,
agentType: member.agentType,
model: member.model,
prompt: member.prompt,
status,
color: member.color,
tmuxPaneId: member.tmuxPaneId,