Skip to main content
Glama
diagnose_ws_stack.py8.77 kB
#!/usr/bin/env python3 """ EX-AI MCP WS Stack Diagnoser Purpose: One-stop diagnosis for WebSocket daemon + stdio shim + classic wrapper. - Prints clear PASS/FAIL per check with actionable hints - Reads .env automatically (or ENV_FILE override) - Validates files, env, venv, Python packages - Tests WS daemon reachability (hello, list_tools, version) - Tests shim as an MCP stdio server (initialize, list_tools) - Validates Augment mcp-config.augmentcode.json entries (paths, args, cwd) Safe to run: does not modify state. Network access only to ws://EXAI_WS_HOST:EXAI_WS_PORT. """ from __future__ import annotations import asyncio import json import os import sys import textwrap from dataclasses import dataclass from pathlib import Path from typing import Any, Optional # Repo root ROOT = Path(__file__).resolve().parents[1] VENV_PY = ROOT / ".venv" / "Scripts" / "python.exe" LOGS = ROOT / "logs" ENV_FILE = os.getenv("ENV_FILE") or str(ROOT / ".env") # Load .env early for consistent behavior across classic and daemon paths try: from dotenv import load_dotenv # type: ignore if os.path.exists(ENV_FILE): load_dotenv(dotenv_path=ENV_FILE) else: # Fallback to repo .env fallback_env = ROOT / ".env" if fallback_env.exists(): load_dotenv(dotenv_path=str(fallback_env)) except Exception: pass WS_HOST = os.getenv("EXAI_WS_HOST", "127.0.0.1") WS_PORT = int(os.getenv("EXAI_WS_PORT", "8765")) WS_TOKEN = os.getenv("EXAI_WS_TOKEN", "") @dataclass class Check: name: str ok: bool info: str = "" hint: str = "" RESULTS: list[Check] = [] def add(name: str, ok: bool, info: str = "", hint: str = "") -> None: RESULTS.append(Check(name, ok, info, hint)) status = "PASS" if ok else "FAIL" print(f"- {name:20s} {status} :: {info or hint}") def section(title: str) -> None: print("\n" + title) print("-" * len(title)) def check_files() -> None: section("1) Files & Structure") expected = { "server.py": ROOT / "server.py", "scripts/run_ws_shim.py": ROOT / "scripts" / "run_ws_shim.py", "scripts/run_ws_daemon.py": ROOT / "scripts" / "run_ws_daemon.py", "scripts/mcp_server_wrapper.py": ROOT / "scripts" / "mcp_server_wrapper.py", "src/daemon/ws_server.py": ROOT / "src" / "daemon" / "ws_server.py", "tools/ws_daemon_smoke.py": ROOT / "tools" / "ws_daemon_smoke.py", "mcp-config.augmentcode.json": ROOT / "mcp-config.augmentcode.json", } ok = True for label, path in expected.items(): exists = path.exists() print(f" {label:32s} exists={exists} path={path}") ok = ok and exists add("structure", ok, hint="Create missing paths or fix repo layout") def check_python_env() -> None: section("2) Python & VENV") try: py = sys.executable print(" python:", py) print(" version:", sys.version) if VENV_PY.exists(): add("venv", True, info=f"VENV present: {VENV_PY}") else: add("venv", False, hint="Create .venv and install deps") except Exception as e: add("python", False, hint=str(e)) def check_packages() -> None: section("3) Packages") ok = True try: import websockets # noqa: F401 except Exception as e: ok = False print(" websockets missing:", e) try: from mcp.server import Server # noqa: F401 from mcp.client.stdio import stdio_client, StdioServerParameters # noqa: F401 from mcp.client.session import ClientSession # noqa: F401 except Exception as e: ok = False print(" mcp package issue:", e) add("packages", ok, hint="Install 'websockets' and 'mcp' in the venv") def check_env() -> None: section("4) Environment") keys = [ "ENV_FILE", "LOG_LEVEL", "DEFAULT_MODEL", "KIMI_API_KEY", "GLM_API_KEY", "EXAI_WS_HOST", "EXAI_WS_PORT", "EXAI_WS_TOKEN", ] for k in keys: v = os.getenv(k) shown = ("[SET]" if ("_KEY" in k and v) else (v if v else "[MISSING]")) print(f" {k:16s} = {shown}") add("env", True, info=".env processed and inspected") async def test_ws_daemon() -> bool: import websockets uri = f"ws://{WS_HOST}:{WS_PORT}" try: async with websockets.connect(uri) as ws: await ws.send(json.dumps({"op": "hello", "session_id": "diag", "token": WS_TOKEN})) ack = json.loads(await ws.recv()) if not ack.get("ok"): print(" hello failed:", ack) return False await ws.send(json.dumps({"op": "list_tools"})) tools = json.loads(await ws.recv()) names = [t.get("name") for t in tools.get("tools", [])] print(f" WS tools: {len(names)}") if "version" in names: rid = "diag-version" await ws.send(json.dumps({"op": "call_tool", "request_id": rid, "name": "version", "arguments": {}})) while True: msg = json.loads(await ws.recv()) if msg.get("op") == "call_tool_res" and msg.get("request_id") == rid: if msg.get("error"): print(" version error:", msg.get("error")) else: outs = msg.get("outputs") or [] text = outs[0].get("text")[:120] if outs else "<none>" print(" version ok:", text) break return True except Exception as e: print(" WS test failed:", e) return False async def test_stdio_shim() -> bool: try: from mcp.client.stdio import stdio_client, StdioServerParameters from mcp.client.session import ClientSession except Exception as e: print(" mcp client not available:", e) return False cmd = str(VENV_PY if VENV_PY.exists() else sys.executable) shim = str(ROOT / "scripts" / "run_ws_shim.py") env = {**os.environ, "PYTHONPATH": str(ROOT), "PYTHONIOENCODING": "utf-8", "LOG_LEVEL": os.getenv("LOG_LEVEL", "DEBUG")} try: async with stdio_client(StdioServerParameters(command=cmd, args=["-u", shim], cwd=str(ROOT), env=env)) as (read, write): async with ClientSession(read, write) as session: await asyncio.wait_for(session.initialize(), timeout=20) tools = await asyncio.wait_for(session.list_tools(), timeout=20) print(" Shim MCP tools:", len(tools.tools)) return True except Exception as e: print(" Shim MCP test failed:", e) return False def check_mcp_config() -> None: section("5) Augment MCP Config") path = ROOT / "mcp-config.augmentcode.json" if not path.exists(): add("mcp_config", False, hint="mcp-config.augmentcode.json missing") return try: cfg = json.loads(path.read_text(encoding="utf-8")) servers = (cfg.get("mcpServers") or {}) exai_ws = servers.get("EXAI-WS") or {} cmd = exai_ws.get("command"); args = exai_ws.get("args") or []; cwd = exai_ws.get("cwd") abs_ok = all([(isinstance(a, str) and (":/" in a or ":\\" in a or a.endswith(".py") and (ROOT / a).exists())) for a in args]) print(" EXAI-WS:") print(" command:", cmd) print(" args:", args) print(" cwd:", cwd) if not abs_ok: print(" NOTE: some args look relative; prefer absolute path to run_ws_shim.py on Windows.") add("mcp_config", True, info="Parsed EXAI-WS entry") except Exception as e: add("mcp_config", False, hint=f"Parse failed: {e}") def summarize() -> int: section("Summary") bad = [r for r in RESULTS if not r.ok] for r in RESULTS: status = "PASS" if r.ok else "FAIL" print(f" {status:4s} - {r.name}: {r.info or r.hint}") print("\nOVERALL:", "OK" if not bad else "ATTENTION NEEDED") return 0 if not bad else 1 def main() -> int: print(f"EX WS Stack Diagnostics — {ROOT}") check_files() check_python_env() check_packages() check_env() # Async tests loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) print("\n6) WS Daemon Test") ok_ws = loop.run_until_complete(test_ws_daemon()) add("ws_daemon", ok_ws, hint="Start run_ws_daemon.py and ensure EXAI_WS_* env") print("\n7) Stdio Shim (MCP) Test") ok_shim = loop.run_until_complete(test_stdio_shim()) add("stdio_shim", ok_shim, hint="Fix stdio launch (args/cwd/encoding) or see Augment Output") check_mcp_config() return summarize() if __name__ == "__main__": sys.exit(main())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Zazzles2908/EX_AI-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server