diff --git a/ui/litellm-dashboard/CLAUDE.md b/ui/litellm-dashboard/CLAUDE.md
index 5ec9392d2b0..701b37ec6aa 100644
--- a/ui/litellm-dashboard/CLAUDE.md
+++ b/ui/litellm-dashboard/CLAUDE.md
@@ -3,3 +3,9 @@ Never put LiteLLM tokens or API keys in `localStorage`. `localStorage` survives
When you fix lint violations that are grandfathered in `eslint-suppressions.json`, run `eslint . --prune-suppressions` and commit the updated baseline so the gate ratchets down instead of leaving a stale suppression
`src/lib/http/schema.d.ts` is generated from the proxy's OpenAPI spec; never hand-edit it. After changing a backend route or response model that the dashboard consumes, run `npm run gen:api` and commit the result (CI `Check UI API Types Sync` enforces this)
+
+Tests come in three tiers, named by the standard definitions. `Foo.test.tsx` is a unit test: one module, collaborators replaced by doubles, no multi-component tree, and it should run in milliseconds. `Foo.integration.test.tsx` renders a real component tree with real children and only stubs the network boundary; it costs seconds per case, so it earns its place by proving wiring that a unit test cannot reach. Browser-level tests live in `tests/e2e/ui/` as Playwright specs against a live proxy
+
+When a component holds logic worth asserting, extract the logic and unit-test it there rather than driving it through a render. `CreateMCPServer` is the worked example: its payload building lives in `createServerPayload.ts` with 46 unit tests that run in single-digit milliseconds, while `CreateMCPServer.integration.test.tsx` keeps only the cases that prove a form field reaches the right payload key. A test that renders a whole modal to assert the shape of one object belongs in the first category, not the second
+
+Most of the suite predates this split and is not yet classified, so an unsuffixed `*.test.tsx` is not evidence that a file is really a unit test. Classify what you touch
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx
similarity index 96%
rename from ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.test.tsx
rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx
index 45da71ed301..c9007e29c3e 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx
@@ -120,10 +120,48 @@ describe("CreateMCPServer", () => {
expect(screen.getByText("Add New MCP Server")).toBeInTheDocument();
});
- it("should not render when user is not an admin", () => {
+ // The modal DOES render for a non-admin; it retitles and routes the submit to the review endpoint.
+ // The assertion this replaced only checked that the admin title was absent, which passed for the
+ // wrong reason and left the whole non-admin submission path uncovered.
+ it("routes a non-admin submission to the review endpoint instead of creating the server", async () => {
render();
+ expect(screen.getByText("Submit MCP Server for Review")).toBeInTheDocument();
expect(screen.queryByText("Add New MCP Server")).not.toBeInTheDocument();
+
+ 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: "Submitted_Server" } });
+ });
+ await act(async () => {
+ fireEvent.change(screen.getByPlaceholderText("https://your-mcp-server.com"), {
+ target: { value: "https://example.com/mcp" },
+ });
+ });
+ await selectAntOption("Authentication", "None");
+
+ vi.mocked(networking.registerMCPServer).mockResolvedValue({
+ server_id: "submitted-1",
+ server_name: "Submitted_Server",
+ alias: "Submitted_Server",
+ url: "https://example.com/mcp",
+ transport: "http",
+ auth_type: "none",
+ 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.registerMCPServer).toHaveBeenCalledTimes(1));
+ expect(networking.createMCPServer).not.toHaveBeenCalled();
});
it("should show transport type options", async () => {
@@ -1591,44 +1629,8 @@ describe("CreateMCPServer", () => {
expect(payload.credentials?.client_secret).toBeUndefined();
});
- it("omits token_validation from payload when token_validation_json is empty", async () => {
- vi.mocked(networking.createMCPServer).mockResolvedValue({
- server_id: "new-server-oauth",
- server_name: "OAuth_Server",
- alias: "OAuth_Server",
- url: "https://example.com/mcp",
- transport: "http",
- auth_type: "oauth2",
- created_at: "2024-01-01T00:00:00Z",
- created_by: "user-1",
- updated_at: "2024-01-01T00:00:00Z",
- updated_by: "user-1",
- });
-
- await setupOAuthInteractive();
-
- const nameInput = document.getElementById("server_name") as HTMLInputElement;
- await act(async () => {
- fireEvent.change(nameInput, { target: { value: "OAuth_Server" } });
- });
- const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com");
- await act(async () => {
- fireEvent.change(urlInput, { target: { value: "https://example.com/mcp" } });
- });
-
- // Leave token_validation_json empty
- 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];
- expect(payload.token_validation).toBeUndefined();
- });
+ // Empty/whitespace token_validation is covered in createServerPayload.test.ts; the sibling
+ // test above still proves the textarea reaches token_validation_json.
it("includes credentials.token_endpoint_auth_method in payload when client_secret_basic is selected", async () => {
vi.mocked(networking.createMCPServer).mockResolvedValue({
@@ -1670,43 +1672,8 @@ describe("CreateMCPServer", () => {
expect(payload.credentials?.token_endpoint_auth_method).toBe("client_secret_basic");
});
- it("omits token_endpoint_auth_method from credentials when left blank", async () => {
- vi.mocked(networking.createMCPServer).mockResolvedValue({
- server_id: "new-server-oauth",
- server_name: "OAuth_Server",
- alias: "OAuth_Server",
- url: "https://example.com/mcp",
- transport: "http",
- auth_type: "oauth2",
- created_at: "2024-01-01T00:00:00Z",
- created_by: "user-1",
- updated_at: "2024-01-01T00:00:00Z",
- updated_by: "user-1",
- });
-
- await setupOAuthInteractive();
-
- const nameInput = document.getElementById("server_name") as HTMLInputElement;
- await act(async () => {
- fireEvent.change(nameInput, { target: { value: "OAuth_Server" } });
- });
- const urlInput = screen.getByPlaceholderText("https://your-mcp-server.com");
- await act(async () => {
- fireEvent.change(urlInput, { target: { value: "https://example.com/mcp" } });
- });
-
- 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];
- expect(payload.credentials?.token_endpoint_auth_method).toBeUndefined();
- });
+ // Blank credential keys are dropped by the shared filter, covered in createServerPayload.test.ts;
+ // the sibling test above still proves the select reaches credentials.token_endpoint_auth_method.
it("persists access + refresh token to the DB on submit for OBO mode", async () => {
// "Authorize & Fetch" produced a token before submit.
@@ -2052,14 +2019,8 @@ describe("CreateMCPServer oauth2_flow persistence", () => {
expect(payload.oauth2_flow).toBe("client_credentials");
});
- it("sends no oauth2_flow for a non-oauth2 create", async () => {
- vi.mocked(networking.createMCPServer).mockResolvedValue({ ...createdServer, auth_type: "none" });
- await setupHttpServerForm();
- await selectAntOption("Authentication", "None");
-
- const payload = await submitCreate();
- expect(payload.oauth2_flow).toBeUndefined();
- });
+ // oauth2_flow branch coverage lives in createServerPayload.test.ts; the two cases above keep
+ // the dropdown-to-payload wiring they uniquely prove.
});
describe("CreateMCPServer dcr_bridge toggle", () => {
@@ -2185,18 +2146,9 @@ describe("CreateMCPServer dcr_bridge toggle", () => {
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);
- });
+ // Forcing dcr_bridge false for every non-client-forwarded auth type is covered in
+ // createServerPayload.test.ts. The two form-state cases below stay: they prove the Form.Item
+ // unmounts on a switch away, and that the live value survives a client-forwarded swap.
it("forces dcr_bridge: false when the auth type is switched away after toggling", async () => {
vi.mocked(networking.createMCPServer).mockResolvedValue({ ...createdServer, auth_type: "none" });
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/createOAuthUiState.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/createOAuthUiState.test.ts
new file mode 100644
index 00000000000..e2e7814f0d7
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/createOAuthUiState.test.ts
@@ -0,0 +1,126 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { setSecureItem } from "@/utils/secureStorage";
+import { CreateUiSnapshot, readCreateUiSnapshot, writeCreateUiSnapshot } from "./createOAuthUiState";
+
+const STORAGE_KEY = "litellm-mcp-oauth-create-state";
+
+const fullSnapshot: CreateUiSnapshot = {
+ modalVisible: true,
+ formValues: { url: "https://example.com/mcp", auth_type: "oauth2", credentials: { client_id: "app-id" } },
+ transportType: "http",
+ costConfig: { default_cost_per_query: 0.02 },
+ allowedTools: ["search"],
+ hasToolAllowlistInteraction: true,
+ searchValue: "group-a",
+ aliasManuallyEdited: true,
+ logoUrl: "https://cdn/logo.png",
+ authorizedIdentity: "identity-abc",
+};
+
+const seedRaw = (value: unknown) => setSecureItem(STORAGE_KEY, JSON.stringify(value));
+
+describe("createOAuthUiState", () => {
+ beforeEach(() => {
+ window.sessionStorage.clear();
+ vi.restoreAllMocks();
+ });
+
+ it("returns null and leaves storage untouched when nothing was persisted", () => {
+ expect(readCreateUiSnapshot()).toBeNull();
+ });
+
+ it("round-trips a full snapshot through the redirect", () => {
+ writeCreateUiSnapshot(fullSnapshot);
+ expect(readCreateUiSnapshot()).toEqual(fullSnapshot);
+ });
+
+ it("does not store the snapshot in plaintext", () => {
+ writeCreateUiSnapshot(fullSnapshot);
+ // secureStorage base64-encodes; a readable url in the raw value would mean the encoding was lost.
+ expect(window.sessionStorage.getItem(STORAGE_KEY)).not.toContain("https://example.com/mcp");
+ });
+
+ it("consumes the snapshot so a second mount cannot replay it", () => {
+ writeCreateUiSnapshot(fullSnapshot);
+ expect(readCreateUiSnapshot()).not.toBeNull();
+ expect(readCreateUiSnapshot()).toBeNull();
+ expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull();
+ });
+
+ it("strips minted token material so a stale token never rehydrates", () => {
+ writeCreateUiSnapshot({
+ ...fullSnapshot,
+ formValues: {
+ url: "https://example.com/mcp",
+ credentials: {
+ client_id: "app-id",
+ client_secret: "app-secret",
+ access_token: "stale-tok",
+ refresh_token: "stale-refresh",
+ expires_in: 3600,
+ scope: "read",
+ },
+ },
+ });
+
+ const restored = readCreateUiSnapshot();
+ expect(restored?.formValues?.credentials).toEqual({ client_id: "app-id", client_secret: "app-secret" });
+ expect(JSON.stringify(restored)).not.toContain("stale-tok");
+ expect(JSON.stringify(restored)).not.toContain("stale-refresh");
+ });
+
+ it("re-arms invalidation by restoring the authorized identity", () => {
+ writeCreateUiSnapshot(fullSnapshot);
+ expect(readCreateUiSnapshot()?.authorizedIdentity).toBe("identity-abc");
+ });
+
+ it("prefers the persisted form transport over the standalone transportType", () => {
+ seedRaw({ formValues: { transport: "sse" }, transportType: "http" });
+ expect(readCreateUiSnapshot()?.transportType).toBe("sse");
+ });
+
+ it("omits falsy scalars so a restore never blanks freshly mounted state", () => {
+ seedRaw({ searchValue: "", logoUrl: "", transportType: "", modalVisible: false });
+ const restored = readCreateUiSnapshot();
+ expect(restored).not.toHaveProperty("searchValue");
+ expect(restored).not.toHaveProperty("logoUrl");
+ expect(restored).not.toHaveProperty("transportType");
+ expect(restored).not.toHaveProperty("modalVisible");
+ });
+
+ it("restores an explicitly empty tool allowlist, which is a real admin choice", () => {
+ seedRaw({ allowedTools: [], hasToolAllowlistInteraction: true });
+ const restored = readCreateUiSnapshot();
+ expect(restored?.allowedTools).toEqual([]);
+ expect(restored?.hasToolAllowlistInteraction).toBe(true);
+ });
+
+ it.each([
+ ["hasToolAllowlistInteraction", false],
+ ["aliasManuallyEdited", false],
+ ])("restores %s when it was persisted as false", (key, value) => {
+ seedRaw({ [key]: value });
+ expect(readCreateUiSnapshot()).toHaveProperty(key, value);
+ });
+
+ it.each([["hasToolAllowlistInteraction"], ["aliasManuallyEdited"]])(
+ "ignores a non-boolean %s rather than coercing it",
+ (key) => {
+ seedRaw({ [key]: "yes" });
+ expect(readCreateUiSnapshot()).not.toHaveProperty(key);
+ },
+ );
+
+ it("ignores a non-string authorizedIdentity", () => {
+ seedRaw({ authorizedIdentity: 42 });
+ expect(readCreateUiSnapshot()).not.toHaveProperty("authorizedIdentity");
+ });
+
+ it("returns null on a corrupted payload but still clears it", () => {
+ vi.spyOn(console, "error").mockImplementation(() => {});
+ setSecureItem(STORAGE_KEY, "{not json");
+
+ expect(readCreateUiSnapshot()).toBeNull();
+ expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/createServerPayload.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/createServerPayload.test.ts
new file mode 100644
index 00000000000..4573069ff07
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/createServerPayload.test.ts
@@ -0,0 +1,296 @@
+import { describe, expect, it } from "vitest";
+import {
+ BuildCreatePayloadResult,
+ CreateServerUiState,
+ buildCreateServerPayload,
+ parseStdioConfig,
+ reduceStaticHeaders,
+} from "./createServerPayload";
+
+const baseUi: CreateServerUiState = {
+ transportType: "http",
+ costConfig: {},
+ allowedTools: [],
+ hasToolAllowlistInteraction: false,
+ toolNameToDisplayName: {},
+ toolNameToDescription: {},
+ logoUrl: undefined,
+ dcrClient: null,
+};
+
+/** Narrow to the success branch so a regression surfaces as a failed assertion, not a type error. */
+const payloadOf = (result: BuildCreatePayloadResult): Record => {
+ expect(result.kind).toBe("ok");
+ if (result.kind !== "ok") throw new Error("unreachable");
+ return result.payload;
+};
+
+const build = (values: Record, ui: Partial = {}) =>
+ buildCreateServerPayload(values, { ...baseUi, ...ui });
+
+describe("reduceStaticHeaders", () => {
+ it("returns an empty map for a non-array", () => {
+ expect(reduceStaticHeaders(undefined)).toEqual({});
+ expect(reduceStaticHeaders("X-Api-Key: v")).toEqual({});
+ });
+
+ it("trims header and value and drops rows with a blank header", () => {
+ expect(
+ reduceStaticHeaders([
+ { header: " X-Api-Key ", value: " secret " },
+ { header: " ", value: "orphaned" },
+ { header: "X-Empty" },
+ ]),
+ ).toEqual({ "X-Api-Key": "secret", "X-Empty": "" });
+ });
+
+ it("keeps the last value when a header repeats", () => {
+ expect(
+ reduceStaticHeaders([
+ { header: "X-Dup", value: "first" },
+ { header: "X-Dup", value: "second" },
+ ]),
+ ).toEqual({ "X-Dup": "second" });
+ });
+});
+
+describe("parseStdioConfig", () => {
+ it("reads a direct command/args/env config", () => {
+ const result = parseStdioConfig('{"command":"npx","args":["-y","srv"],"env":{"TOKEN":"t"}}');
+ expect(result).toEqual({
+ kind: "ok",
+ fields: { command: "npx", args: ["-y", "srv"], env: { TOKEN: "t" } },
+ });
+ });
+
+ it("unwraps the mcpServers form and derives the server name with underscores", () => {
+ const result = parseStdioConfig('{"mcpServers":{"my-github-server":{"command":"npx","args":["-y"]}}}');
+ expect(result).toEqual({
+ kind: "ok",
+ fields: { command: "npx", args: ["-y"], env: undefined },
+ derivedServerName: "my_github_server",
+ });
+ });
+
+ it("takes the first server when mcpServers holds several", () => {
+ const result = parseStdioConfig('{"mcpServers":{"first":{"command":"a"},"second":{"command":"b"}}}');
+ expect(result).toMatchObject({ kind: "ok", fields: { command: "a" }, derivedServerName: "first" });
+ });
+
+ it("treats an empty mcpServers object as a direct config rather than deriving a name", () => {
+ const result = parseStdioConfig('{"mcpServers":{},"command":"direct"}');
+ expect(result).toEqual({ kind: "ok", fields: { command: "direct", args: undefined, env: undefined } });
+ });
+
+ it.each([["not json{"], ["null"]])("reports %s as invalid", (raw) => {
+ expect(parseStdioConfig(raw)).toEqual({ kind: "invalid" });
+ });
+});
+
+describe("buildCreateServerPayload validation", () => {
+ it("rejects a tool display name containing a space and names the offender", () => {
+ const result = build({ auth_type: "none" }, { toolNameToDisplayName: { search: "My Tool" } });
+ expect(result).toEqual({ kind: "invalid_tool_display_name", displayName: "My Tool" });
+ });
+
+ it("accepts letters, digits, underscores and hyphens in a display name", () => {
+ const result = build({ auth_type: "none" }, { toolNameToDisplayName: { search: "my-tool_2" } });
+ expect(result.kind).toBe("ok");
+ });
+
+ it("rejects unparseable stdio JSON only when the stdio transport is selected", () => {
+ expect(build({ stdio_config: "{oops" }, { transportType: "stdio" })).toEqual({ kind: "invalid_stdio_json" });
+ // The same bad string on an http server is an inert leftover field, not a submit blocker.
+ expect(build({ stdio_config: "{oops" }, { transportType: "http" }).kind).toBe("ok");
+ });
+
+ it("rejects unparseable token validation JSON", () => {
+ expect(build({ token_validation_json: "not-valid-json{" })).toEqual({ kind: "invalid_token_validation_json" });
+ });
+
+ it("ignores a whitespace-only token validation body", () => {
+ const payload = payloadOf(build({ token_validation_json: " " }));
+ expect(payload).not.toHaveProperty("token_validation");
+ });
+
+ it("includes parsed token validation rules when the JSON is valid", () => {
+ const payload = payloadOf(build({ token_validation_json: '{"organization":"my-org","team.id":"42"}' }));
+ expect(payload.token_validation).toEqual({ organization: "my-org", "team.id": "42" });
+ });
+});
+
+describe("buildCreateServerPayload transport and naming", () => {
+ it("maps the UI-only openapi transport to http for the backend", () => {
+ const payload = payloadOf(build({ transport: "openapi", spec_path: "https://api.example.com/openapi.json" }));
+ expect(payload.transport).toBe("http");
+ });
+
+ it("leaves http and sse transports untouched", () => {
+ expect(payloadOf(build({ transport: "sse" })).transport).toBe("sse");
+ });
+
+ it("falls back to the stdio JSON's server key when the name field is blank", () => {
+ const payload = payloadOf(
+ build(
+ { transport: "stdio", stdio_config: '{"mcpServers":{"my-server":{"command":"npx"}}}' },
+ { transportType: "stdio" },
+ ),
+ );
+ expect(payload.server_name).toBe("my_server");
+ expect(payload.command).toBe("npx");
+ });
+
+ it("keeps an explicit server name over the stdio JSON's key", () => {
+ const payload = payloadOf(
+ build(
+ { server_name: "Chosen", transport: "stdio", stdio_config: '{"mcpServers":{"my-server":{"command":"npx"}}}' },
+ { transportType: "stdio" },
+ ),
+ );
+ expect(payload.server_name).toBe("Chosen");
+ });
+
+ it("falls back to the url for mcp_info.server_name when no name is given", () => {
+ const payload = payloadOf(build({ url: "https://example.com/mcp" }));
+ expect((payload.mcp_info as Record).server_name).toBe("https://example.com/mcp");
+ });
+});
+
+describe("buildCreateServerPayload credentials", () => {
+ it("drops empty, null and undefined credential entries", () => {
+ const payload = payloadOf(
+ build({ auth_type: "api_key", credentials: { auth_value: "secret", client_id: "", client_secret: null } }),
+ );
+ expect(payload.credentials).toEqual({ auth_value: "secret" });
+ });
+
+ it("filters blank scopes and omits the key when none survive", () => {
+ expect(
+ payloadOf(build({ auth_type: "oauth2", credentials: { client_id: "c", scopes: ["read", "", null] } }))
+ .credentials,
+ ).toEqual({ client_id: "c", scopes: ["read"] });
+ expect(payloadOf(build({ auth_type: "oauth2", credentials: { client_id: "c", scopes: [] } })).credentials).toEqual({
+ client_id: "c",
+ });
+ });
+
+ it("omits credentials entirely for an auth type that needs none", () => {
+ const payload = payloadOf(build({ auth_type: "none", credentials: { auth_value: "stale" } }));
+ expect(payload).not.toHaveProperty("credentials");
+ });
+
+ it.each([["true_passthrough"], ["oauth_delegate"]])(
+ "persists only the declared app for %s, never minted token material",
+ (authType) => {
+ const payload = payloadOf(
+ build({
+ auth_type: authType,
+ credentials: {
+ client_id: "org-app",
+ client_secret: "org-secret",
+ access_token: "upstream-tok",
+ refresh_token: "refresh-tok",
+ expires_in: 3600,
+ scope: "read",
+ },
+ }),
+ );
+ expect(payload.credentials).toEqual({ client_id: "org-app", client_secret: "org-secret" });
+ expect(JSON.stringify(payload)).not.toContain("upstream-tok");
+ expect(JSON.stringify(payload)).not.toContain("refresh-tok");
+ },
+ );
+
+ it("merges the DCR-minted client into an oauth2 payload", () => {
+ const payload = payloadOf(
+ build(
+ { auth_type: "oauth2", credentials: { access_token: "tok" } },
+ { dcrClient: { client_id: "dcr-id", client_secret: "dcr-secret" } },
+ ),
+ );
+ expect(payload.credentials).toMatchObject({
+ client_id: "dcr-id",
+ client_secret: "dcr-secret",
+ access_token: "tok",
+ });
+ });
+
+ it("never leaks the DCR-minted client onto a non-oauth2 server", () => {
+ const payload = payloadOf(
+ build({ auth_type: "true_passthrough" }, { dcrClient: { client_id: "dcr-id", client_secret: "dcr-secret" } }),
+ );
+ expect(JSON.stringify(payload)).not.toContain("dcr-id");
+ });
+});
+
+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);
+ });
+
+ it.each([["true_passthrough"], ["oauth_delegate"]])("honours an explicit dcr_bridge false for %s", (authType) => {
+ expect(payloadOf(build({ auth_type: authType, dcr_bridge: false })).dcr_bridge).toBe(false);
+ });
+
+ it.each([["none"], ["api_key"], ["oauth2"]])(
+ "forces dcr_bridge off for %s even when the form still holds true",
+ (authType) => {
+ expect(payloadOf(build({ auth_type: authType, dcr_bridge: true })).dcr_bridge).toBe(false);
+ },
+ );
+
+ it("stamps the interactive oauth2 flow by default", () => {
+ expect(payloadOf(build({ auth_type: "oauth2" })).oauth2_flow).toBe("authorization_code");
+ });
+
+ it("stamps client_credentials for an M2M oauth2 server", () => {
+ expect(payloadOf(build({ auth_type: "oauth2", oauth_flow_type: "m2m" })).oauth2_flow).toBe("client_credentials");
+ });
+
+ it("sends no oauth2_flow for a non-oauth2 server", () => {
+ expect(payloadOf(build({ auth_type: "api_key", oauth_flow_type: "m2m" }))).not.toHaveProperty("oauth2_flow");
+ });
+
+ it.each([["allow_all_keys"], ["available_on_public_internet"], ["delegate_auth_to_upstream"], ["oauth_passthrough"]])(
+ "coerces %s to a boolean",
+ (key) => {
+ expect(payloadOf(build({ auth_type: "none" }))[key]).toBe(false);
+ expect(payloadOf(build({ auth_type: "none", [key]: true }))[key]).toBe(true);
+ },
+ );
+});
+
+describe("buildCreateServerPayload tool allowlist", () => {
+ it("marks the allowlist enforced once the admin has touched it, even with nothing selected", () => {
+ const payload = payloadOf(build({ auth_type: "none" }, { hasToolAllowlistInteraction: true }));
+ expect((payload.mcp_info as Record).tool_allowlist_enforced).toBe(true);
+ expect(payload.allowed_tools).toEqual([]);
+ });
+
+ it("marks the allowlist enforced when tools are selected without an explicit interaction", () => {
+ const payload = payloadOf(build({ auth_type: "none" }, { allowedTools: ["search"] }));
+ expect((payload.mcp_info as Record).tool_allowlist_enforced).toBe(true);
+ expect(payload.allowed_tools).toEqual(["search"]);
+ });
+
+ it("leaves the allowlist unenforced when untouched and empty", () => {
+ const payload = payloadOf(build({ auth_type: "none" }));
+ expect((payload.mcp_info as Record).tool_allowlist_enforced).toBe(false);
+ });
+});
+
+describe("buildCreateServerPayload mcp_info", () => {
+ it("sends a null cost map when nothing is configured and the map when it is", () => {
+ expect(
+ (payloadOf(build({ auth_type: "none" })).mcp_info as Record).mcp_server_cost_info,
+ ).toBeNull();
+ const priced = payloadOf(build({ auth_type: "none" }, { costConfig: { default_cost_per_query: 0.01 } }));
+ expect((priced.mcp_info as Record).mcp_server_cost_info).toEqual({ default_cost_per_query: 0.01 });
+ });
+
+ it("carries the selected logo and drops the raw stdio_config field", () => {
+ const payload = payloadOf(build({ auth_type: "none", stdio_config: "{}" }, { logoUrl: "https://cdn/logo.png" }));
+ expect((payload.mcp_info as Record).logo_url).toBe("https://cdn/logo.png");
+ expect(payload.stdio_config).toBeUndefined();
+ });
+});