@deepseek-ai/dsh-mcp-client 0.1.1-rc.1 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/mcp/mcp-client/README.md
5
- README.md: f3bf65d90d72f9eb3271cbbbb8ae8c586a7fd082
6
- README.zh.md: 1596ec72c28c4811eabb9f5cafe41cd7bdf1cf5e
5
+ README.md: 929577e886b9f4a438739191191a1f0ee86c6343
6
+ README.zh.md: ca07fd2b5d3547b09524a4097ce10bfb20251563
package/README.md CHANGED
@@ -1,12 +1,35 @@
1
+ ---
2
+ description: "MCP client bridge for deployments and maintainers choosing, configuring, or debugging connections to external MCP servers whose tools register on ctx.tools."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-mcp-client
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- 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>`).
10
+ ## Summary
11
+
12
+ `dsh-mcp-client` attaches external Model Context Protocol (MCP) servers to the harness so their tools work like any native tool. With one configuration entry per server, the model can call that server's tools — a filesystem, GitHub, database, or memory server — under stable names such as `mcp__github__create_issue`. Add it when the model should work with an external tool server; nothing ships enabled, so you opt in. The main cost is the tokens those tool definitions add to every request, and a slow or crashed server can delay startup or leave its tools failing until it recovers. Only tools are bridged: MCP resources and prompts are not supported.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
6
27
 
7
- ## Usage
28
+ Add `dsh-mcp-client` when the model should call tools from an external MCP server as if they were native. One configuration entry per server is the entire setup: give the server a short unique name and a transport, and its tools appear as `mcp__<serverName>__<tool>`. Choose stdio when the server runs as a local program and Streamable HTTP when it runs as a service. If you already use MCP tool servers from another client, the same server rows work here.
8
29
 
9
- One plugin instance per MCP server in `cordis.yml`:
30
+ ### Minimal configuration
31
+
32
+ Add one entry per server; nothing else is required. After the harness starts, the server's tools appear in the model's tool list.
10
33
 
11
34
  ```yaml
12
35
  - id: mcp-github
@@ -29,76 +52,126 @@ One plugin instance per MCP server in `cordis.yml`:
29
52
  Authorization: !!js '`Bearer ${process.env.MCP_TOKEN}`'
