;
mcpToolsets?: string[];
- inheritedMcpServers?: string[];
+ inheritedMcpServers?: InheritedGrant[];
accessToken?: string | null;
}
-const INHERITED_MCP_SERVER_TOOLTIP = "Granted through one of the team's access groups";
-
export function MCPServerPermissions({
mcpServers,
mcpAccessGroups = [],
@@ -57,8 +56,8 @@ export function MCPServerPermissions({
const directServerIds = mcpServers.filter(
(server) => server !== NO_MCP_SERVERS_SENTINEL && server !== ALL_PROXY_MCP_SERVERS_SENTINEL,
);
- const inheritedOnlyServerIds = inheritedMcpServers.filter((server) => !mcpServers.includes(server));
- const serverIdCount = directServerIds.length + inheritedOnlyServerIds.length;
+ const inheritedOnlyServers = inheritedMcpServers.filter((grant) => !mcpServers.includes(grant.id));
+ const serverIdCount = directServerIds.length + inheritedOnlyServers.length;
// Fetch MCP server details when component mounts
useEffect(() => {
@@ -109,9 +108,13 @@ export function MCPServerPermissions({
const grantsAllProxyMcpServers = mcpServers.includes(ALL_PROXY_MCP_SERVERS_SENTINEL);
const mergedItems = [
- ...directServerIds.map((server) => ({ type: "server", value: server, inherited: false })),
- ...inheritedOnlyServerIds.map((server) => ({ type: "server", value: server, inherited: true })),
- ...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group, inherited: false })),
+ ...directServerIds.map((server) => ({ type: "server", value: server, tooltip: `Full ID: ${server}` })),
+ ...inheritedOnlyServers.map((grant) => ({
+ type: "server",
+ value: grant.id,
+ tooltip: inheritedGrantTooltip(grant),
+ })),
+ ...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group, tooltip: "" })),
];
const totalCount = mergedItems.length + mcpToolsets.length;
@@ -160,17 +163,8 @@ export function MCPServerPermissions({
{getMCPServerDisplayName(item.value)}
- {item.inherited && (
-
- Inherited
-
- )}
-
- {item.inherited
- ? `${INHERITED_MCP_SERVER_TOOLTIP}. Full ID: ${item.value}`
- : `Full ID: ${item.value}`}
-
+ {item.tooltip}
) : (
diff --git a/ui/litellm-dashboard/src/components/permissions/inheritedGrants.test.ts b/ui/litellm-dashboard/src/components/permissions/inheritedGrants.test.ts
new file mode 100644
index 00000000000..766f8f23f0b
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/permissions/inheritedGrants.test.ts
@@ -0,0 +1,61 @@
+import { describe, expect, it } from "vitest";
+import { computeInheritedGrants, inheritedGrantTooltip } from "./inheritedGrants";
+import { TeamAccessGroupModelGrant } from "../team/teamModelAccess";
+
+const GRANTS: TeamAccessGroupModelGrant[] = [
+ { access_group_id: "ag-1", access_group_name: "platform-tools", models: [], mcp_server_ids: ["mcp-1", "mcp-2"] },
+ {
+ access_group_id: "ag-2",
+ access_group_name: "support",
+ models: [],
+ mcp_server_ids: ["mcp-2"],
+ agent_ids: ["agent-1"],
+ },
+];
+
+describe("computeInheritedGrants", () => {
+ it("attributes each id to every group that grants it, in group order", () => {
+ expect(computeInheritedGrants(["mcp-1", "mcp-2"], GRANTS, (g) => g.mcp_server_ids)).toEqual([
+ { id: "mcp-1", accessGroupNames: ["platform-tools"] },
+ { id: "mcp-2", accessGroupNames: ["platform-tools", "support"] },
+ ]);
+ });
+
+ it("keeps ids the flat list carries but no group detail explains, with no group names", () => {
+ expect(computeInheritedGrants(["agent-1", "agent-legacy"], GRANTS, (g) => g.agent_ids)).toEqual([
+ { id: "agent-1", accessGroupNames: ["support"] },
+ { id: "agent-legacy", accessGroupNames: [] },
+ ]);
+ });
+
+ it("falls back to the group details when the flat list is missing, without duplicates", () => {
+ expect(computeInheritedGrants(undefined, GRANTS, (g) => g.mcp_server_ids).map((g) => g.id)).toEqual([
+ "mcp-1",
+ "mcp-2",
+ ]);
+ });
+
+ it("returns nothing when neither source has ids", () => {
+ expect(computeInheritedGrants(undefined, undefined, (g) => g.agent_ids)).toEqual([]);
+ });
+});
+
+describe("inheritedGrantTooltip", () => {
+ it("names a single group", () => {
+ expect(inheritedGrantTooltip({ id: "mcp-1", accessGroupNames: ["platform-tools"] })).toBe(
+ "Granted via access group platform-tools. Full ID: mcp-1",
+ );
+ });
+
+ it("lists several groups", () => {
+ expect(inheritedGrantTooltip({ id: "mcp-2", accessGroupNames: ["platform-tools", "support"] })).toBe(
+ "Granted via access groups platform-tools, support. Full ID: mcp-2",
+ );
+ });
+
+ it("stays generic when the proxy did not say which group granted it", () => {
+ expect(inheritedGrantTooltip({ id: "agent-legacy", accessGroupNames: [] })).toBe(
+ "Granted via an access group. Full ID: agent-legacy",
+ );
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/permissions/inheritedGrants.ts b/ui/litellm-dashboard/src/components/permissions/inheritedGrants.ts
new file mode 100644
index 00000000000..fb78de5ed77
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/permissions/inheritedGrants.ts
@@ -0,0 +1,26 @@
+import { describeGroups, TeamAccessGroupModelGrant } from "../team/teamModelAccess";
+
+export interface InheritedGrant {
+ id: string;
+ accessGroupNames: string[];
+}
+
+export function computeInheritedGrants(
+ ids: string[] | undefined,
+ grants: TeamAccessGroupModelGrant[] | undefined,
+ idsOf: (grant: TeamAccessGroupModelGrant) => string[] | undefined,
+): InheritedGrant[] {
+ const known = grants ?? [];
+ const allIds = [...new Set([...(ids ?? []), ...known.flatMap((grant) => idsOf(grant) ?? [])])];
+ return allIds.map((id) => ({
+ id,
+ accessGroupNames: known
+ .filter((grant) => (idsOf(grant) ?? []).includes(id))
+ .map((grant) => grant.access_group_name),
+ }));
+}
+
+export const inheritedGrantTooltip = (grant: InheritedGrant): string => {
+ const source = grant.accessGroupNames.length > 0 ? describeGroups(grant.accessGroupNames) : "an access group";
+ return `Granted via ${source}. Full ID: ${grant.id}`;
+};
diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
index 40a3d2ada1a..ae78ac06a9c 100644
--- a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
@@ -305,7 +305,8 @@ describe("TeamInfoView", () => {
);
});
- it("shows MCP servers and agents inherited from access groups in the Object Permissions card", async () => {
+ it("shows MCP servers and agents inherited from access groups in the Object Permissions card, naming the group on hover", async () => {
+ const user = userEvent.setup();
vi.mocked(networking.fetchMCPServers).mockResolvedValue([
{ server_id: "mcp-github-1234", server_name: "github", alias: "github" },
]);
@@ -318,16 +319,33 @@ describe("TeamInfoView", () => {
access_group_ids: ["ag-1"],
access_group_mcp_server_ids: ["mcp-github-1234"],
access_group_agent_ids: ["agent-support-5678"],
+ access_group_details: [
+ {
+ access_group_id: "ag-1",
+ access_group_name: "platform-tools",
+ models: [],
+ mcp_server_ids: ["mcp-github-1234"],
+ agent_ids: ["agent-support-5678"],
+ },
+ ],
}),
);
renderWithProviders();
- expect(await screen.findByText(/github \(mcp\.\.\.1234\)/)).toBeInTheDocument();
- expect(await screen.findByText(/support_agent \(age\.\.\.5678\)/)).toBeInTheDocument();
- expect(screen.getAllByText("Inherited")).toHaveLength(2);
+ const serverRow = await screen.findByText(/github \(mcp\.\.\.1234\)/);
+ const agentRow = await screen.findByText(/support_agent \(age\.\.\.5678\)/);
expect(screen.queryByText("No MCP servers, access groups, or toolsets configured")).not.toBeInTheDocument();
expect(screen.queryByText("No agents or access groups configured")).not.toBeInTheDocument();
+
+ await user.hover(serverRow);
+ expect(
+ await screen.findByText("Granted via access group platform-tools. Full ID: mcp-github-1234"),
+ ).toBeInTheDocument();
+ await user.hover(agentRow);
+ expect(
+ await screen.findByText("Granted via access group platform-tools. Full ID: agent-support-5678"),
+ ).toBeInTheDocument();
});
it("keeps the all-proxy-models badge non-clickable", async () => {
diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
index 7ebef0691e6..3f6d6a96972 100644
--- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
@@ -58,6 +58,7 @@ import {
TeamModelBadge,
TeamModelBadgeKind,
} from "./teamModelAccess";
+import { computeInheritedGrants } from "../permissions/inheritedGrants";
import MetadataKeyValueFields, {
metadataObjectToPairs,
metadataPairsSchema,
@@ -936,6 +937,17 @@ const TeamInfoView: React.FC = ({
const { team_info: info } = teamData;
+ const inheritedMcpServers = computeInheritedGrants(
+ info.access_group_mcp_server_ids,
+ info.access_group_details,
+ (grant) => grant.mcp_server_ids,
+ );
+ const inheritedAgents = computeInheritedGrants(
+ info.access_group_agent_ids,
+ info.access_group_details,
+ (grant) => grant.agent_ids,
+ );
+
const initialKillSwitchOn = info.metadata?.disable_global_guardrails === true;
const allGuardrails: GuardrailListItem[] = guardrailsData?.guardrails ?? [];
@@ -1035,8 +1047,8 @@ const TeamInfoView: React.FC = ({
@@ -1889,8 +1901,8 @@ const TeamInfoView: React.FC = ({
0 ? models : [NO_DEFAULT_MODELS];
}
-const describeGroups = (names: string[]): string =>
+export const describeGroups = (names: string[]): string =>
names.length > 1 ? `access groups ${names.join(", ")}` : `access group ${names[0]}`;
export function computeTeamModelBadges(