Skip to content

fix(mcp): persist DCR client_id from on-create MCP OAuth Authorize & Fetch - #31920

Merged
tin-berri merged 6 commits into
litellm_internal_stagingfrom
litellm_persist_mcp_dcr_client_oncreate
Jul 3, 2026
Merged

fix(mcp): persist DCR client_id from on-create MCP OAuth Authorize & Fetch#31920
tin-berri merged 6 commits into
litellm_internal_stagingfrom
litellm_persist_mcp_dcr_client_oncreate

Conversation

@tin-berri

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

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

LIT-4154 https://linear.app/litellm-ai/issue/LIT-4154

This is the stacked follow-up to #31912. It covers variant 2 (on-create "Authorize and Fetch"); #31912 covered variant 1 (post-create authorize).

Pre-Submission checklist

  • 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 requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

The "Authorize and Fetch" button on the create form runs OAuth Dynamic Client Registration (RFC 7591) against a temporary in-memory server that has no DB row, then creates the real server afterward. #31912 persists the DCR client_id when authorize runs against a server that already has a DB row, but on-create there is no row yet, so the minted client_id was dropped and the created server could not refresh (its row had credentials={} and token_url null, and the refresh_token grant 401d at the upstream token endpoint).

The UI already holds the DCR client_id (and client_secret when the upstream returns one) from the register step; it was just discarded at onTokenReceived. This change carries it into form.credentials, so it rides the existing settable credentials.client_id field into the create request, and the backend persists it through the same encrypt_credentials path it already uses for manually-entered OAuth clients. token_url is not carried because it is re-discovered on load (RFC 9728 then 8414); token_endpoint_auth_method is not needed because this flow only ever registers as client_secret_post or none, never client_secret_basic.

Backend already persists a create-supplied client_id encrypted (existing behavior, confirming the mechanism this fix feeds):

$ curl -sX POST localhost:4000/v1/mcp/server -H "Authorization: Bearer $KEY" \
    -d '{"server_name":"proof","url":"https://mcp.linear.app/mcp","transport":"http","auth_type":"oauth2","credentials":{"client_id":"proof-dcr-client-xyz"}}'
# server row: has_client_id=t, stored value is encrypted (kBG8OpnL_uk1... != the plaintext sent)

Live UI before/after (Authorize and Fetch on create, real upstream MCP OAuth, same proxy + Postgres):

BEFORE (server added through the pre-fix build): row credentials={}, token_url null
  $ curl -s "localhost:4000/mcp-rest/tools/list?server_id=<old>" -H "Authorization: Bearer $KEY"
    {"tools": [], "message": "Successfully retrieved tools"}          # 0 tools
  log: refresh_user_oauth_token: refresh request failed ... 401 Unauthorized ... https://<upstream>/token

AFTER (server added through the fixed "Authorize and Fetch"): credentials jsonb has an encrypted client_id
  # expire the stored token, then list tools
  $ curl -s "localhost:4000/mcp-rest/tools/list?server_id=<new>" -H "Authorization: Bearer $KEY"
    {"tools": [ ...18 tools... ], "message": "Successfully retrieved tools"}   # 18 tools
  # the refresh rotated the token: stored expires_at moved from the forced-past value to a fresh future one

Type

Bug Fix

Changes

useMcpOAuthFlow already captures the DCR client_id and client_secret from the register response into its flow state and uses them for authorize and token exchange, but it passed only the token to the onTokenReceived callback. This forwards the registered client to onTokenReceived, and the create form writes client_id (and client_secret when present) into form.credentials alongside the tokens, so the create request carries them and the backend persists them. No backend change is needed; the create path already encrypts and stores credentials.client_id.

A regression test in useMcpOAuthFlow.test.tsx asserts the hook forwards the registered client_id / client_secret to onTokenReceived; it fails on the pre-fix code (callback received only the token) and passes with the fix.

