DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-lazymcp

k

dsh-lazymcp

Discovered

Lazy MCP client for DeepSeek Harness: registers a server's tools from a cached manifest at boot and connects on the first call.

dsh-lazy-mcp

A lazy-loading MCP client plugin for DeepSeek Harness.

It is a client only. It ships no MCP server and bundles no server code: the processes it starts are the ones you configure, stdio or streamable HTTP, exactly as you would run them by hand.

The shipped @deepseek-ai/dsh-mcp-client connects to every configured server and discovers its tools during plugin activation. With several servers (or a large one such as godot-mcp) that makes every harness start wait for all of them, even when the session never uses those tools.

dsh-lazy-mcp keeps the same model-facing tool names (mcp__<server>__<tool>) but moves the connection to the first tool call:

StartupWhat happens
First ever start (no cache)bootstrap: eager connects once, registers the tools and writes the manifest. background does the same without blocking boot.
Every later startReads the manifest from disk, registers the same tools/schemas, and starts no process and no socket.
First mcp__… callConnects, refreshes the manifest, swaps stale registrations, then forwards the call.
MCP resource requestConnects on demand; resources stay lazy as well.

The same row can mount many servers. A hub row reads the dsh-lazy-mcp registry and the MCP config files other clients already use (.mcp.json, ~/.claude.json, ~/.cursor/mcp.json, …), then gives each server it finds the same lazy treatment. See One row, many servers.

Measured against godot-mcp (14 tools, ~5 KB of schemas):

  • first run, connect + tools/list + cache write: ~180–220 ms
  • cached start, 14 tools registered, server not started: ~0.5 ms
  • real DSH boot with a warm cache: no [SERVER] Using Godot… line, i.e. the MCP process was never spawned

Install

Install it from the dshmarket catalog: open Plugins → Market, search for dsh-lazy-mcp, and click Install on the kvnis/dsh-lazy-mcp entry. The catalog also carries an unrelated plugin by the same name — installing both collides on the package name, so pick this repository's entry. The market pins the current commit, so the install is reproducible and later updates are offered in the same list.

Without the catalog — or before the entry is accepted — add the GitHub source directly:

dsh plugin --profile main add github:kvnis/dsh-lazy-mcp

Electron app (owns the desktop profile): Plugins → Add plugin → Install a third-party plugin, paste github:kvnis/dsh-lazy-mcp, install, then switch the bundle on if it is not already enabled. The CLI refuses dsh plugin --profile desktop … with “profile desktop is managed exclusively by the Electron application”.

