Skip to main content
Glama
README.md
# 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

A4.4/5.0

Scored across 2 tools

Disambiguation4/5

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.

Naming Consistency3/5

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.

Tool Count3/5

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.

Completeness3/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues