diff --git a/package.json b/package.json index 11727f4..763f37d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@missionsquad/mcp-api", - "version": "1.11.4", + "version": "1.11.5", "description": "MCP Servers exposed via HTTP API", "main": "dist/index.js", "repository": "missionsquad/mcp-api", diff --git a/src/services/mcp.ts b/src/services/mcp.ts index f580cd6..b7946a4 100644 --- a/src/services/mcp.ts +++ b/src/services/mcp.ts @@ -800,6 +800,18 @@ const buildRequestInit = (headers?: Record): RequestInit | undef return { headers } } +const summarizeUrlForLog = (value: string | undefined): string | undefined => { + if (!value) { + return undefined + } + try { + const parsed = new URL(value) + return `${parsed.origin}${parsed.pathname}` + } catch { + return value.replace(/[?#].*$/, '') + } +} + export type TransportFactoryOptions = { requestInit?: RequestInit authProvider?: OAuthClientProvider @@ -1654,12 +1666,12 @@ export class MCPService implements Resource { } oauthLogInfo(`[oauth:${username}:${server.name}] Building transport auth provider ${JSON.stringify({ - transportUrl: server.url, - runtimeUrl, - resourceUri: server.oauthTemplate?.resourceUri, - authorizationServerIssuer: server.oauthTemplate?.authorizationServerIssuer, - registrationEndpoint: server.oauthTemplate?.registrationEndpoint, - tokenEndpoint: server.oauthTemplate?.tokenEndpoint, + transportUrl: summarizeUrlForLog(server.url), + runtimeUrl: summarizeUrlForLog(runtimeUrl), + resourceUri: summarizeUrlForLog(server.oauthTemplate?.resourceUri), + authorizationServerIssuer: summarizeUrlForLog(server.oauthTemplate?.authorizationServerIssuer), + registrationEndpoint: summarizeUrlForLog(server.oauthTemplate?.registrationEndpoint), + tokenEndpoint: summarizeUrlForLog(server.oauthTemplate?.tokenEndpoint), tokenEndpointAuthMethodsSupported: server.oauthTemplate?.tokenEndpointAuthMethodsSupported, persistedRecord: { clientId: record.clientId ? `${record.clientId.slice(0, 4)}...${record.clientId.slice(-4)}` : undefined, @@ -2439,6 +2451,14 @@ export class MCPService implements Resource { sessionId = sessionRecord?.sessionId ?? undefined } const shouldPreferSessionResume = !!sessionId && !forceAuthProvider + oauthLogInfo( + `[oauth:${username}:${server.name}] Preparing user connection ${JSON.stringify({ + hasPersistedSession: !!sessionId, + sessionResumePreferred: shouldPreferSessionResume, + forceAuthProvider, + transportUrl: summarizeUrlForLog(server.url) + })}` + ) const transportErrorHandler = async (error: Error) => { log({ level: 'error', msg: `[${username}:${server.name}] transport error: ${error.message}`, error }) @@ -2495,11 +2515,25 @@ export class MCPService implements Resource { if (transport instanceof StreamableHTTPClientTransport) { await this.persistUserSessionId(server.name, username, transport) } + oauthLogInfo( + `[oauth:${username}:${server.name}] User connection established ${JSON.stringify({ + connectedVia: shouldPreferSessionResume ? 'session_resume' : 'auth_provider', + hasPersistedSession: !!sessionId, + persistedSessionUpdated: transport instanceof StreamableHTTPClientTransport + })}` + ) log({ level: 'info', msg: `[${username}:${server.name}] Connected successfully.` }) return userConn } catch (error) { const httpStatus = extractHttpStatusFromError(error) + oauthLogInfo( + `[oauth:${username}:${server.name}] User connection attempt failed ${JSON.stringify({ + httpStatus, + attemptedMode: shouldPreferSessionResume ? 'session_resume' : 'auth_provider', + hasPersistedSession: !!sessionId + })}` + ) if (sessionId && shouldPreferSessionResume && allowSessionRetry && httpStatus === 401) { log({ diff --git a/src/services/oauthTokens.ts b/src/services/oauthTokens.ts index 6ba8060..2e636dd 100644 --- a/src/services/oauthTokens.ts +++ b/src/services/oauthTokens.ts @@ -109,6 +109,10 @@ const oauthLogInfo = (msg: string): void => { log({ level: 'info', msg }) } +const oauthLifecycleInfo = (msg: string): void => { + log({ level: 'info', msg }) +} + const ACCESS_TOKEN_EXPIRY_SKEW_MS = 30_000 export class McpOAuthTokens { @@ -289,15 +293,15 @@ export class McpOAuthTokens { throw new Error(`OAuth refresh token not found for server ${input.serverName} and user ${input.username}`) } - oauthLogInfo(`[oauth:${input.username}:${input.serverName}] Starting OAuth refresh ${JSON.stringify({ + oauthLifecycleInfo(`[oauth:${input.username}:${input.serverName}] Starting OAuth refresh ${JSON.stringify({ tokenEndpoint: summarizeUrlForLog(input.tokenEndpoint), resource: summarizeUrlForLog(input.resource), clientId: truncateSensitiveValue(existing.clientId), - refreshToken: truncateSensitiveValue(existing.refreshToken), tokenEndpointAuthMethod: existing.tokenEndpointAuthMethod, registrationMode: existing.registrationMode, expiresAt: existing.expiresAt?.toISOString(), - scopes: existing.scopes + scopes: existing.scopes, + hasRefreshToken: !!existing.refreshToken })}`) const params = new URLSearchParams({ @@ -349,7 +353,7 @@ export class McpOAuthTokens { if (!response.ok) { const errorCode = (parsed as { error?: string }).error || `HTTP_${response.status}` const errorDescription = (parsed as { error_description?: string }).error_description - oauthLogInfo(`[oauth:${input.username}:${input.serverName}] Refresh failed ${JSON.stringify({ + oauthLifecycleInfo(`[oauth:${input.username}:${input.serverName}] OAuth refresh failed ${JSON.stringify({ status: response.status, error: errorCode, errorDescription, @@ -365,13 +369,12 @@ export class McpOAuthTokens { throw new Error(`OAuth token record not found after refresh for server ${input.serverName}`) } - oauthLogInfo(`[oauth:${input.username}:${input.serverName}] Refresh succeeded ${JSON.stringify({ + oauthLifecycleInfo(`[oauth:${input.username}:${input.serverName}] OAuth refresh succeeded ${JSON.stringify({ returnedResource: typeof (parsed as Record).resource === 'string' ? summarizeUrlForLog((parsed as Record).resource as string) : undefined, expiresAt: updated.expiresAt?.toISOString(), hasRefreshToken: !!updated.refreshToken, - refreshToken: truncateSensitiveValue(updated.refreshToken), tokenType: updated.tokenType, scopes: updated.scopes })}`) @@ -501,7 +504,7 @@ export class McpOAuthClientProvider implements OAuthClientProvider { } async redirectToAuthorization(authorizationUrl: URL): Promise { - oauthLogInfo(`[oauth:${this.username}:${this.serverName}] Redirecting to authorization ${JSON.stringify({ + oauthLifecycleInfo(`[oauth:${this.username}:${this.serverName}] OAuth authorization required ${JSON.stringify({ authorizationUrl: summarizeUrlForLog(authorizationUrl.toString()), clientId: truncateSensitiveValue(authorizationUrl.searchParams.get('client_id') || undefined), resource: summarizeUrlForLog(authorizationUrl.searchParams.get('resource') || undefined), diff --git a/src/utils/general.ts b/src/utils/general.ts index ff60841..c534ffe 100644 --- a/src/utils/general.ts +++ b/src/utils/general.ts @@ -4,11 +4,74 @@ export interface StringMap { [key: string]: string } +function summarizeUrlForErrorLog(value: string | undefined): string | undefined { + if (!value) { + return undefined + } + try { + const parsed = new URL(value) + return `${parsed.origin}${parsed.pathname}` + } catch { + return value + } +} + +export function summarizeErrorForLog(error: any): string { + if (error == null) { + return 'unknown error' + } + if (typeof error === 'string') { + return error + } + if (error instanceof Error) { + const axiosLike = error as Error & { + code?: string + status?: number + config?: { method?: string; url?: string } + response?: { status?: number; statusText?: string } + } + const method = axiosLike.config?.method?.toUpperCase() + const url = summarizeUrlForErrorLog(axiosLike.config?.url) + const status = axiosLike.response?.status ?? axiosLike.status + const statusText = axiosLike.response?.statusText + const code = axiosLike.code + return JSON.stringify({ + name: axiosLike.name, + message: axiosLike.message, + code, + status, + statusText, + method, + url + }) + } + if (typeof error === 'object') { + const objectError = error as { + message?: string + code?: string + status?: number + method?: string + url?: string + response?: { status?: number; statusText?: string } + config?: { method?: string; url?: string } + } + return JSON.stringify({ + message: objectError.message, + code: objectError.code, + status: objectError.response?.status ?? objectError.status, + statusText: objectError.response?.statusText, + method: objectError.config?.method?.toUpperCase() ?? objectError.method, + url: summarizeUrlForErrorLog(objectError.config?.url ?? objectError.url) + }) + } + return String(error) +} + export function log({ level, msg, error }: { level: string; msg: string; error?: any }) { if (!env.DEBUG && level === 'debug') return console.log(`[${new Date().toISOString()}] [${level.toUpperCase()}] ${msg}`) if (error != null) { - console.error(error) + console.error(summarizeErrorForLog(error)) } } @@ -73,8 +136,7 @@ export function retryWithExponentialBackoff( return await fn() } catch (error) { if (attempt >= maxAttempts) { - // throw error - console.error(error) + console.error(summarizeErrorForLog(error)) return { error } }