Skip to content

feat(mcp): scope gateway session bearers to the RFC 8707 resource - #35045

Merged
tin-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4917_scoped_deeplink
Aug 15, 2026
Merged

feat(mcp): scope gateway session bearers to the RFC 8707 resource#35045
tin-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4917_scoped_deeplink

Conversation

@tin-berri

@tin-berri tin-berri commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Session bearers carried the user's whole MCP entitlement, never one server
  • The RFC 8707 resource param spec clients send was ignored

How it solves it:

  • A per-server resource seals that server through flow, code, and session tokens
  • Admission intersects the sealed scope against resolved grants, fail closed

User Flow

Before: a user connects an MCP client to one gateway server, and the token the client walks away with silently works on every server that user can reach

  1. Their MCP client POSTs https://litellm-domain/register and receives a client_id
  2. The client opens https://litellm-domain/authorize?...&resource=https://litellm-domain/mcp/github in the browser; the user lands on the LiteLLM connect page and approves
  3. The client exchanges the code at POST https://litellm-domain/token (sending the same resource) and receives a bearer token
  4. The client lists tools at POST https://litellm-domain/mcp/github and sees that server's tools
  5. The same bearer POSTed to https://litellm-domain/mcp/internal-billing, a server the user can also reach but the client never asked for, lists that server's tools too
  6. Anyone holding that one client's token can act on every MCP server in the user's entitlement

After: the same connection walk hands the client a token that works only on the server it asked for

  1. Their MCP client POSTs https://litellm-domain/register and receives a client_id
  2. The client opens https://litellm-domain/authorize?...&resource=https://litellm-domain/mcp/github in the browser; the user lands on the LiteLLM connect page and approves
  3. The client exchanges the code at POST https://litellm-domain/token (sending the same resource) and receives a bearer token
  4. The client lists tools at POST https://litellm-domain/mcp/github and sees that server's tools
  5. The same bearer POSTed to https://litellm-domain/mcp/internal-billing returns an empty tool list, and redeeming a code for a different resource than it was issued for fails with invalid_target
  6. Anyone holding that client's token can act only on the one server the user connected it to

Relevant issues

  • Reads the RFC 8707 resource parameter at /authorize and /token; a value resolving to exactly one gateway-managed oauth2 server scopes the session to it, and the token endpoint rejects a conflicting redemption with invalid_target on both grants
  • The scope is a restriction, never a grant: it intersects the admitted subject's resolved server set after every union (operator-open and the exception fallback included), so a scoped bearer can never reach servers outside its scope and a resolver fault can never widen it
  • Absent, aggregate, foreign, unknown, or non-gateway-managed resources keep today's flow byte for byte; unscoped tokens are minted without the new claim so they stay byte-compatible with pods that predate it

Linear ticket

Part of LIT-4917

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Live proxy on localhost:4917 at commit fcb1a5e, real Postgres, real public upstream (mcp.deepwiki.com). Servers: wiki_oauth (gateway-managed oauth2, allow_all_keys, a vaulted per-user token seeded via POST /v1/mcp/server/{id}/oauth-user-credential) and deepwiki (auth none, allow_all_keys), so an unscoped session reaches both and the only narrowing in play is the sealed scope

== W1. DCR register (open /register, loopback redirect) ==
client_id: llm_dcrc_evI4WCg...

== W2. scoped authorize (resource=/mcp/wiki_oauth): connect page interlude, no silent code ==
HTTP/1.1 303 See Other
location: http://localhost:4917/ui/connect?connect_flow=mkqfsitmnGejC4ZqMs1y4aSX-dubtw4X&connect_client=...

== W3. finish the connect page (POST /authorize/complete): code delivered to the client ==
303 -> http://127.0.0.1:8976/callback?code=llm_gcode_9dVUpRisAWg9x3...

== W4. token exchange (resource echoed per RFC 8707) ==
token_type: Bearer | error: None

== W5. scoped bearer at ITS OWN server /mcp/wiki_oauth ==
tools (3): ['wiki_oauth-ask_question', 'wiki_oauth-read_wiki_contents', 'wiki_oauth-read_wiki_structure']

== W6. SAME bearer at /mcp/deepwiki (granted via allow_all_keys, but OUTSIDE the sealed scope) ==
tools (0): []

== W7. redeem a scoped code for a DIFFERENT resource ==
{"error":"invalid_target","error_description":"resource does not match the scope this code was issued for"}

