feat(ui): standalone /connect route for MCP OAuth, decoupled from Chat UI flag - #34334
Conversation
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 SummaryThis PR adds a standalone
Confidence Score: 5/5Frontend-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 No files require special attention.
|
| 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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
bugbot run |
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.
|
bugbot run |
|
@greptileai rereview |
… 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.
|
bugbot run |
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 531854d. Configure here.
|
@greptileai rereview |
TLDR
Problem this solves:
How it solves it:
/ui/connectroute with noenable_chat_uigateRelevant issues
/ui/connectso keyless SSO users can connect their own MCP servers over OAuth without an admin turning on Chat UI firstenable_chat_uigate exactly as they areLinear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito 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_resolverand the dashboard withnpm run devinui/litellm-dashboard, with SSO configured and at least one OAuth-backed MCP server under Tools -> MCP Servers/ui/connectshowing the MCP servers panel, not the empty API-keys dashboard/ui/connectwith the server connected and nomcpOauthReturnleft in the address barPlease capture screenshots of steps 3, 4, and 5
Type
🆕 New Feature
Changes
app/connect/route group: an auth-only layout that never readsenable_chat_uiand does not mount the chat shell, rendering the existingMCPAppsPanel/ui/connectwhen the URL carries?login=success, the user holds an internal-user role, and their key list comes back emptyuseKeystakes an optionalenabledflag so that key lookup only runs on the post-login landing rather than on every dashboard visitwindow.location.hrefat flow start and the shared callback replays it, so a flow begun at/ui/connectreturns thereWhat does not change: the chat playground, the in-chat Integrations tab, and the
enable_chat_uigate are untouched, and admins and users who already have keys land on the dashboard exactly as beforeThings a reviewer will ask about
The redirect lives in the dashboard landing rather than the SSO callback. Putting it in
ui_sso.pywould add a database round trip to the authentication path and would have to reason about the cross-originreturn_tobranch; the landing already knows the user's role and keys. Because?login=successis 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 oninternalUserRolesrather than negatively onisAdminRole, becauseall_admin_rolesmixes raw and formatted role strings (it holds raworg_adminbut not the"Org Admin"thatformatUserRoleemits, which is the formAuthContextstores), so a negative check would read a keyless org admin as a non-admin and redirect them.internalUserRolescarries 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 everyisAdminRolecaller, which is a roles-policy decision of its ownIt 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
AuthContexthydrates in two phases: one effect sets the token and clearsauthLoading, then a token-keyed effect derivesuserRole, 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. Everylogin=successtoken carries a requireduser_roleclaim, so the role always hydrates and the hold cannot become permanent, and the hold is scoped to the landing so ordinary dashboard visits are untouchedFinal Attestation
Note
Low Risk
Frontend-only routing and conditional redirects with role/key guards; no auth or backend changes.
Overview
Adds a standalone
/connectroute that rendersMCPAppsPanelbehind an auth-only layout (noenable_chat_uigate), 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.useKeysgains an optionalenabledflag 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
mcpOauthReturnfrom 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.