feat(ui): dcr_bridge toggle for client-forwarded MCP auth modes

This commit is contained in:
Tin Chi Lo 2026-07-10 11:47:42 -07:00
parent 34602ff627
commit 47f33bda3f
6 changed files with 378 additions and 0 deletions

View file

@ -0,0 +1,40 @@
import React from "react";
import { Form, Switch, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { isClientForwardedTokenMode } from "./types";
/**
* DCR-bridge toggle for the client-forwarded token modes (true_passthrough /
* oauth_delegate); self-gates to those two auth types and renders nothing
* otherwise. When on, OAuth-only clients like Claude Desktop can register and
* sign in through the gateway; when off, the gateway relays the upstream
* server's own OAuth metadata instead. `initialChecked` seeds the antd
* Form.Item `initialValue` (not the Switch's DOM defaultChecked): the create
* form defaults it on, the edit form seeds it from the stored value.
*/
export default function DcrBridgeToggle({
authType,
initialChecked,
}: {
authType?: string | null;
initialChecked?: boolean;
}) {
if (!isClientForwardedTokenMode(authType)) return null;
return (
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Gateway-hosted sign-in (DCR bridge)
<Tooltip title="Lets OAuth-only clients like Claude Desktop register and sign in through the gateway. Turn off to relay the upstream server's own OAuth metadata instead (for clients pre-registered with the upstream IdP).">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="dcr_bridge"
valuePropName="checked"
initialValue={initialChecked}
>
<Switch />
</Form.Item>
);
}

View file

@ -1509,3 +1509,166 @@ describe("CreateMCPServer oauth2_flow persistence", () => {
expect(payload.oauth2_flow).toBeUndefined();
});
});
describe("CreateMCPServer dcr_bridge toggle", () => {
beforeEach(() => {
vi.clearAllMocks();
oauthHook.tokenResponse = null;
oauthHook.onTokenReceived = null;
});
const createdServer = {
server_id: "new-cf-server",
server_name: "CF_Server",
alias: "CF_Server",
url: "https://example.com/mcp",
transport: "http",
auth_type: "true_passthrough",
created_at: "2024-01-01T00:00:00Z",
created_by: "user-1",
updated_at: "2024-01-01T00:00:00Z",
updated_by: "user-1",
};
const getDcrToggle = () => document.getElementById("dcr_bridge");
async function setupHttpServerForm() {
render(<CreateMCPServer {...defaultProps} />);
await selectAntOption("Transport Type", "Streamable HTTP");
await waitFor(() => {
expect(screen.getByPlaceholderText("https://your-mcp-server.com")).toBeInTheDocument();
});
await act(async () => {
fireEvent.change(getServerNameInput(), { target: { value: "CF_Server" } });
});
await act(async () => {
fireEvent.change(screen.getByPlaceholderText("https://your-mcp-server.com"), {
target: { value: "https://example.com/mcp" },
});
});
}
async function submitCreate() {
const submitButton = screen.getByRole("button", { name: "Add MCP Server" });
await act(async () => {
fireEvent.click(submitButton);
});
await waitFor(() => {
expect(networking.createMCPServer).toHaveBeenCalledTimes(1);
});
const [, payload] = vi.mocked(networking.createMCPServer).mock.calls[0];
return payload;
}
it.each([["True Passthrough (no LiteLLM auth)"], ["OAuth Delegate (client-supplied upstream token)"]])(
"renders the toggle default-checked when %s is selected",
async (optionLabel) => {
await setupHttpServerForm();
await selectAntOption("Authentication", optionLabel);
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(screen.getByText("Gateway-hosted sign-in (DCR bridge)")).toBeInTheDocument();
expect(getDcrToggle()).toHaveAttribute("aria-checked", "true");
},
);
it.each([["None"], ["API Key"], ["OAuth"]])("does not render the toggle for %s", async (optionLabel) => {
await setupHttpServerForm();
await selectAntOption("Authentication", optionLabel);
await waitFor(() => {
expect(screen.queryByText("Gateway-hosted sign-in (DCR bridge)")).not.toBeInTheDocument();
});
expect(getDcrToggle()).not.toBeInTheDocument();
});
it.each([
["true_passthrough", "True Passthrough (no LiteLLM auth)"],
["oauth_delegate", "OAuth Delegate (client-supplied upstream token)"],
])("sends dcr_bridge: true by default on create for %s", async (authType, optionLabel) => {
vi.mocked(networking.createMCPServer).mockResolvedValue({ ...createdServer, auth_type: authType });
await setupHttpServerForm();
await selectAntOption("Authentication", optionLabel);
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
const payload = await submitCreate();
expect(payload.dcr_bridge).toBe(true);
});
it("sends an explicit dcr_bridge: false when the toggle is unchecked", async () => {
vi.mocked(networking.createMCPServer).mockResolvedValue({ ...createdServer, auth_type: "oauth_delegate" });
await setupHttpServerForm();
await selectAntOption("Authentication", "OAuth Delegate (client-supplied upstream token)");
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
await act(async () => {
fireEvent.click(getDcrToggle()!);
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "false");
const payload = await submitCreate();
expect(payload.dcr_bridge).toBe(false);
});
it.each([
["none", "None"],
["api_key", "API Key"],
["oauth2", "OAuth"],
])("forces an explicit dcr_bridge: false for %s", async (authType, optionLabel) => {
vi.mocked(networking.createMCPServer).mockResolvedValue({ ...createdServer, auth_type: authType });
await setupHttpServerForm();
await selectAntOption("Authentication", optionLabel);
const payload = await submitCreate();
expect(payload.dcr_bridge).toBe(false);
});
it("forces dcr_bridge: false when the auth type is switched away after toggling", async () => {
vi.mocked(networking.createMCPServer).mockResolvedValue({ ...createdServer, auth_type: "none" });
await setupHttpServerForm();
await selectAntOption("Authentication", "True Passthrough (no LiteLLM auth)");
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
await act(async () => {
fireEvent.click(getDcrToggle()!);
});
await selectAntOption("Authentication", "None");
await waitFor(() => {
expect(getDcrToggle()).not.toBeInTheDocument();
});
const payload = await submitCreate();
expect(payload.dcr_bridge).toBe(false);
});
it("preserves the toggle value when switching between the two client-forwarded modes", async () => {
vi.mocked(networking.createMCPServer).mockResolvedValue({ ...createdServer, auth_type: "oauth_delegate" });
await setupHttpServerForm();
await selectAntOption("Authentication", "True Passthrough (no LiteLLM auth)");
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "true");
// The Form.Item is mounted in both client-forwarded modes, so switching between them keeps the
// live toggle value rather than forcing it back to the default or to false.
await selectAntOption("Authentication", "OAuth Delegate (client-supplied upstream token)");
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "true");
const payload = await submitCreate();
expect(payload.dcr_bridge).toBe(true);
});
});

