claude-code-mcp
# claude-code-mcp
It sounds dumb...
but its literally that. Using Claude Code through an MCP server.
Its meant for orchestrator agents (like [**Hermes**](https://github.com/nousresearch/hermes-agent)), giving it fine-grained control over interactive **Claude Code** sessions running inside **tmux**. Hermes has a built in [Claude Code Skill](https://hermes-agent.nousresearch.com/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-claude-code#reference-full-skillmd), but let's be honest, it's huge and takes an enormous amount of the context window...
The core value: a long-running Claude Code REPL stays alive in a tmux pane, and
your orchestrator can **steer it at any point** — send a task without blocking,
watch progress, inject a follow-up mid-run, interrupt a wrong turn, or collect
the final answer. Way better than using it through `claude -p`.
The server replaces the whole tmux runbook an orchestrator would otherwise have
to carry in its prompt: launch dialogs, readiness gating, idle detection,
response extraction. Point Hermes at it and delete the skill file.
## Why an orchestrator needs this
| Need | Tool |
|------|------|
| Send a task without blocking on it | `session_send` |
| Watch progress while it works | `session_tail` |
| Get the final answer back | `session_wait` |
| Steer a follow-up into a *running* session | `session_send` again |
| Interrupt a wrong-direction turn | `session_interrupt` |
| Answer a question Claude asked | `session_respond` |
| One-shot task, structured result | `claude_run` |
| Parallel work on one repo | `worktree=` |
| Keep quality up on long sessions | `session_compact` |
## The core loop
```python
session_send("work", "implement the auth module", working_dir="/proj")
# → returns immediately: {"dispatched": true, "created": true, ...}
session_tail("work") # progress, any time
session_send("work", "use JWT, not sessions") # steer mid-run
session_wait("work", timeout=300) # → {"response": "...", "timed_out": false}
```
`session_wait` is **resumable**. If it times out, nothing is lost — the pending
dispatch survives and the next call keeps waiting:
```python
r = session_wait("work", timeout=300)
while r["timed_out"]:
r = session_wait("work", timeout=300)
```
## Why not `claude -p`?
`claude -p` exits after one response. There is **no channel to inject a
follow-up** — every turn spawns a fresh process and loses accumulated context.
This server keeps the **interactive REPL** alive in a tmux pane, so prompts can
be injected at any time, history accumulates inside the running process, and
mid-run interrupts work as expected.
Print mode is still available via `claude_run` for one-shot work — and because
sessions are minted a `--session-id` up front, a print-mode call can resume a
tmux session's conversation. Start cheap, escalate to interactive.
## Features
- **Non-blocking dispatch** — `session_send` returns as soon as the prompt is
injected; `session_wait` collects the result whenever you want it.
- **Steer mid-session** — send again while Claude is working. Input is queued
and the eventual result still spans the whole run.
- **Launch dialogs handled** — workspace trust, the bypass-permissions warning
(whose default is "No, exit"), and the effort selector are answered
automatically. Anything *unrecognised* is surfaced as `awaiting_input`
instead of being blind-Entered.
- **Real interrupt** — sends Escape, Claude Code's own interrupt, and verifies
it took. Ctrl-C is only a fallback (a double Ctrl-C exits the process).
- **Send & auto-create** — no separate `session_start` call required.
- **Shared conversations** — a minted `--session-id` lets print mode and tmux
address the same history.
- **Scoped capability** — `allowed_tools`, `model`, `effort`, `add_dir`,
`append_system_prompt`, `permission_mode` per session.
- **Worktree isolation** — `worktree="feature-x"` for parallel tasks on one repo.
- **Context health** — `session_context`, `session_compact`, and an optional
`auto_compact_at` threshold on `session_wait`.
- **Token-efficient** — you get the extracted answer, not the pane dump.
- **Pure stdlib + mcp SDK** — no heavy dependencies.
## Requirements
| Tool | Version |
|------|---------|
| Python | ≥ 3.11 |
| tmux | ≥ 3.4 |
| Claude Code | ≥ 2.0 |
## Installation
```bash
pip install claude-code-mcp
```
Or with [uv](https://docs.astral.sh/uv/):
```bash
uv tool install claude-code-mcp
```
For development:
```bash
git clone https://github.com/joschi655/claude-code-mcp
cd claude-code-mcp
pip install -e ".[dev]"
```
## MCP configuration
### Claude Desktop / Claude Code
```json
{
"mcpServers": {
"claude-code-mcp": {
"command": "claude-code-mcp"
}
}
}
```
### With uvx (no install required)
```json
{
"mcpServers": {
"claude-code-mcp": {
"command": "uvx",
"args": ["claude-code-mcp"]
}
}
}
```
### Hermes / custom MCP client
```json
{
"mcpServers": {
"claude-code-mcp": {
"command": "python",
"args": ["-m", "claude_code_mcp"]
}
}
}
```
## Tools
### `session_send(name, prompt, ...)`
Send a prompt and **return immediately**. Creates the session if it does not
exist. Sending into a session that is already working is a supported *steer*.
```json
{ "name": "work", "prompt": "implement feature X", "working_dir": "/proj" }
```
Returns `{dispatched, name, created, steered, state}`.
Launch options (applied only when the session is created): `working_dir`,
`permission_mode`, `model`, `effort`, `allowed_tools`, `disallowed_tools`,
`add_dir`, `append_system_prompt`, `worktree`.
---
### `session_wait(name, timeout=300, auto_compact_at=None)`
Block until the session finishes, then return the answer. This is the
completion trigger — the result comes back as the tool result.
Returns `{response, state, timed_out, baseline, elapsed_s, steers}`.
- `timed_out: true` — still working. Call again; the pending dispatch survives.
- `baseline: "lost"` — the server restarted since the prompt was sent, so
`response` is a plain tail of the pane rather than an exact diff.
- `question` — present when the session stopped on a prompt.
- `auto_compact_at=70` — run `/compact` automatically past that context usage.
---
### `session_tail(name, lines=40)`
Last *n* lines of pane output, ANSI-stripped. Use this to check on a long task
instead of assuming it is stuck.
---
### `session_interrupt(name)`
Stop the current turn without killing the session. Sends Escape and verifies
the session left the busy state; falls back to Ctrl-C only if that fails.
---
### `session_respond(name, choice)`
Answer a question when `state` is `awaiting_input`. `choice` is an option
number (`"1"`, `"2"`) or a key: `up`, `down`, `enter`, `escape`. Read the
question from `session_status` first.
---
### `session_status(name)` / `session_list()` / `health()`
```json
{
"name": "work",
"tmux_alive": true,
"claude_alive": true,
"state": "busy",
"claude_session_id": "02d8d5c3-...",
"working_dir": "/proj",
"context_pct": null,
"pending": true,
"question": null
}
```
`state` is one of `missing`, `starting`, `busy`, `awaiting_input`, `idle`.
---
### `session_start(name, ...)`
Pre-create a session. Usually unnecessary — `session_send` does it — but useful
to fix launch options up front. A fresh session is minted a UUID so
`claude_run` can address the same conversation later.
---
### `session_compact(name, focus=None)` / `session_context(name)`
Compress context, or read usage as a percentage. Output quality degrades above
roughly 70% context usage. `session_context` returns `null` when the TUI output
cannot be parsed — treat that as unknown, not zero.
---
### `session_stop(name)` / `session_destroy(name)`
`session_stop` kills the tmux session but keeps the conversation, so it can be
resumed later — and so `claude_run` can address it without transcript
contention. `session_destroy` forgets it entirely.
---
### `claude_run(prompt, session_name=None, ...)`
One-shot `claude -p` with structured output:
```json
{
"result": "...",
"session_id": "75e2167f-...",
"num_turns": 3,
"total_cost_usd": 0.0787,
"duration_ms": 10276
}
```
Pass `session_name` to resume a managed session's conversation. A live *busy*
session is refused — two writers corrupt the transcript. `fork=True` branches
to a new session ID that inherits history, avoiding contention entirely.
## Sharing history between print mode and tmux
Sessions get a `--session-id` at launch, so a conversation is addressable
before it produces any output:
```python
session_send("work", "Remember the codeword: BANANA42")
session_wait("work")
session_stop("work") # free the transcript
claude_run("What was the codeword?", session_name="work")
# → "BANANA42"
session_start("work") # back to interactive, history intact
```
A running TUI holds its history in memory, so it will not display a print-mode
turn until restarted. `session_stop` first, or use `fork=True`.
## Permissions
Sessions launch with `--permission-mode bypassPermissions` by default so
unattended runs are not blocked waiting for approval. **The real safety control
is `allowed_tools`** — scope each session to what the task actually needs:
```python
session_send("review", "review the diff vs main",
working_dir="/proj", allowed_tools=["Read", "Bash(git *)"])
```
Override per session with `permission_mode`: `acceptEdits`, `auto`,
`bypassPermissions`, `manual`, `dontAsk`, `plan`.
## Parallel work
Independent sessions run concurrently. For several tasks against one repo, use
worktrees so they don't collide:
```python
session_send("backend", "fix the auth bug", working_dir="/proj", worktree="auth-fix")
session_send("frontend", "update the header", working_dir="/proj", worktree="header")
session_send("tests", "add API tests", working_dir="/proj", worktree="api-tests")
health() # all sessions and their states at a glance
```
## Development
```bash
# Unit tests (no tmux/claude required)
pytest tests/test_parser.py tests/test_session_logic.py -v
# Integration tests (requires tmux + claude, spends tokens)
CLAUDE_TMUX_INTEGRATION=1 pytest tests/test_integration.py -v
```
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 15 tools
Most tools have clearly distinct purposes, but session_stop and session_destroy both kill the tmux session, differentiated only by keeping metadata. Also, session_status, session_context, and session_tail all read session state, though they target different aspects. Descriptions clarify the boundaries sufficiently.
The vast majority follow a consistent session_<verb> pattern (list, start, send, wait, tail, respond, interrupt, status, compact, destroy, stop). The claude_run tool deviates by using a different prefix, but it is still a clear, readable name that fits the server's purpose.
At 15 tools, it sits at the upper edge of the ideal range. Each tool covers a distinct aspect of session lifecycle or health, so the count feels justified rather than bloated. It is slightly heavy but not excessive for the domain.
The tool set covers the full session lifecycle: create, send, wait, tail, respond, interrupt, compact, stop, destroy, plus one-shot execution and health/context checks. There are no obvious missing operations that would leave an agent stuck.