From 6c74e9b988a92da9af4de0404660929c65ff3e52 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Tue, 14 Jul 2026 21:02:15 -0700 Subject: [PATCH] feat(ui): grey out client-forwarded and OBO MCP servers on the gateway connect flow On the aggregate gateway connect flow the client holds only an identity-only session bearer, and upstream credentials are resolved server-side from the per-user vault, which is only populated by interactive authorization_code (oauth2). The client-forwarded modes (true_passthrough, oauth_delegate) need the caller to present the upstream Authorization per call, and oauth2_token_exchange (OBO) needs the caller's own IdP token as the exchange subject; the session bearer is neither, so a tool call to those servers can never complete on this connection. Rather than let them look connectable and then 401, the grid greys those servers and labels them "Not supported on this connection" when rendered in connect mode. Outside the connect flow the normal integrations page is unchanged, since the client forwards its own token there and those modes work. The classification lives in a shared isUnsupportedOnGatewayConnect helper next to isClientForwardedTokenMode so the UI gate and the auth-mode taxonomy cannot drift. --- .../src/components/chat/MCPAppsPanel.tsx | 14 +++++++++++-- .../src/components/mcp_tools/types.test.tsx | 21 +++++++++++++++++++ .../src/components/mcp_tools/types.tsx | 9 ++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx index 9a2930bb503..ac26e90dfb6 100644 --- a/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx +++ b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx @@ -13,7 +13,7 @@ import { getMCPOAuthUserCredentialStatus, listMCPTools, } from "../networking"; -import { AUTH_TYPE, MCPServer, MCPTool, handleTransport } from "../mcp_tools/types"; +import { AUTH_TYPE, MCPServer, MCPTool, handleTransport, isUnsupportedOnGatewayConnect } from "../mcp_tools/types"; import MessageManager from "@/components/molecules/message_manager"; import { useUserMcpOAuthFlow } from "@/hooks/useUserMcpOAuthFlow"; @@ -242,6 +242,13 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange, }; const renderConnectionIndicator = (server: MCPServer) => { + if (connectMode && isUnsupportedOnGatewayConnect(server.auth_type)) { + return ( + + Not supported on this connection + + ); + } if (server.auth_type === AUTH_TYPE.OAUTH2) { if (oauthConnected.has(server.server_id)) { return ; @@ -507,6 +514,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange, const color = getAvatarColor(name); const isLeftCol = idx % 2 === 0; const count = toolCounts[name]; + const unsupported = !!connectMode && isUnsupportedOnGatewayConnect(server.auth_type); return (
= ({ accessToken, selectedServers, onChange, onClick={() => setDetailServer(server)} className={`flex items-center gap-3 p-4 bg-card cursor-pointer transition-colors hover:bg-accent/30 min-w-0 ${ isLeftCol ? "border-r" : "" - } ${Math.floor(idx / 2) < Math.floor((filtered.length - 1) / 2) ? "border-b" : ""}`} + } ${Math.floor(idx / 2) < Math.floor((filtered.length - 1) / 2) ? "border-b" : ""} ${ + unsupported ? "opacity-50" : "" + }`} > {server.mcp_info?.logo_url ? ( { @@ -231,3 +232,23 @@ describe("credentialAuthClass", () => { expect(credentialAuthClass(null)).toBeNull(); }); }); + +describe("isUnsupportedOnGatewayConnect", () => { + it("flags the modes that need a caller-supplied upstream token or subject", () => { + // client-forwarded: caller presents the upstream Authorization per call + expect(isUnsupportedOnGatewayConnect(AUTH_TYPE.TRUE_PASSTHROUGH)).toBe(true); + expect(isUnsupportedOnGatewayConnect(AUTH_TYPE.OAUTH_DELEGATE)).toBe(true); + // OBO: caller's own IdP token is the exchange subject, which the session bearer is not + expect(isUnsupportedOnGatewayConnect(AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE)).toBe(true); + }); + + it("does not flag modes the gateway can serve from server-side state or interactive vaulting", () => { + // interactive authorization_code is the one mode the connect grid vaults per user + expect(isUnsupportedOnGatewayConnect(AUTH_TYPE.OAUTH2)).toBe(false); + // server-configured credentials need no per-user connect + expect(isUnsupportedOnGatewayConnect(AUTH_TYPE.API_KEY)).toBe(false); + expect(isUnsupportedOnGatewayConnect(AUTH_TYPE.NONE)).toBe(false); + expect(isUnsupportedOnGatewayConnect(null)).toBe(false); + expect(isUnsupportedOnGatewayConnect(undefined)).toBe(false); + }); +}); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index dd7ed2bfbe4..2078f6441af 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -52,6 +52,15 @@ export const AUTH_TYPE = { export const isClientForwardedTokenMode = (authType?: string | null): boolean => authType === AUTH_TYPE.TRUE_PASSTHROUGH || authType === AUTH_TYPE.OAUTH_DELEGATE; +// Auth modes that cannot be used through the gateway aggregate connect flow, where the client holds +// only an identity-only session bearer and upstream credentials are resolved server-side from the +// per-user vault. The vault is only populated by interactive authorization_code (oauth2). The +// client-forwarded modes need the caller to present the upstream Authorization per call, and +// oauth2_token_exchange (OBO) needs the caller's own IdP token as the subject to exchange; the +// session bearer is neither, so none of these can complete a tool call on this connection. +export const isUnsupportedOnGatewayConnect = (authType?: string | null): boolean => + isClientForwardedTokenMode(authType) || authType === AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE; + export const OAUTH_FLOW = { INTERACTIVE: "interactive", M2M: "m2m",