Skip to main content
Glama
README.md
# t3-mcp

**Let any agent spawn and manage [T3 Code](https://github.com/pingdotgg/t3code) agents over MCP.**

t3-mcp is a small HTTP [Model Context Protocol](https://modelcontextprotocol.io) server that
drives a T3 Code instance on behalf of another agent. Your orchestrator, whether that's Claude
Code, a custom agent or a cron job, can then:
- start research or coding agents as T3 threads;
- send them follow-ups;
- wait for their results;
- answer their approval prompts.

All of this goes through 15 MCP tools. Every thread also shows up in the T3 app, where you
can watch it or take over.

```
your agent ──HTTP MCP──▶ t3-mcp ──WebSocket RPC──▶ T3 Code ──▶ Claude / Codex / … agents
            Bearer token          paired session
```

- **Pair once, like any other T3 device.** Paste a pairing link from T3 → Settings →
  Connections. No patches to T3 and no access to its database.
- **Research agents in one call.** `create_thread` with just a prompt starts an agent in a fresh
  Scratch folder. `create_threads` starts up to 20 at once.
- **Waiting and approvals built in.** `wait_for_thread` returns when the agent finishes or needs
  input, and `respond_to_request` answers approvals and questions.
- **Safe to retry.** A `clientRequestId` maps to deterministic T3 ids, so a lost response never
  creates duplicate agents.
- **Small and boring.** Node, three runtime dependencies (`@modelcontextprotocol/sdk`, `ws`,
  `zod`) and a hand-written client for T3's WebSocket protocol.

> [!IMPORTANT]
> t3-mcp targets **T3 Code Nightly** (orchestration protocol 2). It uses the same internal
> protocol as T3's own apps, which can change between Nightly builds. The bridge refuses to
> connect to a server that reports a different protocol version. Run `npm run smoke` after T3
> updates.

## Quick start

You need Node ≥ 22.18 and a T3 Code (Nightly) instance the bridge can reach.

**1. Make T3 reachable.** For a bridge on another machine:
- Option A: in T3 open **Settings → Connections** and enable **Network access**.
- Option B: enable **Tailscale HTTPS**, which is recommended because it gives you TLS.

A bridge on the same machine can use `127.0.0.1`.

**2. Serve.**

```sh
npx t3-mcp serve
```

On first start, t3-mcp generates the bearer token MCP clients must send and saves it next to
its other state. It then prints the endpoint and a command to connect Claude Code:

```
t3-mcp 0.1.2 is serving MCP at http://127.0.0.1:8787/mcp
  Bearer token: generated and saved to ~/.local/state/t3-mcp/mcp-bearer-token (print it with `npx t3-mcp token`)

  Add it to Claude Code (run this from the same folder):
    claude mcp add --transport http t3 http://127.0.0.1:8787/mcp --header "Authorization: Bearer $(npx t3-mcp token)"

  Not paired with T3 Code yet.
  Create a pairing link in T3 → Settings → Connections, then run:
    npx t3-mcp pair '<link>'
  in another terminal. This server picks it up without a restart.
```

**3. Pair.** In T3, create a pairing link under **Settings → Connections**. It needs the
`orchestration:read` and `orchestration:operate` scopes. Within 5 minutes, in another terminal
in the same folder, run:

```sh
npx t3-mcp pair 'http://192.168.1.10:3773/pair#token=XXXXXXXXXXXX'
```

The running server switches to the new pairing within a few seconds. Use the same command
whenever you need a new pairing, for example when the 30-day pairing expires: no restart
needed. Quote the link, because it contains `#` and sometimes `?`.

You can also pair at startup with `npx t3-mcp serve '<link>'` or `T3_PAIRING_URL`, or let
your agent call the `t3_pair` tool.

**4. Connect your agent.** Run the `claude mcp add` command that `serve` printed. If the
server is behind a TLS proxy, use its public URL instead:

```sh
claude mcp add --transport http t3 https://t3-mcp.example.com/mcp \
  --header "Authorization: Bearer $(npx t3-mcp token)"
```

Then ask it something like: *"Spin up three research agents in T3 — one each on X, Y and Z —
wait for them, and summarize what they found."*

Settings come from environment variables or a `.env` file in the folder you run t3-mcp from;
see [Configuration](#configuration). Running `pair`, `token` and `serve` from the same folder
makes sure they share the same state. To run from source instead of npm, clone the repository,
run `npm ci && npm run build`, and use `node dist/cli.js` wherever these steps say
`npx t3-mcp`.

## Tools

| Tool | What it does |
|---|---|
| `t3_status` | Pairing/connection state, environment, token expiry (`needsPairing`, `daysLeft`) |
| `t3_pair` | Pair with a pairing URL (or a bare code plus `baseUrl`) |
| `list_providers` | Provider instances (Claude, Codex, …), availability, model slugs, default model |
| `list_projects` | Projects with thread counts |
| `add_project` | Register a folder on the T3 host as a project |
| `create_thread` | Start an agent with an optional first prompt. Defaults to a fresh Scratch folder; choose `project`, `provider`, `model`, `runtimeMode`, `interactionMode` (`plan`), or a `worktree`; optional `wait` |
| `create_threads` | Up to 20 threads in one call; errors reported per entry |
| `list_threads` | Filter by project, status (`active` / `idle` / `needs_input` / `failed`), title |
| `read_thread` | Timeline as `messages` or full `activity` (tools, commands, file changes); incremental with `afterPosition` |
| `send_message` | Follow-up with `auto`, `queue`, `steer` or `restart` delivery |
| `wait_for_thread` | Block until the run finishes or needs input (≤ 10 min; never cancels work) |
| `interrupt_thread` | Stop the active run |
| `list_pending_requests` | Approvals and questions agents are waiting on |
| `respond_to_request` | Answer an approval or question, or dismiss a question |
| `archive_thread` | Archive or unarchive |

Failed tool calls return JSON with an `error` code, for example `needs_pairing`,
`thread_not_found`, `model_unavailable` or `runtime_mode_escalation_denied`, plus a message
written for the calling agent.

## Configuration

Every setting can be given as a command-line flag, an environment variable, or a line in
`./.env` in the working directory. Flags win over environment variables, which win over
`./.env`. [`.env.example`](./.env.example) lists all the variables.

| Flag | Variable | Default | Description |
|---|---|---|---|
| `--host` | `MCP_HOST` | `127.0.0.1` | Address to listen on; `0.0.0.0` for all interfaces |
| `-p`, `--port` | `MCP_PORT` | `8787` | Port to listen on |
| `--path` | `MCP_PATH` | `/mcp` | URL path of the MCP endpoint |
| `--pair`, or a link argument | `T3_PAIRING_URL` | — | Pair at startup. The flag always pairs; the variable is used only when no valid pairing is stored |
| `--state-dir` | `T3_STATE_DIR` | `~/.local/state/t3-mcp` | Where the T3 pairing and the generated bearer token are stored (mode 0600). `pair`, `status`, `token` and `unpair` must use the same one as `serve` |
| `--max-mode` | `T3_MAX_RUNTIME_MODE` | `full-access` | Highest permission level threads may be created with |
| `--default-mode` | `T3_DEFAULT_RUNTIME_MODE` | same as the ceiling | Permission level threads start with |
| `--model` | `T3_DEFAULT_MODEL` | T3's default | `<providerInstanceId>:<model>`, e.g. `claudeAgent:claude-opus-5-5` |
| `--label` | `T3_CLIENT_LABEL` | `t3-mcp (<hostname>)` | Name shown for this client in T3 → Connections |
| `--log-level` | `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error`. Logs go to stderr: readable lines in a terminal, JSON lines otherwise (e.g. under systemd) |
| — | `MCP_BEARER_TOKEN` | generated | Token MCP clients must send; ≥ 32 characters. When unset, `serve` generates one, stores it in the state dir and `t3-mcp token` prints it. There is no flag, because command lines are visible to other users and end up in shell history |

Runtime modes, from narrowest to broadest: `approval-required` < `auto-accept-edits` < `auto` <
`full-access`. If `--max-mode` is below a default set in the environment or `.env`, the default
is lowered to match, with a warning.

### CLI

```
t3-mcp [serve] [<link>] [options]   start the MCP server (the default command)
t3-mcp pair <link>                  pair, or replace the pairing; a running serve switches over within seconds
t3-mcp status [--json]              show the pairing and test the connection
t3-mcp token                        print the bearer token MCP clients must send
t3-mcp unpair                       forget the pairing; a running serve disconnects
```

`t3-mcp --help` lists every option. Some examples:

```sh
npx t3-mcp --port 9000                            # another port
npx t3-mcp --host 0.0.0.0                         # reachable from other machines (put TLS in front)
npx t3-mcp --max-mode approval-required           # agents stop at every approval
npx t3-mcp --model claudeAgent:claude-opus-5-5    # default model for new threads
npx t3-mcp --state-dir ~/t3-work -p 8788          # a second bridge, e.g. for another T3 instance
npx t3-mcp pair '<link>' --state-dir ~/t3-work    # ...and pairing it
```

`pair` also accepts a bare pairing code followed by the T3 server URL:
`t3-mcp pair ABCD2345EFGH http://192.168.1.10:3773`.

## Deployment

- **TLS.** t3-mcp speaks plain HTTP. Put a TLS proxy in front, such as Caddy, nginx, a
  Cloudflare Tunnel or `tailscale serve`.
- **Unauthenticated route.** `/healthz` is the only route that skips the bearer check.
- **Long waits.** `wait_for_thread` can hold a request open for up to 10 minutes. It sends MCP
  progress notifications about every 30 seconds if the client asked for them. If your proxy
  closes idle requests sooner, use smaller `timeoutMs` values and call again.
- **systemd.** `deploy/t3-mcp.service` is a hardened unit.

### Pairing lifetime

- **T3 issues a 30-day bearer token at pairing and has no refresh.**
  - `t3_status` reports `daysLeft` and sets `expiringSoon` in the last 3 days.
  - After expiry or revocation, tools return `needs_pairing` until someone provides a fresh link.
- **To renew**, create a new link and run `t3-mcp pair '<link>'` while the server keeps
  running. The previous session stays listed in T3 → Connections until it expires; you can
  remove it there.
- **To revoke the bridge**, remove its session in T3 under **Settings → Connections**.

## Security

**A bearer token for this server lets someone run agents on your T3 host.** With the default
`full-access` ceiling, that includes running arbitrary commands as the user T3 runs as. Treat
the bearer token like an SSH key, whether it is `MCP_BEARER_TOKEN` or the generated
`$T3_STATE_DIR/mcp-bearer-token`.

- **Lower the ceiling if you can.** When you don't need full access, pass a narrower
  `--max-mode` (or set `T3_MAX_RUNTIME_MODE`). With `approval-required`, agents stop at every
  approval, which the orchestrator (or you, in T3) answers.
- **Pair over HTTPS where possible**, using Tailscale HTTPS or another TLS route. Over a plain
  HTTP LAN route, the pairing code and session token cross the network unencrypted.
- **To rotate a generated bearer token**, delete `$T3_STATE_DIR/mcp-bearer-token` and restart
  `serve`. Then update your MCP clients.
- **The T3 credential is stored on the bridge host** in `$T3_STATE_DIR/t3-credential.json`
  with mode 0600. The bridge only requests the `orchestration:read` and `orchestration:operate`
  scopes, with no terminal or access-management rights.
- **Paths in tool arguments** (`add_project`, `existing_worktree`) refer to the T3 host's
  filesystem.

## Development

```sh
npm ci
npm test            # unit tests + end-to-end tests against an in-process fake T3 server
npm run typecheck
npm run dev         # serve from source
npm run smoke -- [--pair '<url>'] [--provider claudeAgent --model claude-haiku-4-5] [--keep]
```

`npm run smoke` runs against a real T3 instance. It creates a Scratch thread, waits for it,
sends a follow-up and then archives the thread.

The repository ships a [`t3.json`](./t3.json), so working on t3-mcp *in* T3 Code is set up for
you:
- new threads start in their own git worktree;
- `scripts/setup-worktree.ts` installs dependencies and links your main checkout's `.env` before
  the agent starts;
- Test, Typecheck, Build, Serve and Smoke actions appear in the scripts menu.

### Releasing

There are no manual releases. Every merge to `main` is published to npm under the `latest`
tag by [`.github/workflows/release.yml`](./.github/workflows/release.yml), after the
typecheck and tests pass.

- **Versions** take `major.minor` from `package.json`; the patch number is the commit count on
  `main` (for example `0.1.42`). To start a new line, change `package.json` to `0.2.0` or
  `1.0.0` in a PR.
- **Never backwards.** A run whose version is not newer than npm's current `latest` (for
  example a re-run of an old run) publishes nothing.
- **Pull requests** run the same pipeline with `npm publish --dry-run`, so packaging problems
  show up before merging.
- **No npm token** is stored anywhere: the workflow uses npm trusted publishing, and each
  version carries a provenance attestation linking it to the commit it was built from. Only the
  publish job can obtain publish credentials, and it runs no project or dependency code.

[`docs/architecture.md`](./docs/architecture.md) covers how pairing, the WebSocket RPC protocol
and the tools map onto T3's internals.

## License

[MIT](./LICENSE). t3-mcp is an independent project and is not affiliated with T3 Code or Ping
Labs.

Maintenance

ActivityMaintained
ResponsivenessNo issues