== W8. unscoped walk (no resource): flow unchanged, bearer reaches deepwiki via allow_all_keys ==
location: http://localhost:4917/ui/connect?connect_flow=Qhd48usFCKWlRLT47YT9qyJnaKeRtQUq&connect_client=...
tools (3): ['deepwiki-ask_question', 'deepwiki-read_wiki_contents', 'deepwiki-read_wiki_structure']

Type

🆕 New Feature

Caveats (if any)

  • Scoped flows still show the connect page; every code mint stays POST-bound
  • The connect-page skip lands in the follow-up PR behind a real intent step
  • Rolling deploy: old pods reject new SCOPED bearers with the standard re-auth challenge
  • Unscoped artifacts (flow cookie, code, bearer) carry no new key on the wire

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

Cursor Bugbot is generating a summary for commit fcb1a5e. Configure here.

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py Outdated
@veria-ai

veria-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

Comment thread litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds RFC 8707 resource scoping to gateway MCP sessions

  • Resolves per-server resources during authorization and seals the selected server into authorization codes, access tokens, and refresh tokens
  • Rejects conflicting resource values during code and refresh-token exchanges
  • Intersects scoped sessions with live MCP entitlements during admission while preserving existing unscoped token behavior
  • Keeps authorization-code issuance behind the POST-bound connect completion step

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py Propagates validated per-server resource scope through the POST-bound authorization flow and rejects conflicting token exchanges
litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py Transfers the sealed session resource scope onto the admitted user context for downstream authorization
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Intersects live server grants with the admitted session scope across normal and fallback entitlement paths
litellm/proxy/_experimental/mcp_server/outbound_credentials/session_token.py Adds an optional resource server identifier to session token claims while retaining the unscoped wire shape
litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py Accepts and forwards RFC 8707 resource parameters through aggregate authorization and token endpoints
litellm/proxy/_experimental/mcp_server/oauth_utils.py Exposes canonical resource URI normalization for shared resource resolution
litellm/proxy/_types.py Adds a server-only excluded field carrying the admitted session resource restriction
tests/test_litellm/proxy/_experimental/mcp_server/test_gateway_dcr_flow.py Covers scoped authorization, conflicting redemption, refresh propagation, and unscoped compatibility
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py Covers admission-time propagation of scoped and unscoped session claims
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py Covers scope intersection across entitlement unions and resolver-failure fallback behavior

Reviews (4): Last reviewed commit: "feat(mcp): scope gateway session bearers..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Base automatically changed from litellm_lit4864_perserver_keyless_oauth to litellm_internal_staging July 31, 2026 00:40
@devin-ai-integration
devin-ai-integration Bot force-pushed the litellm_lit4917_scoped_deeplink branch from eb27d65 to 7ab8a10 Compare August 14, 2026 03:07
@CLAassistant

CLAassistant commented Aug 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit4917_scoped_deeplink (fcb1a5e) with litellm_internal_staging (87abb87)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (33e9f54) during the generation of this report, so 87abb87 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@tin-berri
tin-berri force-pushed the litellm_lit4917_scoped_deeplink branch from 7ab8a10 to 7723635 Compare August 15, 2026 17:56
@tin-berri tin-berri changed the title feat(mcp): scope gateway session bearers to the RFC 8707 resource and complete scoped flows silently feat(mcp): scope gateway session bearers to the RFC 8707 resource Aug 15, 2026
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py Outdated
@tin-berri
tin-berri force-pushed the litellm_lit4917_scoped_deeplink branch from 7723635 to aff3ef9 Compare August 15, 2026 18:30
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py
@tin-berri
tin-berri force-pushed the litellm_lit4917_scoped_deeplink branch from aff3ef9 to fcb1a5e Compare August 15, 2026 18:54
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fcb1a5e. Configure here.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

QA'd live on a real proxy: scoped bearer is refused on out-of-scope tools/call, not just hidden from tools/list. Refresh rotation, invalid_target matrix, and the unscoped walk all hold

@tin-berri
tin-berri enabled auto-merge (squash) August 15, 2026 21:38
@tin-berri
tin-berri merged commit 4eadf92 into litellm_internal_staging Aug 15, 2026
73 of 74 checks passed
@tin-berri
tin-berri deleted the litellm_lit4917_scoped_deeplink branch August 15, 2026 21:56
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.

3 participants