Skip to main content
Glama
README.md
# cware-hil-mcp

Human-in-the-loop **MCP hub** for Claude Code agents, answered in Obsidian.

Agents call MCP tools to ask a human a question, request approval, or report
status. This standalone hub holds the queue and exposes an MCP endpoint; the
[Obsidian plugin](https://github.com/KilianSen/cware-hil-obsidian) is the human's UI.

```
Claude Code agents ──HTTP /mcp──▶  hub (Go, :22360)  ◀──ws /bridge──  Obsidian plugin
                                   └ SQLite queue (survives restarts)
```

The hub is written in Go (single static binary, pure-Go SQLite — no cgo). Shared
domain types + the bridge protocol live in
[cware-hil-lib](https://github.com/KilianSen/cware-hil-lib): its zod schemas are
the single source of truth, emitted as JSON Schema and code-generated into
`internal/protocol/domain.gen.go`, so a shape change in the lib is a Go compile
error here. Run `go generate ./...` to regenerate after a lib change.

## Build

```bash
go build -o cc-hitl ./cmd/hub
```

Requires Go ≥ 1.26. No native toolchain needed — `modernc.org/sqlite` is pure Go,
so `CGO_ENABLED=0` produces a fully static binary.

## Run

```bash
./cc-hitl start            # start the hub
./cc-hitl status           # health check
./cc-hitl stop             # stop the running hub
./cc-hitl token            # print the bearer token
./cc-hitl setup            # print Claude Code / Obsidian connection details
```

State lives in `~/.cc-hitl/` (`hub.json` holds the token; `hub.db` is the queue).
The hub binds to `127.0.0.1` by default; override with `--host <addr>` or
`CC_HITL_HOST` (e.g. `0.0.0.0` to accept connections from other machines).

This container serves **no UI** — only `/mcp`, `/bridge`, and `/health` (and a plain
pointer at `/`). The web UI is a separate deployable,
[cware-hil-ui](https://github.com/KilianSen/cware-hil-ui), that you point at this hub
for a setup page + live dashboard. The CLI also prints the connection snippets on
`start` (and via `cc-hitl setup`).

## Docker

```bash
export CC_HITL_TOKEN=$(openssl rand -hex 24)
docker compose up -d --build
docker compose exec hub /cc-hitl status
```

The container binds `0.0.0.0` and compose publishes it on `127.0.0.1:22360`.
State persists in the `hub-data` volume (`CC_HITL_HOME=/data`). The image is a
distroless static build (~24 MB). Without compose:

```bash
docker build -t cware-hil-mcp .
docker run -d --name cc-hitl -p 127.0.0.1:22360:22360 \
  -e CC_HITL_TOKEN=$CC_HITL_TOKEN -v cc-hitl-data:/data cware-hil-mcp
```

**Exposing beyond loopback:** once published on a LAN/public interface, the bearer
token is the *only* thing protecting `/mcp` and `/bridge`. Use a strong
`CC_HITL_TOKEN` and put a TLS reverse proxy in front.

### Prebuilt images

CI publishes images to GitHub Container Registry on every push to `main`:

```bash
docker pull ghcr.io/kiliansen/cware-hil-mcp:latest      # newest build
docker pull ghcr.io/kiliansen/cware-hil-mcp:0.1         # latest 0.1.x
docker pull ghcr.io/kiliansen/cware-hil-mcp:0.1.1       # exact version
```

Versioning is driven by commit messages ([Conventional Commits](https://www.conventionalcommits.org/)):
`fix:` bumps the patch, `feat:` the minor, and `feat!:` / a `BREAKING CHANGE:`
footer the major. A commit with no conventional prefix bumps the patch. Each push
tags the repo (`vX.Y.Z`) and pushes matching `X.Y.Z`, `X.Y`, `X`, and `latest`
image tags. See `.github/workflows/docker.yml`.

## Connect Claude Code

`cc-hitl start` prints the exact command (or use the cware-hil-ui setup page). In your project:

```bash
claude mcp add --transport http --scope project hitl http://127.0.0.1:22360/mcp \
  --header "Authorization: Bearer <token>"
```

Set a long per-server `timeout` (the printed snippet uses 24h) because the ask
tools block until a human answers.

### Tools agents get

- `ask_user(prompt, title?, agent_label?, agent_key?)` — open question, returns typed text. **Blocks.**
- `ask_choice(prompt, choices[], multi?, …)` — pick one/several (+ optional note). **Blocks.**
- `request_approval(title, body, diff?)` — approve/reject + comment. **Blocks.**
- `notify(message, level?)` — fire-and-forget notice. Non-blocking.
- `update_status(label?, status?, current_task?, progress?, done?)` — dashboard feed. Non-blocking.
- `check_messages(agent_key?)` — receive messages the human sent you. Non-blocking.
- `unregister_agent(agent_key?)` — drop this agent from the dashboard. Non-blocking.

## Test

```bash
go test ./...
```

The `internal/e2e` suite spawns a real hub on a temp `CC_HITL_HOME` and drives each
tool through an MCP client + bridge WebSocket client; `internal/hub` covers
cancellation, timeout, orphaned-answer delivery, persistence across restart, and
the stale-agent sweep.