View file

@ -21,6 +21,7 @@ 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";
@ -380,6 +381,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
available_on_public_internet: availableOnPublicInternetRaw,
delegate_auth_to_upstream: delegateAuthToUpstreamRaw,
oauth_passthrough: oauthPassthroughRaw,
dcr_bridge: dcrBridgeRaw,
token_validation_json: rawTokenValidationJson,
...restValues
} = values;
@ -486,6 +488,11 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
available_on_public_internet: Boolean(availableOnPublicInternetRaw),
delegate_auth_to_upstream: Boolean(delegateAuthToUpstreamRaw),
oauth_passthrough: Boolean(oauthPassthroughRaw),
// ``dcr_bridge`` is only meaningful for the client-forwarded token
// modes (true_passthrough / oauth_delegate) and defaults on when the
// toggle is shown; force false for any other auth type so a stale
// ``true`` is never persisted. Mirrors the sibling flags above.
dcr_bridge: isClientForwardedTokenMode(restValues.auth_type) ? Boolean(dcrBridgeRaw ?? true) : false,
...(restValues.auth_type === AUTH_TYPE.OAUTH2
? {
oauth2_flow:
@ -987,6 +994,8 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
<TruePassthroughWarning authType={authType} />
<DcrBridgeToggle authType={authType} initialChecked />
<PassthroughAuthorizeSection
authType={authType}
oauthFlow={{

View file

@ -1737,3 +1737,155 @@ describe("MCPServerEdit (max concurrent requests)", () => {
expect(payload.max_concurrent_requests).toBeNull();
});
});
describe("MCPServerEdit (dcr_bridge toggle)", () => {
beforeEach(() => {
vi.clearAllMocks();
mockOauth.tokenResponse = null;
});
const getDcrToggle = () => document.getElementById("dcr_bridge");
function renderEdit(server: Record<string, unknown>) {
render(
<MCPServerEdit
mcpServer={{ ...interactiveOAuthServer, ...server }}
accessToken="access-token"
onCancel={vi.fn()}
onSuccess={vi.fn()}
availableAccessGroups={[]}
/>,
);
}
async function saveAndGetPayload() {
const saveButtons = screen.getAllByRole("button", { name: "Save Changes" });
await act(async () => {
fireEvent.click(saveButtons[0]);
});
await waitFor(() => {
expect(networking.updateMCPServer).toHaveBeenCalledTimes(1);
});
const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0];
return payload;
}
it.each([["true_passthrough"], ["oauth_delegate"]])("renders the toggle for a %s server", async (authType) => {
renderEdit({ auth_type: authType });
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(screen.getByText("Gateway-hosted sign-in (DCR bridge)")).toBeInTheDocument();
});
it.each([["oauth2"], ["api_key"], ["none"]])("does not render the toggle for an %s server", async (authType) => {
renderEdit({ auth_type: authType });
await waitFor(() => {
expect(screen.getAllByRole("button", { name: "Save Changes" }).length).toBeGreaterThan(0);
});
expect(screen.queryByText("Gateway-hosted sign-in (DCR bridge)")).not.toBeInTheDocument();
expect(getDcrToggle()).not.toBeInTheDocument();
});
it("initializes unchecked from a null stored value and saves an explicit false", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,
auth_type: "true_passthrough",
});
renderEdit({ auth_type: "true_passthrough", dcr_bridge: null });
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "false");
const payload = await saveAndGetPayload();
expect(payload.dcr_bridge).toBe(false);
});
it("initializes checked from a stored true and saves an explicit true", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,
auth_type: "oauth_delegate",
dcr_bridge: true,
});
renderEdit({ auth_type: "oauth_delegate", dcr_bridge: true });
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "true");
const payload = await saveAndGetPayload();
expect(payload.dcr_bridge).toBe(true);
});
it("saves an explicit false after the admin unchecks a stored true", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,
auth_type: "true_passthrough",
dcr_bridge: false,
});
renderEdit({ auth_type: "true_passthrough", dcr_bridge: true });
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
await act(async () => {
fireEvent.click(getDcrToggle()!);
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "false");
const payload = await saveAndGetPayload();
expect(payload.dcr_bridge).toBe(false);
});
it("forces dcr_bridge: false when the auth type is switched away", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,
auth_type: "api_key",
});
renderEdit({ auth_type: "true_passthrough", dcr_bridge: true });
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
await selectAntOption("Authentication", "API Key");
await waitFor(() => {
expect(getDcrToggle()).not.toBeInTheDocument();
});
// Mirrors the sibling delegate_auth_to_upstream / oauth_passthrough force-false: a stale true is
// never left behind to silently re-activate if the mode is switched back.
const payload = await saveAndGetPayload();
expect(payload.dcr_bridge).toBe(false);
});
it("preserves the toggle value when switching between the two client-forwarded modes", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,
auth_type: "oauth_delegate",
dcr_bridge: true,
});
renderEdit({ auth_type: "true_passthrough", dcr_bridge: true });
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "true");
// The Form.Item stays mounted across the two client-forwarded modes, so the live toggle value is
// preserved rather than forced false by the switch.
await selectAntOption("Authentication", "OAuth Delegate (client-supplied upstream token)");
await waitFor(() => {
expect(getDcrToggle()).toBeInTheDocument();
});
expect(getDcrToggle()).toHaveAttribute("aria-checked", "true");
const payload = await saveAndGetPayload();
expect(payload.dcr_bridge).toBe(true);
});
});