For development, a checkout can be installed instead (git clone https://github.com/kvnis/dsh-lazy-mcp ~/GitHub/dsh-lazy-mcp && npm ci --omit=dev, then add ~/GitHub/dsh-lazy-mcp as the plugin). That produces a link: dependency, which the market lists but does not offer updates for.

The Plugins page lists profile bundles, not individual MCP rows: the package shows up under Installed plugins as dsh-lazy-mcp, and the example mcp-lazy-example row stays disabled until you enable it, so installing starts nothing.

The package declares a bundle patch with one disabled example row, so installing it does not start anything. Do not keep an existing @deepseek-ai/dsh-mcp-client row for the same serverName: both would register the same mcp__<server>__<tool> names. Remove or disable the eager row, then add a row to the active profile's cordis.patch.yml (~/.dsh/profiles/desktop/cordis.patch.yml for the Electron app, ~/.dsh/profiles/main/cordis.patch.yml for the CLI profile). Because a patch replaces the whole config, include it. One row per server — and write it as an insert, because this bundle ships only a disabled example row, not a row named after your server:

- insert:
    - id: mcp-fs-lazy
      name: dsh-lazy-mcp
      config:
        serverName: fs          # tool prefix: mcp__fs__<tool>
        transport: stdio
        command: /usr/bin/node          # absolute path, not `node` — see below
        args:
          - /home/you/mcp/my-server/build/index.js
        # cwd, env: whatever that server needs
        bootstrap: eager

A bare - id: entry is an override, not an insert: it only reconfigures a row some bundle already defines, and an id that matches nothing is warned about and skipped — the usual way to end up with zero tools and no error. Wrap rows you are adding in - insert:; the HTTP example below does the same.

That command/args pair is a server you already have — this plugin spawns it lazily and caches its schemas, nothing more. Any stdio MCP server works. For a streamable-HTTP server, use url/headers instead:

- insert:
    - id: mcp-remote-lazy
      name: dsh-lazy-mcp
      config:
        serverName: remote
        transport: streamable-http
        url: https://example.com/mcp
        headers:
          Authorization: !!js `Bearer ${process.env.REMOTE_MCP_TOKEN}`

Skip this per-server row if you use hub mode: one hub row can mount this server and every other one already declared in another client's config.

The row is spawned, not shell-expanded: command, cwd and every args entry must be absolute paths — ~, $HOME and other variables are passed through verbatim, and so are env values. A bare command: node resolves against the PATH of whatever launched the harness, which a desktop launcher usually trims to /usr/bin:/bin; use what command -v node prints.

env entries are merged over the scrubbed parent environment and are part of the cache key, so editing them re-discovers the tool list on the next start.

One row, many servers (hub mode)

A row that names no server of its own becomes a hub: it mounts every MCP server it can find, one lazy cache per server, and keeps the set current while the harness runs. Tool names still come from each server, so they stay mcp__<name>__<tool>.

The registry is the file this plugin owns — ~/.dsh/lazy-mcp/servers.json, or $DSH_HOME/lazy-mcp/servers.json when DSH_HOME is set:

{
  "servers": {
    "fs": { "command": "/usr/bin/node", "args": ["/home/you/mcp/fs/build/index.js"] },
    "remote": { "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer …" } },
    "old": { "disabled": true, "command": "/usr/bin/node" }
  }
}

{"mcpServers": {…}} and an array of entries carrying a name are accepted too, so most existing MCP config files can be copied in unchanged. An entry takes the connection fields a row takes (command, args, env, cwd, url, headers); disabled: true or enabled: false skips it without deleting the row.

By default the hub also reads config files other MCP clients already maintain:

SourceWhat is readSwitch
Registry~/.dsh/lazy-mcp/servers.jsondiscoverRegistry
Workspace.mcp.json, .cursor/mcp.json, .vscode/mcp.jsondiscoverProject
Claude Desktopclaude_desktop_config.jsondiscoverClaude
Claude Code~/.claude.jsondiscoverClaudeCode
Cursor~/.cursor/mcp.jsondiscoverCursor
VS Code~/.config/Code/User/mcp.jsondiscoverVscode
- id: mcp-hub
  disabled: false
  config:
    discovery: true        # only needed when this row also names a server
    bootstrap: eager       # first run connects once per server, then caches
    schemaMode: compact
    skip: old-experiment   # names that must never mount, comma-separated
    discoverProject: false # do not trust per-repository files here

Discovery defaults to on for a row that names no server and off for one that does (a legacy command/url row or one with servers[]), so an existing single-server config never grows surprise mounts. Set discovery: true to combine an explicit list with discovery.

Hub-level bootstrap, schemaMode, tools, escapeHatch, cache, cacheDir, toolCallTimeoutMs, maxInstructionBytes and failOnStartupError are inherited by every server the hub mounts; a servers[] entry can override any of them as well as its connection fields. An explicit servers[] entry wins over a discovered server of the same name; skip wins over everything. The registry wins over the other files.

While running, the hub re-reads the sources every three seconds: a new server is mounted, a changed command/args/env remounts it, and a removed or disabled one has its tools unregistered. Caches are keyed by server identity, so a remount with an unchanged key restores from cache without connecting, and a server added later is cached on its first call. Removing an entry unregisters its tools but leaves its cache file on disk.

Example: a large server

The numbers at the top were measured on the largest server this was tested against — godot-mcp, 157 tools. It is a normal third-party MCP server; nothing of it ships in this plugin, and the row below is only a worked example of a heavy stdio server.

Prerequisites are that server's own: Godot 4.4 or later (godot --version) and Node 18 or later. Build a checkout (the directory is yours; the row below assumes ~/mcp/godot-mcp):

