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 rich projection: text blocks join with newlines; resource links keep their name and URI as text; supported image blocks become durable core image blocks only when ctx.attachments is mounted and the exact calling model route explicitly declares image input — the whole image batch is decoded and admitted before any member is saved, and a malformed/refused image batch, audio, embedded resources, and unsupported blocks become explicit diagnostic text rather than placeholders. Arguments, mapped text, and durable image references are retained until compaction; inline MCP base64 stays only in the execution-local canonical value and is never copied into a session event.
  • 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: apps/cli/config/examples/mcp-memory ​

The repo ships three default-off reference configurations under apps/cli/config/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 'apps/cli/config/examples/mcp-memory/<name>.cordis.yml' (the path may point to a copied file anywhere on disk; upstream's usage reference is docs/user/guide/mcp-memory.md). 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.
  • Image is the only durable rich-result bridge — PNG, JPEG, WebP, and GIF can enter Native context after exact capability proof; audio and embedded-resource payloads stay execution-local with explicit diagnostics, and resource links preserve only their name and URI as text. Unsupported MCP output schemas are not enforced (structuredContent falls 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 protocol — mcp__<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
  • apps/cli/config/examples/mcp-memory/ — the three memory-server reference configs
  • docs/user/guide/mcp-memory.md — upstream's usage guide for the three reference configs