herdmcp
by joelbqz
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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues