Herdr Grok Bridge
README.md
# herdr-grok-bridge
Unofficial **read-only** [MCP](https://modelcontextprotocol.io) bridge from
[Grok Bot](https://grok.x.ai) (or any MCP stdio client) to a running
[Herdr](https://herdr.dev) session.
This project is **not** affiliated with Herdr. It offers MCP *tools* only — not
an embedded native Herdr UI, Canvas, or refresh button.
## What it does
Strict allowlist of three read operations (plus a non-sensitive `bridge_info`):
| Tool | Herdr command | Purpose |
| --- | --- | --- |
| `list_agents` | `herdr agent list` (+ optional `--format json` retry) | Active agents, statuses, `pane_id` targets |
| `get_agent_status` | `herdr agent get <pane_id>` (+ optional `--format json` retry) | Status for one live pane |
| `read_pane` | `herdr pane read <pane_id> --source …` | Bounded pane text (redacted / truncated) |
| `bridge_info` | _(none)_ | Mode, timeouts, limits — never SSH host |
**Not included (hard-denied):** prompts, send-keys, focus, start/attach, pane
run/close/split, generic RPC/shell, or any user-supplied executable command.
Read targets must be **live `pane_id` values** from `list_agents` (for example
`w1:p2`). Bare kinds such as `claude` are rejected.
## Security risks
- **Read access can reveal terminal secrets** (tokens, passwords, private keys
scrolled into a pane). Output is best-effort redacted and size-limited, but
redaction is not perfect.
- Prefer Tailscale / SSH Host aliases and existing `known_hosts` entries.
- The bridge never changes focus, prompts agents, or restarts Herdr — but a
compromised MCP client can still *read* whatever is visible in panes you
expose.
## Requirements
- Python 3.12+
- A reachable Herdr CLI (`herdr`) on the Herdr host (verified conceptually
against Herdr 0.8.x / local-only socket; this bridge talks to the CLI, not
the Unix socket directly)
- For remote use: OpenSSH client, `BatchMode` key auth, host key already in
`known_hosts`
## Install
### From a release artifact (no git checkout)
```sh
python3 -m venv ~/.venvs/herdr-grok-bridge
~/.venvs/herdr-grok-bridge/bin/pip install /path/to/herdr_grok_bridge-0.1.2-py3-none-any.whl
# or: pip install /path/to/herdr_grok_bridge-0.1.2.tar.gz
```
Entry point: `~/.venvs/herdr-grok-bridge/bin/herdr-grok-bridge`
### From source with uv
```sh
uv sync
uv run herdr-grok-bridge --help
```
## Deployment
### A) Local (MCP server on the Herdr host)
```json
{
"mcpServers": {
"herdr-readonly": {
"command": "/home/USER/.venvs/herdr-grok-bridge/bin/herdr-grok-bridge",
"args": [],
"env": {
"HERDR_BRIDGE_MODE": "local"
}
}
}
}
```
### B) SSH / Tailscale from Grok Bot’s Linux box (recommended remote path)
Configure OpenSSH so a **Host alias** already works non-interactively
(`ssh herdr-box herdr agent list`). Then:
```json
{
"mcpServers": {
"herdr-readonly": {
"command": "/home/USER/.venvs/herdr-grok-bridge/bin/herdr-grok-bridge",
"args": [],
"env": {
"HERDR_BRIDGE_MODE": "ssh",
"HERDR_BRIDGE_SSH_HOST": "herdr-box"
}
}
}
}
```
`herdr-box` is a **generic example** Host alias — use your own. Do not put
private hostnames, IPs, or credentials in this repository.
SSH argv is fixed by the bridge (not tool-callable):
- `BatchMode=yes`
- `StrictHostKeyChecking=yes` (existing `known_hosts` only)
- `PasswordAuthentication=no` / `KbdInteractiveAuthentication=no`
- optional `HERDR_BRIDGE_SSH_USER`, `HERDR_BRIDGE_SSH_PORT`,
`HERDR_BRIDGE_SSH_IDENTITY_FILE`
Remote shape: `ssh … <destination> -- herdr <allowlisted args…>`
### Environment reference
| Variable | Meaning |
| --- | --- |
| `HERDR_BRIDGE_MODE` | `local` or `ssh` (default `ssh` if host set, else `local`) |
| `HERDR_BRIDGE_SSH_HOST` | OpenSSH destination / Host alias |
| `HERDR_BRIDGE_SSH_USER` | Optional user |
| `HERDR_BRIDGE_SSH_PORT` | Optional port |
| `HERDR_BRIDGE_SSH_IDENTITY_FILE` | Optional identity file path |
| `HERDR_BRIDGE_HERDR_BIN` | Default `herdr` |
| `HERDR_BRIDGE_TIMEOUT_SECONDS` | Subprocess timeout (default `30`) |
| `HERDR_BRIDGE_CONNECT_TIMEOUT_SECONDS` | SSH connect timeout (default `10`) |
| `HERDR_BRIDGE_MAX_LINES` | Cap for `--lines` on recent sources (default `200`) |
| `HERDR_BRIDGE_MAX_OUTPUT_CHARS` | Client-side output cap (default `100000`) |
## Compatibility notes
- Herdr **0.8.0** rejects `agent list|get --format json` and already prints
JSON by default. The bridge calls those commands **without** `--format json`
first, then retries once *with* the flag only if the default path fails with
a CLI/JSON error (newer CLIs that require the flag).
- `list_agents` expects JSON shaped like
`{ "id": "cli:agent:list", "result": { "type": "agent_list", "agents": [ … ] } }`.
- Some Herdr versions reject `--lines` with `--source visible` / `detection`.
This bridge **omits** `--lines` for those sources and truncates client-side
when requested.
- For `recent` / `recent-unwrapped`, a capped `--lines` is always sent
(default 80).
## Error signaling (MCP `isError`)
Validation failures, Herdr CLI failures, timeouts, and SSH/transport errors
return a normal `tools/call` **result** with:
```json
{ "content": [{ "type": "text", "text": "…" }], "isError": true }
```
Clients must inspect **`isError` / `CallToolResult.is_error`**. Do not classify
success vs failure by scraping the text body.
**Unknown tools** (including any mutating name such as `agent_prompt` /
`pane_run`): not registered on this server. The official MCP Python SDK
rejects them with `isError: true` and message `Unknown tool: …` without
invoking our runner — so no Herdr/SSH command runs.
## Development
```sh
uv sync --group dev
uv run pytest
uv build # wheel + sdist under dist/
```
Unit tests are mock-backed: command construction, validation, timeouts,
JSON decoding, mutation denial, and an in-memory MCP init/list/call smoke test.
They do not touch a live Herdr host.
## License
MIT. Wholly new code for this repository; inspired by ideas from community
Herdr MCP experiments and the public Herdr docs, without reusing their source.