MCP Integration
MCP (Model Context Protocol) is the open standard for AI applications to connect external data sources and tools. DSH bridges it through @deepseek-ai/dsh-mcp-client: connect to an external MCP server, register its tools into ctx.tools, and the model can call them just like local tools, named mcp__<serverName>__<rawName>.
In one sentence
"One plugin instance = one MCP server," mounted in cordis.yml/patch.
Configuration
The most common is stdio: launching a local subprocess as the server.
- 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
You can also connect a remote server (streamable-http):
- 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}`'
Full field list:
| Field | Transport | Required | Effect |
|---|---|---|---|
transport | both | yes | "stdio" or "streamable-http" |
serverName | both | yes | Tool-name namespace, [A-Za-z0-9_-]{1,32}, unique across live instances |
command | stdio | yes | The executable to launch |
args | stdio | no | Arguments passed to the command |
env | stdio | no | Extra environment variables, merged on top of the scrubbed host environment |
cwd | stdio | no | Subprocess working directory |
url | http | yes | MCP server address |
headers | http | no | Extra request headers (e.g. auth token) |
toolCallTimeoutMs | both | no | Per-callTool timeout (default 60000) |
failOnStartupError | both | no | Reject activation when initial connection/sync fails (default false) |
reconnect.enabled | both | no | Auto-reconnect after a drop (default true) |
reconnect.initialDelayMs | both | no | First reconnect delay, doubled on consecutive failures (default 500) |
reconnect.maxDelayMs | both | no | Backoff cap, also the uptime needed to reset the reconnect budget (default 30000) |
reconnect.maxAttempts | both | no | Consecutive-failure cap per disconnect (default 10) |
Merging tools into the registry
Once connected, external MCP tools become ordinary tools on ctx.tools. Each tool has two names: the raw name (what tools/call sends to the server) and a public name mcp__<serverName>__<rawName> (what the model sees/calls). For example, github's create_issue becomes mcp__github__create_issue. The public name normalizes to the DeepSeek function-name contract (≤64 chars, [A-Za-z0-9_-]), appending a 12-digit hex hash derived from (serverName, rawName) on renaming to prevent name collapses; the name is a pure function of (serverName, rawName), unchanged by connection order or resync.
Name conflicts are deterministic: two servers emitting the same raw name coexist within their own namespaces; two live instances with the same serverName cause the later-mounted one to fail; a server listing the same tool name twice is an invalid list; an external registration squatting the namespace rolls back the whole generation with a loud error.
Once merged, they go through the same tool pipeline (see tool execution): tools/pre-execute gating, timeouts, and result rewriting all apply, so policies like allow/deny lists uniformly govern external tools.
Connection, reconnect, and failure semantics
- Discovery:
await listTools()on activation, thenctx.tools.register()one by one before the first round; failures are logged, and onlyfailOnStartupError: truerejects activation. - Hot updates: listens for
notifications/tools/list_changedto resync; a failed fetch keeps the previous generation of tools, and registration conflicts roll back the attempted generation. - Execution:
client.callTool({ name: rawName, arguments }, { signal })with timeout and cancellation; the public name is never sent to the server;isErrorgoes through the registry error path. - Drop reconnect: the supervisor restarts per the original config with exponential backoff, rerunning discovery on success; recovery replaces the previous generation without duplicating or leaking, and the last good generation stays registered during a disconnect. After
maxAttemptsconsecutive failures for the same disconnect, tools are deregistered and reconnecting stops until an HMR reload or Host restart.
Difference from custom tools
| Local tools | MCP tools | |
|---|---|---|
| Implementation | plugin ctx.tools / defineTool | external MCP server |
| Naming | snake_case names | mcp__<server>__<raw> |
| Lifecycle | in-process | bridged process/remote |
| Both | go through the same pipeline | go through the same pipeline |
When to use
- You already have a bunch of MCP servers (filesystem, GitHub, database, in-house tools) and don't want to write a dedicated integration for each
- You want to bring external tools under DSH's tool governance (gating/timeout/cancellation)
- Typical: connect a GitHub server to let the model open issues / query PRs; connect a database server to expose read-only queries; connect an in-house API gateway so dozens of operations are wired in at once, all governed uniformly under
tools/pre-execute
Current boundaries
- Only tools are bridged: Resources and Prompts have no harness consumer.
- Startup timeout inherits the MCP SDK (60-second default); an unresponsive server slows activation and cleanup;
streamable HTTPfailures surface per request, with unreachability being a per-call retry rather than a restart. - Image is the only durable rich-result bridge (rc.7): PNG/JPEG/WebP/GIF become durable core image blocks in the model context when
ctx.attachmentsis mounted and the calling model route explicitly declares image input; the whole batch is decoded and admitted before any member is saved, and malformed or unsupported batches become diagnostic text. Audio and embedded-resource payloads stay out of model context (the canonical value keeps the JSON block), and resource links keep only their name and URI as text. - Unsupported output schemas are not enforced:
structuredContentfalls back toJsonValue.
Verification
# see the mcp client in the composition tree
dsh web --dump-config | grep -A3 mcp
# see tool calls in the session log (mcp__ prefix naming)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"mcp__' | head
Next steps
- Built-in tools: the shipping tool list
- Writing a tool: how local tools are defined