diff --git a/examples/python/voice-agent/integrations/voice/doctor.py b/examples/python/voice-agent/integrations/voice/doctor.py index a1f0e22..2964cbd 100644 --- a/examples/python/voice-agent/integrations/voice/doctor.py +++ b/examples/python/voice-agent/integrations/voice/doctor.py @@ -17,6 +17,25 @@ def _tcp_open(host: str, port: int, timeout: float = 2.0) -> bool: return False +_MCP_PROBE_TIMEOUT_SEC = 2.0 + + +def _is_slow_mcp_probe(detail: str) -> bool: + lowered = detail.lower() + return any(token in lowered for token in ("timed out", "timeout", "temporarily unavailable")) + + +def _mcp_servers_probe(app_port: int) -> tuple[str, str]: + """Probe MCP list endpoint; slow stdio connect must not fail the whole doctor run.""" + status, detail = _http_status( + f"http://127.0.0.1:{app_port}/api/mcp/servers", + timeout=_MCP_PROBE_TIMEOUT_SEC, + ) + if status == "fail" and _is_slow_mcp_probe(detail): + return "warn", detail + return status, detail + + def _http_status(url: str, timeout: float = 5.0) -> tuple[str, str]: try: req = Request(url, headers={"User-Agent": "praisonai-voice-doctor"}) @@ -64,7 +83,7 @@ def run_voice_doctor(*, app_port: int = 8001, sidecar_port: int = 8002) -> dict[ checks.append({"name": "Speech Engine config", "status": status, "detail": detail}) status, detail = _http_status(f"http://127.0.0.1:{app_port}/api/voice/realtime/config") checks.append({"name": "GPT Realtime config", "status": status, "detail": detail}) - status, detail = _http_status(f"http://127.0.0.1:{app_port}/api/mcp/servers") + status, detail = _mcp_servers_probe(app_port) checks.append({"name": "MCP servers API", "status": status, "detail": detail}) else: checks.append( diff --git a/tests/unit/test_voice_agent.py b/tests/unit/test_voice_agent.py index 774e8d2..b2e03e0 100644 --- a/tests/unit/test_voice_agent.py +++ b/tests/unit/test_voice_agent.py @@ -478,6 +478,26 @@ def test_voice_doctor_report(monkeypatch): assert report["summary"]["failed"] >= 1 +def test_voice_doctor_mcp_probe_timeout_is_warn(monkeypatch): + monkeypatch.setenv("OPENAI_API_KEY", "sk-test-key-12345678") + + def fake_http_status(url: str, timeout: float = 5.0): + if "/api/mcp/servers" in url: + assert timeout == 2.0 + return "fail", "timed out" + return "pass", "HTTP 200" + + with _voice_modules() as imp: + doctor = imp("integrations.voice.doctor") + monkeypatch.setattr(doctor, "_tcp_open", lambda *args, **kwargs: True) + monkeypatch.setattr(doctor, "_http_status", fake_http_status) + report = doctor.run_voice_doctor(app_port=8001, sidecar_port=59998) + + mcp = next(c for c in report["checks"] if c["name"] == "MCP servers API") + assert mcp["status"] == "warn" + assert mcp["detail"] == "timed out" + + def test_call_detail_ui_includes_analytics(): with _voice_modules() as imp: ui = imp("integrations.voice.call_detail_ui")