Skip to main content
Glama
MobileAffairsLTD

hermes-fleet-mcp

README.md
# hermes-fleet-mcp

An MCP server that exposes a [Hermes](https://hermes-agent.nousresearch.com/docs) deployment's
state and a chat surface, so a control plane (or any MCP client) can observe and drive a remote
Hermes box. This is the **Node Bridge** — one install per box, one bearer token per box.

Read-only observability (agents, sessions, crons, skills, tools) + a `chat` tool that runs a
remote agent and returns a session handle to poll. No admin mutations.

## License & Disclaimer

**MIT licensed** — free to use, modify, and redistribute, including for commercial use.
See [LICENSE](LICENSE).

Developed by **Dynamics Mobile OOD** ([dynamicsmobile.com](https://dynamicsmobile.com)).
The software is provided **"AS IS"** for **use at your own risk**: Dynamics Mobile OOD
makes **no warranties** and accepts **no responsibility or liability** for any consequences,
damage, or loss arising from its installation or use — including anything an agent does
while driven through this bridge.

## Install (on the box)

Requires Python 3.10+ and a working `hermes` CLI on `PATH`.

```bash
pip install .            # or: uv tool install .
```

## Run

```bash
# 1. generate a per-box token
hermes-fleet-mcp gen-key

# 2. serve the MCP endpoint (streamable HTTP) — bind only where you trust it
hermes-fleet-mcp serve --host 0.0.0.0 --port 8000 --token '<the key>'
#    (or --token-file key.txt, or env HERMES_FLEET_MCP_TOKEN)
```

The MCP endpoint is `http://<host>:<port>/mcp`. Every request must carry
`Authorization: Bearer <token>`.

**Serve over TLS in production** — put it behind a reverse proxy (Caddy/nginx) or use an
MCP-over-TLS transport. The token is a shared secret; plain HTTP leaks it.

By default the bridge accepts any `Host` header (it's a server-to-server endpoint, so the
bearer token is the real gate). To keep DNS-rebinding protection on and whitelist your
public domain instead, add `--allowed-host yourdomain.com` (repeatable; localhost is
always allowed).

`HERMES_HOME` is honoured (default `~/.hermes`); override with `--hermes-home`.

## Docker

Run as a container — read-only observability works out of the box with `HERMES_HOME` mounted:

```bash
export HERMES_HOME=/home/hermes/.hermes
export HERMES_FLEET_MCP_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
docker compose up -d --build
```

The `chat` tool and the version field need the `hermes` CLI — see `docker-compose.yml`
for how to make it reachable (build FROM a Hermes image, or mount the host install).

## Tool surface

| Tool | Description |
|---|---|
| `node_status` | version, home, counts, configured toolsets, bridge uptime |
| `list_agents` | the node's agents (Hermes profiles) with model/provider/toolsets |
| `get_agent(name)` | one agent's detail + recent sessions |
| `list_sessions(agent?, limit?, active_only?)` | recent sessions (title, workspace, git branch, last activity) |
| `list_crons(agent?)` | configured cron jobs |
| `list_skills(agent?)` | installed skills |
| `list_tools` | union of configured toolsets |
| `chat(agent, message, caller?)` | run an agent → `{session_id, status}` |
| `get_session(session_id)` | poll a chat → `{status, reply, …}` |

`agent` maps to a Hermes profile name (`default` is the root profile). The `caller` claim on
`chat` is recorded for per-human attribution.

## How agents map to Hermes

- **Agent = Hermes profile.** `default` = `<HERMES_HOME>/config.yaml`; named agents =
  `<HERMES_HOME>/profiles/<name>/config.yaml`.
- `--profile <name>` sets an isolated `HERMES_HOME`, so sessions/crons/skills are read
  per-profile.
- **Chat** runs `hermes --profile <agent> -z "<message>"` (one-shot: prints only the final
  reply, approvals auto-bypassed).

## Connect from an MCP client

Point any MCP client at `http://<host>:<port>/mcp` with the bearer header. Example client
config (DMOps or another MCP host):

```json
{
  "mcpServers": {
    "hermes-fleet": {
      "url": "https://box.example.com/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

## Security

- **Bearer token per box** — generated by `gen-key`, validated (constant-time) on every request.
- **Read + chat only** — no admin mutation tools in this version.
- **TLS required** in production; never expose the endpoint anonymously.
- **DNS-rebinding protection** — off by default (server-to-server); opt in with
  `--allowed-host <domain>` to whitelist your public host while keeping it enabled.

## Development

```bash
pip install -e ".[dev]"
pytest
```