fix(mcp): deny the interactive dcr_bridge authorize for a user without server access - #37865
Conversation
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
4/5 The fix is targeted, correct, and well-tested. Here's the breakdown: What works well:
Why not 5/5:
Otherwise this is a clean, minimal, well-reasoned fix for the silent failure described in #36358. |
Greptile SummaryThe PR prevents interactive bridge authorization from completing when the signed-in user lacks access to the requested MCP server
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py | Adds the interactive bridge access gate and OAuth denial redirect without leaving an eligible follow-up defect |
| litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py | Promotes the admitted-user reload helper to a public method and updates internal callers |
| litellm/proxy/_experimental/mcp_server/ui_session_utils.py | Updates the dashboard admitted-user context to use the renamed public helper |
| tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py | Covers granted, denied, missing-user, and retryable-failure bridge authorization behavior |
| basedpyright-code-budget.json | Ratchets the private-usage diagnostic budget after making the helper public |
Reviews (3): Last reviewed commit: "fix(mcp): deny the interactive dcr_bridg..." | Re-trigger Greptile
There was a problem hiding this comment.
✅ 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 f072fd2. Configure here.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Both P2 threads answered: the gate runs once per interactive connect, and the test idiom matches this file's siblings. The lint red is the pre-existing test-tree PT011/PT012 pair, fix in flight on #37864. @greptileai can you re-score with that context? |
f072fd2 to
bf4c892
Compare
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
The PR fixes a real, verified bug with a well-scoped change and solid test coverage. Here's my assessment: Score: 4/5 What's good:
What keeps it at 4 rather than 5: The admitted: Final = await MCPRequestHandler._reload_admitted_user( # pyright: ignore[reportPrivateUsage]is a coupling signal. Calling a private class method across module boundaries from non-test code is a design smell — if |
There was a problem hiding this comment.
✅ 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 bf4c892. Configure here.
bf4c892 to
bedc287
Compare
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4.5/5 Why: What's good:
Why not 5/5 — one thing to verify: The existing Everything else — the caveats about key-hash subjects, the relay-bridge arm being ungated, and the post-mint revocation window — is correctly documented and handled. The implementation is correct. |
There was a problem hiding this comment.
✅ 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 bedc287. Configure here.
…t server access The dcr_bridge oauth_delegate connect flow completed for a signed-in user with no litellm-side grant to the target server: every leg returned 200, the DCR client showed connected, and tools/list then fail-closed to an empty list with the upstream never contacted (#36358). The authorize leg now admits the user the way MCP egress will (same reload_admitted_user constructor, same get_allowed_mcp_servers resolver) and refuses with an RFC 6749 access_denied redirect naming the remedy, before any upstream OAuth runs or an envelope is minted. Availability faults (5xx) propagate; unknown or deactivated users deny fail-closed Promotes MCPRequestHandler reload_admitted_user to public: it already had a cross-module consumer in ui_session_utils, and this gate adds a second, so the private name no longer reflected its use. Ratchets the freed reportPrivateUsage budget headroom down
bedc287 to
f9ecf18
Compare
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4.5/5 Why: Strengths:
Minor concerns keeping it from 5/5:
Overall this is a solid, well-reasoned fix with good evidence of correctness and appropriate test coverage. The approach is conservative (reusing existing resolvers, fail-closed on errors) which is exactly right for an auth path. |
|
All three are deliberate: redirect shaping lives with the endpoint while the access decision is delegated; the registry try/finally and the state echo match sibling idioms |
There was a problem hiding this comment.
✅ 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 f9ecf18. Configure here.
|
@greptileai review the latest commit please |
TLDR
Problem this solves:
How it solves it:
User Flow
Before: an operator adds their gateway's dcr_bridge MCP server as a claude.ai custom connector and ends up connected with no tools and no clue why
https://litellm-domain/mcp/example_bridgeas a custom connector and click connectAfter: the same operator finds out at connect time, with an error naming the fix
error=access_deniedand the description "the signed-in user has no access to MCP server 'example_bridge' on this gateway; grant it through a team or user object permission, or mark the server allow_all_keys"allow_all_keys: true), the same connect flow completes and tools list and runRelevant issues
Addresses the silent failure behind #36358. The issue's traced mechanism (the v2 resolver discarding the admission-injected credential in
_create_mcp_client) does not reproduce: on the reporter's own version (v1.97.0-rc.1) and on current staging, a user who does have a grant gets the unsealed upstream credential delivered correctly through the v2 PassthroughConfig path, proven live below. The observed symptom (connected, zero tools, upstream session count 0) comes from the allowed-servers scope filter failing closed for a bridge-admitted user with no litellm-side grant, silentlyLinear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Shared setup: proxy on
localhost:4610backed by Postgres, a stub upstream MCP server on:8933that 401s unless it receivesAuthorization: Bearer real_upstream_token_12345and counts every authed request, and a stub IdP on:8934whose/tokenreturns that bearer asaccess_token. Three DB-defined MCP servers, allauth_type: oauth_delegate, dcr_bridge: true, pointed at the stub upstream:bridge_gated(signed-in user has no grant),example_bridge(user granted via object permission),bridge_open(allow_all_keys: true). The walk simulates an external DCR client: POST/login, POST/{server}/register, GET/{server}/authorizefollowing redirects, POST/{server}/token, then MCP initialize and tools/list against/mcp/{server}with the minted envelope bearer and no cookiesBefore (40b8300)
no grant: every leg 200, connected, silently zero tools
GET /example_bridge/authorize?...returns 307 to the stub IdP, the callback returns 302 with a gateway code,POST /example_bridge/tokenreturns 200 with{"access_token": "llm_env_...", "token_type": "Bearer"}tools/listreturns{"jsonrpc":"2.0","id":2,"result":{"tools":[]}}{"authed_requests": 0, "unauthed_requests": 0, "last_auth_header": null}, the upstream was never contactedwith grant: works, proving the credential path was never the bug
POST /user/updatewith{"object_permission": {"mcp_servers": ["example_bridge"]}}tools/listreturns theexample_bridge-echotool{"authed_requests": 3, ..., "last_auth_header": "Bearer real_upstream_token_12345"}After (f9ecf18)
no grant: denied loudly at connect time
GET /bridge_gated/authorize?...&state=probe_state_xyzreturns 302 straight back to the client:http://127.0.0.1:9777/callback?error=access_denied&error_description=the+signed-in+user+has+no+access+to+MCP+server+%27bridge_gated%27...&state=probe_state_xyzwith grant: byte-identical success
example_bridgecompletes;tools/listreturnsexample_bridge-echolast_auth_header: "Bearer real_upstream_token_12345"allow_all_keys: byte-identical success
bridge_opencompletes;tools/listreturnsbridge_open-echotools/callonbridge_open-echowith{"text": "proof-36358"}returns"echo: proof-36358"through the gatewayType
🐛 Bug Fix
Caveats (if any)
Promotes
reload_admitted_userto public per review: it already had a cross-module consumer in ui_session_utils, and the gate adds a second; the freed reportPrivateUsage budget is ratcheted downKey-hash-subject envelopes never pass this authorize leg; key scoping already governs them
A grant revoked after mint still fail-closes silently at egress, as any scoped principal does
The relay bridge arm (true_passthrough) is deliberately ungated; egress admits it anonymously by design
Final Attestation