defmodule AiDebug.MCPBridgeManager do @moduledoc """ Manages the MCP HTTP bridge process to ensure connectivity between Phoenix and TypeScript MCP server. Automatically starts the HTTP bridge when needed and monitors its health. """ use GenServer require Logger @bridge_port 3010 @bridge_script_path Path.expand("../../../scripts/start-mcp-http.cjs", __DIR__) @health_check_interval 30_000 # 30 seconds @startup_timeout 10_000 # 10 seconds def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc """ Ensure the MCP HTTP bridge is running and healthy """ def ensure_bridge_running do GenServer.call(__MODULE__, :ensure_bridge_running, @startup_timeout) end @doc """ Check if the MCP HTTP bridge is currently running """ def bridge_running? do case HTTPoison.get("http://localhost:#{@bridge_port}/health", [], recv_timeout: 5000) do {:ok, %{status_code: 200}} -> true _ -> false end end @doc """ Get bridge status information """ def bridge_status do GenServer.call(__MODULE__, :bridge_status) end @impl true def init(_opts) do # Start with bridge check send(self(), :check_bridge) {:ok, %{ bridge_pid: nil, bridge_port: nil, last_health_check: nil, status: :starting }} end @impl true def handle_call(:ensure_bridge_running, _from, state) do case ensure_bridge_healthy(state) do {:ok, new_state} -> {:reply, :ok, new_state} {:error, reason} -> {:reply, {:error, reason}, state} end end @impl true def handle_call(:bridge_status, _from, state) do status = %{ running: bridge_running?(), pid: state.bridge_pid, port: @bridge_port, last_health_check: state.last_health_check, status: state.status } {:reply, status, state} end @impl true def handle_info(:check_bridge, state) do new_state = case ensure_bridge_healthy(state) do {:ok, updated_state} -> updated_state {:error, _reason} -> state end # Schedule next health check Process.send_after(self(), :check_bridge, @health_check_interval) {:noreply, new_state} end @impl true def handle_info({:DOWN, _ref, :process, pid, reason}, %{bridge_pid: pid} = state) do Logger.warning("MCP HTTP bridge process died: #{inspect(reason)}") # Clear the pid and let next health check restart it new_state = %{state | bridge_pid: nil, status: :down } {:noreply, new_state} end @impl true def handle_info(msg, state) do Logger.debug("MCPBridgeManager received unexpected message: #{inspect(msg)}") {:noreply, state} end # Private functions defp ensure_bridge_healthy(state) do if bridge_running?() do {:ok, %{state | status: :running, last_health_check: DateTime.utc_now() }} else Logger.info("MCP HTTP bridge not running, starting...") start_bridge(state) end end defp start_bridge(state) do if not File.exists?(@bridge_script_path) do {:error, "MCP HTTP bridge script not found at #{@bridge_script_path}"} else # Kill existing bridge process if we have one if state.bridge_pid do Process.exit(state.bridge_pid, :kill) end # Start new bridge process try do case System.cmd("node", [@bridge_script_path], [ cd: Path.dirname(@bridge_script_path), stderr_to_stdout: true, into: IO.stream(:stdio, :line) ]) do {_output, 0} -> # Process started successfully, but we need to get the actual PID # For now, we'll rely on health checks to verify it's running # Wait for startup :timer.sleep(3000) if bridge_running?() do Logger.info("MCP HTTP bridge started successfully on port #{@bridge_port}") {:ok, %{state | status: :running, last_health_check: DateTime.utc_now() }} else Logger.error("MCP HTTP bridge failed to start properly") {:error, "Bridge failed to start"} end {output, exit_code} -> Logger.error("Failed to start MCP HTTP bridge: #{output}, exit code: #{exit_code}") {:error, "Bridge startup failed"} end rescue error -> Logger.error("Exception starting MCP HTTP bridge: #{inspect(error)}") {:error, "Bridge startup exception"} end end end defp start_bridge_async(state) do # Start bridge in background pid = spawn_link(fn -> case System.cmd("node", [@bridge_script_path], [ cd: Path.dirname(@bridge_script_path), stderr_to_stdout: false ]) do {_output, 0} -> Logger.info("MCP HTTP bridge started successfully") {output, exit_code} -> Logger.error("MCP HTTP bridge failed: #{output}, exit code: #{exit_code}") end end) # Monitor the process Process.monitor(pid) # Wait for startup :timer.sleep(3000) if bridge_running?() do {:ok, %{state | bridge_pid: pid, status: :running, last_health_check: DateTime.utc_now() }} else {:error, "Bridge failed to start"} end end end