diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.test.tsx
index 888f3066252..02b7e3af09c 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.test.tsx
@@ -91,6 +91,47 @@ describe("OAuthFormFields", () => {
});
});
+ describe("token endpoint auth method selector", () => {
+ it("renders directly below the Token URL field in interactive mode", () => {
+ render(
+
+
+ ,
+ );
+ const tokenUrlLabel = screen.getByText("Token URL (optional)");
+ const authMethodLabel = screen.getByText("Token Endpoint Auth Method (optional)");
+ expect(tokenUrlLabel.compareDocumentPosition(authMethodLabel) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ });
+
+ it("renders directly below the Token URL field in M2M mode", () => {
+ render(
+
+
+ ,
+ );
+ const tokenUrlLabel = screen.getByText("Token URL");
+ const authMethodLabel = screen.getByText("Token Endpoint Auth Method (optional)");
+ expect(tokenUrlLabel.compareDocumentPosition(authMethodLabel) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ });
+
+ it("offers client_secret_basic and client_secret_post options", async () => {
+ render(
+
+
+ ,
+ );
+ const authMethodLabel = screen.getByText("Token Endpoint Auth Method (optional)");
+ const selector = authMethodLabel.closest(".ant-form-item")!.querySelector(".ant-select-selector")!;
+ await act(async () => {
+ fireEvent.mouseDown(selector);
+ });
+ await waitFor(() => {
+ expect(screen.getByText("Client Secret Basic")).toBeInTheDocument();
+ expect(screen.getByText("Client Secret Post")).toBeInTheDocument();
+ });
+ });
+ });
+
// ── token_validation_json inline JSON validator ──────────────────────────────
describe("token_validation_json validation", () => {
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx b/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx
index 27d90d5ae1a..93afefe4358 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx
@@ -3,6 +3,7 @@ import { Form, Input, InputNumber, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, TextInput } from "@tremor/react";
import { OAUTH_FLOW } from "./types";
+import TokenEndpointAuthMethodField from "./TokenEndpointAuthMethodField";
interface OAuthFlowStatus {
startOAuthFlow: () => void;
@@ -101,6 +102,7 @@ const OAuthFormFields: React.FC = ({
>
+
= ({
>
+
= ({ isEditing = false }) => (
+
+ Token Endpoint Auth Method (optional)
+
+
+
+
+ }
+ name={["credentials", "token_endpoint_auth_method"]}
+ >
+
+
+);
+
+export default TokenEndpointAuthMethodField;
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx
index e70548d6a96..80120b5303c 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import * as networking from "../networking";
import { setToken } from "@/utils/mcpTokenStore";
import CreateMCPServer from "./create_mcp_server";
+import { selectAntOption } from "./testUtils";
vi.mock("../networking", () => ({
createMCPServer: vi.fn(),
@@ -88,45 +89,6 @@ const defaultProps = {
/** Helper: get the server_name input by its Ant Form id */
const getServerNameInput = () => document.getElementById("server_name") as HTMLInputElement;
-/** Helper: select a dropdown option by opening a select near a label and clicking an option */
-async function selectAntOption(labelText: string, optionText: string) {
- const label = screen.getByText(labelText);
- // First try to find a .ant-form-item ancestor (standard form fields)
- let select: Element | null = null;
- const formItem = label.closest(".ant-form-item");
- if (formItem) {
- select = formItem.querySelector(".ant-select");
- }
- // If not found, try .ant-collapse-content ancestor (auth type is inside a Collapse panel)
- if (!select) {
- const collapseContent = label.closest(".ant-collapse-item");
- if (collapseContent) {
- select = collapseContent.querySelector(".ant-select");
- }
- }
- // Fallback: look for a sibling or nearby select
- if (!select) {
- const parent = label.closest("div");
- select = parent?.querySelector(".ant-select") ?? null;
- }
- act(() => {
- fireEvent.mouseDown(select!.querySelector(".ant-select-selector")!);
- });
-
- await waitFor(() => {
- const options = document.querySelectorAll(".ant-select-item-option");
- expect(options.length).toBeGreaterThan(0);
- });
-
- const option = Array.from(document.querySelectorAll(".ant-select-item-option")).find((el) =>
- el.textContent?.includes(optionText),
- );
- expect(option).toBeTruthy();
- act(() => {
- fireEvent.click(option!);
- });
-}
-
describe("CreateMCPServer", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -534,6 +496,84 @@ describe("CreateMCPServer", () => {
expect(payload.token_validation).toBeUndefined();
});
+ it("includes credentials.token_endpoint_auth_method in payload when client_secret_basic is selected", 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" } });
+ });
+
+ await selectAntOption("Token Endpoint Auth Method (optional)", "Client Secret Basic");
+
+ 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).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();
+ });
+
it("persists access + refresh token to the DB on submit for OBO mode", async () => {
// "Authorize & Fetch" produced a token before submit.
oauthHook.tokenResponse = {
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
index d7c1241b044..8da77120c09 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
@@ -4,6 +4,7 @@ import { render, screen, waitFor, fireEvent, act } from "@testing-library/react"
import MCPServerEdit from "./mcp_server_edit";
import * as networking from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
+import { selectAntOption } from "./testUtils";
vi.mock("../networking", () => ({
updateMCPServer: vi.fn(),
@@ -506,6 +507,68 @@ describe("MCPServerEdit (interactive OAuth)", () => {
expect(payload.token_validation).toEqual({ organization: "my-org" });
});
+ it("includes credentials.token_endpoint_auth_method in update payload when client_secret_basic is selected", async () => {
+ vi.mocked(networking.updateMCPServer).mockResolvedValue(interactiveOAuthServer);
+
+ render(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("Token Endpoint Auth Method (optional)")).toBeInTheDocument();
+ });
+
+ await selectAntOption("Token Endpoint Auth Method (optional)", "Client Secret Basic");
+
+ 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];
+ expect(payload.credentials?.token_endpoint_auth_method).toBe("client_secret_basic");
+ });
+
+ it("omits token_endpoint_auth_method from the update payload when the selector is left blank", async () => {
+ vi.mocked(networking.updateMCPServer).mockResolvedValue(interactiveOAuthServer);
+
+ render(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("Token Endpoint Auth Method (optional)")).toBeInTheDocument();
+ });
+
+ 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];
+ expect(payload.credentials?.token_endpoint_auth_method).toBeUndefined();
+ });
+
it("does not include token_validation in payload when field is empty and server had none", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue(interactiveOAuthServer);
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
index b992313d4a8..86e898809e5 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
@@ -20,6 +20,7 @@ import MCPToolConfiguration from "./mcp_tool_configuration";
import StdioConfiguration from "./StdioConfiguration";
import MCPLogoSelector from "./MCPLogoSelector";
import EnvVarsSection from "./EnvVarsSection";
+import TokenEndpointAuthMethodField from "./TokenEndpointAuthMethodField";
import { validateMCPServerUrl, validateMCPServerName, normalizeEnvVars } from "./utils";
import NotificationsManager from "../molecules/notifications_manager";
import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow";
@@ -996,6 +997,7 @@ const MCPServerEdit: React.FC = ({
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
+
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/testUtils.ts b/ui/litellm-dashboard/src/components/mcp_tools/testUtils.ts
new file mode 100644
index 00000000000..7911c3eb800
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/mcp_tools/testUtils.ts
@@ -0,0 +1,27 @@
+import { act, fireEvent, screen, waitFor } from "@testing-library/react";
+import { expect } from "vitest";
+
+export async function selectAntOption(labelText: string, optionText: string) {
+ const label = screen.getByText(labelText);
+ const select =
+ label.closest(".ant-form-item")?.querySelector(".ant-select") ??
+ label.closest(".ant-collapse-item")?.querySelector(".ant-select") ??
+ label.closest("div")?.querySelector(".ant-select") ??
+ null;
+
+ act(() => {
+ fireEvent.mouseDown(select!.querySelector(".ant-select-selector")!);
+ });
+
+ await waitFor(() => {
+ expect(document.querySelectorAll(".ant-select-item-option").length).toBeGreaterThan(0);
+ });
+
+ const option = Array.from(document.querySelectorAll(".ant-select-item-option")).find((el) =>
+ el.textContent?.includes(optionText),
+ );
+ expect(option).toBeTruthy();
+ act(() => {
+ fireEvent.click(option!);
+ });
+}