dsh-ops-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-ops-mcprun a task to analyze the codebase and report findings"
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.
DSH Carrot on a Stick
Operate DeepSeek Harness (dsh) over MCP: run an MCP server inside the dsh process and expose session execution, task queues, and workspace management to any MCP client.
π English (this page) Β· δΈζ
What it does
dsh ships a complete agent runtime β model routing, tool sandbox, presets, persistent sessions β but it is a Cordis application that external programs cannot call directly. dsh-carrot-on-a-stick turns it inside out: the plugin starts an MCP server (StreamableHTTP) inside the dsh process and bridges the live harness through ctx.agents / ctx.agentPresets / ctx.tools.
Your MCP client is the commander; dsh is the executor. Works with Claude Code, Codex CLI, Cursor, another dsh instance (via the official dsh-mcp-client), or any automation script.
MCP client (Claude Code / Codex / another dsh / β¦)
β agent_run / task_* / session_* / model_list / select_model (HTTP + Bearer)
βΌ
dsh-carrot-on-a-stick (MCP server, 127.0.0.1:8090)
β ctx.agents.create β mount preset
βΌ
dsh agent β full toolset: bash, fs, todo, webβ¦agent_run lifecycle (progress + cancellation are spec-native):
sequenceDiagram
participant C as MCP Client
participant S as dsh-carrot-on-a-stick
participant A as dsh Agent
C->>S: tools/call agent_run (task, cwd, _meta.progressToken)
S->>S: lock (cwd/session) β pool hit or create
S->>A: followup(userMessage)
S-->>C: notifications/progress (started, events=0)
loop every progressIntervalMs
S-->>C: notifications/progress (events N)
end
Note over C,A: cancel (notifications/cancelled) β official agent.cancel({kind:user})
A-->>S: turn/end (completed | error | cancelled)
S-->>C: CallToolResult (isError?, sessionId, changes/verification/leftoversβ¦)Related MCP server: dsh-harness-mcp-server
Tools
Tool | Purpose |
| connectivity check |
| list the host-global tool registry (name + description; model tools are preset-scoped, usually empty) |
| list currently routable providers, model ids, reasoning efforts, and the default selection (look here before picking a model) |
| run a task synchronously, structured result; |
| push a structured task (task + context + cwd + model) into the async queue, returns |
| fetch a queued task's result; |
| list queued/running/finished tasks (queue observability) |
| cancel a queued or running task (a running one is cancelled through the host's official |
| list known sessions (live + persisted, newest first) to pick |
| read a live session's transcript summary (user/assistant/tool turns, newest first, truncated) |
| switch the model of an existing session (official |
| attach a session to the workspace of its cwd |
| rename an existing session |
Model selection
Priority: per-call arguments > plugin config (provider+model) > host default selection (ctx.agentDefaultModel.currentSelection(), the same source the Web UI uses when creating a session). Supplying only one half completes it from the lower-priority source; if the pair still cannot be resolved the call fails loudly instead of running with an empty model (an empty model leaves the persona's {{model}} variable unset and fails the whole turn at assembly time).
reasoningEffort follows the model's source: call argument > plugin config > the host default selection's own effort. But when the caller or the plugin config pins provider+model explicitly, the host's effort (chosen for some other model) is not inherited β the host rejects unsupported explicit efforts instead of clamping or aliasing, so inheriting one would break a deployment that used to work.
Three ways to change models:
Goal | How |
Run this one task on another model | pass |
Switch models mid-conversation, keeping history |
|
Pin one model for the whole deployment | set |
Two source semantics worth knowing:
The resident session pool is keyed by
cwd + model triple. Task 1 on model A and task 2 on model B in the same directory are two separate sessions (no shared context), so which model a session uses is always predictable; to switch models inside one session, useselect_model.Results now carry
model: {provider, model, reasoningEffort?}so the caller never has to guess who answered.
model_list's source names the catalog's origin: sessionController = the official view (same data source as the Web UI's model selector; includes default, routableProviders, per-provider load failures, and reasoning efforts); llm = fallback (llm.listProviders() + per-provider listModels(), for deployments without sessionController, e.g. headless, and without reasoning metadata). It also reports the plugin's own model config and allowModelOverride, so a caller can see at a glance whether it may choose.
Result detail levels (token budget) β the whole point of this plugin is saving the caller's (operator's) context: execution details stay inside dsh, and read-back is projected by detail:
summary(default, a few hundred tokens): thechanges/verification/leftoversthree-line summary + the answer tail (the summary JSON sits at the end) + tool-name list +errornormal(~2k tokens): the above + truncated tool-call arguments and resultsfull(up to tens of thousands of tokens, for debugging): the full texttask_resultalso has astatuslevel: polling returns only{taskId, status, error?}β fetch the summary once after completion instead of re-injecting the payload on every poll
When continuing the same sessionId, the executor already remembers prior turns β send only the delta in context.
Every result is structured: sessionId / model / assistantText / toolCalls / toolResults / changes / verification / leftovers β ready to be persisted by the caller.
Sessions are reused per cwd + model (LRU, default 8) to avoid reloading project context on every call.
Typical workflows
1. One-off task, synchronous β get a structured summary back:
{ "name": "agent_run", "arguments": { "task": "fix the failing test in src/auth", "cwd": "/workspace/app" } }Continue it later with "sessionId": "<from the result>" β send only the delta in context.
2. Fire-and-forget queue β submit, poll cheaply, cancel if needed:
{ "name": "task_inbox", "arguments": { "task": "β¦", "cwd": "/workspace/app", "provider": "deepseek-official", "model": "β¦" } }
β { "taskId": "β¦" }
{ "name": "task_result", "arguments": { "taskId": "β¦", "detail": "status" } } // poll: no payload re-injection
{ "name": "task_cancel", "arguments": { "taskId": "β¦" } } // optional
{ "name": "task_result", "arguments": { "taskId": "β¦" } } // fetch the summary once donetask_list shows everything queued/running/finished at a glance.
3. Discover and steer sessions β find the right session, check its model, read what happened:
{ "name": "session_list", "arguments": { "limit": 10 } } // sessionId / title / cwd / model
{ "name": "session_history", "arguments": { "sessionId": "β¦" } } // recent turns, truncated
{ "name": "select_model", "arguments": { "sessionId": "β¦", "provider": "β¦", "model": "β¦" } }4. Long synchronous runs β agent_run supports MCP-native progress and cancellation: clients
that pass _meta.progressToken receive notifications/progress heartbeats, and any client can
cancel via the standard notifications/cancelled (both wired to the host agent's official
cancel). No custom polling protocol required.
Install & run
Requires the dsh host to run on Node.js >= 18 (the plugin declares engines accordingly).
The plugin must be installed into a dsh profile directory (the loader resolves plugin names from there; --patch alone from a repo checkout will not find the local package β see finding 1 in the E2E report):
git clone https://github.com/Leawind/dsh-carrot-on-a-stick.git
cd dsh-carrot-on-a-stick
npm install && npm run build
npm pack # produces dsh-carrot-on-a-stick-<ver>.tgz
# install into the profile (Windows note: use the tarball; pnpm mangles file:D:/... specifiers)
pnpm -C ~/.dsh/profiles/<profile> add -w <path-to-tarball>
export DEEPSEEK_API_KEY=... # model credentials (or use what ~/.dsh already stores)
dsh --profile <profile> --patch ./cordis.yml --no-open --port 3081The MCP server listens on 127.0.0.1:8090 (StreamableHTTP). Point any MCP client at http://127.0.0.1:8090/mcp.
Client configuration
Generic (any streamable-http capable MCP client):
{
"mcpServers": {
"dsh": {
"url": "http://127.0.0.1:8090/mcp",
"headers": { "Authorization": "Bearer <your-secret-token>" }
}
}
}Let another dsh operate this one (add to the peer profile's cordis.patch.yml, using the official dsh-mcp-client):
- id: dsh-carrot-on-a-stick
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: dsh
transport: streamable-http
url: http://127.0.0.1:8090/mcp
headers:
Authorization: 'Bearer <your-secret-token>'cordis.yml (patch format)
- insert:
- id: dsh-carrot-on-a-stick
name: 'dsh-carrot-on-a-stick'
config:
http: true
port: 8090
host: 127.0.0.1 # localhost only by default; add auth before exposing
# authToken: 'your-secret-token' # Bearer token auth (constant-time compare)
# workspaceRoots: ['/workspace'] # cwd whitelist (separator/case-safe cross-platform)
# allowedHosts: ['my-box.lan'] # extra allowed Host header values (DNS-rebinding guard)
# preset: 'standard' # agent preset to mount
# model: '' # empty = follow dsh user/default settings
# provider: '' # pair with model; empty = follow host default
# reasoningEffort: '' # default reasoning effort (empty = adapter default)
# allowModelOverride: true # false = pin the model, refuse caller overrides
# sessionTtlMs: 86400000 # idle MCP transport sessions are reaped after this (0 = never)Field | Default | Meaning |
|
| MCP server bind address; a listen failure (port in use, β¦) fails plugin startup loudly |
| β | Bearer token; enforced on every request when set (constant-time compare) |
| β | cwd whitelist; agents may only work inside the listed directories (subdirs included) |
| β | extra allowed Host-header values; the bind host and loopback aliases are always allowed, everything else gets 403 |
| follow host user settings ( | spawned-agent model selection; configure as a pair β a partial setting is completed from the host default |
| adapter default | default reasoning effort (adapter-defined id, see |
|
| whether callers ( |
|
| agent preset to mount |
|
| default detail level for |
|
| bulk-attach ungrouped sessions to workspaces at startup (writes user data; the |
|
| queue capacity, result TTL, session-pool LRU limit |
|
| auto-timeout per agent turn; on expiry the host's official |
|
| heartbeat interval for |
| β (off) | persist the task queue to this file: every change is written, and on startup |
|
| reap idle MCP transport sessions after this long; clients get 404 on the stale session id and re-initialize per the spec ( |
Every result is structured: sessionId / model / changes / verification / leftovers / error / toolCallCount β¦ (projected by detail level); error carries non-normal turn endings (model failure / cancel / blocked), so a silent empty "success" can no longer happen. Empty error/taskId fields are omitted rather than sent as empty strings. Tool-level failures (unknown taskId, model override refused, service unavailable, cwd outside workspaceRoots, a turn that ended in error) come back as tool results with isError: true (the MCP-spec-recommended shape) β strict clients and models can recognize them without parsing the payload.
Zero host copies
The plugin has zero runtime dependencies on @deepseek-ai/*: every dsh capability is reached
through injected host services (ctx.agents / ctx.tools / ctx.agentPresets β¦), types only
augment the compiler via import type (erased at build), and @deepseek-ai/* packages are
devDependencies. A plugin therefore can never drag a mismatched copy of a host package into the
process β the root cause behind the upstream-era scopeOf symbol mismatch that silently left
agents tool-less. Event reads go through the public session.snapshotEvents() API and message
construction uses a local, field-for-field equivalent of the host's createUserMessage.
Security
β οΈ This plugin exposes local execution capability (equivalent to RCE). It binds 127.0.0.1 only by default. When enabling:
Set
authTokenβ against other local processes and DNS-rebinding attacks (constant-time compare);Set
workspaceRootsβ constrain where agents may work;Never bind
0.0.0.0or expose to LAN/WAN without a reverse proxy + TLS + auth.
Built-in guards: a Host-header allowlist (bind host + loopback aliases by default, guarding against
DNS rebinding; a missing Host header gets 400), an Origin-header check on the same allowlist
(requests that carry a cross-origin or unparseable Origin β e.g. Origin: null β get 403;
non-browser MCP clients that send no Origin are unaffected), a /mcp-only HTTP surface (everything
else 404), 401 answers with a WWW-Authenticate: Bearer challenge, and idle transport sessions
are reaped after sessionTtlMs (24 h default). Tools also carry spec-metadata (title,
annotations.readOnlyHint etc., the 2025-06-18 protocol fields) so clients can label and
sandbox-check them. Note: queuePersistPath writes task payloads (task text, caller context,
results) to a plaintext file β point it at a location with appropriate filesystem permissions.
Web settings panel
The bundle also injects a settings section into the dsh web UI (Settings β dsh-carrot-on-a-stick):
live status badge (listening / soft-stopped / http disabled) and uptime;
the MCP endpoint (click to copy) and start / stop buttons β soft stop drains and closes cleanly, start re-listens (verified by tests with a real handshake after restart);
connected MCP clients: session id, user-agent, connected-at, last activity, request count;
model / preset / auth / session-TTL / queue-persistence summary and queue counters (active / done / failed / cancelled).
The panel talks to the host over same-origin routes under /_dsh/dsh-carrot-on-a-stick/*; mutating routes
require same-origin markers, so no extra ports or CORS exposure are needed.
Provenance
The initial source of this project was copied from chushixixin/dsh-harness-mcp-server (MIT, thanks @chushixixin) and then evolved as an independent project β no git fork relationship, no upstream contributions planned. Key changes:
drops the Hermes-specific framing β targets any MCP client;
tracks current dsh releases (see Roadmap);
independent name and repository:
dsh-carrot-on-a-stick.
Roadmap / known limitations (against dsh 0.1.5-rc.2)
The 0.2.0 compatibility issues were fixed in 0.3.0; 0.3.1 completed live-host E2E verification (all green β see docs/e2e-0.1.5-rc.2.zh.md) and fixed what it uncovered: the {{model}} prompt variable (model selection now completed via agentDefaultModel), turn-failure surfacing, pool-session flush, startup reattach off by default, and corrected install docs.
0.5.0 completed the model-selection surface: model_list (official catalog / llm fallback), per-call overrides on agent_run + task_inbox, select_model (in-session switch), reasoningEffort, the allowModelOverride gate, model reported in every result, and a session pool keyed by cwd + model.
The current (unreleased) batch is a large consolidated update covering six areas, developed in internal milestones (see the CHANGELOG for the per-milestone detail):
Protocol conformance: tool errors carry
isError: true, Origin-header check +WWW-Authenticatechallenge join the DNS-rebinding guards, tools exposetitle+annotations, transport sessions are reaped (sessionTtlMs), 10 MB request-body cap;Cancellation:
agent_runhonours MCPnotifications/cancelledand client timeouts (both wired to the host's officialagent.cancel);Observability:
notifications/progressheartbeats (spec_meta.progressToken),task_list, read-onlysession_list(with current model) andsession_history(paginated);Async queue:
task_cancel, opt-in persistence (queuePersistPath, atomic writes), honestrunning/interruptedstatuses;Resilience: lock-table cleanup, LRU eviction skips busy sessions, corrupted persistence file tolerated at startup;
A full protocol audit is documented in docs/protocol-audit-2026-09-24.zh.md.
What remains:
The task queue lives in process memory; a restart loses itβ opt-in persistence (queuePersistPath) added; without it, the queue is still memory-only.No server-side timeout foragent_run/β the opt-intask_inboxtaskTimeoutMs(off by default) fires the host's official cancel; callers can also cancel actively (notifications/cancelledforagent_run,task_cancelfor queue tasks).The queue cannot be listed or cancelled eitherβ done (task_list/task_cancel).Read-only query surface is still incompleteβsession_list+session_history(live sessions) joinattach_session/rename_session/select_model; reading the full log of persisted-only sessions needs a host-side load API.presetremains deployment-level (one persona per MCP server instance); it cannot be chosen per call.Tool calls inside spawned sessions go through the host approval policy (sensitive operations under
askmay pop a dialog or fail closed; the read-only E2E operation was unaffected).dsh_list_toolsonly lists the host-global registry; listing an agent's actually-visible tools needs a host-side API (the ScopeKey is a private symbol, unreachable under the zero-copy principle).select_modelrequires the web profile'ssessionController; where that service is absent (e.g. headless) only the catalog fallback and per-call overrides work, and the tool says so explicitly.Protocol-native task augmentation (2025-11-25 draft, SDK marks the interfaces experimental):
agent_runas a spec-native task withtasks/get/tasks/resultpolling. Deliberately deferred β ourtask_inbox/task_result/task_cancelalready cover the workflow for all clients; revisit when the spec leaves draft.When dsh releases new versions, the
@deepseek-ai/*devDependencies need syncing (compile-time only; the zero-runtime-dependency design is unaffected).
Development
npm install
npm run build # standalone build (plain tsc) -> lib/
npm run smoke # fake-ctx smoke on ports 8099/8098/8096/8095/8094/8093/8092/8091/8089/8088/8087/8086/8085 (132 checks, real MCP protocol round-trips incl. official SDK client)
# + a port-conflict case (apply must fail loudly)Live-host E2E (needs a local dsh with model credentials; costs a few tokens): boot a dedicated
profile as described in docs/e2e-0.1.5-rc.2.zh.md, then run
E2E_WITH_AGENT=1 node e2e.mjs.
License
MIT β see LICENSE (upstream copyright notice retained).
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted MCP server for task-first delegation to remote workstations and workers.
Remote MCP server exposing SMI Aware tools, resources, and skills over Streamable HTTP.
Remote MCP server to read and manage your Atako AI agents, messages, files, and integrations.
MCP server exposing the Backtest360 engine API as tools for AI agents.
Related MCP Servers
- AlicenseAqualityBmaintenanceAn MCP bridge that exposes DeepSeek Harness (DSH) web UI's cordis RPC API as stdio tools, enabling any MCP client to manage workspaces, create/resume sessions, send messages, and fetch session stats.51MIT
- AlicenseNot gradedqualityAmaintenanceExposes DeepSeek Harness agent capabilities as an MCP server, letting any MCP client drive Harness to execute real coding tasks with structured results, context isolation, and parallel execution.156 npm11MIT
- 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.287 npm2GPL 3.0
- AlicenseNot gradedqualityAmaintenanceEnables local workspace management and status checks through MCP tools over an HTTP endpoint, allowing DeepSeek Harness to interact with the platform.MIT