dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DeepSeek Harness 插件:MCP 可视化管理(服务器/工具开关/环境变量/凭据托管)· Visual MCP manager UI plugin for DeepSeek Harness
Supported DSH version:
dsh 0.1.6-alpha.2— developed and verified on0.1.6-alpha.2, declared inpackage.json→dsh.supported. When the DSH and plugin versions do not match, the Settings page shows a diagnosis (check the registration row → restartdsh web→ hard-refresh → upgrade both sides).
Problems it solves:
search mode lets the model hot-inject only the tools it needs via mcp_tool_search, saving tokens at scale.tools/list_changed notifications dispose and re-register same-named tools, jittering the system-prompt tool list and constantly invalidating the prompt cache. Tool-list stabilization keeps unchanged tools registered, maximizing cache hits.Highlights:
${NAME} and are substituted at connect time (e.g. Authorization: Bearer ${TOKEN})search (on-demand, token-saving) and full (inject everything)streamable-http servers using MCP OAuth (authorization-code + PKCE), the browser opens automatically for authorization on connect; tokens and OAuth client info are persisted and refreshed automatically by the SDK (auto-renewed while active within 24h), with automatic re-authorization after expiryMigrated and merged from uncommitted MCP work in the deepseek-harness repository:
| Original package | Migrated to |
|---|---|
packages/mcp/mcp-manager (host registry) | lib/index.js (host half) |
packages/client/ui-settings-mcp (settings UI) | src/client/* → lib/client.js (browser half) |
packages/bundle/web-mcp (bundle assembly) | single row registered via cordis.patch.yml |
packages/mcp/mcp-client/src/probe.ts + transport.ts | lib/probe.js + lib/transport.js (vendored; no in-box changes) |
mcp_servers), per-server
@deepseek-ai/dsh-mcp-client mounts, environment variable injection (plain values in the definition, secrets via credentials),
connection probe (test).search (on-demand, default — the model hot-injects tools via mcp_tool_search) and full (inject every enabled tool each request).lib/oauth.js): on a 401 + OAuth challenge from a streamable-http
server, runs the authorization-code + PKCE flow automatically — opens the browser, receives the
callback on a loopback server, persists tokens, and refreshes them on demand; test connection and
mounts share the same token.mcpManager Remote namespace itself via ctx.remote.$mount() in apply(),
so no in-box package modification is required.lib/cordis-servers.js): @deepseek-ai/dsh-mcp-client rows
declared in the patch layers are listed read-only on the Settings page (see below).@deepseek-ai/* resolve from the DSH profiles module fallback).cordis.patch.yml) and precedenceDSH can declare MCP servers directly in the composition: one row per server, name: '@deepseek-ai/dsh-mcp-client',
in the profile layer $DSH_HOME/profiles/<profile>/cordis.patch.yml or the machine-wide layer
$DSH_HOME/cordis.patch.yml (machine-wide applies to every profile and overrides the profile layer per row id).
- insert:
- id: mcp-github
name: '@deepseek-ai/dsh-mcp-client'
config:
transport: stdio # or streamable-http
serverName: github
command: npx
args: ['-y', '@modelcontextprotocol/server-github']
Since 1.11.0 those declarations appear in Settings → MCP, badged "cordis declaration", read-only, with the file they came from:
serverName is already declared (and not disabled), the manager does not mount the same-named stored row — two mounts under one name collide in the tool registry and roll back that server's whole generation; the page explains the conflict.cordis.patch.yml instead (web/desktop reload live; headless/sdk apply it on the next start).cordis:<rowId>, with origin/declaredIn/declaredRowId), import only — a row created in the plugin is never overwritten; a mirror whose declaration disappeared is removed automatically (a mirror is only a copy of a declaration, so an ownerless one must not pile up); a declaration containing a !!js expression is skipped and flagged with the reason (its value only resolves inside the Loader). Mirrors are never mounted — the composition owns the mount.disabled: true into its own managed block in cordis.patch.yml (a .dsh-mcp.bak backup is taken before the first write; atomic, idempotent, reversible, confined to that block). The declaration releases the serverName and the plugin mounts its own row instead — enabling OAuth authorization, managed credentials, ${VAR} header substitution and connection tests. A hard mount failure rolls back automatically (the block is removed and the row returns to mirror state) and reports why; "Give back" hands the mount back to the declaration and deletes the managed row. A declaration without an explicit id, or one containing a !!js expression, cannot be taken over.needsPlugin): a declaration can carry neither an OAuth provider nor ${VAR} / bare-name placeholder substitution — only the plugin does those. Such a declaration can therefore only work while the plugin owns the mount: the page reports it as failed with the reason (not "connecting"), "Give back" makes its tools disappear (the confirmation and the result warning both say so), and taking it over again restores them.oauth: true (the form's "Use OAuth authorization" checkbox, off by default). Do not tick it for servers that authenticate with static tokens or custom headers — otherwise a 401 is treated as an OAuth challenge and the plugin keeps opening the browser (with its 127.0.0.1:<port>/callback loopback listener). The plugin also opens the browser at most once per server per process; later automatic attempts return the authorization-link error, and "Test connection" can still start one on demand. Upgrading migrates automatically from existing OAuth credentials, so servers that genuinely need OAuth are unaffected.pendingTakeover); the next dsh start mounts it, and "Give back" reports the same need for a restart. A mount is also always skipped when same-named mcp__<server>__ tools already exist without a local mount, so the plugin never fights a still-mounted declaration for one name.mcp__ prefix across the whole tool set, so declared tools need no extra configuration.dsh-mcp/
├── package.json name=dsh-mcp; dsh.client declaration; zero npm dependencies
├── lib/
│ ├── index.js host half (McpManagerService, built from mcp-manager)
│ ├── cordis-servers.js reads natively declared MCP servers from the patch layers (1.11.0)
│ ├── patch-writer.js managed-block writer: disables a declaration on takeover (backup/atomic/idempotent)
│ ├── mcp-client.js vendored MCP client (from @deepseek-ai/dsh-mcp-client, with tool-list stability extension)
│ ├── oauth.js MCP OAuth client provider (authorization-code + PKCE, loopback callback, token persistence)
│ ├── probe.js vendored connection probe (from mcp-client/src/probe.ts)
│ ├── transport.js vendored transport factory (from mcp-client/src/transport.ts)
│ └── client.js browser half (esbuild bundle, ModuleLoader wire format)
├── src/client/ browser half source (TSX + CSS Modules + local types + remote-contribution)
└── scripts/build.mjs build script (esbuild resolved from a DSH checkout, see below)
node scripts/build.mjs
$DSH_SOURCE, or ~/.dsh/source/current when unset.@deepseek-ai/*, zod, @modelcontextprotocol/sdk) are not installed as npm packages;
they resolve from $DSH_HOME/profiles/node_modules (DSH profiles module fallback, $DSH_HOME defaults to ~/.dsh);
the build points nodePaths at the same directory.<style data-plugin="dsh-mcp" data-file="…"> tag, and the module default-exports an identity class-name map.npm test
npm test adapts to the environment: the files that import the DSH module closure
(@deepseek-ai/*, js-yaml) — cordis-servers, patch-writer, takeover — are skipped when
no local DSH installation resolves (as in public CI), while a local checkout with the profile's
node_modules in reach runs the whole suite; the run says which files it skipped.NEEDS_DSH_CLOSURE in
scripts/test.mjs.Option 1: npm (after publishing)
dsh plugin --profile web add dsh-mcp
Option 2: GitHub git source
dsh plugin --profile web add github:ArvinQi/dsh-mcp
# or
dsh plugin --profile web add git+https://github.com/ArvinQi/dsh-mcp.git
Option 3: local development (link)
dsh plugin --profile web add link:<absolute path to this repo>
Note: with a local
link:install, the plugin directory needs anode_modules -> $DSH_HOME/profiles/node_modulessymlink (development-only, not committed); otherwise the linked symlink is realpath-resolved and@deepseek-ai/*cannot be resolved.
Append to $DSH_HOME/profiles/web/cordis.patch.yml ($DSH_HOME defaults to ~/.dsh):
- insert:
- id: dsh-mcp
name: dsh-mcp
⚠️ This step is mandatory: dsh-mcp does not declare
dsh.bundle, sodsh plugin addonly installs the package into the profile — it does not activate the plugin. Without the registration row the plugin never mounts.
Then restart dsh web and hard-refresh the browser (Cmd/Ctrl + Shift + R):
⚠️ Both the restart and the hard refresh are required:
- The settings page (client half) needs the client roster, and roster changes only take effect after restarting
dsh web(refreshing the browser alone is not enough);- After the restart you must hard-refresh (
Cmd/Ctrl + Shift + R) — a normal reload may keep serving the cached old page.
Open the management page: after restart, open DSH Web → Settings → MCP.
Q1: No "MCP" entry in Settings after installing?
Check in order:
$DSH_HOME/profiles/web/cordis.patch.yml has the
- insert: [{ id: dsh-mcp, name: dsh-mcp }] row (id/name must exactly match the package
name dsh-mcp). dsh plugin add does not equal activation — without the registration row
the plugin never mounts.dsh web? Refreshing the browser is not enough — the settings entry comes
from the client roster, and roster changes require restarting the process.Cmd/Ctrl + Shift + R
(Windows/Linux: Ctrl + Shift + R); a plain F5 may load a cached old page.web profile (dsh plugin --profile web add dsh-mcp +
$DSH_HOME/profiles/web/cordis.patch.yml); other profiles have their own settings pages.dsh plugin --profile web add dsh-mcp@latest (or @1.8.0).Q2: "MCP" is visible but the server list is empty or errors?
dsh web process log for mcp-manager initialization errors;client api: ... 404 or env is not iterable — both
come from mixing versions);transport failure for /api/mcpManager/list: HTTP 404 means the host did
not register the mcpManager service: usually the plugin host half is not active (missing
cordis.patch.yml row / wrong profile) or the client and host versions disagree. Verify the
registration row per Q1, confirm the install targets the web profile, restart, and
hard-refresh; if it persists, upgrade both dsh web and the plugin to the latest versions.Q3: MCP tools do not show up in an agent session?
mcp_tool_search and hot-injects them,
so tools not searched are absent from the system prompt by design; switch to "Full injection"
to verify.Q4: A server with an Authorization header still asks for OAuth / fails to mount?
Authorization (static bearer/token) header is configured, dsh-mcp does NOT treat the
server as OAuth: the authorization-code + PKCE flow is enabled only for servers WITHOUT a static
Authorization header, so a 401 is never mistaken for an OAuth challenge that opens the browser.
For a static-token server, make sure the request headers are correct;fetch failed / unable to verify the first certificate, the
host Node does not trust the internal CA: start dsh web with NODE_OPTIONS=--use-system-ca
(or add the root cert to NODE_EXTRA_CA_CERTS), then restart the host and hard-refresh.Add a server:
serverName, determines the tool prefix mcp__<serverName>__), transport
(streamable-http → URL / stdio → command), headers, tool-call timeout, etc.Process env vars (below the injection mode, expanded by default):
process.env)
under the same name, that value is used verbatim (name unchanged) at connect/display time and
stored values only act as fallback — export it in your startup script first
(e.g. export ADA_TOKEN=...) then restart dsh webNAME=value per line) or add rows one by one${NAME} (e.g. Authorization: Bearer ${GITLAB_TOKEN}),
substituted at connect time (priority: server env > process-level env > system environment)JSON config editor (top-right of the MCP config module):
OAuth servers (streamable-http using MCP OAuth, e.g. OAuth-protected gateway services):
serverName)
and refreshed automatically by the MCP SDK (auto-renewed while active within 24h); after authorizing once,
mounts and later test connections reuse the same token, and an expired token triggers a fresh
browser authorization automatically.Day-to-day management:
Tool control (the key to saving tokens):
search (on-demand, default) and full (inject everything).
search mode, the model calls mcp_tool_search to discover and hot-inject the MCP tools it needs.Verifying the effect:
mcp__<serverName>__<tool>.search mode, tools not retrieved stay out of the system prompt, saving tokens and improving
prompt-cache hit rate.list_changed notifications no longer dispose and re-register
same-named tools, keeping the tool list stable.lib/index.js is a build artifact of mcp-manager (spec/types inlined); edit the lib files
directly, or rebuild from TypeScript with the repository toolchain.src/client/*, re-run node scripts/build.mjs; host-half changes take effect without
reinstalling (link install).dsh web to enter
the client roster.- **Supported DSH version**: dsh <version> line to the CHANGELOG entry and the GitHub Release notes, and
update package.json → dsh.supported plus the "Supported DSH version" note at the top of this README.See CHANGELOG.md. Released under the MIT License.