# 157-tool fork — what this README measures
git clone https://github.com/tugcantopaloglu/godot-mcp.git ~/mcp/godot-mcp
# or the 14-tool upstream, instead:
# git clone https://github.com/Coding-Solo/godot-mcp.git ~/mcp/godot-mcp
cd ~/mcp/godot-mcp && npm install && npm run build
- id: mcp-godot-lazy        # any id; this one is just the example's
  disabled: false
  config:
    serverName: godot
    transport: stdio
    command: /usr/bin/node          # absolute path, not `node`
    args:
      - /home/you/mcp/godot-mcp/build/index.js
    cwd: /home/you/mcp/godot-mcp
    env:
      GODOT_PATH: /home/you/bin/godot
    toolCallTimeoutMs: 90000
    bootstrap: eager

GODOT_PATH is that server's own convention for finding the engine binary, not a plugin setting. The first start connects once and writes ~/.dsh/lazy-mcp/godot.json; every start after that is lazy.

What success looks like

You brought a server; this section is how you tell the plugin is serving it rather than getting in the way. After a restart, all three should hold:

  1. Tools are listed. The tool panel shows mcp__<serverName>__* — 157 of them in the example above, your server's own count otherwise.
  2. The cache exists. ~/.dsh/lazy-mcp/<serverName>.json holds the tools/list result, mode 0600.
  3. Nothing started early. No process for that server was spawned at boot. The first mcp__<serverName>__… call starts it, and it stays connected.

With hub mode the same three checks apply per discovered server: one cache file per server keeps appearing as it is used, and none of them run until a tool from that server is called.

From there the tools behave like any MCP tools: whatever arguments the server expects is what you pass, and nothing about them is specific to this plugin. For a server that work is scoped by directory, naming the directory once is enough:

In /home/you/games/platformer, add a Player node to res://scenes/main.tscn and save the scene. Then run the project and show me the last 20 lines of output.

Configuration

FieldDefaultMeaning
serverNamerequired[A-Za-z0-9_-]{1,32}; prefixes every tool name
transportrequiredstdio or streamable-http
command, args, env, cwd—stdio server; environment is the scrubbed parent env plus env
url, headers—streamable-HTTP server
cachetrueset false to disable the manifest and always bootstrap eagerly
cacheFile<cacheDir>/<serverName>.jsonexplicit manifest path
cacheDir$DSH_HOME/lazy-mcpmanifest directory
bootstrapeagerwhat to do when no manifest exists: eager, background, or off
failOnStartupErrortruefail activation when the eager first-run bootstrap fails
toolCallTimeoutMs60000per tools/call and resource request
maxInstructionBytes32768reject oversized server instructions
toolsallallowlist of exact MCP tool names to expose; unlisted tools stay reachable through the escape hatch
escapeHatchtruewith tools, also register mcp__<server>__call to invoke any tool by name
schemaModefullcompact strips schema titles, property descriptions, defaults, examples and bounds, and collapses descriptions to one line

Hub mode adds these:

FieldDefaultMeaning
servers—explicit hub list: [{name, command, args, …}]
discoveryon when the row names no serverread the hub registry and the config files below
discoverRegistry, discoverProject, discoverClaude, discoverCursor, discoverVscode, discoverClaudeCodetrueper-source switches, see Hub mode
registryFile$DSH_HOME/lazy-mcp/servers.jsonhub registry path
skip—comma-separated server names that must never mount

A manifest is invalidated automatically when the server name, transport, command/args, cwd, URL, or the set of env/header names changes.

Token budget profiles

Every registered tool costs its name, description and input schema on each model request, whether or not it is called. A server with 157 tools can spend tens of thousands of tokens per turn doing nothing. Two independent levers shrink that:

  • tools registers only the listed tools. Point it at the handful a task actually needs.
  • schemaMode: compact keeps the same tools but trims the schema text roughly in half.

Measured against the 157-tool Godot server: the full profile registers 79,812 characters of name + description + input schema (~20k tokens); compact registers 48,050 (~12k), a 40 % cut with the same 157 callable tools. The allowlist is the bigger lever — an unregistered tool costs nothing at all.

