from __future__ import annotations import atexit import os import shutil import socket import subprocess import sys import tempfile import time from dataclasses import dataclass, field from pathlib import Path import httpx OPENDOTA_MCP_HTTP_URL = "https://opendota-mcp-server-jylza6gata-ew.a.run.app/mcp" LOCAL_OPENDOTA_MCP_HOST = "127.0.0.1" LOCAL_OPENDOTA_MCP_PORT = int(os.getenv("OPENDOTA_MCP_PORT", "8080")) _managed_process: subprocess.Popen[bytes] | None = None _stderr_log: tempfile._TemporaryFileWrapper[bytes] | None = None @dataclass(frozen=True) class OpenDotaMCPServer: """Configuration for the OpenDota MCP server. The repo intentionally owns this file as `mcp.py`, so the agent uses a lightweight JSON-RPC client instead of importing the Python package named `mcp`, which would collide with this module name. """ transport: str = field(default_factory=lambda: os.getenv("OPENDOTA_MCP_TRANSPORT", "http")) url: str = field(default_factory=lambda: os.getenv("OPENDOTA_MCP_URL", OPENDOTA_MCP_HTTP_URL)) @property def is_http(self) -> bool: return self.transport.lower() == "http" def initialize_opendota_mcp_server() -> OpenDotaMCPServer: """Configure the OpenDota MCP HTTP server for the Gradio app. The Hugging Face Space uses the hosted OpenDota MCP server by default. Set OPENDOTA_MCP_LOCAL=1 to launch a local subprocess instead. """ use_local = os.getenv("OPENDOTA_MCP_LOCAL", "").lower() in {"1", "true", "yes", "on"} if os.getenv("OPENDOTA_MCP_URL") or not use_local: return OpenDotaMCPServer() port = _available_port(LOCAL_OPENDOTA_MCP_PORT) url = f"http://{LOCAL_OPENDOTA_MCP_HOST}:{port}/mcp" os.environ["OPENDOTA_MCP_TRANSPORT"] = "http" os.environ["OPENDOTA_MCP_URL"] = url if _healthcheck(port): return OpenDotaMCPServer() command = _opendota_command() env = os.environ.copy() env["MCP_TRANSPORT"] = "http" env["PORT"] = str(port) if os.getenv("OPENDOTA_API_KEY"): env["OPENDOTA_API_KEY"] = os.environ["OPENDOTA_API_KEY"] global _managed_process, _stderr_log _stderr_log = tempfile.NamedTemporaryFile(prefix="opendota-mcp-", suffix=".log") _managed_process = subprocess.Popen( command, cwd="/tmp", env=env, stdout=subprocess.DEVNULL, stderr=_stderr_log, ) atexit.register(shutdown_opendota_mcp_server) _wait_for_server(port) return OpenDotaMCPServer() def shutdown_opendota_mcp_server() -> None: global _managed_process, _stderr_log if _managed_process is None or _managed_process.poll() is not None: _managed_process = None if _stderr_log is not None: _stderr_log.close() _stderr_log = None return _managed_process.terminate() try: _managed_process.wait(timeout=5) except subprocess.TimeoutExpired: _managed_process.kill() _managed_process.wait(timeout=5) _managed_process = None if _stderr_log is not None: _stderr_log.close() _stderr_log = None def _opendota_command() -> list[str]: configured_command = os.getenv("OPENDOTA_MCP_COMMAND", "") if configured_command: return [configured_command, *_split_args(os.getenv("OPENDOTA_MCP_ARGS", ""))] executable = shutil.which("opendota-mcp") if executable: return [executable] venv_executable = Path(sys.executable).with_name("opendota-mcp") if venv_executable.exists(): return [str(venv_executable)] return [sys.executable, "-m", "opendota_mcp.server"] def _split_args(raw_args: str) -> list[str]: return [item for item in raw_args.split(" ") if item] def _available_port(preferred_port: int) -> int: if _port_is_available(preferred_port): return preferred_port with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind((LOCAL_OPENDOTA_MCP_HOST, 0)) return int(sock.getsockname()[1]) def _port_is_available(port: int) -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: return sock.connect_ex((LOCAL_OPENDOTA_MCP_HOST, port)) != 0 def _healthcheck(port: int) -> bool: try: response = httpx.get( f"http://{LOCAL_OPENDOTA_MCP_HOST}:{port}/health", timeout=0.5, ) return response.status_code == 200 except httpx.HTTPError: return False def _wait_for_server(port: int) -> None: deadline = time.monotonic() + 30 while time.monotonic() < deadline: if _managed_process is not None and _managed_process.poll() is not None: stderr = _read_stderr_log() raise RuntimeError(f"OpenDota MCP server exited during startup. {stderr}") if _healthcheck(port): return time.sleep(0.25) raise TimeoutError("Timed out waiting for the OpenDota MCP server to start.") def _read_stderr_log() -> str: if _stderr_log is None: return "" _stderr_log.flush() with open(_stderr_log.name, "rb") as log_file: return log_file.read().decode("utf-8", errors="replace")