Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add @hyzyn/dsh-mcp
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
中文 | English
The “MCP Server Configuration” card under DSH Settings → Plugins: maintain MCP servers graphically, saving hot-reloads them, no host restart needed.
Features
- Hot-loaded on save: rewrites the managed block of
~/.dsh/cordis.patch.yml; DSH’s HMR watcher reloads it automatically, and tools are registered to the model asmcp__<server>__<tool>. - stdio and streamable-http transports: stdio (command / args / env / cwd) and streamable-http (url / headers, including SSE and session headers); values can be written as
js:expressions, so secrets never land in the patch file. - Connection Test without the MCP SDK: the host speaks JSON-RPC directly (initialize → tools/list), returning protocol version / serverInfo / tool list / elapsed time.
- Liveness and name conflicts are visible: reads liveness from the loader fiber (running / disabled / error / loading). Name clashes come in two distinct tiers, never conflated (issue #5):
conflictsholds only rows that actually reuse an external serverName — both instances would claim the samemcp__<serverName>__*tool names, so the warning banner appears only then; the inventory of instances outside this card travels inexternalServersand is shown as a neutral info banner (“these names are already taken elsewhere — don’t reuse them here”). Servers can be enabled / disabled (disabled: true) / edited / deleted. “Mounted” is not the same as “connected”: fiber liveness comes from the loader, while a server that cannot be reached still resolves itsmcp-clientapply (failOnStartupErrordefaults tofalse, so nothing throws) — an unreachable address therefore keeps showing a green “running”. Each managed row now carries atoolCount(the number ofmcp__<serverName>__*tools in the registry): when the row isactiveandtoolCount === 0, the badge reads “Not connected” instead (overriding the green “running”), and a line underneath spells out the cost (every boot waits once for that connection attempt) with a “disable” button right there. When the tool registry cannot be read,toolCountis absent (not 0) and the UI draws no conclusion — “cannot read” must never be rendered as “not connected”. - Capability notice injected into systemPrompt: injects a capability notice (systemPrompt section) so that when “MCP config / MCP server” comes up, the model knows it refers to this plugin.
Where the settings surface lives depends on the DSH version, but it is always the same form: from
0.2.0-rc.1 it is registered as plugins.bundle.config — inline on the plugin detail page, directly
below the description, with no extra ">" step; older hosts (the 0.1.6 line) fall back to the row's ">"
sub-page under the Plugins sidebar, and ≤0.1.5 uses the settings-page card.
Structure
| File | Description |
|---|---|
src/index.ts |
Host half: managed block read/write, validation, status reading, JSON-RPC probing, /api/dsh-mcp/* routes (loopback-only fence) |
client.js |
Browser half: registers the settings.plugin.item card (React shell + pure-DOM admin panel, window.__ModuleLoader__.load format) |
cordis.patch.yml |
Bundle patch: inserts the plugin row into the profile lineup |
Routes (loopback + same-origin only):
GET /api/dsh-mcp/servers— list + status (including each row’stoolCount, see below) + real clashes (conflicts) / instances outside this card (externalServers). A failed DTO read returns 500 with the original reason: the host webserver answers a throwing handler with an empty 400, which shows nothing in the UI (the fence has already passed, so returning the text is safe and makes the next incident obvious at a glance)POST /api/dsh-mcp/servers/save— save the whole set (after validation, write back to the managed block)POST /api/dsh-mcp/test— run one Connection Test with the form configuration
Fence (same tier as docker / tty since 2026-09-25): the loopback fence uses
isLoopbackRequestStrict from @hyzyn/dsh-kit (the whole 127/8 range plus DNS confirmation for
alias hostnames), and the two write routes above additionally require a same-origin proof
(Sec-Fetch-Site: same-origin or a matching Origin; when both are absent, only requests carrying
the host session cookie pass — that is the desktop shell's forwarding chain, see docker D139).
The read-only GET /servers needs no proof, so bare curl still works. The rationale and the single
implementation live in packages/kit/src/http.ts.
Installation
pnpm --filter @hyzyn/dsh-mcp build
dsh plugin --profile web add link:$(pwd)/packages/mcp
dsh plugin add installs this package into the profile dependencies and, because it declares dsh.bundle,
also adds it to the dsh.profile.bundles patch layer — which is what makes the
insert: { id: mcp-config, name: '@hyzyn/dsh-mcp' } plugin row in cordis.patch.yml take effect,
so you only mount it once. After the plugin code is updated, restart dsh web for it to take effect.
⚠️ Do not manually append the same plugin row to
~/.dsh/cordis.patch.ymlagain: inserting the sameidonce in each of two patch layers triggersduplicate loader entry id: mcp-configat startup, and the host process exits immediately. The home patch layer should only keep the plugin’s own# --- dsh-mcp-config managed ...managed block (server rows, not the plugin row).
The plugin row itself needs no HMR: after you save the MCP Server Configuration (the rows inside the managed
block), DSH’s watcher on the home patch layer hot-loads them as mcp__<server>__<tool>
tools within 1–2 seconds. After refreshing the page, expand the “MCP Server Configuration”
card under Settings → Plugins in the Web GUI to manage servers. Note: the browser
half depends on the core slots service, and only the official settings panel of
dsh-web-app provides that slot.
Uninstallation
dsh plugin --profile web remove @hyzyn/dsh-mcp
After restarting dsh web, the plugin row disappears with it. The managed block can stay (just empty lines when no plugin
is present), or you can first clear the server list in the panel and then manually delete the
# --- dsh-mcp-config managed ... block.
Notes
- The managed block is marked with
# --- dsh-mcp-config managed (auto-generated; do not edit) ---; the plugin only rewrites that block and keeps everything else as is; the configuration inside the block aligns with the official mcp-client Config schema. An empty list is written as- insert: [](a no-op for the entry tree) — be careful not to write a bare[], which would break the top-level YAML document of the home patch file, making HMR config refresh fail to parse and deleted servers unable to unload - The save endpoint validates serverName (
[A-Za-z0-9_-]{1,32}), required transport fields, and reconnect parameter bounds - An empty list needs explicit confirmation to clear:
/servers/savereplaces the whole table, soservers: []means clearing the managed block. To stop a startup race or a stale card from silently wiping the servers you already configured (measured: the file was left with nothing but- insert: [], and there was no way to tell afterwards who cleared it), the host answers 400 for a save that is "an empty list withoutclearAll: true" and says in the message how many entries would be removed; the card only sets that flag when the user explicitly deletes the last row (the delete confirmation says so too). A block that was already empty (no entries to begin with) is not destructive and is allowed through — saving an empty card does not error - The
!!jsexpressions of a Connection Test are evaluated inside the host (the same trust model as the loader) - The criterion for “mounted but not connected” is a tool count, not the fiber state: tools are the only
artefact “a successful connection +
tools/list” leaves behind, so counting them yields the real connection state for free (a walk over the in-memory tool registry, with no network probing). Attribution is by themcp__<serverName>__prefix, using the same character rules aspublicName()in the core@deepseek-ai/dsh-mcp-client; serverName is capped at 32 characters, so the prefix always stays inside the core’s 51-character truncation line and the attribution is reliable — should an over-long prefix ever appear, that row is skipped rather than guessed. Rows withdisabledshould have no tools anyway and do not count as “not connected”. Note this does not change startup cost: an unreachable address still costs one connection attempt per boot (about 10s; up to 60s when reachable but unresponsive) — disable the row to skip it - The browser half is hand-written ESM (the React shell is resolved through
__ModuleLoader__’srequire, and the panel itself is pure DOM), so no tsdown is needed; the host half emits ESM straight from tsc
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.