mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
feat(ui/mcp): surface BYOK toggle for static auth types in create/edit forms
Gate is_byok on api_key/bearer_token/token/basic instead of OpenAPI transport, in both forms, so BYOK no longer requires an API or direct DB change.
This commit is contained in:
parent
a6b7dcc7d6
commit
5a7f9157fb
5 changed files with 342 additions and 96 deletions
89
ui/litellm-dashboard/src/components/mcp_tools/ByokFields.tsx
Normal file
89
ui/litellm-dashboard/src/components/mcp_tools/ByokFields.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import React from "react";
|
||||
import { Form, Select, Input, Switch, Tooltip } from "antd";
|
||||
import type { FormInstance } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { AUTH_TYPE } from "./types";
|
||||
|
||||
export const BYOK_AUTH_FORMAT_HINT: Record<string, string> = {
|
||||
[AUTH_TYPE.BEARER_TOKEN]: "Authorization: Bearer {key}",
|
||||
[AUTH_TYPE.TOKEN]: "Authorization: token {key}",
|
||||
[AUTH_TYPE.API_KEY]: "x-api-key: {key}",
|
||||
[AUTH_TYPE.BASIC]: "Authorization: Basic {key}",
|
||||
};
|
||||
|
||||
interface ByokFieldsProps {
|
||||
form: FormInstance;
|
||||
}
|
||||
|
||||
const ByokFields: React.FC<ByokFieldsProps> = ({ form }) => {
|
||||
const isByok = Form.useWatch("is_byok", form);
|
||||
const authType = Form.useWatch("auth_type", form) as string | undefined;
|
||||
const formatHint = authType ? BYOK_AUTH_FORMAT_HINT[authType] : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
BYOK (Bring Your Own Key)
|
||||
<Tooltip title="When enabled, each user supplies their own API key for this server instead of a single shared key. Keys are stored per-user and never shared.">
|
||||
<InfoCircleOutlined className="text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="is_byok"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{isByok && (
|
||||
<>
|
||||
{formatHint && (
|
||||
<div className="mb-4 p-3 bg-blue-50 rounded-lg text-sm text-blue-700 flex items-start gap-2">
|
||||
<InfoCircleOutlined className="mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
User keys will be sent as: <code className="font-mono bg-blue-100 px-1 rounded">{formatHint}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Access Description
|
||||
<Tooltip title="List of permissions shown to users in the connection modal (e.g. 'Create and manage Jira issues')">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="byok_description"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Add access description items (press Enter after each)"
|
||||
className="w-full"
|
||||
tokenSeparators={[","]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
API Key Help URL
|
||||
<Tooltip title="Optional link shown to users to help them find their API key">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="byok_api_key_help_url"
|
||||
>
|
||||
<Input placeholder="https://docs.example.com/api-keys" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ByokFields;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as networking from "../networking";
|
||||
|
|
@ -773,3 +773,127 @@ describe("CreateMCPServer", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
const BYOK_LABEL = "BYOK (Bring Your Own Key)";
|
||||
|
||||
function getByokSwitch(): HTMLButtonElement | null {
|
||||
const label = screen.queryByText(BYOK_LABEL);
|
||||
const formItem = label?.closest(".ant-form-item");
|
||||
return (formItem?.querySelector('button[role="switch"]') as HTMLButtonElement) ?? null;
|
||||
}
|
||||
|
||||
describe("CreateMCPServer BYOK toggle", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
oauthHook.tokenResponse = null;
|
||||
oauthHook.onTokenReceived = null;
|
||||
});
|
||||
|
||||
async function selectHttpTransport() {
|
||||
render(<CreateMCPServer {...defaultProps} />);
|
||||
await selectAntOption("Transport Type", "Streamable HTTP");
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText("https://your-mcp-server.com")).toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
|
||||
it("shows the BYOK toggle for each static-credential auth type", async () => {
|
||||
for (const authLabel of ["API Key", "Bearer Token", "Token", "Basic Auth"]) {
|
||||
await selectHttpTransport();
|
||||
await selectAntOption("Authentication", authLabel);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(BYOK_LABEL)).toBeInTheDocument();
|
||||
});
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("hides the BYOK toggle for non-static auth types (none, oauth, sigv4)", async () => {
|
||||
for (const authLabel of ["None", "OAuth", "AWS SigV4 (Bedrock AgentCore MCPs)"]) {
|
||||
await selectHttpTransport();
|
||||
await selectAntOption("Authentication", authLabel);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(BYOK_LABEL)).not.toBeInTheDocument();
|
||||
});
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("swaps the shared Authentication Value field for per-user fields when BYOK is on", async () => {
|
||||
await selectHttpTransport();
|
||||
await selectAntOption("Authentication", "Bearer Token");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Authentication Value")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const byokSwitch = getByokSwitch();
|
||||
expect(byokSwitch).toBeTruthy();
|
||||
await act(async () => {
|
||||
fireEvent.click(byokSwitch!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Authentication Value")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Access Description")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("sends is_byok=true and omits the shared credential when BYOK is enabled", async () => {
|
||||
await selectHttpTransport();
|
||||
|
||||
const user = userEvent.setup({ delay: null });
|
||||
await user.type(getServerNameInput(), "Byok_Server");
|
||||
await user.type(screen.getByPlaceholderText("https://your-mcp-server.com"), "https://example.com/mcp");
|
||||
|
||||
await selectAntOption("Authentication", "Bearer Token");
|
||||
await waitFor(() => expect(screen.getByText(BYOK_LABEL)).toBeInTheDocument());
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(getByokSwitch()!);
|
||||
});
|
||||
await waitFor(() => expect(screen.queryByText("Authentication Value")).not.toBeInTheDocument());
|
||||
|
||||
vi.mocked(networking.createMCPServer).mockResolvedValue({ server_id: "byok-1" } as any);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add MCP Server" }));
|
||||
});
|
||||
|
||||
await waitFor(() => expect(networking.createMCPServer).toHaveBeenCalledTimes(1));
|
||||
|
||||
const [, payload] = vi.mocked(networking.createMCPServer).mock.calls[0];
|
||||
expect(payload.is_byok).toBe(true);
|
||||
expect(payload.auth_type).toBe("bearer_token");
|
||||
expect(payload.credentials).toBeUndefined();
|
||||
});
|
||||
|
||||
it("forces is_byok=false when auth_type is switched away from a static type after enabling it", async () => {
|
||||
await selectHttpTransport();
|
||||
|
||||
const user = userEvent.setup({ delay: null });
|
||||
await user.type(getServerNameInput(), "Switched_Server");
|
||||
await user.type(screen.getByPlaceholderText("https://your-mcp-server.com"), "https://example.com/mcp");
|
||||
|
||||
await selectAntOption("Authentication", "Bearer Token");
|
||||
await waitFor(() => expect(screen.getByText(BYOK_LABEL)).toBeInTheDocument());
|
||||
await act(async () => {
|
||||
fireEvent.click(getByokSwitch()!);
|
||||
});
|
||||
|
||||
// Switching to OAuth removes BYOK eligibility; a stale is_byok=true must not persist.
|
||||
await selectAntOption("Authentication", "None");
|
||||
await waitFor(() => expect(screen.queryByText(BYOK_LABEL)).not.toBeInTheDocument());
|
||||
|
||||
vi.mocked(networking.createMCPServer).mockResolvedValue({ server_id: "switched-1" } as any);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add MCP Server" }));
|
||||
});
|
||||
|
||||
await waitFor(() => expect(networking.createMCPServer).toHaveBeenCalledTimes(1));
|
||||
|
||||
const [, payload] = vi.mocked(networking.createMCPServer).mock.calls[0];
|
||||
expect(payload.is_byok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { Modal, Tooltip, Form, Select, Input, Switch, Collapse } from "antd";
|
||||
import { Modal, Tooltip, Form, Select, Input, Collapse } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
import { createMCPServer, registerMCPServer, storeMCPOAuthUserCredential } from "../networking";
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
MCP_OAUTH2_FLOW_M2M,
|
||||
} from "./types";
|
||||
import OAuthFormFields from "./OAuthFormFields";
|
||||
import ByokFields from "./ByokFields";
|
||||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
import MCPConnectionStatus from "./mcp_connection_status";
|
||||
import MCPToolConfiguration from "./mcp_tool_configuration";
|
||||
|
|
@ -100,6 +101,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
|
||||
const authType = formValues.auth_type as string | undefined;
|
||||
const shouldShowAuthValueField = authType ? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType) : false;
|
||||
const isByok = shouldShowAuthValueField && Boolean(formValues.is_byok);
|
||||
const isOAuthAuthType = authType === AUTH_TYPE.OAUTH2;
|
||||
const isAwsSigV4AuthType = authType === AUTH_TYPE.AWS_SIGV4;
|
||||
const isM2MFlow = isOAuthAuthType && formValues.oauth_flow_type === OAUTH_FLOW.M2M;
|
||||
|
|
@ -382,6 +384,14 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
restValues.transport = "http";
|
||||
}
|
||||
|
||||
// BYOK is only valid for static-credential auth types; drop a stale ``true``
|
||||
// (and its metadata) left behind by switching auth_type after toggling it on.
|
||||
const isByokEnabled =
|
||||
AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(restValues.auth_type) && Boolean(restValues.is_byok);
|
||||
restValues.is_byok = isByokEnabled;
|
||||
restValues.byok_description = isByokEnabled ? restValues.byok_description || [] : [];
|
||||
restValues.byok_api_key_help_url = isByokEnabled ? restValues.byok_api_key_help_url || null : null;
|
||||
|
||||
// Parse token_validation JSON if provided
|
||||
let tokenValidation: Record<string, any> | null = null;
|
||||
if (rawTokenValidationJson && rawTokenValidationJson.trim() !== "") {
|
||||
|
|
@ -422,7 +432,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
};
|
||||
|
||||
const includeCredentials =
|
||||
restValues.auth_type && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(restValues.auth_type);
|
||||
!isByokEnabled && restValues.auth_type && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(restValues.auth_type);
|
||||
|
||||
if (includeCredentials && credentialsPayload && Object.keys(credentialsPayload).length > 0) {
|
||||
payload.credentials = credentialsPayload;
|
||||
|
|
@ -742,96 +752,6 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* BYOK toggle - only for OpenAPI */}
|
||||
{transportType === TRANSPORT.OPENAPI && (
|
||||
<>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
BYOK (Bring Your Own Key)
|
||||
<Tooltip title="When enabled, each user provides their own API key for this service. Keys are stored per-user and never shared.">
|
||||
<InfoCircleOutlined className="text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="is_byok"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prev, cur) => prev.is_byok !== cur.is_byok || prev.auth_type !== cur.auth_type}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue("is_byok") ? (
|
||||
<>
|
||||
{/* Auth format hint */}
|
||||
{getFieldValue("auth_type") && getFieldValue("auth_type") !== "none" && (
|
||||
<div className="mb-4 p-3 bg-blue-50 rounded-lg text-sm text-blue-700 flex items-start gap-2">
|
||||
<InfoCircleOutlined className="mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
User keys will be sent as:{" "}
|
||||
<code className="font-mono bg-blue-100 px-1 rounded">
|
||||
{getFieldValue("auth_type") === "bearer_token" && "Authorization: Bearer {key}"}
|
||||
{getFieldValue("auth_type") === "token" && "Authorization: token {key}"}
|
||||
{getFieldValue("auth_type") === "api_key" && "x-api-key: {key}"}
|
||||
{getFieldValue("auth_type") === "basic" && "Authorization: Basic {key}"}
|
||||
{getFieldValue("auth_type") === "authorization" && "Authorization: {key}"}
|
||||
</code>
|
||||
{!getFieldValue("auth_type") && "Set Authentication Type below to specify the format."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!getFieldValue("auth_type") && (
|
||||
<div className="mb-4 p-3 bg-yellow-50 rounded-lg text-sm text-yellow-700 flex items-start gap-2">
|
||||
<InfoCircleOutlined className="mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
Set the <strong>Authentication Type</strong> below to specify how user keys are sent
|
||||
(e.g., Bearer Token, API Key header).
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Access Description
|
||||
<Tooltip title="List of permissions shown to users in the connection modal (e.g. 'Create and manage Jira issues')">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="byok_description"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Add access description items (press Enter after each)"
|
||||
className="w-full"
|
||||
tokenSeparators={[","]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
API Key Help URL
|
||||
<Tooltip title="Optional link shown to users to help them find their API key">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="byok_api_key_help_url"
|
||||
>
|
||||
<Input placeholder="https://docs.example.com/api-keys" />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Authentication - show for HTTP, SSE, and OpenAPI */}
|
||||
{transportType !== "stdio" && transportType !== "" && (
|
||||
<Collapse
|
||||
|
|
@ -855,7 +775,9 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{shouldShowAuthValueField && (
|
||||
{shouldShowAuthValueField && <ByokFields form={form} />}
|
||||
|
||||
{shouldShowAuthValueField && !isByok && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
|
|
|
|||
|
|
@ -940,3 +940,101 @@ describe("MCPServerEdit (OAuth token persistence on save)", () => {
|
|||
expect(mockSetToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MCPServerEdit (BYOK)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const byokServer = {
|
||||
server_id: "byok_server_1",
|
||||
server_name: "ByokServer",
|
||||
alias: "byok_server",
|
||||
description: "BYOK MCP server",
|
||||
transport: "http",
|
||||
url: "https://example.com/mcp",
|
||||
auth_type: "bearer_token",
|
||||
is_byok: true,
|
||||
byok_description: ["Create and manage issues"],
|
||||
byok_api_key_help_url: "https://docs.example.com/keys",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
created_by: "user-1",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
updated_by: "user-1",
|
||||
mcp_access_groups: [],
|
||||
};
|
||||
|
||||
const getByokSwitch = () => {
|
||||
const formItem = screen.queryByText("BYOK (Bring Your Own Key)")?.closest(".ant-form-item");
|
||||
return (formItem?.querySelector('button[role="switch"]') as HTMLButtonElement) ?? null;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
const saveButtons = screen.getAllByRole("button", { name: "Save Changes" });
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButtons[0]);
|
||||
});
|
||||
await waitFor(() => expect(networking.updateMCPServer).toHaveBeenCalledTimes(1));
|
||||
return vi.mocked(networking.updateMCPServer).mock.calls[0][1];
|
||||
};
|
||||
|
||||
it("renders the BYOK toggle on for an existing BYOK server and hides the shared auth value field", async () => {
|
||||
render(
|
||||
<MCPServerEdit
|
||||
mcpServer={byokServer}
|
||||
accessToken={null}
|
||||
onCancel={vi.fn()}
|
||||
onSuccess={vi.fn()}
|
||||
availableAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByokSwitch()).toBeTruthy());
|
||||
expect(getByokSwitch()).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.queryByText("Authentication Value")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("persists is_byok=true when saving an existing BYOK server", async () => {
|
||||
vi.mocked(networking.updateMCPServer).mockResolvedValue(byokServer as any);
|
||||
|
||||
render(
|
||||
<MCPServerEdit
|
||||
mcpServer={byokServer}
|
||||
accessToken="access-token"
|
||||
onCancel={vi.fn()}
|
||||
onSuccess={vi.fn()}
|
||||
availableAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByokSwitch()).toBeTruthy());
|
||||
|
||||
const payload = await save();
|
||||
expect(payload.is_byok).toBe(true);
|
||||
expect(payload.credentials).toBeUndefined();
|
||||
});
|
||||
|
||||
it("persists is_byok=false and restores the shared auth value field when BYOK is toggled off", async () => {
|
||||
vi.mocked(networking.updateMCPServer).mockResolvedValue({ ...byokServer, is_byok: false } as any);
|
||||
|
||||
render(
|
||||
<MCPServerEdit
|
||||
mcpServer={byokServer}
|
||||
accessToken="access-token"
|
||||
onCancel={vi.fn()}
|
||||
onSuccess={vi.fn()}
|
||||
availableAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByokSwitch()).toBeTruthy());
|
||||
await act(async () => {
|
||||
fireEvent.click(getByokSwitch()!);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Authentication Value")).toBeInTheDocument());
|
||||
|
||||
const payload = await save();
|
||||
expect(payload.is_byok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import MCPServerCostConfig from "./mcp_server_cost_config";
|
|||
import MCPPermissionManagement from "./MCPPermissionManagement";
|
||||
import MCPToolConfiguration from "./mcp_tool_configuration";
|
||||
import StdioConfiguration from "./StdioConfiguration";
|
||||
import ByokFields from "./ByokFields";
|
||||
import MCPLogoSelector from "./MCPLogoSelector";
|
||||
import EnvVarsSection from "./EnvVarsSection";
|
||||
import { validateMCPServerUrl, validateMCPServerName, normalizeEnvVars } from "./utils";
|
||||
|
|
@ -65,6 +66,8 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
const isOpenAPITransport = transportType === TRANSPORT.OPENAPI;
|
||||
const isMCPTransport = !isStdioTransport && !isOpenAPITransport;
|
||||
const shouldShowAuthValueField = authType ? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType) : false;
|
||||
const isByokRaw = Form.useWatch("is_byok", form);
|
||||
const isByok = shouldShowAuthValueField && Boolean(isByokRaw);
|
||||
const isOAuthAuthType = authType === AUTH_TYPE.OAUTH2;
|
||||
const isAwsSigV4AuthType = authType === AUTH_TYPE.AWS_SIGV4;
|
||||
const oauthFlowTypeValue = Form.useWatch("oauth_flow_type", form) as string | undefined;
|
||||
|
|
@ -587,6 +590,14 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
restValues.transport = "http";
|
||||
}
|
||||
|
||||
// BYOK is only valid for static-credential auth types; drop a stale ``true``
|
||||
// (and its metadata) left behind by switching auth_type after toggling it on.
|
||||
const isByokEnabled =
|
||||
AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(restValues.auth_type) && Boolean(restValues.is_byok);
|
||||
restValues.is_byok = isByokEnabled;
|
||||
restValues.byok_description = isByokEnabled ? restValues.byok_description || [] : [];
|
||||
restValues.byok_api_key_help_url = isByokEnabled ? restValues.byok_api_key_help_url || null : null;
|
||||
|
||||
// Parse token_validation JSON if provided
|
||||
let tokenValidation: Record<string, any> | null = null;
|
||||
if (rawTokenValidationJson && rawTokenValidationJson.trim() !== "") {
|
||||
|
|
@ -670,7 +681,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
};
|
||||
|
||||
const includeCredentials =
|
||||
restValues.auth_type && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(restValues.auth_type);
|
||||
!isByokEnabled && restValues.auth_type && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(restValues.auth_type);
|
||||
|
||||
if (includeCredentials && credentialsPayload && Object.keys(credentialsPayload).length > 0) {
|
||||
payload.credentials = credentialsPayload;
|
||||
|
|
@ -824,6 +835,8 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
</Form.Item>
|
||||
)}
|
||||
|
||||
{!isStdioTransport && shouldShowAuthValueField && <ByokFields form={form} />}
|
||||
|
||||
{isStdioTransport && (
|
||||
<div className="rounded-lg border border-gray-200 p-4 space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
|
|
@ -884,7 +897,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!isStdioTransport && shouldShowAuthValueField && (
|
||||
{!isStdioTransport && shouldShowAuthValueField && !isByok && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue