diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 3992c2cac86..c4643a465c8 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -2855,6 +2855,7 @@ class MCPServerManager: # resolution only when the tool->server mapping isn't populated (e.g. # OAuth2 servers skipped during init). mcp_server: Optional[MCPServer] = None + resolved_by_server_name_only = False 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: @@ -2863,6 +2864,7 @@ class MCPServerManager: server_name ): mcp_server = candidate + resolved_by_server_name_only = True break if mcp_server is None: # Last resort: lookup by unprefixed tool name. Only accept the @@ -2878,6 +2880,17 @@ class MCPServerManager: if mcp_server is None: raise ValueError(f"Tool {name} not found") + # Server-name fallback is for mapping-not-yet-populated paths (e.g. REST). + # If this server already has tools in the mapping, unknown names should fail + # fast instead of opening a real upstream session. + if resolved_by_server_name_only: + tool_known = ( + name in self.tool_name_to_mcp_server_name_mapping + or prefixed_tool_name in self.tool_name_to_mcp_server_name_mapping + ) + if not tool_known and self._mapping_has_tools_for_server(mcp_server): + raise ValueError(f"Tool {name} not found") + ######################################################### # Pre MCP Tool Call Hook # Allow validation and modification of tool calls before execution @@ -3031,6 +3044,21 @@ class MCPServerManager: self.tool_name_to_mcp_server_name_mapping[original_name] = server.name self.tool_name_to_mcp_server_name_mapping[tool.name] = server.name + def _mapping_has_tools_for_server(self, server: MCPServer) -> bool: + """Return True if the tool mapping lists any tool for this server.""" + server_identifiers = { + normalize_server_name(server.name), + } + if server.server_name: + server_identifiers.add(normalize_server_name(server.server_name)) + if server.alias: + server_identifiers.add(normalize_server_name(server.alias)) + + for mapped_server_name in self.tool_name_to_mcp_server_name_mapping.values(): + if normalize_server_name(mapped_server_name) in server_identifiers: + return True + return False + def _get_mcp_server_from_tool_name(self, tool_name: str) -> Optional[MCPServer]: """ Get the MCP Server from the tool name (handles both prefixed and non-prefixed names) diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py index 1b9eb9ec399..809b13aeea6 100644 --- a/tests/mcp_tests/test_mcp_server.py +++ b/tests/mcp_tests/test_mcp_server.py @@ -382,6 +382,11 @@ async def test_mcp_http_transport_tool_not_found(): } ) + # Mapping populated for this server but not for the requested tool + test_manager.tool_name_to_mcp_server_name_mapping["gmail_send_email"] = ( + "test_http_server" + ) + # Try to call a tool that doesn't exist in mapping with pytest.raises(ValueError, match="Tool nonexistent_tool not found"): await test_manager.call_tool(