Hermolaos.Transport.Stdio (Hermolaos v0.3.0)

View Source

Stdio transport for MCP communication with local subprocess servers.

This transport launches an MCP server as a subprocess and communicates via stdin/stdout using newline-delimited JSON messages.

How It Works

  1. The transport spawns the server command as a subprocess using Erlang ports
  2. JSON-RPC messages are written to the server's stdin (one per line)
  3. Responses are read from stdout and buffered until complete
  4. The owner process receives messages via {:transport_message, pid, msg}

Example

{:ok, transport} = Hermolaos.Transport.Stdio.start_link(
  owner: self(),
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
)

:ok = Hermolaos.Transport.Stdio.send_message(transport, %{
  "jsonrpc" => "2.0",
  "id" => 1,
  "method" => "initialize",
  "params" => %{}
})

# Wait for response
receive do
  {:transport_message, ^transport, message} ->
    IO.inspect(message)
end

Messages Sent to Owner

  • {:transport_ready, pid} - Transport is ready
  • {:transport_message, pid, map} - Received a decoded JSON message
  • {:transport_closed, pid, reason} - Server process exited
  • {:transport_error, pid, error} - Error occurred

Options

  • :owner - PID to receive messages (required)
  • :command - Command to execute (required)
  • :args - Command arguments (default: [])
  • :env - Environment variables as keyword list (default: [])
  • :cd - Working directory for the command (default: current directory)

Summary

Functions

Sends a message asynchronously (non-blocking).

Returns a specification to start this module under a supervisor.

Closes the transport, terminating the subprocess.

Checks if the transport is connected to a running subprocess.

Returns transport information and statistics.

Sends a JSON-RPC message to the server via stdin.

Starts the stdio transport.

Types

option()

@type option() ::
  {:owner, pid()}
  | {:command, String.t()}
  | {:args, [String.t()]}
  | {:env, [{String.t(), String.t()}]}
  | {:cd, String.t()}
  | {:name, GenServer.name()}

state()

@type state() :: %{
  owner: pid(),
  port: port() | nil,
  command: String.t(),
  args: [String.t()],
  env: [{charlist(), charlist()}],
  cd: String.t() | nil,
  buffer: Hermolaos.Transport.MessageBuffer.t(),
  connected: boolean(),
  exit_status: integer() | nil
}

Functions

cast_message(transport, message)

@spec cast_message(GenServer.server(), map()) :: :ok

Sends a message asynchronously (non-blocking).

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close(transport)

@spec close(GenServer.server()) :: :ok

Closes the transport, terminating the subprocess.

connected?(transport)

@spec connected?(GenServer.server()) :: boolean()

Checks if the transport is connected to a running subprocess.

info(transport)

@spec info(GenServer.server()) :: map()

Returns transport information and statistics.

send_message(transport, message)

@spec send_message(GenServer.server(), map()) :: :ok | {:error, term()}

Sends a JSON-RPC message to the server via stdin.

The message map is JSON-encoded and sent as a single line.

Examples

:ok = Hermolaos.Transport.Stdio.send_message(transport, %{
  "jsonrpc" => "2.0",
  "id" => 1,
  "method" => "ping"
})

start_link(opts)

@spec start_link(keyword()) :: {:ok, pid()} | {:error, term()}

Starts the stdio transport.

Options

  • :owner - PID to receive transport messages (required)
  • :command - The command to execute (required)
  • :args - List of command arguments (default: [])
  • :env - Environment variables as [{name, value}] (default: [])
  • :cd - Working directory for the command (optional)
  • :name - GenServer name (optional)

Examples

{:ok, pid} = Hermolaos.Transport.Stdio.start_link(
  owner: self(),
  command: "/usr/bin/python3",
  args: ["-m", "mcp_server"]
)