diff --git a/packages/api/src/flow/manager.ts b/packages/api/src/flow/manager.ts index 1c0ff6b47b..ec78a7d897 100644 --- a/packages/api/src/flow/manager.ts +++ b/packages/api/src/flow/manager.ts @@ -272,16 +272,28 @@ export class FlowStateManager { ): Promise { const flowKey = this.getFlowKey(flowId, type); let existingState = (await this.keyv.get(flowKey)) as FlowState | undefined; - if (existingState) { - logger.debug(`[${flowKey}] Flow already exists`); + const hasAccessTokenExpired = + existingState?.result && + typeof existingState.result === 'object' && + 'expires_at' in existingState.result && + typeof existingState.result.expires_at === 'number' && + existingState.result.expires_at < Date.now(); + if (existingState && !hasAccessTokenExpired) { + logger.debug(`[${flowKey}] Flow already exists with valid token`); return this.monitorFlow(flowKey, type, signal); } await new Promise((resolve) => setTimeout(resolve, 250)); existingState = (await this.keyv.get(flowKey)) as FlowState | undefined; - if (existingState) { - logger.debug(`[${flowKey}] Flow exists on 2nd check`); + const hasAccessTokenExpiredRecheck = + existingState?.result && + typeof existingState.result === 'object' && + 'expires_at' in existingState.result && + typeof existingState.result.expires_at === 'number' && + existingState.result.expires_at < Date.now(); + if (existingState && !hasAccessTokenExpiredRecheck) { + logger.debug(`[${flowKey}] Flow exists on 2nd check with valid token`); return this.monitorFlow(flowKey, type, signal); } diff --git a/packages/api/src/mcp/MCPConnectionFactory.ts b/packages/api/src/mcp/MCPConnectionFactory.ts index fe3be8b396..c2f3a114bd 100644 --- a/packages/api/src/mcp/MCPConnectionFactory.ts +++ b/packages/api/src/mcp/MCPConnectionFactory.ts @@ -167,6 +167,10 @@ export class MCPConnectionFactory { config?.oauth, ); + // Delete any existing flow state to ensure we start fresh + // This prevents stale codeVerifier issues when re-authenticating + await this.flowManager!.deleteFlow(flowId, 'mcp_oauth'); + // Create the flow state so the OAuth callback can find it // We spawn this in the background without waiting for it this.flowManager!.createFlow(flowId, 'mcp_oauth', flowMetadata).catch(() => { diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index e97bf05c6c..5c99e12a4f 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -615,7 +615,7 @@ export class MCPConnection extends EventEmitter { } // Check if it's an OAuth authentication error - if (errorCode === 401 || errorCode === 403) { + if (this.isOAuthError(error)) { logger.warn(`${this.getLogPrefix()} OAuth authentication error detected`); this.emit('oauthError', error); } @@ -778,6 +778,10 @@ export class MCPConnection extends EventEmitter { if (message.includes('invalid_token')) { return true; } + // Check for invalid_grant (OAuth servers return this for expired/revoked grants) + if (message.includes('invalid_grant')) { + return true; + } // Check for authentication required if (message.includes('authentication required') || message.includes('unauthorized')) { return true;