mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
feat(ui): build secret headers into the MCP server create and edit payloads
This commit is contained in:
parent
2112645192
commit
04509b4886
7 changed files with 74 additions and 3 deletions
|
|
@ -239,6 +239,32 @@ describe("buildCreateServerPayload credentials", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("buildCreateServerPayload secret headers", () => {
|
||||
const rows = [
|
||||
{ header: " DD-API-KEY ", value: " api-secret " },
|
||||
{ header: "DD-APPLICATION-KEY", value: "app-secret" },
|
||||
];
|
||||
|
||||
it("sends the rows as credentials.headers and never as a top-level field", () => {
|
||||
const payload = payloadOf(build({ auth_type: "secret_headers", secret_headers: rows }));
|
||||
expect(payload.credentials).toEqual({
|
||||
headers: { "DD-API-KEY": "api-secret", "DD-APPLICATION-KEY": "app-secret" },
|
||||
});
|
||||
expect(payload).not.toHaveProperty("secret_headers");
|
||||
});
|
||||
|
||||
it("leaves out rows that a different auth type left behind", () => {
|
||||
const payload = payloadOf(build({ auth_type: "api_key", credentials: { auth_value: "k" }, secret_headers: rows }));
|
||||
expect(payload.credentials).toEqual({ auth_value: "k" });
|
||||
expect(payload).not.toHaveProperty("secret_headers");
|
||||
});
|
||||
|
||||
it("omits credentials when no row names a header", () => {
|
||||
const payload = payloadOf(build({ auth_type: "secret_headers", secret_headers: [{ header: " ", value: "v" }] }));
|
||||
expect(payload).not.toHaveProperty("credentials");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCreateServerPayload flags", () => {
|
||||
it.each([["true_passthrough"], ["oauth_delegate"]])("defaults dcr_bridge on for %s", (authType) => {
|
||||
expect(payloadOf(build({ auth_type: authType })).dcr_bridge).toBe(true);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const AUTH_TYPES_REQUIRING_CREDENTIALS = [
|
|||
AUTH_TYPE.OAUTH2_TOKEN_EXCHANGE,
|
||||
AUTH_TYPE.OAUTH2_ID_JAG,
|
||||
AUTH_TYPE.AWS_SIGV4,
|
||||
AUTH_TYPE.SECRET_HEADERS,
|
||||
AUTH_TYPE.TRUE_PASSTHROUGH,
|
||||
AUTH_TYPE.OAUTH_DELEGATE,
|
||||
];
|
||||
|
|
@ -75,6 +76,15 @@ export const reduceStaticHeaders = (list: unknown): Record<string, string> => {
|
|||
}, {});
|
||||
};
|
||||
|
||||
export const withSecretHeaders = (
|
||||
credentials: Readonly<Record<string, unknown>> | undefined,
|
||||
authType: string | undefined,
|
||||
secretHeadersList: unknown,
|
||||
): Readonly<Record<string, unknown>> | undefined => {
|
||||
const headers = authType === AUTH_TYPE.SECRET_HEADERS ? reduceStaticHeaders(secretHeadersList) : {};
|
||||
return Object.keys(headers).length > 0 ? { ...credentials, headers } : credentials;
|
||||
};
|
||||
|
||||
// Accepts both the full `{"mcpServers": {"name": {...}}}` shape a user copies out of a client config
|
||||
// and a bare `{"command": ..., "args": ..., "env": ...}`. A non-object JSON body (null, a number)
|
||||
// falls through to the invalid branch, which is what the caller surfaces to the admin.
|
||||
|
|
@ -137,6 +147,7 @@ export const buildCreateServerPayload = (
|
|||
|
||||
const {
|
||||
static_headers: staticHeadersList,
|
||||
secret_headers: secretHeadersList,
|
||||
env_vars: envVarsList,
|
||||
stdio_config: rawStdioConfig,
|
||||
credentials: credentialValues,
|
||||
|
|
@ -172,7 +183,7 @@ export const buildCreateServerPayload = (
|
|||
const transport = restValues.transport === TRANSPORT.OPENAPI ? "http" : restValues.transport;
|
||||
const authType = restValues.auth_type as string | undefined;
|
||||
|
||||
const credentialsPayload = filterCredentials(credentialValues);
|
||||
const credentialsPayload = withSecretHeaders(filterCredentials(credentialValues), authType, secretHeadersList);
|
||||
const includeCredentials = authType !== undefined && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(authType);
|
||||
// Client-forwarded rows persist ONLY the declared app; strip any token material that lingered in
|
||||
// the form (e.g. from a prior oauth2 authorize on the same session) so it can never reach the row.
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ const AUTH_TYPES = [
|
|||
"oauth2_token_exchange",
|
||||
"oauth2_id_jag",
|
||||
"aws_sigv4",
|
||||
"secret_headers",
|
||||
"true_passthrough",
|
||||
"oauth_delegate",
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -358,6 +358,32 @@ describe("buildEditServerPayload wire contract", () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe("buildEditServerPayload secret headers", () => {
|
||||
const rows = [{ header: "DD-API-KEY", value: "rotated-secret" }];
|
||||
const payloadOf = (values: EditServerFormValues) => {
|
||||
const result = buildEditServerPayload({ ...EDIT_FORM_VALUES, auth_type: "secret_headers", ...values }, baseUi);
|
||||
expect(result.kind).toBe("ok");
|
||||
if (result.kind !== "ok") throw new Error("Expected valid MCP server payload");
|
||||
return result.payload;
|
||||
};
|
||||
|
||||
it("replaces the stored headers when rows are entered", () => {
|
||||
const payload = payloadOf({ secret_headers: rows });
|
||||
expect(payload.credentials).toEqual({ headers: { "DD-API-KEY": "rotated-secret" } });
|
||||
expect(payload).not.toHaveProperty("secret_headers");
|
||||
});
|
||||
|
||||
it("sends no credentials, so the stored headers are kept, when no rows are entered", () => {
|
||||
expect(payloadOf({ secret_headers: [] })).not.toHaveProperty("credentials");
|
||||
});
|
||||
|
||||
it("ignores rows once the server is switched to another auth type", () => {
|
||||
const payload = payloadOf({ auth_type: "none", secret_headers: rows });
|
||||
expect(payload).not.toHaveProperty("credentials");
|
||||
expect(payload).not.toHaveProperty("secret_headers");
|
||||
});
|
||||
});
|
||||
|
||||
void ADMIN_CONFIG_CREDENTIAL_KEYS;
|
||||
void AUTH_TYPE;
|
||||
void AUTH_TYPES_REQUIRING_CREDENTIALS;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
isClientForwardedTokenMode,
|
||||
preservedAdminCredentials,
|
||||
} from "@/components/mcp_tools/types";
|
||||
import { AUTH_TYPES_REQUIRING_CREDENTIALS, reduceStaticHeaders } from "./createServerPayload";
|
||||
import { AUTH_TYPES_REQUIRING_CREDENTIALS, reduceStaticHeaders, withSecretHeaders } from "./createServerPayload";
|
||||
import { TOOL_DISPLAY_NAME_PATTERN, normalizeEnvVars } from "./utils";
|
||||
|
||||
export type MCPAccessGroupValue = string | { readonly name?: string };
|
||||
|
|
@ -288,6 +288,7 @@ export const buildEditServerPayload = (values: EditServerFormValues, ui: EditSer
|
|||
|
||||
const {
|
||||
static_headers: staticHeadersList,
|
||||
secret_headers: secretHeadersList,
|
||||
env_vars: envVarsList,
|
||||
credentials: credentialValues,
|
||||
stdio_config: rawStdioConfig,
|
||||
|
|
@ -308,7 +309,11 @@ export const buildEditServerPayload = (values: EditServerFormValues, ui: EditSer
|
|||
);
|
||||
const staticHeaders = reduceStaticHeaders(staticHeadersList);
|
||||
const envVars = normalizeEnvVars(envVarsList);
|
||||
const credentialsPayload = buildCredentials(credentialValues);
|
||||
const credentialsPayload = withSecretHeaders(
|
||||
buildCredentials(credentialValues),
|
||||
rawRestValues.auth_type,
|
||||
secretHeadersList,
|
||||
);
|
||||
|
||||
const stdio =
|
||||
rawRestValues.transport === "stdio"
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export interface McpEnvVarRow {
|
|||
|
||||
export interface McpListValues {
|
||||
static_headers: McpStaticHeaderRow[];
|
||||
secret_headers: McpStaticHeaderRow[];
|
||||
env_vars: McpEnvVarRow[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const AUTH_TYPE = {
|
|||
OAUTH2_TOKEN_EXCHANGE: "oauth2_token_exchange",
|
||||
OAUTH2_ID_JAG: "oauth2_id_jag",
|
||||
AWS_SIGV4: "aws_sigv4",
|
||||
SECRET_HEADERS: "secret_headers",
|
||||
TRUE_PASSTHROUGH: "true_passthrough",
|
||||
OAUTH_DELEGATE: "oauth_delegate",
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue