diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.integration.test.tsx index 280c08ed539..b334f8d4d44 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.integration.test.tsx @@ -458,6 +458,7 @@ it("creates a provider toolset from registration choices without MCP discovery", await openCreate(user); fireEvent.change(screen.getByPlaceholderText("e.g. github-linear-tools"), { target: { value: "native_research" } }); await user.click(await screen.findByText("native_docs")); + expect(screen.getByText("provider · permission only")).toBeInTheDocument(); await user.click(await screen.findByRole("checkbox", { name: /^search/ })); await user.click(screen.getByRole("button", { name: "Create Toolset" })); await waitFor(() => diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx index ed4172bcd10..ff0caa27fdd 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx @@ -133,6 +133,7 @@ const selectedToolIssue = ( interface ServerToolGroupProps { serverId: string; serverName: string; + providerNative: boolean; state: MCPServerToolsState; visibleTools: readonly MCPServerToolEntry[]; expanded: boolean; @@ -146,6 +147,7 @@ interface ServerToolGroupProps { function ServerToolGroup({ serverId, serverName, + providerNative, state, visibleTools, expanded, @@ -235,6 +237,11 @@ function ServerToolGroup({ > {serverName} + {providerNative && ( + + {t("mcpServers.toolsetsTab.serverGroup.providerPermissionOnly")} + + )} {selectedCount > 0 && ( {t("mcpServers.toolsetsTab.serverGroup.selectedCount", { count: selectedCount })} @@ -429,6 +436,7 @@ function CreateToolsetModal({ open, onClose, onSave, initialToolset }: CreateToo key={server.server_id} serverId={server.server_id} serverName={serverName} + providerNative={server.execution_mode === "provider"} state={state} visibleTools={visibleTools} expanded={searchActive || expandedServerIds.has(server.server_id)} diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.integration.test.tsx b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.integration.test.tsx index 95dd9c8a9c5..5137c89ed0a 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.integration.test.tsx @@ -1154,3 +1154,35 @@ it("selects stored provider tools and reflects registration edits without discov expect(screen.getByRole("checkbox", { name: "fetch" })).toBeInTheDocument(); expect(networking.listMCPTools).not.toHaveBeenCalled(); }); + +it("keeps the delete-blocked default for every provider server that loads in the same pass", async () => { + vi.clearAllMocks(); + testQueryClient.clear(); + vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); + vi.mocked(networking.fetchMCPToolsets).mockResolvedValue([]); + const provider = (server_id: string, allowed_tools: string[]) => ({ + server_id, + server_name: server_id, + execution_mode: "provider" as const, + allowed_tools, + created_at: "", + created_by: "", + updated_at: "", + updated_by: "", + }); + vi.mocked(networking.fetchMCPServers).mockResolvedValue([ + provider("docs", ["search", "delete"]), + provider("files", ["read", "purge"]), + ]); + const changed = vi.fn(); + renderWithProviders( + , + ); + await waitFor(() => expect(changed).toHaveBeenCalledTimes(2)); + expect(changed).toHaveBeenLastCalledWith({ docs: ["search"], files: ["read"] }, "default"); +}); diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx index acefbe50efd..240ff26b24b 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx @@ -172,10 +172,11 @@ const MCPToolPermissions: React.FC = ({ const nonDeleteTools = fetchedTools .filter((t) => classifyToolOp(t.name, t.description || "") !== "delete") .map((t) => t.name); - onChange( - applyToolPermissionWrite({ toolPermissions: latestPermissions, entry, allowed: nonDeleteTools }), - "default", - ); + const next = applyToolPermissionWrite({ toolPermissions: latestPermissions, entry, allowed: nonDeleteTools }); + // Provider servers resolve synchronously in one pass, before the parent re-renders, + // so advance the ref here or the next server's default overwrites this one. + toolPermissionsRef.current = next; + onChange(next, "default"); } } } catch (err) { diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json index 2bc1392d883..39f2a5dab18 100644 --- a/ui/litellm-dashboard/src/i18n/locales/en.json +++ b/ui/litellm-dashboard/src/i18n/locales/en.json @@ -10259,7 +10259,8 @@ "noTools": "No tools found for this server.", "selectMatching": "Select matching tools on {{serverName}}", "selectAll": "Select all tools on {{serverName}}", - "selectedCount": "{{count}} selected" + "selectedCount": "{{count}} selected", + "providerPermissionOnly": "provider · permission only" }, "modal": { "editTitle": "Edit Toolset", @@ -10295,7 +10296,7 @@ }, "usageGuide": { "title": "How toolsets work", - "body": "Create a toolset, assign it to a key via API Keys → Edit Key → MCP Servers, then point your MCP client at the toolset URL. The client only sees the tools you picked.", + "body": "Create a toolset, assign it to a key via API Keys → Edit Key → MCP Servers, then point your MCP client at the toolset URL. The client only sees the tools you picked. Tools from provider-native registrations are permission grants for provider-native requests only and are not served at the toolset URL.", "configLabel": "Claude Code / Cursor config", "copy": "copy" },