dsh-project-mcp-manager
English | 中文
A project-level MCP auto-loading plugin for DSH: write MCP server configs in
<projectRoot>/.dsh/mcp.yml or .dsh/mcp.json and they are mounted
automatically (via the official @deepseek-ai/dsh-mcp-client) whenever a dsh
session opens in that project. Changes to the file hot-reload into the running
dsh process, and tool visibility follows the session's original project
(session.header.cwd). The default
dsh-project-mcp-manager bundle includes the loader and the companion
dsh-project-mcp-ui MCP Manager page for the web and desktop apps (left
sidebar from 0.10; the old settings tab stays for one release). The UI is
enabled by default and can be disabled independently; the loader keeps working.
Capability boundary (dsh ≥ 0.2.1-rc.5): the official client owns the
protocol, reconnect, tool names, resources, and server instructions. Shipped
profiles already mount shared MCP resource tools. Official configuration is
a profile-layer Cordis patch (with that layer's own HMR) plus
plugin_manager. This plugin still owns what the host does not:
- Project-level discovery of
<projectRoot>/.dsh/mcp.yml, .dsh/mcp.json,
and the read-only legacy .mcp.json.
- Tool visibility isolated by the session's original project
(
session.header.cwd). working_directory does not retarget it.
- The MCP file format, the
dsh-mcp CLI, and the MCP Manager page.
Project-file hot reload is this plugin's file watcher. It does not replace
official profile HMR. Transport types are decided by the official client.
v0.10.x targets the dsh 0.2 line starting at 0.2.1-rc.5 (engines.dsh is
^0.2.1-rc.5); v0.9.x stays the release for 0.2.1-rc.2 through rc.4. Hosts on
dsh 0.2.0 stay on plugin v0.8.2. Hosts still on
dsh 0.1.5 should stay on plugin v0.6.0.
If this plugin is useful, a GitHub
star is appreciated.
Bugs, host mismatches, or ideas belong in
Issues — even a
short report helps.
Documentation
Feature documentation lives in docs/, English and Chinese side by side:
- Configuration format — native YAML managed
block, JSON dialect, divergences from the cordis dialect.
- MCP Manager page — use the bundled UI, read
the server list, add servers (with fill from clipboard), switch, remove, and
manage tools.
- Configuration sources and layers — the
seven-layer source model, shadow priority, global vs project mounting, and the
read-only legacy Claude Code layer.
${VAR} expansion — mount-time interpolation and
its diagnostics.
- CLI
dsh-mcp — scopes, write formats, ownership contract.
- Query surface —
ctx.projectMcp, the
projectMcp/updated event, exported view types, and the semver contract for
the companion UI.
Design and release records (Chinese): dsh 0.2.1-rc.5 adaptation ·
dsh 0.2.1-rc.2 adaptation ·
dsh 0.2.0-rc.2 adaptation ·
dsh 0.1.6-alpha.2 adaptation plan ·
dsh 0.1.5-rc.2 adaptation ·
dsh 0.1.5-rc.1 adaptation ·
dsh 0.1.2-rc.1 adaptation ·
JSON config layer proposal ·
Runtime robustness & JSON interop proposal ·
v0.10.0 release notes ·
v0.9.0 release notes ·
v0.8.2 release notes ·
v0.7.2 release notes ·
v0.7.1 release notes ·
v0.7.0 release notes ·
v0.6.0 release notes ·
v0.4.3 release notes ·
·
·
·
.
Code review records (Chinese): TypeScript changes since v0.3.1 ·
v0.4.3 to v0.6.0 ·
7e0088d to 804662f (fix follow-up) ·
feat/adapt-dsh-0.2.0-rc.2 (v0.7.0).
Installation (mount into a profile)
The plugin is mounted through a bundle patch: once the package is added to
dsh.profile.bundles, dsh synthesizes each bundle's patch (the
cordis.patch.yml pointed to by dsh.bundle.patch) into plugin lines at
startup, in order. The manager's default bundle contains two rows:
mcp-project (core loader) and mcp-project-ui (MCP Manager page). The manager
installs its same-exact-version UI dependency automatically; the UI package is
not a standalone dsh.bundle and should not be selected separately.
The core-plus-UI bundle is included in source version 0.8.0. The npm
examples below provide that layout once both 0.8.0 packages are published;
a Git tag alone does not publish packages or change npm's latest tag.
Prerequisite: install dsh itself (for users who don't have dsh yet):
npm install -g @deepseek-ai/dsh # official npm package
npm install -g deepseek-ai/dsh # or install from the GitHub source
Option 1: the dsh plugin command (recommended) — dsh plugin forwards
pnpm inside the profile directory and handles installing/upgrading
dependencies:
# Install the latest version with the UI included (choose your profile)
dsh plugin --profile web add dsh-project-mcp-manager@latest
dsh plugin --profile desktop add dsh-project-mcp-manager@latest
# For other profiles, e.g. headless, substitute the profile name.
# Pin a release version (check available versions with
# npm view dsh-project-mcp-manager versions; bundled UI needs the coordinated release)
dsh plugin --profile web add dsh-project-mcp-manager@<version>
Option 2: install directly with pnpm (equivalent to option 1):
# dshHome defaults to %USERPROFILE%\.dsh (uses $DSH_HOME if set)
cd $env:USERPROFILE\.dsh\profiles\web
pnpm add dsh-project-mcp-manager@latest
Option 3: local development install — build the checkout, then link only
its root manager package. The workspace dependency supplies the matching UI;
no separate UI link or bundle selection is needed:
# Run in the source checkout
pnpm install
pnpm run build # core first, then UI (host code + browser bundle)
dsh plugin --profile web add link:<path-to-your-dsh-mcp-project-source>
# e.g. link:D:\dev\dsh-mcp-project; use --profile desktop for the desktop app
The link points at the checkout; rebuild after source changes so the host sees
updated compiled output.
dsh ≥ 0.1.2 note: whether the plugin loads depends on the profile's
dsh.profile.bundles list, not just installed dependencies. The recommended
dsh plugin ... add command handles bundle reconcile. If you installed with
pnpm directly, run dsh plugin --profile web list once to trigger it (use
desktop for that profile). In the web or desktop app's Plugins page, inspect
the manager's two contained components. The desktop profile is managed by
Electron; do not use a desktop CLI --dump-config command to inspect it.
Select only dsh-project-mcp-manager, not its UI dependency.
Upgrading / pinning versions: re-run the add command from option 1 with
the desired version suffix — @latest upgrades to the newest release,
@<version> pins a specific release. For the bundled UI, choose the coordinated
manager/UI release described above. v0.10.x needs dsh 0.2.1-rc.5 or a later 0.2
release (engines.dsh: ^0.2.1-rc.5); v0.9.x needs 0.2.1-rc.2 through rc.4.
v0.7.x and v0.8.x need dsh 0.2.0-rc.2
(the 0.2.0 line). dsh 0.1.5 keeps working with plugin @0.6.0.
MCP Manager entry (included by default): the manager install above supplies
the UI automatically in web and desktop profiles. Open MCP Manager in the
left sidebar. The previous entry, Settings → Built-in plugins → Plugin: MCP
Manager, is kept for one release and renders the same page; it is planned for
removal in the next release. To hide both entries without stopping the loader,
turn off only the manager bundle's mcp-project-ui component in the plugin
manager's contained-components controls (or disable that row with a profile
patch override). Keep mcp-project enabled.
A headless profile installs the same bundle but has no browser page. Without
the dsh connection service, the UI registers no routes;
its connection integration waits without blocking the core loader or MCP
mounting.
Migrating from a separate UI install: after installing the manager bundle,
remove dsh-project-mcp-ui from the profile's bundle selection
(dsh.profile.bundles), keeping the manager selected. You may then remove the
direct UI dependency with dsh plugin --profile web remove dsh-project-mcp-ui
(use desktop for the desktop profile); the manager still supplies its own UI
dependency. Before running the new bundle, remove an old direct UI dependency
(recommended) or align it to the manager's exact version; it can take precedence
over transitive resolution. If the old UI bundle was disabled, transfer that
intent to a mcp-project-ui row override with disabled: true before loading
the manager. Neither old bundle selection nor direct dependencies are promised
to be removed automatically. Details: MCP Manager page.
Build & test
pnpm install
pnpm run build # core: tsc → lib/; then UI: tsc + browser bundle → packages/ui/lib/
pnpm test # node test/*.mjs (loader, CLI, and settings-page helpers)
How it works
- Project discovery: the
session.header.cwd of an active agent session,
plus the dsh process start directory → walk up to the nearest ancestor
containing .git as the project root (falls back to the directory itself
when there is no .git).
- Mounting: each
(project, serverName) pair in the project layers mounts
one @deepseek-ai/dsh-mcp-client instance (ctx.plugin) on the host ctx and
registers it into the global tool layer; multiple sessions inside the same
project share a single connection. Before a workspace is focused,
project-layer fibers are created only for projects with a live session or
the process cwd, and they unmount after a 5 minute grace. Once the user
focuses a workspace, only that project stays mounted and the others
unmount immediately, so a desktop workspace switch cannot leave another
project's tools in the global list. The catalog entry and watcher remain.
Every
user-layer row mounts exactly one instance (global, independent of the
number of projects) — see
configuration sources and layers.
- Hot reload: chokidar watches each known project root and its
.dsh
directory (depth 1). The allow-list is that root, .dsh, and the three
config files; every other path is ignored, so the watcher does not open
lib/, src/, or build/ and a Windows build can delete those directories
while dsh is running. Edits to the exact config files of known project
roots — <projectRoot>/.dsh/mcp.yml, <projectRoot>/.dsh/mcp.json and
<projectRoot>/.mcp.json — trigger a full reconciliation after a 150 ms
debounce: added rows are mounted, removed rows are unmounted, and config
changes are remounted. A second watcher covers the
user layer as exact file paths — ~/.dsh/mcp.yml,
~/.dsh/mcp.json, ~/.dsh/profiles/<active profile>/mcp.yml and
mcp.json, and
$DSH_HOME/dsh-mcp.json (the other plugin's global store; watched only so
creating it can be diagnosed, never mounted). chokidar v5 can deliver an
event for a watched missing file when it is created, as long as its parent
directory exists — never the home directory at large.
- Profile name resolution: derived from the loader root include's
config.path (~/.dsh/profiles/<name>/cordis.yml) or ctx.baseUrl, and
overridable with ; when it cannot be resolved the
profile layer is not read (the other layers still are).
Query surface
Other plugins and the companion UI read mount state from ctx.projectMcp
(snapshot, serverView, globalState, reload). Queries are the previous
reconcile's memory; they do not read disk. A successful reconcile emits
projectMcp/updated with no payload. The listener calls snapshot() and
diffs. The core loader does not open a browser SSE channel; the bundled UI
owns that channel. The same service has
the write methods the MCP Manager page uses (addServer, setServerEnabled,
removeServer, setToolEnabled); they only write a managed mcp.yml.
That surface — the methods, the event, and the view types re-exported from the
package entry (ProjectFileState, McpServerRuntimeView, McpServerView,
McpRowSource, and the types those views name) — follows semantic versioning
for the companion UI. The manager depends on dsh-project-mcp-ui at the
same exact version, and the UI peer-depends on dsh-project-mcp-manager
at that exact version (no ^ or ~ in either published contract). Both packages
must be versioned and released together; users install only the manager.
Details: query surface.
Security boundary
stdio lines in .dsh/mcp.yml, .dsh/mcp.json and .mcp.json spawn their
command inside the dsh host process — config files are executable code
carriers, so only add them in projects you trust. The user layers
(~/.dsh/mcp.yml, ~/.dsh/mcp.json, the profile yml and json) are executable code
carriers too, they just belong to your own machine: user-layer rows mount
globally (one host-level connection, visible to every project) and are no
longer fanned out per project. Lines that fail to mount or are invalid are
skipped with a warning and do not affect other servers. Claude user-state
monoliths such as ~/.claude.json (mixing credentials with project history)
are no longer read at all as of v0.4.0.
The MCP Manager page writes the same managed files, so adding a local-command
server there spawns that command on the host. Its routes
(/api/project-mcp/*) are served on the dsh connection: whoever can use the
dsh web GUI can add and start servers through it.
Coexistence with other MCP manager plugins
This plugin and @wingsky-1/dsh-mcp-manager both auto-load per-project MCP
servers, but they do not share a file format:
- Project files are mutually incompatible. This plugin reads
{ mcpServers: { … } } in <projectRoot>/.dsh/mcp.json. The other plugin
stores { version, servers: [] } at the same path. A missing mcpServers
key is a legal empty layer here, so the other format would otherwise look
like "I configured it but nothing happens". The loader now writes a
diagnostic naming that format and suggesting mcpServers or
.dsh/mcp.yml. The same hint applies to ~/.dsh/dsh-mcp.json. If that
file already uses this plugin's mcpServers dialect, the diagnostic tells
you to move the object into mcp.json — it is still not loaded from the
other plugin's filename.
- The same
serverName can be started twice (once by each plugin).
stdio servers may contend for ports or exclusive resources.
- Prefer one plugin per project, or keep this plugin on
.dsh/mcp.yml
and the other on .dsh/mcp.json.
globalNames() only sees official loader patch rows, not tools registered by
the other plugin at runtime, so rename-to-avoid-collision does not cover
that other instance.