- id: mcp-godot-lazy
  disabled: false
  config:
    serverName: godot
    transport: stdio
    command: /usr/bin/node
    args: [/home/you/mcp/godot-mcp/build/index.js]
    cwd: /home/you/mcp/godot-mcp
    tools:
      - launch_editor
      - run_project
      - get_debug_output
      - stop_project
      - get_scene_tree
      - game_screenshot
      - game_get_logs
    schemaMode: compact
    escapeHatch: true

Unlisted tools stay reachable through one meta-tool, mcp__godot__call, so a narrowed profile is not a dead end — the model can still run any tool by name when the task turns out to need it:

{ "name": "game_set_camera", "arguments": { "position": { "x": 0, "y": 2, "z": 6 } } }

The escape hatch costs one small schema instead of one per tool. Set escapeHatch: false for a hard allowlist. Tool names are matched exactly; get the real list from ~/.dsh/lazy-mcp/<serverName>.json after a discovery, or from the harness tool panel.

Because tools and schemaMode only change how cached tools are presented, they do not invalidate the manifest: editing them costs a restart, not a reconnect.

How it works

  • Manifest cache. After a successful tools/list, the raw tools (name, description, input schema, output schema) and server instructions are written atomically with mode 0600. On the next activation these are registered as thin proxies.
  • Proxy execution. Each proxy waits for one shared connection attempt, then calls tools/call on the real server. A closed transport clears the client, so the next call reconnects and re-syncs.
  • Live refresh. A tools/list_changed notification schedules a serialized re-sync; registrations are added, replaced, or removed before the next model request. The manifest is rewritten.
  • Resources and instructions. Instructions are served from the cache until a connection refreshes them. The MCP resource provider connects only when a resource is requested.
  • Hub reconcile. A hub row re-reads the registry and discovery files every three seconds and diffs the desired set against the mounted one: new servers mount, changed identities remount, removed ones unmount. The diff is serialized, and a malformed entry is logged and skipped without affecting its neighbours.
  • Failure containment. A tool whose schema is rejected by the harness is skipped with a warning instead of failing the whole server. A failed cached-start call leaves the cached tools registered for the next attempt.

Troubleshooting

initial connection or tool discovery failed ... Connection closed on first enable almost always means the configured command/args do not start the server. Check that args[0] is the built build/index.js and that the path is absolute. The current version names a missing absolute entry directly; restart the harness (or toggle the component) after fixing the config.

Nothing appears and the log has no server line — check the row itself: the shipped example is disabled: true on purpose, and a row added to a profile other than the one running (main while the app runs desktop) never loads.

Tools vanish after upgrading the package — bundle row ids are owned by the package, and an upgrade may rename or drop the row an override was written against. For example, 0.2.0 shipped an mcp-godot-lazy row that 0.3.0 replaced with a disabled mcp-lazy-example row. An id-targeted override for an id no bundle defines anymore is warned about and skipped, so nothing mounts and no server starts. Rewrite the entry as an - insert: row (see Install) — that both fixes the override and survives the next id rename.

A tool returns nothing though the server works — arguments are passed to the server verbatim; this plugin does not normalize them. Server-specific quirks still apply, e.g. a Godot list_project_files extension filter needs dot-prefixed values ([".tscn"], not ["tscn"]); the wrong spelling returns an empty list rather than an error. Check the server's own docs when in doubt.

spawn ENOENT, or the server exits immediately — command is not on the app's PATH. Put in the absolute path that command -v node prints, and use absolute paths for args/cwd while you are there.

The first successful connection writes ~/.dsh/lazy-mcp/<serverName>.json. To force a fresh discovery, delete that file and restart. With DSH_LAZY_MCP_DEBUG=1, the plugin writes activation decisions to stderr.

