diff --git a/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js b/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js --- a/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js +++ b/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js @@ -33,6 +33,27 @@ } } exports.UnauthorizedError = UnauthorizedError; +function __cursorIsRetryableOAuthRefreshError(error) { + if (!(error instanceof Error)) { + return false; + } + if (error.name === 'AbortError' || error.name === 'TimeoutError') { + return true; + } + const cause = error.cause; + const code = error.code ?? (cause && typeof cause === 'object' && 'code' in cause ? cause.code : undefined); + if (typeof code === 'string' && /^(EAI_AGAIN|ECONNABORTED|ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETDOWN|ENETUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT|EADDRNOTAVAIL)$/.test(code)) { + return true; + } + const msg = (error.message ?? '').toLowerCase(); + if (/fetch failed|network error|socket hang up|client network socket disconnected|connection (?:timed out|timeout|closed|terminated)/i.test(msg)) { + return true; + } + if (error instanceof errors_js_1.OAuthError && (error instanceof errors_js_1.ServerError || error instanceof errors_js_1.TemporarilyUnavailableError)) { + return true; + } + return false; +} function isClientAuthMethod(method) { return ['client_secret_basic', 'client_secret_post', 'none'].includes(method); } @@ -168,6 +189,9 @@ return await authInternal(provider, options); } catch (error) { + if (error instanceof Error && error.name === 'OAuthRefreshTransientError') { + throw error; + } // Handle recoverable error types by invalidating credentials and retrying if (error instanceof errors_js_1.InvalidClientError || error instanceof errors_js_1.UnauthorizedClientError) { await provider.invalidateCredentials?.('all'); @@ -254,6 +278,8 @@ // Handle token refresh or new authorization if (tokens?.refresh_token) { try { + // [CURSOR PATCH] Allow the provider to coordinate concurrent refreshes + await provider.prepareForRefresh?.(); // Attempt to refresh the token const newTokens = await refreshAuthorization(authorizationServerUrl, { metadata, @@ -267,6 +293,43 @@ return 'AUTHORIZED'; } catch (error) { + // [CURSOR PATCH] Re-throw SiblingAlreadyRefreshedError so the FSM + // can reconnect with the winner's fresh tokens instead of starting + // a redundant full re-authorization flow. + if (error && error.name === 'SiblingAlreadyRefreshedError') { throw error; } + // [CURSOR PATCH] Log the catch-branch decision point before any branch is taken. + const __isOAuthError = error instanceof errors_js_1.OAuthError; + const __isServerError = error instanceof errors_js_1.ServerError; + const __isRetryable = __cursorIsRetryableOAuthRefreshError(error); + const __isDefiniteOAuthFailure = __isOAuthError && !__isServerError && !(error instanceof errors_js_1.TemporarilyUnavailableError); + const __willRethrow = __isDefiniteOAuthFailure || __isRetryable; + provider.logRefreshCatchBranch?.({ + errorIsOAuthError: __isOAuthError, + errorIsServerError: __isServerError, + errorIsRetryable: __isRetryable, + willFallThrough: !__willRethrow, + willRethrow: __willRethrow, + errorName: error instanceof Error ? error.name : typeof error, + errorMessage: error instanceof Error ? error.message : String(error), + }); + // [CURSOR PATCH] Release the refresh lease acquired in prepareForRefresh + // so sibling providers are not blocked until TTL expiry. + await provider.releaseRefreshLeaseOnError?.(error); + // [CURSOR PATCH] Log the refresh error so we can diagnose failures in telemetry. + provider.onRefreshError?.(error); + // Definite OAuth protocol failures (except server_error / temporarily_unavailable below) should surface to callers. + if (error instanceof errors_js_1.OAuthError && !(error instanceof errors_js_1.ServerError) && !(error instanceof errors_js_1.TemporarilyUnavailableError)) { + throw error; + } + // Transient network / overload: do not fall through to interactive re-auth. + if (__cursorIsRetryableOAuthRefreshError(error)) { + const wrapped = new Error(error instanceof Error ? error.message : String(error)); + wrapped.name = 'OAuthRefreshTransientError'; + if (error instanceof Error && error.stack) { + wrapped.stack = error.stack; + } + throw wrapped; + } // If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry. if (!(error instanceof errors_js_1.OAuthError) || error instanceof errors_js_1.ServerError) { // Could not refresh OAuth tokens diff --git a/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js b/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js --- a/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js +++ b/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js @@ -153,7 +153,15 @@ this._reconnectionTimeout = setTimeout(() => { // Use the last event ID to resume where we left off this._startOrAuthSse(options).catch(error => { - this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`)); + if (error instanceof Error && error.name === 'SiblingAlreadyRefreshedError') { + this.onerror?.(error); + return; + } + const wrapped = new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`); + if (error instanceof Error) { + wrapped.cause = error; + } + this.onerror?.(wrapped); // Schedule another attempt if this one failed, incrementing the attempt counter this._scheduleReconnection(options, attemptCount + 1); }); @@ -250,7 +258,15 @@ }, 0); } catch (error) { - this.onerror?.(new Error(`Failed to reconnect: ${error instanceof Error ? error.message : String(error)}`)); + if (error instanceof Error && error.name === 'SiblingAlreadyRefreshedError') { + this.onerror?.(error); + return; + } + const wrapped = new Error(`Failed to reconnect: ${error instanceof Error ? error.message : String(error)}`); + if (error instanceof Error) { + wrapped.cause = error; + } + this.onerror?.(wrapped); } } }