Align preview HTTP method order with register_tools_from_openapi

The preview endpoint and register_tools_from_openapi both use
order-dependent collision disambiguation (_2, _3 suffixes). When the
iteration order differs, two operations on the same path with sanitized
names that collide get different suffixes in preview vs registration,
so the dashboard shows names that don't match what actually got
registered.

Also adds a regression test that fails on the swapped order.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
This commit is contained in:
mateo-berri 2026-05-05 20:11:25 +00:00
parent b47d25318d
commit 56ef1ed003
No known key found for this signature in database
2 changed files with 91 additions and 1 deletions

View file

@ -990,7 +990,7 @@ if MCP_AVAILABLE:
tools: List[dict] = []
used_names: set = set()
for path, path_item in paths.items():
for method in ("get", "post", "put", "patch", "delete"):
for method in ("get", "post", "put", "delete", "patch"):
operation = path_item.get(method)
if operation is None:
continue

View file

@ -1447,3 +1447,93 @@ class TestPreviewOpenAPITools:
), f"preview tool name {name!r} violates ^[a-zA-Z0-9_-]+$"
assert "actions_download-job-logs-for-workflow-run" in names
assert "pulls_list-files" in names
async def test_preview_method_order_matches_registration(self, monkeypatch):
"""Preview must iterate HTTP methods in the same order as
register_tools_from_openapi, otherwise collision-disambiguation
suffixes (_2, _3, ...) get assigned to different operations and the
dashboard shows names that differ from what's actually registered.
"""
from litellm.proxy._experimental.mcp_server import (
openapi_to_mcp_generator,
)
spec = {
"paths": {
"/items/{id}": {
"delete": {
"operationId": "items/delete",
"summary": "Delete item",
},
"patch": {
"operationId": "items.delete",
"summary": "Soft-delete item",
},
}
}
}
async def fake_load_spec(spec_path): # noqa: ANN001
return spec
monkeypatch.setattr(
openapi_to_mcp_generator,
"load_openapi_spec_async",
fake_load_spec,
raising=False,
)
payload = NewMCPServerRequest(
server_name="collision_openapi_mcp",
spec_path="https://example.invalid/openapi.json",
transport="http",
)
request = _build_request()
from litellm.proxy._types import LitellmUserRoles
result = await rest_endpoints.test_tools_list(
request,
payload,
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
)
assert result.get("error") is None, result
preview_summary_to_name = {t["description"]: t["name"] for t in result["tools"]}
registered_summary_to_name: dict = {}
def fake_create_tool_function(
path, method, operation, base_url
): # noqa: ANN001
def _f():
return None
return _f
monkeypatch.setattr(
openapi_to_mcp_generator,
"create_tool_function",
fake_create_tool_function,
)
class _StubRegistry:
def register_tool(
self, name, description, input_schema, handler
): # noqa: ANN001
registered_summary_to_name[description] = name
monkeypatch.setattr(
openapi_to_mcp_generator,
"global_mcp_tool_registry",
_StubRegistry(),
)
openapi_to_mcp_generator.register_tools_from_openapi(
spec, base_url="https://example.invalid"
)
assert preview_summary_to_name == registered_summary_to_name, (
f"preview {preview_summary_to_name} != "
f"registered {registered_summary_to_name} — method iteration "
"order is out of sync, so collision suffixes (_2, _3, ...) "
"land on different operations"
)