mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
feat(ui): add the secret headers auth type to the MCP server forms
This commit is contained in:
parent
2dd151d4fc
commit
c89908d035
11 changed files with 312 additions and 6 deletions
|
|
@ -436,6 +436,43 @@ describe("CreateMCPServer", () => {
|
|||
expect(payload.credentials).toEqual({ auth_value: "ApiKey raw-key-123" });
|
||||
});
|
||||
|
||||
it("drops the Secret Headers selection when the transport switches to OpenAPI, so the server can still be saved", async () => {
|
||||
await selectHttpTransport();
|
||||
await selectOption("Authentication", "Secret Headers");
|
||||
await screen.findByPlaceholderText("Header name (e.g., DD-API-KEY)");
|
||||
|
||||
await selectOption("Transport Type", "OpenAPI Spec");
|
||||
const specInput = await screen.findByPlaceholderText("https://petstore3.swagger.io/api/v3/openapi.json");
|
||||
fireEvent.change(getServerNameInput(), { target: { value: "Switched_To_OpenAPI" } });
|
||||
fireEvent.change(specInput, { target: { value: "https://example.com/openapi.json" } });
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add MCP Server" }));
|
||||
});
|
||||
expect(networking.createMCPServer).not.toHaveBeenCalled();
|
||||
await selectOption("Authentication", "None");
|
||||
|
||||
vi.mocked(networking.createMCPServer).mockResolvedValue({
|
||||
server_id: "new-server-3",
|
||||
server_name: "Switched_To_OpenAPI",
|
||||
alias: "Switched_To_OpenAPI",
|
||||
url: "",
|
||||
transport: "http",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
created_by: "user-1",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
updated_by: "user-1",
|
||||
});
|
||||
|
||||
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.auth_type).toBe("none");
|
||||
expect(payload.credentials?.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not write the browser-authorized token into form.credentials for true_passthrough", async () => {
|
||||
await selectHttpTransport();
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
MCPServerCostInfo,
|
||||
TRANSPORT,
|
||||
TRANSPORT_ITEMS,
|
||||
AUTH_TYPE_ITEMS,
|
||||
authTypeItemsFor,
|
||||
getMcpOAuthMode,
|
||||
MCP_OAUTH2_FLOW_M2M,
|
||||
isClientForwardedTokenMode,
|
||||
|
|
@ -40,6 +40,7 @@ import {
|
|||
import { DUPLICATE_IDENTIFIER_MESSAGE_KEY, findDuplicateMcpServer, mcpSubmitErrorReason } from "./duplicateServerCheck";
|
||||
import { readCreateUiSnapshot, writeCreateUiSnapshot } from "./createOAuthUiState";
|
||||
import AwsSigV4Fields from "./AwsSigV4Fields";
|
||||
import SecretHeadersFields from "./SecretHeadersFields";
|
||||
import OpenApiByokFields from "./OpenApiByokFields";
|
||||
import OAuthFormFields from "./OAuthFormFields";
|
||||
import TruePassthroughWarning from "./TruePassthroughWarning";
|
||||
|
|
@ -177,6 +178,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
const isTokenExchangeAuthType = authType === AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE;
|
||||
const isIdJagAuthType = authType === AUTH_TYPE.OAUTH2_ID_JAG;
|
||||
const isAwsSigV4AuthType = authType === AUTH_TYPE.AWS_SIGV4;
|
||||
const isSecretHeadersAuthType = authType === AUTH_TYPE.SECRET_HEADERS;
|
||||
const isM2MFlow = isOAuthAuthType && formValues.oauth_flow_type === OAUTH_FLOW.M2M;
|
||||
|
||||
const persistCreateUiState = () => {
|
||||
|
|
@ -879,12 +881,12 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
}}
|
||||
>
|
||||
{(control) => (
|
||||
<Select {...selectControl<string>(control)} items={AUTH_TYPE_ITEMS}>
|
||||
<Select {...selectControl<string>(control)} items={authTypeItemsFor(transportType)}>
|
||||
<SelectTrigger {...selectTriggerControl(control)} className="w-full rounded-lg">
|
||||
<SelectValue placeholder={t("mcpServers.createMcpServer.auth.typePlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AUTH_TYPE_ITEMS.map((item) => (
|
||||
{authTypeItemsFor(transportType).map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -958,6 +960,11 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
|
||||
{transportType !== "stdio" && transportType !== "" && isAwsSigV4AuthType && <AwsSigV4Fields />}
|
||||
|
||||
{transportType !== "stdio" &&
|
||||
transportType !== "" &&
|
||||
transportType !== TRANSPORT.OPENAPI &&
|
||||
isSecretHeadersAuthType && <SecretHeadersFields />}
|
||||
|
||||
{/* Stdio Configuration - only show for stdio transport */}
|
||||
{transportType === "stdio" && !stdioEnabled && <StdioDisabledBanner />}
|
||||
<StdioConfiguration isVisible={transportType === "stdio"} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import SecretHeadersFields from "./SecretHeadersFields";
|
||||
import { McpFormHarness as WithForm } from "./McpFormTestHarness";
|
||||
|
||||
const NAME_PLACEHOLDER = "Header name (e.g., DD-API-KEY)";
|
||||
const VALUE_PLACEHOLDER = "Header value";
|
||||
|
||||
describe("SecretHeadersFields", () => {
|
||||
it("starts a new server with one empty row whose value is masked", () => {
|
||||
render(
|
||||
<WithForm>
|
||||
<SecretHeadersFields />
|
||||
</WithForm>,
|
||||
);
|
||||
expect(screen.getAllByPlaceholderText(NAME_PLACEHOLDER)).toHaveLength(1);
|
||||
expect(screen.getByPlaceholderText(VALUE_PLACEHOLDER)).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
it("adds a single starter row even when React runs effects twice", () => {
|
||||
render(
|
||||
<React.StrictMode>
|
||||
<WithForm>
|
||||
<SecretHeadersFields />
|
||||
</WithForm>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
expect(screen.getAllByPlaceholderText(NAME_PLACEHOLDER)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("starts an existing server with no rows, since the stored headers are never returned", () => {
|
||||
render(
|
||||
<WithForm>
|
||||
<SecretHeadersFields isEditing />
|
||||
</WithForm>,
|
||||
);
|
||||
expect(screen.queryByPlaceholderText(NAME_PLACEHOLDER)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/leave this empty to keep it/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("submits every row under secret_headers and drops a removed row", async () => {
|
||||
const onFinish = vi.fn();
|
||||
render(
|
||||
<WithForm onFinish={onFinish}>
|
||||
<SecretHeadersFields isEditing />
|
||||
</WithForm>,
|
||||
);
|
||||
const addHeader = screen.getByRole("button", { name: /Add Header/ });
|
||||
await act(async () => fireEvent.click(addHeader));
|
||||
await act(async () => fireEvent.click(addHeader));
|
||||
const names = screen.getAllByPlaceholderText(NAME_PLACEHOLDER);
|
||||
const values = screen.getAllByPlaceholderText(VALUE_PLACEHOLDER);
|
||||
await act(async () => {
|
||||
fireEvent.change(names[0], { target: { value: "DD-API-KEY" } });
|
||||
fireEvent.change(values[0], { target: { value: "api-secret" } });
|
||||
fireEvent.change(names[1], { target: { value: "DD-APPLICATION-KEY" } });
|
||||
fireEvent.change(values[1], { target: { value: "app-secret" } });
|
||||
});
|
||||
await act(async () => fireEvent.click(screen.getAllByLabelText("Remove header")[1]));
|
||||
await act(async () => fireEvent.click(screen.getByRole("button", { name: "Submit" })));
|
||||
expect(onFinish).toHaveBeenCalledWith({ secret_headers: [{ header: "DD-API-KEY", value: "api-secret" }] });
|
||||
});
|
||||
|
||||
it("offers removal as a labeled button and keeps the last row of a new server", async () => {
|
||||
render(
|
||||
<WithForm>
|
||||
<SecretHeadersFields />
|
||||
</WithForm>,
|
||||
);
|
||||
const remove = screen.getByRole("button", { name: "Remove header" });
|
||||
expect(remove).toBeDisabled();
|
||||
await act(async () => fireEvent.click(screen.getByRole("button", { name: /Add Header/ })));
|
||||
const buttons = screen.getAllByRole("button", { name: "Remove header" });
|
||||
expect(buttons).toHaveLength(2);
|
||||
buttons.forEach((button) => expect(button).toBeEnabled());
|
||||
await act(async () => fireEvent.click(buttons[1]));
|
||||
expect(screen.getAllByPlaceholderText(NAME_PLACEHOLDER)).toHaveLength(1);
|
||||
expect(screen.getByRole("button", { name: "Remove header" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("lets a saved server drop every row, since no rows keeps the stored headers", async () => {
|
||||
render(
|
||||
<WithForm>
|
||||
<SecretHeadersFields isEditing />
|
||||
</WithForm>,
|
||||
);
|
||||
await act(async () => fireEvent.click(screen.getByRole("button", { name: /Add Header/ })));
|
||||
const remove = screen.getByRole("button", { name: "Remove header" });
|
||||
expect(remove).toBeEnabled();
|
||||
await act(async () => fireEvent.click(remove));
|
||||
expect(screen.queryByPlaceholderText(NAME_PLACEHOLDER)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { CircleMinus, Plus } from "lucide-react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
MountedFormField,
|
||||
useMountedName,
|
||||
type MountedFormValues,
|
||||
} from "@/components/common_components/MountedFormField";
|
||||
import { PasswordInput } from "@/components/shared/PasswordInput";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { textControl } from "./mcpFieldRules";
|
||||
import { listControl } from "./mcpFormStore";
|
||||
|
||||
const fieldClassName = "rounded-lg border-border focus:border-info focus:ring-ring";
|
||||
|
||||
const nonBlankRule =
|
||||
(message: string) =>
|
||||
(value: unknown): true | string =>
|
||||
typeof value === "string" && value.trim() !== "" ? true : message;
|
||||
|
||||
const SecretHeadersFields: React.FC<{ isEditing?: boolean }> = ({ isEditing = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<MountedFormValues>();
|
||||
const { fields, append, remove } = useFieldArray({ control: listControl(control), name: "secret_headers" });
|
||||
useMountedName("secret_headers");
|
||||
|
||||
const addedStarterRow = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isEditing || addedStarterRow.current || fields.length > 0) return;
|
||||
addedStarterRow.current = true;
|
||||
append({});
|
||||
}, [isEditing, fields.length, append]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(isEditing ? "mcpServers.secretHeadersFields.editIntro" : "mcpServers.secretHeadersFields.intro")}
|
||||
</p>
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.id} className="flex w-full items-baseline gap-4">
|
||||
<MountedFormField
|
||||
name={["secret_headers", String(index), "header"]}
|
||||
className="flex-1"
|
||||
rules={{ validate: { required: nonBlankRule(t("mcpServers.secretHeadersFields.headerNameRequired")) } }}
|
||||
>
|
||||
{(control) => (
|
||||
<Input
|
||||
{...textControl(control)}
|
||||
placeholder={t("mcpServers.secretHeadersFields.headerNamePlaceholder")}
|
||||
className={fieldClassName}
|
||||
/>
|
||||
)}
|
||||
</MountedFormField>
|
||||
<MountedFormField
|
||||
name={["secret_headers", String(index), "value"]}
|
||||
className="flex-1"
|
||||
rules={{ validate: { required: nonBlankRule(t("mcpServers.secretHeadersFields.headerValueRequired")) } }}
|
||||
>
|
||||
{(control) => (
|
||||
<PasswordInput
|
||||
{...textControl(control)}
|
||||
placeholder={t("mcpServers.secretHeadersFields.headerValuePlaceholder")}
|
||||
autoComplete="new-password"
|
||||
groupClassName={fieldClassName}
|
||||
/>
|
||||
)}
|
||||
</MountedFormField>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={t("mcpServers.secretHeadersFields.remove")}
|
||||
disabled={fields.length <= 1 && !isEditing}
|
||||
onClick={() => remove(index)}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<CircleMinus />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" className="w-full border-dashed" onClick={() => append({})}>
|
||||
<Plus />
|
||||
{t("mcpServers.secretHeadersFields.add")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretHeadersFields;
|
||||
|
|
@ -485,4 +485,33 @@ describe("MCPServerEdit live tool preview", () => {
|
|||
expect(await screen.findByText("Saved credentials rejected")).toBeInTheDocument();
|
||||
expect(networking.listMCPTools).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("drops the Secret Headers selection when the transport switches to OpenAPI", async () => {
|
||||
const server = { ...BASE, auth_type: "secret_headers" } as MCPServer;
|
||||
vi.mocked(networking.updateMCPServer).mockResolvedValue(server as never);
|
||||
render(
|
||||
<MCPServerEdit
|
||||
mcpServer={server}
|
||||
accessToken="access-token"
|
||||
userID="user-1"
|
||||
onCancel={vi.fn()}
|
||||
onSuccess={vi.fn()}
|
||||
availableAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
await selectOption("Transport Type", "OpenAPI Spec");
|
||||
const specInput = await screen.findByPlaceholderText("https://petstore3.swagger.io/api/v3/openapi.json");
|
||||
fireEvent.change(specInput, { target: { value: "https://example.com/openapi.json" } });
|
||||
await act(async () => {
|
||||
screen.getAllByRole("button", { name: "Save Changes" })[0].click();
|
||||
});
|
||||
expect(networking.updateMCPServer).not.toHaveBeenCalled();
|
||||
await selectOption("Authentication", "None");
|
||||
await act(async () => {
|
||||
screen.getAllByRole("button", { name: "Save Changes" })[0].click();
|
||||
});
|
||||
await waitFor(() => expect(networking.updateMCPServer).toHaveBeenCalled());
|
||||
const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0];
|
||||
expect((payload as { auth_type?: string }).auth_type).toBe("none");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
MCPServerCostInfo,
|
||||
TRANSPORT,
|
||||
TRANSPORT_ITEMS,
|
||||
AUTH_TYPE_ITEMS,
|
||||
authTypeItemsFor,
|
||||
getMcpOAuthMode,
|
||||
oauth2FlowToFormValue,
|
||||
} from "@/components/mcp_tools/types";
|
||||
|
|
@ -47,6 +47,7 @@ import MCPToolConfiguration from "./mcp_tool_configuration";
|
|||
import StdioConfiguration from "./StdioConfiguration";
|
||||
import { StdioDisabledBanner, TransportSelectItems } from "./StdioAvailability";
|
||||
import TokenExchangeFormFields from "./TokenExchangeFormFields";
|
||||
import SecretHeadersFields from "./SecretHeadersFields";
|
||||
import IdJagFormFields from "./IdJagFormFields";
|
||||
import OAuthFormFields from "./OAuthFormFields";
|
||||
import MCPLogoSelector from "./MCPLogoSelector";
|
||||
|
|
@ -195,6 +196,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
const isTokenExchangeAuthType = authType === AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE;
|
||||
const isIdJagAuthType = authType === AUTH_TYPE.OAUTH2_ID_JAG;
|
||||
const isAwsSigV4AuthType = authType === AUTH_TYPE.AWS_SIGV4;
|
||||
const isSecretHeadersAuthType = authType === AUTH_TYPE.SECRET_HEADERS;
|
||||
// Same fallback as the delegate switch below: the value is undefined until the field mounts, so
|
||||
// reading it alone flashes the "no OAuth flow set" warning at a server that already has one.
|
||||
const oauthFlowTypeValue =
|
||||
|
|
@ -688,6 +690,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
args: undefined,
|
||||
env_json: undefined,
|
||||
stdio_config: undefined,
|
||||
...(isSecretHeadersAuthType ? { auth_type: undefined } : {}),
|
||||
};
|
||||
setFieldsValue(form, clearedForOpenapi);
|
||||
} else {
|
||||
|
|
@ -1014,12 +1017,12 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
}}
|
||||
>
|
||||
{(control) => (
|
||||
<Select {...selectControl<string>(control)} items={AUTH_TYPE_ITEMS}>
|
||||
<Select {...selectControl<string>(control)} items={authTypeItemsFor(transportType)}>
|
||||
<SelectTrigger {...selectTriggerControl(control)} className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AUTH_TYPE_ITEMS.map((item) => (
|
||||
{authTypeItemsFor(transportType).map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -1155,6 +1158,10 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
|
||||
{!isStdioTransport && isIdJagAuthType && <IdJagFormFields isEditing />}
|
||||
|
||||
{!isStdioTransport && !isOpenAPITransport && isSecretHeadersAuthType && (
|
||||
<SecretHeadersFields isEditing={mcpServer.auth_type === AUTH_TYPE.SECRET_HEADERS} />
|
||||
)}
|
||||
|
||||
{!isStdioTransport && isAwsSigV4AuthType && (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
|
|
|
|||
|
|
@ -111,6 +111,14 @@ describe("edit root: auth_type gates", () => {
|
|||
expect(entra).toContain("token_exchange_profile");
|
||||
});
|
||||
|
||||
it("mounts the secret header rows only for secret_headers, on edit and on create", () => {
|
||||
expect(editRoot({ transport: "http", auth_type: "secret_headers" })).toContain("secret_headers");
|
||||
expect(createRoot({ transport: "http", auth_type: "secret_headers" })).toContain("secret_headers");
|
||||
expect(editRoot({ transport: "http", auth_type: "api_key" })).not.toContain("secret_headers");
|
||||
expect(createRoot({ transport: "http", auth_type: "none" })).not.toContain("secret_headers");
|
||||
expect(editRoot({ transport: "stdio", auth_type: "secret_headers" })).not.toContain("secret_headers");
|
||||
});
|
||||
|
||||
it("mounts the seven aws credentials only for aws_sigv4", () => {
|
||||
expect(sorted(editCreds({ transport: "http", auth_type: "aws_sigv4" }))).toStrictEqual(
|
||||
sorted([
|
||||
|
|
|
|||
|
|
@ -97,6 +97,9 @@ const authSubtreeRoot = ({ authType, oauthFlowType, tokenExchangeProfile }: Auth
|
|||
if (authType === AUTH_TYPE.OAUTH2_ID_JAG) {
|
||||
return ["token_exchange_endpoint", "audience", "subject_token_type"];
|
||||
}
|
||||
if (authType === AUTH_TYPE.SECRET_HEADERS) {
|
||||
return ["secret_headers"];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
withoutMintedTokenCredentials,
|
||||
credentialAuthClass,
|
||||
isUnsupportedOnGatewayConnect,
|
||||
authTypeItemsFor,
|
||||
} from "./types";
|
||||
|
||||
describe("getOAuthAuthorizationIdentity", () => {
|
||||
|
|
@ -355,3 +356,15 @@ describe("preservedAdminCredentials vs preservedDeclaredAppCredentials", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("authTypeItemsFor", () => {
|
||||
it("offers secret headers for every transport except OpenAPI", () => {
|
||||
const offered = (transport: string) => authTypeItemsFor(transport).some((item) => item.value === "secret_headers");
|
||||
expect([TRANSPORT.HTTP, TRANSPORT.SSE, undefined].map((transport) => offered(transport as string))).toEqual([
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
]);
|
||||
expect(offered(TRANSPORT.OPENAPI)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,10 +59,16 @@ export const AUTH_TYPE_ITEMS = [
|
|||
{ value: AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE, label: "OAuth Token Exchange (OBO)" },
|
||||
{ value: AUTH_TYPE.OAUTH2_ID_JAG, label: "ID-JAG (Okta Cross App Access)" },
|
||||
{ value: AUTH_TYPE.AWS_SIGV4, label: "AWS SigV4 (Bedrock AgentCore MCPs)" },
|
||||
{ value: AUTH_TYPE.SECRET_HEADERS, label: "Secret Headers" },
|
||||
{ value: AUTH_TYPE.TRUE_PASSTHROUGH, label: "True Passthrough (no LiteLLM auth)" },
|
||||
{ value: AUTH_TYPE.OAUTH_DELEGATE, label: "OAuth Delegate (client-supplied upstream token)" },
|
||||
];
|
||||
|
||||
export const authTypeItemsFor = (transport: string | undefined) =>
|
||||
transport === TRANSPORT.OPENAPI
|
||||
? AUTH_TYPE_ITEMS.filter((item) => item.value !== AUTH_TYPE.SECRET_HEADERS)
|
||||
: AUTH_TYPE_ITEMS;
|
||||
|
||||
// The two client-forwarded token modes: the caller supplies the upstream Authorization (forwarded
|
||||
// verbatim for true_passthrough, alongside LiteLLM admission for oauth_delegate). The dashboard holds
|
||||
// their token in sessionStorage instead of persisting it, and the browser-authorize temp payload keeps
|
||||
|
|
|
|||
|
|
@ -11776,6 +11776,16 @@
|
|||
"placeholder": "litellm-prod (optional, auto-generated if blank)"
|
||||
}
|
||||
},
|
||||
"secretHeadersFields": {
|
||||
"intro": "Sent with every request to this MCP server. Values are stored encrypted and are not shown again after you save.",
|
||||
"editIntro": "Header names and values are not shown after saving. Add headers here to replace the stored set, or leave this empty to keep it.",
|
||||
"headerNameRequired": "Header name is required",
|
||||
"headerNamePlaceholder": "Header name (e.g., DD-API-KEY)",
|
||||
"headerValueRequired": "Header value is required",
|
||||
"headerValuePlaceholder": "Header value",
|
||||
"remove": "Remove header",
|
||||
"add": "Add Header"
|
||||
},
|
||||
"passthroughAuthorizeSection": {
|
||||
"description": "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.",
|
||||
"appMayNotMatchUpstream": "You changed the upstream URL or endpoints; the OAuth app entered here was registered for the previous upstream and may not be valid. Update the client ID, or clear it to use dynamic client registration.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue