Skip to main content
Glama
README.md
# 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

A4/5.0

Scored across 15 tools

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count4/5

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.

Completeness5/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues