@deepseek-ai/dsh-mcp-client 0.1.0-rc.3 → 0.1.0-rc.7
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 +2 -2
- package/README.md +6 -4
- package/README.zh.md +6 -4
- package/lib/index.js +195 -20
- package/package.json +16 -13
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: f3bf65d90d72f9eb3271cbbbb8ae8c586a7fd082
|
|
6
|
+
README.zh.md: 1596ec72c28c4811eabb9f5cafe41cd7bdf1cf5e
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Every MCP tool has two names: the raw MCP name (sent on the wire in `tools/call`
|
|
|
65
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
66
|
- Tool execute: `client.callTool({ name: rawName, arguments }, { signal })` with timeout + abort support—the public name is never sent to the server.
|
|
67
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
|
|
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
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
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
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.
|
|
@@ -75,6 +75,8 @@ Every MCP tool has two names: the raw MCP name (sent on the wire in `tools/call`
|
|
|
75
75
|
| Service | Usage |
|
|
76
76
|
|---|---|
|
|
77
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 |
|
|
78
80
|
|
|
79
81
|
## Model Experience
|
|
80
82
|
|
|
@@ -96,11 +98,11 @@ Prefix-stable while the discovered tool set and schemas are unchanged. A re-sync
|
|
|
96
98
|
|
|
97
99
|
#### What the model sees
|
|
98
100
|
|
|
99
|
-
The public tool name and JSON arguments remain in assistant history.
|
|
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.
|
|
100
102
|
|
|
101
103
|
#### Token effect
|
|
102
104
|
|
|
103
|
-
Arguments
|
|
105
|
+
Arguments, mapped text, and durable image references are retained until compaction. Inline MCP base64 stays only in the execution-local canonical value and is never copied into a session event; the provider reads verified bytes from the attachment store. Audio and embedded-resource payloads stay out of model context.
|
|
104
106
|
|
|
105
107
|
#### KV Cache effect
|
|
106
108
|
|
|
@@ -111,5 +113,5 @@ Append-only; newly visible content follows the reusable request prefix and does
|
|
|
111
113
|
- **Tools are the only bridged MCP capability** — Resources and Prompts have no harness consumer and are deferred.
|
|
112
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.
|
|
113
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.
|
|
114
|
-
- **
|
|
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.
|
|
115
117
|
- **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
CHANGED
|
@@ -65,7 +65,7 @@ MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelc
|
|
|
65
65
|
- 监听 `notifications/tools/list_changed` → 重新同步;获取阶段失败时保留上一世代的注册,注册冲突则会回滚本次尝试的世代,并且不保留该服务器的任何工具。
|
|
66
66
|
- 工具执行:`client.callTool({ name: rawName, arguments }, { signal })`,支持超时 + 中止;公开名称绝不会发给服务器。
|
|
67
67
|
- 规范成功值是 `{ content: JsonValue[], structuredContent? }`;完整的 JSON MCP 块会保留给编程调用方。受支持且已声明的 `outputSchema` 会验证 `structuredContent`;不受支持的 schema 词汇会回退为不受约束的 `JsonValue`。
|
|
68
|
-
- Native
|
|
68
|
+
- Native/模型渲染会保留 MCP 块顺序。文本类连续块以换行连接;资源链接以文本保留名称和 URI;只有挂载 `ctx.attachments` 且确切调用模型路由明确声明支持图片输入时,受支持的图片才会成为持久核心图片块。整个图片批次会先完成解码与准入,再保存任一成员。格式错误或被拒绝的图片批次、音频、嵌入资源和不受支持的块会成为明确诊断文本,而不会消失。
|
|
69
69
|
- 断开/崩溃时:supervisor 以指数退避(`reconnect.initialDelayMs` 逐次翻倍,上限 `reconnect.maxDelayMs`)重启原始服务器配置,成功后重新执行发现——恢复的世代会替换前一个,因此工具既不会重复也不会泄漏。中断期间最后一个正常世代保持注册;针对它的调用在恢复前会失败。
|
|
70
70
|
- 重连按中断预算控制:连续失败达到 `reconnect.maxAttempts` 次后,该服务器的工具会被注销,重连停止,直到 HMR 重载或重启 Host。连接存活超过 `maxDelayMs` 会重置预算,因此偶尔崩溃的服务器可以无限恢复,而崩溃循环的服务器——即使短暂连接成功——仍会耗尽上限而非永远重启。
|
|
71
71
|
- 重连状态在日志中对用户可见:reconnecting(warn,含尝试次数和延迟)、recovered(info)、最终失败和 disabled-loss(error)。dispose(资源释放)会取消任何待执行的重连。设置 `reconnect.enabled: false` 时,连接丢失后工具保持注册但调用失败,直到重载——即手动恢复行为。
|
|
@@ -75,6 +75,8 @@ MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelc
|
|
|
75
75
|
| 服务 | 用途 |
|
|
76
76
|
|---|---|
|
|
77
77
|
| `ctx.tools` | 注册/注销 MCP 工具 |
|
|
78
|
+
| `ctx.attachments` | 可选;在模型投影前校验并持久保存图片结果批次 |
|
|
79
|
+
| `ctx.llm` | 可选;证明确切调用路由明确支持图片输入 |
|
|
78
80
|
|
|
79
81
|
## 模型体验
|
|
80
82
|
|
|
@@ -96,11 +98,11 @@ MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelc
|
|
|
96
98
|
|
|
97
99
|
#### 模型看到的内容
|
|
98
100
|
|
|
99
|
-
公开工具名称和 JSON 参数会保留在 assistant
|
|
101
|
+
公开工具名称和 JSON 参数会保留在 assistant 历史中。执行局部的规范值始终为程序化调用方和 Code Mode 保留完整 JSON MCP 块及可选结构化内容。在 Native 上下文中,受支持的图片块会在确切路由能力得到证明后,按原始顺序与文本一起持久投影;Code Mode 还会经外层 `run_code` 结果转运这份已经结算的丰富投影,而不改变规范绑定值。被拒绝的图片、音频、嵌入资源、资源链接和未知块会继续以有界文本诊断可见;MCP `isError` 会在持久化图片前拒绝调用。
|
|
100
102
|
|
|
101
103
|
#### Token 影响
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
参数、映射后的文本和持久图片引用会保留到压缩(compaction)发生时。内联 MCP base64 只存在于执行局部的规范值中,绝不会复制进会话事件;提供方会从附件存储读取经过校验的字节。音频和嵌入资源载荷仍不会进入模型上下文。
|
|
104
106
|
|
|
105
107
|
#### KV Cache 影响
|
|
106
108
|
|
|
@@ -111,5 +113,5 @@ MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelc
|
|
|
111
113
|
- **只桥接 MCP 的工具能力**:资源和提示词没有 harness 消费接口,暂缓实现。
|
|
112
114
|
- **启动超时继承自 MCP SDK**:DSH 尚未公开连接/发现超时。每次 initialize 请求或分页 `tools/list` 请求都使用 SDK 默认的 60 秒,因此在初始同步完成期间,无响应的 server 或 cursor chain 可能同时延迟激活与 teardown。
|
|
113
115
|
- **重连在传输关闭时触发**:崩溃的 stdio 子进程会触发重连;Streamable HTTP 失败通过每次请求以及 SDK 传输自身的 SSE(Server-Sent Events)流恢复机制暴露,因此不可达的 HTTP 服务器会按调用重试,而非由 supervisor 重新 spawn。
|
|
114
|
-
-
|
|
116
|
+
- **图片是唯一的持久丰富结果桥接**:PNG、JPEG、WebP 和 GIF 可以在确切能力得到证明后进入 Native 上下文。音频和嵌入资源载荷仍只存在于执行局部,并配有明确诊断;资源链接只以文本保留名称和 URI。
|
|
115
117
|
- **不强制执行不受支持的 MCP 输出 schema**:已声明 schema 使用 harness 子集之外的词汇时,`structuredContent` 会回退到 `JsonValue`。
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,9 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
6
6
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
7
|
import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
10
|
import { z as z$1 } from "zod";
|
|
11
|
+
import { isImageAdmissionError } from "@deepseek-ai/dsh-attachment";
|
|
10
12
|
import { assertSupportedJsonSchema } from "@deepseek-ai/dsh-tools";
|
|
11
13
|
//#region lib/types/transport.js
|
|
12
14
|
/**
|
|
@@ -71,6 +73,15 @@ const INVALID_NAME_CHARS = /[^A-Za-z0-9_-]/g;
|
|
|
71
73
|
const HASH_LENGTH = 12;
|
|
72
74
|
/** Raw result record: the bridge owns JSON-value validation after transport. */
|
|
73
75
|
const RawCallToolResultSchema = z$1.record(z$1.string(), z$1.unknown());
|
|
76
|
+
/** Raster formats supported by the durable attachment vocabulary. */
|
|
77
|
+
const IMAGE_MEDIA_TYPES = [
|
|
78
|
+
"image/png",
|
|
79
|
+
"image/jpeg",
|
|
80
|
+
"image/webp",
|
|
81
|
+
"image/gif"
|
|
82
|
+
];
|
|
83
|
+
/** Canonical RFC 4648 base64, excluding whitespace and URL-safe aliases. */
|
|
84
|
+
const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
74
85
|
/** List without mutating the SDK's per-page output-validator cache. */
|
|
75
86
|
function listToolsUncached(client, cursor) {
|
|
76
87
|
return client.request({
|
|
@@ -144,13 +155,7 @@ async function syncTools(client, ctx, opts, previous) {
|
|
|
144
155
|
for (const tool of response.tools) {
|
|
145
156
|
const publicName = publicToolName(opts.serverName, tool.name);
|
|
146
157
|
if (definitions.has(publicName)) throw new Error(`mcp-client(${opts.serverName}): server listed tool "${tool.name}" more than once — invalid tool list`);
|
|
147
|
-
definitions.set(publicName,
|
|
148
|
-
name: publicName,
|
|
149
|
-
description: tool.description ?? "",
|
|
150
|
-
parameters: tool.inputSchema,
|
|
151
|
-
output: createOutput(tool.name, supportedOutputSchema(tool.outputSchema)),
|
|
152
|
-
execute: createExecutor(client, tool.name, tool.execution?.taskSupport === "required", opts)
|
|
153
|
-
});
|
|
158
|
+
definitions.set(publicName, createDefinition(client, ctx, publicName, tool.name, tool.description ?? "", tool.inputSchema, supportedOutputSchema(tool.outputSchema), tool.execution?.taskSupport === "required", opts));
|
|
154
159
|
}
|
|
155
160
|
cursor = response.nextCursor;
|
|
156
161
|
} while (cursor);
|
|
@@ -176,6 +181,38 @@ function supportedOutputSchema(candidate) {
|
|
|
176
181
|
return;
|
|
177
182
|
}
|
|
178
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Build one generation-local tool definition and its execution-local rich projections.
|
|
186
|
+
* @param client - connected MCP client used for calls.
|
|
187
|
+
* @param ctx - plugin context carrying optional attachment and model services.
|
|
188
|
+
* @param publicName - registry-qualified public tool name.
|
|
189
|
+
* @param rawName - MCP wire tool name.
|
|
190
|
+
* @param description - model-facing tool description.
|
|
191
|
+
* @param parameters - MCP input schema.
|
|
192
|
+
* @param structuredSchema - supported structured-output schema, when advertised.
|
|
193
|
+
* @param taskRequired - whether this MCP tool requires unsupported task execution.
|
|
194
|
+
* @param opts - bridge timeout and namespace options.
|
|
195
|
+
* @returns a complete ToolRuntime definition.
|
|
196
|
+
*/
|
|
197
|
+
function createDefinition(client, ctx, publicName, rawName, description, parameters, structuredSchema, taskRequired, opts) {
|
|
198
|
+
const projections = /* @__PURE__ */ new WeakMap();
|
|
199
|
+
return {
|
|
200
|
+
name: publicName,
|
|
201
|
+
description,
|
|
202
|
+
parameters,
|
|
203
|
+
output: createOutput(rawName, structuredSchema),
|
|
204
|
+
execute: createExecutor(client, ctx, rawName, taskRequired, opts, projections),
|
|
205
|
+
finalizeContent(exec, result) {
|
|
206
|
+
const projection = projections.get(exec);
|
|
207
|
+
if (projection === void 0) return void 0;
|
|
208
|
+
projections.delete(exec);
|
|
209
|
+
if (result.isError) return void 0;
|
|
210
|
+
if (!isDeepStrictEqual(result.value, projection.value)) return void 0;
|
|
211
|
+
if (!isDeepStrictEqual(result.content, projection.fallback)) return void 0;
|
|
212
|
+
return projection.content;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
179
216
|
/** Build the canonical result schema and existing Native text projection. */
|
|
180
217
|
function createOutput(rawName, structuredSchema) {
|
|
181
218
|
return {
|
|
@@ -209,7 +246,7 @@ function createOutput(rawName, structuredSchema) {
|
|
|
209
246
|
* When the MCP server returns `isError: true`, the executor throws so that
|
|
210
247
|
* the ToolRuntime's catch path produces an `isError` result for the model.
|
|
211
248
|
*/
|
|
212
|
-
function createExecutor(client, rawName, taskRequired, opts) {
|
|
249
|
+
function createExecutor(client, ctx, rawName, taskRequired, opts, projections) {
|
|
213
250
|
return async (args, exec) => {
|
|
214
251
|
if (taskRequired) throw new Error(`Tool "${rawName}" requires task-based execution, which this bridge does not support`);
|
|
215
252
|
const result = await callToolUncached(client, rawName, typeof args === "object" && args !== null ? args : {}, exec, opts);
|
|
@@ -228,13 +265,124 @@ function createExecutor(client, rawName, taskRequired, opts) {
|
|
|
228
265
|
const content = result.content;
|
|
229
266
|
const text = extractText(content, rawName);
|
|
230
267
|
if (result.isError === true) throw new Error(text);
|
|
231
|
-
|
|
268
|
+
const value = {
|
|
232
269
|
content,
|
|
233
270
|
...result.structuredContent !== void 0 ? { structuredContent: result.structuredContent } : {}
|
|
234
271
|
};
|
|
272
|
+
if (containsImage(content)) {
|
|
273
|
+
const fallback = [{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: extractText(content, rawName)
|
|
276
|
+
}];
|
|
277
|
+
const projected = await prepareImageProjection(ctx, exec, content, rawName);
|
|
278
|
+
projections.set(exec, {
|
|
279
|
+
value,
|
|
280
|
+
fallback,
|
|
281
|
+
content: projected
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Whether an untrusted MCP content array contains a declared image block. */
|
|
288
|
+
function containsImage(content) {
|
|
289
|
+
return content.some((value) => isRecord(value) && value.type === "image");
|
|
290
|
+
}
|
|
291
|
+
/** Narrow one JSON value to a string-keyed object. */
|
|
292
|
+
function isRecord(value) {
|
|
293
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
294
|
+
}
|
|
295
|
+
/** Narrow a declared MIME string to the durable image vocabulary. */
|
|
296
|
+
function isImageMediaType(value) {
|
|
297
|
+
return IMAGE_MEDIA_TYPES.includes(value);
|
|
298
|
+
}
|
|
299
|
+
/** Decode one untrusted MCP image block without accepting base64 aliases. */
|
|
300
|
+
function decodeImage(block) {
|
|
301
|
+
if (block.mimeType === void 0 || !isImageMediaType(block.mimeType)) throw new Error("the declared media type is not PNG, JPEG, WebP, or GIF");
|
|
302
|
+
if (block.data === void 0 || !CANONICAL_BASE64.test(block.data)) throw new Error("the image data is not canonical base64");
|
|
303
|
+
const data = Buffer.from(block.data, "base64");
|
|
304
|
+
if (data.toString("base64") !== block.data) throw new Error("the image data is not canonical base64");
|
|
305
|
+
return {
|
|
306
|
+
data,
|
|
307
|
+
mediaType: block.mimeType
|
|
235
308
|
};
|
|
236
309
|
}
|
|
237
310
|
/**
|
|
311
|
+
* Resolve the active model route and durable store for an image-bearing result.
|
|
312
|
+
* @param ctx - plugin context with optional services.
|
|
313
|
+
* @param exec - exact tool execution whose agent supplies the latest route.
|
|
314
|
+
* @returns the attachment store after exact positive image-capability proof.
|
|
315
|
+
*/
|
|
316
|
+
async function resolveImageAdmission(ctx, exec) {
|
|
317
|
+
const attachments = ctx.get("attachments");
|
|
318
|
+
if (attachments === void 0) throw new Error("no attachment store is mounted");
|
|
319
|
+
const routed = exec.agent?.session.requestHeader()?.config;
|
|
320
|
+
const provider = routed?.provider ?? exec.agent?.options.provider;
|
|
321
|
+
const model = routed?.model ?? exec.agent?.options.model;
|
|
322
|
+
const llm = ctx.get("llm");
|
|
323
|
+
if (provider === void 0 || model === void 0 || llm === void 0) throw new Error("the current model route could not be resolved");
|
|
324
|
+
let info;
|
|
325
|
+
try {
|
|
326
|
+
info = await llm.resolveModelInfo(provider, model, exec.signal);
|
|
327
|
+
} catch {
|
|
328
|
+
throw new Error("the current model route could not be verified");
|
|
329
|
+
}
|
|
330
|
+
if (info.inputModalities === void 0 || !info.inputModalities.includes("image")) throw new Error(`model "${model}" does not declare image input`);
|
|
331
|
+
if (exec.signal.aborted) throw new Error("the tool call was canceled before image storage");
|
|
332
|
+
return attachments;
|
|
333
|
+
}
|
|
334
|
+
/** Stable diagnostic text for an image block that was not admitted. */
|
|
335
|
+
function imageDiagnostic(block, reason) {
|
|
336
|
+
return `[image unavailable: ${block.mimeType ?? "unknown media type"}; ${reason}; raw image data remains available to programmatic callers]`;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Decode, preflight, and durably save one MCP result's ordered image batch.
|
|
340
|
+
* Any refusal projects every image as text while retaining the canonical raw
|
|
341
|
+
* value for programmatic callers.
|
|
342
|
+
*/
|
|
343
|
+
async function prepareImageProjection(ctx, exec, content, toolName) {
|
|
344
|
+
const decoded = [];
|
|
345
|
+
const validationErrors = /* @__PURE__ */ new Map();
|
|
346
|
+
const imageIndexes = [];
|
|
347
|
+
for (const [index, value] of content.entries()) {
|
|
348
|
+
if (!isRecord(value) || value.type !== "image") continue;
|
|
349
|
+
imageIndexes.push(index);
|
|
350
|
+
try {
|
|
351
|
+
decoded.push(decodeImage(value));
|
|
352
|
+
} catch (error) {
|
|
353
|
+
validationErrors.set(index, error.message);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (validationErrors.size > 0) return projectContent(content, toolName, (block, index) => ({
|
|
357
|
+
type: "text",
|
|
358
|
+
text: imageDiagnostic(block, validationErrors.get(index) ?? "another image in the same result was invalid")
|
|
359
|
+
}));
|
|
360
|
+
let attachments;
|
|
361
|
+
try {
|
|
362
|
+
attachments = await resolveImageAdmission(ctx, exec);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const reason = error.message;
|
|
365
|
+
return projectContent(content, toolName, (block) => ({
|
|
366
|
+
type: "text",
|
|
367
|
+
text: imageDiagnostic(block, reason)
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const refs = await attachments.saveImages(decoded);
|
|
372
|
+
const byIndex = new Map(imageIndexes.map((index, offset) => [index, refs[offset]]));
|
|
373
|
+
return projectContent(content, toolName, (_block, index) => ({
|
|
374
|
+
type: "image",
|
|
375
|
+
attachment: byIndex.get(index)
|
|
376
|
+
}));
|
|
377
|
+
} catch (error) {
|
|
378
|
+
const reason = isImageAdmissionError(error) ? `image admission rejected the result: ${error.message}` : "durable image storage rejected the result";
|
|
379
|
+
return projectContent(content, toolName, (block) => ({
|
|
380
|
+
type: "text",
|
|
381
|
+
text: imageDiagnostic(block, reason)
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
238
386
|
* Extract text from an MCP content array into a single string.
|
|
239
387
|
* - text blocks: join with '\n'
|
|
240
388
|
* - image/audio/resource blocks: replaced with a placeholder
|
|
@@ -243,31 +391,58 @@ function createExecutor(client, rawName, taskRequired, opts) {
|
|
|
243
391
|
* guarded with fallbacks because this is a network trust boundary.
|
|
244
392
|
*/
|
|
245
393
|
function extractText(mcpContent, toolName) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
394
|
+
return projectContent(mcpContent, toolName).map((block) => block.text).join("\n");
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Project ordered MCP blocks into the core content vocabulary.
|
|
398
|
+
* Text-like runs are newline-coalesced; admitted images split those runs at
|
|
399
|
+
* their original position.
|
|
400
|
+
*/
|
|
401
|
+
function projectContent(mcpContent, toolName, image = (block) => ({
|
|
402
|
+
type: "text",
|
|
403
|
+
text: imageDiagnostic(block, "this result was not admitted to durable model context")
|
|
404
|
+
})) {
|
|
405
|
+
const projected = [];
|
|
406
|
+
const text = [];
|
|
407
|
+
const flushText = () => {
|
|
408
|
+
if (text.length === 0) return;
|
|
409
|
+
projected.push({
|
|
410
|
+
type: "text",
|
|
411
|
+
text: text.splice(0).join("\n")
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
for (const [index, value] of mcpContent.entries()) {
|
|
415
|
+
if (!isRecord(value)) {
|
|
416
|
+
text.push("[unsupported MCP content block: expected an object]");
|
|
250
417
|
continue;
|
|
251
418
|
}
|
|
252
419
|
const block = value;
|
|
253
420
|
switch (block.type) {
|
|
254
421
|
case "text":
|
|
255
|
-
if (block.text !== void 0)
|
|
422
|
+
if (block.text !== void 0) text.push(block.text);
|
|
256
423
|
break;
|
|
257
424
|
case "image":
|
|
258
|
-
|
|
425
|
+
flushText();
|
|
426
|
+
projected.push(image(block, index));
|
|
427
|
+
break;
|
|
428
|
+
case "resource_link":
|
|
429
|
+
if (block.name === void 0 || block.uri === void 0) text.push("[resource link unavailable: the MCP block is missing its name or URI]");
|
|
430
|
+
else text.push(`Resource link: ${block.name} (${block.uri})`);
|
|
259
431
|
break;
|
|
260
432
|
case "audio":
|
|
261
|
-
|
|
433
|
+
text.push(`[audio result unsupported: ${block.mimeType ?? "unknown media type"}; raw audio data remains available to programmatic callers]`);
|
|
262
434
|
break;
|
|
263
435
|
case "resource":
|
|
264
|
-
|
|
265
|
-
parts.push("[resource: content discarded]");
|
|
436
|
+
text.push("[embedded resource unsupported; raw resource data remains available to programmatic callers]");
|
|
266
437
|
break;
|
|
267
|
-
default:
|
|
438
|
+
default: text.push(`[unsupported MCP content type: ${block.type}]`);
|
|
268
439
|
}
|
|
269
440
|
}
|
|
270
|
-
|
|
441
|
+
flushText();
|
|
442
|
+
return projected.length > 0 ? projected : [{
|
|
443
|
+
type: "text",
|
|
444
|
+
text: `(${toolName} returned no model-visible content)`
|
|
445
|
+
}];
|
|
271
446
|
}
|
|
272
447
|
//#endregion
|
|
273
448
|
//#region lib/types/connection.js
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.7",
|
|
5
5
|
"publishConfig": {
|
|
6
|
-
"access": "
|
|
6
|
+
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/dsh-
|
|
36
|
-
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.
|
|
37
|
-
"@deepseek-ai/dsh-
|
|
38
|
-
"@deepseek-ai/dsh-
|
|
39
|
-
"@deepseek-ai/dsh-
|
|
35
|
+
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.7",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
|
|
37
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
38
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.7",
|
|
39
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
|
|
40
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
|
|
40
41
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
@@ -47,11 +48,13 @@
|
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@modelcontextprotocol/server-everything": "^2026.7.4",
|
|
49
50
|
"@modelcontextprotocol/server-filesystem": "^2026.7.4",
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/
|
|
51
|
+
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.7",
|
|
52
|
+
"@deepseek-ai/dsh-attachment-local": "^0.1.0-rc.7",
|
|
53
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.7",
|
|
54
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
|
|
55
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
|
|
56
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
|
|
57
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7"
|
|
56
59
|
}
|
|
57
60
|
}
|