Repository navigation
Conversation
With an OAuthHandler on StreamableClientTransport, Client.Connect sends server/discover first. If that request is still unauthorized after OAuthHandler.Authorize ran, or Authorize itself failed, Connect treated the error like any other non-modern discover error and fell back to initialize. The initialize POST got the same 401/403, so the transport ran Authorize a second time, which for interactive handlers means a second consent prompt. A 401 or 403 is not a protocol era signal: a legacy server that requires authorization rejects initialize the same way. The streamable client now wraps authorization failures with an unexported sentinel, and Connect returns that error instead of falling back. A legacy server behind authorization still connects through initialize, since the retried discover reaches it with valid credentials and fails for a non-auth reason. Fixes modelcontextprotocol#1423
kerthans
force-pushed
the
fix/discover-auth-no-fallback
branch
from
October 22, 2026 07:26
1db0d6a to
5e069e9
Compare
Contributor
Author
|
Rebased on main (b874fad). |
Comment on lines
+2625
to
+2627
| if (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden) && c.oauthHandler != nil { | ||
| err = fmt.Errorf("%w: %w", err, errAuthorizationFailed) | ||
| } |
Contributor
There was a problem hiding this comment.
when Authorize succeeds and the retried server/discover is still 401/403, we should still fall back to initialize with the acquired token (a server may authorize per method), just without calling Authorize a second time (see my comment on #1423). Currently a server that returns 403 for server/discover but allows initialize connects on main and fails with this PR. Could you tag only the Authorize error, and skip Authorize for the fallback initialize instead?
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With an
OAuthHandleronStreamableClientTransport,Client.Connectsendsserver/discoverfirst. If that request is still unauthorized afterOAuthHandler.Authorizeran, orAuthorizeitself fails,Connecttreats the error like any other non-modern discover error and falls back toinitialize. TheinitializePOST gets the same 401/403, soAuthorizeruns a second time. Withauth.AuthorizationCodeHandlerthat is a second full flow and a second consent prompt for a singleConnect, and the returned error namesinitialize, a method a modern server never needed.A 401 or 403 is not a protocol era signal: a legacy server that requires authorization rejects
initializethe same way, so falling back cannot succeed and only repeats the authorization flow.This change:
streamableClientConn.Write, wraps authorization failures with an unexported sentinel, both whenAuthorizereturns an error and when the retried request is still 401/403.Client.Connect, closes the session and returns that error instead of falling back toinitialize.A legacy server behind authorization still connects as before: the first 401 runs
Authorize, the retriedserver/discoverreaches the server with valid credentials and fails with a non-auth error, and theinitializefallback proceeds with a single authorization.Tests:
Authorizereturns an error:Authorizeis called once,initializeis never sent, and the error wraps the handler's error.Authorizesucceeds but the retried request is still 401:Authorizeis called once andinitializeis never sent.RequireBearerTokenstill connects throughinitialize(2025-11-25) with one authorization.Non-goals: no exported API changes (an exported
ErrUnauthorizedwould need a proposal), and no change to the stdio discover fallback (#1334) or to authorization handling for requests other than the handshake.AI assistance was used to prepare this change; I reviewed and tested it.
Fixes #1423