Skip to content

The Model Context Protocol (MCP) lets dsh talk to external tool servers without writing a per-server adapter. In dsh this is a single package, @deepseek-ai/dsh-mcp-client (packages/mcp/mcp-client), a bridge plugin: it connects to one MCP server, discovers the server's tools, and registers each as a native dsh tool under a server-qualified name. One plugin instance per MCP server, configured in cordis.yml.

FieldVersion / role
packages/mcp/mcp-clientMCP client bridge

The official reference lives at packages/mcp/mcp-client/README.md; there is no dedicated subsystem page, so this page is sourced directly from that README and packages/mcp/mcp-client/src/*.

How MCP tools surface

Every MCP tool gets two names:

  • the raw MCP name — sent on the wire in tools/call;
  • the public name mcp__<serverName>__<rawName> — registered on ctx.tools and what the model sees.

Public names are normalized to the DeepSeek function-name contract (64 chars, [A-Za-z0-9_-]); when normalization changes the name, a deterministic 12-hex-char hash of (serverName, rawName) is appended so distinct tools never collapse into one. Names are a pure function of (serverName, rawName) — connection order, re-syncs, and other servers never rename a tool. This is the same shape Claude Code and Codex use, e.g. mcp__github__create_issue, mcp__web__search.

On connect, plugin activation awaits listTools() and registers every advertised tool via ctx.tools.register() before the composition starts its first turn. The lifecycle in the tool registry: each MCP tool is a normal ToolDefinition whose execute calls client.callTool({ name: rawName, arguments }, { signal }) with timeout + abort support — the public name is never sent to the server.

Transport & configuration

Two transports. Both spawn/originate a real MCP client over the Model Context Protocol:

yaml
- id: mcp-github
  name: '@deepseek-ai/dsh-mcp-client'
  config:
    serverName: github
    transport: stdio
    command: npx
    args: ['-y', '@modelcontextprotocol/server-github']
    env: { GITHUB_TOKEN: '!!js process.env.GITHUB_TOKEN' }

- id: mcp-web
  name: '@deepseek-ai/dsh-mcp-client'
  config:
    serverName: web
    transport: streamable-http
    url: 'http://localhost:3000/mcp'
    headers: { Authorization: '!!js `Bearer ${process.env.MCP_TOKEN}`' }
Config fieldApplied toRequiredNotes
transportbothyes"stdio" or "streamable-http"
serverNamebothyesnamespace for tool names; [A-Za-z0-9_-]{1,32}, unique across live instances
commandstdioyesexecutable to spawn
args / env / cwdstdionoargs passed without shell; scrubbed ambient env + env overrides; working directory
urlhttpyesMCP server URL
headershttpnoextra HTTP headers (e.g. auth tokens)
toolCallTimeoutMsbothnoper callTool timeout (default 60000)
failOnStartupErrorbothnoreject activation if initial connect/sync fails (default false)
reconnect.enabled / initialDelayMs / maxDelayMs / maxAttemptsbothnoexponential-backoff reconnect: 500 → 30000 ms ceiling, 10 attempts

The stdio spawn deliberately screens environment: ambient variables whose names usually identify credentials and all DSH_* variables are removed before launch; other ambient variables remain inherited.

Lifecycle, re-sync and reconnect

  • Discovery: initial listTools() gates activation; failure is logged and, unless failOnStartupError, the plugin activates with no tools.
  • Re-sync: the client listens for notifications/tools/list_changed and re-syncs. A fetch-phase failure keeps the previous generation registered; a registration conflict rolls back the attempted generation and leaves no tools from that server.
  • Reconnect: on disconnect/crash, a supervisor restarts the original server config with exponential backoff (initialDelayMs doubling to maxDelayMs) and re-runs discovery on success — the recovered generation replaces the previous one, so tools neither duplicate nor leak. During the outage the last good generation stays registered (calls fail until recovery). Reconnection is budgeted per outage: after maxAttempts consecutive failures the server's tools are unregistered and reconnection stops until an HMR reload or Host restart; a connection that survives past maxDelayMs resets the budget.
  • HMR: editing the entry triggers disconnect + reconnect without process restart; an unchanged serverName reproduces identical tool names.

Model-facing behavior

  • Tool result: canonical success is { content: JsonValue[], structuredContent? }; complete JSON blocks survive for programmatic callers. When an advertised outputSchema is supported, structuredContent is validated against it; unsupported schema vocabulary falls back to unconstrained JsonValue. isError rejects the call through the registry's error path.
  • Native text projection: text blocks join with newlines; image, audio, resource, and unsupported blocks become short placeholders (their full JSON blocks remain in the execution-local value). Arguments and mapped text are retained until compaction; binary/resource payloads are discarded rather than added to context.
  • KV cache: prefix-stable while the discovered set/schemas are unchanged; a re-sync that changes a tool replaces definitions and may invalidate reuse from the first changed schema. Recovering an unchanged list stays prefix-stable.

Examples: examples/mcp-memory

The repo ships three default-off reference configurations under examples/mcp-memory/ connecting third-party memory servers to dsh via this client:

FileSystemTransport
memorix.cordis.ymlMemorixstdio
mcp-reference-memory.cordis.ymlMCP Reference Memorystdio
engram.cordis.ymlEngramstdio

Enable one with dsh web --patch 'examples/…/<name>.cordis.yml'. dsh parses the overlay, starts the stdio command (or connects to the URL), discovers tools, and exposes them as mcp__<serverName>__<tool>. dsh does not download the server, initialize its database, choose the model, or supervise a separate HTTP service; for stdio the generic client launches and stops the child with the dsh plugin lifecycle.

Known limitations

  • Tools are the only bridged MCP capability — Resources and Prompts have no harness consumer and are deferred.
  • Startup timeout is inherited from the MCP SDK — dsh does not expose its own connect/discovery timeout; each initialize or paginated tools/list uses the SDK's 60-second default.
  • Reconnect triggers on transport close — HTTP failures surface per request through the SDK's SSE-stream recovery rather than being respawned by the supervisor.
  • Native non-text rendering is lossy and unsupported MCP output schemas are not enforced (fall back to JsonValue).

Further reading

  • Tool registry & execution pipeline — where registered MCP tools run
  • Settings & configuration — how cordis.yml/cordis.patch.yml configures plugins
  • SDK protocolmcp__<server>__<tool> name normalization for programmatic callers
  • packages/mcp/mcp-client/README.md — the bridge plugin's full behavior and config
  • packages/mcp/mcp-client/src/connection.ts — connection supervisor and reconnect
  • examples/mcp-memory/README.md — the three memory-server reference configs