Skip to main content
Glama
README.md
# herdmcp

*Rope in your herd.* An MCP server for [herdr](https://herdr.dev). It lets any MCP client (Claude Code, Claude.ai, Cursor, …) see and drive your herdr workspaces, panes and coding agents.

- **TypeScript + [Effect 4](https://effect.website)**. The herdr client is an Effect service. The tools are an `effect/ai` Toolkit, served by Effect's native `McpServer`.
- **[better-auth](https://better-auth.com)** acts as the OAuth 2.1 authorization server: dynamic client registration, PKCE, and JWT access tokens scoped to `/mcp`.
- **Cloudflare Tunnels** are built in. Use a free quick tunnel or a named tunnel. `cloudflared` is downloaded automatically if it isn't installed.
- **No Docker, nothing to install.** It's a single bundled file with no dependencies, run with `npx herdmcp` on Node 22.13 or newer.

It talks to herdr over herdr's own local socket API (newline-delimited JSON on `herdr.sock`), so it runs **on the machine where herdr runs**. To reach several machines, run one instance on each.

## Quick start

```bash
# Local client on the same machine: stdio, no auth
claude mcp add herdr -- npx -y herdmcp stdio

# Remote: create an account, then serve through a Cloudflare quick tunnel
npx herdmcp user add you@example.com        # prints a generated password
npx herdmcp serve --tunnel quick            # prints https://<random>.trycloudflare.com/mcp
claude mcp add --transport http herdr https://<random>.trycloudflare.com/mcp
```

For other clients, use the same stdio command (`npx -y herdmcp stdio`) or the remote URL. If you'll run it often, install it globally with `npm i -g herdmcp`.

When the client connects, it opens a browser window. Sign in, click **Allow**, and you're connected.

## Modes

| Command | What it does |
|---|---|
| `herdmcp stdio` | MCP over stdin/stdout. For local clients. It's unauthenticated because it's your own process. |
| `herdmcp serve` | Streamable HTTP at `/mcp` on `127.0.0.1:8787`, protected by OAuth. |
| `herdmcp serve --tunnel quick` | Same, published at a random `*.trycloudflare.com` URL. No Cloudflare account is needed. The URL changes on every restart. |
| `herdmcp serve --tunnel token --public-url https://herdr.example.com` | A named Cloudflare Tunnel (`CLOUDFLARE_TUNNEL_TOKEN`) with a stable hostname. Use this for always-on setups. |
| `herdmcp user add <email> [--password …]` | Creates a user, or resets their password. Public sign-up is disabled. |

### Named tunnel setup (stable URL)

1. In the Cloudflare dashboard, go to **Zero Trust → Networks → Tunnels → Create tunnel** (type *cloudflared*) and copy the token.
2. Add a **public hostname**, e.g. `herdr.example.com` → service `http://127.0.0.1:8787`.
3. Run:
   ```bash
   CLOUDFLARE_TUNNEL_TOKEN=… herdmcp serve --tunnel token --public-url https://herdr.example.com
   ```

Use one tunnel and hostname per machine, for example `herdr-laptop.example.com` and `herdr-vps.example.com`.

## Configuration

Every flag can also be set with an environment variable (see `.env.example`):

| Env | Default | |
|---|---|---|
| `HERDMCP_DATA_DIR` | `~/.herdmcp` | Holds `auth.db` (SQLite), `auth-secret`, and the `bin/cloudflared` binary |
| `HERDMCP_HOST` / `HERDMCP_PORT` | `127.0.0.1` / `8787` | Bind address |
| `HERDMCP_PUBLIC_URL` | tunnel URL, or `http://host:port` | The OAuth issuer and resource origin. It must match the URL clients use. |
| `HERDMCP_TUNNEL` | `none` | `none` \| `quick` \| `token` |
| `CLOUDFLARE_TUNNEL_TOKEN` | | Required for `--tunnel token` |
| `HERDMCP_AUTH_SECRET` | generated into `auth-secret` | better-auth signing secret |
| `HERDR_SOCKET_PATH` | `$XDG_CONFIG_HOME/herdr/herdr.sock` | herdr sets this inside its panes. Point it at another session's socket to target that session. |
| `HERDMCP_ALLOW_RAW` | unset | Set `1` to expose `herdr_call`, which can call any herdr API method |
| `CLOUDFLARED_BIN` | | Path to a specific `cloudflared` binary |

## Tools

| Read-only | Mutating |
|---|---|
| `herdr_status`, `herdr_snapshot` | `herdr_workspace_create`, `herdr_tab_create` |
| `herdr_workspace_list`, `herdr_tab_list` | `herdr_pane_split`, `herdr_pane_run`, `herdr_pane_send_text`, `herdr_pane_send_keys` |
| `herdr_pane_list`, `herdr_pane_get`, `herdr_pane_layout`, `herdr_pane_read` | `herdr_pane_wait_output`, `herdr_pane_rename`, `herdr_pane_close` *(destructive)* |
| `herdr_agent_list`, `herdr_agent_get`, `herdr_agent_read`, `herdr_agent_explain`, `herdr_agent_kinds` | `herdr_agent_start`, `herdr_agent_prompt`, `herdr_agent_wait`, `herdr_agent_send_keys`, `herdr_agent_rename` |
| | `herdr_notify`, `herdr_call` *(opt-in)* |

A typical delegation loop runs `herdr_pane_split`, then `herdr_agent_start`, then `herdr_agent_prompt` with `wait: true`, then `herdr_agent_read`.

## Running it permanently (no Docker)

Install it once with `npm i -g herdmcp`. On machines without Node, use `bun run build:binary` instead; it builds a self-contained ~85 MB executable that includes the runtime. Add `--target=bun-darwin-arm64` etc. to cross-compile.

**Linux (systemd user service)**, in `~/.config/systemd/user/herdmcp.service`:

```ini
[Unit]
Description=herdr MCP server
After=network-online.target

[Service]
ExecStart=/usr/bin/env herdmcp serve --tunnel token --public-url https://herdr.example.com
Environment=CLOUDFLARE_TUNNEL_TOKEN=...
Restart=on-failure

[Install]
WantedBy=default.target
```

Then run `systemctl --user enable --now herdmcp` (and `loginctl enable-linger $USER` so it keeps running after you log out).

**macOS:** use a `launchd` agent with the same command, or simply run `npx herdmcp serve …` in a herdr pane.

## Security notes

- Only accounts you create with `user add` can authorize clients. Each client also needs an explicit consent click.
- Access tokens are short-lived JWTs (1 h, refreshable). Their audience is `<public-url>/mcp`, and they're verified locally against better-auth's JWKS.
- Dynamic client registration is open, which MCP clients need. Registering a client grants nothing until a user signs in and consents.
- Anyone who can authorize can run arbitrary commands in your terminals. Treat the account like SSH access.
- The HTTP server binds to `127.0.0.1` by default. The tunnel is the only public path.

## Development

```bash
bun install
bun run dev              # watch mode (Bun runs the TypeScript directly)
bun run typecheck
bun run build            # → dist/herdmcp.js, the single Node bundle that gets published
npm publish              # runs typecheck + build first (prepack)
```

## Layout

```
src/
  cli.ts            effect/cli entrypoint: serve | stdio | user add
  herdr/Herdr.ts    Effect service for herdr's socket API
  mcp/tools.ts      Tool definitions + handlers (effect/ai Toolkit)
  mcp/server.ts     McpServer layers (stdio + Streamable HTTP)
  auth/auth.ts      better-auth: email/password, jwt, oauth-provider (SQLite via node:sqlite)
  auth/pages.ts     Login / consent / home pages
  http.ts           Node HTTP router: OAuth metadata, auth routes, bearer-checked /mcp
  tunnel.ts         Cloudflare quick/named tunnel lifecycle
```

## Releasing

Publishing happens from GitHub Actions (`.github/workflows/publish.yml`) whenever a GitHub release is published:

1. Bump the version in `package.json`, commit, and push.
2. Create a release: `gh release create v0.1.1 --generate-notes`.

The first time, add an npm **granular access token** with publish rights as the repo secret `NPM_TOKEN`. After the package exists, you can switch to [trusted publishing](https://docs.npmjs.com/trusted-publishers) (npmjs.com → herdmcp → Settings → Trusted publisher → GitHub Actions, workflow `publish.yml`). Then delete the secret.