feat(ui): move the dcr_bridge toggle next to the OAuth app fields

Render DcrBridgeToggle inside PassthroughAuthorizeSection, after the OAuth
client ID/secret fields and just before the Authorize & Fetch Tools button,
in both the create and edit flows. Also update the section copy to say a
configured OAuth app is saved with the server, using the same wording as the
credential lifecycle rework in #32752 so whichever PR lands second rebases
cleanly
This commit is contained in:
Tin Chi Lo 2026-07-10 15:47:05 -07:00
parent 47f33bda3f
commit 2b4c054403
5 changed files with 50 additions and 20 deletions

View file

@ -1,5 +1,6 @@
import React from "react";
import { Button, Form, Input } from "antd";
import DcrBridgeToggle from "./DcrBridgeToggle";
import { isClientForwardedTokenMode } from "./types";
interface PassthroughOAuthFlow {
@ -11,20 +12,23 @@ interface PassthroughOAuthFlow {
/**
* Browser-only Authorize & Fetch for the client-forwarded token modes
* (true_passthrough / oauth_delegate). LiteLLM never stores upstream
* credentials for these modes, so the token obtained here lives in this
* browser session only: it is forwarded per-server for the tools preview and
* allowlist configuration, and is never written to the server row or the
* per-user credential store. The optional client credentials cover IdPs
* without dynamic client registration (e.g. a pre-registered Slack app) and
* ride the temporary authorize session only.
* (true_passthrough / oauth_delegate). Tokens are never stored: the token
* obtained here lives in this browser session only, forwarded per-server for
* the tools preview and allowlist configuration, and is never written to the
* server row or the per-user credential store. The optional OAuth client
* credentials cover IdPs without dynamic client registration (e.g. a
* pre-registered Slack app); unlike the token they ARE saved onto the server
* as declared config, so internal users' Authorize relays through the org's
* app instead of dead-ending on upstreams that cannot mint clients.
*/
export default function PassthroughAuthorizeSection({
authType,
oauthFlow,
dcrBridgeInitialChecked,
}: {
authType?: string | null;
oauthFlow: PassthroughOAuthFlow;
dcrBridgeInitialChecked?: boolean;
}) {
if (!isClientForwardedTokenMode(authType)) return null;
const authorizeButtonLabels: Record<string, string> = {
@ -35,14 +39,15 @@ export default function PassthroughAuthorizeSection({
return (
<div className="rounded-lg border border-dashed border-gray-300 p-4 space-y-2 mb-4">
<p className="text-sm text-gray-600">
Callers bring their own upstream token for this auth type, so LiteLLM stores no upstream credentials. To preview
tools and configure the tool allowlist, authorize against the upstream here: the token stays in this browser
session only and is never saved to LiteLLM.
Callers bring their own upstream token for this auth type, so LiteLLM never stores tokens. To preview tools and
configure the tool allowlist, authorize against the upstream here: the token stays in this browser session only
and is never saved to LiteLLM. An OAuth app configured below IS saved with the server, so internal users who
authorize from the Tools page go through it.
</p>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">OAuth Client ID (optional, not saved)</span>}
label={<span className="text-sm font-medium text-gray-700">OAuth Client ID (optional, saved)</span>}
name={["credentials", "client_id"]}
extra="Only needed when the upstream does not support dynamic client registration (e.g. a pre-registered Slack app). Used for this browser authorization only."
extra="Set this to make everyone authorize through a specific app; required for upstreams without dynamic client registration (e.g. a pre-registered Slack app)."
>
<Input.Password
placeholder="Leave blank to use dynamic client registration"
@ -50,7 +55,7 @@ export default function PassthroughAuthorizeSection({
/>
</Form.Item>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">OAuth Client Secret (optional, not saved)</span>}
label={<span className="text-sm font-medium text-gray-700">OAuth Client Secret (optional, saved)</span>}
name={["credentials", "client_secret"]}
>
<Input.Password
@ -58,6 +63,7 @@ export default function PassthroughAuthorizeSection({
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<DcrBridgeToggle authType={authType} initialChecked={dcrBridgeInitialChecked} />
<Button
onClick={oauthFlow.startOAuthFlow}
disabled={oauthFlow.status === "authorizing" || oauthFlow.status === "exchanging"}

View file

@ -202,8 +202,8 @@ describe("CreateMCPServer", () => {
await waitFor(() => {
expect(screen.getByRole("button", { name: "Authorize & Fetch Tools (browser-only)" })).toBeInTheDocument();
});
expect(screen.getByText("OAuth Client ID (optional, not saved)")).toBeInTheDocument();
expect(screen.getByText("OAuth Client Secret (optional, not saved)")).toBeInTheDocument();
expect(screen.getByText("OAuth Client ID (optional, saved)")).toBeInTheDocument();
expect(screen.getByText("OAuth Client Secret (optional, saved)")).toBeInTheDocument();
},
);
@ -1586,6 +1586,21 @@ describe("CreateMCPServer dcr_bridge toggle", () => {
expect(getDcrToggle()).not.toBeInTheDocument();
});
it("renders the toggle between the OAuth client fields and the Authorize button", async () => {
await setupHttpServerForm();
await selectAntOption("Authentication", "True Passthrough (no LiteLLM auth)");
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
const toggle = getDcrToggle() as HTMLElement;
const secretInput = screen.getByPlaceholderText("Leave blank for public clients / PKCE");
const authorizeButton = screen.getByRole("button", { name: "Authorize & Fetch Tools (browser-only)" });
expect(secretInput.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(toggle.compareDocumentPosition(authorizeButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it.each([
["true_passthrough", "True Passthrough (no LiteLLM auth)"],
["oauth_delegate", "OAuth Delegate (client-supplied upstream token)"],

View file

@ -21,7 +21,6 @@ import {
} from "./types";
import OAuthFormFields from "./OAuthFormFields";
import TruePassthroughWarning from "./TruePassthroughWarning";
import DcrBridgeToggle from "./DcrBridgeToggle";
import PassthroughAuthorizeSection from "./PassthroughAuthorizeSection";
import TokenExchangeFormFields from "./TokenExchangeFormFields";
import MCPServerCostConfig from "./mcp_server_cost_config";
@ -994,10 +993,9 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
<TruePassthroughWarning authType={authType} />
<DcrBridgeToggle authType={authType} initialChecked />
<PassthroughAuthorizeSection
authType={authType}
dcrBridgeInitialChecked
oauthFlow={{
startOAuthFlow,
status: oauthStatus,

View file

@ -1789,6 +1789,19 @@ describe("MCPServerEdit (dcr_bridge toggle)", () => {
expect(getDcrToggle()).not.toBeInTheDocument();
});
it("renders the toggle between the OAuth client fields and the Authorize button", async () => {
renderEdit({ auth_type: "true_passthrough" });
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
const toggle = getDcrToggle() as HTMLElement;
const secretInput = screen.getByPlaceholderText("Leave blank for public clients / PKCE");
const authorizeButton = screen.getByRole("button", { name: "Authorize & Fetch Tools (browser-only)" });
expect(secretInput.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(toggle.compareDocumentPosition(authorizeButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it("initializes unchecked from a null stored value and saves an explicit false", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,

View file

@ -23,7 +23,6 @@ import { buildMcpPassthroughAuthHeader } from "@/utils/mcpHeaderUtils";
import MCPServerCostConfig from "./mcp_server_cost_config";
import MCPPermissionManagement from "./MCPPermissionManagement";
import TruePassthroughWarning from "./TruePassthroughWarning";
import DcrBridgeToggle from "./DcrBridgeToggle";
import PassthroughAuthorizeSection from "./PassthroughAuthorizeSection";
import MCPToolConfiguration from "./mcp_tool_configuration";
import StdioConfiguration from "./StdioConfiguration";
@ -1044,7 +1043,6 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
</Select>
</Form.Item>
<TruePassthroughWarning authType={authType} />
<DcrBridgeToggle authType={authType} />
<PassthroughAuthorizeSection
authType={authType}
oauthFlow={{