# `Raxol.MCP.Transport.Stdio`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/mcp/transport/stdio.ex#L1)

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)

# `t`

```elixir
@type t() :: %Raxol.MCP.Transport.Stdio{
  closing: boolean(),
  in_flight: %{optional(term()) =&gt; 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
}
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `handle_manager_call`

# `handle_manager_cast`

# `start_link`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