30
53
  ```
31
54
 
32
- 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.
33
-
34
- ## Config
35
-
36
- | Field | Transport | Required | Description |
37
- |---|---|---|---|
38
- | `transport` | both | yes | `"stdio"` or `"streamable-http"` |
39
- | `serverName` | both | yes | Namespace for this server's model-facing tool names; `[A-Za-z0-9_-]{1,32}`, unique across live instances |
40
- | `command` | stdio | yes | Executable to spawn |
41
- | `args` | stdio | no | Arguments passed to the command |
42
- | `env` | stdio | no | Extra env vars merged on top of scrubbed ambient env |
43
- | `cwd` | stdio | no | Working directory for the child process |
44
- | `url` | http | yes | MCP server URL |
45
- | `headers` | http | no | Extra headers (e.g. auth tokens) |
46
- | `toolCallTimeoutMs` | both | no | Timeout per `callTool` invocation (default 60000) |
47
- | `failOnStartupError` | both | no | Reject plugin activation when initial connection or tool synchronization fails (default `false`) |
48
- | `reconnect.enabled` | both | no | Reconnect automatically after a lost connection (default `true`) |
49
- | `reconnect.initialDelayMs` | both | no | First reconnect delay in ms; doubles per consecutive failed attempt (default 500) |
50
- | `reconnect.maxDelayMs` | both | no | Backoff ceiling in ms; also the uptime after which the attempt budget resets (default 30000) |
51
- | `reconnect.maxAttempts` | both | no | Consecutive failed attempts per outage before giving up for good (default 10) |
52
-
53
- ## Tool naming
54
-
55
- 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.
56
-
57
- - Two servers publishing the same raw name (e.g. `search`) coexist under their namespaces.
58
- - A duplicate `serverName` across live instances fails the later plugin instance at load.
59
- - A server listing the same tool name twice is rejected as an invalid tool list.
60
- - A foreign registration squatting on this server's namespace rolls back the whole generation (never a partial set), with a loud error.
61
-
62
- ## Behavior
63
-
64
- - On connect: plugin activation awaits `listTools()` and registers each tool via `ctx.tools.register()` under its public name before the composition starts its first turn. Initial connection, discovery, or registration failure is always logged; it rejects activation when `failOnStartupError` is true and otherwise activates with no tools.
65
- - Listens for `notifications/tools/list_changed` → re-syncs; a fetch-phase failure keeps the previous generation registered, while a registration conflict rolls back the attempted generation and leaves no tools from that server.
66
- - Tool execute: `client.callTool({ name: rawName, arguments }, { signal })` with timeout + abort support—the public name is never sent to the server.
67
- - Canonical success is `{ content: JsonValue[], structuredContent? }`; complete JSON MCP blocks survive for programmatic callers. A supported advertised `outputSchema` validates `structuredContent`; unsupported schema vocabulary falls back to unconstrained `JsonValue`.
68
- - Native/model rendering preserves MCP block order. Text-like runs join with newlines; resource links keep their name and URI as text; supported images become durable core image blocks only when `ctx.attachments` is mounted and the exact calling model route explicitly declares image input. The whole image batch is decoded and admitted before any member is saved. A malformed/refused image batch, audio, embedded resources, and unsupported blocks become explicit diagnostic text rather than disappearing.
69
- - On disconnect/crash: the supervisor restarts the original server config with exponential backoff (`reconnect.initialDelayMs` doubling up to `reconnect.maxDelayMs`) 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 against it fail until recovery.
70
- - Reconnection is budgeted per outage: after `reconnect.maxAttempts` consecutive failures the server's tools are unregistered and reconnection stops until an HMR reload or Host restart. A connection that survives past `maxDelayMs` resets the budget, so an occasionally-crashing server recovers indefinitely while a crash-looping one — even with briefly successful connects — still exhausts the cap instead of restarting forever.
71
- - Reconnect states are user-visible in logs: reconnecting (warn, with attempt count and delay), recovered (info), final failure and disabled-loss (error). Disposal cancels any pending reconnect. With `reconnect.enabled: false`, a lost connection keeps tools registered but failing until a reload — the manual-recovery behavior.
72
-
73
- ## Services consumed
74
-
75
- | Service | Usage |
55
+ | Field | Default | Meaning |
56
+ |---|---|---|
57
+ | `transport` | required | `stdio` or `streamable-http` |
58
+ | `serverName` | required | Namespace for the server's tool names; `[A-Za-z0-9_-]{1,32}`, unique inside one registration scope |
59
+ | `command` / `args` / `env` / `cwd` | — | stdio: executable, arguments, extra env merged over scrubbed ambient env, working directory |
60
+ | `url` / `headers` | — | streamable-http: endpoint URL and extra request headers |
61
+ | `toolCallTimeoutMs` | `60,000` | Timeout per `tools/call` invocation |
62
+ | `failOnStartupError` | `false` | Reject plugin activation when the initial connection or tool synchronization fails |
63
+ | `reconnect.enabled` | `true` | Reconnect automatically after a lost connection |
64
+ | `reconnect.initialDelayMs` | `500` | First reconnect delay; doubles per consecutive failed attempt |
65
+ | `reconnect.maxDelayMs` | `30,000` | Backoff ceiling; also the uptime after which the attempt budget resets |
66
+ | `reconnect.maxAttempts` | `10` | Consecutive failed attempts per outage before giving up |
67
+
68
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-mcp-client) is the exhaustive source for every accepted field.
69
+
70
+ After startup, the server's tools appear as `mcp__<serverName>__<tool>` — try a prompt that uses one. If the initial connection fails, the harness still starts but no tools from that server appear, and an error is logged; set `failOnStartupError: true` to make a startup failure abort the harness instead.
71
+
72
+ ### Tool naming and coexistence
73
+
74
+ The model sees each tool under a stable server-qualified name: `mcp__<serverName>__<rawName>`, for example `mcp__github__create_issue` — the same naming shape Claude Code and Codex use. Names stay stable while the server keeps the same tool name, so session history and permission rules survive restarts and reloads. Two servers can both offer a tool named `search` and coexist as `mcp__github__search` and `mcp__web__search`.
75
+
76
+ - Two servers publishing the same tool name (for example `search`) coexist under their own namespaces.
77
+ - Two entries using the same server name: the later one fails to load with a clear error.
78
+ - A server that lists the same tool twice gets its tool list rejected as invalid, and the previous tool set stays active.
79
+ - An update that conflicts with an already-registered tool name is rejected entirely — you never get a partial tool set from that server.
80
+
81
+ ### Calling tools and reading results
82
+
83
+ When the model calls an MCP tool, the call runs against the remote server with a per-call timeout (default 60 seconds) and can be cancelled like any other tool call. The result comes back as ordinary text in block order; resource links appear as text with their name and URI. If the server reports an error, the call fails visibly — the model does not see a fake success.
84
+
85
+ Images are supported when the current model accepts image input and the harness attachment feature is enabled; they then appear in the conversation like other images. Otherwise — and for audio or embedded resources — the model sees a clear diagnostic message instead of nothing.
86
+
87
+ ### Startup, updates, and reconnection
88
+
89
+ The server's tools appear before the harness starts its first turn. When the server changes its tool list, the model's tool set updates automatically; if the update fails, the previous tool set keeps working.
90
+
91
+ When a server connection drops — for example a local server process crashes — the plugin reconnects automatically with delays that double from 500 ms up to 30 s and then refreshes the tool set; reconnect progress is visible in the logs. During an outage the last known tools stay listed but calls to them fail until the server recovers. After ten consecutive failed attempts the server's tools are removed and reconnection stops until you reload the configuration or restart the harness; a server that stays connected for a while resets that counter. Set `reconnect.enabled: false` to disable automatic reconnection — tools then stay listed but fail until you reload. Editing the configuration entry reloads the server connection in place, and unchanged names stay unchanged.
92
+
93
+ -----
94
+
95
+ <a id="understand-the-implementation"></a>
96
+ ## Understand the implementation
97
+
98
+ <details>
99
+ <summary>Implementation internals — click to expand</summary>
100
+
101
+ This section explains the design decisions behind the bridge and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
102
+
103
+ ### Design philosophy
104
+
105
+ - **Server-qualified identity.** Every MCP tool has the stable identity `(serverName, rawName)`. The namespace is local configuration, never the remote `serverInfo.name` — the remote name is untrusted, not unique across deployments, and can change on upgrade, none of which may silently rename model-facing tools.
106
+ - **Naming is a pinned contract.** Public names are pure functions of `(serverName, rawName)` and satisfy the DeepSeek function-name contract; lossy normalization appends a 12-hex-char SHA-256 hash so distinct identities never collapse. Session history and permission rules therefore survive HMR swaps, re-syncs, and other servers' changes.
107
+ - **The raw name is the only wire name.** `tools/call` always receives the raw name; the public name is never sent to the server and never parsed to recover the raw name.
108
+ - **Full generation or none.** Syncs swap generations atomically: a fetch failure keeps the previous generation, and a registration conflict rolls back the entire attempted generation.
109
+ - **One canonical value, one projection.** The executor returns the protocol-complete canonical `McpResult`; a separate ordered projection prepares Native content, and `finalizeContent` installs it only when the registry's post-execute result is unchanged, so policy blocks and value replacements stay authoritative.
110
+
111
+ ### Source map
112
+
113
+ | File | Role |
76
114
  |---|---|
77
- | `ctx.tools` | Register/unregister MCP tools |
78
- | `ctx.attachments` | Optionally validate and persist image result batches before model projection |
79
- | `ctx.llm` | Optionally prove the exact calling route explicitly supports image input |
115
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `Config` schema, `serverName` reservation, activation await |
116
+ | [`src/connection.ts`](src/connection.ts) | Connection supervisor: client generations, reconnect policy, attempt budget, disposal |
117
+ | [`src/tools.ts`](src/tools.ts) | Tool bridge: discovery, naming, registration swap, execution, image projection |
118
+ | [`src/transport.ts`](src/transport.ts) | Transport factory: stdio spawn with scrubbed env, Streamable HTTP |
119
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; generations are observable only through the tool registry) |
120
+
121
+ ### Lifecycle and sync
122
+
123
+ `apply` resolves the reconnect policy, reserves the `serverName` inside the current registration scope, starts the supervisor, and awaits the initial connection plus discovery. Independent Agent scopes may reuse the same namespace because their tools and transports are isolated; a duplicate inside one scope fails at load. The supervisor serializes every sync — initial, notification, and reconnect — through one queue so two syncs can never interleave their dispose-previous/register-next swap. Disposal cancels pending reconnects, closes the live client, waits for the in-flight attempt and queued syncs to quiesce, and unregisters the current generation. The [auto-reconnect Agent Note](../../../.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.md) owns the reconnect decision.
124
+
125
+ The supervisor listens for `notifications/tools/list_changed` and queues a re-sync; a fetch-phase failure keeps the previous generation registered, while a registration conflict rolls back the attempted generation. Each outage shares one attempt budget: after `maxAttempts` consecutive failures the tools are unregistered and reconnection stops, and a connection that stays up past `maxDelayMs` resets the budget.
126
+
127
+ ### Tool execution internals
128
+
129
+ A tool call sends an uncached `tools/call` request carrying the raw MCP name, the JSON arguments, the abort signal, and the configured timeout; the public name is never sent to the server and never parsed back. Canonical success is `{ content: JsonValue[], structuredContent? }`, preserving the complete MCP JSON blocks for programmatic and PTC mode callers. A supported advertised `outputSchema` validates `structuredContent`; unsupported schema vocabulary falls back to unconstrained `JsonValue`. An MCP `isError` result throws before any image persistence, so the registry produces a failed tool result. Image batches are decoded and validated as a whole before any member is saved; any refusal projects every image as diagnostic text.
130
+
131
+ ### Environment scrubbing (stdio)
132
+
133
+ The child environment starts from the subprocess seam's `scrubbedParentEnv()` — ambient names matching `/KEY|PASSWORD|SECRET|TOKEN/i` and ambient `DSH_*` names are dropped — and the configured `env` merges on top, so explicit overrides survive. The MCP SDK owns the actual spawn; this package shares the scrub definition, not the spawn path.
134
+
135
+ </details>
136
+
137
+ -----
138
+
139
+ <a id="further-exploration"></a>
140
+ ## Further Exploration
80
141
 
142
+ Read these pages when the package-level contract is not enough. They move from the shared tool registry to the bridge's design evidence and worked example configurations.
143
+
144
+ - [Tools subsystem reference](../../../docs/subsystems/tools.md) — the `ToolRuntime` and `ctx.tools.register()` contract that receives the bridged tools.
145
+ - [MCP client plugin Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md) — the naming invariants, discovery and execution design, alternatives, and consequences.
146
+ - [MCP client auto-reconnect Agent Note](../../../.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.md) — the reconnect policy, attempt budget, and opt-out rationale.
147
+ - [Canonical tool output contract Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) — how MCP results map into the canonical tool-output contract.
148
+ - [Third-party memory MCP guide](../../../docs/user/guide/mcp-memory.md) — three memory-server overlays using this package.
149
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-mcp-client) — every accepted config field and its source declaration.
150
+
151
+ -----
152
+
153
+ <a id="model-experience"></a>
81
154
  ## Model Experience
82
155
 
83
156
  ### Discovered MCP tools
84
157
 
85
158
  #### What the model sees
86
159
 
87
- After initial discovery succeeds, each advertised MCP tool appears as a native tool named `mcp__<serverName>__<rawName>` (or its deterministic normalized form), with the server-provided description and input schema. A successful re-sync — including the one after an automatic reconnect — replaces the generation; plugin disposal or an exhausted reconnect budget removes it.
160
+ After initial discovery succeeds, every advertised MCP tool appears as a native tool named `mcp__<serverName>__<rawName>` (or its deterministic normalized form) with the server-provided description and input schema. A successful re-sync — including the one after an automatic reconnect — replaces the generation; plugin disposal or an exhausted reconnect budget removes it.
88
161
 
89
162
  #### Token effect
90
163
 
91
- Data-dependent schema cost is paid on every request while the tools are registered. Re-sync replaces rather than accumulates schemas, and the server-qualified name adds tokens to every tool definition and call.
164
+ The tool descriptions and input schemas enter every request while the tools are registered; re-syncs replace rather than accumulate schemas, and the server-qualified name adds tokens to every tool definition and call.
92
165
 
93
166
  #### KV Cache effect
94
167
 
95
- Prefix-stable while the discovered tool set and schemas are unchanged. A re-sync that adds, removes, renames, or changes a tool replaces definitions and may invalidate reuse from the first changed schema token; a reconnect that recovers an unchanged list reproduces identical definitions and stays prefix-stable.
168
+ The tool-definition prefix stays stable while the discovered set and schemas are unchanged. A re-sync that adds, removes, renames, or changes a tool replaces definitions and may invalidate reuse from the first changed schema token onward; a reconnect that recovers an unchanged list reproduces identical definitions and stays prefix-stable.
96
169
 
97
170
  ### Tool-call history and results
98
171
 
99
172
  #### What the model sees
100
173
 
101
- The public tool name and JSON arguments remain in assistant history. The execution-local canonical value always retains the complete JSON MCP blocks and optional structured content for programmatic and Code Mode callers. In Native context, supported image blocks are durably projected beside text in their original order after exact route-capability proof; Code Mode additionally ferries that settled rich projection through the outer `run_code` result without changing the canonical binding value. Refused images, audio, embedded resources, resource links, and unknown blocks remain visible as bounded text diagnostics, and MCP `isError` rejects the call before image persistence.
174
+ The public tool name and JSON arguments remain in assistant history. The canonical value retains the complete MCP JSON blocks and optional structured content for programmatic and PTC mode callers; supported image blocks project beside text in their original order after exact route-capability proof. Refused images, audio, embedded resources, resource links, and unknown blocks remain visible as bounded text diagnostics, and MCP `isError` rejects the call before image persistence.
102
175
 
103
176
  #### Token effect
104
177
 
@@ -110,8 +183,30 @@ Append-only; newly visible content follows the reusable request prefix and does
110
183
 
111
184
  ## Known Limitations and Deferred Work
112
185
 
113
- - **Tools are the only bridged MCP capability** — Resources and Prompts have no harness consumer and are deferred.
114
- - **Startup timeout is inherited from the MCP SDK** — DSH does not yet expose a connection/discovery timeout. Each initialize or paginated `tools/list` request uses the SDK's 60-second default, so an unresponsive server or cursor chain can delay both activation and teardown while the initial synchronization settles.
115
- - **Reconnect triggers on transport close** — a crashed stdio child fires it; Streamable HTTP failures surface per request and through the SDK transport's own SSE-stream recovery, so an unreachable HTTP server is retried per call rather than respawned by the supervisor.
116
- - **Image is the only durable rich-result bridge** — PNG, JPEG, WebP, and GIF can enter Native context after exact capability proof. Audio and embedded-resource payloads remain execution-local with explicit diagnostics, while resource links preserve only their name and URI as text.
186
+ <a id="known-limitations-and-deferred-work"></a>
187
+
188
+
189
+ These limits describe what you cannot do with this plugin and when it needs operational attention. They are current package constraints, not a comparison with other MCP clients or a task backlog.
190
+
191
+ - **Tools are the only bridged MCP capability** — Resources and Prompts have no harness consumer mechanism and are deferred.
192
+ - **Startup and discovery timeouts are inherited from the MCP SDK** — the plugin exposes no connection or discovery timeout; each `initialize` and paginated `tools/list` request uses the SDK's 60-second request default, so an unresponsive server or cursor chain can delay both activation and teardown while the initial synchronization settles.
193
+ - **Reconnect triggers on transport close** — a crashed stdio child fires it; Streamable HTTP failures surface per request through the SDK transport's own recovery, so an unreachable HTTP server is retried per call rather than respawned by the supervisor.
194
+ - **Image is the only durable rich-result bridge** — PNG, JPEG, WebP, and GIF enter Native context after exact capability proof. Audio and embedded-resource payloads remain execution-local with explicit diagnostics, while resource links preserve only their name and URI as text.
117
195
  - **Unsupported MCP output schemas are not enforced** — `structuredContent` falls back to `JsonValue` when the advertised schema uses vocabulary outside the harness subset.
196
+ - **Task-required MCP tools are rejected at call time** — a tool that requires the task-based execution extension throws instead of bridging; the extension is not implemented.
197
+
198
+ <a id="dev-note"></a>
199
+ ### Dev Note
200
+
201
+ <details>
202
+ <summary>Working context for maintainers — click to expand</summary>
203
+
204
+ This Dev Note is working context for maintainers: open design questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
205
+
206
+ - The public-name algorithm is a v1 contract pinned by tests; changing it after release would break session history and permission rules.
207
+ - An explicit DSH-owned connection and discovery timeout is an open direction; the SDK's 60-second default bounds startup and teardown.
208
+ - Reconnect ownership for Streamable HTTP is open: per-request retry is SDK behavior, and the supervisor could also own the HTTP generation.
209
+ - Bridging MCP Resources needs a harness-side injection decision (system prompt, on demand, or model-triggered); bridging Prompts needs a prompt-template concept the harness lacks.
210
+ - The pinned MCP SDK is still evolving; a breaking upstream change requires updating the bridge.
211
+
212
+ </details>
package/README.zh.md CHANGED
@@ -1,12 +1,35 @@
1
+ ---
2
+ description: "面向部署方与维护者的 MCP 客户端桥接说明,用于选择、配置或排查连接到外部 MCP 服务器、并将其工具注册到 ctx.tools 的插件。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-mcp-client
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelcontextprotocol.io/) 服务器,把它们的工具注册到 `ctx.tools`,使模型能够通过服务器限定名称(`mcp__<serverName>__<rawName>`)将其作为原生工具使用。
10
+ ## 概述
11
+
12
+ `dsh-mcp-client` 把外部 MCP(Model Context Protocol)服务器挂载到 harness 上,让它们的工具像原生工具一样可用。每台服务器一条配置项,模型就能调用该服务器的工具——文件系统、GitHub、数据库或记忆服务器——名称稳定,例如 `mcp__github__create_issue`。当模型需要使用外部工具服务器时添加它;默认不启用任何服务器,因此由你开启。主要成本是这些工具定义给每次请求增加的 token,而且缓慢或崩溃的服务器可能延迟启动,或在恢复前让它的工具一直调用失败。只桥接工具能力:MCP resources 与 prompts 不受支持。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
6
27
 
7
- ## 用法
28
+ 当模型需要把外部 MCP 服务器的工具当作原生工具调用时,添加 `dsh-mcp-client`。每台服务器一条配置项就是全部设置:给服务器一个简短的唯一名称和一种传输方式,它的工具就会以 `mcp__<serverName>__<tool>` 形式出现。服务器作为本地程序运行时选择 stdio,作为服务运行时选择 Streamable HTTP。如果你已经用其他客户端连接过 MCP 工具服务器,同样的配置行在这里也能用。
8
29
 
9
- `cordis.yml` 中每个 MCP 服务器使用一个插件实例:
30
+ ### 最小配置
31
+
32
+ 每台服务器添加一条配置项即可,无需其他内容。harness 启动后,服务器的工具会出现在模型的工具列表中。
10
33
 
11
34
  ```yaml
