DeepSeek Harness · Models & External Integration

MCP & Extensions: Two Paths to External Tools

How bridging the ecosystem standard and native extensions divide the work. Core source:packages/mcp/mcp-client/ and packages/extensions/

Course goalAfter reading, you can state three things clearly: DSH has two paths to external capability — the MCP bridge connects ready-made tool servers in the protocol ecosystem, while Extensions let the model write and run plugins live inside the harness; why the MCP bridge deliberately bridges only tools, how tool names use a hash to avoid collisions, and what happens to tools in the model’s hands when a server disconnects; and where the two trust models differ — one keeps risk outside the process, the other relies on approval and a sandbox.
Interactive demo · Integration approach comparison board

Play first, then we explain. The same external capability — “check the weather” — wires in on the left via the MCP bridge and on the right via a native Extension, both at once. Watch three things: how tool names are generated, what the model sees when the server disconnects, and how far the two capability surfaces differ. Hit “Play” to run through automatically, or “Step” frame by frame.

Path A · MCP bridge (external process)generation G1
Outside world
weather server (not started yet)
Raw tool name get_forecast(only appears on the wire)
ctx.tools registry inside the harness
Public name mcp__weather__get_forecast
Tools Events Services UI
Path B · Native Extension (in-process)
Model's action
Calls cordis_define, submits plugin source
Waiting for user approval: allow this plugin to run?
The two running halves
Host half: logic runs in the node:vm sandbox
Browser half: renders a weather panel in the page
Tools Events Services UI
Hit “Play” to see the same capability wired into the harness via both paths.
How it works · Path A: MCP bridge, plugging in someone else’s server

First, the terms. MCP (Model Context Protocol) is an open protocol: anyone can write a tool server, and any MCP-capable client can connect and use its tools. DSH’s dsh-mcp-client plugin is that protocol’s client — one plugin instance per server, supporting both stdio subprocess and streamable-http transports. Once connected, what it does is straightforward: listTools() pulls the tool list, registers each under a public name into ctx.tools, and from then on the model uses them as native tools.

Naming is the first design point. Every MCP tool has two names: the raw name appears only on the wire (tools/call uses it); the public name the model sees is mcp__serverName__rawName. That format matches Claude Code and Codex — the mcp-client README says so itself. Names must satisfy DeepSeek’s function-name rules: at most 64 characters, letters/digits/underscore/hyphen only. If character replacement or truncation changes the name, a 12-hex-digit SHA-256 hash is appended so two distinct tool identities never collapse into one name. The whole function is pure over (serverName, rawName): connection order, re-sync, or other servers cannot rename a tool.

packages/mcp/mcp-client/src/tools.tslines 96–102
export function publicToolName(serverName: string, rawName: string): string {
  const joined = `mcp__${serverName}__${rawName}`
  const normalized = joined.replace(INVALID_NAME_CHARS, '_')
  if (normalized === joined && normalized.length <= MAX_PUBLIC_NAME_LENGTH) return normalized
  const hash = createHash('sha256').update(`${serverName}\0${rawName}`).digest('hex').slice(0, HASH_LENGTH)
  return `${normalized.slice(0, MAX_PUBLIC_NAME_LENGTH - HASH_LENGTH - 1)}_${hash}`
}
Source snapshot note: Based on the local deepseek-harness-master repo; verified against packages/mcp/mcp-client/src/tools.ts, verified on 2026-08-13. Code blocks keep the original source text.

The second design point is generation. A server’s tool list can change, and when it does you re-sync. Sync has two phases: first pull and build the full next-generation tool definitions — any failure leaves the registry untouched, so the previous generation stays as-is; only after that succeeds do you swap — unregister the old generation, then register the new.

The swap phase is worth a close look. In the registration loop, every successful register stores its unregister function in a table. If any registration throws a conflict (meaning some foreign registration has seized this server’s namespace), the catch branch unregisters everything already in that table — not a single tool left — then logs an error. The comment states the intent plainly: rollback so the model sees either a complete generation or nothing at all — never a half set.

Source: the register-and-rollback branch at packages/mcp/mcp-client/src/tools.ts lines 159–172, verified on 2026-08-13.

Reconnect-after-disconnect is also built on generations. When a stdio subprocess crashes, the supervisor restarts it with exponential backoff: first delay defaults to 500 ms, doubles each time, caps at 30 s, at most 10 attempts per outage (README.zh.md config table). During the outage the last healthy generation stays registered — calls fail, but tool names don’t vanish; after a successful reconnect, rediscovery replaces the old generation wholesale, so tools neither duplicate nor leak. If serverName is unchanged, the new generation’s names match character for character, so KV-cache prefixes survive. Budget matters too: if a connection lives past 30 s, the attempt budget resets — so an occasionally crashing server can recover forever, while a crash-looping one eventually exhausts the budget and gets unregistered instead of restarting forever.

The last design point is easiest to miss: this bridge deliberately bridges only MCP’s tools capability. The protocol also has resources and prompts (prompt templates); DSH wires up neither. The README’s “Known limitations and deferred items” section is frank:

