dsh-mcp-bridge
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-mcp-bridgedelegate to DSH: add the missing tests for parser.js in ~/project"
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-mcp-relay
Expose a running DeepSeek Harness (DSH) instance over the Model Context Protocol, so an external agent — Codex, or any MCP client — can drive that instance's own sessions.
A DSH session behaves like a subagent from the caller's point of view: it keeps its own context across calls, works in the workspace you name, and returns a final answer. The relay also lets the caller answer the questions that DSH agent asks, so a delegated task never deadlocks waiting for a human who is not there.
Why
Driving a DSH instance through its web GUI from another agent costs a screenshot or DOM round trip per step. This relay replaces that with a small structured API: one tool call in, one result out.
Related MCP server: dsh-web-bridge-mcp
Architecture
MCP client (stdio)
└── bin/dsh-mcp-relay.mjs MCP server; polls the relay, holds questions for the caller
└── HTTP 127.0.0.1:<port>/dsh-relay
└── lib/index.js host Cordis plugin mounted inside the running DSH process
└── ctx.sessionController → DSH agentTwo halves, and the split matters:
lib/index.jsis a DSH host plugin. It runs in the same process as the Web GUI, so it can read and write the sessions that instance already has. It registers an HTTP route and does the DSH work.bin/dsh-mcp-relay.mjsis a standalone MCP server that speaks stdio to your agent and HTTP to the plugin. It exists because DSH's own process owns stdout, so the plugin cannot serve stdio itself.
Waiting and polling happen in the MCP server, where they cost nothing; your model spends one tool call and receives the finished result.
Requirements
DSH with a Web profile (
dsh webor the desktop app).Node.js 18 or newer on the same machine as DSH.
An MCP client that can spawn a stdio server.
Install the plugin
Install straight from this repository — the package is not published to npm:
dsh plugin --profile web add github:Nigheart/dsh-mcp-relayThen restart DSH. On startup the log should show:
[dsh-mcp-relay] mounted /dsh-relay (tokenRequired=false)Verify it independently:
curl -s http://127.0.0.1:3080/dsh-relay/ping
# {"ok":true,"data":{"relay":"dsh-mcp-relay","version":"0.6.0","tokenRequired":false}}If your instance listens on a port other than 3080, use that port here and set DSH_RELAY_URL for the MCP
server (below).
Manual install
If you prefer to wire it up by hand, add the package to the profile's package.json and list it as a bundle:
{
"dependencies": { "dsh-mcp-relay": "github:Nigheart/dsh-mcp-relay" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-mcp-relay"] } }
}The package ships its own cordis.patch.yml, which inserts one loader row. That row declares
inject: [webServer, sessionController] — keep it. Without inject the plugin can be activated before those
services exist, find nothing to bind to, and mount no route at all.
Configure your MCP client
Codex (~/.codex/config.toml):
[mcp_servers.dsh]
command = "node"
args = ["/absolute/path/to/dsh-mcp-relay/bin/dsh-mcp-relay.mjs"]
startup_timeout_sec = 60
# Only needed when DSH is not on the default port, or when a token is set.
[mcp_servers.dsh.env]
DSH_RELAY_URL = "http://127.0.0.1:8080/dsh-relay"The server sends its own usage notes in the MCP handshake (initialize.instructions), so the client learns
the workflow without any extra prompting.
Environment variables:
Variable | Default | Purpose |
|
| Where the plugin listens. |
| (unset) | Sent as |
|
| Default wait for one delegated turn. |
|
| Progress poll interval. |
Tools
Tool | Purpose |
| List sessions of the running instance, optionally filtered to one workspace. |
| List the provider/model ids this instance can route, with each model's reasoning efforts. |
| Create a session in a workspace, or continue one by |
| Answer a question the DSH agent asked, so it can resume. |
| Choose how a session resolves future approval requests: |
| Cancel the active turn — the way out of an approval nobody can answer. |
| Read a session's status and latest output; use after a wait that returned |
Delegating work
dsh_delegate({ workspace: "/home/me/project", instruction: "Add the missing tests for parser.js" })Omit sessionId to start a new DSH agent; pass the returned sessionId back to continue the same agent,
which still remembers the earlier work. status tells you what happened:
| Meaning |
| The turn finished; |
| Blocked on a human: either a question takeover is holding for you, or an open approval request. |
| Still running; call |
The relay also attaches every session it creates (or reuses) to the DSH workspace owning the target
directory. DSH groups sessions in its GUI by that explicit attachment rather than by working directory, so
without it a delegated session runs fine but shows up as ungrouped. Sessions created before this behaviour
existed can be repaired with one call to the relay's POST /dsh-relay/attach-all endpoint.
Choosing the model
A session runs on the instance's default model unless you say otherwise. dsh_models reports what this
instance can route right now — provider groups, model ids, and each model's reasoning efforts:
dsh_models()
dsh_delegate({ workspace: "/home/me/project", instruction: "…",
model: "deepseek-v4.1-pro", reasoningEffort: "high" })model takes a model id; provider is inferred when that id is unambiguous, and provider/model is
always accepted. reasoningEffort must be one of the efforts dsh_models lists for that model. A route
this instance cannot serve fails the call before the instruction is sent, rather than quietly running
it on some other model.
The selection applies to this turn and is also saved as the instance default for sessions created
without one — the same stickiness DSH's own model picker has, so choose deliberately. Omit model to
leave the session on whatever it already uses. dsh_delegate echoes the selection it installed as
model (with modelNote); dsh_list_sessions and dsh_poll report each session's current model,
read from DSH's model-selection projection (a hint: it is null until the session has recorded a
request, and on deployments without that projection).
Answering questions
By default a delegated session runs under takeover: when the DSH agent calls its question tool, the call is
suspended and handed to the caller instead of the GUI. dsh_delegate then returns immediately with the
question rather than waiting:
{
"status": "waiting",
"waiting": {
"kind": "user-question",
"toolName": "ask_user_question",
"intercepted": true,
"detail": [{ "id": "choice", "question": "Which database?", "options": ["sqlite", "postgres"] }]
}
}Answer it with the ids from detail:
dsh_answer({ sessionId, answers: [{ id: "choice", selected: ["sqlite"] }] })If nobody answers within about three minutes, the question falls back to the DSH GUI, so an unattended
delegation cannot hang forever. Pass takeover: false to dsh_delegate to leave questions with the GUI from
the start.
Approvals
Some operations need a decision before DSH will run them — for example escalating a sandbox so a tool can write outside the session workspace. The session then reports:
{
"status": "waiting",
"waiting": {
"kind": "approval",
"detail": [{
"id": "d0f853e3-...",
"toolName": "pwsh",
"callId": "call_00_...",
"reason": "escalate sandbox to danger-full-access: ... outside the session workspace"
}]
}
}reason is DSH's own explanation of what it wants and why, so the caller can judge whether granting it is
appropriate.
An approval that is already pending has only two exits: a person answers it in the DSH Web GUI, or the turn
is abandoned with dsh_cancel. Nothing below can rescue the request that is already open.
dsh_approval({ sessionId, mode }) decides how future approvals in that session are handled. The policy
takes effect at the agent's next step, so it never applies retroactively:
"gui"(the default) — leave them to the DSH Web GUI for a person to answer."deny"— set the session's policy tonever: every subsequent request resolves as rejected without prompting. For unattended work this beats waiting forever — a clean failure instead of a hang."auto-approve"— this plugin answers the request itself with a one-shot grant. Whether that is even possible is deployment-dependent: the plugin has to sit upstream of the GUI's answerer. Registering the mode is therefore not proof that it works — check the relay's/dsh-relay/diagnostics(approvalListener.listenerHits); the endpoint reportseffective: falsewith a warning when it cannot. On the deployment this was built against,listenerHitsis 0, so onlydenyandguitake effect there.
A tool call that has simply not finished yet is not reported as waiting; such a session stays running
and carries an informational pendingTool field.
Security
The relay is a control surface: whoever can reach it can send instructions to, and read the history of, every session in that DSH instance.
It serves loopback callers only and answers
403to anything else. This matters because the shipped Web profile binds0.0.0.0.Set
DSH_RELAY_TOKENon both sides to additionally require anx-dsh-relay-tokenheader.The route is registered directly on DSH's HTTP server, so it is not covered by the
/apitrusted-host fence. Its own checks above are the only gate.
Limitations
The MCP server and DSH must run on the same machine, because the relay is loopback-only.
One delegated turn is bounded by
DSH_RELAY_TIMEOUT_MS; there is no streaming of partial output.cancelstops the active turn only — messages already queued in DSH keep running.The plugin reads a session's tail through
sessionController.page, so a very large session reports its recent history rather than all of it.
Development
The plugin can be developed without restarting DSH: load it as a dynamic Cordis plugin first, verify behaviour
against the live services, then install it as a bundle package. Do finish with one real restart — dynamic
plugins are activated only after the services they need already exist, so they cannot catch a startup-order
bug such as a missing inject.
The relay's route table has a dependency-free test that stubs the Cordis context, so it needs neither DSH nor a restart:
npm testLicense
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Hosted MCP runtime where your agent publishes its own tools by tool call and keeps working.
Remote MCP server for AI.TV creators — delegate account operations to your AI agent over MCP.
Read and write shared BitsWeave context, projects, tasks, and work sessions through MCP.
Related MCP Servers
- 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.361 npm6GPL 3.0
- 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
- AlicenseNot gradedqualityCmaintenanceEnables permission-scoped MCP clients to interact with DeepSeek Harness Desktop over a local loopback HTTP bridge, including checking status, listing models/workspaces/sessions, reading bounded session history, and creating, sending, waiting for, or canceling chats. Access is restricted per bearer token and only exposes the tools and sessions granted to that token.MIT
- AlicenseNot gradedqualityBmaintenanceLets Claude Code, Codex, or any MCP client drive DeepSeek Harness as a long-lived coding agent: starting and resuming conversations, dispatching tasks in the background, streaming live output and reasoning stats, interjecting or interrupting mid-turn, handling approval prompts, and collecting results later. Sessions persist by ID, so work survives process restarts and can be handed back to a DSH GUI when needed.158 npm15MIT