feat(mcp): let users select the entra_obo token_exchange profile in the UI and API - #32144
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR surfaces the
Confidence Score: 4/5The backend changes are additive and well-tested; the main risk is in the edit-mode scope validation in the dashboard form. The Profile selector's scope validation uses ui/litellm-dashboard/src/components/mcp_tools/TokenExchangeFormFields.tsx — the scope
|
| Filename | Overview |
|---|---|
| litellm-proxy-extras/litellm_proxy_extras/migrations/20260703120000_add_token_exchange_profile_to_mcp_servers/migration.sql | Adds nullable token_exchange_profile column to LiteLLM_MCPServerTable with IF NOT EXISTS guard — additive and safe. |
| litellm/proxy/_experimental/mcp_server/mcp_server_manager.py | Adds column-first read (then blob fallback, then rfc8693 default) for token_exchange_profile in build_mcp_server_from_table, and carries it through both _build_mcp_server_table builders. |
| litellm/proxy/_experimental/mcp_server/db.py | Adds token_exchange_profile to _AUTH_FLOW_SCOPED_FIELDS (cleared on auth-type switch) and _TOKEN_EXCHANGE_COLUMN_FIELDS (lifted out of blob on write) — consistent with sibling fields. |
| litellm/proxy/management_endpoints/mcp_management_endpoints.py | Both non-admin and virtual-key sanitizers now also null out token_exchange_profile; changes are minimal and consistent. |
| ui/litellm-dashboard/src/components/mcp_tools/TokenExchangeFormFields.tsx | Adds a Profile dropdown; Entra OBO selection hides Audience/Subject Token Type and makes Scopes required — but required: true on scopes has no isEditing guard, blocking edits to existing entra_obo servers. |
| tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py | Adds four focused tests for column read, default, blob fallback, and round-trip for token_exchange_profile; also adds a config-loading test. All new tests, no existing assertions weakened. |
Reviews (4): Last reviewed commit: "test(mcp): assert every token-exchange s..." | Re-trigger Greptile
This comment was marked as outdated.
This comment was marked as outdated.
149784a to
65a4708
Compare
…he UI and API
The backend token_exchange arm supports two wire dialects via token_exchange_profile
("rfc8693" default, or "entra_obo" for Microsoft Entra's On-Behalf-Of, the RFC 7523
jwt-bearer grant), but it could only be set through config.yaml. This surfaces it to the
create/update REST API and the dashboard so an admin can create an entra_obo server there,
completing the parity started in the parent PR for the other token-exchange fields.
token_exchange_profile becomes a dedicated column on LiteLLM_MCPServerTable, mirroring the
sibling fields: it is added to the request models, read column-first in
build_mcp_server_from_table with the credentials-blob as a back-compat fallback and a
default of rfc8693, and carried through both runtime-to-table builders so registry
round-trips preserve it. It is a non-secret dialect selector, so it is not scrubbed from
non-admin or virtual-key responses.
In the dashboard a Profile dropdown (RFC 8693 vs Microsoft Entra OBO) is added to the
token-exchange section. Entra OBO carries the target resource in the scope, so selecting it
makes the scope required and hints the api://<app-id>/.default form, while audience and
subject_token_type (which that dialect ignores) are hidden.
…oken_exchange_profile token_exchange_profile gets the same storage contract as the other three token-exchange settings: the column is authoritative, a blob copy is the legacy shape — lifted into the column on every write and stripped from the stored blob — and switching auth_type away from token exchange clears it (_AUTH_FLOW_SCOPED_FIELDS). Both restricted-view sanitizers scrub it for uniformity, and the edit form's auth-switch payload nulling includes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0a03da8 to
de8debc
Compare
|
@greptileai rereview |
|
bugbot run |
| /> | ||
| } | ||
| name="token_exchange_profile" | ||
| {...(isEditing ? {} : { initialValue: "rfc8693" })} |
There was a problem hiding this comment.
Create form writes default profile
Low Severity
The create flow sets initialValue: "rfc8693" on token_exchange_profile, so dashboard creates POST rfc8693 into the dedicated column. The PR storage contract says a null column means the rfc8693 default at egress only and that default must never be written to the database.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit de8debc. Configure here.
|
Create form writes default profile (Low) — intentional; the storage-contract wording in the description was imprecise and has been fixed. The "never written to the DB" clause is about the backend: the egress default-application never persists anything, and REST creates omitting the field leave the column null. The dashboard persists the admin's visible selection — the create form preselects "RFC 8693 (standard)", so submitting stamps it explicitly, consistent with how the oauth2_flow selector persists shown choices. Stamped- |
…nfig.yaml Pins the config surface: token_exchange_endpoint, audience, subject_token_type and token_exchange_profile load from top-level config keys onto the built server and through to the resolver spec; omitted keys resolve to their documented defaults (RFC 8693 subject token type, rfc8693 profile), and token_exchange servers need no oauth2_flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mateo-berri
left a comment
There was a problem hiding this comment.
The only discrepancy is between the main PR description prose and the actual scrubbing implementation, which is a documentation error rather than a code defect.
The PR description's main prose claims token_exchange_profile is left visible in non-admin and virtual-key responses, but mcp_management_endpoints.py actually scrubs it in both sanitizers — worth clarifying which behavior is intended before the description is used as a reference.
Worth clarifying here?
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c77fffd. Configure here.
|
Description/code discrepancy on token_exchange_profile scrubbing — good catch; the code is what's intended and the prose was stale. The original commit deliberately left the profile visible (non-secret dialect selector); the follow-up lift commit changed that decision to the uniformity rule #31772 settled on for |


Relevant issues
Stacked on #31772 (the token-exchange config UI/API PR); its base is that branch and it retargets down the stack as the parents merge. This completes the frontend/API parity for the
entra_obobackend added in #31983 (LIT-4163): that PR taught the token-exchange arm Microsoft Entra's On-Behalf-Of dialect, but the profile could only be set through config.yamlLinear ticket
Complements LIT-4163
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy on
localhost:4000backed by Postgres (master keysk-1234). Create an entra_obo server and an rfc8693 (default) server through the API, then read them back and inspect the DB columnThe DB column holds the selected dialect for the entra server and stays null for the default, and the read path fills in rfc8693 for the null case
Dashboard: at http://localhost:4000/ui/?page=mcp-servers, Add New MCP Server, pick Streamable HTTP, choose auth type "OAuth Token Exchange (OBO)", then set Profile to "Microsoft Entra OBO". The Scope field becomes required (hinting api:///.default) and Audience/Subject Token Type hide since that dialect ignores them; fill Client ID/Secret and a scope, then create
Dashboard Profile selector (before / after)
Rendered from the real create-form components on the parent branch head
ff6dc33291(before) vs this PR's headde8debc750(after), same steps each time: Add New MCP Server, Streamable HTTP, auth type "OAuth Token Exchange (OBO)"Before (parent #31772): the token-exchange section has no dialect control, so a dashboard user can only ever create an rfc8693 server
After: a Profile dropdown appears, defaulting to "RFC 8693 (standard)" with the standard Audience, Subject Token Type and optional Scopes fields intact
The dropdown offers the two wire dialects
After, Microsoft Entra OBO selected: Audience and Subject Token Type disappear (that dialect ignores them) and Scopes flips to required with the
api://<app-id>/.defaulthintType
🆕 New Feature
Changes
The token-exchange arm supports two wire dialects through
token_exchange_profile:rfc8693(the default standard token-exchange grant) andentra_obo(Microsoft Entra On-Behalf-Of, the RFC 7523 jwt-bearer grant withrequested_token_use=on_behalf_of). The backend already reads it from config.yaml and the credentials blob, but the create/update REST API and the dashboard had no way to set it, so dashboard and API users could only ever create rfc8693 servers. This surfaces the selector, completing the parity the parent PR started for the other token-exchange fieldstoken_exchange_profilebecomes a dedicated column onLiteLLM_MCPServerTable, mirroring the sibling fields exactly. It is added toNewMCPServerRequest/UpdateMCPServerRequest, read column-first inbuild_mcp_server_from_tablewith the credentials blob as a back-compat fallback and a default ofrfc8693, and carried through both runtime-to-table builders so a registry round-trip preserves it. Although it is a non-secret dialect selector, it is scrubbed from non-admin and virtual-key responses like every other token-exchange setting: those views receive no token-exchange config at all, matching the uniformity rule #31772 settled on forsubject_token_typeOn the dashboard a Profile dropdown is added to the token-exchange section. Selecting Microsoft Entra OBO makes the scope required, since that dialect carries the target resource in the scope and the exchanger fails closed without one, and hides Audience and Subject Token Type since the Entra dialect ignores them
Tests cover the profile column read, the credentials-blob fallback, the rfc8693 default, the table round-trip, the REST create and partial-update write paths carrying
token_exchange_profile=entra_obo, and the dashboard making the scope required when Entra OBO is selectedStorage contract: token-exchange settings (blob → columns)
token_exchange_profilefollows the storage contract #31772 establishes for the other token-exchange settings: the dedicated column is authoritative, and the same key insidecredentialsis the legacy pre-column shape — still accepted, but lifted into the column on write and stripped from the stored blob (an explicit top-level value, including an explicit null, always wins). Thecolumn or blobread fallback therefore only serves untouched pre-column rows. A null column means therfc8693default, applied at the egress build site — the backend never persists the default on its own (REST creates that omit the field leave the column null). The dashboard, by contrast, persists the admin's visible selection, which may equal the default: the create form preselects "RFC 8693 (standard)", so dashboard creates stamprfc8693explicitly. Stamped-rfc8693and null are behaviorally identical at every read site, and the write path must support persistingrfc8693regardless, since editing anentra_oboserver back to the standard dialect has to write something. The lift fortoken_exchange_profileis implemented in this PR's second commit, together with restricted-view scrubbing (both sanitizers) and auth-switch clearing (_AUTH_FLOW_SCOPED_FIELDS+ the edit form's payload nulling).Note
Medium Risk
Changes OAuth token-exchange configuration persistence and runtime spec building (dialect selection), but follows established column/lift patterns and is covered by broad tests including auth-switch and sanitization paths.
Overview
Adds
token_exchange_profileas a first-class MCP server setting so operators can chooserfc8693vsentra_obothrough the REST API and dashboard, not onlyconfig.yamlor the legacycredentialsblob.A new
LiteLLM_MCPServerTable.token_exchange_profilecolumn (with migration) is wired through request/response models, DB prepare/lift/strip logic (same contract as other token-exchange columns),build_mcp_server_from_table(column → blob fallback → defaultrfc8693), and table round-trips. Auth-type switches clear the field; restricted API views scrub it like other token-exchange settings.The dashboard Profile control on the token-exchange form defaults to RFC 8693; Microsoft Entra OBO hides Audience/Subject Token Type and requires scopes. Tests cover create/update, blob migration, config load, and UI validation.
Reviewed by Cursor Bugbot for commit c77fffd. Bugbot is set up for automated code reviews on this repo. Configure here.
Link to Devin session: https://app.devin.ai/sessions/f03da2725ec94d28b3facf766871b102