"""Synchronous MCP client for Flask (stdio subprocess).""" from __future__ import annotations import asyncio import atexit import os import threading from typing import Any from backend.mcp_client import PaperMCPClient _client: PaperMCPClient | None = None _connect_lock = threading.Lock() _shutdown_registered = False _loop: asyncio.AbstractEventLoop | None = None _loop_thread: threading.Thread | None = None _loop_ready = threading.Event() def use_mcp() -> bool: return os.getenv("USE_MCP", "1").lower() in {"1", "true", "yes"} def _start_loop_thread() -> asyncio.AbstractEventLoop: global _loop, _loop_thread if _loop is not None and _loop.is_running(): return _loop def _runner() -> None: global _loop loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) _loop = loop _loop_ready.set() loop.run_forever() _loop_ready.clear() _loop_thread = threading.Thread(target=_runner, daemon=True, name="mcp-async-loop") _loop_thread.start() _loop_ready.wait() if _loop is None: raise RuntimeError("MCP event loop failed to start.") return _loop def _run_async(coro, *, timeout: float = 600): loop = _start_loop_thread() future = asyncio.run_coroutine_threadsafe(coro, loop) return future.result(timeout=timeout) def ensure_mcp_connected() -> PaperMCPClient: global _client, _shutdown_registered if _client is not None and _client._session is not None: return _client with _connect_lock: if _client is not None and _client._session is not None: return _client client = PaperMCPClient() _run_async(client.connect()) _client = client if not _shutdown_registered: atexit.register(shutdown_mcp) _shutdown_registered = True return _client def shutdown_mcp() -> None: global _client, _loop if _client is None: return try: _run_async(_client.close(), timeout=30) except Exception: pass _client = None if _loop is not None and _loop.is_running(): _loop.call_soon_threadsafe(_loop.stop) def call_mcp_tool(name: str, arguments: dict[str, Any] | None = None) -> Any: client = ensure_mcp_connected() return _run_async(client.call_tool(name, arguments))