@deepseek-ai/dsh-mcp-client 0.0.1-rc.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/mcp/mcp-client/README.md
5
+ README.md: 76d1271f6f7a3e9c959bdcf5e969906f25563c56
6
+ README.zh.md: b2da1119af3a8d52761a5040059e7a9d922aa567
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @deepseek-ai/dsh-mcp-client
2
+
3
+ English | [中文](README.zh.md)
4
+
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>`).
6
+
7
+ ## Usage
8
+
9
+ One plugin instance per MCP server in `cordis.yml`:
10
+
11
+ ```yaml
12
+ - id: mcp-github
13
+ name: '@deepseek-ai/dsh-mcp-client'
14
+ config:
15
+ serverName: github
16
+ transport: stdio
17
+ command: npx
18
+ args: ['-y', '@modelcontextprotocol/server-github']
19
+ env:
20
+ GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
21
+
22
+ - id: mcp-web
23
+ name: '@deepseek-ai/dsh-mcp-client'
24
+ config:
25
+ serverName: web
26
+ transport: streamable-http
27
+ url: http://localhost:3000/mcp
28
+ headers:
29
+ Authorization: !!js '`Bearer ${process.env.MCP_TOKEN}`'
30
+ ```
31
+
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
+
49
+ ## Tool naming
50
+
51
+ 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.
52
+
53
+ - Two servers publishing the same raw name (e.g. `search`) coexist under their namespaces.
54
+ - A duplicate `serverName` across live instances fails the later plugin instance at load.
55
+ - A server listing the same tool name twice is rejected as an invalid tool list.
56
+ - A foreign registration squatting on this server's namespace rolls back the whole generation (never a partial set), with a loud error.
57
+
58
+ ## Behavior
59
+
60
+ - 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.
61
+ - 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.
62
+ - Tool execute: `client.callTool({ name: rawName, arguments }, { signal })` with timeout + abort support—the public name is never sent to the server.
63
+ - 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`.
64
+ - Native/model rendering keeps the existing text projection: text blocks join with newlines while image, audio, resource, and unsupported blocks become placeholders.
65
+ - On disconnect/crash: no auto-reconnect. Registered tools remain until plugin disposal or a successful re-sync, and calls can fail against the closed transport; reload with HMR or restart the Host to reconnect.
66
+
67
+ ## Services consumed
68
+
69
+ | Service | Usage |
70
+ |---|---|
71
+ | `ctx.tools` | Register/unregister MCP tools |
72
+
73
+ ## Model Experience
74
+
75
+ ### Discovered MCP tools
76
+
77
+ #### What the model sees
78
+
79
+ 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 replaces the generation; plugin disposal removes it.
80
+
81
+ #### Token effect
82
+
83
+ 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.
84
+
85
+ #### KV Cache effect
86
+
87
+ 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.
88
+
89
+ ### Tool-call history and results
90
+
91
+ #### What the model sees
92
+
93
+ The public tool name and JSON arguments remain in assistant history. Text result blocks are joined with newlines into one retained Native text result; image, audio, resource, and unsupported blocks become short placeholders there. Their full JSON blocks and optional structured content remain in the execution-local canonical value, and MCP `isError` rejects the call through the registry's error path.
94
+
95
+ #### Token effect
96
+
97
+ Arguments and mapped text are retained until compaction. Binary and resource payloads are discarded rather than added to context.
98
+
99
+ #### KV Cache effect
100
+
101
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
102
+
103
+ ## Known Limitations and Deferred Work
104
+
105
+ - **Tools are the only bridged MCP capability** — Resources and Prompts have no harness consumption surface and are deferred.
106
+ - **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.
107
+ - **Crash recovery is manual** — transport closure does not auto-reconnect; registered tools can remain visible but fail against the closed transport until an HMR reload or Host restart.
108
+ - **Native non-text rendering is lossy** — image, audio, and resource payloads become placeholders in model context even though the execution-local canonical value preserves their JSON blocks. Richer Native multimedia projection is deferred.
109
+ - **Unsupported MCP output schemas are not enforced** — `structuredContent` falls back to `JsonValue` when the advertised schema uses vocabulary outside the harness subset.
package/README.zh.md ADDED
@@ -0,0 +1,109 @@
1
+ # @deepseek-ai/dsh-mcp-client
2
+
3
+ [English](README.md) | 中文
4
+
5
+ MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelcontextprotocol.io/) 服务器,把它们的工具注册到 `ctx.tools`,使模型能够通过服务器限定名称(`mcp__<serverName>__<rawName>`)将其作为原生工具使用。
6
+
7
+ ## 用法
8
+
9
+ `cordis.yml` 中每个 MCP 服务器使用一个插件实例:
10
+
11
+ ```yaml
12
+ - id: mcp-github
13
+ name: '@deepseek-ai/dsh-mcp-client'
14
+ config:
15
+ serverName: github
16
+ transport: stdio
17
+ command: npx
18
+ args: ['-y', '@modelcontextprotocol/server-github']
19
+ env:
20
+ GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
21
+
22
+ - id: mcp-web
23
+ name: '@deepseek-ai/dsh-mcp-client'
24
+ config:
25
+ serverName: web
26
+ transport: streamable-http
27
+ url: http://localhost:3000/mcp
28
+ headers:
29
+ Authorization: !!js '`Bearer ${process.env.MCP_TOKEN}`'
30
+ ```
31
+
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
+
49
+ ## 工具命名
50
+
51
+ 每个 MCP 工具都有两个名称:通过 `tools/call` 在协议上传送的原始 MCP 名称,以及公开名称 `mcp__<serverName>__<rawName>`,后者注册到 `ctx.tools`。公开名称会规范化为 DeepSeek 函数名称约定(64 个字符、`[A-Za-z0-9_-]`);如果替换或截断改变名称,就会追加 `(serverName, rawName)` 的确定性 12 位十六进制 hash,确保不同工具绝不会折叠为同一个名称。名称是 `(serverName, rawName)` 的纯函数:连接顺序、重新同步和其他服务器永远不会重命名工具。
52
+
53
+ - 发布相同原始名称(例如 `search`)的两个服务器会在各自 namespace 下共存。
54
+ - 存活实例中的重复 `serverName` 会使后加载的插件实例失败。
55
+ - 服务器在工具列表中两次列出同一工具名称时,该列表会作为无效工具列表被拒绝。
56
+ - 外部注册抢占该服务器 namespace 时,会回滚整个世代(绝不保留部分集合),并明确报错。
57
+
58
+ ## 行为
59
+
60
+ - 连接时:插件激活会等待 `listTools()`,并在组合开始首个轮次前通过 `ctx.tools.register()` 以公开名称注册每个工具。初始连接、发现或注册失败始终会记录日志;`failOnStartupError` 为 true 时拒绝激活,否则插件仍会激活但不注册工具。
61
+ - 监听 `notifications/tools/list_changed` → 重新同步;获取阶段失败时保留上一世代的注册,注册冲突则会回滚本次尝试的世代,并且不保留该服务器的任何工具。
62
+ - 工具执行:`client.callTool({ name: rawName, arguments }, { signal })`,支持超时 + 中止;公开名称绝不会发给服务器。
63
+ - 规范成功值是 `{ content: JsonValue[], structuredContent? }`;完整的 JSON MCP 块会保留给编程调用方。受支持且已声明的 `outputSchema` 会验证 `structuredContent`;不受支持的 schema 词汇会回退为不受约束的 `JsonValue`。
64
+ - Native/模型渲染保留现有文本投影:文本块以换行连接,图片、音频、资源和不受支持的块会变成占位符。
65
+ - 断开/崩溃时:不自动重新连接。已注册工具会一直保留到对插件执行 dispose(资源释放)或成功重新同步,针对已关闭传输的调用可能失败;请通过 HMR 重新加载或重启 Host 来重新连接。
66
+
67
+ ## 消费的服务
68
+
69
+ | 服务 | 用途 |
70
+ |---|---|
71
+ | `ctx.tools` | 注册/注销 MCP 工具 |
72
+
73
+ ## 模型体验
74
+
75
+ ### 已发现的 MCP 工具
76
+
77
+ #### 模型看到的内容
78
+
79
+ 初始发现成功后,每个已声明的 MCP 工具都会显示为名为 `mcp__<serverName>__<rawName>`(或其确定性规范化形式)的原生工具,并携带服务器提供的描述和输入 schema。成功的重新同步会替换整个世代;对插件执行 dispose 会移除该世代。
80
+
81
+ #### Token 影响
82
+
83
+ 工具注册期间,每次请求都会承担数据相关的 schema 成本。重新同步会替换而非累积 schema,服务器限定名称也会为每个工具定义和调用增加 token。
84
+
85
+ #### KV Cache 影响
86
+
87
+ 只要已发现工具集合及其 schema 不变,前缀就保持稳定。增加、移除、重命名或更改工具的重新同步会替换定义,并可能使从第一个变化的 schema token 起的复用失效。
88
+
89
+ ### 工具调用历史与结果
90
+
91
+ #### 模型看到的内容
92
+
93
+ 公开工具名称和 JSON 参数会保留在 assistant 历史中。文本结果块会以换行连接为一个保留的 Native 文本结果;图片、音频、资源和不受支持的块在其中变为简短占位符。它们的完整 JSON 块及可选结构化内容保留在执行局部的规范值中;MCP `isError` 会通过注册表的错误路径拒绝调用。
94
+
95
+ #### Token 影响
96
+
97
+ 参数和映射后的文本会保留到压缩(compaction)发生时。二进制与资源载荷会被丢弃,而不会加入上下文。
98
+
99
+ #### KV Cache 影响
100
+
101
+ 仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
102
+
103
+ ## 已知限制与暂缓事项
104
+
105
+ - **只桥接 MCP 的工具能力**:资源和提示词没有 harness 消费接口,暂缓实现。
106
+ - **启动超时继承自 MCP SDK**:DSH 尚未公开连接/发现超时。每次 initialize 请求或分页 `tools/list` 请求都使用 SDK 默认的 60 秒,因此在初始同步完成期间,无响应的 server 或 cursor chain 可能同时延迟激活与 teardown。
107
+ - **崩溃恢复需要手动触发**:传输关闭后不会自动重新连接;已注册工具可能仍然可见,但会因传输已关闭而调用失败,直到 HMR 重载或重启 Host。
108
+ - **Native 非文本渲染有损**:图片、音频与资源载荷在模型上下文中会变成占位符,即使执行局部的规范值保留了其 JSON 块。更丰富的 Native 多媒体投影暂缓实现。
109
+ - **不强制执行不受支持的 MCP 输出 schema**:已声明 schema 使用 harness 子集之外的词汇时,`structuredContent` 会回退到 `JsonValue`。
package/lib/index.js ADDED
@@ -0,0 +1,381 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { ListToolsResultSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+ import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
7
+ import { createHash } from "node:crypto";
8
+ import { z as z$1 } from "zod";
9
+ import { assertSupportedJsonSchema } from "@deepseek-ai/dsh-tools";
10
+ //#region lib/types/transport.js
11
+ /**
12
+ * Transport factory: creates the appropriate MCP transport based on the
13
+ * plugin's resolved config. Stdio spawns a child process (with credential
14
+ * scrubbing); Streamable HTTP connects to a URL.
15
+ *
16
+ * @module
17
+ */
18
+ /**
19
+ * The subprocess seam's scrubbed parent env (credential-shaped and stale
20
+ * `DSH_*` names dropped), plus the spec's explicit env. The MCP SDK owns the
21
+ * actual spawn, so this transport shares the scrub definition rather than the
22
+ * spawn path.
23
+ */
24
+ function buildChildEnv(extra) {
25
+ return {
26
+ ...scrubbedParentEnv(),
27
+ ...extra
28
+ };
29
+ }
30
+ /**
31
+ * Create an MCP transport from the resolved plugin config.
32
+ *
33
+ * @param config - Resolved plugin config discriminated on `transport`.
34
+ * @returns A connected-ready MCP Transport (stdio or Streamable HTTP).
35
+ */
36
+ function createTransport(config) {
37
+ switch (config.transport) {
38
+ case "stdio": return new StdioClientTransport({
39
+ command: config.command,
40
+ args: config.args,
41
+ env: buildChildEnv(config.env),
42
+ cwd: config.cwd
43
+ });
44
+ case "streamable-http": return new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers: config.headers } });
45
+ }
46
+ }
47
+ //#endregion
48
+ //#region lib/types/tools.js
49
+ /**
50
+ * Tool bridge: discovers MCP tools, registers them on the harness ToolRegistry
51
+ * under deterministic server-qualified public names, and handles re-sync when
52
+ * the server's tool list changes.
53
+ *
54
+ * Naming contract (see the mcp-client Agent Note "Naming invariants"): every MCP tool
55
+ * has the stable identity `(serverName, rawName)`; the model-facing public name
56
+ * is `mcp__<serverName>__<rawName>`, normalized to the DeepSeek function-name
57
+ * constraints. The raw name is only ever sent on the wire (`tools/call`); the
58
+ * public name is never parsed to recover it.
59
+ *
60
+ * @module
61
+ */
62
+ /**
63
+ * DeepSeek function-name contract: at most 64 characters. Wire-protocol
64
+ * constant, not configuration.
65
+ */
66
+ const MAX_PUBLIC_NAME_LENGTH = 64;
67
+ /** DeepSeek function-name contract: only `[A-Za-z0-9_-]` is allowed. */
68
+ const INVALID_NAME_CHARS = /[^A-Za-z0-9_-]/g;
69
+ /** Hex chars of the SHA-256 identity hash appended on lossy normalization. */
70
+ const HASH_LENGTH = 12;
71
+ /** Raw result record: the bridge owns JSON-value validation after transport. */
72
+ const RawCallToolResultSchema = z$1.record(z$1.string(), z$1.unknown());
73
+ /** List without mutating the SDK's per-page output-validator cache. */
74
+ function listToolsUncached(client, cursor) {
75
+ return client.request({
76
+ method: "tools/list",
77
+ ...cursor === void 0 ? {} : { params: { cursor } }
78
+ }, ListToolsResultSchema);
79
+ }
80
+ /** Call without the SDK pre-validating an output schema the bridge may not support. */
81
+ function callToolUncached(client, rawName, args, exec, opts) {
82
+ return client.request({
83
+ method: "tools/call",
84
+ params: {
85
+ name: rawName,
86
+ arguments: args
87
+ }
88
+ }, RawCallToolResultSchema, {
89
+ signal: exec.signal,
90
+ timeout: opts.toolCallTimeoutMs
91
+ });
92
+ }
93
+ /**
94
+ * Derive the model-facing public name for one MCP tool.
95
+ *
96
+ * Deterministic pure function of `(serverName, rawName)`: the clean case is
97
+ * `mcp__<serverName>__<rawName>` verbatim. When character replacement or
98
+ * truncation to the DeepSeek function-name contract (64 chars,
99
+ * `[A-Za-z0-9_-]`) changes the name, a 12-hex-char SHA-256 hash of the
100
+ * identity is appended so distinct MCP identities never collapse into the
101
+ * same public name.
102
+ *
103
+ * @param serverName - Stable local namespace from plugin config.
104
+ * @param rawName - The MCP server's own tool name.
105
+ * @returns The globally unique, model-facing ToolRegistry name.
106
+ */
107
+ function publicToolName(serverName, rawName) {
108
+ const joined = `mcp__${serverName}__${rawName}`;
109
+ const normalized = joined.replace(INVALID_NAME_CHARS, "_");
110
+ if (normalized === joined && normalized.length <= MAX_PUBLIC_NAME_LENGTH) return normalized;
111
+ const hash = createHash("sha256").update(`${serverName}\0${rawName}`).digest("hex").slice(0, HASH_LENGTH);
112
+ return `${normalized.slice(0, MAX_PUBLIC_NAME_LENGTH - HASH_LENGTH - 1)}_${hash}`;
113
+ }
114
+ /**
115
+ * Sync the MCP server's tool list into the harness ToolRegistry.
116
+ *
117
+ * Two phases keep the swap safe:
118
+ *
119
+ * 1. Fetch: drain uncached `tools/list` pagination and build the full next
120
+ * generation of `ToolDefinition`s under public names. Any failure here
121
+ * (network error, duplicate raw name in the server's list) rejects and
122
+ * leaves the previous generation registered untouched.
123
+ * 2. Swap: dispose the previous generation, register the new one. A registry
124
+ * conflict here can only mean a foreign registration squats on this
125
+ * server's `mcp__<serverName>__` namespace — the partial generation is
126
+ * rolled back (zero tools from this server) and logged. Initial strict
127
+ * synchronization may propagate the conflict so its parent transaction
128
+ * rejects; ordinary clients and later re-syncs return an empty map.
129
+ *
130
+ * @param client - Connected MCP Client instance used to list and call tools.
131
+ * @param ctx - Cordis context providing the `tools` service for registration.
132
+ * @param opts - Bridge options: server namespace and per-call timeout.
133
+ * @param previous - Disposer map from the prior sync generation; disposed
134
+ * during the swap phase (only after the fetch phase succeeded).
135
+ * @returns A map of registered public tool names to their unregister
136
+ * disposers — the exact set of live registrations owned by this server.
137
+ */
138
+ async function syncTools(client, ctx, opts, previous) {
139
+ const definitions = /* @__PURE__ */ new Map();
140
+ let cursor;
141
+ do {
142
+ const response = await listToolsUncached(client, cursor);
143
+ for (const tool of response.tools) {
144
+ const publicName = publicToolName(opts.serverName, tool.name);
145
+ if (definitions.has(publicName)) throw new Error(`mcp-client(${opts.serverName}): server listed tool "${tool.name}" more than once — invalid tool list`);
146
+ definitions.set(publicName, {
147
+ name: publicName,
148
+ description: tool.description ?? "",
149
+ parameters: tool.inputSchema,
150
+ output: createOutput(tool.name, supportedOutputSchema(tool.outputSchema)),
151
+ execute: createExecutor(client, tool.name, tool.execution?.taskSupport === "required", opts)
152
+ });
153
+ }
154
+ cursor = response.nextCursor;
155
+ } while (cursor);
156
+ for (const dispose of previous.values()) dispose();
157
+ const disposers = /* @__PURE__ */ new Map();
158
+ try {
159
+ for (const [publicName, definition] of definitions) disposers.set(publicName, ctx.tools.register(definition));
160
+ } catch (error) {
161
+ for (const dispose of disposers.values()) dispose();
162
+ ctx.logger.error(`mcp-client(${opts.serverName}): tool registration failed, no tools registered: ${String(error)}`);
163
+ if (opts.registrationFailure === "throw") throw error;
164
+ return /* @__PURE__ */ new Map();
165
+ }
166
+ return disposers;
167
+ }
168
+ /** Keep a supported advertised schema; unsupported MCP vocabulary falls back to JsonValue. */
169
+ function supportedOutputSchema(candidate) {
170
+ if (candidate === void 0) return void 0;
171
+ try {
172
+ assertSupportedJsonSchema(candidate);
173
+ return candidate;
174
+ } catch {
175
+ return;
176
+ }
177
+ }
178
+ /** Build the canonical result schema and existing Native text projection. */
179
+ function createOutput(rawName, structuredSchema) {
180
+ return {
181
+ schema: {
182
+ type: "object",
183
+ properties: {
184
+ content: {
185
+ type: "array",
186
+ items: {}
187
+ },
188
+ structuredContent: structuredSchema ?? {}
189
+ },
190
+ required: structuredSchema === void 0 ? ["content"] : ["content", "structuredContent"],
191
+ additionalProperties: false
192
+ },
193
+ render(_args, value) {
194
+ return [{
195
+ type: "text",
196
+ text: extractText(value.content, rawName)
197
+ }];
198
+ }
199
+ };
200
+ }
201
+ /**
202
+ * Create an execute function for one MCP tool. The executor closes over the
203
+ * raw MCP tool name and sends an uncached `tools/call` request with it (never
204
+ * the public name), with abort signal and timeout, then maps the result to
205
+ * harness ContentBlocks. Owning the raw request prevents the SDK's internal
206
+ * per-page schema cache from pre-validating a different contract.
207
+ *
208
+ * When the MCP server returns `isError: true`, the executor throws so that
209
+ * the ToolRegistry's catch path produces an `isError` result for the model.
210
+ */
211
+ function createExecutor(client, rawName, taskRequired, opts) {
212
+ return async (args, exec) => {
213
+ if (taskRequired) throw new Error(`Tool "${rawName}" requires task-based execution, which this bridge does not support`);
214
+ const result = await callToolUncached(client, rawName, typeof args === "object" && args !== null ? args : {}, exec, opts);
215
+ if (!Array.isArray(result.content)) {
216
+ const rendered = "toolResult" in result ? JSON.stringify(result.toolResult) : "(no output)";
217
+ const text = typeof rendered === "string" ? rendered : "(no output)";
218
+ if (result.isError === true) throw new Error(text);
219
+ return {
220
+ content: [{
221
+ type: "text",
222
+ text
223
+ }],
224
+ ...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {}
225
+ };
226
+ }
227
+ const content = result.content;
228
+ const text = extractText(content, rawName);
229
+ if (result.isError === true) throw new Error(text);
230
+ return {
231
+ content,
232
+ ...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {}
233
+ };
234
+ };
235
+ }
236
+ /**
237
+ * Extract text from an MCP content array into a single string.
238
+ * - text blocks: join with '\n'
239
+ * - image/audio/resource blocks: replaced with a placeholder
240
+ *
241
+ * Defensive: fields that the MCP spec declares required (mimeType, text) are
242
+ * guarded with fallbacks because this is a network trust boundary.
243
+ */
244
+ function extractText(mcpContent, toolName) {
245
+ const parts = [];
246
+ for (const value of mcpContent) {
247
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
248
+ parts.push("[unsupported content type: unknown]");
249
+ continue;
250
+ }
251
+ const block = value;
252
+ switch (block.type) {
253
+ case "text":
254
+ if (block.text !== void 0) parts.push(block.text);
255
+ break;
256
+ case "image":
257
+ parts.push(`[image: ${block.mimeType ?? "unknown"}, content discarded]`);
258
+ break;
259
+ case "audio":
260
+ parts.push(`[audio: ${block.mimeType ?? "unknown"}, content discarded]`);
261
+ break;
262
+ case "resource":
263
+ case "resource_link":
264
+ parts.push("[resource: content discarded]");
265
+ break;
266
+ default: parts.push(`[unsupported content type: ${block.type}]`);
267
+ }
268
+ }
269
+ return parts.join("\n") || `(${toolName} returned no text content)`;
270
+ }
271
+ //#endregion
272
+ //#region lib/types/index.js
273
+ /**
274
+ * MCP client bridge plugin: connects to an external MCP server and registers
275
+ * its tools on `ctx.tools` under server-qualified public names
276
+ * (`mcp__<serverName>__<rawName>`). Each plugin instance connects to one MCP
277
+ * server; load multiple instances in `cordis.yml` for multiple servers.
278
+ *
279
+ * Namespace plugin (named exports, no default export). Lifecycle is
280
+ * effect-scoped: disposal disconnects from the server, unregisters all tools,
281
+ * and releases the `serverName` namespace reservation. HMR hot-swaps by
282
+ * disposing the old instance and creating a new one; identical `serverName`
283
+ * reproduces identical public tool names.
284
+ *
285
+ * @module @deepseek-ai/dsh-mcp-client
286
+ */
287
+ /** Cordis plugin name used by loader diagnostics. */
288
+ const name = "mcp-client";
289
+ /** Services required by this plugin. */
290
+ const inject = ["tools"];
291
+ /** Default timeout for individual MCP tool calls (ms). */
292
+ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 6e4;
293
+ /** Valid `serverName`, kept below the public tool-name budget. */
294
+ const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
295
+ /**
296
+ * Live `serverName` reservations per app, keyed off `ctx.root` (multiple apps
297
+ * in one process — tests — must not see each other's names). A duplicate
298
+ * namespace is a configuration error surfaced at plugin load, never silent
299
+ * shadowing.
300
+ */
301
+ const activeServerNames = /* @__PURE__ */ new WeakMap();
302
+ const Config = z.union([z.object({
303
+ transport: z.const("stdio"),
304
+ serverName: z.string().required().pattern(SERVER_NAME_PATTERN),
305
+ command: z.string().required(),
306
+ args: z.array(String).default([]),
307
+ env: z.dict(String).default({}),
308
+ cwd: z.string().default(""),
309
+ toolCallTimeoutMs: z.number().default(DEFAULT_TOOL_CALL_TIMEOUT_MS),
310
+ failOnStartupError: z.boolean().default(false)
311
+ }), z.object({
312
+ transport: z.const("streamable-http"),
313
+ serverName: z.string().required().pattern(SERVER_NAME_PATTERN),
314
+ url: z.string().required(),
315
+ headers: z.dict(String).default({}),
316
+ toolCallTimeoutMs: z.number().default(DEFAULT_TOOL_CALL_TIMEOUT_MS),
317
+ failOnStartupError: z.boolean().default(false)
318
+ })]);
319
+ /**
320
+ * Connect one MCP server and publish its initial tool generation before activation.
321
+ * This entry remains explicitly `async`: Cordis treats a prototype-bearing
322
+ * ordinary function as a constructor, whose returned Promise is not startup work.
323
+ * @param ctx - plugin context carrying the tool registry.
324
+ * @param config - resolved transport and server namespace configuration.
325
+ * @returns startup readiness after connection and initial tool discovery settle.
326
+ */
327
+ async function apply(ctx, config) {
328
+ ctx.effect(() => {
329
+ let names = activeServerNames.get(ctx.root);
330
+ if (!names) {
331
+ names = /* @__PURE__ */ new Set();
332
+ activeServerNames.set(ctx.root, names);
333
+ }
334
+ 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`);
335
+ names.add(config.serverName);
336
+ return () => void names.delete(config.serverName);
337
+ }, "mcp-client.serverName");
338
+ const transport = createTransport(config);
339
+ const client = new Client({
340
+ name: "dsh-mcp-client",
341
+ version: "0.0.1"
342
+ }, { capabilities: {} });
343
+ const opts = {
344
+ registrationFailure: "contain",
345
+ serverName: config.serverName,
346
+ toolCallTimeoutMs: config.toolCallTimeoutMs
347
+ };
348
+ const ready = (async () => {
349
+ await client.connect(transport);
350
+ let disposers = await syncTools(client, ctx, {
351
+ ...opts,
352
+ registrationFailure: config.failOnStartupError ? "throw" : "contain"
353
+ }, /* @__PURE__ */ new Map());
354
+ client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
355
+ ctx.logger.info(`mcp-client(${config.serverName}): tool list changed, re-syncing`);
356
+ try {
357
+ disposers = await syncTools(client, ctx, opts, disposers);
358
+ } catch (error) {
359
+ ctx.logger.error(`mcp-client(${config.serverName}): tool re-sync failed: ${String(error)}`);
360
+ }
361
+ });
362
+ return { getDisposers: () => disposers };
363
+ })().catch((error) => {
364
+ ctx.logger.error(`mcp-client(${config.serverName}): startup failed: ${String(error)}`);
365
+ return {
366
+ getDisposers: () => /* @__PURE__ */ new Map(),
367
+ error
368
+ };
369
+ });
370
+ ctx.effect(() => async () => {
371
+ const outcome = await ready;
372
+ for (const dispose of outcome.getDisposers().values()) dispose();
373
+ try {
374
+ await client.close();
375
+ } catch {}
376
+ }, "mcp-client.connection");
377
+ const outcome = await ready;
378
+ if ("error" in outcome && config.failOnStartupError) throw new Error(`mcp-client(${config.serverName}): initial connection or tool synchronization failed`, { cause: outcome.error });
379
+ }
380
+ //#endregion
381
+ export { Config, apply, inject, name };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-mcp-client`.
4
+ * @module @deepseek-ai/dsh-mcp-client/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-mcp-client";
7
+ /** Cordis companion plugin name. */
8
+ const name = "mcp-client-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: MCP generations contribute through the tool registry, but the bridge
13
+ * exposes no independent server-to-tool snapshot after an asynchronous resync.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * MCP client bridge plugin: connects to an external MCP server and registers
3
+ * its tools on `ctx.tools` under server-qualified public names
4
+ * (`mcp__<serverName>__<rawName>`). Each plugin instance connects to one MCP
5
+ * server; load multiple instances in `cordis.yml` for multiple servers.
6
+ *
7
+ * Namespace plugin (named exports, no default export). Lifecycle is
8
+ * effect-scoped: disposal disconnects from the server, unregisters all tools,
9
+ * and releases the `serverName` namespace reservation. HMR hot-swaps by
10
+ * disposing the old instance and creating a new one; identical `serverName`
11
+ * reproduces identical public tool names.
12
+ *
13
+ * @module @deepseek-ai/dsh-mcp-client
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ import z from '@deepseek-ai/schemastery';
17
+ export type { McpResult } from './tools.ts';
18
+ /** Cordis plugin name used by loader diagnostics. */
19
+ export declare const name = "mcp-client";
20
+ /** Services required by this plugin. */
21
+ export declare const inject: string[];
22
+ /** Config for connecting to an MCP server via a spawned child process over stdio. */
23
+ export interface StdioConfig {
24
+ /** Selects child-process stdio transport. */
25
+ transport: 'stdio';
26
+ /**
27
+ * Stable local namespace for this server's model-facing tool names
28
+ * (`mcp__<serverName>__<rawName>`). Must match `[A-Za-z0-9_-]{1,32}` and be
29
+ * unique across live mcp-client instances.
30
+ */
31
+ serverName: string;
32
+ /** Executable used to start the server. */
33
+ command: string;
34
+ /** Arguments passed directly, without shell interpolation. */
35
+ args: string[];
36
+ /** Extra env vars merged on top of scrubbed ambient env. */
37
+ env: Record<string, string>;
38
+ /** Working directory for the child process. */
39
+ cwd: string;
40
+ /** Per-tool-call timeout in milliseconds. */
41
+ toolCallTimeoutMs: number;
42
+ /** Fail plugin activation when the initial connection or tool synchronization fails. */
43
+ failOnStartupError: boolean;
44
+ }
45
+ /** Config for connecting to an MCP server over Streamable HTTP (SSE). */
46
+ export interface StreamableHttpConfig {
47
+ /** Selects Streamable HTTP transport. */
48
+ transport: 'streamable-http';
49
+ /**
50
+ * Stable local namespace for this server's model-facing tool names
51
+ * (`mcp__<serverName>__<rawName>`). Must match `[A-Za-z0-9_-]{1,32}` and be
52
+ * unique across live mcp-client instances.
53
+ */
54
+ serverName: string;
55
+ /** MCP endpoint URL. */
56
+ url: string;
57
+ /** Additional headers attached to MCP requests. */
58
+ headers: Record<string, string>;
59
+ /** Per-tool-call timeout in milliseconds. */
60
+ toolCallTimeoutMs: number;
61
+ /** Fail plugin activation when the initial connection or tool synchronization fails. */
62
+ failOnStartupError: boolean;
63
+ }
64
+ /** Configuration for one stdio or Streamable HTTP MCP server. */
65
+ export type Config = StdioConfig | StreamableHttpConfig;
66
+ export declare const Config: z<Config>;
67
+ /**
68
+ * Connect one MCP server and publish its initial tool generation before activation.
69
+ * This entry remains explicitly `async`: Cordis treats a prototype-bearing
70
+ * ordinary function as a constructor, whose returned Promise is not startup work.
71
+ * @param ctx - plugin context carrying the tool registry.
72
+ * @param config - resolved transport and server namespace configuration.
73
+ * @returns startup readiness after connection and initial tool discovery settle.
74
+ */
75
+ export declare function apply(ctx: Context, config: Config): Promise<void>;
76
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-mcp-client`.
3
+ * @module @deepseek-ai/dsh-mcp-client/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "mcp-client-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tool bridge: discovers MCP tools, registers them on the harness ToolRegistry
3
+ * under deterministic server-qualified public names, and handles re-sync when
4
+ * the server's tool list changes.
5
+ *
6
+ * Naming contract (see the mcp-client Agent Note "Naming invariants"): every MCP tool
7
+ * has the stable identity `(serverName, rawName)`; the model-facing public name
8
+ * is `mcp__<serverName>__<rawName>`, normalized to the DeepSeek function-name
9
+ * constraints. The raw name is only ever sent on the wire (`tools/call`); the
10
+ * public name is never parsed to recover it.
11
+ *
12
+ * @module
13
+ */
14
+ import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ import type { JsonValue } from '@deepseek-ai/dsh-tools';
17
+ /** Resolved options relevant to tool bridging. */
18
+ export interface ToolBridgeOptions {
19
+ /** Whether a registry conflict is contained or rejects this synchronization. */
20
+ registrationFailure: 'contain' | 'throw';
21
+ serverName: string;
22
+ toolCallTimeoutMs: number;
23
+ }
24
+ /** State for one sync generation: the current set of disposers keyed by public name. */
25
+ export type ToolDisposers = Map<string, () => void>;
26
+ /** Canonical MCP result exposed to Code Mode without discarding protocol blocks. */
27
+ export type McpResult<Structured extends JsonValue = JsonValue> = {
28
+ content: JsonValue[];
29
+ structuredContent?: Structured;
30
+ };
31
+ /**
32
+ * Derive the model-facing public name for one MCP tool.
33
+ *
34
+ * Deterministic pure function of `(serverName, rawName)`: the clean case is
35
+ * `mcp__<serverName>__<rawName>` verbatim. When character replacement or
36
+ * truncation to the DeepSeek function-name contract (64 chars,
37
+ * `[A-Za-z0-9_-]`) changes the name, a 12-hex-char SHA-256 hash of the
38
+ * identity is appended so distinct MCP identities never collapse into the
39
+ * same public name.
40
+ *
41
+ * @param serverName - Stable local namespace from plugin config.
42
+ * @param rawName - The MCP server's own tool name.
43
+ * @returns The globally unique, model-facing ToolRegistry name.
44
+ */
45
+ export declare function publicToolName(serverName: string, rawName: string): string;
46
+ /**
47
+ * Sync the MCP server's tool list into the harness ToolRegistry.
48
+ *
49
+ * Two phases keep the swap safe:
50
+ *
51
+ * 1. Fetch: drain uncached `tools/list` pagination and build the full next
52
+ * generation of `ToolDefinition`s under public names. Any failure here
53
+ * (network error, duplicate raw name in the server's list) rejects and
54
+ * leaves the previous generation registered untouched.
55
+ * 2. Swap: dispose the previous generation, register the new one. A registry
56
+ * conflict here can only mean a foreign registration squats on this
57
+ * server's `mcp__<serverName>__` namespace — the partial generation is
58
+ * rolled back (zero tools from this server) and logged. Initial strict
59
+ * synchronization may propagate the conflict so its parent transaction
60
+ * rejects; ordinary clients and later re-syncs return an empty map.
61
+ *
62
+ * @param client - Connected MCP Client instance used to list and call tools.
63
+ * @param ctx - Cordis context providing the `tools` service for registration.
64
+ * @param opts - Bridge options: server namespace and per-call timeout.
65
+ * @param previous - Disposer map from the prior sync generation; disposed
66
+ * during the swap phase (only after the fetch phase succeeded).
67
+ * @returns A map of registered public tool names to their unregister
68
+ * disposers — the exact set of live registrations owned by this server.
69
+ */
70
+ export declare function syncTools(client: Client, ctx: Context, opts: ToolBridgeOptions, previous: ToolDisposers): Promise<ToolDisposers>;
71
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Transport factory: creates the appropriate MCP transport based on the
3
+ * plugin's resolved config. Stdio spawns a child process (with credential
4
+ * scrubbing); Streamable HTTP connects to a URL.
5
+ *
6
+ * @module
7
+ */
8
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
9
+ import type { Config } from './index.ts';
10
+ /**
11
+ * Create an MCP transport from the resolved plugin config.
12
+ *
13
+ * @param config - Resolved plugin config discriminated on `transport`.
14
+ * @returns A connected-ready MCP Transport (stdio or Streamable HTTP).
15
+ */
16
+ export declare function createTransport(config: Config): Transport;
17
+ //# sourceMappingURL=transport.d.ts.map
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-mcp-client",
3
+ "description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/mcp/mcp-client"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
37
+ "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1",
38
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
39
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.12.0",
43
+ "zod": "^4.4.3",
44
+ "@deepseek-ai/schemastery": "^3.18.1-rc.1"
45
+ },
46
+ "devDependencies": {
47
+ "@modelcontextprotocol/server-everything": "^2026.7.4",
48
+ "@modelcontextprotocol/server-filesystem": "^2026.7.4",
49
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
50
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
51
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
52
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
53
+ "@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1"
54
+ }
55
+ }