diff --git a/ui/litellm-dashboard/src/components/object_permissions_view.tsx b/ui/litellm-dashboard/src/components/object_permissions_view.tsx
index 011791fa18d..7d1c4ce4ac9 100644
--- a/ui/litellm-dashboard/src/components/object_permissions_view.tsx
+++ b/ui/litellm-dashboard/src/components/object_permissions_view.tsx
@@ -6,6 +6,8 @@ import type { ObjectPermission } from "./object_permission_types";
interface ObjectPermissionsViewProps {
objectPermission?: ObjectPermission | null;
+ inheritedMcpServerIds?: string[];
+ inheritedAgentIds?: string[];
variant?: "card" | "inline";
className?: string;
accessToken?: string | null;
@@ -13,6 +15,8 @@ interface ObjectPermissionsViewProps {
export function ObjectPermissionsView({
objectPermission,
+ inheritedMcpServerIds = [],
+ inheritedAgentIds = [],
variant = "card",
className = "",
accessToken,
@@ -34,9 +38,15 @@ export function ObjectPermissionsView({
mcpAccessGroups={mcpAccessGroups}
mcpToolPermissions={mcpToolPermissions}
mcpToolsets={mcpToolsets}
+ inheritedMcpServers={inheritedMcpServerIds}
+ accessToken={accessToken}
+ />
+
-
Search tools
{searchTools.length === 0 ? (
diff --git a/ui/litellm-dashboard/src/components/permissions/AgentPermissions.test.tsx b/ui/litellm-dashboard/src/components/permissions/AgentPermissions.test.tsx
new file mode 100644
index 00000000000..14fe6177ac5
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/permissions/AgentPermissions.test.tsx
@@ -0,0 +1,50 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import AgentPermissions from "./AgentPermissions";
+import * as networking from "../networking";
+
+vi.mock("../networking");
+
+describe("AgentPermissions", () => {
+ const accessToken = "test-token";
+ const agentId = "90337622-756e-4f25-98f0-01fc8174aa24";
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("lists agents inherited from access groups with an Inherited tag and counts them", async () => {
+ vi.mocked(networking.getAgentsList).mockResolvedValue({
+ agents: [{ agent_id: agentId, agent_name: "support_agent" }],
+ });
+
+ render(
);
+
+ expect(await screen.findByText(/support_agent/)).toBeInTheDocument();
+ expect(screen.getByText("Inherited")).toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.queryByText("No agents or access groups configured")).not.toBeInTheDocument();
+ expect(networking.getAgentsList).toHaveBeenCalledWith(accessToken);
+ });
+
+ it("does not double-list an agent that is both granted directly and inherited", async () => {
+ vi.mocked(networking.getAgentsList).mockResolvedValue({
+ agents: [{ agent_id: agentId, agent_name: "support_agent" }],
+ });
+
+ render(
);
+
+ expect(await screen.findByText(/support_agent/)).toBeInTheDocument();
+ expect(screen.getAllByText(/support_agent/)).toHaveLength(1);
+ expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ });
+
+ it("shows the empty state when nothing is granted directly or inherited", () => {
+ render(
);
+
+ expect(screen.getByText("No agents or access groups configured")).toBeInTheDocument();
+ expect(screen.getByText("0")).toBeInTheDocument();
+ expect(networking.getAgentsList).not.toHaveBeenCalled();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/permissions/AgentPermissions.tsx b/ui/litellm-dashboard/src/components/permissions/AgentPermissions.tsx
index d84bd1834f8..21bacc23ce1 100644
--- a/ui/litellm-dashboard/src/components/permissions/AgentPermissions.tsx
+++ b/ui/litellm-dashboard/src/components/permissions/AgentPermissions.tsx
@@ -14,16 +14,26 @@ interface Agent {
interface AgentPermissionsProps {
agents: string[];
agentAccessGroups?: string[];
+ inheritedAgents?: string[];
accessToken?: string | null;
}
-export function AgentPermissions({ agents, agentAccessGroups = [], accessToken }: AgentPermissionsProps) {
+const INHERITED_AGENT_TOOLTIP = "Granted through one of the team's access groups";
+
+export function AgentPermissions({
+ agents,
+ agentAccessGroups = [],
+ inheritedAgents = [],
+ accessToken,
+}: AgentPermissionsProps) {
const [agentDetails, setAgentDetails] = useState
([]);
+ const inheritedOnlyAgents = inheritedAgents.filter((agent) => !agents.includes(agent));
+ const agentIdCount = agents.length + inheritedOnlyAgents.length;
// Fetch agent details when component mounts
useEffect(() => {
const fetchAgentDetails = async () => {
- if (accessToken && agents.length > 0) {
+ if (accessToken && agentIdCount > 0) {
try {
const response = await getAgentsList(accessToken);
if (response && response.agents && Array.isArray(response.agents)) {
@@ -35,7 +45,7 @@ export function AgentPermissions({ agents, agentAccessGroups = [], accessToken }
}
};
fetchAgentDetails();
- }, [accessToken, agents.length]);
+ }, [accessToken, agentIdCount]);
// Function to get display name for agent
const getAgentDisplayName = (agentId: string) => {
@@ -47,10 +57,11 @@ export function AgentPermissions({ agents, agentAccessGroups = [], accessToken }
return agentId;
};
- // Merge agents and access groups into one list
+ // Merge agents, inherited agents and access groups into one list
const mergedItems = [
- ...agents.map((agent) => ({ type: "agent", value: agent })),
- ...agentAccessGroups.map((group) => ({ type: "accessGroup", value: group })),
+ ...agents.map((agent) => ({ type: "agent", value: agent, inherited: false })),
+ ...inheritedOnlyAgents.map((agent) => ({ type: "agent", value: agent, inherited: true })),
+ ...agentAccessGroups.map((group) => ({ type: "accessGroup", value: group, inherited: false })),
];
const totalCount = mergedItems.length;
@@ -76,8 +87,17 @@ export function AgentPermissions({ agents, agentAccessGroups = [], accessToken }
{getAgentDisplayName(item.value)}
+ {item.inherited && (
+
+ Inherited
+
+ )}
- {`Full ID: ${item.value}`}
+
+ {item.inherited
+ ? `${INHERITED_AGENT_TOOLTIP}. Full ID: ${item.value}`
+ : `Full ID: ${item.value}`}
+
) : (
diff --git a/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.test.tsx b/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.test.tsx
index c2df945f367..fe7e3a7b971 100644
--- a/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.test.tsx
+++ b/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.test.tsx
@@ -406,4 +406,47 @@ describe("MCPServerPermissions", () => {
);
await waitFor(() => expect(screen.getByText("Blocked")).toHaveAttribute("data-variant", "destructive"));
});
+
+ it("lists servers inherited from access groups with an Inherited tag and counts them", async () => {
+ vi.mocked(networking.fetchMCPServers).mockResolvedValue([
+ { server_id: mockServerId1, server_name: mockServerName1, alias: mockServerName1 },
+ ]);
+
+ render(
+ ,
+ );
+
+ expect(await screen.findByText(/DW_MCP/)).toBeInTheDocument();
+ expect(screen.getByText("Inherited")).toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.queryByText("No MCP servers, access groups, or toolsets configured")).not.toBeInTheDocument();
+ expect(networking.fetchMCPServers).toHaveBeenCalledWith(mockAccessToken);
+ });
+
+ it("does not double-list a server that is both granted directly and inherited", async () => {
+ vi.mocked(networking.fetchMCPServers).mockResolvedValue([
+ { server_id: mockServerId2, server_name: mockServerName2, alias: mockServerName2 },
+ ]);
+
+ render(
+ ,
+ );
+
+ expect(await screen.findByText(/Test Server/)).toBeInTheDocument();
+ expect(screen.getAllByText(/Test Server/)).toHaveLength(1);
+ expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ });
});
diff --git a/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.tsx b/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.tsx
index f11bc6b9627..742409f3f8d 100644
--- a/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.tsx
+++ b/ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.tsx
@@ -11,14 +11,18 @@ interface MCPServerPermissionsProps {
mcpAccessGroups?: string[];
mcpToolPermissions?: Record;
mcpToolsets?: string[];
+ inheritedMcpServers?: string[];
accessToken?: string | null;
}
+const INHERITED_MCP_SERVER_TOOLTIP = "Granted through one of the team's access groups";
+
export function MCPServerPermissions({
mcpServers,
mcpAccessGroups = [],
mcpToolPermissions = {},
mcpToolsets = [],
+ inheritedMcpServers = [],
accessToken,
}: MCPServerPermissionsProps) {
const [mcpServerDetails, setMCPServerDetails] = useState([]);
@@ -50,10 +54,16 @@ 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;
+
// Fetch MCP server details when component mounts
useEffect(() => {
const fetchMCPServerDetails = async () => {
- if (accessToken && mcpServers.length > 0) {
+ if (accessToken && serverIdCount > 0) {
try {
const response = await fetchMCPServers(accessToken);
if (response && Array.isArray(response)) {
@@ -67,7 +77,7 @@ export function MCPServerPermissions({
}
};
fetchMCPServerDetails();
- }, [accessToken, mcpServers.length]);
+ }, [accessToken, serverIdCount]);
// Fetch toolset details
useEffect(() => {
@@ -98,12 +108,11 @@ export function MCPServerPermissions({
const blocksAllMcpServers = mcpServers.includes(NO_MCP_SERVERS_SENTINEL);
const grantsAllProxyMcpServers = mcpServers.includes(ALL_PROXY_MCP_SERVERS_SENTINEL);
- // Merge servers and access groups into one list
+ // Merge servers, inherited servers and access groups into one list
const mergedItems = [
- ...mcpServers
- .filter((server) => server !== NO_MCP_SERVERS_SENTINEL && server !== ALL_PROXY_MCP_SERVERS_SENTINEL)
- .map((server) => ({ type: "server", value: server })),
- ...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group })),
+ ...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 })),
];
const totalCount = mergedItems.length + mcpToolsets.length;
@@ -152,8 +161,17 @@ export function MCPServerPermissions({
{getMCPServerDisplayName(item.value)}
+ {item.inherited && (
+
+ Inherited
+
+ )}
- {`Full ID: ${item.value}`}
+
+ {item.inherited
+ ? `${INHERITED_MCP_SERVER_TOOLTIP}. Full ID: ${item.value}`
+ : `Full ID: ${item.value}`}
+
) : (
diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
index ca1e0413dcb..40a3d2ada1a 100644
--- a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
@@ -38,6 +38,9 @@ vi.mock("@/components/networking", () => ({
organizationInfoCall: vi.fn(),
getRouterSettingsCall: vi.fn().mockResolvedValue({ fields: [] }),
getPassThroughEndpointsCall: vi.fn(),
+ fetchMCPServers: vi.fn().mockResolvedValue([]),
+ fetchMCPToolsets: vi.fn().mockResolvedValue([]),
+ getAgentsList: vi.fn().mockResolvedValue({ agents: [] }),
}));
const can = vi.fn();
@@ -302,6 +305,31 @@ describe("TeamInfoView", () => {
);
});
+ it("shows MCP servers and agents inherited from access groups in the Object Permissions card", async () => {
+ vi.mocked(networking.fetchMCPServers).mockResolvedValue([
+ { server_id: "mcp-github-1234", server_name: "github", alias: "github" },
+ ]);
+ vi.mocked(networking.getAgentsList).mockResolvedValue({
+ agents: [{ agent_id: "agent-support-5678", agent_name: "support_agent" }],
+ });
+ vi.mocked(networking.teamInfoCall).mockResolvedValue(
+ createMockTeamData({
+ object_permission: null,
+ access_group_ids: ["ag-1"],
+ access_group_mcp_server_ids: ["mcp-github-1234"],
+ access_group_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);
+ expect(screen.queryByText("No MCP servers, access groups, or toolsets configured")).not.toBeInTheDocument();
+ expect(screen.queryByText("No agents or access groups configured")).not.toBeInTheDocument();
+ });
+
it("keeps the all-proxy-models badge non-clickable", async () => {
vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData({ models: ["all-proxy-models"] }));
diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
index 44f0d420fd6..7ebef0691e6 100644
--- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
@@ -1033,7 +1033,13 @@ const TeamInfoView: React.FC = ({
-
+
= ({