diff --git a/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.test.ts index 3382c353612..bd81a9b30c7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.test.ts @@ -337,6 +337,41 @@ describe("customer payload", () => { }); }); + it("drops a shared-alias tool grant once no server it names is selected", () => { + const sharedServer = { ...server, server_id: "server-a", alias: "shared" }; + const anotherSharedServer = { ...server, server_id: "server-b", alias: "shared" }; + const keptServer = { ...server, server_id: "server-c" }; + const stored = { + ...customer, + object_permission: { + ...permission, + mcp_servers: [sharedServer.server_id, keptServer.server_id], + mcp_access_groups: [], + mcp_toolsets: [], + mcp_tool_permissions: { shared: ["read"] }, + }, + }; + const values = { + ...toCustomerFormValues(stored), + mcp_servers_and_groups: { servers: [keptServer.server_id], accessGroups: [], toolsets: [] }, + }; + + expect( + buildCustomerPayload(values, stored, [sharedServer, anotherSharedServer, keptServer], []).object_permission, + ).toEqual({ + mcp_servers: [keptServer.server_id], + mcp_access_groups: [], + mcp_toolsets: [], + mcp_tool_permissions: {}, + }); + }); + + it("keeps the stored ID of an existing customer exactly as stored", () => { + const stored = { ...customer, user_id: " customer-id " }; + + expect(buildCustomerPayload(toCustomerFormValues(stored), stored, [], []).user_id).toBe(" customer-id "); + }); + it("sends an empty models list when clearing the selection on update", () => { const stored = { ...customer, models: ["model-id"] }; const values = { ...toCustomerFormValues(stored), models: [] }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.ts b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.ts index f5cd99191c4..75047eb7d49 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerPayload.ts @@ -87,8 +87,14 @@ const extractEditedMcpGrant = ( mcp_tool_permissions: storedGrant.mcp_tool_permissions, }; const previousPermissions = extractMcpEntitlement(previousValues, servers, toolsets)?.mcp_tool_permissions ?? {}; + // A stored tool-permission key that no stored server, group or toolset reaches is itself a server grant, so it + // survives edits to the selection. A key normalization rewrote into server ids is not one of these const independentKeys = new Set( - Object.keys(storedGrant.mcp_tool_permissions).filter((key) => !Object.hasOwn(previousPermissions, key)), + Object.keys(storedGrant.mcp_tool_permissions).filter( + (key) => + !Object.hasOwn(previousPermissions, key) && + !mcpServersForIdentifier(servers, key).some((server) => Object.hasOwn(previousPermissions, server.server_id)), + ), ); const independentServerIds = new Set( Array.from(independentKeys).flatMap((key) => @@ -136,7 +142,7 @@ export const buildCustomerPayload = ( ): UpdateCustomerRequest => { const grant = changedMcpGrant(values, existing, servers, toolsets); return { - user_id: values.user_id.trim(), + user_id: existing?.user_id ?? values.user_id.trim(), ...optionalCustomerFields(values, existing), ...(existing || values.models.length > 0 ? { models: values.models } : {}), ...(grant ? { object_permission: grant } : {}),