DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

anthonyyu-verkada /

anthonyyu-verkada/dsh-mcp-client-plus

Verified

DeepSeek Harness MCP client bridge with OAuth 2.1, bounded connect/discovery timeouts, environment-indirected secrets, optional indefinite reconnection, and a mcp_status diagnostic tool

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@b368d5af

dsh-mcp-client-plus

An MCP client bridge for DeepSeek Harness (dsh), forked from the built-in @deepseek-ai/dsh-mcp-client and extended with the capabilities its own documentation lists as deferred.

Everything the upstream bridge does, it still does: one plugin instance per MCP server, tools registered as native mcp__<serverName>__<rawName> tools, serverName namespacing, tool-list-change refresh, and reconnect with exponential backoff.

What it adds:

Capability Upstream
1 OAuth 2.1 — discovery, RFC 7591 dynamic client registration, PKCE, refresh rotation, browser redirect static headers only
2 connectTimeoutMs — bounds initialize inherits the SDK's 60s default
3 discoveryTimeoutMs — bounds each tools/list page passes no timeout at all
4 Secrets by environment — tokenEnv, headerEnv raw values inlined in cordis.yml
5 maxAttempts: 0 — reconnect indefinitely gives up permanently after 10
6 mcp_status tool — live per-server state from inside a session log output only

Why these six

These are not speculative features. Each one maps to a concrete failure of the upstream bridge, and the first five were reproduced against a real harness before this fork was written.

The upstream bridge can give up permanently and never recover. On a startup failure it retries with backoff, then unregisters and logs giving up after 10 consecutive failed reconnect attempts — tools unregistered; reload the plugin or restart the Host to reconnect. Nothing self-heals, and the failure is visible only in host stderr. In the harness this was diagnosed on, five OAuth-protected servers were dead this way — valid credentials, reachable endpoints, zero tools — while the two servers without an Authorization header worked fine.

OAuth is the root cause of most of that. Because upstream accepts only static headers, an OAuth server has to be fed a token by an out-of-band bridge — commonly a config expression that shells out to another agent's credential store on every load. That path is fragile (a Keychain ACL denial is silently swallowed into an empty Bearer header), and it cannot refresh. auth: oauth removes the bridge entirely: the SDK performs discovery, registration, and refresh, and the first connect prints an authorization URL.

Timeouts were unbounded in two places. listToolsUncached passed no RequestOptions at all, so every tools/list page waited the SDK's 60-second default, and connect() did the same for initialize. A single unresponsive server could stall plugin activation for minutes. Both are now explicit and configurable.

A dead server is invisible from a session. mcp_status reports every configured server, its state, registered tool count, retry position, and last error — so "why is this tool missing" is answerable without reading host logs.


Install

npx -p @deepseek-ai/dsh dsh plugin --profile web add github:anthonyyu-verkada/dsh-mcp-client-plus

Then declare your servers in your profile patch (~/.dsh/profiles/web/cordis.patch.yml) and restart dsh.

The bundled cordis.patch.yml is intentionally empty: the bridge is one instance per server, so a packaged patch cannot know your servers. See the comments in that file for copy-pasteable row templates.

Quick start

OAuth-protected remote server

- insert:
    - id: mcp-linear
      name: 'dsh-mcp-client-plus'
      config:
        transport: streamable-http
        serverName: linear
        url: https://mcp.linear.app/mcp
        auth: oauth
        reconnect:
          maxAttempts: 0

On first connect the host logs an authorization URL and opens your browser. After consent the loopback listener captures the code, tokens are stored, and the tools register. Tokens refresh automatically, and rotation is persisted.

Credentials live in $DSH_HOME/.dsh/mcp-client-plus-oauth.json, mode 0600, keyed by resource-server URL. Treat that file as a secret.

Token from the environment

- insert:
    - id: mcp-tracecat
      name: 'dsh-mcp-client-plus'
      config:
        transport: streamable-http
        serverName: tracecat
        url: https://tracecat.example.com/mcp
        auth: bearer
        tokenEnv: TRACECAT_MCP_TOKEN

The token itself never appears in config. auth: none sends no Authorization header at all.

Local stdio server

- insert:
    - id: mcp-terraform
      name: 'dsh-mcp-client-plus'
      config:
        transport: stdio
        serverName: terraform
        command: /Users/you/go/bin/terraform-mcp-server
        args: [stdio]
        env:
          TFE_ADDRESS: https://app.terraform.io
          TFE_TOKEN: ''

Migrating from the built-in client

Replace the name on each existing row and drop any header-expression bridge:

     - id: mcp-linear
-      name: '@deepseek-ai/dsh-mcp-client'
+      name: 'dsh-mcp-client-plus'
       config:
         transport: streamable-http
         serverName: linear
         url: https://mcp.linear.app/mcp
-        headers:
-          Authorization: !!js >-
-            (function () { try { return 'Bearer ' + ...keychain read... } catch (e) { return ''; } })()
+        auth: oauth

