Spaces:
Running
Running
| 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 | |
| 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)) | |
| 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") | |