The phux MCP adapter

evolving

The phux MCP adapter

TL;DR. This doc covers what is MCP-specific in phux-mcp: the six JSON-RPC stdio tools (phux_ls, phux_snapshot, phux_send_keys, phux_run, phux_wait, phux_new), the stdio transport and lifecycle, how a tool resolves a target client-side, and the tools/call envelope. The structured shapes the tools return and the selector grammar are the shared agent surface and live in their owning docs; this file links them.


0. What this is, what this isn’t

This is the MCP adapter only. phux-mcp has no separate core: each tool is a thin wrapper over the same phux-client functions the agent CLI verbs use (agents.md). Like every consumer it holds no protocol-level privilege (ADR-0017); the structured agent surface is a local projection over the shared engine, exposed through the CLI and its versioned JSON schema, not a wire service (ADR-0030).

Two things this file does not restate, by the “one fact, one home” rule:

  • The structured return shapes — ScreenState, RunResult, SessionListJson — are owned by agents.md §3. Each tool below names the shape and links there.
  • The selector grammar is owned by tui.md §3. §2 below links it.

1. Transport and lifecycle

phux-mcp speaks JSON-RPC 2.0 over the MCP stdio transport: newline-delimited JSON, one message per line on stdin and stdout. The JSON-RPC is hand-rolled over serde_json — no framework dependency.

The MCP protocol version is pinned to the 2024-11-05 revision. Newer MCP revisions are additive; the pin is bumped when the adapter adopts one.

The methods:

MethodReply
initializeprotocolVersion, capabilities ({ "tools": {} }), and serverInfo (name = "phux", version = the crate version)
notifications/initializednone (it is a notification)
tools/listthe tool catalog (see §3)
tools/calldispatch by tool name (see §3, §4)
pingan empty result (keepalive)

Robustness: a malformed line yields a JSON-RPC parse error with a null id; an unknown method on a request yields a method-not-found error (an unknown notification, having no id, is silently ignored). Neither stops the loop, which runs until stdin EOF.


2. How a tool resolves a target

Every targeted tool (phux_snapshot, phux_send_keys, phux_run, phux_wait) takes a target selector string in the same grammar as the CLI’s TARGET, whose table and examples live in tui.md §3. In one line, the forms are: . (current), = (last), name (session), name:N / name:tag (window), name:N.M (pane), and @N (opaque id).

Resolution is client-side, exactly as the CLI resolves it (ADR-0021): the adapter fetches a state snapshot, expands the selector to candidate TerminalIds, then narrows to a single pane — the focused pane if it is among the candidates, else the first in snapshot order. This is the same pick_target_pane tiebreak the CLI uses. The server never parses a selector.

target optionality differs per tool:

  • phux_snapshot and phux_wait make target optional; when absent it defaults to the focused/last session (Selector::Last).
  • phux_send_keys and phux_run require target.

Every tool also takes an optional socket string naming the Unix-domain socket to connect to. Precedence: an explicit socket argument, then the PHUX_SOCKET environment variable, then the daemon default ($XDG_RUNTIME_DIR/phux/phux.sock, falling back to /tmp/phux-$UID/phux.sock — the segment is $UID, then $USER, then the literal default).


3. The tool catalog

Six tools, returned verbatim by tools/list. Each inputSchema is a JSON Schema object. Tools that take no required argument (e.g. phux_ls) work with no arguments at all. The return shapes are the shared agent shapes owned by agents.md §3; each tool names its shape and links there.

3.1 phux_ls

Lists phux sessions on the running server. No target.

ParamTypeRequiredMeaning
socketstringnoOverride the UDS path (see §2).

Result: { "sessions": [ { "name", "window_count", "attached_client_count" } ] }, sorted by name. The MCP tool surfaces the raw wire fields (window_count / attached_client_count); the CLI’s ls --json projects them to windows / attached. The two surfaces do not share identical keys (agents.md §3.1) — do not carry a parser across them.

3.2 phux_snapshot

Captures a pane as structured screen data. Side-effect-free: it does not attach or resize.

ParamTypeRequiredMeaning
targetstringnoSelector (see §2). Defaults to focused/last.
scrollbacknumbernoTri-state — see below.
cellsbooleannoWhen true, include per-cell OSC-133 marks and styles. Default false.
socketstringnoOverride the UDS path (see §2).

scrollback is tri-state: absent captures the viewport only; 0 captures all retained history; N captures the most-recent N rows.

Result: a serialized ScreenState — the same struct phux snapshot emits, with cells populated only when cells is true. The field catalog (schema version, cursor, lines, scrollback, the sparse cells array) is owned by agents.md §3.2.

3.3 phux_send_keys

Routes input to the resolved pane by id. No attach, no resize.

ParamTypeRequiredMeaning
targetstringyesSelector (see §2).
keysarray of stringyesKeys to send; must be non-empty.
socketstringnoOverride the UDS path (see §2).

Each entry in keys is a named key (Enter, Tab, C-c, …) or a literal string, tmux-style.

Result: { "sent": true, "pane": "<pane>" }. pane is rendered via the TerminalId Debug formatting, not a stable numeric id — TerminalId has no Serialize impl yet (tracked under phux-93b), so do not parse it as a number.

3.4 phux_run

Runs a command in the resolved pane and reports its result. Assumes a POSIX shell.

ParamTypeRequiredMeaning
targetstringyesSelector (see §2).
commandstringyesThe command line to run.
timeout_secsnumbernoDefault 600; 0 waits indefinitely.
socketstringnoOverride the UDS path (see §2).

Result on completion: a serialized RunResult ({ command, exit_code, output, duration_ms, truncated }), shape owned by agents.md §3.3.

MCP-vs-CLI divergence — the timeout shape. On timeout the MCP tool returns a JSON body: { "outcome": "timed_out", "command", "duration_ms" }. The CLI’s run --json does not: it emits no JSON on timeout and signals the timeout through exit code 125 (agents.md §3.3). An agent driving MCP reads the outcome body; an agent driving the CLI reads the exit code. This is the one genuine shape difference between the two surfaces; everything else is name-for-name.

3.5 phux_wait

Polls the resolved pane until a condition holds.

ParamTypeRequiredMeaning
targetstringnoSelector (see §2). Defaults to focused/last.
untilstringnoSucceed once a visible line contains this substring.
idle_msnumbernoSucceed once the screen holds still this long.
timeout_secsnumbernoGive up after this many seconds. Default: wait forever.
socketstringnoOverride the UDS path (see §2).

Condition precedence: until wins when present (succeed on a substring match); otherwise the tool settles on idle, using idle_ms or the default dwell when idle_ms is absent.

Result: { "outcome": "met" | "timed_out", "polls": N }.

3.6 phux_new

Creates a named session on the running server without attaching. The server must already be running (this tool does not auto-spawn one).

ParamTypeRequiredMeaning
namestringyesName for the new session. A name already in use is rejected.
commandarraynoInitial command (argv) for the seed pane. Omit or pass [] for the server’s default shell.
cwdstringnoWorking directory for the seed pane.
socketstringnoOverride the UDS path (see §2).

Result: the new session’s name and seed pane id.

3.7 Event stream (phux watch) — not yet an MCP tool

The push half of the agent surface — a subscribed stream of tagged lifecycle/activity events — ships today as the CLI verb phux watch, an accelerator of phux_wait’s poll floor. It is not exposed as an MCP tool in this pass: MCP tools/call is request/response, whereas the event stream is a long-lived push, so a streaming phux_watch tool needs an MCP notification/streaming shape that is a separate ticket. The phux_wait polling tool remains the MCP-native way to block on a pane condition; an MCP host that wants the stream meanwhile shells out to phux watch --json.


4. A worked tools/call example

A phux_run against an explicit pane, target work:1.0:

Request (one line on stdin):

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"phux_run","arguments":{"target":"work:1.0","command":"cargo test"}}}

Success response:

{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\n  \"command\": \"cargo test\",\n  \"exit_code\": 0,\n  \"output\": \"...\",\n  \"duration_ms\": 8123,\n  \"truncated\": false\n}"}],"isError":false}}

The result is a single text content block. A structured result is pretty-printed JSON (here, the serialized RunResult); a bare error string is shown verbatim.

A tool failure — no such target, no running server, a malformed argument — is a successful JSON-RPC response carrying isError: true, never a JSON-RPC error and never a crash. Contrast this with protocol-level errors (a parse error, an unknown method, missing tools/call params), which are JSON-RPC error responses.

A second example, phux_snapshot of a pane by opaque id with scrollback and per-cell data:

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"phux_snapshot","arguments":{"target":"@42","scrollback":200,"cells":true}}}

This returns the last 200 scrollback rows plus the viewport, with the sparse per-cell cells array populated.


5. Relationship to the CLI

The MCP tools are name-for-name the CLI’s agent subcommands: phux_lsphux ls, and phux_snapshot / phux_send_keys / phux_run / phux_wait / phux_newphux snapshot / send-keys / run / wait / new (agents.md §1). Same client-side resolution, same tiebreaks, because the adapter wraps the same phux-client functions the CLI does. The one shape difference is the phux_run timeout body (§3.4).

Per ADR-0022, the CLI and its JSON schema are the stable agent contract; the wire underneath stays additive and versioned, and MCP is one thin adapter over it among several.