diff --git a/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js b/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js index f89657f..6033843 100644 --- a/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js +++ b/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js @@ -33,6 +33,9 @@ class UnauthorizedError extends Error { } } exports.UnauthorizedError = UnauthorizedError; +function __cursorIsRetryableOAuthRefreshError(error, provider) { + return provider?.isRetryableOAuthRefreshError?.(error) === true; +} function isClientAuthMethod(method) { return ['client_secret_basic', 'client_secret_post', 'none'].includes(method); } @@ -168,6 +171,9 @@ async function auth(provider, options) { 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'); @@ -182,6 +188,8 @@ async function auth(provider, options) { } } async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { + const oauthFetchFn = provider.getOAuthHttpFetch?.(); + const tokenFetchFn = oauthFetchFn ?? fetchFn; let resourceMetadata; let authorizationServerUrl; try { @@ -231,7 +239,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res const fullInformation = await registerClient(authorizationServerUrl, { metadata, clientMetadata: provider.clientMetadata, - fetchFn + fetchFn: tokenFetchFn }); await provider.saveClientInformation(fullInformation); clientInformation = fullInformation; @@ -245,7 +253,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res metadata, resource, authorizationCode, - fetchFn + fetchFn: tokenFetchFn }); await provider.saveTokens(tokens); return 'AUTHORIZED'; @@ -254,6 +262,8 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res // 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, @@ -261,12 +271,48 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res refreshToken: tokens.refresh_token, resource, addClientAuthentication: provider.addClientAuthentication, - fetchFn + fetchFn: tokenFetchFn }); await provider.saveTokens(newTokens); 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, provider); + 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), + underlyingError: error, + }); + // [CURSOR PATCH] Release the refresh lease acquired in prepareForRefresh + // so sibling providers are not blocked until TTL expiry. + await provider.releaseRefreshLeaseOnError?.(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, provider)) { + 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 index a29a7d3..eea8413 100644 --- a/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js +++ b/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js @@ -153,7 +153,15 @@ class StreamableHTTPClientTransport { 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 @@ class StreamableHTTPClientTransport { }, 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); } } } diff --git a/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js b/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js index de86ff9..ccc75fd 100644 --- a/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js +++ b/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js @@ -3,12 +3,15 @@ import { LATEST_PROTOCOL_VERSION } from '../types.js'; import { OAuthErrorResponseSchema, OpenIdProviderDiscoveryMetadataSchema } from '../shared/auth.js'; import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema } from '../shared/auth.js'; import { checkResourceAllowed, resourceUrlFromServerUrl } from '../shared/auth-utils.js'; -import { InvalidClientError, InvalidClientMetadataError, InvalidGrantError, OAUTH_ERRORS, OAuthError, ServerError, UnauthorizedClientError } from '../server/auth/errors.js'; +import { InvalidClientError, InvalidClientMetadataError, InvalidGrantError, OAUTH_ERRORS, OAuthError, ServerError, TemporarilyUnavailableError, UnauthorizedClientError } from '../server/auth/errors.js'; export class UnauthorizedError extends Error { constructor(message) { super(message ?? 'Unauthorized'); } } +function __cursorIsRetryableOAuthRefreshError(error, provider) { + return provider?.isRetryableOAuthRefreshError?.(error) === true; +} function isClientAuthMethod(method) { return ['client_secret_basic', 'client_secret_post', 'none'].includes(method); } @@ -144,6 +147,9 @@ export async function auth(provider, options) { 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 InvalidClientError || error instanceof UnauthorizedClientError) { await provider.invalidateCredentials?.('all'); @@ -158,6 +164,8 @@ export async function auth(provider, options) { } } async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { + const oauthFetchFn = provider.getOAuthHttpFetch?.(); + const tokenFetchFn = oauthFetchFn ?? fetchFn; let resourceMetadata; let authorizationServerUrl; try { @@ -207,7 +215,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res const fullInformation = await registerClient(authorizationServerUrl, { metadata, clientMetadata: provider.clientMetadata, - fetchFn + fetchFn: tokenFetchFn }); await provider.saveClientInformation(fullInformation); clientInformation = fullInformation; @@ -221,7 +229,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res metadata, resource, authorizationCode, - fetchFn + fetchFn: tokenFetchFn }); await provider.saveTokens(tokens); return 'AUTHORIZED'; @@ -230,6 +238,8 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res // 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, @@ -237,12 +247,48 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res refreshToken: tokens.refresh_token, resource, addClientAuthentication: provider.addClientAuthentication, - fetchFn + fetchFn: tokenFetchFn }); await provider.saveTokens(newTokens); 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 OAuthError; + const __isServerError = error instanceof ServerError; + const __isRetryable = __cursorIsRetryableOAuthRefreshError(error, provider); + const __isDefiniteOAuthFailure = __isOAuthError && !__isServerError && !(error instanceof 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), + underlyingError: error, + }); + // [CURSOR PATCH] Release the refresh lease acquired in prepareForRefresh + // so sibling providers are not blocked until TTL expiry. + await provider.releaseRefreshLeaseOnError?.(error); + // Definite OAuth protocol failures (except server_error / temporarily_unavailable below) should surface to callers. + if (error instanceof OAuthError && !(error instanceof ServerError) && !(error instanceof TemporarilyUnavailableError)) { + throw error; + } + // Transient network / overload: do not fall through to interactive re-auth. + if (__cursorIsRetryableOAuthRefreshError(error, provider)) { + 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 OAuthError) || error instanceof ServerError) { // Could not refresh OAuth tokens diff --git a/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js b/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js index 624172a..369f450 100644 --- a/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js +++ b/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js @@ -149,7 +149,15 @@ export class StreamableHTTPClientTransport { 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); }); @@ -246,7 +254,15 @@ export class StreamableHTTPClientTransport { }, 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); } } }