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
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# @deepseek-ai/dsh-mcp-client
|
||||
|
||||
MCP client bridge plugin: connects to external [Model Context Protocol](https://modelcontextprotocol.io/) servers and registers their tools on `ctx.tools`, making them available to the model as native tools.
|
||||
MCP client bridge plugin: connects to external [Model Context Protocol](https://modelcontextprotocol.io/) 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
|
||||
|
||||
@@ -10,6 +10,7 @@ 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']
|
||||
@@ -19,36 +20,45 @@ One plugin instance per MCP server in `cordis.yml`:
|
||||
- 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}`'
|
||||
```
|
||||
|
||||
HMR hot-swaps: editing the entry triggers disconnect + reconnect without process restart.
|
||||
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) |
|
||||
| `toolPrefix` | both | no | Prefix prepended to each tool name before registration |
|
||||
| `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()`.
|
||||
- Listens for `notifications/tools/list_changed` → re-syncs tool registrations.
|
||||
- Tool execute: `client.callTool({ name, arguments }, { signal })` with timeout + abort support.
|
||||
- Image content in results is discarded with a warning (the harness has no image block type).
|
||||
- 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.
|
||||
- Name conflicts: if a tool name collides, it is skipped with a warning. Use `toolPrefix` to disambiguate.
|
||||
|
||||
## Services consumed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user