Files
deepseek-harness/packages/mcp/mcp-client/README.md
lintianle af3152fefe feat(mcp): adopt mainstream server-qualified MCP tool naming
Research across 8 multi-server agent clients (Claude Code, Codex, Gemini
CLI, VS Code, Cline, Roo Code, Goose, OpenCode) showed all of them keep
the server namespace in model-facing MCP tool names; the RFC's premise
for raw names ("servers already prefix their tools") is false for the
official GitHub/filesystem/Sentry servers.

- Config: drop toolPrefix; require serverName ([A-Za-z0-9_-]{1,32}),
  duplicate serverName fails the later instance at load (per-root
  reservation, released on dispose)
- Names: always mcp__<serverName>__<rawName>; normalize to the DeepSeek
  64-char [A-Za-z0-9_-] contract with a deterministic 12-hex identity
  hash on lossy normalization; raw name is the only thing sent on the
  wire (tools/call)
- Sync: two-phase fetch/swap — fetch failure keeps the previous
  generation; a swap conflict rolls back the whole generation (never a
  partial set); duplicate raw names reject the tool list
- RFC: moved to implemented/ (status + skeleton rewritten per the
  format contract), naming design + tier-level test coverage recorded
- Tests: naming algorithm unit suite; keyless Streamable HTTP e2e
  against an in-process StreamableHTTPServerTransport (namespace
  discovery, execution, per-request auth headers); dotted-name
  normalization e2e via a new fixture tool
2026-07-13 23:46:49 +08:00

3.4 KiB

@deepseek-ai/dsh-mcp-client

MCP client bridge plugin: connects to external Model Context Protocol servers and registers their tools on ctx.tools, making them available to the model as native tools under server-qualified names (mcp__<serverName>__<rawName>).

Usage

One plugin instance per MCP server in cordis.yml:

- 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}`'

The model sees mcp__github__create_issue, mcp__web__search, … — the same server-qualified shape Claude Code and Codex use. HMR hot-swaps: editing the entry triggers disconnect + reconnect without process restart; an unchanged serverName reproduces identical tool names.

Config

Field Transport Required Description
transport both yes "stdio" or "streamable-http"
serverName both yes Namespace for this server's model-facing tool names; [A-Za-z0-9_-]{1,32}, unique across live instances
command stdio yes Executable to spawn
args stdio no Arguments passed to the command
env stdio no Extra env vars merged on top of scrubbed ambient env
cwd stdio no Working directory for the child process
url http yes MCP server URL
headers http no Extra headers (e.g. auth tokens)
toolCallTimeoutMs both no Timeout per callTool invocation (default 60000)

Tool naming

Every MCP tool has two names: the raw MCP name (sent on the wire in tools/call) and the public name mcp__<serverName>__<rawName> registered on ctx.tools. Public names are normalized to the DeepSeek function-name contract (64 chars, [A-Za-z0-9_-]); when replacement or truncation changes the name, a deterministic 12-hex-char hash of (serverName, rawName) is appended so distinct tools never collapse into one name. Names are pure functions of (serverName, rawName) — connection order, re-syncs, and other servers never rename a tool.

  • Two servers publishing the same raw name (e.g. search) coexist under their namespaces.
  • A duplicate serverName across live instances fails the later plugin instance at load.
  • A server listing the same tool name twice is rejected as an invalid tool list.
  • A foreign registration squatting on this server's namespace rolls back the whole generation (never a partial set), with a loud error.

Behavior

  • On connect: listTools() → registers each tool via ctx.tools.register() under its public name.
  • Listens for notifications/tools/list_changed → re-syncs; a failed re-sync keeps the previous generation registered.
  • Tool execute: client.callTool({ name: rawName, arguments }, { signal }) with timeout + abort support — the public name is never sent to the server.
  • Image content in results is discarded with a placeholder (the harness has no image block type).
  • On disconnect/crash: all tools are unregistered; no auto-reconnect.

Services consumed

Service Usage
ctx.tools Register/unregister MCP tools