From b0869568b070a37e442dd967db9a27d8a0d70d58 Mon Sep 17 00:00:00 2001 From: bunnysayzz Date: Wed, 16 Sep 2026 09:24:01 +0530 Subject: [PATCH] fix(mcp): parse remote openapi bodies off the event loop --- .../mcp_server/openapi_to_mcp_generator.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 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 0cf35711e60..e05c26a0332 100644 --- a/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py +++ b/litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py @@ -215,6 +215,21 @@ def _load_local_openapi_spec(filepath: str) -> dict[str, Any]: raise json_exc from None +def _load_remote_openapi_spec(text: str, as_yaml: bool) -> dict[str, Any]: + """Parse a fetched spec body. Runs in a worker thread: YAML parsing is + synchronous CPU work with no nesting/alias limits, so it must not run on + the event loop where a pathological document could stall the proxy.""" + if as_yaml: + return _load_yaml_mapping(text) + try: + return json.loads(text) + except ValueError as json_exc: + try: + return _load_yaml_mapping(text) + except (TypeError, ValueError): + raise json_exc from None + + async def load_openapi_spec_async(filepath: str, *, max_bytes: int | None = None) -> dict[str, Any]: if filepath.startswith("http://") or filepath.startswith("https://"): client: Final = get_async_httpx_client(llm_provider=httpxSpecialProvider.MCP) @@ -226,17 +241,10 @@ async def load_openapi_spec_async(filepath: str, *, max_bytes: int | None = None r.raise_for_status() content_type = r.headers.get("content-type", "") - if _is_yaml_content(filepath, content_type): - return _load_yaml_mapping(r.text) # Try JSON first; fall back to YAML for specs served without # proper Content-Type headers (common with raw GitHub URLs). - try: - return r.json() - except ValueError as json_exc: - try: - return _load_yaml_mapping(r.text) - except (TypeError, ValueError): - raise json_exc from None + as_yaml = _is_yaml_content(filepath, content_type) + return await asyncio.to_thread(_load_remote_openapi_spec, r.text, as_yaml) # Local files go through a worker thread: the async path must not # perform blocking disk I/O directly (ruff ASYNC230).