mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-28 01:32:17 +00:00
Merge 79060acfc9 into 252c71c0b2
This commit is contained in:
commit
ad54cc088d
3 changed files with 98 additions and 19 deletions
|
|
@ -6923,25 +6923,33 @@ class MCPServerManager:
|
|||
|
||||
if not should_skip_health_check:
|
||||
try:
|
||||
resolved_static_headers: Final = await self._resolve_static_headers_with_env_vars(
|
||||
server=server,
|
||||
user_api_key_auth=None,
|
||||
raise_on_missing=False,
|
||||
)
|
||||
extra_headers: Final = dict(resolved_static_headers) if resolved_static_headers else {}
|
||||
client: Final = await self._create_mcp_client(
|
||||
server=server,
|
||||
mcp_auth_header=None,
|
||||
extra_headers=extra_headers,
|
||||
stdio_env=None,
|
||||
)
|
||||
if server.spec_path:
|
||||
# url is a REST base, not an MCP endpoint, so the spec is the only thing to probe
|
||||
from litellm.proxy._experimental.mcp_server.openapi_to_mcp_generator import (
|
||||
load_openapi_spec_async,
|
||||
)
|
||||
|
||||
async def _noop(session):
|
||||
return "ok"
|
||||
await asyncio.wait_for(load_openapi_spec_async(server.spec_path), timeout=MCP_HEALTH_CHECK_TIMEOUT)
|
||||
else:
|
||||
resolved_static_headers: Final = await self._resolve_static_headers_with_env_vars(
|
||||
server=server,
|
||||
user_api_key_auth=None,
|
||||
raise_on_missing=False,
|
||||
)
|
||||
extra_headers: Final = dict(resolved_static_headers) if resolved_static_headers else {}
|
||||
client: Final = await self._create_mcp_client(
|
||||
server=server,
|
||||
mcp_auth_header=None,
|
||||
extra_headers=extra_headers,
|
||||
stdio_env=None,
|
||||
)
|
||||
|
||||
# Add timeout wrapper to prevent hanging
|
||||
await asyncio.wait_for(client.run_with_session(_noop), timeout=MCP_HEALTH_CHECK_TIMEOUT)
|
||||
self._remember_upstream_initialize_instructions(server, client)
|
||||
async def _noop(session):
|
||||
return "ok"
|
||||
|
||||
# Add timeout wrapper to prevent hanging
|
||||
await asyncio.wait_for(client.run_with_session(_noop), timeout=MCP_HEALTH_CHECK_TIMEOUT)
|
||||
self._remember_upstream_initialize_instructions(server, client)
|
||||
status = "healthy"
|
||||
except asyncio.TimeoutError:
|
||||
health_check_error = f"Health check timed out after {MCP_HEALTH_CHECK_TIMEOUT} seconds"
|
||||
|
|
|
|||
|
|
@ -174,8 +174,10 @@ async def load_openapi_spec_async(filepath: str, *, max_bytes: int | None = None
|
|||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
# fallback: local file
|
||||
# Local filesystem path
|
||||
return await asyncio.to_thread(_read_local_openapi_spec, filepath)
|
||||
|
||||
|
||||
def _read_local_openapi_spec(filepath: str) -> dict[str, Any]:
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"OpenAPI spec not found at {filepath}")
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
|
|
|
|||
|
|
@ -4728,6 +4728,75 @@ class TestMCPServerManager:
|
|||
assert result.status == "unhealthy"
|
||||
assert "OAuth discovery unavailable" in (result.health_check_error or "")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_server_openapi_spec_is_probed_instead_of_mcp_session(self, tmp_path):
|
||||
"""An OpenAPI server is healthy when its spec loads, and is never asked to speak MCP.
|
||||
|
||||
Regression for #40079: these servers store the REST base url in ``url`` and only look like
|
||||
http transport, so opening an MCP session against them always failed with "Session
|
||||
terminated" even though every tool had loaded from the spec. ``url`` is a closed port, so
|
||||
an MCP handshake would report unhealthy.
|
||||
"""
|
||||
spec = tmp_path / "openapi.json"
|
||||
spec.write_text(json.dumps({"openapi": "3.0.0", "paths": {}}))
|
||||
manager = MCPServerManager()
|
||||
server = MCPServer(
|
||||
server_id="openapi-server",
|
||||
name="openapi-server",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.none,
|
||||
url="http://127.0.0.1:1",
|
||||
spec_path=str(spec),
|
||||
)
|
||||
manager.config_mcp_servers[server.server_id] = server
|
||||
|
||||
result = await manager.health_check_server("openapi-server")
|
||||
|
||||
assert result.status == "healthy"
|
||||
assert result.health_check_error is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_server_openapi_spec_failure_is_unhealthy(self, tmp_path):
|
||||
"""A spec that stopped loading is unhealthy, and the upstream error is what gets reported."""
|
||||
missing = tmp_path / "openapi.json"
|
||||
manager = MCPServerManager()
|
||||
server = MCPServer(
|
||||
server_id="openapi-server",
|
||||
name="openapi-server",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.none,
|
||||
url="http://127.0.0.1:1",
|
||||
spec_path=str(missing),
|
||||
)
|
||||
manager.config_mcp_servers[server.server_id] = server
|
||||
|
||||
result = await manager.health_check_server("openapi-server")
|
||||
|
||||
assert result.status == "unhealthy"
|
||||
assert result.health_check_error == f"OpenAPI spec not found at {missing}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_server_openapi_spec_keeps_per_user_auth_skip(self, tmp_path):
|
||||
"""Per-user auth still wins: fetching the spec userless could 401 and report a false failure.
|
||||
|
||||
The spec file is absent, so probing it would have reported unhealthy rather than unknown.
|
||||
"""
|
||||
manager = MCPServerManager()
|
||||
server = MCPServer(
|
||||
server_id="openapi-server",
|
||||
name="openapi-server",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.bearer_token,
|
||||
url="http://127.0.0.1:1",
|
||||
spec_path=str(tmp_path / "openapi.json"),
|
||||
)
|
||||
manager.config_mcp_servers[server.server_id] = server
|
||||
|
||||
result = await manager.health_check_server("openapi-server")
|
||||
|
||||
assert result.status == "unknown"
|
||||
assert result.health_check_error is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_server_not_found(self):
|
||||
"""Test health check for a server that doesn't exist"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue