diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx
index 9a3ff0cc6cbe..2f4fcfd5f69c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx
@@ -20,8 +20,12 @@ vi.mock("@/components/networking", () => ({
listMCPUserEnvVarStatus: vi.fn().mockResolvedValue([]),
fetchMCPGatewaySessions: vi.fn(),
terminateMCPGatewaySessions: vi.fn(),
+ getUiConfig: vi.fn().mockResolvedValue({}),
}));
+const stubUiConfig = (config: object) =>
+ vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response(JSON.stringify(config), { status: 200 }));
+
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
@@ -135,6 +139,7 @@ describe("MCPServers", () => {
beforeEach(() => {
vi.clearAllMocks();
+ stubUiConfig({});
});
it("should render the MCPServers component with title", async () => {
@@ -299,6 +304,90 @@ describe("MCPServers", () => {
expect(screen.getByText("No servers match the current filters or search.")).toBeVisible();
});
+ it.each([
+ [false, true],
+ [true, false],
+ ])("marks stdio servers as disabled only when the proxy reports stdio off (enabled=%s)", async (enabled, flagged) => {
+ stubUiConfig({ mcp_stdio_enabled: enabled });
+ vi.mocked(networking.fetchMCPServers).mockResolvedValue([
+ {
+ server_id: "stdio-1",
+ server_name: "local_tools",
+ alias: "local_tools",
+ transport: "stdio",
+ command: "python",
+ args: ["server.py"],
+ created_by: "user",
+ updated_by: "user",
+ } as MCPServer,
+ ]);
+ vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]);
+
+ render(
+
+
+ ,
+ );
+
+ const grid = await screen.findByTestId("mcp-servers-grid");
+ await waitFor(() => expect(globalThis.fetch).toHaveBeenCalled());
+ await waitFor(() => expect(within(grid).queryByText("stdio disabled") !== null).toBe(flagged));
+ expect(within(grid).getByText("STDIO")).toBeInTheDocument();
+ });
+
+ const stdioServer = {
+ server_id: "stdio-1",
+ server_name: "local_tools",
+ alias: "local_tools",
+ transport: "stdio",
+ command: "python",
+ args: ["server.py"],
+ created_by: "user",
+ updated_by: "user",
+ } as MCPServer;
+
+ it("greys out stdio in the create form when the proxy reports stdio off", async () => {
+ stubUiConfig({ mcp_stdio_enabled: false });
+ vi.mocked(networking.fetchMCPServers).mockResolvedValue([]);
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+ await waitFor(() => expect(globalThis.fetch).toHaveBeenCalled());
+
+ await user.click(await screen.findByRole("button", { name: "+ Submit MCP Server" }));
+ await user.click(await screen.findByRole("combobox", { name: /Transport Type/ }));
+
+ expect(await screen.findByRole("option", { name: /Standard Input\/Output \(stdio\)/ })).toHaveAttribute(
+ "data-disabled",
+ );
+ expect(screen.getByRole("option", { name: /Streamable HTTP/ })).not.toHaveAttribute("data-disabled");
+
+ await user.keyboard("{Escape}");
+ await waitFor(() => expect(screen.queryByRole("listbox")).not.toBeInTheDocument());
+ });
+
+ it("explains on the edit page why an existing stdio server cannot run when the proxy reports stdio off", async () => {
+ stubUiConfig({ mcp_stdio_enabled: false });
+ vi.mocked(networking.fetchMCPServers).mockResolvedValue([stdioServer]);
+ vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]);
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+ const grid = await screen.findByTestId("mcp-servers-grid");
+ await waitFor(() => expect(within(grid).getByText("stdio disabled")).toBeInTheDocument());
+
+ await user.click(within(grid).getAllByText("local_tools")[0]);
+ await user.click(await screen.findByRole("tab", { name: "Settings" }));
+
+ expect(await screen.findByText("stdio is disabled on this proxy")).toBeInTheDocument();
+ });
+
it("should render mocked MCP servers data in the table", async () => {
// Mock MCP servers data
const mockServers = [
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx
index 56a18e0ca4c6..13510cadf39c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx
@@ -29,6 +29,7 @@ import CreateMCPServer from "./CreateMCPServer";
import ImportMCPServers from "./ImportMCPServers";
import MCPConnect from "./mcp_connect";
import MCPServerCard from "./MCPServerCard";
+import { useMcpStdioEnabled } from "./StdioAvailability";
import { MCPServerView } from "./mcp_server_view";
import type {
DiscoverableMCPServer,
@@ -225,6 +226,8 @@ const MCPServers: React.FC
= ({ accessToken, userRole, userID, i
const [sortKey, setSortKey] = useState("created_desc");
const isInternalUser = userRole === "Internal User";
+ const stdioEnabled = useMcpStdioEnabled();
+
// Single bulk fetch of this user's per-server env-var status. Drives the
// red "N user fields missing" footer on each card with no per-row request.
const { data: envVarStatuses, refetch: refetchEnvVarStatus } = useQuery({
@@ -500,6 +503,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID, i
availableAccessGroups={uniqueMcpAccessGroups}
existingServers={mcpServers}
prefillData={prefillData}
+ stdioEnabled={stdioEnabled}
onBackToDiscovery={() => {
setModalVisible(false);
setPrefillData(null);
@@ -614,6 +618,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID, i
availableAccessGroups={uniqueMcpAccessGroups}
existingServers={mcpServers}
initialTabIndex={selectedServerId === toolsTabServerId ? 1 : 0}
+ stdioEnabled={stdioEnabled}
/>
) : (
@@ -752,6 +757,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID, i
onByokConnect={server.is_byok ? () => setByokModalServer(server) : undefined}
onOpenFillFields={() => setEnvVarsModalServer(server)}
onDelete={isAdminRole(userRole) ? () => handleDelete(server.server_id) : undefined}
+ stdioEnabled={stdioEnabled}
/>
))}
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 97d1782b08ca..a8fcd872a4ab 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -370,6 +370,7 @@ export interface LiteLLMWellKnownUiConfig {
hide_default_credentials_hint?: boolean;
is_control_plane?: boolean;
workers?: WorkerInfo[];
+ mcp_stdio_enabled?: boolean;
}
export interface CredentialsResponse {
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index ebc6d0e70ccf..b47689d18518 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -46110,6 +46110,11 @@ export interface components {
* @default false
*/
is_control_plane: boolean;
+ /**
+ * Mcp Stdio Enabled
+ * @default false
+ */
+ mcp_stdio_enabled: boolean;
/** Proxy Base Url */
proxy_base_url: string | null;
/** Server Root Path */