skip to content

CLI & MCP Server

The xmsg binary exposes five subcommands: serve, mcp, register, unregister, and check-peers.

#[derive(Parser, Debug)]
#[command(
name = "xmsg",
version,
about = "Fast, lightweight local HTTP bridge into live agent sessions"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Start the HTTP bridge server
Serve(Box<ServeArgs>),
/// Run the stdio MCP server for agent harnesses
Mcp(McpArgs),
/// Register credentials with running xmsg server
Register(RegisterArgs),
/// Explicitly revoke or unregister a session (e.g. `--codex-session <P>`)
Unregister(UnregisterArgs),
/// Load a peers file with every rule `serve` applies, and exit 0 or name the failure
CheckPeers(CheckPeersArgs),
}
#[derive(Parser, Debug)]
pub struct UnregisterArgs {
/// Deliberately revoke a Codex root session ID (`RevokedByOperator`, sticky across resume)
#[arg(long = "codex-session")]
pub codex_session: String,
/// Agent socket path (defaults to $XDG_RUNTIME_DIR/xmsg/agent.sock)
#[arg(long, env = "XMSG_AGENT_SOCK")]
pub sock: Option<PathBuf>,
}
#[derive(Parser, Debug)]
pub struct CheckPeersArgs {
/// Path to the peers file (JSON)
pub peers_file: PathBuf,
/// This node's federation certificate (PEM); with it, the peers file is also checked
/// against this node's own name and key
#[arg(long)]
pub fed_cert: Option<PathBuf>,
}

Starts the local IPC and optional mTLS federation daemon:

#[derive(Parser, Debug)]
pub struct ServeArgs {
/// Optional TCP listen address (IP:PORT), for environments like Kubernetes pods where loopback is a private netns. With no flag, no TCP socket is bound.
#[arg(long, env = "XMSG_LISTEN")]
pub listen: Option<String>,
/// Run as a leaf node (in-pod sidecar, loopback HTTP only, outbound-only federation client)
#[arg(long, env = "XMSG_LEAF")]
pub leaf: bool,
/// Principal to attribute all local requests to in leaf mode (e.g. "svc:matrix")
#[arg(long, env = "XMSG_LEAF_PRINCIPAL")]
pub leaf_principal: Option<String>,
/// Path to unix domain socket for HTTP server (defaults to $XDG_RUNTIME_DIR/xmsg/http.sock; on macOS without it, the Darwin user temp dir)
#[arg(long, env = "XMSG_HTTP_SOCK")]
pub http_sock: Option<PathBuf>,
/// Path to session metadata directory (repeatable, or separated by ':' in env XMSG_SESSIONS_DIR; defaults to ~/.claude/sessions)
#[arg(
long = "sessions-dir",
env = "XMSG_SESSIONS_DIR",
value_delimiter = ':'
)]
pub sessions_dirs: Vec<PathBuf>,
/// Maximum request body size in bytes
#[arg(
long,
env = "XMSG_MAX_BODY",
default_value_t = storage::DEFAULT_MAX_BODY_BYTES
)]
pub max_body: usize,
/// Path to SQLite database
#[arg(long, env = "XMSG_DB_PATH")]
pub db_path: Option<PathBuf>,
/// Reply TTL in seconds (default 7 days: 604800)
#[arg(long, env = "XMSG_REPLY_TTL", default_value_t = 604800)]
pub reply_ttl: u64,
/// Idempotency key retention window in seconds (default 24h: 86400)
#[arg(long, env = "XMSG_IDEMPOTENCY_TTL", default_value_t = 86400)]
pub idempotency_ttl: u64,
/// Per-service pending queue capacity (default: 100)
#[arg(
long,
env = "XMSG_SVC_QUEUE_CAP",
default_value_t = storage::MAX_SVC_QUEUE_PER_SESSION
)]
pub svc_queue_cap: usize,
/// Registration socket path (defaults to $XDG_RUNTIME_DIR/xmsg/register.sock; on macOS without it, the Darwin user temp dir)
#[arg(long, env = "XMSG_REGISTER_SOCK")]
pub register_sock: Option<PathBuf>,
/// Agent socket path (defaults to $XDG_RUNTIME_DIR/xmsg/agent.sock; on macOS without it, the Darwin user temp dir)
#[arg(long, env = "XMSG_AGENT_SOCK")]
pub agent_sock: Option<PathBuf>,
/// Leaf register socket path (defaults to $XDG_RUNTIME_DIR/xmsg/register.sock)
#[arg(long, env = "XMSG_LEAF_REGISTER_SOCK")]
pub leaf_register_sock: Option<PathBuf>,
/// Trusted Antigravity executable paths (comma-separated or multiple flags)
#[arg(long = "agy-exe", env = "XMSG_AGY_EXE", value_delimiter = ',')]
pub agy_exes: Vec<PathBuf>,
/// Trusted Codex executable paths (comma-separated or multiple flags)
#[arg(long = "codex-exe", env = "XMSG_CODEX_EXE", value_delimiter = ',')]
pub codex_exes: Vec<PathBuf>,
/// Trusted Codex home directories (repeatable, or separated by ':' in env XMSG_CODEX_HOME; defaults to $CODEX_HOME or ~/.codex)
#[arg(long = "codex-home", env = "XMSG_CODEX_HOME", value_delimiter = ':')]
pub codex_homes: Vec<PathBuf>,
/// Trusted Svc daemon executable paths per name (format: NAME=PATH, repeatable or comma-separated)
#[arg(long = "svc-exe", env = "XMSG_SVC_EXE", value_delimiter = ',')]
pub svc_exes: Vec<String>,
/// Path to peers configuration file (JSON)
#[arg(long, env = "XMSG_PEERS_FILE")]
pub peers_file: Option<PathBuf>,
/// Federation listen address (IP:PORT), e.g. <ts-ip>:7788
#[arg(long, env = "XMSG_FED_LISTEN")]
pub fed_listen: Option<String>,
/// Path to federation TLS certificate (PEM)
#[arg(long, env = "XMSG_FED_CERT")]
pub fed_cert: Option<PathBuf>,
/// Path to federation TLS private key (PEM)
#[arg(long, env = "XMSG_FED_KEY")]
pub fed_key: Option<PathBuf>,
/// Dynamic federation TLS credential and CA reload interval in seconds (default: 30)
#[arg(long, env = "XMSG_FED_RELOAD_INTERVAL_SECS", default_value = "30")]
pub fed_reload_interval_secs: u64,
}

Starts the stdio Model Context Protocol (MCP) server for agent harnesses:

#[derive(Parser, Debug)]
pub struct McpArgs {
/// Path to session metadata directory (repeatable, or separated by ':' in env XMSG_SESSIONS_DIR; defaults to ~/.claude/sessions)
#[arg(
long = "sessions-dir",
env = "XMSG_SESSIONS_DIR",
value_delimiter = ':'
)]
pub sessions_dirs: Vec<PathBuf>,
/// URL of xmsg HTTP server
#[arg(long, env = "XMSG_URL", default_value = "http://127.0.0.1:7787")]
pub xmsg_url: String,
/// HTTP unix socket path
#[arg(long, env = "XMSG_HTTP_SOCK")]
pub http_sock: Option<PathBuf>,
/// Agent socket path (defaults to $XDG_RUNTIME_DIR/xmsg/agent.sock; on macOS without it, the Darwin user temp dir)
#[arg(long, env = "XMSG_AGENT_SOCK")]
pub agent_sock: Option<PathBuf>,
/// Expose only the reply tool (no list, no send)
#[arg(long, env = "XMSG_REPLY_ONLY")]
pub reply_only: bool,
}

Registers credentials or handles lifecycle hooks for Antigravity, Claude Code, and Codex:

#[derive(Parser, Debug)]
pub struct RegisterArgs {
#[command(subcommand)]
pub harness: RegisterHarness,
}
#[derive(Subcommand, Debug)]
pub enum RegisterHarness {
/// Register Antigravity session credentials
Agy(RegisterAgyArgs),
/// Claude Code subagent hook handler (PreToolUse, SubagentStart, PostToolUse, SubagentStop)
Claude(RegisterClaudeArgs),
/// Codex CLI lifecycle and subagent hook handler
Codex(RegisterCodexArgs),
}
#[derive(Parser, Debug)]
pub struct RegisterCodexArgs {
/// Hook mode for Codex hooks
#[arg(long)]
pub hook: bool,
/// Hook event when `--hook` is passed
#[arg(long, default_value = "")]
pub event: String,
/// Deliberately revoke a Codex root session ID (`RevokedByOperator`, sticky across resume)
#[arg(long, conflicts_with = "unrevoke")]
pub revoke: Option<String>,
/// Clear `RevokedByOperator` on a Codex root session ID back to `StaleAfterResume`
#[arg(long, conflicts_with = "revoke")]
pub unrevoke: Option<String>,
/// Agent socket path (defaults to $XDG_RUNTIME_DIR/xmsg/agent.sock)
#[arg(long, env = "XMSG_AGENT_SOCK")]
pub sock: Option<PathBuf>,
/// Optional label override to assign to the registered Codex session
#[arg(long, env = "XMSG_CODEX_LABEL")]
pub label: Option<String>,
}
#[derive(Parser, Debug)]
pub struct RegisterClaudeArgs {
/// Hook mode for Claude Code hooks
#[arg(long)]
pub hook: bool,
/// Agent socket path (defaults to $XDG_RUNTIME_DIR/xmsg/agent.sock)
#[arg(long, env = "XMSG_AGENT_SOCK")]
pub sock: Option<PathBuf>,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum AgyHookEvent {
PreInvocation,
PreToolUse,
Stop,
}
#[derive(Parser, Debug)]
pub struct RegisterAgyArgs {
/// Registration socket path (defaults to $XDG_RUNTIME_DIR/xmsg/register.sock; on macOS without it, the Darwin user temp dir)
#[arg(long, env = "XMSG_REGISTER_SOCK")]
pub sock: Option<PathBuf>,
/// Agent socket path for subagent hook frames (defaults to the directory of `--sock` or $XDG_RUNTIME_DIR/xmsg/agent.sock)
#[arg(long, env = "XMSG_AGENT_SOCK")]
pub agent_sock: Option<PathBuf>,
/// Hook mode for agy hooks
#[arg(long)]
pub hook: bool,
/// Hook event when `--hook` is passed
#[arg(long, value_enum, default_value_t = AgyHookEvent::PreInvocation)]
pub event: AgyHookEvent,
/// URL of xmsg HTTP server (for checking session credentials in hook mode)
#[arg(long, env = "XMSG_URL", default_value = "http://127.0.0.1:7787")]
pub url: String,
/// Label to assign to the registered Antigravity session
#[arg(long, env = "XMSG_AGY_LABEL", default_value = "agy")]
pub label: String,
}

Validates a peers.json file against all naming, trust-model (pin vs. ca + identities), and non-overlap rules that xmsg serve enforces, exiting 0 on success or non-zero naming the failure:

Terminal window
xmsg check-peers /path/to/peers.json --fed-cert /path/to/node.crt

xmsg mcp exposes five tools over stdio JSON-RPC (or [reply, replies] when started with --reply-only / XMSG_REPLY_ONLY=1):

list(name?, node?, kind?, status?, within?)

Section titled “list(name?, node?, kind?, status?, within?)”

Lists recipients the caller may see on this node and every reachable peer:

  • status: "live" (default, each entry carries registered_at in epoch seconds) or "inactive" (shows parked and closed instances seen within within seconds, default 86400 = 24 hours, capped at the reply TTL, each carrying last_seen).
  • kind: Filter by "service", "instance", or "subagent".
  • name: Filter by service name or instance label.
  • node: An exact canonical name or local nickname queries one node. A bare-host selector queries all matching nodes in the current view; see node selectors.
  • Returns: { recipients: [{ address, kind, harness?, label?, status, node, user, registered_at | last_seen }], nodes: [{ node, status, detail? }] }.

Sends a message to <id or name>[@<node>] (at most one @):

  • to: An instance UUID (optionally ~n), a delegated sub-address (<P>/<child>), a service name, or a unique live instance label, optionally followed by @node. Passing ref, from, or a label containing ' · ' is refused with a migration hint.
  • as_agent: Set automatically by the harness PreToolUse hook when called by a subagent.
  • Returns: { message_id, address, outcome } where outcome is "delivered" or "queued". Use the returned address for follow-ups.

Answers message_id’s other party symmetrically (if you received the message, answers the sender; if you sent it, sends a follow-up to the recipient in the same exchange):

  • Returns: { bytes, messageId, inReplyTo, outcome }, where messageId is the new reply’s ULID and inReplyTo is message_id.

Reads stored replies to message_id, optionally waiting up to wait seconds (clamped to 0..=60) for the first reply.

Drains unexpired messages queued for a bound subagent’s <P>/<child>@<node> turn mailbox while its turn is active, waiting up to wait seconds (clamped to 0..=60) for the first message, and marks drained messages delivered:

  • Returns: { address, messages: [{ id, from, body, created_at }] }.
palette
dark
light
↑↓ select↵ apply esc close

Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).