From 4e2f18ba4943558ce866454d26831029802973c0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 11 Mar 2026 19:48:40 -0700 Subject: [PATCH] fix: add OAuth headers to call_tool_rest_api, fix stale accessToken closure, fix group toggle on filtered subset --- .../_experimental/mcp_server/rest_endpoints.py | 12 +++++++++++- .../MCPToolPermissions.tsx | 14 +++++++++----- .../mcp_tools/McpCrudPermissionPanel.tsx | 17 +++++++++++++++-- .../mcp_tools/mcp_tool_configuration.tsx | 8 ++------ 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index faf6f80b280..dc846de0bf4 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -557,6 +557,16 @@ if MCP_AVAILABLE: request, user_api_key_dict, server_id ) + # Look up per-user OAuth headers for this server (mirrors list_tool_rest_api). + user_oauth_extra_headers: Optional[Dict[str, str]] = None + target_server = next( + (s for s in allowed_mcp_servers if s.server_id == server_id), None + ) + if target_server is not None: + user_oauth_extra_headers = await _get_user_oauth_extra_headers( + target_server, user_api_key_dict + ) + # Call execute_mcp_tool directly (permission checks already done) result = await execute_mcp_tool( name=tool_name, @@ -566,7 +576,7 @@ if MCP_AVAILABLE: user_api_key_auth=data.get("user_api_key_auth"), mcp_auth_header=data.get("mcp_auth_header"), mcp_server_auth_headers=data.get("mcp_server_auth_headers"), - oauth2_headers=data.get("oauth2_headers"), + oauth2_headers=user_oauth_extra_headers or data.get("oauth2_headers"), raw_headers=data.get("raw_headers"), litellm_logging_obj=data.get("litellm_logging_obj"), ) 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 829051c1a5c..5e16a5c4b84 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx @@ -43,12 +43,13 @@ const MCPToolPermissions: React.FC = ({ }, [allServers, selectedServers]); // Fetch tools for a specific server; applies delete-blocked-by-default for new servers. - const fetchToolsForServer = async (serverId: string) => { + // `token` is passed explicitly so the closure never captures a stale accessToken. + const fetchToolsForServer = async (serverId: string, token: string) => { setLoadingTools((prev) => ({ ...prev, [serverId]: true })); setToolErrors((prev) => ({ ...prev, [serverId]: "" })); try { - const response = await listMCPTools(accessToken, serverId); + const response = await listMCPTools(token, serverId); if (response.error) { setToolErrors((prev) => ({ ...prev, [serverId]: response.message || "Failed to fetch tools" })); @@ -76,14 +77,17 @@ const MCPToolPermissions: React.FC = ({ } }; - // Auto-fetch tools when servers change + // Auto-fetch tools when servers or accessToken change useEffect(() => { servers.forEach((server) => { if (!serverTools[server.server_id] && !loadingTools[server.server_id]) { - fetchToolsForServer(server.server_id); + fetchToolsForServer(server.server_id, accessToken); } }); - }, [servers]); + // fetchToolsForServer is defined in this render scope but receives `accessToken` + // as an explicit argument, so it is safe to omit from deps here. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [servers, accessToken]); const handleCrudPanelChange = (serverId: string, allowed: string[]) => { onChange({ ...toolPermissions, [serverId]: allowed }); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx b/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx index 6fddb6f2e5a..34985e00cd8 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx @@ -33,6 +33,12 @@ interface McpCrudPermissionPanelProps { /** Called whenever the allowed set changes. Always emits a concrete string[]. */ onChange: (allowed: string[]) => void; readOnly?: boolean; + /** + * Optional search filter string. When set, only tools whose name or description + * contain this string (case-insensitive) are shown. Group-level toggles still + * operate on the complete group — not just the visible (filtered) subset. + */ + searchFilter?: string; } const CRUD_ORDER: CrudOp[] = ["read", "create", "update", "delete", "unknown"]; @@ -67,6 +73,7 @@ const McpCrudPermissionPanel: React.FC = ({ value, onChange, readOnly = false, + searchFilter = "", }) => { const [collapsed, setCollapsed] = useState>({ read: false, @@ -197,10 +204,16 @@ const McpCrudPermissionPanel: React.FC = ({ )} - {/* Tool list */} + {/* Tool list — searchFilter narrows display only; group toggles still cover all tools */} {!isCollapsed && (
- {group.map((tool) => { + {group + .filter((t) => + !searchFilter || + t.name.toLowerCase().includes(searchFilter.toLowerCase()) || + (t.description ?? "").toLowerCase().includes(searchFilter.toLowerCase()) + ) + .map((tool) => { const allowed = isToolAllowed(tool.name); return (
= ({ size="large" /> - !toolSearchTerm || - t.name.toLowerCase().includes(toolSearchTerm.toLowerCase()) || - (t.description ?? "").toLowerCase().includes(toolSearchTerm.toLowerCase()) - )} + tools={tools} + searchFilter={toolSearchTerm} value={allowedTools} onChange={(allowed) => onAllowedToolsChange(allowed)} />