View file

@ -23,6 +23,7 @@ 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";
@ -276,6 +277,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
env_vars: initialEnvVars,
extra_headers: mcpServer.extra_headers || [],
oauth_flow_type: oauth2FlowToFormValue(mcpServer.oauth2_flow),
dcr_bridge: Boolean(mcpServer.dcr_bridge),
token_validation_json: mcpServer.token_validation
? JSON.stringify(mcpServer.token_validation, null, 2)
: undefined,
@ -627,6 +629,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
available_on_public_internet: availableOnPublicInternetRaw,
delegate_auth_to_upstream: delegateAuthToUpstreamRaw,
oauth_passthrough: oauthPassthroughRaw,
dcr_bridge: dcrBridgeRaw,
token_validation_json: rawTokenValidationJson,
...restValues
} = values;
@ -837,6 +840,15 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
? Boolean(oauthPassthroughRaw ?? mcpServer.oauth_passthrough)
: false;
})(),
// ``dcr_bridge`` is only meaningful for the client-forwarded token
// modes (true_passthrough / oauth_delegate). The Form.Item is
// conditionally rendered so the value drops out of the form on
// auth_type change; force false for any other configuration to avoid
// persisting a stale ``true`` that would silently re-activate if the
// mode is later switched back.
dcr_bridge: isClientForwardedTokenMode(restValues.auth_type)
? Boolean(dcrBridgeRaw ?? mcpServer.dcr_bridge)
: false,
...(restValues.auth_type === AUTH_TYPE.OAUTH2 && restValues.oauth_flow_type
? {
oauth2_flow:
@ -1032,6 +1044,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
</Select>
</Form.Item>
<TruePassthroughWarning authType={authType} />
<DcrBridgeToggle authType={authType} />
<PassthroughAuthorizeSection
authType={authType}
oauthFlow={{

View file

@ -319,6 +319,7 @@ export interface MCPServer {
available_on_public_internet?: boolean;
delegate_auth_to_upstream?: boolean;
oauth_passthrough?: boolean;
dcr_bridge?: boolean | null;
max_concurrent_requests?: number | null;
/** Stdio-only fields (present when transport === 'stdio') */