Repository navigation
OAuth on the MCP SDK v2; any 401 challenge starts authorization - #28
Merged
Merged
Conversation
Replace the hand-written OAuth client (metadata discovery, DCR, PKCE, token exchange, refresh) with the official SDK's auth() and an mcpx OAuthClientProvider, and move from @modelcontextprotocol/sdk 1.x to @modelcontextprotocol/client 2.4. A server's need for OAuth is now decided by its challenge, not by an @add probe. The HTTP transport carries an authProvider whose onUnauthorized surfaces the 401 (or 403 insufficient_scope) challenge; the session pool joins the shared Authentication Flow with it and retries once. Mixed-auth servers such as whiteboardanimation.ai — anonymous tools beside protected ones, 405 on GET — previously froze as auth "none" and failed every protected call with reauth-required. - Credential Store v3: one OAuth grant per server in the SDK's stored shapes (client + tokens, issuer-stamped). A journaled migration converts v2 tokens; manual clients keep their secret and fixed redirect URI. - Refresh never opens the browser; a new sign-in never replays stored tokens or a client registered for another loopback port, and only replaces the grant once it succeeds. Invalidation deletes only what the server refused. - @add no longer probes with GET; a challenged tool listing records OAuth. - Manual clients (Slack) keep http://127.0.0.1:65245/callback; Slack's authed_user token response is still accepted. - ADR 0009 records the challenge as the OAuth discovery entry. Bump to 0.14.0. Why: src/oauth.ts#OAuthCallback | a stopped Bun server reports port 0, so the code exchange must reuse the redirect URI captured at listen time Why: src/mcp-client.ts#challengeAuthProvider | the transport must not run OAuth inside a request; the Runtime owns single-flight, Caller Input and registry writes
Slack is reached through its own CLI, not an mcpx server, so the authed_user token-response shim goes. Manually registered OAuth clients stay generic.
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.
Summary
A server's need for OAuth is now decided by its Authorization Challenge, on any request, not by an
@addGET probe. OAuth itself is the official SDK'sauth()(@modelcontextprotocol/client2.4) driven by an mcpxOAuthClientProvider;src/oauth.tsno longer hand-rolls discovery/DCR/PKCE/exchange/refresh.src/ -├── auth-discovery.ts # GET probe at @add, frozen as auth kind ├── mcp-client.ts # + challengeAuthProvider, authorizationChallenge() ├── oauth.ts # SDK auth() + McpxOAuthProvider + loopback callback ├── runtime-stores.ts # Credential Store v3: oauth[server] = {client, tokens}; journaled v2→v3 migration └── runtime-authentication.tshttp://127.0.0.1:65245/callback. Slack-specificauthed_userhandling is removed (Slack is used through its own CLI).@addreportsauth-requiredwhen the tool listing is challenged; it still never opens a browser.Evidence
mcpx wb.status(whiteboardanimation.ai, registered asauth: nonebecauseGET /mcp→ 405) →Credentials for wb must be refreshed.After (local build, isolated
MCPX_HOMEcopy of real state):bun run check: 127 pass. Newtests/oauth-challenge.test.tsruns the real SDK transport against a mixed-auth fixture: anonymousquoteok →status401 → browser → retry ok → next call reuses grant.tests/oauth.test.tscovers DCR, refresh, revoked-refresh fallback, compare-and-delete invalidation, manual client,issmismatch.port 0afterstop(), so the code exchange sent a differentredirect_uri); fixed, and the fixture now enforcesredirect_uriequality.prototype:cancellation:evidencere-run on v2: same verdicts; only the error type changed (SdkError REQUEST_TIMEOUT) and v2 no longer sends a stray cancel after completion.Merge Danger
Door: one-way
The first 0.14.0 Runtime start migrates
state-v2/credentials.jsonto version 3 (journaled). A 0.13.x binary cannot read v3 credentials, so downgrading after running 0.14.0 means re-authenticating OAuth servers.Blast Radius: authentication
Every OAuth server and every HTTP call path (token now via transport
authProvider). Bearer servers use the same path (round-robin unchanged). Servers previously recordedunknownbecomenoneand are re-decided by their next challenge. SDK v2 also touches stdio/notifications (covered by existing dogfood tests).Need help on this PR? Tag
@codesmith-botwith what you need.