@marwansaab/claude-code-stateful-cli-mcp
v0.1.0
Published
Stateless MCP server exposing Claude Code's stateful CLI sessions to MCP clients by spawning `claude -p` per call. Tools: start_session, send_message.
Readme
claude-code-stateful-cli-mcp
A stateless MCP server that exposes Claude Code's stateful CLI sessions to MCP clients
(primary client: Cowork) by spawning claude -p per call over the stdio transport.
Status:
start_sessionimplemented (BI-0099).send_messageis schema-complete and returns a structuredNOT_IMPLEMENTEDerror until BI-0100. Not yet published to npm.
⚠️ Do not install this MCP in Claude Code's own config
This server spawns the claude CLI. If Claude Code itself is configured to launch this server,
every spawned claude could launch another wrapper — unbounded recursion. The server carries a
recursion sentinel (CLAUDE_CODE_WRAPPER_DEPTH, hard depth cap 1): a wrapper that finds itself
running inside another wrapper's spawn refuses to register tools and exits. The sentinel is
a backstop, not permission — install this MCP only in clients that are not Claude Code
(e.g. Cowork).
Architecture
- Stateless wrapper. The server keeps no session registry, no database, nothing. The client
carries
session_id+cwdon every call; the server validates both each time and stores neither. Sessions live where Claude Code puts them:~/.claude/projects/<hash>/<UUID>.jsonl. - One spawn per call.
start_sessionspawnsclaude --session-id <new-UUID> --model <model> --permission-mode auto --output-format stream-json --verbose --include-partial-messages -p;send_messagewill spawnclaude --resume <session_id> --model <model> --permission-mode auto -p(BI-0100). Prompt text is delivered via stdin, never argv (Windows command lines cap at 32,767 chars, and prompts must never transit a shell). All spawns go through one sanctioned spawn module (src/spawn/) —shell:falsealways, recursion sentinel stamped, idle + wall timeouts enforced on every spawn. - Per-session serialisation. Concurrent calls on the same
session_idwait for the in-flight call (never a busy-rejection); different sessions run in parallel. The lock map is in-memory and ephemeral — a server restart loses nothing but the locks, and on-disk sessions are untouched. - Restartable by design. Kill it, restart it, keep calling
send_messagewith the samesession_id+cwd— the sessions are on disk.
Tools
| Tool | Purpose | Required inputs |
| --------------- | -------------------------------------- | --------------------------------------- |
| start_session | Create a NEW session, eager first turn | cwd, initial_prompt, model |
| send_message | Continue an EXISTING session | session_id, cwd, message, model |
model is required on both tools with no default. There is deliberately no end_session —
sessions are files, not server state.
start_session
Starts a NEW session in a working directory, runs the first turn, and returns the new
session_id together with the assistant's response. The session is created eagerly: once the
call returns successfully, it exists on disk and is resumable with (session_id, same cwd).
A failed start never returns a session identifier.
Inputs
| Field | Required | Description |
| ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| cwd | yes | Absolute path (on the host where the server runs) of an existing directory. Pass the SAME cwd on every later send_message for this session. |
| initial_prompt | yes | First user message; non-empty, not whitespace-only. No size limit — delivered via stdin, intact. |
| model | yes | Must be on the accepted list below. Passed to the CLI verbatim, never substituted. |
| timeout_idle_ms | no | Rolling inactivity limit in ms; any output resets it. Default 60000 (60 s). Expiry → IDLE_TIMEOUT. |
| timeout_wall_ms | no | Overall time budget in ms. Default 1800000 (30 min). Expiry → WALL_TIMEOUT. |
Accepted models (wrapper-maintained list, updated by wrapper release): aliases opus,
sonnet, haiku — aliases track upstream's "latest in line" and drift over time — and
version-tagged claude-opus-4-8, claude-sonnet-5, claude-haiku-4-5-20251001. Matching is
exact; anything else is refused pre-spawn as MODEL_REJECTED with the accepted list in the
error details.
Result (JSON text content): session_id, response, is_error: false, plus pass-through
usage, model_usage, total_cost_usd, duration_ms, num_turns (each an object/number or
an explicit null when the platform reported no figures — keys always present, never
fabricated) and terminal_reason.
Progress (opt-in): send a progressToken in the request's _meta (standard MCP progress
mechanism) to receive one notifications/progress per underlying stream-json event — the raw
event line verbatim in message, with a per-call counter increasing from 1 and no total.
Without a token, no progress is sent. The final response text never appears in progress.
Errors: exactly one stable code per failure, never accompanied by a session identifier, and never retried by the wrapper. The full code → condition table lives in the contract: specs/001-start-session/contracts/start_session.md.
Stable codes across both tools: SCHEMA_VALIDATION, CWD_INVALID,
SPAWN_FAILED, AUTH_FAILED, IDLE_TIMEOUT, WALL_TIMEOUT, SESSION_ID_IN_USE,
SESSION_NOT_FOUND, CREDIT_EXHAUSTED, RATE_LIMITED, MODEL_REJECTED, CLI_EXIT_UNKNOWN,
CLI_OUTPUT_INVALID, CANCELLED (+ NOT_IMPLEMENTED, now only from the send_message stub
until BI-0100).
Prerequisites
claudeCLI on PATH (orCLAUDE_BINset to the executable's full path). On Windows the server dereferences the npmclaude.cmdshim to the realclaude.exe— it never spawns.cmdfiles and never uses a shell.- OAuth subscription auth —
claude auth statusmust reportloggedIn: true.ANTHROPIC_API_KEYmust NOT be set at any scope. - Node.js >= 22.11.
Install (Cowork / any MCP client, stdio)
Until npm publication (post-BI-0099), install from a local clone:
git clone https://github.com/marwansaab/claude-code-stateful-cli-mcp.git
cd claude-code-stateful-cli-mcp
npm ci && npm run buildMCP client config (stdio transport):
{
"mcpServers": {
"claude-code": {
"command": "node",
"args": ["C:\\Github\\claude-code-stateful-cli-mcp\\dist\\index.js"]
}
}
}Once published, the config becomes the npx style:
{
"mcpServers": {
"claude-code": {
"command": "npx",
"args": ["-y", "claude-code-stateful-cli-mcp"]
}
}
}At boot the server runs a pre-flight (claude --version, claude auth status --json) and
refuses to register tools if the binary is missing or auth is not an active login. The
resolved auth mode is logged to stderr.
Billing model — read this
All wrapper-driven claude -p usage draws the operator's flat-rate subscription usage
limits. The previously announced Agent SDK monthly credit was paused by Anthropic before
taking effect (ADR-027 amendment, 2026-07-04), so wrapper calls share the usage bucket with
interactive Claude Code / Cowork use until Anthropic ships a replacement. Operator requirement:
usage credits stay DISABLED in the Anthropic console, so usage can never silently overflow
to pay-per-token API rates. CREDIT_EXHAUSTED stays reserved in the error enum; while the
pause holds, exhaustion presents as subscription-limit throttling (trigger semantics land with
BI-0102). If the pre-flight detects API-key billing it warns loudly — that configuration is
never correct on this host.
Development
npm ci
npm run test # vitest + coverage gate (aggregate statements floor)
npm run lint # eslint flat config, zero warnings
npm run typecheck # tsc --noEmit
npm run build # tsc -p tsconfig.build.jsonProject law lives in .specify/memory/constitution.md. CI runs the full gate on windows-latest + ubuntu-latest — production is Windows; platform-specific code must be seam-injected so both legs exercise every branch.
Attributions
Per constitution Principle V (Attribution & Layered Composition Transparency):
| Upstream | License | Version | What was lifted / adapted |
| ------------------------------------------------------------------ | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| obsidian-cli-mcp | MIT | 0.8.10 | Toolchain baseline (tsconfig, eslint flat config, vitest coverage-gate layout, CI pipeline shape), UpstreamError shape, registerTool factory pattern, server bootstrap pattern. Each adapted file carries a header naming what was lifted vs. adapted. |
All other modules are original (// Original — no upstream. headers).
License
MIT © Marwan Saab
