Skip to main content
Glama
minmax

qwen-cli-mcp

README.md
# qwen-cli-mcp

MCP server that delegates coding tasks to your **locally installed**
[Qwen Code](https://github.com/QwenLM/qwen-code) CLI.

It wraps the real `qwen` binary instead of bundling its own copy of the agent, so every call
inherits your qwen auth, models, MCP servers and settings. Nothing about your model stack is
duplicated here, and the server does not drift when you upgrade qwen.

Sibling of [pi-cli-mcp](https://www.npmjs.com/package/pi-cli-mcp) — same architecture, same
principles, qwen behind the wheel. The authoritative design document is [SPEC.md](SPEC.md).

Use it when your primary agent (Claude Code, Cursor, pi itself, any MCP client) should hand work
to qwen: a second opinion from a different model family, an investigation you want kept out of the
main context window, or parallel work.

## Install

```bash
npx -y qwen-cli-mcp         # no install
npm install -g qwen-cli-mcp # or global
```

Requires Node ≥ 22 and a working `qwen` on `PATH` (`npm i -g @qwen-code/qwen-code`).

### Claude Code

```bash
claude mcp add-json qwen -s user '{
  "type": "stdio",
  "command": "npx",
  "args": ["-y", "qwen-cli-mcp"],
  "timeout": 3600000
}'
claude mcp list | grep '^qwen:'    # expect: ✔ Connected
```

The generous `timeout` matters: the server applies no run deadline by default, and a real
delegated task can run for minutes.

### Any other MCP client

```json
{
  "mcpServers": {
    "qwen": { "command": "npx", "args": ["-y", "qwen-cli-mcp"] }
  }
}
```

Keep the server name short (`qwen`): it becomes part of the tool names your model sees.

> **⚠️ Default approval mode is `yolo`.** Delegation is only useful when the delegate can act, so
> this server starts qwen with full tool approval by default — inside `cwd`, as your user. Narrow
> it with `QWEN_MCP_APPROVAL_MODE`, per-call `approval_mode`, or `allowed_tools`. For analysis-only
> work pass `allowed_tools: "read,grep,ls"`-style allowlists.

## Tools

| Tool | Purpose |
|---|---|
| `qwen` | Start a session. Returns the run's identity (`[session:]`, `[session-key:]`, `[transcript:]`), the result, and stats. |
| `qwen_reply` | Continue a finished or interrupted session — including one killed by a timeout. |
| `qwen_models` | List models qwen can actually reach right now (live probe via the control plane). |
| `qwen_send` | Deliver into a turn executing right now (`abort` / `steer` / `follow_up`; acp/stream). |
| `qwen_running` | List turns executing right now that `qwen_send` can reach. |
| `qwen_sessions` | List known sessions, newest first, with working directory, accounting key and transcript path. |
| `qwen_history` | Read a bounded snapshot of a session's visible conversation from qwen's native chat JSONL. |

### `qwen`

| Argument | Notes |
|---|---|
| `prompt` | Required. Must be self-contained — qwen cannot see your conversation. |
| `cwd` | Absolute path; defaults to this server's cwd. |
| `model` | `-m` value. Defaults to qwen's own settings; `qwen_models` lists valid values. |
| `approval_mode` | `plan` \| `default` \| `auto-edit` \| `auto` \| `yolo`. Server default: see the warning above. |
| `allowed_tools` | Comma-separated tool allowlist passed as `--allowed-tools`. |
| `effort` | `low` … `max` reasoning effort. ACP (`reasoning_effort`) and stream (initialize handshake). Print rejects it. |
| `system_prompt_append` | Appended to qwen's system prompt for this run. |
| `transport` | `acp` (default), `stream`, or `print`. Usually omit. |
| `timeout_ms` | Wall clock for this run. Off unless you set it — the task decides whether it needs a deadline. |

## What comes back

Only qwen's final result plus aggregate stats — never the transcript, tool arguments or raw stdout:

```
[session: 0927adc5-a840-4b68-93ca-5ca344c9fafb]
[session-key: qwen:0927adc5-a840-4b68-93ca-5ca344c9fafb]
[transcript: /Users/you/.qwen/projects/-Users-you-projects-thing/chats/0927adc5-a840-4b68-93ca-5ca344c9fafb.jsonl]

Refactored retry() in src/http.rs; all 12 tests pass.

---
qwen: qwen3-coder-plus · 6 turns · 5 tool calls: bash×2, read×2, edit · 18k in / 310 out · 41s
```

### Identifying a run

`[session-key: qwen:<uuid>]` is the key session-accounting tools use; `[transcript:]` is the JSONL
qwen wrote the run to — `<runtime>/projects/<cwd with every non-alphanumeric turned into ->/chats/<session id>.jsonl`,
with `QWEN_RUNTIME_DIR` / `QWEN_HOME` honoured and the cwd resolved the way qwen resolves its own.
The whole path is derived, not searched. A broken run keeps the SPEC §7 behaviour and gets no
identity prefix — `qwen_sessions` still resolves both from the id in the note.

A blocking call cannot report anything until it returns, and a run the host cuts short never
returns. Two things cover that: a `notifications/progress` message sent the moment the run starts
(`qwen started · qwen:<uuid> · <model> · cwd=<dir>`, then `transcript <path>`), and `qwen_sessions`,
which reads state written *before* each run and carries the key and the path for every session.

The answer is the `result` envelope qwen emits at the end of a turn — there is no answer-selection
guesswork. An error envelope (`error_max_turns`, `error_during_execution`) fails the call while
keeping everything qwen managed to say, so the work stays resumable. A stream that ends without any
result envelope is reported as broken, never silently replaced by raw output.

## Sessions

`qwen` returns a session id; `qwen_reply` resumes it with `--resume`. The conversation lives in
qwen's own session store, so follow-ups keep working across restarts of this server — the
session → directory map is persisted in `~/.local/state/qwen-mcp/sessions.json`.

Concurrent replies to one session are serialized per server process: two qwen processes writing one
session file would corrupt it. **Cross-process caveat:** if you run two MCP clients against two
server processes and both reply to the *same* session id at the same time, nothing serializes them.
In practice one client owns a session; for a hard guarantee, keep one server process.

### `qwen_history`

Read-only snapshot of qwen's native chat JSONL
(`<runtime>/projects/<sanitized cwd>/chats/<id>.jsonl`). It does not prompt qwen, take the session
lock, or wait for a running turn. `state` is `active` only when this server process currently has
the turn; after a restart it is `unknown`. Parts with `thought: true` are omitted. Pass the returned
`cursor` back to continue, including at the current EOF. Caps: `limit` (default 50, max 200),
`max_chars` per item (default 4000, max 16000), and 100000 characters on the whole JSON page.

## Transports

| | `acp` (default) | `stream` | `print` |
|---|---|---|---|
| command | `qwen --acp` | `--input-format stream-json --output-format stream-json` | `-p --output-format stream-json` |
| process | stays up, ACP JSON-RPC on stdin | stays up, JSONL control plane on stdin | one process per turn, exits when done |
| mid-run delivery | `qwen_send` (`session/cancel` / steer / follow_up) | `qwen_send` (interrupt / steer / follow_up) | impossible: qwen reads nothing while working |
| deadline / cancel | `session/cancel` first, signals as fallback | qwen's own `interrupt` first, signals as fallback | SIGTERM, then SIGKILL |

`acp` is the default because it is the protocol qwen's own clients speak (`qwen --acp`), and a
running turn stays reachable. `stream` is the previous JSONL default. `print` is for a process
that cannot be talked to. Pick per call with `transport`, or set the default with
`QWEN_MCP_TRANSPORT=stream` / `print`.

This is stdio ACP, not a client of `qwen serve`. Sessions still live in qwen's own store and
are resumable with `qwen_reply`; they do not appear as live daemon sessions in `qwen serve`.

**This server never sends anything into qwen on its own.** No automatic wrap-up before a deadline,
no injected instructions: `qwen_send` fires only when the caller calls it.

## Environment

| Variable | Default | Meaning |
|---|---|---|
| `QWEN_MCP_BIN` | `qwen` | Path to the qwen binary. |
| `QWEN_MCP_APPROVAL_MODE` | `yolo` | Default approval mode for every call. See the warning above. |
| `QWEN_MCP_MODEL` | unset | Default model for every call. |
| `QWEN_MCP_TRANSPORT` | `acp` | Default transport: `acp`, `stream`, or `print`. |
| `QWEN_MCP_TIMEOUT_MS` | unset | Server-wide default wall clock; unset means no deadline. `timeout_ms` overrides it per call. |
| `QWEN_MCP_MAX_TIMEOUT_MS` | `86400000` | Ceiling on what `timeout_ms` may ask for. |
| `QWEN_MCP_MAX_CONCURRENT` | `100` | Concurrent qwen processes. |
| `QWEN_MCP_MAX_OUTPUT` | unset | Cap on the answer. Unset means no truncation. |
| `QWEN_MCP_STDERR_LIMIT` | `1500` | stderr tail included in the response. |
| `QWEN_MCP_STDERR_KEEP_EVENTS` | unset | `1` forwards stderr verbatim, protocol lines included. |
| `QWEN_MCP_MAX_CAPTURE` | `16000000` | Read-buffer guard against a runaway stream. |
| `QWEN_MCP_MAX_LINE` | `8000000` | Longest single message line from qwen before it is dropped. |
| `QWEN_MCP_MAX_FRAME` | `8000000` | Longest single JSON-RPC frame from the client. |
| `QWEN_MCP_MAX_SESSIONS` | `200` | Remembered sessions before the oldest is dropped. |
| `QWEN_MCP_KILL_GRACE_MS` | `5000` | SIGTERM → SIGKILL grace period. |
| `QWEN_MCP_INTERRUPT_GRACE_MS` | `5000` | How long in-protocol stop (`session/cancel` / stream `interrupt`) gets before signals. |
| `QWEN_MCP_INIT_TIMEOUT_MS` | `15000` | Initialize-handshake timeout (ACP and stream). |
| `QWEN_MCP_MODELS_TIMEOUT_MS` | `60000` | Whole-run budget for the `qwen_models` probe. |
| `QWEN_MCP_STATE` | `~/.local/state/qwen-mcp/sessions.json` | Session → cwd map. |
| `QWEN_RUNTIME_DIR` / `QWEN_HOME` | `~/.qwen` | qwen's own state directory. Read, not set, so `[transcript:]` points where qwen actually writes. |
| `QWEN_MCP_WRAP` | unset | Command prefix, e.g. a sandbox wrapper around qwen. |

## Design

- **Process per call.** Qwen's own session files are the source of truth, which is what makes
  follow-ups survive a restart of this server.
- **Default wire is ACP** (`qwen --acp`): JSON-RPC `initialize` / `session/new|load|prompt|cancel`
  on stdio. Print and stream still speak qwen's stream-json protocol. Stream-json shapes are
  borrowed from [`@qwen-code/sdk`](https://www.npmjs.com/package/@qwen-code/sdk) through
  `import type`, so an upstream change breaks the build instead of the server. Zero runtime
  dependencies. ACP updates are converted into the same internal `assistant` / `result` shapes.
- **Fail closed on anything from qwen.** An unknown result subtype, a failed handshake, a line that
  does not parse — reported as such, never normalized into success.
- **No process outlives its request.** Timeouts, cancellations and shutdown reap the whole qwen
  process tree; nothing is left behind on any path.

## Development

TypeScript (native `tsc`), Biome, Vitest. Tests drive the real server binary over stdio against a
fixture that speaks qwen's protocol; live tests against the installed `qwen` are opt-in.

```bash
npm run hooks          # once per clone: git hooks from .githooks/
npm run build          # tsc -> dist/
npm test               # unit + type tests, no API access, no tokens
npm run test:live      # 4 live tests against the real qwen binary (spends tokens)
npm run check          # format + types + tests
npm run fix            # biome --write
```

## License

MIT

TDQS

A4.6/5.0

Scored across 6 tools

Disambiguation5/5

Each tool has a clearly distinct role: starting a new session, resuming an idle one, messaging a running turn, listing models, listing currently running sessions, and listing historical sessions. The boundaries between qwen_reply and qwen_send (idle vs. executing) and between qwen_running and qwen_sessions (current vs. all) are explicitly explained, leaving no ambiguity for an agent.

Naming Consistency5/5

All tools share the snake_case prefix `qwen_`, with descriptive suffixes (`qwen_reply`, `qwen_models`, `qwen_running`). While the suffixes mix verbs and nouns, the pattern is uniform and predictable, making it easy to infer purpose from the name alone.

Tool Count5/5

With 6 tools, the server is well-scoped for managing a CLI coding agent. Each tool addresses a distinct need (creation, interaction, inspection, enumeration) without redundancy, fitting comfortably within the ideal 3–15 range.

Completeness4/5

The tool surface covers the full lifecycle: start, resume, interrupt (via send), list running, list models, and list historical sessions. A terminate/kill tool for running sessions is absent, but this is a minor gap given that sessions can be resumed after timeout and the descriptions encourage using `qwen_reply` for stalled sessions.

Maintenance

ActivityMaintained
ResponsivenessNo issues