rex-mcp
# rex-mcp
Let an AI agent read, type into, and run commands in [Rex](https://www.superlogical.com/rex) terminal blocks on your own machine, through the `rex` CLI.
rex-mcp is an MCP server over stdio by default, with an optional HTTP mode (bearer token) for clients on other machines. Ask the agent to list your sessions, read a block's screen, type a command into a block, or run a command and wait for it to finish.
It follows the design of herdr-mcp: one passthrough tool that runs a fixed allowlist of CLI verbs, plus a skill tool that serves the CLI's own guide. The differences are the CLI underneath and the transports (stdio by default; HTTP with a bearer token when you enable it, where herdr-mcp is HTTP-only).
## What you need
- **Rex** installed, with its app or server available on this machine. The CLI finds the local server on its own; like plain `rex`, a call may autostart a missing server.
- **Node.js 22 or later.**
- **`rex` on the PATH** of the process that spawns the MCP server, or `REX_MCP_BINARY` pointing at it. Inside Rex terminals this is automatic; outside, check `which rex`.
## Install
```sh
git clone <this repo> && cd rex-mcp
npm install
npm test
```
## Run it
The server speaks MCP over stdio, so a client spawns it; running it by hand just waits for JSON-RPC on stdin. Point a client's MCP config at it:
```json
{
"mcpServers": {
"rex-mcp": {
"command": "node",
"args": ["/absolute/path/to/rex-mcp/bin/rex-mcp.mjs"]
}
}
}
```
On startup it prints one probe result to stderr: the Rex server endpoint it will drive (a unix socket or a host address), the server version and session count. There is no HTTP host or port — if you see `Rex server: not reachable`, the first tool call may still autostart the local server, like plain `rex` does.
Or install the package and use the binary: `npm install -g rex-mcp` (or `just link` from a checkout), then set `"command": "rex-mcp"`.
Optional environment variables:
| Variable | Default | Meaning |
| --- | --- | --- |
| `REX_MCP_BINARY` | `rex` | Path to the `rex` executable. `REX_BIN_DIR` is used as a fallback parent directory. |
| `REX_MCP_WAIT_MAX` | `90000` | Wait ceiling in milliseconds for `wait` calls. |
| `REX_MCP_HTTP` | unset | Set to `1` to serve Streamable HTTP instead of stdio. Same as `--http`. |
| `REX_MCP_HOST` | `127.0.0.1` | HTTP bind address (`--host` overrides). |
| `REX_MCP_PORT` | `8791` | HTTP port (`--port` overrides). |
| `REX_MCP_TOKEN_FILE` | `~/.config/rex-mcp/token` | Bearer token file for HTTP mode, mode 0600, 64 hex chars. |
| `REX_MCP_ORIGINS` | empty | Comma-separated Origin allowlist for HTTP mode; empty allows all. |
On startup it prints one probe result to stderr: the Rex server endpoint it will drive (a unix socket or a host address), the server version and session count. In stdio mode there is no HTTP host or port — if you see `Rex server: not reachable`, the first tool call may still autostart the local server, like plain `rex` does.
## HTTP mode for other machines
`rex-mcp --http` (or `just serve`) serves Streamable HTTP at `/mcp` with bearer auth instead of stdio. On first start it generates the token file (`~/.config/rex-mcp/token`, mode 0600); `rex-mcp token` prints a fresh token for rotation. Clients authenticate with a header:
```
Authorization: Bearer <token>
```
A generic remote client config looks like:
```json
{
"mcpServers": {
"rex-mcp": {
"url": "https://<host>.<tailnet>.ts.net:8443/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
```
### Reaching it over Tailscale
Rex ships with its own Tailscale integration (`rex server status` shows the node), but the bridge is a separate process, so put it on the tailnet with `tailscale serve`:
```sh
rex-mcp --http & # binds 127.0.0.1:8791
tailscale serve --bg --https=8443 http://127.0.0.1:8791
```
The remote instance then talks to `https://<host>.<tailnet>.ts.net:8443/mcp` with the token header — use the URL `tailscale serve` prints, since the node name there can differ from the one Rex's own integration shows (macOS can run two Tailscale nodes). Port 8443 keeps this clear of Rex's built-in Tailscale HTTPS, which occupies the node's default port. Traffic stays inside the tailnet: WireGuard-encrypted, invisible to the public internet.
On macOS the Tailscale CLI lives inside the app: `/Applications/Tailscale.app/Contents/MacOS/Tailscale serve ...`. Serve targets must be on this machine (`127.0.0.1`), so bind the bridge to its default and let serve proxy it.
Alternatives:
- **Bind to the tailnet IP directly**: `rex-mcp --http --host <tailscale-ip>`. No proxy, plain HTTP inside the tailnet; fine for tailnet-only clients.
- **Public internet**: `tailscale funnel --bg --https=8443 http://127.0.0.1:8791` exposes the URL beyond the tailnet. Only do this if a hosted client outside your tailnet needs it; the token is then the only gate.
To undo a serve rule: `tailscale serve --https=8443 off`.
## Tasks
`just` drives everything; run `just` with no arguments to list recipes.
| Recipe | What it does |
| --- | --- |
| `just install` | Install dependencies. |
| `just test` | Run the test suite, no Rex server needed. |
| `just run` | Start the server on stdio. |
| `just serve` | Start the server in HTTP mode on 127.0.0.1:8791. |
| `just live` | End-to-end probe against the real Rex server (creates and closes two blocks). |
| `just pack` | Build the npm tarball into `dist/`. |
| `just link` | `npm link` so `rex-mcp` is on PATH. |
| `just publish` | Publish the current version to npm. |
| `just clean` | Remove `dist/`. |
## Package and release
`npm pack` ships only `bin/`, `src/`, README and LICENSE (see the `files` field), and `prepublishOnly` runs the tests first.
CI (`.github/workflows/ci.yml`) runs the suite on push and pull requests across Ubuntu and macOS with Node 22 and 24.
Releases (`.github/workflows/release.yml`) run when you push a `v*` tag: tests, tarball, GitHub release with the tarball attached, then `npm publish`. Setup, once:
1. Push the repo to GitHub and `git init` + commit if you have not already.
2. Create an npm automation token and add it as the repository secret `NPM_TOKEN`.
3. Cut a release: `npm version patch && git push --follow-tags`.
For a local release without CI: `just pack`, then `npm publish` (or `just publish`).
## Tools
**`rex`** runs one `rex` command. Supply exactly one of `args` (an array of strings) or `command` (a quote-only string; no shell operators, substitution, or expansion).
Allowed verbs, with what the bridge does to them:
| Verb | Notes |
| --- | --- |
| `list`, `ls` | List sessions. |
| `session ls/inspect`, `window ls/inspect` | Discovery. |
| `block ls/inspect/types/capture/send/send-key` | Inspect and drive one block. |
| `client ls/inspect` | See connected clients. |
| `api ls/list/describe` | Read the server's control API surface. |
| `capture` | Print a block's screen (`text`, `html` or `vt`). An explicit target is required. |
| `send`, `send-key` | Type into a terminal. An explicit target is required: `-b BLOCK`, `-w WINDOW`, or `-s SESSION`. |
| `run` | Start a command in a new block; add `--wait` for its exit status and `--keep-open` to read its output with `capture` afterwards. |
| `wait` | Block until a target ends. The bridge pins `--for` at the ceiling (default 90 s). |
| `help` | Read command syntax. |
Listing and inspection calls get `--json` appended, so output is stable to parse. Failure envelopes carry a `code`, a `message`, and recovery advice in `next`. Rex's exit statuses map as: 2 invalid arguments, 3 target not found or ambiguous, 4 server unreachable, 130 cancelled.
The bridge runs outside any Rex terminal, so there is no default session or block: scope calls explicitly. Two patterns cover most work:
```sh
# Run a command and read its output
rex run -s SESSION --wait --keep-open -- CMD # exit status comes back; block stays open
rex capture -b BLOCK --trim # the command's output
# Work in an existing terminal
rex send -b BLOCK 'make test' # type; delivery is not completion
rex wait BLOCK --for 90s # exit status when the command ends
```
**`rex_skill`** returns the installed CLI guide verbatim. Read it once before the first `rex` call.
## What is disabled
`do` (Lua), `events`, `block stream`, `api call`, `attach`, every create, layout, rename, close, and kill command, and `-S` server routing. The bridge drives the local Rex server only. This keeps an agent from deleting sessions, rearranging layout, or reaching other hosts through the MCP surface.
## Wait semantics
Rex blocks are terminals, not agents. `wait TARGET` returns when the target's child process exits, with that exit status; `wait --for` bounds the wait and returns non-zero on timeout. There is no idle/working/done state to poll. To check on a running command, `capture` the block and read the screen; to wait for a command to finish, call `wait` again with the same target. On `wait_timeout`, capture first: the reply may already be on screen.
## Risks
The `rex` tool types into your real terminals and starts real processes with the permissions of the MCP client. A malicious prompt or document can steer an agent into using that access. Terminal output may contain passwords, keys, or private work, and a `capture` can surface them into agent context. The allowlist reduces accidents, not intent; treat a connected agent as someone typing at your keyboard.
In HTTP mode the token is shell-equivalent access to this machine, as the user running the bridge. Keep the endpoint tailnet-only unless a hosted client forces a funnel, keep the token out of chat and screenshots, and rotate it with `rex-mcp token` if it leaks, then update every client.
## Development
```sh
just test
just live
```
The tests cover argument splitting and allowlist rules, wait budgeting, exit-status mapping, the spawn runner, the token file semantics, a full MCP handshake over stdio, and a full bearer-authenticated HTTP session against a fake `rex` binary, so they run without a Rex server. `scripts/live.mjs` is the same handshake against your real Rex server; it creates two short-lived blocks and closes them at the end.
## Credits
rex-mcp is inspired by [herdr-mcp](https://github.com/IgorWarzocha/herdr-mcp) by Igor Warzocha, the MCP bridge for [Herdr](https://github.com/herdrdev/herdr). The tool design (a passthrough CLI tool with an allowlist plus a skill tool) and parts of the bridge and HTTP code are adapted from it, under the MIT licence. rex-mcp targets Rex instead of Herdr, runs as a local stdio server by default (HTTP with a bearer token for remote machines), and waits on terminal process exits instead of agent state.
## Licence
[MIT](LICENSE) © Edgardo Yoliani. Contains code adapted from [herdr-mcp](https://github.com/IgorWarzocha/herdr-mcp), MIT © Igor Warzocha; see the notice in [LICENSE](LICENSE).
TDQS
Scored across 2 tools
rex_skill only retrieves the installed Rex CLI guide, while rex executes scoped CLI commands against the local server; the documentation-vs-execution split is understandable. There is minor overlap because rex also exposes help subcommands and refers back to rex_skill, but the primary purposes are distinct.
Both tools share the rex namespace, but the set does not follow a predictable verb_noun pattern: rex is a bare command name and rex_skill is a noun-like guide tool. The names are readable but not self-explanatory without descriptions.
Two tools is thin for a CLI bridge, but one is an omnibus executor and the other provides necessary version-specific documentation. Each earns its place, though the surface is intentionally narrow rather than comfortably scoped.
The set covers discovery, inspection, capture, send/send-key, run, wait, and help, so core terminal interaction is available. However, lifecycle operations such as create, close, kill, rename, and layout are explicitly disabled, along with events/streaming, API calls, and server routing, leaving notable gaps.