“Only bridge MCP tools: resources and prompts have no harness consumer interface, deferred.” Source: packages/mcp/mcp-client/README.zh.md line 111, verified on 2026-08-13

The logic is easy to reconstruct: nothing inside the harness consumes an external resource or external prompt, so building those bridge piers first would be pointless. Tools have a clear consumer (the agent loop’s tool calls), so tools get bridged first. That’s bridging on demand. Non-text results like images and audio also get a lossy projection — placeholders in model context; binary payloads stay out.

How it works · Path B: Extensions, letting the model grow its own plugins

The second path is entirely different. Cordis is DSH’s plugin framework; the whole harness is a Cordis plugin tree. The Extensions subsystem lets the model write a Cordis plugin in-session and run it on the spot: before coding, cordis_inspect queries which services and interfaces the current runtime exposes; then cordis_define submits source, cordis_run starts it, and when done cordis_stop or cordis_undefine. These five tools are registered by packages/extensions/tool-cordis.

A dynamic plugin has two halves. The Host half runs logic in a Node-side node:vm sandbox; the Browser half renders UI in the page; both lifecycles are owned by ctx.dynamicCordisRunner (from packages/extensions/cordis-host-runner/src/index.ts line 124 on). Starts that include a Browser half need approval: the cordis/request-run event sends the request to the page; only after the user allows does it continue, and they can also check “approve future versions” of this plugin (runHostHalf’s approveFutureVersions). Each Package version is immutable — editing code means appending a new version.

Side by side, the two paths are orthogonal — each owns one end. The MCP bridge faces ready-made capability outside the process; its trust model is isolation: crashes, garbage returns, and disconnects are kept outside the bridge by generations and error paths — but all it can give the model is tools. Extensions face model-generated code running in-process, with a much larger surface: add tools, emit events, register services, draw UI — at the cost of approval and sandbox on every run. One plugs into outside power; the other generates its own.

Names are a pure function

The public name is decided only by (serverName, rawName). Two servers each exposing a tool called search coexist in their own namespaces; connection order and re-sync never rename tools.

Generations are all-or-nothing

A failed pull leaves the registry untouched; a registration conflict rolls back the whole generation. The model always sees a complete tool set — never a half set. During disconnect the old generation stays registered: calls fail, but names remain.

The bridge bridges only tools

resources and prompts are deliberately deferred — the harness has no consumer interface for them. The capability gap is filled by Extensions: tools, events, services, and UI can all be added.

Side-by-side · How three products wire external capability

Claude Code: a full-spec MCP client

The restored-source MCP implementation is much thicker than DSH’s: six transports (stdio, sse, sse-ide, http, ws, sdk — see restored-src/src/services/mcp/types.ts lines 23–26), seven config-source layers (local, user, project, dynamic, enterprise, claudeai, managed), OAuth plus a 15-minute cache. Tool naming matches DSH’s shape, mcp__server__tool, with permission rules down to tool or server. One defense DSH lacks: tool descriptions truncated to 2048 characters, after observing OpenAPI-generated servers stuffing 15–60KB of docs into descriptions (comment at services/mcp/client.ts lines 217–219). Source: claude-code-sourcemap-main/study/chapters/08-mcp.md.

The difference is orientation. Claude Code treats MCP as the sole official extension point and goes deep and wide; DSH keeps the MCP bridge thin (tools only) and leaves heavy capability to native Extensions. The former’s extensions run out-of-process; the latter adds an in-process path.

Grok Build: the plugin-marketplace route

In the Grok Build repo, an MCP client (crates/codegen/xai-grok-mcp/) and a plugin marketplace (crates/codegen/xai-grok-plugin-marketplace/) coexist: MCP for protocol compatibility, the marketplace for distribution and trust — a centrally reviewed ecosystem route. Its MCP connect/discover/recover mechanics were already line-checked in the on-site Grok series — see MCP connect, discover & recover; marketplace discovery and trust is in Plugin Marketplace discovery & trust. We won’t repeat that here.

Put all three together and the spectrum appears: Grok centralizes trust in a marketplace; Claude Code spreads trust across seven config layers and permission rules; DSH splits the two paths, each with its own trust model — isolation outside the bridge, approval inside.

Classroom Exercise
01

Walk through a full disconnect-and-reconnect timeline

The weather server crashes right after the model gets the tool list; 8 seconds later the supervisor brings it back, and this time the list includes an extra get_alerts. Walk the timeline in order: what’s in the registry at the crash instant? What does the model get calling mcp__weather__get_forecast during the outage? After reconnect succeeds, what operations hit the registry, and does get_forecast’s public name change? Then: if two different servers weather and weather2 both expose get_forecast, do they collide, and why? (Hint: generation swap; names are a pure function of (serverName, rawName).)

Takeaway: The MCP bridge wires someone else’s capability; Extensions extend your own runtime — orthogonal paths, each with its own trust model. Bridging only tools is deliberate: no consumer, no bridge pier. Tool names are a pure function of (serverName, rawName); generation swap guarantees the model’s tool set is either complete or empty — never an in-between state.