Do not run both bridges for the same serverName: each holds its own namespace reservation, and the second registration fails on a duplicate public tool name.

Config reference

Fields common to both transports:

Field Default Meaning
serverName required Namespace for mcp__<serverName>__*; [A-Za-z0-9_-]{1,32}, unique among live instances
connectTimeoutMs 15000 Bound on one connection attempt
discoveryTimeoutMs 30000 Bound on one tools/list page
toolCallTimeoutMs 60000 Bound on one tool call
failOnStartupError false Reject plugin activation when the first attempt fails
reconnect.enabled true Reconnect after a lost connection
reconnect.initialDelayMs 500 First backoff delay; doubles per consecutive failure
reconnect.maxDelayMs 30000 Backoff ceiling, and the uptime that resets the outage budget
reconnect.maxAttempts 10 Per-outage ceiling. 0 retries indefinitely

transport: streamable-http only:

Field Default Meaning
url required MCP endpoint
auth none none, bearer, or oauth
headers {} Literal header values
headerEnv {} Header name → environment variable name
tokenEnv — Environment variable holding the bearer token
oauth.scopes — Scopes to request
oauth.callbackPath /callback Loopback redirect path
oauth.authorizationTimeoutMs 300000 How long to wait for the human
oauth.openBrowser true Launch the platform browser
oauth.storePath $DSH_HOME/.dsh/mcp-client-plus-oauth.json Credential store

For headless or remote hosts, set oauth.openBrowser: false — the authorization URL is always logged, so you can open it wherever your browser lives. The authorization server must accept a http://127.0.0.1:<port>/... loopback redirect, which the MCP specification requires for local clients.

The mcp_status tool

One mcp_status tool is registered per scope, reporting every server:

linear [connected] · 42 tools · auth=oauth · streamable-http
hex [failed] · 0 tools · auth=oauth · streamable-http · last error: giving up after 10 consecutive failed reconnect attempts …

Pass { "server": "hex" } to filter, or call it with no arguments to list all.

Compatibility

  • DeepSeek Harness >=0.1.5-rc.1 <0.2.0. Tested on 0.1.5-rc.2.
  • Node ^22.19.0 || >=24.0.0.
  • Declares no npm dependencies or peerDependencies, matching the convention used by other dsh plugins: the harness's own @deepseek-ai/* packages and @modelcontextprotocol/sdk resolve from the profile's node_modules.
  • Upstream master (0.1.6-alpha.2) moved to @modelcontextprotocol/client v2 and requires @deepseek-ai/dsh-mcp-resources; this fork is based on dsh-v0.1.5-rc.2. See PROVENANCE.md.

Relationship to dsh-mcp-manager

hyqhyq3/dsh-mcp-manager is a fuller-featured alternative and a good choice if you want a GUI: a Settings → MCP page, workspace-scoped servers, and an on-demand tool broker. This fork stays a drop-in replacement for the built-in bridge instead — same one-row-per-server composition model, same tool names, no client half and no GUI.

Development

npm run build      # tsc -> lib/  (lib/ is committed: dsh loads it directly)
npm test           # build first; runs node --test over tests/**/*.test.js

Because the plugin declares no dependencies, build and test resolve @deepseek-ai/* and @modelcontextprotocol/sdk from a harness install. Point node_modules at one before building:

ln -s "$(npm root -g)/@deepseek-ai/dsh/node_modules" node_modules

51 tests across three layers:

  • Unit — the config and reconnect policy (including maxAttempts: 0), header and secret resolution, the OAuth provider's persistence and credential invalidation, and the loopback redirect listener driven over real HTTP.
  • Transport end-to-end — a real MCP server over stdio through the transport factory: connect, tools/list, and tools/call. Plus an assertion that an unresponsive server fails inside connectTimeoutMs rather than the 60-second SDK default, and that bearer and headerEnv credentials reach the wire.
  • Harness integration — apply() mounted on a real Cordis context against a real ctx.tools registry: tool registration and public naming, mcp_status reporting and rendering, disposal unregistering everything, duplicate serverName rejection, and failOnStartupError rejecting activation.

Known limitations

  • Tools only. MCP resources and prompts are not bridged, same as upstream.
  • One authorization per process for a new server. The redirect is received by a loopback listener owned by the plugin instance; if the host is headless, you complete the flow in whatever browser can reach the printed URL.
  • OAuth tokens are stored in plaintext in a 0600 JSON file. Static bearer tokens are read from the environment and never persisted.
  • connectTimeoutMs bounds initialize, not the whole TCP/TLS establishment; a hung DNS lookup may still take longer before the request timer applies.
  • maxAttempts defaults to 10, unchanged from upstream. Set 0 for the retry-forever behaviour; the default was left alone deliberately so the fork does not silently change an existing deployment's resource profile.

License

MIT. Portions derived from DeepSeek Harness (packages/mcp/mcp-client, MIT) at dsh-v0.1.5-rc.2. See LICENSE and PROVENANCE.md.

—/ 5

No ratings yet

Verified DSH bundle

Commit b368d5af9104

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout