From 681554ca362226f408262fc322969e41c040450a Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Mon, 31 Aug 2026 22:11:31 +0800 Subject: [PATCH] fix(mcp): package YAML parser with MCP extra --- .../_experimental/mcp_server/openapi_to_mcp_generator.py | 4 ++-- pyproject.toml | 2 +- .../mcp_server/test_openapi_spec_path_url.py | 8 ++++++++ uv.lock | 2 ++ 4 files changed, 13 insertions(+), 3 deletions(-) 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 382be19247f..23b34ade96e 100644 --- a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py +++ b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py @@ -158,7 +158,7 @@ def _sanitize_path_parameter_value(param_value: object, param_name: str) -> str: return quote(value_str, safe="") -def load_openapi_spec(filepath: str) -> dict[str, Any]: +def load_openapi_spec(filepath: str) -> Mapping[str, Any]: """ Sync wrapper. For URL specs, use the shared/custom MCP httpx client. """ @@ -176,7 +176,7 @@ def load_openapi_spec(filepath: str) -> dict[str, Any]: return asyncio.run(load_openapi_spec_async(filepath)) -def _parse_openapi_spec(text: str) -> dict[str, Any]: +def _parse_openapi_spec(text: str) -> Mapping[str, Any]: """Parse JSON or YAML OpenAPI documents into a mapping.""" try: parsed: Any = json.loads(text) diff --git a/pyproject.toml b/pyproject.toml index e670b10547e..a41c7109701 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ utils = [ "numpydoc>=1.8.0,<2.0", ] caching = ["diskcache>=5.6.3,<6.0"] -mcp = ["mcp>=2.2.0,<3", "httpx2>=2.5.0,<3", "pydantic>=2.12.0,<3"] +mcp = ["mcp>=2.2.0,<3", "httpx2>=2.5.0,<3", "pydantic>=2.12.0,<3", "pyyaml>=6.0.3,<7.0"] # Driver for the MongoDB Atlas vector store; Atlas Vector Search has no HTTP query API. # The floor is 4.9 because that is the release AsyncMongoClient landed in. # SAML SSO for the admin UI. python3-saml pulls in xmlsec/lxml, whose wheels diff --git a/tests/unit/proxy/_experimental/mcp_server/test_openapi_spec_path_url.py b/tests/unit/proxy/_experimental/mcp_server/test_openapi_spec_path_url.py index 26854dfa44f..d0a16f63f45 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_openapi_spec_path_url.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_openapi_spec_path_url.py @@ -119,3 +119,11 @@ def test_load_openapi_spec_supports_local_file_path(tmp_path, monkeypatch: pytes spec = gen.load_openapi_spec(str(p)) assert spec == expected + + +def test_load_openapi_spec_rejects_non_object_document(tmp_path) -> None: + p = tmp_path / "openapi.yaml" + p.write_text("- not an OpenAPI object\n", encoding="utf-8") + + with pytest.raises(TypeError, match="OpenAPI spec must be a JSON or YAML object"): + gen.load_openapi_spec(str(p)) diff --git a/uv.lock b/uv.lock index a481ecb2a2c..0a5d1634a1f 100644 --- a/uv.lock +++ b/uv.lock @@ -4562,6 +4562,7 @@ mcp = [ { name = "httpx2" }, { name = "mcp" }, { name = "pydantic" }, + { name = "pyyaml" }, ] mlflow = [ { name = "mlflow" }, @@ -4840,6 +4841,7 @@ requires-dist = [ { name = "python3-saml", marker = "extra == 'saml'", specifier = ">=1.16.0,<2.0" }, { name = "pyyaml", specifier = ">=6.0.3,<7.0" }, { name = "pyyaml", marker = "extra == 'cli'", specifier = ">=6.0.3,<7.0" }, + { name = "pyyaml", marker = "extra == 'mcp'", specifier = ">=6.0.3,<7.0" }, { name = "pyyaml", marker = "extra == 'proxy'", specifier = ">=6.0.3,<7.0" }, { name = "redisvl", marker = "extra == 'extra-proxy'", specifier = ">=0.4.1,<1.0" }, { name = "requests", marker = "extra == 'cli'", specifier = ">=2.32.0,<3.0" },