From 812b93df960c686e3c58658fb2189027a6fb7454 Mon Sep 17 00:00:00 2001 From: mrinal Date: Tue, 3 Nov 2026 17:52:40 +0000 Subject: [PATCH 1/4] feat(ui): manage provider MCP registrations and tool permissions Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../mcpServers/useMCPServerTools.test.ts | 32 ++++ .../hooks/mcpServers/useMCPServerTools.ts | 22 ++- .../_components/CreateMCPServer.tsx | 66 ++++++- .../mcp-servers/_components/MCPServerCard.tsx | 67 ++++--- .../MCPToolsetsTab.integration.test.tsx | 48 +++++ .../_components/MCPToolsetsTab.tsx | 2 +- ...oviderMCPRegistration.integration.test.tsx | 137 ++++++++++++++ .../ProviderMCPRegistrationForm.tsx | 161 ++++++++++++++++ .../_components/ProviderMCPTools.tsx | 33 ++++ .../_components/mcp_server_edit.tsx | 10 +- .../_components/mcp_server_view.tsx | 173 +++++++++++------- .../providerRegistrationForm.test.ts | 92 ++++++++++ .../_components/providerRegistrationForm.ts | 79 ++++++++ ...> MCPToolPermissions.integration.test.tsx} | 38 ++++ .../MCPToolPermissions.tsx | 26 ++- .../src/components/mcp_tools/providerTools.ts | 4 + .../src/components/mcp_tools/types.tsx | 1 + 17 files changed, 881 insertions(+), 110 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ProviderMCPRegistration.integration.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ProviderMCPRegistrationForm.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ProviderMCPTools.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/providerRegistrationForm.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/providerRegistrationForm.ts rename ui/litellm-dashboard/src/components/mcp_server_management/{MCPToolPermissions.test.tsx => MCPToolPermissions.integration.test.tsx} (96%) create mode 100644 ui/litellm-dashboard/src/components/mcp_tools/providerTools.ts 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 14a6e3d941f..a1594442159 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}
@@ -1044,4 +1049,63 @@ const CreateMCPServer: React.FC = ({ ); }; +const CreateMCPServer: React.FC = (props) => { + const [executionMode, setExecutionMode] = useState<"litellm" | "provider">("litellm"); + const selectorId = React.useId(); + const { setModalVisible } = props; + 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 6524985b74a..59edde1dbb5 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,8 @@ const MCPServerCard: FC = ({ stdioEnabled = true, }) => { const { t } = useTranslation(); + const isProvider = server.execution_mode === "provider"; + const recheckHealth = isProvider ? undefined : onRecheckHealth; 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 +117,7 @@ const MCPServerCard: FC = ({ } }; - const hasMenu = !!onRecheckHealth || !!onDelete; + const hasMenu = !!recheckHealth || !!onDelete; // Card uses role="button" + nested