From 4c9f578a383f42a226499a18008879ec33dd25e5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 19 May 2026 09:02:39 +0000 Subject: [PATCH] fix(mcp): validate tool/server in call_tool; skip JWT signer when not configured or static auth present Co-authored-by: Yassin Kortam --- .../mcp_server/mcp_server_manager.py | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index aa7f7be1f9b..c881d3e0898 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -1433,19 +1433,31 @@ class MCPServerManager: extra_headers = {} extra_headers.update(server.static_headers) - # MCPJWTSigner: inject signed JWT for tools/list (list path skips pre_call_hook) + # MCPJWTSigner: inject signed JWT for tools/list (list path skips pre_call_hook). + # Skip entirely when the signer is not configured (avoid an unnecessary + # dict copy on every list call) and when the server has its own + # static Authorization header — admin-configured static auth must + # take precedence per-server so the signer doesn't silently + # overwrite e.g. an upstream API key. if user_api_key_auth is not None and not server.spec_path: from litellm.proxy.guardrails.guardrail_hooks.mcp_jwt_signer.mcp_jwt_signer import ( + get_mcp_jwt_signer, inject_mcp_jwt_headers_for_upstream, ) - extra_headers = await inject_mcp_jwt_headers_for_upstream( - user_api_key_dict=user_api_key_auth, - extra_headers=extra_headers, - raw_headers=raw_headers, - for_list_tools=True, + has_static_authorization = bool(server.static_headers) and any( + isinstance(k, str) and k.lower() == "authorization" + for k in server.static_headers.keys() ) + if get_mcp_jwt_signer() is not None and not has_static_authorization: + extra_headers = await inject_mcp_jwt_headers_for_upstream( + user_api_key_dict=user_api_key_auth, + extra_headers=extra_headers, + raw_headers=raw_headers, + for_list_tools=True, + ) + stdio_env = self._build_stdio_env(server, raw_headers) client = await self._create_mcp_client( @@ -2836,19 +2848,32 @@ class MCPServerManager: """ start_time = datetime.datetime.now() - # Resolve server (REST may pass server_name + prefixed or unprefixed tool name) + # Resolve server (REST may pass server_name + prefixed or unprefixed tool name). + # Prefer prefixed-name lookup first so the resolution validates that the + # tool actually belongs to server_name. Fall back to name-based server + # resolution only when the tool->server mapping isn't populated (e.g. + # OAuth2 servers skipped during init). mcp_server: Optional[MCPServer] = None - for candidate in self.get_registry().values(): - if normalize_server_name(candidate.name) == normalize_server_name( - server_name + prefixed_tool_name = add_server_prefix_to_name(name, server_name) + mcp_server = self._get_mcp_server_from_tool_name(prefixed_tool_name) + if mcp_server is None: + for candidate in self.get_registry().values(): + if normalize_server_name(candidate.name) == normalize_server_name( + server_name + ): + mcp_server = candidate + break + if mcp_server is None: + # Last resort: lookup by unprefixed tool name. Only accept the + # match when it agrees with the caller's server_name — otherwise + # we'd silently dispatch to a different server than requested. + fallback = self._get_mcp_server_from_tool_name(name) + if fallback is not None and ( + not server_name + or normalize_server_name(fallback.name) + == normalize_server_name(server_name) ): - mcp_server = candidate - break - if mcp_server is None: - prefixed_tool_name = add_server_prefix_to_name(name, server_name) - mcp_server = self._get_mcp_server_from_tool_name(prefixed_tool_name) - if mcp_server is None: - mcp_server = self._get_mcp_server_from_tool_name(name) + mcp_server = fallback if mcp_server is None: raise ValueError(f"Tool {name} not found")