12
35
  - id: mcp-github
@@ -29,89 +52,161 @@ MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelc
29
52
  Authorization: !!js '`Bearer ${process.env.MCP_TOKEN}`'
30
53
  ```
31
54
 
32
- 模型会看到 `mcp__github__create_issue`、`mcp__web__search` 等工具,这与 Claude Code 和 Codex 使用的服务器限定形状相同。HMR(热模块替换)支持热替换:编辑配置项会触发断开 + 重新连接,无需重启进程;`serverName` 不变时会生成完全相同的工具名称。
33
-
34
- ## 配置
35
-
36
- | 字段 | 传输 | 必填 | 描述 |
37
- |---|---|---|---|
38
- | `transport` | 两者 | 是 | `"stdio"` 或 `"streamable-http"` |
39
- | `serverName` | 两者 | 是 | 该服务器面向模型工具名称的 namespace;`[A-Za-z0-9_-]{1,32}`,在存活实例中唯一 |
40
- | `command` | stdio | 是 | 要 spawn 的可执行文件 |
41
- | `args` | stdio | 否 | 传给命令的参数 |
42
- | `env` | stdio | 否 | 合并到已清理环境中的额外环境变量 |
43
- | `cwd` | stdio | 否 | 子进程工作目录 |
44
- | `url` | http | 是 | MCP 服务器 URL |
45
- | `headers` | http | 否 | 额外标头(例如认证 token) |
46
- | `toolCallTimeoutMs` | 两者 | 否 | 每次 `callTool` 调用的超时(默认 60000) |
47
- | `failOnStartupError` | 两者 | 否 | 初始连接或工具同步失败时拒绝插件激活(默认 `false`) |
48
- | `reconnect.enabled` | 两者 | 否 | 连接丢失后自动重新连接(默认 `true`) |
49
- | `reconnect.initialDelayMs` | 两者 | 否 | 首次重连延迟(毫秒);每次连续失败尝试翻倍(默认 500) |
50
- | `reconnect.maxDelayMs` | 两者 | 否 | 退避上限(毫秒);同时也是重置尝试预算所需的正常运行时长(默认 30000) |
51
- | `reconnect.maxAttempts` | 两者 | 否 | 每次中断期间连续失败尝试次数上限,超出后彻底放弃(默认 10) |
52
-
53
- ## 工具命名
54
-
55
- 每个 MCP 工具都有两个名称:通过 `tools/call` 在协议上传送的原始 MCP 名称,以及公开名称 `mcp__<serverName>__<rawName>`,后者注册到 `ctx.tools`。公开名称会规范化为 DeepSeek 函数名称约定(64 个字符、`[A-Za-z0-9_-]`);如果替换或截断改变名称,就会追加 `(serverName, rawName)` 的确定性 12 位十六进制 hash,确保不同工具绝不会折叠为同一个名称。名称是 `(serverName, rawName)` 的纯函数:连接顺序、重新同步和其他服务器永远不会重命名工具。
56
-
57
- - 发布相同原始名称(例如 `search`)的两个服务器会在各自 namespace 下共存。
58
- - 存活实例中的重复 `serverName` 会使后加载的插件实例失败。
59
- - 服务器在工具列表中两次列出同一工具名称时,该列表会作为无效工具列表被拒绝。
60
- - 外部注册抢占该服务器 namespace 时,会回滚整个世代(绝不保留部分集合),并明确报错。
61
-
62
- ## 行为
63
-
64
- - 连接时:插件激活会等待 `listTools()`,并在组合开始首个轮次前通过 `ctx.tools.register()` 以公开名称注册每个工具。初始连接、发现或注册失败始终会记录日志;`failOnStartupError` 为 true 时拒绝激活,否则插件仍会激活但不注册工具。
65
- - 监听 `notifications/tools/list_changed` → 重新同步;获取阶段失败时保留上一世代的注册,注册冲突则会回滚本次尝试的世代,并且不保留该服务器的任何工具。
66
- - 工具执行:`client.callTool({ name: rawName, arguments }, { signal })`,支持超时 + 中止;公开名称绝不会发给服务器。
67
- - 规范成功值是 `{ content: JsonValue[], structuredContent? }`;完整的 JSON MCP 块会保留给编程调用方。受支持且已声明的 `outputSchema` 会验证 `structuredContent`;不受支持的 schema 词汇会回退为不受约束的 `JsonValue`。
68
- - Native/模型渲染会保留 MCP 块顺序。文本类连续块以换行连接;资源链接以文本保留名称和 URI;只有挂载 `ctx.attachments` 且确切调用模型路由明确声明支持图片输入时,受支持的图片才会成为持久核心图片块。整个图片批次会先完成解码与准入,再保存任一成员。格式错误或被拒绝的图片批次、音频、嵌入资源和不受支持的块会成为明确诊断文本,而不会消失。
69
- - 断开/崩溃时:supervisor 以指数退避(`reconnect.initialDelayMs` 逐次翻倍,上限 `reconnect.maxDelayMs`)重启原始服务器配置,成功后重新执行发现——恢复的世代会替换前一个,因此工具既不会重复也不会泄漏。中断期间最后一个正常世代保持注册;针对它的调用在恢复前会失败。
70
- - 重连按中断预算控制:连续失败达到 `reconnect.maxAttempts` 次后,该服务器的工具会被注销,重连停止,直到 HMR 重载或重启 Host。连接存活超过 `maxDelayMs` 会重置预算,因此偶尔崩溃的服务器可以无限恢复,而崩溃循环的服务器——即使短暂连接成功——仍会耗尽上限而非永远重启。
71
- - 重连状态在日志中对用户可见:reconnecting(warn,含尝试次数和延迟)、recovered(info)、最终失败和 disabled-loss(error)。dispose(资源释放)会取消任何待执行的重连。设置 `reconnect.enabled: false` 时,连接丢失后工具保持注册但调用失败,直到重载——即手动恢复行为。
72
-
73
- ## 消费的服务
74
-
75
- | 服务 | 用途 |
55
+ | 字段 | 默认值 | 含义 |
56
+ |---|---|---|
57
+ | `transport` | 必填 | `stdio` 或 `streamable-http` |
58
+ | `serverName` | 必填 | 服务器工具名称的 namespace;`[A-Za-z0-9_-]{1,32}`,在一个注册作用域内唯一 |
59
+ | `command` / `args` / `env` / `cwd` | — | stdio:可执行文件、参数、合并到清洗过的环境之上的额外环境变量、工作目录 |
60
+ | `url` / `headers` | — | streamable-http:端点 URL 与额外请求标头 |
61
+ | `toolCallTimeoutMs` | `60,000` | 每次 `tools/call` 调用的超时 |
62
+ | `failOnStartupError` | `false` | 初始连接或工具同步失败时拒绝插件激活 |
63
+ | `reconnect.enabled` | `true` | 连接丢失后自动重新连接 |
64
+ | `reconnect.initialDelayMs` | `500` | 首次重连延迟;每次连续失败尝试翻倍 |
65
+ | `reconnect.maxDelayMs` | `30,000` | 退避上限;同时是重置尝试预算所需的正常运行时长 |
66
+ | `reconnect.maxAttempts` | `10` | 每次中断内连续失败尝试次数上限,超出后放弃 |
67
+
68
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-mcp-client)是每个受支持字段及其 JSDoc 的穷尽式真源。
69
+
70
+ 启动后,服务器的工具会以 `mcp__<serverName>__<tool>` 形式出现——试着用一条提示词调用其中一个。如果初始连接失败,harness 仍会启动,但该服务器的工具不会出现,并会记录一条错误;设置 `failOnStartupError: true` 可让启动失败改为中止 harness。
71
+
72
+ ### 工具命名与共存
73
+
74
+ 模型看到每个工具都带有稳定的服务器限定名称:`mcp__<serverName>__<rawName>`,例如 `mcp__github__create_issue`——与 Claude Code 和 Codex 使用的命名形态相同。只要服务器保持相同的工具名称,名称就保持不变,因此会话历史与权限规则在重启和重载后仍然有效。两个服务器可以同时提供名为 `search` 的工具,分别以 `mcp__github__search` 和 `mcp__web__search` 共存。
75
+
76
+ - 发布相同工具名称(例如 `search`)的两个服务器会在各自的 namespace 下共存。
77
+ - 两条配置项使用相同的服务器名称时,后加载的一条会在加载时以明确错误失败。
78
+ - 服务器在工具列表中两次列出同一工具时,其工具列表会被作为无效列表拒绝,上一组工具保持可用。
79
+ - 工具更新与已有工具名称冲突时,该更新会被整体拒绝——绝不会得到该服务器的部分工具集。
80
+
81
+ ### 调用工具与读取结果
82
+
83
+ 模型调用 MCP 工具时,调用会以每次调用超时(默认 60 秒)发往远程服务器,并像其他工具调用一样可以取消。结果按块顺序以普通文本返回;资源链接以文本形式显示名称与 URI。如果服务器报告错误,调用会明确失败——模型不会看到虚假的成功。
84
+
85
+ 当前模型接受图片输入且 harness 启用了附件功能时支持图片;图片会像其他图片一样出现在对话中。不支持图片时——以及服务器返回音频或嵌入资源时——模型会看到清晰的诊断消息,而不是什么都没有。
86
+
87
+ ### 启动、工具更新与重连
88
+
89
+ 服务器的工具会在 harness 开始首个轮次之前出现。服务器更改工具列表时,模型的工具集会自动更新;更新失败时,上一组工具继续可用。
90
+
91
+ 服务器连接断开时——例如本地服务器进程崩溃——插件会以从 500 ms 起逐次翻倍、上限 30 s 的延迟自动重连,并刷新工具集;重连进度在日志中可见。中断期间最后已知的工具仍会列出,但对它们的调用会失败,直到服务器恢复。连续失败十次后,该服务器的工具会被移除,重连停止,直到你重载配置或重启 harness;服务器持续连接一段时间后,该计数会重置。设置 `reconnect.enabled: false` 可禁用自动重连——此时工具在断开后仍会列出,但调用失败,直到你重载。编辑配置项会在原地重载服务器连接,未变的名称保持不变。
92
+
93
+ -----
94
+
95
+ <a id="understand-the-implementation"></a>
96
+ ## 理解实现
97
+
98
+ <details>
99
+ <summary>实现细节——点击展开</summary>
100
+
101
+ 本节解释桥接背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
102
+
103
+ ### 设计理念
104
+
105
+ - **服务器限定身份。** 每个 MCP 工具都有稳定的身份 `(serverName, rawName)`。namespace 是本地配置,绝不采用远程 `serverInfo.name`——远程名称不可信、在部署间不唯一、且升级时可能变化,这些都不允许静默重命名面向模型的工具。
106
+ - **命名是固定约定。** 公开名称是 `(serverName, rawName)` 的纯函数,并满足 DeepSeek 函数名称约定;有损规范化会追加 12 位十六进制 SHA-256 hash,使不同身份绝不会折叠。会话历史与权限规则因此能在 HMR 替换、重新同步和其他服务器变化后保持有效。
107
+ - **原始名称是唯一的协议名称。** `tools/call` 始终收到原始名称;公开名称绝不会发给服务器,也绝不会被解析来还原原始名称。
108
+ - **要么完整世代,要么没有。** 同步会原子地交换世代:获取失败保留上一世代,注册冲突则回滚整个尝试中的世代。
109
+ - **一个规范值,一个投影。** 执行器返回协议完整的规范 `McpResult`;另一个有序投影准备 Native 内容,`finalizeContent` 只在注册表的执行后结果未变时安装它,因此策略块与值替换保持权威。
110
+
111
+ ### 源码地图
112
+
113
+ | 文件 | 职责 |
76
114
  |---|---|
77
- | `ctx.tools` | 注册/注销 MCP 工具 |
78
- | `ctx.attachments` | 可选;在模型投影前校验并持久保存图片结果批次 |
79
- | `ctx.llm` | 可选;证明确切调用路由明确支持图片输入 |
115
+ | [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、`serverName` 预留、激活等待 |
116
+ | [`src/connection.ts`](src/connection.ts) | 连接监督器:客户端世代、重连策略、尝试预算、dispose |
117
+ | [`src/tools.ts`](src/tools.ts) | 工具桥接:发现、命名、注册交换、执行、图片投影 |
118
+ | [`src/transport.ts`](src/transport.ts) | 传输工厂:带清洗环境的 stdio spawn、Streamable HTTP |
119
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;世代只能通过工具注册表观察) |
120
+
121
+ ### 生命周期与同步
122
+
123
+ `apply` 解析重连策略、在当前注册作用域内预留 `serverName`、启动监督器,并等待初始连接加发现完成。独立 Agent 作用域可以复用相同 namespace,因为其工具与传输彼此隔离;同一作用域内重复会在加载时失败。监督器把所有同步——初始、通知与重连——串行到同一条队列,因此两次同步绝不会交错执行各自的先 dispose 后注册交换。dispose 会取消待执行的重连、关闭活动客户端、等待进行中的尝试与排队同步完全停稳,然后注销当前世代。[自动重连 Agent Note](../../../.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.zh.md) 拥有重连决策。
124
+
125
+ 监督器监听 `notifications/tools/list_changed` 并排队一次重新同步;获取阶段失败时保留上一世代注册,注册冲突则回滚本次尝试的世代。每次中断共享一个尝试预算:连续失败达到 `maxAttempts` 次后工具被注销、重连停止;连接存活超过 `maxDelayMs` 会重置预算。
126
+
127
+ ### 工具执行内部细节
128
+
129
+ 工具调用会发送一次未缓存的 `tools/call` 请求,携带原始 MCP 名称、JSON 参数、中止信号与配置的超时;公开名称绝不会发给服务器,也绝不会被解析还原。规范成功值是 `{ content: JsonValue[], structuredContent? }`,为程序化调用方与 PTC mode 调用方保留完整的 MCP JSON 块。受支持且已声明的 `outputSchema` 会验证 `structuredContent`;不受支持的 schema 词汇回退为不受约束的 `JsonValue`。MCP 的 `isError` 结果会在任何图片持久化之前抛出,使注册表产生失败的工具结果。图片批次会先整体解码并校验,再保存任一成员;任何拒绝都会把每张图片投影为诊断文本。
130
+
131
+ ### 环境清洗(stdio)
132
+
133
+ 子进程环境以子进程 seam 的 `scrubbedParentEnv()` 为基座——删除匹配 `/KEY|PASSWORD|SECRET|TOKEN/i` 的环境名称与所有 `DSH_*` 名称——再在其上合并配置的 `env`,因此显式覆盖得以保留。实际 spawn 由 MCP SDK 负责;本包共享清洗定义,而非 spawn 路径。
134
+
135
+ </details>
136
+
137
+ -----
138
+
139
+ <a id="further-exploration"></a>
140
+ ## 进一步探索
80
141
 
142
+ 当包级约定不够用时阅读以下页面。它们从共享工具注册表逐步进入桥接的设计证据与可运行的示例配置。
143
+
144
+ - [工具子系统参考](../../../docs/subsystems/tools.zh.md)——接收已桥接工具的 `ToolRuntime` 与 `ctx.tools.register()` 约定。
145
+ - [MCP 客户端插件 Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.zh.md)——命名不变式、发现与执行设计、备选方案与后果。
146
+ - [MCP 客户端自动重连 Agent Note](../../../.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.zh.md)——重连策略、尝试预算与退出开关的依据。
147
+ - [规范工具输出约定 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md)——MCP 结果如何映射进规范工具输出约定。
148
+ - [第三方记忆 MCP 指南](../../../docs/user/guide/mcp-memory.zh.md)——使用本包的三份记忆服务器 overlay。
149
+ - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-mcp-client)——每个受支持配置字段及其源声明。
150
+
151
+ -----
152
+
153
+ <a id="model-experience"></a>
81
154
  ## 模型体验
82
155
 
83
156
  ### 已发现的 MCP 工具
84
157
 
85
- #### 模型看到的内容
158
+ #### 模型看到什么
86
159
 
87
- 初始发现成功后,每个已声明的 MCP 工具都会显示为名为 `mcp__<serverName>__<rawName>`(或其确定性规范化形式)的原生工具,并携带服务器提供的描述和输入 schema。成功的重新同步——包括自动重连后的同步——会替换整个世代;对插件执行 dispose(资源释放)或重连预算耗尽会移除该世代。
160
+ 初始发现成功后,每个已声明的 MCP 工具都会显示为名为 `mcp__<serverName>__<rawName>`(或其确定性规范化形式)的原生工具,并携带服务器提供的描述与输入 schema。成功的重新同步——包括自动重连后的同步——会替换整个世代;对插件执行 dispose(资源释放)或重连预算耗尽会移除该世代。
88
161
 
89
162
  #### Token 影响
90
163
 
91
- 工具注册期间,每次请求都会承担数据相关的 schema 成本。重新同步会替换而非累积 schema,服务器限定名称也会为每个工具定义和调用增加 token。
164
+ 工具注册期间,工具描述与输入 schema 会进入每次请求;重新同步会替换而非累积 schema,服务器限定名称也会为每个工具定义和调用增加 token。
92
165
 
93
166
  #### KV Cache 影响
94
167
 
95
- 只要已发现工具集合及其 schema 不变,前缀就保持稳定。增加、移除、重命名或更改工具的重新同步会替换定义,并可能使从第一个变化的 schema token 起的复用失效;恢复了未变列表的重连会生成完全相同的定义,前缀保持稳定。
168
+ 已发现工具集合及其 schema 不变时,工具定义前缀保持稳定。增加、移除、重命名或更改工具的重新同步会替换定义,并可能使从第一个变化的 schema token 起的复用失效;恢复未变列表的重连会生成完全相同的定义,前缀保持稳定。
96
169
 
97
170
  ### 工具调用历史与结果
98
171
 
99
- #### 模型看到的内容
172
+ #### 模型看到什么
100
173
 
101
- 公开工具名称和 JSON 参数会保留在 assistant 历史中。执行局部的规范值始终为程序化调用方和 Code Mode 保留完整 JSON MCP 块及可选结构化内容。在 Native 上下文中,受支持的图片块会在确切路由能力得到证明后,按原始顺序与文本一起持久投影;Code Mode 还会经外层 `run_code` 结果转运这份已经结算的丰富投影,而不改变规范绑定值。被拒绝的图片、音频、嵌入资源、资源链接和未知块会继续以有界文本诊断可见;MCP `isError` 会在持久化图片前拒绝调用。
174
+ 公开工具名称和 JSON 参数保留在 assistant 历史中。规范值始终为程序化调用方与 PTC mode 调用方保留完整的 MCP JSON 块与可选结构化内容;受支持的图片块在确切路由能力得到证明后,按原始顺序与文本一起投影。被拒绝的图片、音频、嵌入资源、资源链接与未知块继续以有界文本诊断可见;MCP `isError` 会在图片持久化之前拒绝调用。
102
175
 
103
176
  #### Token 影响
104
177
 
105
- 参数、映射后的文本和持久图片引用会保留到压缩(compaction)发生时。内联 MCP base64 只存在于执行局部的规范值中,绝不会复制进会话事件;提供方会从附件存储读取经过校验的字节。音频和嵌入资源载荷仍不会进入模型上下文。
178
+ 参数、映射后的文本与持久图片引用保留到压缩(compaction)发生时。内联 MCP base64 只存在于执行局部的规范值中,绝不会复制进会话事件;提供方会从附件存储读取经过校验的字节。音频与嵌入资源载荷不会进入模型上下文。
106
179
 
107
180
  #### KV Cache 影响
108
181
 
109
182
  仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
110
183
 
111
- ## 已知限制与暂缓事项
184
+ ## 已知限制与延期工作
185
+
186
+ <a id="known-limitations-and-deferred-work"></a>
187
+
188
+
189
+ 这些限制说明你无法用本插件做什么、以及何时需要运维注意。它们是当前包约束,不是与其他 MCP 客户端的对比,也不是任务积压。
190
+
191
+ - **只桥接 MCP 的工具能力**——Resources 与 Prompts 没有 harness 消费机制,暂缓实现。
192
+ - **启动与发现超时继承自 MCP SDK**——插件不暴露连接或发现超时;每次 `initialize` 与分页 `tools/list` 请求都使用 SDK 默认的 60 秒请求超时,因此无响应的服务器或 cursor chain 在初始同步完成期间可能同时延迟激活与 teardown。
193
+ - **重连在传输关闭时触发**——崩溃的 stdio 子进程会触发重连;Streamable HTTP 失败按请求经 SDK 传输自身的恢复机制暴露,因此不可达的 HTTP 服务器会按调用重试,而非由 supervisor 重新 spawn。
194
+ - **图片是唯一的持久丰富结果桥接**——PNG、JPEG、WebP 与 GIF 在确切能力得到证明后进入 Native 上下文。音频与嵌入资源载荷仍只存在于执行局部并带明确诊断,资源链接只以文本保留名称与 URI。
195
+ - **不强制执行不受支持的 MCP 输出 schema**——已声明 schema 使用 harness 子集之外的词汇时,`structuredContent` 回退为 `JsonValue`。
196
+ - **要求基于任务的 MCP 工具在调用时被拒绝**——要求使用基于任务的执行(task-based execution)扩展的工具会抛出异常而非被桥接;该扩展未实现。
197
+
198
+ <a id="dev-note"></a>
199
+ ### 开发备注
200
+
201
+ <details>
202
+ <summary>维护者的工作上下文——点击展开</summary>
203
+
204
+ 本开发备注是维护者的工作上下文:开放设计问题与尚未决定的探索方向。它明确不具权威性——已交付行为、限制与既定理由以上文、包代码与所链接的 Agent Note 为准。
205
+
206
+ - 公开名称算法是由测试固定的 v1 约定;发布后更改会破坏会话历史与权限规则。
207
+ - 由 DSH 显式拥有的连接与发现超时是开放的探索方向;SDK 的 60 秒默认值约束着启动与 teardown。
208
+ - Streamable HTTP 的重连归属仍未决定:按请求重试是 SDK 行为,supervisor 也可以拥有 HTTP 世代。
209
+ - 桥接 MCP Resources 需要 harness 侧的注入决策(系统提示词、按需或模型触发);桥接 Prompts 需要 harness 缺少的提示词模板概念。
210
+ - 固定的 MCP SDK 仍在演化;上游破坏性变更需要更新桥接。
112
211
 
113
- - **只桥接 MCP 的工具能力**:资源和提示词没有 harness 消费接口,暂缓实现。
114
- - **启动超时继承自 MCP SDK**:DSH 尚未公开连接/发现超时。每次 initialize 请求或分页 `tools/list` 请求都使用 SDK 默认的 60 秒,因此在初始同步完成期间,无响应的 server 或 cursor chain 可能同时延迟激活与 teardown。
115
- - **重连在传输关闭时触发**:崩溃的 stdio 子进程会触发重连;Streamable HTTP 失败通过每次请求以及 SDK 传输自身的 SSE(Server-Sent Events)流恢复机制暴露,因此不可达的 HTTP 服务器会按调用重试,而非由 supervisor 重新 spawn。
116
- - **图片是唯一的持久丰富结果桥接**:PNG、JPEG、WebP 和 GIF 可以在确切能力得到证明后进入 Native 上下文。音频和嵌入资源载荷仍只存在于执行局部,并配有明确诊断;资源链接只以文本保留名称和 URI。
117
- - **不强制执行不受支持的 MCP 输出 schema**:已声明 schema 使用 harness 子集之外的词汇时,`structuredContent` 会回退到 `JsonValue`。
212
+ </details>
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import { scopeOf } from "@deepseek-ai/dsh-scope";
2
3
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
3
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
5
  import { ListToolsResultSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -723,10 +724,9 @@ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 6e4;
723
724
  /** Valid `serverName`, kept below the public tool-name budget. */
724
725
  const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
725
726
  /**
726
- * Live `serverName` reservations per app, keyed off `ctx.root` (multiple apps
727
- * in one process — tests — must not see each other's names). A duplicate
728
- * namespace is a configuration error surfaced at plugin load, never silent
729
- * shadowing.
727
+ * Live `serverName` reservations per registration scope. Agent-scoped MCP
728
+ * servers may reuse a namespace in another Agent, while global instances and
729
+ * duplicates inside one Agent remain mutually exclusive.
730
730
  */
731
731
  const activeServerNames = /* @__PURE__ */ new WeakMap();
732
732
  const Reconnect = z.object({
@@ -765,10 +765,11 @@ const Config = z.union([z.object({
765
765
  async function apply(ctx, config) {
766
766
  const reconnect = resolveReconnectPolicy(config.reconnect, `mcp-client(${config.serverName}): reconnect`);
767
767
  ctx.effect(() => {
768
- let names = activeServerNames.get(ctx.root);
768
+ const owner = scopeOf(ctx) ?? ctx.root;
769
+ let names = activeServerNames.get(owner);
769
770
  if (!names) {
770
771
  names = /* @__PURE__ */ new Set();
771
- activeServerNames.set(ctx.root, names);
772
+ activeServerNames.set(owner, names);
772
773
  }
773
774
  if (names.has(config.serverName)) throw new Error(`mcp-client: serverName "${config.serverName}" is already in use by another mcp-client instance — pick a unique serverName in cordis.yml`);
774
775
  names.add(config.serverName);
@@ -69,7 +69,10 @@ export interface StreamableHttpConfig {
69
69
  }
70
70
  /** Configuration for one stdio or Streamable HTTP MCP server. */
71
71
  export type Config = StdioConfig | StreamableHttpConfig;
72
- export declare const Config: z<Config>;
72
+ type StdioConfigInput = Omit<StdioConfig, 'args' | 'env' | 'cwd' | 'toolCallTimeoutMs' | 'failOnStartupError'> & Partial<Pick<StdioConfig, 'args' | 'env' | 'cwd' | 'toolCallTimeoutMs' | 'failOnStartupError'>>;
73
+ type StreamableHttpConfigInput = Omit<StreamableHttpConfig, 'headers' | 'toolCallTimeoutMs' | 'failOnStartupError'> & Partial<Pick<StreamableHttpConfig, 'headers' | 'toolCallTimeoutMs' | 'failOnStartupError'>>;
74
+ type ConfigInput = StdioConfigInput | StreamableHttpConfigInput;
75
+ export declare const Config: z<ConfigInput, Config>;
73
76
  /**
74
77
  * Connect one MCP server and publish its initial tool generation before activation.
75
78
  * This entry remains explicitly `async`: Cordis treats a prototype-bearing
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
15
  import type { Context } from '@deepseek-ai/cordis';
16
- import type { JsonValue } from '@deepseek-ai/dsh-tools';
16
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values';
17
17
  /** Resolved options relevant to tool bridging. */
18
18
  export interface ToolBridgeOptions {
19
19
  /** Whether a registry conflict is contained or rejects this synchronization. */
@@ -23,7 +23,7 @@ export interface ToolBridgeOptions {
23
23
  }
24
24
  /** State for one sync generation: the current set of disposers keyed by public name. */
25
25
  export type ToolDisposers = Map<string, () => void>;
26
- /** Canonical MCP result exposed to Code Mode without discarding protocol blocks. */
26
+ /** Canonical MCP result exposed to PTC mode without discarding protocol blocks. */
27
27
  export type McpResult<Structured extends JsonValue = JsonValue> = {
28
28
  content: JsonValue[];
29
29
  structuredContent?: Structured;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-mcp-client",
3
3
  "description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools",
4
- "version": "0.1.1-rc.1",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,29 +32,31 @@
32
32
  ],
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@deepseek-ai/dsh-attachment": "^0.1.1-rc.1",
36
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
37
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
38
- "@deepseek-ai/dsh-subprocess": "^0.1.1-rc.1",
39
- "@deepseek-ai/dsh-timeout": "^0.1.1-rc.1",
40
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
41
- "@deepseek-ai/cordis": "^4.0.1"
35
+ "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.2",
36
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
37
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
38
+ "@deepseek-ai/dsh-scope": "^0.1.2-alpha.2",
39
+ "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2",
40
+ "@deepseek-ai/cordis": "^4.0.2",
41
+ "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
42
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2"
42
43
  },
43
44
  "dependencies": {
44
45
  "@modelcontextprotocol/sdk": "^1.12.0",
45
46
  "zod": "^4.4.3",
46
- "@deepseek-ai/schemastery": "^3.18.1"
47
+ "@deepseek-ai/schemastery": "^3.18.2"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@modelcontextprotocol/server-everything": "^2026.7.4",
50
51
  "@modelcontextprotocol/server-filesystem": "^2026.7.4",
51
- "@deepseek-ai/dsh-attachment": "^0.1.1-rc.1",
52
- "@deepseek-ai/dsh-attachment-local": "^0.1.1-rc.1",
53
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
54
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
55
- "@deepseek-ai/dsh-subprocess": "^0.1.1-rc.1",
56
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
57
- "@deepseek-ai/dsh-timeout": "^0.1.1-rc.1",
58
- "@deepseek-ai/cordis": "^4.0.1"
52
+ "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.2",
53
+ "@deepseek-ai/dsh-attachment-local": "^0.1.2-alpha.2",
54
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
55
+ "@deepseek-ai/dsh-scope": "^0.1.2-alpha.2",
56
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
57
+ "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2",
58
+ "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
59
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
60
+ "@deepseek-ai/cordis": "^4.0.2"
59
61
  }
60
62
  }