Deliberate limits

  • First run is eager by default. A lazy client cannot know tool schemas before connecting, so the manifest has to come from somewhere. Use bootstrap: background to avoid even that first-run wait, at the cost of tools appearing shortly after boot.
  • Cached schemas can be stale. They are refreshed on the first call or on tools/list_changed; a tool removed upstream may fail once before the refresh lands.
  • No reconnect supervisor. Unlike the shipped client, this plugin does not keep retrying in the background. A dropped connection is retried by the next tool call.
  • Image/audio MCP blocks become text markers. The shipped client stores them as durable attachments; this plugin's smaller adapter does not.
  • Discovery is opt-in for explicit rows. A legacy command/url row does not start reading ~/.claude.json behind your back; hub mode is the default only for a row that names no server.
  • Discovery files are metadata, not a transport. An entry that uses an unsupported transport or a missing command is skipped with a log line; env values and args are passed through verbatim, never shell-expanded.
  • A hub polls instead of watching. Changes appear within three seconds, not instantly, and a tools/list_changed push still refreshes schemas on the usual path.
  • No per-server duplicate-name reservation across agent scopes. Registering two instances with the same serverName in one composition produces a tool-name conflict warning; use distinct names.
  • Task-based tool execution is rejected, matching the shipped bridge's limitation.

Verification

There is a dependency-free test suite:

npm test                         # node test/hub.test.mjs

It runs the real apply() path against a throwaway stdio MCP server (test/fixtures/fake-server.mjs) and covers both shapes: single-server allowlists and the escape hatch, schemaMode: compact, manifest writes, hub discovery from the registry, cached restores without a process, first-call connect/forward, live add/change/disable remounts, malformed entries, duplicate filtering, and legacy-config compatibility. Each phase builds its own fake ctx, so the suite also checks the tear-down path.

For a syntax/export check without running anything:

node --check index.js
node -e "import('./index.js').then(m => console.log(Object.keys(m)))"
                                 # Config, apply, normalizeConfig, normalizeHubConfig, compactSchema, compactText

For a real DSH boot check, run the CLI with a profile that has the row configured:

# cold: connects once, writes the cache
DSH_LAZY_MCP_DEBUG=1 node apps/cli/lib/bin.js --profile main
# warm: logs "157 tools restored from cache" and no godot server output
DSH_LAZY_MCP_DEBUG=1 node apps/cli/lib/bin.js --profile main

DSH_LAZY_MCP_DEBUG=1 writes activation decisions to stderr; it does not change behavior.

Comments

Loading…

Similar plugins

dsh-mcp-lazy

by wings1848

Lazy MCP gateway for DeepSeek Harness: one stable proxy tool instead of N tool schemas, servers connect on first use and idle out, metadata cached to disk.

Manifest valid

★ 11

MIT

TypeScript

Oct 28, 2026

dsh plugin --profile web add dsh-mcp-lazy

by leaforbook

Lazy on-demand MCP bridge for DeepSeek Harness

Tools & CapabilitiesManifest valid

★ 7

MIT

JavaScript

Aug 20, 2026

dsh plugin --profile web add @yilinxiao/dsh-mcp-lazy

by zhe97396-tech

MCP gateway for dsh that connects each server on first use and disconnects it after an idle window; tool lists are cached on disk, existing mcpServers files can be imported, and child-process environm

Tools & CapabilitiesModels & ProvidersManifest valid

★ 1

MIT

JavaScript

Oct 19, 2026

dsh plugin --profile web add dsh-lazy-mcp

by MikotoMyWife

Lazy-loading MCP tools for DeepSeek Harness (DSH): one loader tool per multi-tool MCP server, per-agent tool masking, description presets

Manifest valid

★ 0

MIT

TypeScript

Oct 8, 2026

dsh plugin --profile web add dsh-mcp-loader

by CoderKuo

Manage the MCP servers of a DeepSeek Harness profile: status, switch, edit, export; add from the official MCP Registry, by hand (tested first), or with AI; find local software that can be connected

Terminal & ClientsManifest valid

★ 3

↓ 101/wk

MIT

JavaScript

Oct 14, 2026

dsh plugin --profile web add dsh-mcp-console

by fatefgo2050

Scan and display locally registered MCP tools in the DeepSeek Harness settings page.

Terminal & ClientsManifest valid

★ 0

MIT

JavaScript

Sep 21, 2026

dsh plugin --profile web add dsh-mcp-scanner