Raxol.MCP.Transport.Stdio (Raxol MCP v2.8.0)

Copy Markdown View Source

Stdio transport for MCP.

Reads JSON-RPC messages line-by-line from stdin, routes them to the MCP Server, and writes responses to stdout. This is the transport used by Claude Code and other CLI MCP clients.

Bytes

The wire is bytes, read and written unchanged. A device left in its own encoding re-encodes them: under a non-UTF-8 locale standard input and output are latin1, so non-ASCII text would arrive and leave corrupted, and one invalid byte from the peer switches a UTF-8 device to latin1. The transport therefore sets both devices to latin1, in which a byte is a character, and reads and writes binaries.

Ordering and cancellation

The reader never waits for the server: each line goes out in read order through Raxol.MCP.Server.request/3, so a slow tools/call does not hold up the tools/list behind it, and methods the server handles inline keep their order. Replies arrive in this process's mailbox and only this process writes stdout. Requests run concurrently, up to the server's :max_in_flight_per_connection share of :max_in_flight (half, by default), so a stdio client's excess calls are answered server_busy while other connections keep the rest.

notifications/cancelled cancels the named in-flight request, which kills its callback worker. That holds for a tools/call waiting on an elicitation answer too: it stays in flight until it is answered, so a cancel drops it unanswered, or kills its tool once the answer approved it. A request that reuses the id of one still in flight is answered -32600 and not run. At end of input every reply already produced is still written (methods the server answers inline, such as initialize and ping, and worker-run requests that finished first); every request still running in a callback worker (tools/*, resources/*, prompts/*, completion/complete, logging/setLevel) or waiting on an elicitation answer is cancelled and gets no reply. The transport stops :normal once nothing is outstanding; halting the VM is the caller's job.

If the transport dies any other way (killed, a crash), the server cancels its outstanding requests, since it monitors every Raxol.MCP.Server.request/3 caller, and the linked reader exits with it rather than go on consuming input.

The transport monitors the server it subscribed to and stops with {:server_down, reason} when that server goes down, whether or not a request is pending, and with {:server_down, :noproc} at start if no server is running. A restarted server (Raxol.MCP.Supervisor restarts it under the same name) knows nothing of this transport's subscription, so carrying on would leave it unable to receive notifications or be asked for an elicitation; stopping lets the client restart the whole server process.

The transport traps exits (to see its reader's), and keeps ordinary link semantics otherwise: when the process that started it exits, it stops with the same reason, as any OTP process that traps exits does, and an abnormal exit of any other linked process stops it with that reason (a :normal one is ignored, as an untrapped link would ignore it).

Notifications

Subscribes to the MCP Server for push notifications (e.g. notifications/tools/list_changed). Notifications are written to stdout as unsolicited JSON-RPC messages.

Important

When using this transport, configure Logger to write to stderr to avoid corrupting the JSON-RPC stream on stdout:

config :logger, :default_handler, %{config: %{type: :standard_error}}

Or at runtime:

Logger.configure_backend(:console, device: :standard_error)

Summary

Types

t()

@type t() :: %Raxol.MCP.Transport.Stdio{
  closing: boolean(),
  in_flight: %{optional(term()) => reference()},
  io_device: IO.device(),
  output_device: IO.device(),
  reader: pid() | nil,
  requests: :gen_server.request_id_collection(),
  server: GenServer.server(),
  server_monitor: reference() | nil
}

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

handle_manager_call(request, from, state)

Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_call/3.

handle_manager_cast(msg, state)

Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.

start_link(init_opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Start the stdio transport.

Options

  • :server -- MCP Server pid or name (default: Raxol.MCP.Server)
  • :name -- GenServer name (optional)
  • :io_device -- input device (default: :stdio), useful for testing
  • :output_device -- output device (default: :stdio), useful for testing