Files
litellm/tests/mcp_tests/test_openapi_spec_path_url.py
T
moophlo 96206ec141 mcp: support http(s) URLs for spec_path in OpenAPI MCP loader (#20753)
* fix(responses): preserve streamed tool deltas when id is omitted

* fix(responses): guard ambiguous tool-call index reuse

* add missing indexes on VerificationToken table

* mcp: support http(s) URLs for spec_path in OpenAPI MCP loader

* test(mcp): add unit test for OpenAPI spec_path URL support

* Fix OpenAPI spec URL loading to use shared MCP httpx client

Ensure URL-based OpenAPI loading honors LiteLLM’s custom httpx configuration, add missing imports, and harden tests to prevent regressions or accidental direct httpx usage.

* removed unused import urlparse

* removed unsupported timeout argument

---------

Co-authored-by: Emerson Gomes <emerson.gomes@thalesgroup.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: Carlo Alberto Ferraris <cafxx@mercari.com>
Co-authored-by: Andrea Odorisio <Andrea@BR-FHH9MWDQ2PMAC.local>
2026-02-10 16:00:36 +05:30

93 lines
2.8 KiB
Python

from __future__ import annotations
from typing import Any, Dict
import httpx
import pytest
from litellm.proxy._experimental.mcp_server import openapi_to_mcp_generator as gen
class _FakeAsyncHTTPHandler:
"""
Minimal stand-in for the object returned by get_async_httpx_client().
openapi_to_mcp_generator.load_openapi_spec_async() calls:
client = get_async_httpx_client(...)
r = await client.get(url, timeout=30.0)
So we must implement async get().
"""
def __init__(self, response: httpx.Response, expected_url: str):
self._response = response
self._expected_url = expected_url
self.calls = 0
async def get(self, request_url: str, timeout: float = 30.0):
self.calls += 1
assert request_url == self._expected_url
assert timeout == 30.0
return self._response
def test_load_openapi_spec_supports_http_url(monkeypatch: pytest.MonkeyPatch) -> None:
url = "http://example.local/openapi.json"
expected: Dict[str, Any] = {
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0.0"},
"paths": {},
}
# httpx.Response must include a Request for raise_for_status() to work.
req = httpx.Request("GET", url)
resp = httpx.Response(status_code=200, json=expected, request=req)
calls = {"get_async_httpx_client": 0}
handler_holder: Dict[str, Any] = {}
def fake_get_async_httpx_client(*args, **kwargs):
calls["get_async_httpx_client"] += 1
h = _FakeAsyncHTTPHandler(resp, expected_url=url)
handler_holder["handler"] = h
return h
# Ensure shared/custom client path is used
monkeypatch.setattr(gen, "get_async_httpx_client", fake_get_async_httpx_client)
# Fail loudly if someone reintroduces direct httpx.get()
def boom(*args, **kwargs):
raise AssertionError("Direct httpx.get() must not be used for URL spec loading")
monkeypatch.setattr(httpx, "get", boom)
spec = gen.load_openapi_spec(url)
assert spec == expected
assert calls["get_async_httpx_client"] == 1
assert handler_holder["handler"].calls == 1
def test_load_openapi_spec_supports_local_file_path(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
expected: Dict[str, Any] = {
"openapi": "3.0.0",
"info": {"title": "Local API", "version": "1.0.0"},
"paths": {},
}
p = tmp_path / "openapi.json"
p.write_text(
'{"openapi":"3.0.0","info":{"title":"Local API","version":"1.0.0"},"paths":{}}',
encoding="utf-8",
)
# For local files, shared client must NOT be used.
def boom_client(*args, **kwargs):
raise AssertionError("get_async_httpx_client() must not be called for local file paths")
monkeypatch.setattr(gen, "get_async_httpx_client", boom_client)
spec = gen.load_openapi_spec(str(p))
assert spec == expected