It also flips hasPreconfiguredCredentials from requiring both client_id and client_secret to requiring just client_id, so the UI skips re-registration when a client already exists (a public client has a client_id but no secret). This matches the backend reuse guard in the base PR; together they give one client per server reused across users, rather than re-minting per authorize.


Note

Medium Risk
Touches OAuth credential persistence and invalidation in the dashboard; mistakes could leak wrong clients or break refresh, but scope is UI-only with added regression tests and no backend changes.

Overview
Fixes on-create “Authorize & Fetch” so Dynamic Client Registration client_id / client_secret are not dropped before the server is created. useMcpOAuthFlow now passes the registered client into onTokenReceived, treats a preconfigured client_id alone as enough to skip re-registration (public clients), and uses a reset version so a late token exchange cannot repopulate credentials after reset().

The create MCP server form merges those values into form.credentials, records which URL/spec was authorized, and clears OAuth state (credentials, discovered endpoints, tools, flow reset) when MCP URL or OpenAPI spec_path changes after authorization so stale DCR clients are not submitted.

Tests cover hook forwarding, in-flight exchange after reset, DCR reuse vs fresh registration, and URL/spec invalidation on create.

Reviewed by Cursor Bugbot for commit d059a91. Bugbot is set up for automated code reviews on this repo. Configure here.

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes on-create "Authorize & Fetch" so that DCR client_id/client_secret minted during OAuth registration are no longer dropped before the MCP server row is created. useMcpOAuthFlow now forwards the registered client to onTokenReceived, and the create form writes those values into form.credentials so they ride the existing create-request path into the backend's encrypt_credentials store.

  • useMcpOAuthFlow gains a resetVersionRef that prevents a stale in-flight token exchange from calling onTokenReceived after reset(), and the hasPreconfiguredCredentials guard is relaxed from requiring both client_id + client_secret to just client_id, correctly handling public PKCE clients.
  • create_mcp_server.tsx introduces authorizedUrl state and clearAuthorizedOAuthState; together they invalidate stored DCR credentials (and reset the OAuth flow) when the MCP URL, OpenAPI spec path, or transport type changes after an authorization, preventing stale clients from being persisted for a different server endpoint.
  • New regression tests cover: hook forwarding of registeredClient, in-flight-exchange-after-reset guard, DCR reuse vs fresh registration, and URL/spec/transport invalidation on the create form.

Confidence Score: 5/5

UI-only change with no backend modifications; DCR credentials are forwarded through an existing encrypted-credentials path already verified in production.

The fix is narrow and well-contained: it threads DCR client data through the callback signature and records which URL was authorized, both backed by new regression tests that fail on the pre-fix code. The reset-version guard correctly prevents stale token exchanges from updating state after reset. No new data is exposed that wasn't already handled by the backend's encrypt_credentials path.

No files require special attention.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx Adds resetVersionRef for in-flight-exchange cancellation, relaxes hasPreconfiguredCredentials to client_id-only, and forwards {clientId, clientSecret} from stored flow state to onTokenReceived. Logic is correct and consistent with RFC 7591 public-client semantics.
ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx Introduces authorizedUrl state and clearAuthorizedOAuthState helper; onTokenReceived now merges DCR client credentials into form fields and records the authorized URL; URL/spec/transport changes trigger invalidation. No backend changes required.
ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.test.tsx Adds tests for: hook forwarding of DCR client to callback, in-flight exchange ignored after reset, DCR reuse with existing client_id, fresh registration without client_id. Updated assertions match corrected behavior.
ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx Adds tests for URL/spec_path/transport invalidation of authorized OAuth state on the create form; mock type signatures updated to reflect the new two-argument onTokenReceived contract.

Reviews (7): Last reviewed commit: "Merge branch 'litellm_internal_staging' ..." | Re-trigger Greptile

Comment thread ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx
@greptile-apps

