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 settings page for the web and desktop apps. The UI is
enabled by default and can be disabled independently; the loader keeps working.
Capability boundary (dsh ≥ 0.2.1-rc.2): 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_directorydoes not retarget it. - The MCP file format, the
dsh-mcpCLI, and the settings 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.9.x targets the dsh 0.2 line starting at 0.2.1-rc.2 (engines.dsh is
^0.2.1-rc.2). 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.
- Settings 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, theprojectMcp/updatedevent, exported view types, and the semver contract for the companion UI.
Design and release records (Chinese): 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.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 · v0.4.2 release notes · v0.4.1 release notes · v0.4.0 release notes · v0.3.1 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 (settings 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
latesttag.
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.bundleslist, not just installed dependencies. The recommendeddsh plugin ... addcommand handles bundle reconcile. If you installed with pnpm directly, rundsh plugin --profile web listonce to trigger it (usedesktopfor 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-configcommand to inspect it. Select onlydsh-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.9.x needs dsh 0.2.1-rc.2 or a later 0.2
release (engines.dsh: ^0.2.1-rc.2). 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.
Settings page (included by default): the manager install above supplies
the UI automatically in web and desktop profiles. Open Settings → Built-in plugins →
Plugin: MCP Manager. To hide the page 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: settings 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.cwdof an active agent session, plus the dsh process start directory → walk up to the nearest ancestor containing.gitas 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-clientinstance (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
.dshdirectory (depth 1). The allow-list is that root,.dsh, and the three config files; every other path is ignored, so the watcher does not openlib/,src/, orbuild/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.jsonand<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.ymlandmcp.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) orctx.baseUrl, and overridable withDSH_MCP_PROFILE=<name>; when it cannot be resolved the profile layer is not read (the other layers still are). - Effective names: when the original
serverNameis unique across the whole catalog (host global rows + all project rows) it keeps its name; on a conflict project rows are renamed top<first 6 hex chars of sha256(project root)>_<original name>(truncated to 32 characters, deterministic and independent of mount order) to avoid the serverName reservation conflicts thatdsh-mcp-clientmakes per process root. Global rows (profile patch lines and user-layer rows) participate in occupancy determination but are never renamed. Model-visible tool names are built from the effective server name and the MCP tool's own name (mcp__<effectiveServerName>__<toolName>), which may differ from theserverNamewritten in the file. - Session visibility: when an agent is created, its session cwd resolves to
a project, and
tools.restrict({ deny })is applied to that agent to deny every project server except those of the session's own project, plus the global servers suppressed by the project's own rows; a session without a cwd falls back to the owner project (subagents), then to the project containing the dsh process cwd. Released when the session is destroyed.
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 settings 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 settings 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 missingmcpServerskey 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 suggestingmcpServersor.dsh/mcp.yml. The same hint applies to~/.dsh/dsh-mcp.json. If that file already uses this plugin'smcpServersdialect, the diagnostic tells you to move the object intomcp.json— it is still not loaded from the other plugin's filename. - The same
serverNamecan 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.ymland 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.
No comments yet. Be the first to write one.