From 6767bf29bdb4fa3c8902642d3747fe7b833bd483 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 5 May 2026 18:32:14 +0000 Subject: [PATCH] Fix OpenAPI tool name collision: disambiguate sanitized names with numeric suffixes sanitize_openapi_tool_name replaces all invalid chars with '_', but when two operationIds differ only by sanitized characters (e.g. 'foo/list' and 'foo.list' both become 'foo_list'), the second registration silently overwrites the first in the tool registry. Add collision disambiguation in register_tools_from_openapi that appends _2, _3, ... suffixes when a sanitized name is already taken, mirroring the existing logic in _build_anthropic_tool_name_maps. --- .../mcp_server/openapi_to_mcp_generator.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py index 8628fbcb793..718435cce6f 100644 --- a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py +++ b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py @@ -11,7 +11,6 @@ from pathlib import PurePosixPath from typing import Any, Dict, List, Optional from urllib.parse import quote - # Tool names emitted from OpenAPI specs must work across all major LLM providers. # OpenAI/Anthropic/Bedrock all enforce a character class roughly equivalent to # ^[a-zA-Z0-9_-]+$ on tool names. Many specs (notably GitHub's REST API) use @@ -424,6 +423,7 @@ def create_tool_function( def register_tools_from_openapi(spec: Dict[str, Any], base_url: str): """Register MCP tools from OpenAPI specification.""" paths = spec.get("paths", {}) + used_names: set = set() for path, path_item in paths.items(): for method in ["get", "post", "put", "delete", "patch"]: @@ -438,6 +438,22 @@ def register_tools_from_openapi(spec: Dict[str, Any], base_url: str): operation_id = operation.get("operationId", f"{method}_{path}") tool_name = sanitize_openapi_tool_name(operation_id) + # Disambiguate collisions: two operationIds that differ only + # by sanitized characters (e.g. "foo/list" and "foo.list") + # would both become "foo_list". Append _2, _3, … to keep + # every tool reachable, mirroring the Anthropic-side logic + # in _build_anthropic_tool_name_maps. + unique = tool_name + n = 1 + while unique in used_names: + n += 1 + suffix = f"_{n}" + unique = ( + tool_name[: _OPENAPI_TOOL_NAME_MAX_LEN - len(suffix)] + suffix + ) + tool_name = unique + used_names.add(tool_name) + # Get description description = operation.get( "summary", operation.get("description", f"{method.upper()} {path}")