diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.test.ts index 3ef8e88b2c9..d60b52875ba 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.test.ts @@ -4,6 +4,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { mcpServerToolKeys, useMCPServerTools } from "./useMCPServerTools"; +import type { MCPServer } from "@/components/mcp_tools/types"; import * as networking from "@/components/networking"; vi.mock("@/components/networking", () => ({ @@ -116,3 +117,34 @@ describe("useMCPServerTools", () => { }); }); }); + +describe("provider tool choices", () => { + it("reads current registered tools without discovery, including an empty allowlist", async () => { + vi.clearAllMocks(); + vi.mocked(networking.listMCPTools).mockResolvedValue(toolsFor("gateway")); + const provider: MCPServer = { + server_id: "native", + execution_mode: "provider", + allowed_tools: ["search"], + tool_name_to_description: { search: "Search docs" }, + created_at: "", + created_by: "", + updated_at: "", + updated_by: "", + }; + const { result, rerender } = renderHook( + ({ server }) => useMCPServerTools(["native", "gateway"], new Set(["native", "gateway"]), [server]), + { wrapper, initialProps: { server: provider } }, + ); + await waitFor(() => expect(result.current.get("gateway")?.status).toBe("success")); + expect(result.current.get("native")).toEqual({ + tools: [{ name: "search", description: "Search docs" }], + status: "success", + }); + expect(networking.listMCPTools).toHaveBeenCalledTimes(1); + expect(networking.listMCPTools).toHaveBeenCalledWith("test-token-123", "gateway"); + rerender({ server: { ...provider, allowed_tools: [] } }); + expect(result.current.get("native")).toEqual({ tools: [], status: "success" }); + expect(networking.listMCPTools).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.ts index bc4a6696a6e..685457476fb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServerTools.ts @@ -2,6 +2,8 @@ import { useQueries, type QueryObserverResult } from "@tanstack/react-query"; import { createQueryKeys } from "../common/queryKeysFactory"; import { listMCPTools } from "@/components/networking"; import useAuthorized from "../useAuthorized"; +import type { MCPServer } from "@/components/mcp_tools/types"; +import { providerToolChoices } from "@/components/mcp_tools/providerTools"; export const mcpServerToolKeys = createQueryKeys("mcpServerTools"); @@ -48,23 +50,33 @@ const toStatus = ({ export const useMCPServerTools = ( serverIds: readonly string[], enabledServerIds: ReadonlySet, + servers: readonly MCPServer[] = [], ): ReadonlyMap => { const { accessToken } = useAuthorized(); + const providers = new Map( + servers.filter((server) => server.execution_mode === "provider").map((server) => [server.server_id, server]), + ); return useQueries({ queries: serverIds.map((serverId) => ({ queryKey: mcpServerToolKeys.detail(serverId), queryFn: () => fetchServerTools(accessToken!, serverId), - enabled: !!accessToken && enabledServerIds.has(serverId), + enabled: !!accessToken && enabledServerIds.has(serverId) && !providers.has(serverId), staleTime: 5 * 60 * 1000, retry: false, refetchOnWindowFocus: false, })), combine: (results) => new Map( - results.map((result, idx): [string, MCPServerToolsState] => [ - serverIds[idx], - { tools: result.data ?? [], status: toStatus(result) }, - ]), + results.map((result, idx): [string, MCPServerToolsState] => { + const serverId = serverIds[idx]; + const provider = providers.get(serverId); + return [ + serverId, + provider + ? { tools: providerToolChoices(provider), status: "success" } + : { tools: result.data ?? [], status: toStatus(result) }, + ]; + }), ), }); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx index c4790abe8da..434356c5950 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx @@ -54,6 +54,8 @@ import { StdioDisabledBanner, TransportSelectItems } from "./StdioAvailability"; import MCPPermissionManagement from "./MCPPermissionManagement"; import OpenAPIFormSection, { OpenAPIKeyTool } from "./OpenAPIFormSection"; import MCPLogoSelector from "./MCPLogoSelector"; +import ProviderMCPRegistrationForm from "./ProviderMCPRegistrationForm"; +import { Label } from "@/components/ui/label"; import EnvVarsSection from "./EnvVarsSection"; import { isAdminRole } from "@/utils/roles"; import { validateMCPServerUrl, validateMCPServerName } from "./utils"; @@ -86,6 +88,7 @@ interface CreateMCPServerProps { prefillData?: DiscoverableMCPServer | null; onBackToDiscovery?: () => void; stdioEnabled?: boolean; + executionModeSelector?: React.ReactNode; } const payloadErrorMessage = (result: Exclude, t: TFunction): string => { @@ -106,7 +109,7 @@ const CREATE_DEFAULTS: MountedFormValues = { oauth_passthrough: false, }; -const CreateMCPServer: React.FC = ({ +const GatewayCreateMCPServer: React.FC = ({ userID, userRole, accessToken, @@ -118,6 +121,7 @@ const CreateMCPServer: React.FC = ({ prefillData, onBackToDiscovery, stdioEnabled = true, + executionModeSelector, }) => { const { t } = useTranslation(); const form = useForm({ mode: "onChange", defaultValues: CREATE_DEFAULTS }); @@ -654,6 +658,7 @@ const CreateMCPServer: React.FC = ({ + {executionModeSelector}
@@ -1047,4 +1052,70 @@ const CreateMCPServer: React.FC = ({ ); }; +const CreateMCPServer: React.FC = (props) => { + const [executionMode, setExecutionMode] = useState<"litellm" | "provider">("litellm"); + const selectorId = React.useId(); + const { setModalVisible, isModalVisible } = props; + // A parent can close the modal by flipping isModalVisible without going through setVisible, so + // reset on every close; the next open always starts on the gateway form. + const [wasModalVisible, setWasModalVisible] = useState(isModalVisible); + if (wasModalVisible !== isModalVisible) { + setWasModalVisible(isModalVisible); + if (!isModalVisible) setExecutionMode("litellm"); + } + const setVisible = React.useCallback( + (visible: boolean) => { + if (!visible) setExecutionMode("litellm"); + setModalVisible(visible); + }, + [setModalVisible], + ); + const executionModeSelector = isAdminRole(props.userRole) ? ( +
+ + +
+ ) : null; + if (executionMode !== "provider" || !isAdminRole(props.userRole)) { + return ( + + ); + } + return ( + + + + Add New MCP Server + + {executionModeSelector} + setVisible(false)} + onSuccess={(server) => { + setVisible(false); + props.onCreateSuccess(server); + }} + /> + + + ); +}; + export default CreateMCPServer; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx index 62e6782bcfc..68683ee210f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx @@ -58,6 +58,9 @@ const MCPServerCard: FC = ({ stdioEnabled = true, }) => { const { t } = useTranslation(); + const isProvider = server.execution_mode === "provider"; + const recheckHealth = isProvider ? undefined : onRecheckHealth; + const deleteServer = isProvider && server.is_config ? undefined : onDelete; const alias = server.alias || server.server_name || ""; const name = server.server_name || alias || server.server_id; // Logo is sourced exclusively from the admin-set `mcp_info.logo_url`. @@ -115,7 +118,7 @@ const MCPServerCard: FC = ({ } }; - const hasMenu = !!onRecheckHealth || !!onDelete; + const hasMenu = !!recheckHealth || !!deleteServer; // Card uses role="button" + nested