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.
| Field | Version / role |
|---|---|
packages/mcp/mcp-client | MCP 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 onctx.toolsand 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:
- 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 field | Applied to | Required | Notes |
|---|---|---|---|
transport | both | yes | "stdio" or "streamable-http" |
serverName | both | yes | namespace for tool names; [A-Za-z0-9_-]{1,32}, unique across live instances |
command | stdio | yes | executable to spawn |
args / env / cwd | stdio | no | args passed without shell; scrubbed ambient env + env overrides; working directory |
url | http | yes | MCP server URL |
headers | http | no | extra HTTP headers (e.g. auth tokens) |
toolCallTimeoutMs | both | no | per callTool timeout (default 60000) |
failOnStartupError | both | no | reject activation if initial connect/sync fails (default false) |
reconnect.enabled / initialDelayMs / maxDelayMs / maxAttempts | both | no | exponential-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, unlessfailOnStartupError, the plugin activateswith no tools. - Re-sync: the client listens for
notifications/tools/list_changedand 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 (
initialDelayMsdoubling tomaxDelayMs) 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: aftermaxAttemptsconsecutive failures the server's tools are unregistered and reconnection stops until an HMR reload or Host restart; a connection that survives pastmaxDelayMsresets the budget. - HMR: editing the entry triggers disconnect + reconnect without process restart; an unchanged
serverNamereproduces identical tool names.
Model-facing behavior
- Tool result: canonical success is
{ content: JsonValue[], structuredContent? }; complete JSON blocks survive for programmatic callers. When an advertisedoutputSchemais supported,structuredContentis validated against it; unsupported schema vocabulary falls back to unconstrainedJsonValue.isErrorrejects 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:
| File | System | Transport |
|---|---|---|
memorix.cordis.yml | Memorix | stdio |
mcp-reference-memory.cordis.yml | MCP Reference Memory | stdio |
engram.cordis.yml | Engram | stdio |
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/listuses 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.ymlconfigures plugins - SDK protocol —
mcp__<server>__<tool>name normalization for programmatic callers packages/mcp/mcp-client/README.md— the bridge plugin's full behavior and configpackages/mcp/mcp-client/src/connection.ts— connection supervisor and reconnectexamples/mcp-memory/README.md— the three memory-server reference configs