Skip to main content
Glama
README.md
<p align="center">
  <img src="https://img.shields.io/badge/MCP-server-4D6BFE?style=flat-square" alt="MCP server">
  <img src="https://img.shields.io/badge/Node-%E2%89%A522.19-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node 22.19+">
  <img src="https://img.shields.io/github/license/mbj733/dsh-subagent-mcp?style=flat-square" alt="MIT">
  <img src="https://img.shields.io/github/stars/mbj733/dsh-subagent-mcp?style=flat-square" alt="stars">
</p>

# dsh-subagent-mcp

**Give Codex a DeepSeek crew.** One tool call hands a task to a fresh DeepSeek Harness agent that
already has your skills, your `AGENTS.md` and your sign-in — and it works in the project directory
you name, then hands back the answer.

| What you get | Why it matters |
|---|---|
| **≈9k tokens per delegation** | A lite run costs about one file read, not a second conversation. |
| **V4.1-Flash at reasoning `max`** | The strong preset is the default. Nothing to tune. |
| **Your skills and `AGENTS.md`** | It behaves like the DSH you configured, because it uses your `DSH_HOME`. |
| **Long tasks can't be killed by a timeout** | It waits `wait_sec`, then returns a task id you collect later. |
| **Zero runtime dependencies** | ~600 lines with a hand-rolled JSON-RPC loop. Nothing to audit but the bridge. |
| **One process, one turn** | No daemon, no state to leak between tasks, no edits to your DSH profile. |

```text
Codex / Claude Code / any MCP client
        │  MCP over stdio
        ▼
  dsh-subagent-mcp                     ← this package, zero deps
        │  spawn: dsh --profile <p> --patch <overlay> --json
        ▼
  DeepSeek Harness (headless)          ← inherits DSH_HOME: AGENTS.md, skills, sign-in
        ▼
  your project directory · one turn · final answer
```

## Compared with other bridges in this space

Bridges between agent hosts and DeepSeek Harness come in three shapes: a **resident daemon** that
keeps agents alive and calls back into the host, an **SDK bridge** that runs workers in git
worktrees, and a **relay** that exposes an already-running DSH instance. This package is the
fourth: a **stateless one-shot bridge**. Where it wins, and where it deliberately loses:

| Dimension | This package | Typical alternative | Why it matters |
|---|---|---|---|
| **Runtime dependencies** | **Zero** — only `dsh` itself | `ws` + `@modelcontextprotocol/sdk` + `zod`, or a chain of SDK packages | A ~600-line single file is auditable in one sitting and cannot break because a transitive dep changed |
| **Your DSH profile** | **Never touched** — config arrives as `--patch` overlays | Written into the profile, or requires a plugin on the DSH side | Upgrading DSH can't collide with a bridge that wrote itself into your config |
| **Per-call cost** | **Two explicit tiers**: ≈9k or ≈300k tokens | One implicit size you discover on the bill | You choose the cost before the run |
| **Lifetime** | One process per task, then exit | Resident daemon / worker pool | Nothing runs in the background; a crash can't poison the next task |
| **Orphan processes** | Process-**tree** kill on cancel and shutdown | Sometimes leaves the MCP children `dsh` spawned | No slow memory drain after a few cancelled runs |
| **Config inheritance** | Real `DSH_HOME` → your skills, `AGENTS.md`, sign-in | Some runtimes get an isolated temporary `DSH_HOME` (an empty shell) | The child actually behaves like the DSH you configured |
| **Install** | `npx -y github:…`, no registry account | npm publish only | One config block, works today |
| **Cold start** | ≈1.7 s per delegation ⚠️ | Warm daemon is faster | The honest price of statelessness |
| **Native callback / session attach** | Not offered — needs host hooks ⚠️ | Daemon bridges call back natively and can attach to running sessions | If you need either, a daemon-shaped bridge is the right tool |

Short version: pick this one when you want **a cheap, auditable, no-moving-parts way to hand work
to DSH**; pick a daemon-shaped bridge when you want **long-lived agents that wake the host by
themselves**.

- **Default preset:** DeepSeek-V4.1-Flash (id `deepseek-flash`) at reasoning effort `max`.
- **Nothing to configure:** the first call creates its own `dsh-subagent` profile from the shipped
  headless template.

## Install

Requires Node.js ≥ 22.19. The bundled dependency provides the `dsh` CLI.

**A · straight from GitHub** — no clone, no build, no npm account:

```toml
# ~/.codex/config.toml
[mcp_servers.dsh_subagent]
command = 'npx'
args = ['-y', 'github:mbj733/dsh-subagent-mcp']
startup_timeout_sec = 60
```

**B · pinned tarball**

```bash
npm i -g https://github.com/mbj733/dsh-subagent-mcp/releases/download/v0.5.0/mbj733-dsh-subagent-mcp-0.5.0.tgz
```

**C · clone** (when you want to read or patch the code)

```bash
git clone https://github.com/mbj733/dsh-subagent-mcp
cd dsh-subagent-mcp && npm install
```

```toml
[mcp_servers.dsh_subagent]
command = 'node'
args = ['/absolute/path/to/dsh-subagent-mcp/index.mjs']
startup_timeout_sec = 30
```

Restart the host afterwards. Then just ask:

> Delegate to DSH: summarise `src/` and list the files that look unused. Work in `/path/to/project`.

## Tools

| Tool | Overlay | What it does |
|---|---|---|
| `subagent_dsh` | lite | Delegate a task: skills, shell, file editing, web search — no MCP tools. ≈9k input tokens. |
| `subagent_dsh_full` | full | Same, plus every MCP server declared in `overlay.yml`. ≈300k input tokens with a dozen servers. |
| `subagent_dsh_status` | — | Collect a task. Accepts a task id (`t-…`) **or** a dsh session id. |
| `subagent_dsh_continue` | — | Follow-up in the same dsh session; `cwd`/`overlay` default to the remembered values. |
| `subagent_dsh_cancel` | — | Kill a running or queued task and its whole process tree. |

| Argument | Default | Meaning |
|---|---|---|
| `prompt` | — | Required. The child cannot see the host conversation — include everything it needs. |
| `cwd` | — | Required. Absolute project directory. **Not a sandbox** (see Security). |
| `wait_sec` | 120 | Wait before returning a task id. `0` returns immediately. |
| `timeout_sec` | 1800 | Hard limit; on expiry the tree is killed and partial output returned. |
| `read_only` | false | Ask the child to make no filesystem changes (instruction + detection, not a kernel sandbox). |
| `format` | text | `json` returns `{ok, state, answer, usage, meta}`. |

Long tasks are safe by design: the call waits `wait_sec`, then returns a task id, so a host-side
tool timeout can never kill the work.

## Model and cost

The child runs **DeepSeek-V4.1-Flash** — model id `deepseek-flash` — at reasoning effort **max**.
That is what both overlays ship; verify it yourself:

```bash
dsh --profile dsh-subagent --patch overlay.lite.yml --dump-config | grep -A4 agent-default-model
```

The account line exposes two ids (checked against `GET https://api.deepseek.com/models`):

| id | displayed as |
|---|---|
| `deepseek-flash` | DeepSeek-V4.1-Flash · **default** |
| `deepseek-v4-pro` | DeepSeek-V4-Pro |

Switch per machine without editing files: `export DSH_SUBAGENT_MODEL=deepseek-v4-pro`.

> Pin the **id**, not the label. Provider-specific ids from other lines — for example opencode's
> `deepseek-v4.1-flash` — are not valid here; they need that provider's key and `base_url` on the
> `dsh-subagent` profile.

| Overlay | Mounts | Typical input tokens | Use it for |
|---|---|---|---|
| `lite` | credentials + skills | **~9,000** | writing, refactoring, analysis — anything done with shell and files |
| `full` | + 11 MCP servers | **~300,000** | tasks that genuinely need PDF/office/browser/GitHub/map tools |

A short `lite` reply, measured end to end: `startup 1.67s · work 1.19s · tokens in 9073 / out 2`.

## Verify an install without a host

```bash
printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | npx -y github:mbj733/dsh-subagent-mcp
```

Expect `serverInfo.name = dsh-subagent-mcp` and exactly five tools.

**Installing this for someone else, or as an agent?** Read [`llms.txt`](llms.txt) for the
machine-readable guide and [`AGENTS.md`](AGENTS.md) for registration rules and repository
conventions. Generic client config:

```json
{"mcpServers":{"dsh_subagent":{"command":"npx","args":["-y","github:mbj733/dsh-subagent-mcp"]}}}
```

## Optional: report finished tasks back into the host

MCP has no channel for a late result, so long tasks return a task id. On hosts with hooks,
`notify-hook.mjs` injects any finished task into the next turn.

```toml
# Codex. `type = "command"` is mandatory — a missing type breaks the whole config file.
[[hooks.PostToolUse]]

[[hooks.PostToolUse.hooks]]
type = "command"
command = "'node' '/absolute/path/to/dsh-subagent-mcp/notify-hook.mjs' PostToolUse"
timeout = 5
```

It reads `state/tasks.json`, reports each finished task exactly once, and stays silent otherwise.

## How it behaves

- **Stateless per call** — a new process and exactly one turn; nothing to supervise, nothing to
  leak between tasks.
- **Overlay, not profile surgery** — configuration arrives as `dsh --patch` layers, so your
  existing profiles are never rewritten.
- **Bounded concurrency** — `DSH_SUBAGENT_MAX_CONCURRENCY` (default 2) with a FIFO queue; extra
  calls return `queued` plus their position.
- **Usage reporting** — tokens, tool calls and names, `startup_ms` / `work_ms`, `exit_code`,
  `error_code` (`TIMEOUT`, `CANCELLED`, `SPAWN_FAILED`, `EMPTY_ANSWER`, `CHILD_EXIT`, `RATE_LIMIT`).
- **Oversized answers** — above `DSH_SUBAGENT_INLINE_LIMIT` (default 24000 chars) the full text
  spills to `state/results/<task>.md`; the reply states the percentage shown.
- **No orphans** — cancellation and shutdown kill the whole process tree.

## Security

- **`cwd` is not a sandbox.** It selects the working directory only; the child inherits the host
  user's filesystem and network rights.
- **`read_only` is advisory.** Enforced by instruction and reported by detection, not by the kernel.
- The child inherits `DSH_HOME`, so it reads your `AGENTS.md`, skills and stored credentials.
- Nothing is sent anywhere beyond the model provider you configured.

## Differences and limits

Deliberately stateless: a process per task, one turn, then exit. That keeps the failure surface
small and the billing predictable, and it means there is

- no daemon and no queue beyond the in-process FIFO;
- no native "wake the parent when done" callback — that needs host hooks, above;
- no way to attach to a DSH session already running elsewhere;
- no streaming and no mid-run questions — use `subagent_dsh_continue` for follow-ups;
- no visibility from the host into the child's conversation; only the final answer crosses back.

Several host windows each run their own server instance and share `state/tasks.json`, so hook
notifications can duplicate.

## Built on

[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness). This is the reverse of
[`@deepseek-ai/dsh-subagent-codex`](https://www.npmjs.com/package/@deepseek-ai/dsh-subagent-codex),
which lets DSH drive a Codex thread.

## Author

Built and maintained by [@mbj733](https://github.com/mbj733). MIT licensed.