← Back to home@mbj733

dsh-subagent-mcp

Give Codex a DeepSeek crew: one MCP call hands a task to a fresh DSH agent that already has your skills, AGENTS.md and sign-in. V4.1-Flash at max reasoning, ~9k tokens per delegation, in the project directory you name.

Stars
0
Language
JavaScript
Created
Oct 17, 2026
Updated
Oct 19, 2026

Introduction

MCP server Node 22.19+ MIT stars

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 getWhy it matters
≈9k tokens per delegationA lite run costs about one file read, not a second conversation.
V4.1-Flash at reasoning maxThe strong preset is the default. Nothing to tune.
Your skills and AGENTS.mdIt behaves like the DSH you configured, because it uses your DSH_HOME.
Long tasks can't be killed by a timeoutIt 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 turnNo daemon, no state to leak between tasks, no edits to your DSH profile.
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:

DimensionThis packageTypical alternativeWhy it matters
Runtime dependenciesZero — only dsh itselfws + @modelcontextprotocol/sdk + zod, or a chain of SDK packagesA ~600-line single file is auditable in one sitting and cannot break because a transitive dep changed
Your DSH profileNever touched — config arrives as --patch overlaysWritten into the profile, or requires a plugin on the DSH sideUpgrading DSH can't collide with a bridge that wrote itself into your config
Per-call costTwo explicit tiers: ≈9k or ≈300k tokensOne implicit size you discover on the billYou choose the cost before the run
LifetimeOne process per task, then exitResident daemon / worker poolNothing runs in the background; a crash can't poison the next task
Orphan processesProcess-tree kill on cancel and shutdownSometimes leaves the MCP children dsh spawnedNo slow memory drain after a few cancelled runs
Config inheritanceReal DSH_HOME → your skills, AGENTS.md, sign-inSome runtimes get an isolated temporary DSH_HOME (an empty shell)The child actually behaves like the DSH you configured
Installnpx -y github:…, no registry accountnpm publish onlyOne config block, works today
Cold start≈1.7 s per delegation ⚠️Warm daemon is fasterThe honest price of statelessness
Native callback / session attachNot offered — needs host hooks ⚠️Daemon bridges call back natively and can attach to running sessionsIf 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:

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

B · pinned tarball

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)

git clone https://github.com/mbj733/dsh-subagent-mcp
cd dsh-subagent-mcp && npm install
[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

ToolOverlayWhat it does
subagent_dshliteDelegate a task: skills, shell, file editing, web search — no MCP tools. ≈9k input tokens.
subagent_dsh_fullfullSame, 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.
ArgumentDefaultMeaning
prompt—Required. The child cannot see the host conversation — include everything it needs.
cwd—Required. Absolute project directory. Not a sandbox (see Security).
wait_sec120Wait before returning a task id. 0 returns immediately.
timeout_sec1800Hard limit; on expiry the tree is killed and partial output returned.
read_onlyfalseAsk the child to make no filesystem changes (instruction + detection, not a kernel sandbox).
formattextjson 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:

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):

iddisplayed as
deepseek-flashDeepSeek-V4.1-Flash · default
deepseek-v4-proDeepSeek-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.

OverlayMountsTypical input tokensUse it for
litecredentials + skills~9,000writing, refactoring, analysis — anything done with shell and files
full+ 11 MCP servers~300,000tasks 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

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 for the machine-readable guide and AGENTS.md for registration rules and repository conventions. Generic client config:

{"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.

# 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. This is the reverse of @deepseek-ai/dsh-subagent-codex, which lets DSH drive a Codex thread.

Author

Built and maintained by @mbj733. MIT licensed.

Troubleshooting

failed to parse overlay … YAMLException: end of the stream or a document separator is expected

The dsh-subagent profile patch is broken. That file starts with a [] placeholder, so any block appended after it makes the whole file invalid YAML — typically a persona/plugin block copied in from another profile. The bridge detects this on every run, keeps a backup and resets the file automatically, so a task still succeeds (the footer then says profile-patch auto-reset). To do it by hand:

node repair-profile.mjs          # backs the file up, then writes the clean template

unknown prompt variable "{{cwd}}"

The profile patch sets personaSuffix to text containing {{cwd}}, which the unattended profiles do not register (only provider and model exist there). Either drop the personaSuffix line or keep the persona in personaPrefix, which takes no variables.

unknown task id after restarting the host

Task ids live in the server process. After a restart, continue by dsh session id instead: subagent_dsh_continue(session_id="session-…", prompt="…").