dsh-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@dsh-mcpstart a session in my repo and fix the failing tests in the background"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
π³ DeepSeek-DSH-MCP
English | δΈζ
Give Claude Code / Codex a coding buddy that sticks around β powered by DeepSeek Harness.
An MCP server that drives DSH as a long-lived agent runtime instead of wrapping a CLI. Open a conversation, hand it a goal, and it writes code, runs scripts and spawns its own subagents β while you watch, cut in, and collect the result whenever you like. π οΈ
π§ͺ 504 checks green Β· π MCP over stdio Β· π MIT Β· π¬ Community: linux.do
π Read the Usage Notes first β the practical gotchas that will actually bite you: the ACP model-config trap, external MCP tools bypassing DSH's sandbox, the workspace/GUI registry cache, cost control, and a troubleshooting table.
π Contents
π Dispatch then collect Β· π Completion notifications Β· π§΅ Many conversations
π Permissions Β· π€« Reasoning Β· πΆοΈ Logging and privacy
βοΈ Environment Β· π§ͺ Tests Β· β οΈ Known limitations
Claude / Codex ββMCP(stdio)βββΆ dsh-mcp
β newline-delimited JSON-RPC (ACP)
βΌ
dsh --profile dsh-mcp β one long-lived process per conversation
β
ββ DSH's own tools (files, shell, search, skills, subagents, workflows)
ββ dsh-mcp-client ββΆ your other MCP servers (e.g. IDA Pro)Related MCP server: hermes-dsh-bridge
β¨ Why it's different
π A dead process never kills a conversation | The identity lives on disk. A crash, an idle reap, even restarting this whole server costs you nothing β the next call transparently resumes it, memory intact (verified across processes). |
π£οΈ You are never stuck waiting | Steer a conversation mid-flight β interject to stop and redirect (~18 ms to converge), or queue a remark for after the current turn. Interrupting never damages the conversation. |
π‘ Completion notification, not polling | Long tasks return instantly with a |
πͺ Many conversations at once |
|
π§ Reasoning hidden by default | You get "thought for N chars / M seconds" statistics instead of context-burning prose β and it is never logged, never written to disk. |
π€« Silent by default | Not one byte on stderr, so nothing pollutes your host's logs. stdout carries the protocol and nothing else. |
π§© Capabilities compose | DSH can mount its own MCP servers (IDA Pro, browsers, your internal tooling), so this is a bridge to a whole toolbox. |
π‘οΈ Read-only that really is read-only |
|
β‘ Why not just wrap the DSH CLI?
DSH ships two programmable entry points. The sdk profile looks simpler, but its JSON-RPC surface has only 3 requests and 4 notifications β and it lacks three things that make an agent unusable in practice. The acp profile (Agent Client Protocol) has all of them. Everything below was verified against DSH 0.1.7-rc.2.
Capability |
|
|
Resume a conversation | β no resume method; reusing an id fails with | β
|
Interrupt a running turn | β only by killing the process | β
|
Answer approval prompts | β no channel; operations silently fail closed | β
|
List / close sessions | β none | β
|
Gray vs. black streaming | one message, split by a | β
two separate channels: |
Per-session MCP servers | β process-wide | β
|
Core idea: the process is a cache, not the identity. A conversation's identity is DSH's sessionId, persisted under ~/.dsh/sessions. If the process is reaped, crashes, or the whole MCP server restarts, the next call transparently session/resumes it β history intact.
π¦ Requirements
Node.js β₯ 20
DSH installed (
@deepseek-ai/dsh), with working credentials under~/.dshA DSH profile named
dsh-mcpderived from the shippedacptemplate, containing your own provider/model config (this repo deliberately ships no provider config)
π Setup
1. Create the DSH profile
dsh dsh-mcp --from-default-profile acpThen edit ~/.dsh/profiles/dsh-mcp/cordis.patch.yml and declare your provider and model. Start from profile-example/cordis.patch.yml.
β οΈ Two traps here (both learned the hard way):
ACP's model does NOT come from
agent-default-model. It comes from thedsh-acpplugin's ownconfig.provider/config.model, which theacpbundle hard-codes todeepseek-official. If you don't override theacprow, your sessions silently run on the wrong provider and fail withno API key for provider route "deepseek-official".ACP's
reasoning_effortdefaults to empty (= "Provider default"), not to the maximum. This server explicitly sets it tomaxon everysession/newand after everysession/resumeβ because a fresh process does not remember the previous choice.
2. Register the server
Claude Code
claude mcp add dsh-mcp -- node /absolute/path/to/DeepSeek-DSH-MCP/bin/dsh-mcp.mjsCodex (~/.codex/config.toml)
[mcp_servers.dsh-mcp]
command = "node"
args = ["/absolute/path/to/DeepSeek-DSH-MCP/bin/dsh-mcp.mjs"]Generic MCP client
{
"mcpServers": {
"dsh-mcp": {
"command": "node",
"args": ["/absolute/path/to/DeepSeek-DSH-MCP/bin/dsh-mcp.mjs"]
}
}
}3. Try it
node test/smoke.mjs # no LLM calls, verifies the whole plumbingπ§° Tools
Tool | What it does |
| Create a conversation: workspace, permission tier, approval policy, reasoning effort (default |
| Hand it a task. Defaults to |
| All conversations with live state: |
| Cursor-based incremental read of what a running conversation is producing right now |
| Conversation details; with |
| Interject while it's busy: |
| Preempt the write lock. The only way out when another DSH process holds it (DSH has no steal API β preempting means killing the holder). It classifies the holder first: our own orphaned child β taken automatically; another live instance β needs |
| Stop the current turn (the conversation stays healthy) |
| Hand the conversation back (frees the write lock so you can open it in your own DSH GUI). Still resumable |
| Adjudicate a pending approval (only when |
| Lightweight status check |
π Workflow: dispatch, then collect later
MCP tool calls block, so dsh_send does not block by default (wait=false): you get a receipt immediately, the turn runs in the background, and the sentinel file wakes you when it finishes. The whole flow:
1. dsh_send(conversation_id, prompt, wait=false)
β { run_id: "run-abc123", background: true } # returns in ~1 ms
2. β¦keep working on something else (verified: another conversation ran a full turn meanwhile)β¦
3. dsh_status(conversation_id)
β state=running | elapsed=3.5s | current_tool=pwsh | out=37 chars
4. dsh_read(conversation_id, cursor) # peek mid-flight
β [me] β¦ | [turn] turn 1 started | [tool] pwsh [in_progress]
5. dsh_get(conversation_id, run_id) # collect the result
β status=done, elapsed=20.3s, full answerrun_id is the receipt. Run records are memory-only, last 20 kept; under many concurrent runs old ones get evicted and dsh_get will fail β which is exactly why the completion sentinel below carries the result.
π Workflow: get notified on completion (no polling)
Besides run_id, wait=false returns a sentinel_file path plus sentinel_file_posix (the Bash/MSYS form β use it directly; hand-converting Windows paths is the easiest way to make your waiter hang until timeout). When the turn ends β whether it succeeds, fails, or is cancelled β the service atomically writes a small file there containing the final status and result. So you neither poll nor block: wait for that file with a background task in your own host (Claude/Codex); the moment it appears, you're woken up.
# sent="$sentinel_file_posix" β copy it straight from the tool result
sent='/d/AI_MCP/DSH_MCP/.state/runs/<conversation_id>/<run_id>.json'
dl=$(( $(date +%s) + 2100 )) # 35min safety net so a dead service can't hang you forever
until [ -f "$sent" ]; do
[ "$(date +%s)" -ge "$dl" ] && { echo "TIMEOUT"; exit 1; }
sleep 2
done
echo "DONE" # task exits β host wakes you β read the file to collectOnce the file exists: read it for status and result (preferred β immune to the in-memory window and survives restarts), or use dsh_get(conversation_id, run_id).
Design notes (especially under concurrent multi-session use):
No cross-talk: the path is
.state/runs/<conversation_id>/<run_id>.json, namespaced by conversation.run_idsequence numbers are counted per-conversation, so two conversations can mint the samerun_idin the same millisecond β the per-conversation directory keeps them apart. Arm one background task per run; each completes and wakes you independently.Latch semantics: the file is written once and kept. Even if the run finishes before you arm the waiter,
[ -f ]is immediately true β you never miss it.Atomic write:
tmp+rename; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.No server push: the service only answers requests over MCP (it sends no notifications). "Completion notification" is entirely the sentinel file plus your background waiter.
β No reasoning in the sentinel: the payload carries the answer and thinking statistics only β
result.thinkingis stripped and replaced bythinking_omitted: true, because the project's rule is that reasoning never reaches disk.dsh_get(run_id)still returns it from memory while the record lives.DSH_MCP_SENTINEL_INCLUDE_REASONING=1breaks that guarantee deliberately.
Cleanup: sentinels contain full answers, so the service prunes sentinels older than 7 days at startup (tune with
DSH_MCP_SENTINEL_TTL_MS;0disables). Your waiter should stillrmthe file after consuming it.
π§΅ Workflow: many conversations at once
1. dsh_list(only_running=true)
- β¦ | id=448b239bβ¦ | π΅ running (9s, tool=pwsh, 40 chars this turn) | cwd=β¦
- β¦ | id=33427096β¦ | π΅ running (8s, 34 chars this turn) | cwd=β¦
2. dsh_read(id, cursor=0) # read the first, note the cursor
3. dsh_read(id2, cursor=0) # switch to the second
4. dsh_read(id, cursor=<prev>) # switch back, deltas onlyπ€« Reasoning: hidden by default
DSH separates reasoning from answer text at the model layer, and ACP exposes them as two independent streams. This server hides reasoning by default and returns only statistics (characters, duration, chunk count) β so the caller knows thinking happened without paying for it in context.
reasoning=hide(default) β content dropped, stats returnedreasoning=marker/summary/fullβ progressively more content (mind your context budget)dsh_read(include_reasoning=true)β live reasoning of the currently running turn only; it is never buffered and is discarded when the turn ends
π Permissions
Tier | Approval default | Use for |
|
| Zero friction |
|
| Out-of-workspace operations surface as pending approvals |
|
| Analyzing untrusted samples (malware, unknown dumps) |
on_approval can be auto-allow, auto-deny, or ask (which exposes a pending approval for dsh_approval_decide to adjudicate).
πͺ Workspace visibility in the DSH GUI
Two different things β don't confuse them:
Depends on | Status | |
Workspace effectiveness | the conversation's | β Always correct. Process working directory, file placement and sandbox write root all follow it |
GUI grouping |
| β οΈ This server registers it, but the running DSH server caches the registry in memory β external file edits only take effect after a DSH restart |
Verified experimentally (test/workspace-effect.mjs): two conversations in two desktop folders, given no path at all, each wrote its file into its own workspace, self-reported the correct working directory, and leaked nothing into the server's cwd / home / temp / the other workspace (24/24 checks).
Maintenance CLI:
node bin/dsh-mcp-workspaces.mjs --list # inspect the registry
node bin/dsh-mcp-workspaces.mjs --backfill # register every session found on disk
node bin/dsh-mcp-workspaces.mjs --prune-temp # drop temp-directory entries
node bin/dsh-mcp-workspaces.mjs --prune-empty # drop empty workspaces whose path is gone
node bin/dsh-mcp-workspaces.mjs --purge-test-sessions # delete test sessions (strict rules)β οΈ Before restarting DSH, don't touch workspaces in the GUI β the server will write its in-memory state back over the file.
πΆοΈ Logging and privacy
Silent by default β not one byte is written.
MCP's stdout is the protocol channel; anything mixed in corrupts the stream.
This server is launched by Claude/Codex, so its stderr lands in their logs.
DSH's own stderr may carry model output including reasoning, so it is never forwarded by default.
Level | Behavior |
| nothing at all |
| this server's own lifecycle events only β never any DSH output |
| more of our own events; still no DSH output |
| additionally forward DSH's raw stderr (may contain reasoning) |
Reasoning never reaches disk β with exactly one deliberate exception:
Not in logs (silent by default), and the state file holds metadata only (id, cwd, permissions, counters) β a smoke-test assertion guards this.
The async completion sentinel does persist the answer (it must, so a caller can collect offline) but strips
result.thinkingand marksthinking_omitted: true. A test asserts that even whenreasoning=fullis requested, no reasoning text appears in the sentinel β whiledsh_get(run_id)still returns it from memory. SetDSH_MCP_SENTINEL_INCLUDE_REASONING=1to break this on purpose.Sentinels are pruned after 7 days by default (
DSH_MCP_SENTINEL_TTL_MS;0keeps them forever).
(DSH itself writes its own session log under ~/.dsh/sessions; that is what makes resume possible and is outside this server's control.)
βοΈ Environment variables
Variable | Default | Purpose |
| auto-detected | Path to |
|
| DSH profile to drive |
|
| Conversation registry, survives restarts |
|
| Root for async completion sentinels ( |
|
| Holder registrations for the write lock. β οΈ All instances must agree on this value, or they cannot see each other's registrations and will misread each other as an unidentifiable holder (a GUI) and refuse to preempt |
|
| How long without a heartbeat before a holder counts as lost (and becomes auto-preemptible by |
|
| How long a conversation may sit idle before its DSH process is reaped. β
β
|
|
| β
How many conversation processes to keep alive at once ( |
|
| Cache lifetime for |
|
| Startup pruning age for sentinels; |
| unset |
|
|
| Default tier for |
|
| Default reasoning effort |
|
| Wait bound for |
|
| How long a pending approval waits |
|
|
|
|
|
|
| unset |
|
π§ͺ Tests
node test/run.mjs # smoke only (no LLM calls)
node test/run.mjs --all # everything
node test/cleanup.mjs # suite teardown on its own (temp dirs only; --dry-run to preview)The suite cleans up after itself: run.mjs always ends with cleanup.mjs, which removes test sessions and workspace registrations inside the OS temp directory only. Real project sessions and the repo's .state/ (your live conversation registry and sentinels) are never touched. To also remove Desktop\dsh-mcp-test-* artifacts, run node bin/dsh-mcp-workspaces.mjs --purge-test-sessions explicitly.
Suite | Checks | Covers |
| 28 | handshake, tool table, conversation creation, config, registry purity |
| 42 | protocol edges (double initialize, malformed lines, unknown method), argument validation, unknown ids, cursor edges, lifecycle idempotence, unicode |
| 30 | resume-with-memory across processes, interrupt, both interject modes |
| 16 | fire-and-forget + later collection |
| 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and reasoning never reaching the file |
| 11 | sentinel retention: prunes over-age and crash-leftover files, keeps fresh ones, removes empty shells, |
| 31 | environment-variable guards: odd numbers always land on the safe side β |
| 38 | timeout semantics: |
| 79 | write lock and preemption: four holder classifications, |
| 36 | the async dispatch lifecycle: dispatch β collect the sentinel β idle reap β dispatch again, three rounds with no lock error; re-dispatch right on the reap boundary (widening the race); an immediate re-dispatch after |
| 31 | three simultaneous conversations + live incremental reads |
| 22 | writing code, running scripts, spawning its own subagents (verified on disk via child session headers) |
| 24 | workspace actually effective when no path is given |
| 36 | two folders Γ two conversations doing a read-only IDA Pro analysis |
| multi | 19 | several instances and non-ASCII paths: reproduces "two instances share the registry and the later save drops the earlier instance's conversation", then proves that conversation is recoverable by id alone from the session store and actually usable; recovery never silently escalates privileges (a read-only conversation stays read-only); no .tmp residue; and CJK / emoji workspace paths create sessions, do real work, and place files in the right directory |
| permission | 11 | β
whether the permission tiers actually take effect (a safety property): it ignores our own return values (the very thing that used to lie) and reads the session's own record (permissions.preset / sandboxMode in the projection cache), checking all three tiers and that their recorded values differ. This suite caught a silent privilege escalation: a defaultPreset in the profile overrides DSH_PERMISSION_MODE at session creation |
| list | 13 | the cost of dsh_list (measured): a default call with the on-disk probe takes ~1 s (it spawns a DSH process) while repeated calls hit a cache and drop to single-digit milliseconds; only_running=true and include_closed=false skip the probe with equivalent semantics (unopened on-disk sessions are simply excluded) |
Total: 504 checks, all green. (468 in-suite + 36 acceptance)
π The write lock: one writer per conversation at a time
DSH conversations carry a cross-process write lock: it never expires while the holder lives, and no API can take it from a live holder β so whoever gets there first decides everything. This server turns that into "you can look, we can write":
Situation | Outcome |
This server holds the lock (the default) | β
Opening it in the GUI is read-only: you still see the content, but you cannot take the lock. The default |
You open it in the GUI while the lock is free | β The GUI keeps it permanently (navigating away, waiting and archiving all fail to release it) and this server can never reconnect β only restarting |
Recommended | Watch progress with |
Who holds it is reported by dsh_status.lock_holder; dsh_takeover handles the four cases:
| Meaning | Action |
| this process holds it | nothing to preempt |
| the holder is alive but silent (no heartbeat for 90 s β typically that MCP is wedged) | β
|
| another live dsh-mcp instance is using it | β οΈ refused by default; |
| no registration β most likely your own DSH GUI | β never killed (GUI conversations live inside the single |
Two measured footnotes. β A crash leaves no orphan lock β
SIGKILLthe MCP and its children exit with it (their stdio pipe closes), releasing the lock automatically β; sostale-mcpreally means "alive but wedged". β‘ Several instances: the registry is last-writer-wins (a later save drops conversations it never saw), but the session store is authoritative β any session on disk is recoverable by id alone; if you give instances differentDSH_MCP_STATEfiles, pointDSH_MCP_LOCKS_DIRat one shared directory, or they cannot see each other's holder registrations.The full measurements (the A/B probe method, the evidence that the GUI opens read-only, why even archiving does not release the lock, and crash-vs-wedge) are in Usage Notes Β§3.5β3.12.
β οΈ Known limitations
No token-level streaming into model context β an MCP limitation, not DSH's. Callers get per-step results; humans can follow progress via stderr logs.
True mid-turn steering is impossible β ACP rejects concurrent prompts (
a prompt is already in flight for this session).dsh_interjectis the practical equivalent: cancel, then immediately start a new turn, history preserved.Image prompts are unsupported β ACP advertises
promptCapabilities: {image: false}.Restarting the MCP server takes its children with it β when this service is restarted or killed, the DSH children it spawned exit too (their stdio is a pipe to us): in-flight turns are interrupted and no sentinel lands (judge by inspecting the workspace, don't just wait for the file). The good news: the write lock is released automatically, so a restart always gets it back. The service also pushes no MCP notifications; "completion notification" is the sentinel file plus the caller's background waiter.
A conversation that dies before its first successful turn may never have materialized on disk β resume then fails with a clear error. Safe after the first message.
No renaming β DSH's title subsystem has no external rename API (
SessionTitleService.renamerequires a live in-process session). Titles are auto-generated from the first message.session/listreturns only{sessionId, cwd}and excludes already-open sessions β titles are filled in by this server from DSH's projection cache."Never reap" costs memory (measured ~120 MB per conversation) β that is what buys "the GUI can never take the write lock". Two ways to spend less: β
DSH_MCP_MAX_LIVE(default 8 β over the cap, the least recently used process is reaped β bounded memory, locks still held where it matters); β‘ setDSH_MCP_IDLE_TTL_MSto a millisecond value (at the cost of the lock being takeable). To drop to zero right after a batch of work:dsh_release(all=true)β (lossless, everything resumes on demand).
π¬ Community
This project is announced and discussed on linux.do β usage questions, war stories and suggestions are all welcome there. Issues work too, but you'll usually get a faster answer in the community.
π License
MIT β see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Persistent memory and cross-session learning for AI coding assistants (hosted remote MCP).
- mcpOAuthco.aistoryhub
Remote MCP server for AIStoryHub: stories, chapters, story bible, Voiceprints, AI generation.
Nifty's MCP server β exposes tasks, projects, messages, and files as tools for AI agents.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceAn experimental MCP gateway for controlling durable DeepSeek Harness agent sessions from MCP clients, enabling session creation, observation, steering, and resumption across chat sessions.3MIT
- AlicenseNot gradedqualityAmaintenanceEnables external MCP clients to drive DeepSeek Harness agents for real coding tasks, providing tools for task execution and queueing, session management, sandboxed file access, preset switching, and usage statistics.464 npm4GPL 3.0
- AlicenseNot gradedqualityBmaintenanceLets any MCP-capable coding harness (Cursor, Claude Code, Codex, Gemini CLI, VS Code/Copilot, opencode, and others) hand a self-contained task to a real, isolated DeepSeek Harness process that works in a chosen workspace with its own context window, model, and toolchain, then returns the final answer. Exposes task submission with time estimates and acceptance criteria, status polling with live progress and process-tree telemetry, graceful cancel and forced kill of whole process trees, and a health probe reporting concurrency, deadlines, and recent jobs.MIT
- AlicenseAqualityBmaintenanceEnables MCP clients like Codex to delegate coding tasks to DeepSeek Harness, reusing the same Web-visible session for feedback and keeping the conversation in the Harness Web UI.101MIT