Skip to content

[v1.x] fix(client): send the scope parameter on refresh-token requests - #2937

Open
thegodshatetexas wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
thegodshatetexas:fix/v1x-refresh-token-scope
Open

thegodshatetexas wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
thegodshatetexas:fix/v1x-refresh-token-scope

Conversation

@thegodshatetexas

Copy link
Copy Markdown

Backport of #2720 to v1.x. Fixes #2718 for the v1 line.

refreshAuthorization() gains an optional scope and sets it on the refresh request when it is a non-empty string, and the auth() flow sends exactly the granted scope recorded on the stored tokens. When no granted scope is recorded, nothing changes on the wire.

Motivation and Context

#2720 fixes main. #2718's reporter and I are both blocked on clients that bundle v1.x, and #2720 says so itself: "v1.x (src/client/auth.ts) has the same two-parameter request body; happy to open the backport if wanted." That backport has not been opened, so the fix currently reaches nobody on v1.

Concretely, Claude Code 2.1.287 bundles this line. Its refreshAuthorization and executeTokenRequest match tag 1.31.0 parameter for parameter and do not match main, which adds dpop. Merging #2720 alone therefore does not fix the client named in #2718.

The underlying problem: refreshAuthorization() builds its token request from grant_type and refresh_token only, so there is no way to put scope on a refresh request. Omitting it is legal per RFC 6749 §6, but some authorization servers require it to resolve the target resource. Microsoft Entra ID rejects a scope-less refresh with AADSTS90009 ("Application is requesting a token for itself") whenever the OAuth client application is also the resource, a standard setup for Entra-protected MCP servers. Initial authorization succeeds because the scope travels on /authorize, so affected clients work until the first access-token expiry and then wedge.

Design notes, kept deliberately identical to #2720 so the two lines stay consistent:

  • scope is set only when non-empty. An empty granted scope (GitHub returns "scope": "") sends no parameter, since a literal empty scope= is invalid per RFC 6749 §3.3.
  • The result preserves the granted scope when the response omits it, the same way it already preserves the refresh token. RFC 6749 §5.1 lets the server omit scope when it equals the request, and dropping it would strand the next refresh with nothing to send. A scope returned by the server still wins.
  • auth() sends the stored granted scope, not a recomputed value, which may have widened since the grant and would draw invalid_scope where today's scope-less request succeeds.

How Has This Been Tested?

Against a live Entra tenant, and in the suite.

On the tenant, using a fresh refresh token and varying only resource and scope: three scope-less variants fail with AADSTS90009 across three different resource values, and the same grant carrying the granted scope returns 200 both with and without resource. The resource form is irrelevant; the missing scope is the whole cause.

Unit tests were written failing-first. With them in place and src/client/auth.ts reverted, exactly the two behavioral tests fail:

  • refreshAuthorization with scope puts it on the body (fails without the change)
  • the granted scope is preserved when the response omits it (fails without the change)
  • refreshAuthorization without scope sends no parameter (back-compat pin, passes either way)
  • scope: '' sends no parameter (RFC 6749 §3.3 pin, passes either way)
  • a server-returned scope overrides the requested one (down-scope case)

Full suite: 1857 passed across 56 files. tsgo --noEmit, eslint and prettier all clean.

Breaking Changes

None. The new option is additive, no public API is removed or changed, and with scope omitted the request body is byte-for-byte what it was.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

A changeset is included (patch, @modelcontextprotocol/sdk), and the new option carries TSDoc explaining the RFC 6749 §6 position and the Entra behavior.

#2720 has not merged yet, so if its design changes in review I will rebase this to match rather than let the two lines diverge. Happy to hold until #2720 lands if you would rather take them in order.

refreshAuthorization() accepts a new optional `scope` and sets it on the request
body when it is a non-empty string. The auth() flow sends exactly the granted
scope recorded on the stored tokens. When no granted scope is recorded, no scope
parameter is sent, so the wire shape is unchanged by default.

Omitting scope on a refresh grant is legal per RFC 6749 section 6, but some
authorization servers require it to resolve the target resource. Microsoft Entra
ID rejects a scope-less refresh with AADSTS90009 whenever the OAuth client
application is also the resource, a common setup for Entra-protected MCP servers.
Initial authorization succeeds because the scope travels on the authorize
request, so affected clients work until the first access-token expiry and then
wedge.

Backport of the v2 fix for modelcontextprotocol#2718.
@thegodshatetexas
thegodshatetexas requested review from a team as code owners October 2, 2026 17:47
@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a40eb50

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2937

commit: a40eb50

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v1 Issues / PRs related to v1.x

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant