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
Functions
Returns a specification to start this module under a supervisor.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_call/3.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
Start the stdio transport.
Types
@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
Returns a specification to start this module under a supervisor.
See Supervisor.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_call/3.
Callback implementation for Raxol.Core.Behaviours.BaseManager.handle_manager_cast/2.
@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