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

[![npm](https://img.shields.io/npm/v/@stateforge-labs/herdr-mcp.svg)](https://www.npmjs.com/package/@stateforge-labs/herdr-mcp)
[![CI](https://github.com/stateforge-labs/herdr-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/stateforge-labs/herdr-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

MCP server for herdr: one read-only tool per herdr method, over streamable HTTP with a bearer token.

## Contents

- [What it does](#what-it-does)
- [Prerequisites](#prerequisites)
- [Install](#install)
- [Usage](#usage)
- [Configure a client](#configure-a-client)
- [Tools](#tools)
- [Configuration](#configuration)
- [Exit codes and failure modes](#exit-codes-and-failure-modes)
- [Security scope](#security-scope)
- [Run as a service](#run-as-a-service)
- [Use as a library](#use-as-a-library)
- [Support](#support)
- [License](#license)
- [Maintainers](#maintainers)
- [Contributing](#contributing)

## What it does

- Serves one MCP tool per read-only herdr structure method. The tool name is `herdr_` plus the method with dots turned into
  underscores (`pane.list` becomes `herdr_pane_list`). Every tool is annotated `readOnlyHint: true`, `destructiveHint: false` and `idempotentHint: true`.
- Terminal content, long-poll methods, event subscriptions, writes, control and admin methods are never exposed. The tool set is
  computed from the herdr SDK's method table ([ADR-0001](https://github.com/stateforge-labs/herdr-mcp/blob/main/docs/decisions/ADR-0001-read-only-tools-generated-from-the-sdk.md)).
- Streamable HTTP transport with MCP protocol versions 2025-11-25, 2025-06-18 and 2025-03-26. Tools only: no resources, prompts
  or sampling.
- Every request must carry `Authorization: Bearer <token>`. The server listens on loopback (`127.0.0.1`) by default.

## Prerequisites

- **Bun 1.4.4 or later.** The server uses Bun's HTTP server, so it does not run on Node. Check with `bun --version`.
- **A running herdr server** with its unix socket. The socket is resolved in this order: `HERDR_MCP_SOCKET`, then the SDK's own
  resolution (`HERDR_SOCKET`, then `~/.config/herdr/herdr.sock`). The SDK targets herdr 0.9.5 (socket protocol 22).

The server starts without a herdr socket. Tool calls then return an error result until herdr is reachable (see
[Exit codes and failure modes](#exit-codes-and-failure-modes)).

## Install

```sh
bun add -g @stateforge-labs/herdr-mcp
```

The package provides the `herdr-mcp` command. Bun runs its TypeScript source directly, so there is no build step. Development
checks are described in [CONTRIBUTING.md](https://github.com/stateforge-labs/herdr-mcp/blob/main/CONTRIBUTING.md).

## Usage

Start the server:

```sh
herdr-mcp
```

It prints the listen address and nothing else:

```text
herdr-mcp: listening on http://127.0.0.1:7423
```

On the first start the bearer token is created in the token file (see [Configuration](#configuration)). The token is never
printed. Read it with:

```sh
cat ~/.local/state/herdr-mcp/token
```

A minimal call: initialize a session, then call a tool. The session ID comes back in the `Mcp-Session-Id` response header.

```sh
TOKEN="$(cat ~/.local/state/herdr-mcp/token)"
H=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
   -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: 2025-11-25")

SID=$(curl -s -D - -o /dev/null http://127.0.0.1:7423/mcp "${H[@]}" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"example","version":"0.0.0"}}}' \
  | tr -d '\r' | awk -F': ' 'tolower($1)=="mcp-session-id"{print $2}')

curl -s http://127.0.0.1:7423/mcp "${H[@]}" -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"herdr_workspace_list","arguments":{}}}'
```

Sample output (abridged; the workspace names and IDs depend on your herdr):

```json
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"type\":\"workspace_list\",\"workspaces\":[{\"workspace_id\":\"w1\",\"number\":1,\"label\":\"example\",\"focused\":true,\"pane_count\":1,\"tab_count\":1,\"active_tab_id\":\"w1:t1\",\"agent_status\":\"unknown\"}]}"}]}}
```

Tool arguments are checked against each tool's input schema before herdr is called. Unknown keys and missing required keys are
rejected with `isError: true` and a text that starts with `invalid params:`.

## Configure a client

The server speaks streamable HTTP only. Clients that support only stdio servers cannot connect to it directly. Keep the token in
your environment, not in a file that is committed.

### Claude Code

Put this in the project's `.mcp.json`:

```json
{
  "mcpServers": {
    "herdr": {
      "type": "http",
      "url": "http://127.0.0.1:7423/mcp",
      "headers": { "Authorization": "Bearer ${HERDR_MCP_TOKEN}" }
    }
  }
}
```

Export the token in the shell that starts the client:

```sh
export HERDR_MCP_TOKEN="$(cat ~/.local/state/herdr-mcp/token)"
```

### VS Code

Put this in `.vscode/mcp.json`, with `HERDR_MCP_TOKEN` exported in the environment VS Code starts from:

```json
{
  "servers": {
    "herdr": {
      "type": "http",
      "url": "http://127.0.0.1:7423/mcp",
      "headers": { "Authorization": "Bearer ${env:HERDR_MCP_TOKEN}" }
    }
  }
}
```

Change the port in `url` if `HERDR_MCP_PORT` is set.

## Tools

All tools take a JSON object and reject keys that are not listed (`additionalProperties: false`). `string or null` parameters
accept `null` to mean "not given". Every tool has `readOnlyHint: true`, `destructiveHint: false` and `idempotentHint: true`. On success a tool returns herdr's result as
`structuredContent` and as JSON text in `content`; a non-object result is wrapped as `{ "result": ... }`. On failure it returns
`isError: true` with a text.

| Tool | herdr method | Inputs |
| --- | --- | --- |
| `herdr_ping` | `ping` | none |
| `herdr_server_agent_manifests` | `server.agent_manifests` | none |
| `herdr_session_snapshot` | `session.snapshot` | none |
| `herdr_workspace_list` | `workspace.list` | none |
| `herdr_workspace_get` | `workspace.get` | `workspace_id` (required): string |
| `herdr_worktree_list` | `worktree.list` | `cwd`: string or null<br>`trust_repository`: boolean<br>`workspace_id`: string or null |
| `herdr_tab_list` | `tab.list` | `workspace_id`: string or null |
| `herdr_tab_get` | `tab.get` | `tab_id` (required): string |
| `herdr_agent_list` | `agent.list` | none |
| `herdr_agent_get` | `agent.get` | `target` (required): string |
| `herdr_agent_explain` | `agent.explain` | `target` (required): string |
| `herdr_pane_layout` | `pane.layout` | `pane_id`: string or null |
| `herdr_pane_process_info` | `pane.process_info` | `pane_id`: string or null |
| `herdr_layout_export` | `layout.export` | `pane_id`: string or null<br>`tab_id`: string or null |
| `herdr_pane_neighbor` | `pane.neighbor` | `direction` (required): string, one of `left`, `right`, `up`, `down`<br>`pane_id`: string or null |
| `herdr_pane_edges` | `pane.edges` | `pane_id`: string or null |
| `herdr_pane_list` | `pane.list` | `workspace_id`: string or null |
| `herdr_pane_current` | `pane.current` | `caller_pane_id`: string or null |
| `herdr_pane_get` | `pane.get` | `pane_id` (required): string |
| `herdr_pane_link_resolve` | `pane.link.resolve` | `col` (required): integer<br>`viewport_row` (required): integer<br>`pane_id` (required): string<br>`content_revision`: integer or null<br>`offset_from_bottom`: integer or null |
| `herdr_integration_list` | `integration.list` | none |
| `herdr_plugin_list` | `plugin.list` | `plugin_id`: string or null |
| `herdr_plugin_action_list` | `plugin.action.list` | `plugin_id`: string or null |
| `herdr_plugin_log_list` | `plugin.log.list` | `limit`: integer or null<br>`plugin_id`: string or null |

The table is generated from the method table of `@stateforge-labs/herdr-sdk`. A method is listed when its class is a structure read
(R-meta), it is not a long poll, and it is not `events.subscribe`.

## Configuration

The program reads only environment variables. A bad value stops the program with a message that names the setting, never its
value.

| Variable | Default | Meaning |
| --- | --- | --- |
| `HERDR_MCP_HOST` | `127.0.0.1` | Interface to listen on. Keep it on loopback unless something else guards the port. |
| `HERDR_MCP_PORT` | `7423` | TCP port, 1 to 65535. |
| `HERDR_MCP_TOKEN_FILE` | `$XDG_STATE_HOME/herdr-mcp/token`, else `$HOME/.local/state/herdr-mcp/token` | The bearer token file. Created with mode `0600` in a `0700` directory when it is missing, empty or malformed. |
| `HERDR_MCP_SOCKET` | unset | The herdr socket path. When unset, the SDK uses `HERDR_SOCKET`, then `~/.config/herdr/herdr.sock`. |

A malformed token file is replaced at startup, so every client must read the token again after a restart.

## Exit codes and failure modes

The program exits with status `1` on every startup failure. No other exit status is defined.

| Situation | What you see | Exit |
| --- | --- | --- |
| `HERDR_MCP_PORT` is not an integer from 1 to 65535 | `HERDR_MCP_PORT must be an integer from 1 to 65535` | 1 |
| No token path can be derived (`HERDR_MCP_TOKEN_FILE`, `XDG_STATE_HOME` and `HOME` all unset) | `set HERDR_MCP_TOKEN_FILE, XDG_STATE_HOME or HOME: there is nowhere to keep the token` | 1 |
| The token file cannot be read or written | `herdr-mcp: the token file could not be read or written` | 1 |
| The port is already in use | `Failed to start server. Is port 7423 in use?` | 1 |
| The herdr socket is missing or unreachable | The server still starts. A tool call returns `isError: true` with `herdr unreachable: the call got no usable answer` or `herdr call failed`. The next call tries to connect again. | server keeps running |
| A tool call has invalid arguments | `isError: true`, text starts with `invalid params:` | server keeps running |
| The bearer token is missing or wrong | HTTP `401` with an empty body | server keeps running |

## Security scope

- **What an agent that holds the token can read:** the structure reads in the [Tools](#tools) table. These cover workspaces,
  tabs, panes, agents, layouts, integrations and plugins, plus process information for panes (`pane.process_info`) and plugin
  logs (`plugin.log.list`). Those last two can contain data from your session, so check them before you expose the port.
- **What it cannot do:** write, send input, run actions, read terminal content, open event subscriptions, or call admin methods.
  None of these methods is in the tool set.
- **Network:** the server listens on `127.0.0.1` by default. Setting `HERDR_MCP_HOST` to another interface exposes it to that
  network. The server speaks plain HTTP with no TLS, so the bearer token is the only guard, and it crosses the network unencrypted.
- **Token:** kept in a `0600` file inside a `0700` directory. Compared in constant time. Never logged or printed. The listen line
  and the request log are disabled. To rotate it, stop the server, delete the token file and start the server again.

To report a vulnerability, see [SECURITY.md](https://github.com/stateforge-labs/herdr-mcp/blob/main/SECURITY.md).

## Run as a service

`systemd/herdr-mcp.service` is a user unit with the sandboxing and loopback defaults described above. It starts `~/.bun/bin/herdr-mcp`,
the command that `bun add -g` installs by default. Install the package, copy the unit from the installed package to
`~/.config/systemd/user/`, then run:

```sh
bun add -g @stateforge-labs/herdr-mcp
mkdir -p ~/.config/systemd/user
cp ~/.bun/install/global/node_modules/@stateforge-labs/herdr-mcp/systemd/herdr-mcp.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now herdr-mcp
```

If Bun is installed somewhere else, or `BUN_INSTALL` is set, change the `ExecStart` path in the copied unit to the `herdr-mcp` command
that `bun add -g` installed.

## Use as a library

The package exports the server, its HTTP layer and the auth helpers. Import them from `@stateforge-labs/herdr-mcp`:

| Export | What it is |
| --- | --- |
| `HerdrMcpLive(options)` | A layer that serves the read-only herdr tools over authenticated MCP streamable HTTP. |
| `READ_ONLY_METHOD_NAMES` | The herdr method names exposed as tools. |
| `McpHttpLive(options, registrations, handlers)` | The generic MCP HTTP server with the bearer check. |
| `tokenFile({ path })` | Read-or-create the bearer token file. `readOrCreate()` returns the token. |
| `bearerGuard(token)` | A function that checks an `Authorization` header in constant time. |
| `ensureOwnerDir(path)` | Creates a directory with mode `0700`. |
| `toolNameOf(method)` | The MCP tool name for a herdr method. |
| `MCP_PATH`, `MCP_SERVER_NAME`, `MCP_SERVER_VERSION` | The endpoint path (`/mcp`) and the server identity. |

A runnable host program, the same as `src/main.ts` without the environment reading:

```ts
import { Effect, Layer } from "effect"
import { HerdrMcpLive, tokenFile } from "@stateforge-labs/herdr-mcp"

const token = await tokenFile({ path: "/absolute/path/to/token" }).readOrCreate()
const server = HerdrMcpLive({ host: "127.0.0.1", port: 7423, token })

await Effect.runPromise(Layer.launch(server) as Effect.Effect<never, unknown, never>)
```

Run it with `bun run host.ts` in a project that depends on the package, where `host.ts` is the file above. Pass `socketPath` to
`HerdrMcpLive` to use a socket other than the default.

## Support

Ask questions and report bugs in [GitHub issues](https://github.com/stateforge-labs/herdr-mcp/issues). For security problems, use
[SECURITY.md](https://github.com/stateforge-labs/herdr-mcp/blob/main/SECURITY.md) instead of a public issue.

## License

MIT. See [LICENSE](LICENSE).

## Maintainers

Maintained by stateforge-labs.

## Contributing

See [CONTRIBUTING.md](https://github.com/stateforge-labs/herdr-mcp/blob/main/CONTRIBUTING.md) for how to report bugs, set up
the project, and propose changes.