Skip to content

feat(ui): standalone /connect route for MCP OAuth, decoupled from Chat UI flag - #34334

Merged
tin-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_connect_page_standalone
Jul 24, 2026
Merged

feat(ui): standalone /connect route for MCP OAuth, decoupled from Chat UI flag#34334
tin-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_connect_page_standalone

Conversation

@tin-berri

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

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Keyless SSO users cannot reach the MCP connect surface unless an admin enables Chat UI
  • After login they land on an empty API-keys dashboard with no path to connect

How it solves it:

  • Adds a standalone /ui/connect route with no enable_chat_ui gate
  • Sends keyless internal users there on login instead of the empty dashboard

Relevant issues

  • Adds /ui/connect so keyless SSO users can connect their own MCP servers over OAuth without an admin turning on Chat UI first
  • Redirects keyless internal users to it on login, so the surface is actually reachable rather than just un-gated
  • Leaves the chat playground, its in-chat Integrations tab, and the enable_chat_ui gate exactly as they are

Linear ticket

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)

Delays 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

UI change, so the proof is a manual walk-through. Run the proxy with python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver and the dashboard with npm run dev in ui/litellm-dashboard, with SSO configured and at least one OAuth-backed MCP server under Tools -> MCP Servers

  1. Confirm Chat UI is off at http://localhost:4000/ui/?page=admin-panel -> UI Settings ("Enable Chat UI" unchecked)
  2. Visit http://localhost:4000/ui/chat and confirm you are still bounced to the dashboard (unchanged behavior)
  3. Log in via SSO as an internal user that has no keys; expect to land on /ui/connect showing the MCP servers panel, not the empty API-keys dashboard
  4. Click Connect on an OAuth server, finish the provider consent screen, and confirm you return to /ui/connect with the server connected and no mcpOauthReturn left in the address bar
  5. Navigate back to http://localhost:4000/ui and confirm you can still reach the dashboard (the redirect only fires on the post-login landing)
  6. Log in as a proxy admin, as an org admin, and separately as an internal user who does have a key; all three should land on the dashboard as before

Please capture screenshots of steps 3, 4, and 5

Type

🆕 New Feature

Changes

  • New app/connect/ route group: an auth-only layout that never reads enable_chat_ui and does not mount the chat shell, rendering the existing MCPAppsPanel
  • The dashboard landing redirects to /ui/connect when the URL carries ?login=success, the user holds an internal-user role, and their key list comes back empty
  • useKeys takes an optional enabled flag so that key lookup only runs on the post-login landing rather than on every dashboard visit
  • No backend changes: the MCP OAuth flow already captures its return URL from window.location.href at flow start and the shared callback replays it, so a flow begun at /ui/connect returns there

What does not change: the chat playground, the in-chat Integrations tab, and the enable_chat_ui gate are untouched, and admins and users who already have keys land on the dashboard exactly as before

Things a reviewer will ask about

The redirect lives in the dashboard landing rather than the SSO callback. Putting it in ui_sso.py would add a database round trip to the authentication path and would have to reason about the cross-origin return_to branch; the landing already knows the user's role and keys. Because ?login=success is also set by the username and password login flows, the role gate is what keeps this scoped to the users the flow targets. It gates positively on internalUserRoles rather than negatively on isAdminRole, because all_admin_roles mixes raw and formatted role strings (it holds raw org_admin but not the "Org Admin" that formatUserRole emits, which is the form AuthContext stores), so a negative check would read a keyless org admin as a non-admin and redirect them. internalUserRoles carries both representations, so any role that is not unambiguously an internal user is simply left on the dashboard. Completing the shared admin list instead would change org-admin access across every isAdminRole caller, which is a roles-policy decision of its own

It fires only on the post-login landing, never on an ordinary dashboard visit, so a keyless user can still navigate to the dashboard afterwards instead of being trapped on the connect page. An explicit stored return URL also outranks it, so a deep link that survived login is not swallowed

If the key lookup fails, no redirect happens and the dashboard renders, so a transient error cannot strand anyone on the connect page

AuthContext hydrates in two phases: one effect sets the token and clears authLoading, then a token-keyed effect derives userRole, so there is a render where the user is signed in but the role is still empty. On the landing that interim state holds the loading screen rather than painting the dashboard, so the positive role check cannot flash the api-keys view before the role arrives. Every login=success token carries a required user_role claim, so the role always hydrates and the hold cannot become permanent, and the hold is scoped to the landing so ordinary dashboard visits are untouched

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

Low Risk
Frontend-only routing and conditional redirects with role/key guards; no auth or backend changes.

Overview
Adds a standalone /connect route that renders MCPAppsPanel behind an auth-only layout (no enable_chat_ui gate), so keyless SSO users can complete MCP OAuth without Chat UI enabled.

On the dashboard post-login landing (?login=success), internal users with no API keys are redirected to connect instead of an empty API-keys view. Admins, users who already have keys, and non-landing visits are unchanged. useKeys gains an optional enabled flag so the key lookup runs only on that landing path.

The landing shows a loading screen while role hydrates or keys load, and defers to an explicit stored return URL over the connect redirect. The connect page strips mcpOauthReturn from the URL after OAuth. Unit tests cover redirect rules, layout auth gating, and connect page behavior.

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

The MCP connect surface only existed as the Integrations tab inside the
enable_chat_ui-gated /chat shell, so a keyless SSO user was bounced to the
dashboard and could never reach it unless an admin enabled Chat UI first.

Add a sibling /connect route with its own thin, auth-only layout that renders
the same MCPAppsPanel without the chat-ui gate or chat shell. The user OAuth
flow already returns to whatever URL started it, so no backend changes are
needed. The chat playground and its gate are left unchanged.
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a standalone /ui/connect route that renders MCPAppsPanel behind an auth-only layout with no enable_chat_ui gate, so keyless SSO users can complete MCP OAuth without an admin enabling Chat UI. On the post-login dashboard landing, keyless internal users are redirected there instead of seeing an empty API-keys view.

  • /connect route (layout.tsx + page.tsx): new auth-only shell using useAuthorized (which handles redirect-to-login); strips the mcpOauthReturn OAuth-return param from the URL after consent completes.
  • Dashboard landing redirect (page.tsx): fires only when ?login=success is present, the role has hydrated to an internal-user value, and the key lookup returns an empty list; yields to any stored return-URL and is a no-op on ordinary non-login visits.
  • useKeys opt-in enabled flag: skips the key-list network request on every non-landing dashboard visit, keeping the existing behavior unchanged for all other consumers.

Confidence Score: 5/5

Frontend-only change with no backend modifications; all redirect paths are guarded and fall back gracefully on error or missing state.

The redirect logic correctly handles every relevant edge case — unhydrated role, key-fetch error, stored return URL precedence, non-landing visits, and admin/org-admin roles — each verified by a targeted unit test. The useAuthorized hook's existing redirect-to-login behavior covers the unauthenticated case for the new layout, and the enable_chat_ui flag is intentionally and correctly absent from the new route.

No files require special attention.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts Adds optional enabled parameter (default true) to the useKeys hook, ANDed into the React Query enabled flag. Straightforward, backward-compatible, and safe.
ui/litellm-dashboard/src/app/(dashboard)/page.tsx Dashboard landing gains a post-login redirect for keyless internal users. Logic correctly guards behind isPostLoginLanding, hydrated role check (isAwaitingRole), key-fetch result, and didReturnRedirectRef to yield to any existing stored return-URL redirect. Edge cases (key-fetch error, unhydrated role, non-landing visits) are all handled.
ui/litellm-dashboard/src/app/connect/layout.tsx New auth-only layout for /connect using useAuthorized (which already handles redirect-to-login). Returns null while loading or unauthorized — the useAuthorized effect handles the redirect, so no flash of protected content occurs. No enable_chat_ui gate, as intended.
ui/litellm-dashboard/src/app/connect/page.tsx New standalone connect page rendering MCPAppsPanel. Correctly strips mcpOauthReturn from the URL after OAuth return using URL + router.replace, and wraps the search-params consumer in Suspense as required by Next.js.
ui/litellm-dashboard/src/app/(dashboard)/page.test.tsx Comprehensive unit tests for the redirect logic: covers keyless internal user (both role variants), admin/org-admin landing-on-dashboard, user-with-key, non-login visit, unhydrated-role hold, keysLoading hold, and return-URL precedence. All assertions are valid and align with the production logic.
ui/litellm-dashboard/src/app/connect/layout.test.tsx Tests authorized render, unauthorized no-render, and loading no-render for ConnectLayout. Correct and sufficient for the layout's narrow responsibility.
ui/litellm-dashboard/src/app/connect/page.test.tsx Tests panel render with access token, OAuth-return URL stripping, and no-op when param absent. Correct coverage for the page's two responsibilities.

Reviews (3): Last reviewed commit: "fix(ui): hold the landing until the role..." | Re-trigger Greptile

A standalone /ui/connect route is only reachable if something points a user
at it. Post-login the dashboard always rendered the API-keys view, so a
keyless SSO user saw an empty dashboard and no path to connect.

Redirect to /ui/connect from the dashboard landing when the URL carries
?login=success, the user is not an admin, and their key list is empty.
Gating on the post-login marker keeps the dashboard reachable afterwards,
and an explicit stored return URL still wins. useKeys takes an optional
enabled flag so the lookup only runs on that landing.
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_connect_page_standalone (531854d) with litellm_internal_staging (1b2a7ce)

Open in CodSpeed

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread ui/litellm-dashboard/src/app/(dashboard)/page.tsx
isAdminRole compares against a list that mixes raw and formatted role
strings: it holds raw org_admin but not the "Org Admin" that
formatUserRole produces, and AuthContext stores the formatted form. A
keyless org admin therefore read as a non-admin and was redirected to
the connect page.

Gate positively on internalUserRoles instead, which carries both
representations, so the redirect targets the persona it is meant for and
any role that is not unambiguously an internal user is left on the
dashboard. The shared admin list is left alone: completing it would
change org-admin access across every isAdminRole caller, which is a
roles-policy decision of its own.
@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai rereview

Comment thread ui/litellm-dashboard/src/app/(dashboard)/page.tsx
… redirect

AuthContext sets token and clears authLoading in one effect, then a
second token-keyed effect populates userRole, so there is a render where
the user is signed in but userRole is still the initial empty string. The
positive internalUserRoles check reads that interim role as non-internal,
which let the api-keys dashboard paint for a frame before the role
arrived and the keyless redirect ran.

Treat "signed in on the post-login landing with an unhydrated role" as a
resolving state that holds the loading screen, so the dashboard never
flashes. Every login=success token carries a required user_role claim, so
the role always hydrates within a tick and this cannot hang; it is scoped
to the landing, so ordinary dashboard visits are unaffected.
@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 531854d. Configure here.

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai rereview

@tin-berri
tin-berri merged commit c742a90 into litellm_internal_staging Jul 24, 2026
80 of 81 checks passed
@tin-berri
tin-berri deleted the litellm_connect_page_standalone branch July 24, 2026 17:27
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.

2 participants