From d2438b5bef6387b154f7867de582b5b0a215d133 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 17:58:32 +0000 Subject: [PATCH] fix(mcp): preserve tools/call scope on missing tool name; pass user_api_key_auth in list_tools Co-authored-by: Yassin Kortam --- .../mcp_server/mcp_server_manager.py | 1 + .../mcp_jwt_signer/mcp_jwt_signer.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index b698a59c21b..7ee7658fe9c 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -1226,6 +1226,7 @@ class MCPServerManager: tools = await self._get_tools_from_server( server=server, mcp_auth_header=server_auth_header, + user_api_key_auth=user_api_key_auth, ) return tools except Exception as e: diff --git a/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py b/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py index 967c7f8be90..0d505907ff1 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py +++ b/litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py @@ -605,7 +605,11 @@ class MCPJWTSigner(CustomGuardrail): # FR-10: Scope building # ------------------------------------------------------------------ - def _build_scope(self, raw_tool_name: str) -> str: + def _build_scope( + self, + raw_tool_name: str, + call_type: Optional[CallTypesLiteral] = None, + ) -> str: """ Build the JWT scope string. @@ -627,6 +631,12 @@ class MCPJWTSigner(CustomGuardrail): ) if tool_name: scopes = ["mcp:tools/call", f"mcp:tools/{tool_name}:call"] + elif call_type == "call_mcp_tool": + # Tool-call request reached the signer without a tool name (e.g. + # missing mcp_tool_name in hook data). Fall back to a generic + # tools/call scope so the upstream server still accepts the + # invocation rather than rejecting it as a tools/list-only token. + scopes = ["mcp:tools/call"] else: scopes = ["mcp:tools/list"] return " ".join(scopes) @@ -677,6 +687,7 @@ class MCPJWTSigner(CustomGuardrail): user_api_key_dict: UserAPIKeyAuth, data: dict, jwt_claims: Optional[Dict[str, Any]] = None, + call_type: Optional[CallTypesLiteral] = None, ) -> Dict[str, Any]: """ Build JWT claims for the outbound MCP access token. @@ -717,7 +728,7 @@ class MCPJWTSigner(CustomGuardrail): # scope (FR-10) raw_tool_name: str = data.get("mcp_tool_name", "") - claims["scope"] = self._build_scope(raw_tool_name) + claims["scope"] = self._build_scope(raw_tool_name, call_type=call_type) # optional_claims passthrough (FR-15) claims = self._passthrough_optional_claims(claims, jwt_claims) @@ -845,7 +856,9 @@ class MCPJWTSigner(CustomGuardrail): # ------------------------------------------------------------------ # Build outbound access token # ------------------------------------------------------------------ - claims = self._build_claims(user_api_key_dict, hook_data, jwt_claims) + claims = self._build_claims( + user_api_key_dict, hook_data, jwt_claims, call_type=call_type + ) signed_token = jwt.encode( claims,