This comment was marked as duplicate.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tin-berri tin-berri changed the title fix(ui): persist DCR client_id from on-create MCP OAuth Authorize & Fetch fix(mcp): persist DCR client_id from on-create MCP OAuth Authorize & Fetch Jul 2, 2026
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client branch from 9a253ae to 462abc4 Compare July 2, 2026 00:15
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client_oncreate branch from f38c536 to d682ac8 Compare July 2, 2026 00:20
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client branch from 462abc4 to d56e0d1 Compare July 2, 2026 03:40
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client_oncreate branch from d682ac8 to 3775643 Compare July 2, 2026 04:49
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client branch from d56e0d1 to 5641933 Compare July 2, 2026 17:16
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client_oncreate branch from 3775643 to 8af6771 Compare July 2, 2026 17:18
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client branch from 5641933 to 680140e Compare July 2, 2026 17:45
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client_oncreate branch from 8af6771 to 2e9e592 Compare July 2, 2026 17:46
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re-review please (rebased onto the updated base branch).

@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client branch from 680140e to f984d02 Compare July 2, 2026 18:02
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client_oncreate branch from 2e9e592 to 29dce7e Compare July 2, 2026 18:02
@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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale client_id skips registration
    • Cleared fetched OAuth credentials and reset OAuth state when the create form server URL changes, preventing stale DCR client IDs from being reused or saved for a new endpoint.

You can send follow-ups to the cloud agent here.

Comment thread ui/litellm-dashboard/src/hooks/useMcpOAuthFlow.tsx
@CLAassistant

CLAassistant commented Jul 2, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ tin-berri
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

tin-berri and others added 2 commits July 2, 2026 15:13
Interactive authorization_code MCP servers register an OAuth client via Dynamic
Client Registration (RFC 7591) during the authorize flow, but the minted
client_id and the discovered token_url were returned to the caller and never
written to the server row. The autonomous refresh_token grant reads client_id,
client_secret and token_url off the server, so an expired access token could not
be refreshed; the user was bounced back to re-authorize and tools/list returned
zero tools

Persist the DCR client_id (plus client_secret and token_endpoint_auth_method when
the registration returns them) and the discovered token_url onto the server row,
reusing the encrypt_credentials write that client_credentials and token exchange
already use, then refresh the in-memory registry so the value is live at refresh
time. Both the v1 refresher and the v2 AuthorizationCodeRefresher read those same
fields, so egress needs no change
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client branch from 84ba218 to efe3523 Compare July 2, 2026 22:14
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client_oncreate branch from c413b9f to 40830ed Compare July 2, 2026 22:16
@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai rereview

@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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: OpenAPI spec change skips invalidation
    • OAuth authorization now tracks the transport-specific target and clears credentials when an OpenAPI spec URL changes.
  • ✅ Fixed: Late OAuth repopulates cleared credentials
    • OAuth reset now invalidates in-flight exchanges so late token responses cannot repopulate cleared credentials.

You can send follow-ups to the cloud agent here.

Comment thread ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx Outdated
Comment thread ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx Outdated
…Fetch"

The interactive "Authorize & Fetch" flow on the create form registers an OAuth
client (RFC 7591) against a temporary server that has no DB row, then creates the
real server afterward. useMcpOAuthFlow captured the DCR client_id and client_secret
but passed only the token to onTokenReceived, so the create request dropped the
client identity and the created server could not refresh its access token; its row
had credentials={} and the refresh_token grant 401d at the upstream token endpoint

Forward the registered client to onTokenReceived and write client_id (and
client_secret when present) into the create form credentials, so the create request
carries them and the backend persists them through its existing encrypt_credentials
path. token_url is omitted because it is re-discovered on load (RFC 9728 then 8414);
token_endpoint_auth_method is unused because this flow only ever registers as
client_secret_post or none, never client_secret_basic
@tin-berri
tin-berri force-pushed the litellm_persist_mcp_dcr_client_oncreate branch from 40830ed to c33aeaf Compare July 2, 2026 22:41
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

