Skip to content

fix(oauth): ask for the media scopes, grant them only on a media-eligible project - #541

Merged
ABB65 merged 2 commits into
mainfrom
fix/oauth-challenge-media-scopes
Nov 4, 2026
Merged

ABB65 merged 2 commits into
mainfrom
fix/oauth-challenge-media-scopes

Conversation

@ABB65

@ABB65 ABB65 commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

New grants never got the media scopes, so the media tools never appeared, even on eligible projects.

Cause: the 401 challenge (CHALLENGE_SCOPES) asked only for content:read content:write project:metadata, and clients request the challenge scope. Media was meant to arrive by step-up: a media tool call answers 403 insufficient_scope and the client re-consents. Since #536, tools/list hides media tools from a grant without a media scope. The model never sees one and never calls one, so the step-up never fires.

Change:

  • Ask for media where it exists. challengeScopes({ mediaAvailable }) (scopes.ts) is the base three, plus media:read media:write where the media stack exists. This is the same useMediaProvider() !== null switch advertisedScopes uses. mcpUnauthorized takes { mediaAvailable }, and /api/mcp/remote passes it.
  • Grant media only where the chosen project can use it. OAuth lets the server grant less than was requested.
    • projectMediaEligible (server/utils/media-eligibility.ts) is the proxy's media rule, moved out unchanged: media stack, plan media.upload, cdn_enabled, workspace owner. The proxy, consent GET and consent POST all call it.
    • Consent POST narrows the code's scope with withoutMediaScopes on a project that isn't eligible. That helper goes through normalizeScope, so a grant never ends up with nothing callable.
    • The token endpoint already issues stored.scope, so the grant and the token response carry the narrowed scope.
  • Consent page. GET adds mediaEligible per project. The page lists the media permissions only once a media-eligible project is picked. A project that becomes eligible later needs a reconnect. No new copy.
  • Unchanged. Step-up stays as it is. fix(mcp): tools/list lists only the tools the key or grant can call #536's tools/list filter stays, so no listed tool answers 403. /oauth/authorize already keeps every known scope (normalizeScope).

Tests:

  • oauth-consent (functional), on a media deployment with the challenge scope requested:
    • GET flags a CDN project as mediaEligible: true and a non-CDN one as false;
    • approving the eligible project mints a code that carries media:read media:write;
    • approving without CDN delivery, on a plan without media.upload, or on a deployment without the media stack mints a code without media.
  • mcp-oauth-proxy (functional):
    • on a media deployment, the bare 401's scope goes through normalizeScope into a grant that carries media;
    • that grant's tools/list on an eligible project lists the media tools, and the loopback gets the media identity headers;
    • where the media stack is absent, the challenge has no media:.
  • Nuxt oauth-consent-page: the media permission appears only after a media-eligible project is picked.
  • Unit: challengeScopes in both modes, withoutMediaScopes, and the 401 header on a media deployment.

Docs: one clause in CLAUDE.md / AGENTS.md (MCP Cloud paragraph) and in docs/REMOTE_MCP.md (scopes). Existing connections reconnect once after deploy to pick up media.

Class: GENERIC

ABB65 added 2 commits November 4, 2026 13:36
The 401 challenge only asked for content:read content:write
project:metadata, so every new grant lacked media:*. Media was meant to
arrive by step-up (403 insufficient_scope on a media tool call), but
tools/list hides media tools from a grant without a media scope, so the
model never sees one, never calls it, and the step-up never fires.

On deployments with the media stack (the same useMediaProvider() switch
advertisedScopes uses), the 401 challenge now also asks for media:read
media:write. Per-project eligibility (plan, cdn_enabled, owner) still
decides what tools/list shows, and step-up is unchanged.
The challenge asks for media:* on every media deployment, but consent
showed the requested scopes read-only, so every first grant carried
media:write (URL ingest + delete) even on a project that cannot use it.

projectMediaEligible (media stack, plan media.upload, cdn_enabled,
workspace owner) is the proxy's media rule moved out unchanged; the
proxy, consent GET and consent POST share it. Consent POST narrows the
code's scope with withoutMediaScopes on a non-eligible project, and the
token endpoint issues that narrowed scope. The consent page lists the
media permissions only once a media-eligible project is picked. A
project that becomes eligible later needs a reconnect.
@ABB65 ABB65 changed the title fix(oauth): first consent asks for the media scopes where media exists fix(oauth): ask for the media scopes, grant them only on a media-eligible project Nov 4, 2026
@ABB65
ABB65 merged commit f9cb482 into main Nov 4, 2026
2 checks passed
@ABB65
ABB65 deleted the fix/oauth-challenge-media-scopes branch November 4, 2026 11:07
ABB65 added a commit that referenced this pull request Nov 4, 2026
#543)

* chore(release): v0.4.23, connector asks for media on eligible projects

The connector gets the media tools on media-eligible projects (#541).
No migrations, no new environment variables.

* chore(release): v0.4.23 changelog lists #544
ABB65 added a commit that referenced this pull request Nov 4, 2026
#542 landed as a squash (89f30db); the branch carried its commits. Conflicts resolved as
this branch's changes on top of main (3-way with #542's head as base), keeping #541/#543/#544.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant