defmodule AiDebug.MCP.StdioTransport do @moduledoc """ STDIO transport for MCP communication. Handles reading from stdin and writing to stdout. """ use GenServer require Logger alias AiDebug.MCP.Protocol def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end def init(_opts) do # Set binary mode for stdio :io.setopts(:standard_io, [:binary, encoding: :utf8]) # Start reading from stdin send(self(), :read_loop) {:ok, %{buffer: ""}} end def handle_info(:read_loop, state) do # Read from stdin case IO.binread(:standard_io, :line) do :eof -> Logger.info("STDIO closed, shutting down") {:stop, :normal, state} {:error, reason} -> Logger.error("STDIO read error: #{inspect(reason)}") {:stop, reason, state} data -> # Process the incoming data new_state = process_data(data, state) # Continue reading send(self(), :read_loop) {:noreply, new_state} end end # Process incoming data defp process_data(data, state) do buffer = state.buffer <> data # Check if we have a complete message (ending with newline) case String.split(buffer, "\\n", parts: 2) do [complete_message, rest] -> # Process the complete message handle_message(complete_message) # Update buffer with remaining data %{state | buffer: rest} [incomplete] -> # Keep buffering %{state | buffer: incomplete} end end # Handle a complete MCP message defp handle_message(message) do case Protocol.decode_request(message) do {:ok, request} -> # Process the request response = case Protocol.handle_method(request.method, request.params) do {:ok, result} -> Protocol.encode_response(request.id, result) {:error, code, message, data} -> Protocol.encode_error(request.id, Protocol.error_code(code), message, data) {:error, message} -> Protocol.encode_error(request.id, Protocol.error_code(:internal_error), message) end # Write response to stdout write_response(response) {:error, :parse_error} -> error = Protocol.encode_error(nil, Protocol.error_code(:parse_error), "Parse error") write_response(error) {:error, :invalid_request} -> error = Protocol.encode_error(nil, Protocol.error_code(:invalid_request), "Invalid request") write_response(error) end end # Write response to stdout defp write_response(response) do IO.binwrite(:standard_io, response <> "\\n") :ok end @doc """ Send a notification to the client """ def send_notification(method, params) do notification = %{ jsonrpc: "2.0", method: method, params: params } |> Jason.encode!() IO.binwrite(:standard_io, notification <> "\\n") end end