Base automatically changed from litellm_persist_mcp_dcr_client to litellm_internal_staging July 3, 2026 17:42

@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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: OAuth survives transport switch
    • OAuth credentials, DCR client data, tokens, tools, and the authorization target are now cleared when transport changes after authorization.
  • ✅ Fixed: authorizedUrl survives modal close
    • The modal-close cleanup now clears the stored authorized URL along with the rest of the OAuth state.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit d059a91. Configure here.

Comment thread ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
Comment thread ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
cursoragent and others added 2 commits July 3, 2026 17:52
…client_oncreate

Resolve conflicts in discoverable_endpoints.py and its test by taking the
canonical version now on the base branch, which is #31912 squash-merged with its
post-review refinements (client_secret and token_endpoint_auth_method
persistence, the decrypt/apply helpers, and the DcrRegistrationPersistenceResult
result). This branch's copies of those backend commits predate the merge and
none of the on-create UI commits touch those files, so taking base keeps the
merged behavior with no loss.

The on-create UI change sits cleanly on top of the base's token endpoint auth
method selector work; the net PR diff is only the four dashboard files
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri mateo-berri 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.

LGTM; thanks!

@tin-berri
tin-berri merged commit 3235f4a into litellm_internal_staging Jul 3, 2026
122 checks passed
@tin-berri
tin-berri deleted the litellm_persist_mcp_dcr_client_oncreate branch July 3, 2026 19:15
yuneng-berri added a commit that referenced this pull request Jul 4, 2026
chore(release): backport #31912/#31920/#31921 (+#31923/#31929 parity, #31635 prereq) onto patch-1.91.0rc1
yuneng-berri added a commit that referenced this pull request Jul 4, 2026
chore(ui): rebuild Next.js bundle for #31921/#31920 on patch-1.91.0rc1
ap-anton-r-susilo pushed a commit to ap-anton-r-susilo/litellm that referenced this pull request Jul 6, 2026
…Fetch (BerriAI#31920)

* fix(mcp): persist DCR client_id so interactive OAuth token refresh works

Interactive authorization_code MCP servers register an OAuth client via Dynamic
Client Registration (RFC 7591) during the authorize flow, but the minted
client_id and the discovered token_url were returned to the caller and never
written to the server row. The autonomous refresh_token grant reads client_id,
client_secret and token_url off the server, so an expired access token could not
be refreshed; the user was bounced back to re-authorize and tools/list returned
zero tools

Persist the DCR client_id (plus client_secret and token_endpoint_auth_method when
the registration returns them) and the discovered token_url onto the server row,
reusing the encrypt_credentials write that client_credentials and token exchange
already use, then refresh the in-memory registry so the value is live at refresh
time. Both the v1 refresher and the v2 AuthorizationCodeRefresher read those same
fields, so egress needs no change

* fix: reuse persisted MCP DCR clients

* fix(ui): persist DCR client_id from on-create MCP OAuth "Authorize & Fetch"

The interactive "Authorize & Fetch" flow on the create form registers an OAuth
client (RFC 7591) against a temporary server that has no DB row, then creates the
real server afterward. useMcpOAuthFlow captured the DCR client_id and client_secret
but passed only the token to onTokenReceived, so the create request dropped the
client identity and the created server could not refresh its access token; its row
had credentials={} and the refresh_token grant 401d at the upstream token endpoint

Forward the registered client to onTokenReceived and write client_id (and
client_secret when present) into the create form credentials, so the create request
carries them and the backend persists them through its existing encrypt_credentials
path. token_url is omitted because it is re-discovered on load (RFC 9728 then 8414);
token_endpoint_auth_method is unused because this flow only ever registers as
client_secret_post or none, never client_secret_basic

* fix(ui): prevent stale MCP OAuth credentials

* fix(ui): reset MCP OAuth authorization state

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
(cherry picked from commit 3235f4a)
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.

4 participants