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

An MCP server that exposes a Clermes worker fleet as typed tools over HTTP and stdio.

Clermes is a file-based coordination harness for a fleet of long-lived coding-agent workers. The bash dispatcher at [`canopy-workshop/clermes`](https://github.com/canopy-workshop/clermes) is the source of truth for tmux and session state. `clermes-mcp` is a thin Python shell over that dispatcher, so a coordinator that speaks MCP (Hermes, Claude, an editor MCP client, anything) can drive the fleet with structured tool calls instead of scraping panes and writing poll loops.

`clermes-mcp` is an enhancement, not a requirement. The `clermes` CLI is fully usable on its own. This server is the integration layer that makes a Clermes fleet a first-class citizen in multiagent workflows.

The reference worker is Claude Code. The reference coordinator is Hermes. Any other coding agent (Codex, Aider, antigravity, opencode) and any other MCP-speaking coordinator fit the same harness. Nothing about the protocol or this server is agent-specific.

## What you get

- **Typed read tools** for fleet inventory, pane capture, lock and dispute inspection.
- **Typed action tools** (operator role only) for dispatch, respawn, reply, halt, spawn, kill, fleet-wide heal.
- **Blocking or async per call.** The client picks. Blocking returns the done file inline. Async returns a job id and the caller polls.
- **Auto-heal before send.** If a worker pane is in bash, dead, or some other state, dispatch kills and respawns it before sending.
- **Anti-compact resend.** After dispatch, the server watches for the conversation-compacted-then-idle pane state and re-sends the brief once if it sees the swallow signature.
- **Role gating.** `operator` exposes the full surface. `worker` exposes read tools only. A worker-role MCP client cannot see dispatch or kill at all. The gate is structural: the action tools are never registered in that role.
- **No outbound to humans.** The server never exposes a tool that contacts humans. I treat that as a fixed invariant of the project. Don't fork it back in.

## Install

```
pip install clermes-mcp
```

You also need:

- The `clermes` bash dispatcher on your PATH (or at `$CLERMES_HOME/clermes`).
- `tmux` and `jq` on the host running the worker fleet.
- A worker fleet (Claude Code, or any equivalent CLI agent with an interactive TUI).

## Configure

Everything resolves from environment variables. Defaults work for a typical install at `~/.clermes`.

| Setting          | Env var            | Default                                                        |
| ---------------- | ------------------ | -------------------------------------------------------------- |
| Clermes home     | `CLERMES_HOME`     | `~/.clermes`                                                   |
| Dispatcher bin   | `CLERMES_BIN`      | `$CLERMES_HOME/clermes`, then `$(which clermes)`               |
| Bind host        | `CLERMES_MCP_HOST` | `127.0.0.1`                                                    |
| Bind port        | `CLERMES_MCP_PORT` | `9675`                                                         |
| Bearer token     | `CLERMES_MCP_TOKEN`| unset (server warns on HTTP transport)                         |
| Role             | `CLERMES_MCP_ROLE` | `operator`                                                     |
| Default mode     | `CLERMES_MCP_MODE` | `blocking`                                                     |

If `$CLERMES_HOME/voice-preamble.txt` exists, its contents prepend to dispatches with `public_artifact=true`. Otherwise a neutral default applies. The preamble is the right place to put your repo's voice rules. Don't bake them into the server.

`config.yaml` under `$CLERMES_HOME` is read if present, for the `paths.home` override only. Every other field is informational.

Run the diagnostic check:

```
clermes-mcp doctor
```

It verifies paths, the dispatcher, the registry, and protocol directories.

## Run

HTTP (recommended for multiagent integration):

```
clermes-mcp serve --transport http --host 127.0.0.1 --port 9675
```

stdio (for local subprocess clients):

```
clermes-mcp serve --transport stdio
```

To inspect the schema for every tool:

```
clermes-mcp tools
```

## Run as a service

Drop the unit file at `examples/clermes-mcp.service` into `~/.config/systemd/user/` and enable it:

```
systemctl --user daemon-reload
systemctl --user enable --now clermes-mcp
journalctl --user -u clermes-mcp -f
```

On systems where user-systemd lingering is unreliable (WSL across `wsl --shutdown`, some headless setups), use whatever your platform's session-supervisor of choice is. A simple cron `@reboot` line works too. tmux is a last resort because the MCP server has no TTY needs.

## Wire into a coordinator

The reference is Hermes:

```yaml
mcp_servers:
  clermes:
    url: "http://127.0.0.1:9675/mcp"
    headers:
      Authorization: "Bearer ${CLERMES_MCP_TOKEN}"
    timeout: 1800
    connect_timeout: 30
```

A 30-minute timeout lines up with the blocking ceiling on long dispatches. For async use, the request returns immediately with a job id and the timeout can drop to anything.

Other MCP-speaking coordinators wire in the same way.

## Tool surface

**Read tools (every role):**

- `list_workers()`: flat list of `{name, state, alive, locked, task_id?, workdir}`.
- `fleet_snapshot()`: full per-worker pane text and classification.
- `capture(worker, lines=80)`: pane tail with classification.
- `worker_health(worker)`: `claude | bash | dead | other:<proc>`.
- `list_open_tasks()`: lock files joined with the assigned-task map.
- `read_dispute(task_id)`: raw markdown of a dispute file, or null.
- `read_result(task_id)`: parsed JSON of a done file, or null.
- `list_done(limit=50)`: recent completed task ids.
- `list_jobs() / job_status(job_id) / job_result(job_id)`: async job plumbing.

**Action tools (operator role only):**

- `dispatch(worker, brief, slug?, wait=true, mode=blocking, timeout=1800, public_artifact=false, auto_heal=true, anti_compact_resend=true)`: the headline tool.
- `respawn(worker)`: kill then spawn (the dispatcher's `heal` is fleet-wide; this is the single-worker equivalent).
- `spawn(worker) / kill(worker) / heal_fleet()`: direct dispatcher wrappers.
- `reply(worker, message, wait_for_task_id?, mode=blocking, timeout=1800)`: send without a new `TASK_ID` footer. Use to answer a dispute or select an injection-dialog option.
- `halt(task_id)`: touch the halt file so an announced action does not proceed past its grace window.

## Security model

- **Bind localhost by default.** `127.0.0.1:9675`. Don't bind `0.0.0.0` without a reverse proxy and a token in front of it.
- **Bearer token auth on HTTP.** Set `CLERMES_MCP_TOKEN` and require it on every HTTP request. Without it the server logs a warning at startup and accepts anything.
- **Role gating.** `CLERMES_MCP_ROLE=worker` strips every action tool from the registered surface. A worker-role MCP client cannot see, let alone call, dispatch or kill.
- **No outbound to humans.** Nothing in the tool surface contacts email, SMS, agencies, webhooks, or chat. Adding one breaks the threat model of this project; don't do it.
- **No `shell=True` anywhere.** All subprocess calls pass argv as a list. Briefs are arbitrary text and cannot escape the process boundary.
- **Token redaction** in error strings surfaced to the client.

## Layout

```
clermes-mcp/
├── README.md
├── LICENSE
├── pyproject.toml
├── .env.example
├── src/clermes_mcp/
│   ├── __init__.py
│   ├── __main__.py        CLI entry point
│   ├── server.py          FastMCP app, tool registration, transport wiring
│   ├── dispatcher.py      async subprocess wrapper around the bash CLI
│   ├── protocol.py        file I/O for done/dispute/announce/halt/locks/assigned
│   ├── pane.py            pane-text classification heuristics
│   ├── jobs.py            in-process async job registry
│   ├── config.py          env + optional config.yaml loader
│   ├── doctor.py          diagnose paths, dispatcher, deps
│   └── models.py          pydantic schemas
├── tests/                 pytest suite, all hermetic against the stub
├── examples/
│   ├── stub-clermes       fake dispatcher for CI and demo
│   ├── clermes-mcp.service systemd --user unit
│   └── hermes-config.snippet.yaml
└── docs/PROTOCOL.md       generic coordination contract
```

## Testing

```
pip install -e ".[dev]"
pytest
ruff check src tests
```

Every test runs against `examples/stub-clermes`. No live fleet, no tmux. CI on its own can validate the full server surface.

## Status

Pre-1.0. The tool surface is stable enough to ship, but expect minor breakage in tool names and arg shapes until the integration with at least two coordinators settles.

## License

MIT.