From 81895d7bc1862861898fb0639eafe10fc4879353 Mon Sep 17 00:00:00 2001 From: Mrinal Chanshetty Date: Tue, 3 Nov 2026 14:35:27 -0800 Subject: [PATCH] fix(ui): keep provider default restrictions and mark toolset provider tools Provider servers resolve their tools synchronously, so every default write in one pass read the same stale permissions ref and the last one silently dropped the others' delete-blocked defaults. Advance the ref before each write. The toolset editor now tags provider-native servers as permission only, and the usage guide says their tools are not served at the toolset URL. --- .../MCPToolsetsTab.integration.test.tsx | 1 + .../_components/MCPToolsetsTab.tsx | 8 +++++ .../MCPToolPermissions.integration.test.tsx | 32 +++++++++++++++++++ .../MCPToolPermissions.tsx | 9 +++--- ui/litellm-dashboard/src/i18n/locales/en.json | 5 +-- 5 files changed, 49 insertions(+), 6 deletions(-) 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" },