diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 3d44315341b..6fb1341d572 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -2856,6 +2856,20 @@ ], "title": "Mcp Servers" }, + "mcp_tool_overrides": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/MCPToolOverrideEntry" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Mcp Tool Overrides" + }, "mcp_tool_permissions": { "anyOf": [ { @@ -3574,6 +3588,27 @@ "title": "KeyMetricWithMetadata", "type": "object" }, + "MCPToolOverrideEntry": { + "description": "Per-server tool overrides stored on an object permission row's\n``mcp_tool_overrides``: ``allow`` re-arms names the convention denies,\n``deny`` disables names the convention or an allowlist would permit.", + "properties": { + "allow": { + "items": { + "type": "string" + }, + "title": "Allow", + "type": "array" + }, + "deny": { + "items": { + "type": "string" + }, + "title": "Deny", + "type": "array" + } + }, + "title": "MCPToolOverrideEntry", + "type": "object" + }, "MakeAgentsPublicRequest": { "properties": { "agent_ids": { diff --git a/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py index 77e52f30bb7..aeec17954dd 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_customer_endpoints.py @@ -817,6 +817,9 @@ _EXPECTED_CUSTOMER = { "mcp_servers": ["s1"], "mcp_access_groups": [], "mcp_tool_permissions": None, + "mcp_tool_overrides": None, + "mcp_tool_permissions_archive": None, + "mcp_permission_version": None, "vector_stores": [], "agents": [], "agent_access_groups": [], diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.integration.test.tsx index 37e00766a75..cd5f47016c2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_info.integration.test.tsx @@ -179,7 +179,7 @@ describe("AgentInfoView update payload", () => { rpm_limit: 222, session_tpm_limit: 333, session_rpm_limit: 444, - object_permission: { mcp_servers: [], mcp_access_groups: [], mcp_toolsets: [], mcp_tool_permissions: {} }, + object_permission: { mcp_servers: [], mcp_access_groups: [], mcp_toolsets: [], mcp_tool_permissions: {}, mcp_tool_overrides: {} }, access_group_ids: [], }); }); @@ -221,7 +221,7 @@ describe("AgentInfoView update payload", () => { rpm_limit: 222, session_tpm_limit: 333, session_rpm_limit: 444, - object_permission: { mcp_servers: [], mcp_access_groups: [], mcp_toolsets: [], mcp_tool_permissions: {} }, + object_permission: { mcp_servers: [], mcp_access_groups: [], mcp_toolsets: [], mcp_tool_permissions: {}, mcp_tool_overrides: {} }, access_group_ids: [], }); }); @@ -300,7 +300,7 @@ describe("AgentInfoView update payload", () => { api_base: "https://other.example.com", model: "langgraph/asst_1", }, - object_permission: { mcp_servers: [], mcp_access_groups: [], mcp_toolsets: [], mcp_tool_permissions: {} }, + object_permission: { mcp_servers: [], mcp_access_groups: [], mcp_toolsets: [], mcp_tool_permissions: {}, mcp_tool_overrides: {} }, access_group_ids: [], }); }); @@ -311,6 +311,7 @@ describe("AgentInfoView update payload", () => { mcp_access_groups: ["grp-a"], mcp_toolsets: ["toolset-1"], mcp_tool_permissions: { "srv-1": ["tool_x"] }, + mcp_tool_overrides: {}, }; vi.mocked(networking.getAgentInfo).mockResolvedValue({ ...A2A_AGENT, diff --git a/ui/litellm-dashboard/src/components/Teams.tsx b/ui/litellm-dashboard/src/components/Teams.tsx index e48571c9c8f..a88c9970801 100644 --- a/ui/litellm-dashboard/src/components/Teams.tsx +++ b/ui/litellm-dashboard/src/components/Teams.tsx @@ -103,7 +103,9 @@ const teamCreateFieldsSchema = z.object({ }) .optional(), mcp_tool_permissions: z.record(z.string(), z.array(z.string())).optional(), - mcp_tool_overrides: z.record(z.string(), z.object({ allow: z.array(z.string()), deny: z.array(z.string()) })).optional(), + mcp_tool_overrides: z + .record(z.string(), z.object({ allow: z.array(z.string()), deny: z.array(z.string()) })) + .optional(), allowed_agents_and_groups: z.object({ agents: z.array(z.string()), accessGroups: z.array(z.string()) }).optional(), object_permission_search_tools: z.array(z.string()).optional(), object_permission_skills: z.array(z.string()).optional(), 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 8358f50865e..76806ab7fd6 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx @@ -350,7 +350,8 @@ const MCPToolPermissions: React.FC = ({ permissionKey: entry.permissionKey, edits: tools .filter( - (tool) => allowed.includes(tool.name) !== stateFor(tool).checked && !stateFor(tool).locked, + (tool) => + allowed.includes(tool.name) !== stateFor(tool).checked && !stateFor(tool).locked, ) .map((tool) => ({ toolName: tool.name, diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.test.ts b/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.test.ts index 3e6b97dab8e..f262b95ff50 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.test.ts +++ b/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.test.ts @@ -612,7 +612,11 @@ describe("convention servers and tool overrides", () => { }); it("locks toolset tools as granted", () => { - const toolset = { toolset_id: "ts-1", toolset_name: "TS", tools: [{ server_id: "srv-1", tool_name: "list_pages" }] }; + const toolset = { + toolset_id: "ts-1", + toolset_name: "TS", + tools: [{ server_id: "srv-1", tool_name: "list_pages" }], + }; const input: Parameters[0] = { ...emptyInput, allServers: [srv], @@ -640,7 +644,13 @@ describe("convention servers and tool overrides", () => { it("moves a delete tool in and out of allow on toggle", () => { const overrides = { "srv-1": { allow: [], deny: ["list_pages"] } }; - const grant = { toolOverrides: overrides, permissionKey: "srv-1", toolName: "delete_page", isDeleteTool: true, checked: true }; + const grant = { + toolOverrides: overrides, + permissionKey: "srv-1", + toolName: "delete_page", + isDeleteTool: true, + checked: true, + }; expect(applyToolOverrideWrite(grant)).toEqual({ "srv-1": { allow: ["delete_page"], deny: ["list_pages"] } }); @@ -656,7 +666,13 @@ describe("convention servers and tool overrides", () => { }); it("moves a non-delete tool in and out of deny on toggle without writing allows", () => { - const deny = { toolOverrides: {}, permissionKey: "srv-1", toolName: "list_pages", isDeleteTool: false, checked: false }; + const deny = { + toolOverrides: {}, + permissionKey: "srv-1", + toolName: "list_pages", + isDeleteTool: false, + checked: false, + }; expect(applyToolOverrideWrite(deny)).toEqual({ "srv-1": { allow: [], deny: ["list_pages"] } }); @@ -731,14 +747,14 @@ describe("retainedMcpToolOverrides", () => { }); it("keeps an override while any server the key names stays granted", () => { - expect( - retainedMcpToolOverrides({ shared: { allow: [], deny: ["t"] } }, new Set(["srv-2"]), catalog), - ).toEqual({ shared: { allow: [], deny: ["t"] } }); + expect(retainedMcpToolOverrides({ shared: { allow: [], deny: ["t"] } }, new Set(["srv-2"]), catalog)).toEqual({ + shared: { allow: [], deny: ["t"] }, + }); }); it("keeps an override whose key resolves to nothing, so an unloaded catalog prunes nothing", () => { - expect(retainedMcpToolOverrides({ "not-yet-loaded": { allow: [], deny: ["t"] } }, new Set(), catalog)).toEqual( - { "not-yet-loaded": { allow: [], deny: ["t"] } }, - ); + expect(retainedMcpToolOverrides({ "not-yet-loaded": { allow: [], deny: ["t"] } }, new Set(), catalog)).toEqual({ + "not-yet-loaded": { allow: [], deny: ["t"] }, + }); }); }); diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.ts b/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.ts index 461ad7e4c25..d74705be4c7 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.ts +++ b/ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.ts @@ -265,11 +265,7 @@ export const resolveEffectiveMcpServers = ({ ); }; -export const mcpToolState = ( - entry: EffectiveMcpServer, - toolName: string, - isDeleteTool: boolean, -): McpToolState => { +export const mcpToolState = (entry: EffectiveMcpServer, toolName: string, isDeleteTool: boolean): McpToolState => { const denied = (entry.overrides?.deny ?? []).includes(toolName); if (denied) { if (entry.keyedTools !== undefined) { @@ -335,7 +331,16 @@ export const applyToolOverrideWrites = ({ readonly permissionKey: string; readonly edits: readonly { toolName: string; isDeleteTool: boolean; checked: boolean }[]; }): Record => - edits.reduce((overrides, edit) => { - const write = { toolOverrides: overrides, permissionKey, toolName: edit.toolName, isDeleteTool: edit.isDeleteTool, checked: edit.checked }; - return applyToolOverrideWrite(write); - }, { ...toolOverrides }); + edits.reduce( + (overrides, edit) => { + const write = { + toolOverrides: overrides, + permissionKey, + toolName: edit.toolName, + isDeleteTool: edit.isDeleteTool, + checked: edit.checked, + }; + return applyToolOverrideWrite(write); + }, + { ...toolOverrides }, + ); diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index 313fd15e1d6..9ca6c6d8c69 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -404,7 +404,9 @@ const teamUpdateFieldsSchema = z.object({ }) .optional(), mcp_tool_permissions: z.record(z.string(), z.array(z.string())).optional(), - mcp_tool_overrides: z.record(z.string(), z.object({ allow: z.array(z.string()), deny: z.array(z.string()) })).optional(), + mcp_tool_overrides: z + .record(z.string(), z.object({ allow: z.array(z.string()), deny: z.array(z.string()) })) + .optional(), agents_and_groups: z.object({ agents: z.array(z.string()), accessGroups: z.array(z.string()) }).optional(), object_permission_search_tools: z.array(z.string()).optional(), object_permission_skills: z.array(z.string()).optional(), diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx index 9e3e19f1e1b..1a007f2e2c8 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx @@ -244,6 +244,7 @@ describe("KeyEditView", () => { mcp_servers: [], mcp_access_groups: [], mcp_tool_permissions: {}, + mcp_tool_overrides: {}, vector_stores: [], }, auto_rotate: false, @@ -2112,6 +2113,7 @@ describe("KeyEditView", () => { vector_stores: [], mcp_servers_and_groups: { servers: [], accessGroups: [], toolsets: [] }, mcp_tool_permissions: {}, + mcp_tool_overrides: {}, agents_and_groups: { agents: [], accessGroups: [] }, skills: [], organization_id: null, diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index caade650903..cb39583a618 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -24417,6 +24417,10 @@ export interface components { mcp_access_groups?: string[] | null; /** Mcp Servers */ mcp_servers?: string[] | null; + /** Mcp Tool Overrides */ + mcp_tool_overrides?: { + [key: string]: components["schemas"]["MCPToolOverrideEntry"]; + } | null; /** Mcp Tool Permissions */ mcp_tool_permissions?: { [key: string]: string[]; diff --git a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts index 487842317d5..1b05ba9f98c 100644 --- a/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts +++ b/ui/litellm-dashboard/src/utils/mcpToolCrudClassification.ts @@ -91,12 +91,17 @@ const descriptionTokens = (description: string): string[] => .filter((token) => token.length > 0) .map((token) => token.toLowerCase()); -const stripSuffix = (token: string, suffix: string): string => (token.endsWith(suffix) ? token.slice(0, -suffix.length) : token); +const stripSuffix = (token: string, suffix: string): string => + token.endsWith(suffix) ? token.slice(0, -suffix.length) : token; const tokenVariants = (token: string): string[] => - [token, stripSuffix(token, "es"), stripSuffix(token, "s"), stripSuffix(token, "ed"), stripSuffix(token, "ing")].filter( - (variant) => variant.length > 0, - ); + [ + token, + stripSuffix(token, "es"), + stripSuffix(token, "s"), + stripSuffix(token, "ed"), + stripSuffix(token, "ing"), + ].filter((variant) => variant.length > 0); const CONJUNCTION_TOKENS = new Set(["and", "then", "or", "n"]);