dsh-mcp-connector 0.2.65 → 0.2.67

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/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.67] - 2026-10-06
8
+
9
+ ### Fixed
10
+
11
+ - 面板标题图标与侧栏入口统一为继承 `currentColor` 的单色 SVG,并补充对字面 emoji、Unicode 转义和直接码点的回归检查,修复 #118 后遗漏的面板标题彩色图标(#122、#123)。
12
+ - 为重启恢复的 stdio MCP 连接增加有界的 `server/discover`、`initialize` 与首次 `tools/list` 分页请求;超时后关闭自有 transport 并回收进程树,避免单个异常 Server 阻塞 DSH Host,同时不禁用已保存连接,也不改变用户主动连接或 HTTP 连接流程(#125)。
13
+
14
+ ### Verification
15
+
16
+ - 在最新 `main` 合并态通过 326 项测试(323 通过、3 项环境性跳过、0 失败)、Node 20/22/24、Windows、Zod 4.1.8、PR 安装包与发布文件白名单校验。
17
+
18
+ ## [0.2.66] - 2026-10-03
19
+
20
+ ### Changed
21
+
22
+ - 侧栏“MCP连接器”入口由平台相关的彩色 emoji 改为继承 `currentColor` 的单色线性 SVG,与 DSH 宿主导航的尺寸、线性风格和深浅主题保持一致(#117)。
23
+ - 同步无凭据 UI harness 和入口文档,图标保持 `aria-hidden`,不改变“MCP连接器”的无障碍名称。
24
+
25
+ ### Verification
26
+
27
+ - 新增侧栏图标回归测试;在当前 `main` 合并态通过 309 项测试、Node 20/22/24、Windows、Zod 4.1.8 与 PR 安装包校验。
28
+
7
29
  ## [0.2.65] - 2026-10-03
8
30
 
9
31
  ### Added
package/README.en.md CHANGED
@@ -85,7 +85,7 @@ If the plugin helps you connect an MCP server faster, consider [starring the rep
85
85
  - Explicit, non-destructive migration from the two earlier Qichacha OAuth plugins, plus active-plugin conflict detection that blocks duplicate server management and credential overwrites.
86
86
 
87
87
  <!-- catalog-stats:start -->
88
- As of 2026-09-30, the public Registry publishes 107 connector descriptors. After merging and deduplicating them with the 4 bundled Qichacha cards, the Marketplace exposes 111 cards across 9 business categories. Recommendations remain limited to the four Qichacha cards, PKULaw, and Wind, for 6 featured cards in total. The Registry evolves independently; the badge shown after a client refresh and the live badges above are the authoritative current counts.
88
+ As of 2026-10-05, the public Registry publishes 108 connector descriptors. After merging and deduplicating them with the 4 bundled Qichacha cards, the Marketplace exposes 112 cards across 9 business categories. Recommendations remain limited to the four Qichacha cards, PKULaw, and Wind, for 6 featured cards in total. The Registry evolves independently; the badge shown after a client refresh and the live badges above are the authoritative current counts.
89
89
  <!-- catalog-stats:end -->
90
90
 
91
91
  ## Interface and demo
@@ -202,7 +202,7 @@ npm run dev:ui
202
202
 
203
203
  Every Registry merge regenerates `catalog-stats.json`; an hourly workflow in this repository synchronizes the Chinese and English product copy plus a local stats snapshot. The static npm README updates with package releases, while the live badges above read the Registry directly and therefore stay current without another npm release.
204
204
 
205
- The current public version is [`dsh-mcp-connector@0.2.65`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.65](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.65).
205
+ The current public version is [`dsh-mcp-connector@0.2.67`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.67](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.67).
206
206
 
207
207
  See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
208
208
 
@@ -213,7 +213,8 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](d
213
213
  - Unchecked or Host-unobservable connections are reported as `unknown`, never as healthy. Health history and last-success timestamps are currently process-local.
214
214
  - External URLs use HTTPS by default, and HTTP is allowed for loopback development. A user-created connection may explicitly opt in to plaintext HTTP for literal RFC1918 IPv4 or RFC4193 IPv6 ULA addresses; hostnames, public HTTP, link-local addresses, and cloud metadata addresses remain blocked.
215
215
  - Remote descriptors and catalogs are limited to 2 MiB, Web API requests to 1 MiB, and imported JSON is scanned for credential fields before normalization.
216
- - Streamable HTTP and stdio are supported end to end. Legacy `sse` entries are normalized to Streamable HTTP. The connector passes stdio `command/args/env/cwd` to `@deepseek-ai/dsh-mcp-client` instead of reimplementing process transport.
216
+ - Streamable HTTP and stdio are supported end to end. Legacy `sse` entries are normalized to Streamable HTTP. Restored stdio connections run their original command through a transparent startup guard; persisted records, environment and working directory remain unchanged. Tool registration and execution still belong to `@deepseek-ai/dsh-mcp-client`.
217
+ - On restoration, `server/discover`, `initialize` and initial `tools/list` requests (including pagination) use `requestTimeoutMs`, capped at 15 seconds per request. A timeout closes that connection and its child processes rather than blocking Host startup. Connections stay enabled; requests after initial discovery keep the Host deadline.
217
218
  - Official CLIs that are not MCP servers can be exposed through a separate [controlled CLI Provider](docs/CLI-PROVIDERS.md). Only code-reviewed commands and arguments are available; the initial DingTalk `dws` provider is read-only.
218
219
  - stdio starts a local process. Import or connect only trusted commands and packages. Catalog descriptors may declare `credentialFields` and `credentialBindings`, but may never contain actual token/secret values; user input is injected only into the local Host process environment.
219
220
  - OAuth DCR client secrets share the same local-only boundary as access and refresh tokens and are omitted from catalog/status responses and logs.
package/README.md CHANGED
@@ -36,7 +36,7 @@ OAuth 是否可用取决于服务商的客户端注册、账号权限与授权
36
36
  dsh plugin --profile web add dsh-mcp-connector
37
37
  ```
38
38
 
39
- 安装或升级后完全重启 DeepSeek Harness Desktop 或 `dsh web`,然后打开左侧「🧩 MCP连接器」;也可从“设置 → 插件 → 插件配置 → MCP连接器”直接打开。
39
+ 安装或升级后完全重启 DeepSeek Harness Desktop 或 `dsh web`,然后打开左侧「MCP连接器」;也可从“设置 → 插件 → 插件配置 → MCP连接器”直接打开。
40
40
 
41
41
  首次使用建议依次确认:连接已保存且范围正确 → “工具”页能找到预期工具与来源 → 在正常 DSH 会话中通过 Host 审批链完成一次服务商许可的只读调用。缓存可见不等于当前服务可调用。
42
42
 
@@ -86,7 +86,7 @@ dsh plugin --profile web add dsh-mcp-connector
86
86
  - 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`export_config`、`snapshot`、`install_from_url`、`status`、`scope`、`health_check`、`policy`、`set_enabled`、`session_tools`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`、`tool_search`、`tool_detail`。搜索/详情只做渐进式能力发现,不执行目标 MCP 工具。
87
87
 
88
88
  <!-- catalog-stats:start -->
89
- 截至 2026-09-30,公共 Registry 已发布 107 条连接器描述;与随包的 4 张企查查卡片合并去重后,市场页可浏览 111 张卡片,覆盖企业数据、金融投资、法律合规、开发工具、办公协作、调研分析、设计创意、效率工具、其他 9 类。推荐位严格保留 4 张企查查卡片、北大法宝和 Wind,共 6 张;其他连接器按业务分类展示。Registry 可独立持续更新,实际数量以客户端刷新后的市场页签徽标和上方实时统计徽标为准。
89
+ 截至 2026-10-05,公共 Registry 已发布 108 条连接器描述;与随包的 4 张企查查卡片合并去重后,市场页可浏览 112 张卡片,覆盖企业数据、金融投资、法律合规、开发工具、办公协作、调研分析、设计创意、效率工具、其他 9 类。推荐位严格保留 4 张企查查卡片、北大法宝和 Wind,共 6 张;其他连接器按业务分类展示。Registry 可独立持续更新,实际数量以客户端刷新后的市场页签徽标和上方实时统计徽标为准。
90
90
  <!-- catalog-stats:end -->
91
91
 
92
92
  ## 界面与演示
@@ -121,7 +121,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-mcp-connector/m
121
121
 
122
122
  ## 使用
123
123
 
124
- 1. 点击左侧“🧩 MCP连接器”,或从“设置 → 插件 → 插件配置 → MCP连接器”点击“打开 MCP连接器”。
124
+ 1. 点击左侧“MCP连接器”,或从“设置 → 插件 → 插件配置 → MCP连接器”点击“打开 MCP连接器”。
125
125
  2. 在市场中选择连接器,确认“当前项目”或“所有项目(全局)”,再完成授权或配置。
126
126
  3. 打开卡片详情,可点击示例 Prompt 的发送按钮,在当前工作区创建/复用空白会话并写入草稿。
127
127
  4. 在“已安装”或对话工具中查看、停用、恢复或断开连接。
@@ -197,7 +197,7 @@ npm run dev:ui
197
197
 
198
198
  公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
199
199
 
200
- 当前公开版本为 [`dsh-mcp-connector@0.2.65`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.65](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.65)。
200
+ 当前公开版本为 [`dsh-mcp-connector@0.2.67`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.67](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.67)。
201
201
 
202
202
  版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
203
203
  Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
@@ -212,7 +212,8 @@ stdio 传输的架构、透传边界与安全约束见 [docs/STDIO-SUPPORT.md](d
212
212
  - 未检查或 Host 状态不可见时显示“状态未知”,不会冒充健康;健康摘要与最近成功时间当前只保留在插件进程内。
213
213
  - 外部 URL 默认仅允许 HTTPS,HTTP 默认仅允许回环地址。用户自建连接可在明确风险确认后放行 RFC1918 IPv4 / RFC4193 IPv6 ULA 字面量;域名、公网 HTTP、链路本地与云元数据地址仍拒绝。
214
214
  - 远程目录/描述响应限制 2 MiB,Web API 请求限制 1 MiB;原始 JSON 在归一化前扫描凭据字段。
215
- - 完整覆盖 Streamable HTTP 与 stdio;旧 `sse` 配置在导入/恢复时归一为 Streamable HTTP。stdio 的 `command/args/env/cwd` 原样交给 `@deepseek-ai/dsh-mcp-client`,插件本身不重复实现进程传输。
215
+ - 完整覆盖 Streamable HTTP 与 stdio;旧 `sse` 配置在导入/恢复时归一为 Streamable HTTP。重启恢复 stdio 连接时使用透明的启动保护进程包装原命令,`env/cwd` 与持久化记录保持不变;工具注册和正式执行仍由 `@deepseek-ai/dsh-mcp-client` 负责。
216
+ - 恢复连接的 `server/discover`、`initialize` 与首次 `tools/list`(含分页)按 `requestTimeoutMs` 限时,最长每次 15 秒;超时结束该连接的子进程,而不是让可选 MCP 卡住 Host 启动。连接保持启用,首次发现完成后的请求仍使用 Host 原有超时。
216
217
  - 对本身不是 MCP Server 的官方 CLI,使用独立的[受控 CLI Provider](docs/CLI-PROVIDERS.md)桥接为 stdio MCP;只允许代码登记的命令与参数,首个钉钉 `dws` Provider 仅开放只读能力。
217
218
  - stdio 会启动本机进程:仅导入或连接可信命令/软件包。市场目录只能用 `credentialFields` + `credentialBindings` 声明输入与 env 映射,不得携带真实 token/secret;用户填写值只写入本机连接记录并交给 Host。
218
219
  - OAuth DCR 的 `client_secret` 与 Access/Refresh Token 采用相同的本机存储边界,不会进入市场 API、状态输出或日志。
@@ -8,6 +8,9 @@
8
8
  > **就绪语义加固(2026-08-26)**:用户主动连接时显式启用 `failOnStartupError`,必须等待 Host 完成首次 `initialize + tools/list` 后才持久化;健康检查与详情页改读 Host 实际注册的 `mcp__<serverName>__*` 工具。启动失败、进程退出或超时不再提前显示“已连接”。依赖默认值仍为 `false`,下文对应代码仅作为依赖能力说明。
9
9
  > 状态:已完成(v0.2.12)
10
10
 
11
+ > **启动恢复保护(2026-10-04)**:`failOnStartupError: false` 只控制连接失败是否抛错,并不让官方客户端跳过首次连接等待;动态客户端 fiber 仍参加 Host 的启动屏障。恢复已保存的 stdio 连接时,provisioning 包装原命令并为 `server/discover`、`initialize` 与首次 `tools/list`(含全部分页)设置 `requestTimeoutMs`、最多 15 秒的逐请求截止时间。超时关闭 transport 并清理所属子进程树,不改变记录的启用状态、凭据、命令、环境或工作目录。不能仅对 `loader.create()` 使用 `Promise.race`,否则未完成的 fiber 仍会阻塞 loader。首次发现结束后的列表/工具执行,以及用户主动连接时的 `startupTimeoutMs` 语义保持原样。
12
+ >
13
+ > 默认保护进程透明转发协议,**不**强制把现代服务器降级为旧协议。独立运行保护脚本时,可为已确认只支持 `initialize` 的服务器显式传入 `--legacy`,在一次性 discovery probe 中本地返回 `-32601` 而不启动真正的服务器。此选项不是默认恢复策略。
11
14
  ---
12
15
 
13
16
  ## 一、背景与目标
@@ -47,7 +47,7 @@ dsh web
47
47
 
48
48
  重启后,可以用以下任一方式打开:
49
49
 
50
- - 在 DSH 左侧主导航点击“🧩 MCP连接器”。入口通常位于“新会话”下方、工作区/会话列表上方;如果当前 DSH 版本没有对应的公开插槽,入口会回退到左侧底部。
50
+ - 在 DSH 左侧主导航点击“MCP连接器”。入口通常位于“新会话”下方、工作区/会话列表上方;如果当前 DSH 版本没有对应的公开插槽,入口会回退到左侧底部。
51
51
  - 打开“设置 → 插件 → 插件配置 → MCP连接器”,展开卡片并点击“打开 MCP连接器”。该按钮直接复用同一个连接器弹框,不要求侧边栏入口可见;关闭弹框后仍回到设置页。
52
52
 
53
53
  如需腾出侧边栏空间,可在同一配置卡片关闭“在侧边栏显示 MCP连接器”。该偏好作用于当前 DSH profile,并在重启后保留;它只隐藏快捷入口,不停用插件、已连接 MCP Server 或工具。隐藏后可以随时从该配置卡片直接打开连接器,或重新开启入口。旧版 DSH 没有 Settings 能力时,插件保持入口可见,不会因设置功能进入 Safe Mode。
@@ -13,7 +13,7 @@
13
13
 
14
14
  ## 导入一个最小示例
15
15
 
16
- 打开左侧“🧩 MCP连接器”(或“设置 → 插件 → 插件配置 → MCP连接器 → 打开 MCP连接器”),点击“+ 添加连接 → 导入 JSON”,在本机粘贴:
16
+ 打开左侧“MCP连接器”(或“设置 → 插件 → 插件配置 → MCP连接器 → 打开 MCP连接器”),点击“+ 添加连接 → 导入 JSON”,在本机粘贴:
17
17
 
18
18
  ```json
19
19
  {
@@ -12,7 +12,7 @@
12
12
  dsh plugin --profile web add dsh-mcp-connector
13
13
  ```
14
14
 
15
- 安装或升级后,只重启你正在使用的宿主:DSH Desktop 需完全退出应用后重新打开;`dsh web` 需停止原进程后重新启动。打开左侧“🧩 MCP连接器”;若侧边栏入口已隐藏,从“设置 → 插件 → 插件配置 → MCP连接器”点击“打开 MCP连接器”。自定义过 profile 布局的部署应以其宿主配置为准。
15
+ 安装或升级后,只重启你正在使用的宿主:DSH Desktop 需完全退出应用后重新打开;`dsh web` 需停止原进程后重新启动。打开左侧“MCP连接器”;若侧边栏入口已隐藏,从“设置 → 插件 → 插件配置 → MCP连接器”点击“打开 MCP连接器”。自定义过 profile 布局的部署应以其宿主配置为准。
16
16
 
17
17
  ## 2. 只选一条连接路径
18
18
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 先定位正确的工具
8
8
 
9
- 1. 在 DSH 选中目标 Workspace,再打开“🧩 MCP连接器 → 工具”。未选 Workspace 时仅能看到全局连接;项目连接不会越界显示。
9
+ 1. 在 DSH 选中目标 Workspace,再打开“MCP连接器 → 工具”。未选 Workspace 时仅能看到全局连接;项目连接不会越界显示。
10
10
  2. 输入工具名或描述中的关键词。精确工具名优先;同名结果仍需核对连接、服务和来源。
11
11
  3. 使用“连接”“服务”“发现状态”筛选。若结果为空,先清除筛选,再确认连接已启用、范围正确且至少成功发现过一次。
12
12
  4. 点击“参数详情”:先看类型、必填、枚举和可展开的嵌套摘要;复杂规则可打开“原始 Schema(安全缓存)”。它仍经过安全裁剪和容量限制,不是服务端完整原文。
package/lib/client.js CHANGED
@@ -91,8 +91,13 @@ window.__ModuleLoader__.load({
91
91
  flex: 0 0 20px;
92
92
  width: 20px;
93
93
  height: 20px;
94
- font-size: 18px;
95
- line-height: 1;
94
+ color: inherit;
95
+ }
96
+
97
+ .mcpConnectorLauncherIcon > svg {
98
+ display: block;
99
+ width: 18px;
100
+ height: 18px;
96
101
  }
97
102
 
98
103
  .mcpConnectorLauncherLabel {
@@ -142,6 +147,16 @@ window.__ModuleLoader__.load({
142
147
  .mcpConnectorMarketPanel { background: #ffffff; color: #111827; }
143
148
  .mcpConnectorMarketHeader { border-color: #e5e7eb; }
144
149
  .mcpConnectorMarketTitle { color: #111827; }
150
+ .mcpConnectorMarketIcon {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ color: inherit;
154
+ }
155
+ .mcpConnectorMarketIcon > svg {
156
+ display: block;
157
+ width: 18px;
158
+ height: 18px;
159
+ }
145
160
  .mcpConnectorVersion {
146
161
  color: #6b7280;
147
162
  background: #f3f4f6;
@@ -1596,7 +1611,21 @@ window.__ModuleLoader__.load({
1596
1611
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1597
1612
  style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", minWidth: 0, flex: 1 },
1598
1613
  children: [
1599
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 22 }, children: "\u{1F9E9}" }),
1614
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1615
+ className: "mcpConnectorMarketIcon",
1616
+ "aria-hidden": true,
1617
+ children: (0, react_jsx_runtime.jsxs)("svg", {
1618
+ viewBox: "0 0 16 16",
1619
+ fill: "none",
1620
+ "aria-hidden": "true",
1621
+ children: [
1622
+ (0, react_jsx_runtime.jsx)("path", { d: "M6 2V4.5", stroke: "currentColor", strokeLinecap: "round" }),
1623
+ (0, react_jsx_runtime.jsx)("path", { d: "M10 2V4.5", stroke: "currentColor", strokeLinecap: "round" }),
1624
+ (0, react_jsx_runtime.jsx)("path", { d: "M3.5 4.5H12.5V8.25C12.5 10.7353 10.4853 12.75 8 12.75C5.51472 12.75 3.5 10.7353 3.5 8.25V4.5Z", stroke: "currentColor", strokeLinejoin: "round" }),
1625
+ (0, react_jsx_runtime.jsx)("path", { d: "M8 12.75V14.5", stroke: "currentColor", strokeLinecap: "round" })
1626
+ ]
1627
+ })
1628
+ }),
1600
1629
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { id: "mcp-connector-market-title", className: "mcpConnectorMarketTitle", style: { fontSize: 18, fontWeight: 600 }, children: "MCP连接器" }),
1601
1630
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1602
1631
  className: "mcpConnectorVersion",
@@ -1716,7 +1745,21 @@ window.__ModuleLoader__.load({
1716
1745
  "aria-expanded": open,
1717
1746
  onClick: () => { try { actions.open(); } catch (error) { console.error('[mcp-connector] open failed:', error); } },
1718
1747
  children: [
1719
- (0, react_jsx_runtime.jsx)("span", { className: "mcpConnectorLauncherIcon", "aria-hidden": true, children: "🧩" }),
1748
+ (0, react_jsx_runtime.jsx)("span", {
1749
+ className: "mcpConnectorLauncherIcon",
1750
+ "aria-hidden": true,
1751
+ children: (0, react_jsx_runtime.jsxs)("svg", {
1752
+ viewBox: "0 0 16 16",
1753
+ fill: "none",
1754
+ "aria-hidden": "true",
1755
+ children: [
1756
+ (0, react_jsx_runtime.jsx)("path", { d: "M6 2V4.5", stroke: "currentColor", strokeLinecap: "round" }),
1757
+ (0, react_jsx_runtime.jsx)("path", { d: "M10 2V4.5", stroke: "currentColor", strokeLinecap: "round" }),
1758
+ (0, react_jsx_runtime.jsx)("path", { d: "M3.5 4.5H12.5V8.25C12.5 10.7353 10.4853 12.75 8 12.75C5.51472 12.75 3.5 10.7353 3.5 8.25V4.5Z", stroke: "currentColor", strokeLinejoin: "round" }),
1759
+ (0, react_jsx_runtime.jsx)("path", { d: "M8 12.75V14.5", stroke: "currentColor", strokeLinecap: "round" })
1760
+ ]
1761
+ })
1762
+ }),
1720
1763
  wide ? (0, react_jsx_runtime.jsx)("span", { className: "mcpConnectorLauncherLabel", children: "MCP连接器" }) : null
1721
1764
  ]
1722
1765
  });
package/lib/index.js CHANGED
@@ -3254,7 +3254,11 @@ export async function apply(ctx, config) {
3254
3254
  logger.warn(`skip provision "${key}" at startup: ${failure.message}`);
3255
3255
  continue;
3256
3256
  }
3257
- await provision(ctx, config, record, grantMap()).catch((error) => {
3257
+ // Optional MCPs still participate in the Host loader's startup barrier.
3258
+ // Bound their stdio bootstrap RPCs at the transport, not with Promise.race
3259
+ // around loader.create(): an abandoned fiber would remain pending there.
3260
+ const startupRequestTimeoutMs = Math.max(100, Math.min(DEFAULT_REQUEST_TIMEOUT_MS, Math.trunc(Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS)));
3261
+ await provision(ctx, config, record, grantMap(), { startupRequestTimeoutMs }).catch((error) => {
3258
3262
  logger.warn(`provision "${key}" failed: ${error.message}`);
3259
3263
  });
3260
3264
  }
@@ -4,6 +4,9 @@
4
4
  */
5
5
 
6
6
  import { cliBridgeArgs } from './cli-providers.js';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const STDIO_STARTUP_GUARD = fileURLToPath(new URL('./stdio-startup-guard.js', import.meta.url));
7
10
 
8
11
  export function entryIdFor(config, record) {
9
12
  return `${config.entryPrefix}-${record.key}`;
@@ -26,18 +29,23 @@ export function authHeaders(record, grants) {
26
29
  return headers;
27
30
  }
28
31
 
29
- export function buildEntryConfig(record, grants, { failOnStartupError = false } = {}) {
32
+ export function buildEntryConfig(record, grants, { failOnStartupError = false, startupRequestTimeoutMs } = {}) {
30
33
  if (record.transport === 'stdio') {
31
34
  const env = { ...(record.env ?? {}) };
32
35
  if (record.auth?.mode === 'oauth' && record.oauthTokenEnv) {
33
36
  const grant = record.auth.grantKey ? grants.get(record.auth.grantKey) : null;
34
37
  if (grant?.accessToken) env[record.oauthTokenEnv] = `Bearer ${grant.accessToken}`;
35
38
  }
39
+ const guarded = startupRequestTimeoutMs !== undefined;
40
+ if (guarded && (!Number.isSafeInteger(startupRequestTimeoutMs) || startupRequestTimeoutMs < 100 || startupRequestTimeoutMs > 2_147_483_647)) {
41
+ throw new RangeError('stdio startup request timeout must be an integer between 100 and 2147483647 ms');
42
+ }
43
+ const args = cliBridgeArgs(record);
36
44
  return {
37
45
  transport: 'stdio',
38
46
  serverName: record.serverName,
39
- command: record.command,
40
- args: cliBridgeArgs(record),
47
+ command: guarded ? process.execPath : record.command,
48
+ args: guarded ? [STDIO_STARTUP_GUARD, '--timeout-ms', String(startupRequestTimeoutMs), '--', record.command, ...args] : args,
41
49
  env,
42
50
  cwd: record.cwd || process.cwd(),
43
51
  failOnStartupError,
@@ -62,9 +70,9 @@ function hasEntry(ctx, id) {
62
70
  }
63
71
 
64
72
  /** 创建或更新一条 mcp-client 条目(幂等;serverName 不变则工具名不变) */
65
- export async function provision(ctx, config, record, grants, { failOnStartupError = false } = {}) {
73
+ export async function provision(ctx, config, record, grants, { failOnStartupError = false, startupRequestTimeoutMs } = {}) {
66
74
  const id = entryIdFor(config, record);
67
- const entryConfig = buildEntryConfig(record, grants, { failOnStartupError });
75
+ const entryConfig = buildEntryConfig(record, grants, { failOnStartupError, startupRequestTimeoutMs });
68
76
  const disabled = record.enabled === false;
69
77
  if (hasEntry(ctx, id)) {
70
78
  const existing = ctx.loader.resolve(id);
@@ -0,0 +1,116 @@
1
+ // stdout is reserved for MCP JSON-RPC. No payloads or credentials are logged.
2
+ // Derived from the proven legacy stdio adapter: transparent unless --legacy
3
+ // is explicitly requested for a known initialize-era server.
4
+ import { spawn } from 'node:child_process';
5
+ import { createInterface } from 'node:readline';
6
+
7
+ const argv = process.argv.slice(2);
8
+ const separator = argv.indexOf('--');
9
+ const timeoutMs = Number(argv[1]);
10
+ const legacy = argv[2] === '--legacy';
11
+ if (argv[0] !== '--timeout-ms' || separator !== (legacy ? 3 : 2)
12
+ || !Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 2_147_483_647 || !argv[separator + 1]) {
13
+ process.stderr.write('[mcp-stdio-startup] Invalid invocation\n');
14
+ process.exit(2);
15
+ }
16
+ const command = argv[separator + 1];
17
+ const args = argv.slice(separator + 2);
18
+ const startupMethods = new Set(['server/discover', 'initialize', 'tools/list']);
19
+ let child;
20
+ let stopping = false;
21
+ let discovered = false;
22
+ let killer;
23
+ let closeTimer;
24
+ const pending = new Map();
25
+ const write = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
26
+
27
+ function clearPending() {
28
+ for (const { timer } of pending.values()) clearTimeout(timer);
29
+ pending.clear();
30
+ }
31
+
32
+ function forceStop() {
33
+ if (!child) return;
34
+ if (process.platform === 'win32') {
35
+ // Capture the tree while its root is alive; do not wait for a parent to
36
+ // exit and leave unaddressable descendants behind.
37
+ if (child.exitCode !== null || killer) return;
38
+ killer = spawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
39
+ killer.on('error', () => child.kill('SIGKILL'));
40
+ killer.on('close', () => { killer = undefined; if (child.exitCode !== null) process.exit(process.exitCode ?? 0); });
41
+ } else {
42
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }
43
+ }
44
+ }
45
+
46
+ function stop(reason, code = 0) {
47
+ if (stopping) return;
48
+ stopping = true;
49
+ clearPending();
50
+ if (reason) process.stderr.write(`[mcp-stdio-startup] ${reason}\n`);
51
+ process.exitCode = code;
52
+ if (!child || child.exitCode !== null) process.exit(code);
53
+ if (process.platform === 'win32') forceStop();
54
+ else child.stdin.end();
55
+ closeTimer = setTimeout(forceStop, 300);
56
+ // A broken descendant must not retain the transport indefinitely.
57
+ setTimeout(() => process.exit(code), 2500).unref();
58
+ }
59
+
60
+ function launch() {
61
+ child = spawn(command, args, {
62
+ cwd: process.cwd(), env: process.env,
63
+ stdio: ['pipe', 'pipe', 'inherit'], windowsHide: true,
64
+ detached: process.platform !== 'win32',
65
+ });
66
+ child.on('error', () => stop('Server launch failed', 1));
67
+ child.stdin.on('error', () => stop('Server input closed', 1));
68
+ const output = createInterface({ input: child.stdout, crlfDelay: Infinity });
69
+ output.on('line', (line) => {
70
+ try {
71
+ const message = JSON.parse(line);
72
+ if (Object.hasOwn(message, 'id') && !message.method) {
73
+ const request = pending.get(message.id);
74
+ if (request) {
75
+ clearTimeout(request.timer);
76
+ pending.delete(message.id);
77
+ if (request.method === 'tools/list' && !message.error && !message.result?.nextCursor) {
78
+ discovered = true;
79
+ clearPending();
80
+ }
81
+ }
82
+ }
83
+ } catch { /* The SDK reports non-protocol stdout, without leaking it here. */ }
84
+ if (!stopping) process.stdout.write(`${line}\n`);
85
+ });
86
+ child.on('close', (code) => {
87
+ clearPending();
88
+ clearTimeout(closeTimer);
89
+ // A parent may exit on EOF while descendants remain in its process group.
90
+ if (process.platform !== 'win32') forceStop();
91
+ if (!killer) process.exit(stopping ? (process.exitCode ?? 0) : (code ?? 1));
92
+ });
93
+ }
94
+
95
+ process.stdout.on('error', () => stop(undefined, 0));
96
+ process.on('SIGTERM', () => stop(undefined, 0));
97
+ process.on('SIGINT', () => stop(undefined, 0));
98
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
99
+ input.on('close', () => stop(undefined, 0));
100
+ input.on('line', (line) => {
101
+ if (stopping || !line.trim()) return;
102
+ let message;
103
+ try { message = JSON.parse(line); } catch { stop('Invalid JSON-RPC input', 1); return; }
104
+ if (legacy && message.method === 'server/discover' && Object.hasOwn(message, 'id')) {
105
+ write({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: 'Legacy MCP server: use initialize' } });
106
+ return;
107
+ }
108
+ if (!child) launch();
109
+ if (stopping) return;
110
+ if (!discovered && startupMethods.has(message.method) && Object.hasOwn(message, 'id')) {
111
+ clearTimeout(pending.get(message.id)?.timer);
112
+ const timer = setTimeout(() => stop(`${message.method} exceeded ${timeoutMs}ms; stopped server`, 1), timeoutMs);
113
+ pending.set(message.id, { method: message.method, timer });
114
+ }
115
+ child.stdin.write(`${line}\n`);
116
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcp-connector",
3
- "version": "0.2.65",
3
+ "version": "0.2.67",
4
4
  "description": "DeepSeek Harness MCP Connector: connect servers, search tools across active connections, and troubleshoot discovery. Includes a continuously updated catalog; supports OAuth 2.0 PKCE, API keys, Streamable HTTP/stdio, and mcpServers JSON import.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "scripts": {
48
48
  "test": "node --test",
49
- "lint": "node --check lib/discovery-policy.js && node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/settings.js && node --check lib/version-status.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/diagnostics.js && node --check lib/connection-backup.js && node --check lib/connection-editor.js && node --check lib/connection-scopes.js && node --check lib/governance.js && node --check lib/session-injection.js && node --check lib/tool-catalog-cache.js && node --check lib/status-events.js && node --check lib/mcp-validation.js && node --check lib/mcp-http.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/grant-journal.js && node --check lib/oauth.js && node --check lib/grant-lifecycle.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/cli-providers.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/cli-bridge.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/check-distribution-sync.mjs && node --check scripts/check-readme-version.mjs && node --check scripts/check-first-use-docs.mjs && node --check scripts/check-marketing-metadata.mjs && node --check scripts/check-storefront-screenshots.mjs && node --check scripts/sync-registry-stats.mjs && node --check scripts/verify-pack.mjs",
49
+ "lint": "node --check lib/discovery-policy.js && node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/settings.js && node --check lib/version-status.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/diagnostics.js && node --check lib/connection-backup.js && node --check lib/connection-editor.js && node --check lib/connection-scopes.js && node --check lib/governance.js && node --check lib/session-injection.js && node --check lib/tool-catalog-cache.js && node --check lib/status-events.js && node --check lib/mcp-validation.js && node --check lib/mcp-http.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/grant-journal.js && node --check lib/oauth.js && node --check lib/grant-lifecycle.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/stdio-startup-guard.js && node --check lib/cli-providers.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/cli-bridge.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/check-distribution-sync.mjs && node --check scripts/check-readme-version.mjs && node --check scripts/check-first-use-docs.mjs && node --check scripts/check-marketing-metadata.mjs && node --check scripts/check-storefront-screenshots.mjs && node --check scripts/sync-registry-stats.mjs && node --check scripts/verify-pack.mjs",
50
50
  "registry:build": "node scripts/build-registry.mjs",
51
51
  "registry:validate": "node scripts/probe-connector.mjs catalog/catalog.json --validate-only",
52
52
  "dev:ui": "node scripts/ui-harness.mjs",
@@ -151,7 +151,7 @@ function captureShell() {
151
151
  </head>
152
152
  <body>
153
153
  <section class="capture-panel" aria-label="MCP连接器产品面板">
154
- <header class="capture-header"><span aria-hidden="true">🧩</span><span class="capture-title">MCP连接器</span><span class="capture-version">v${packageJson.version}</span><span class="capture-state">无凭据 Mock</span></header>
154
+ <header class="capture-header"><svg aria-hidden="true" width="18" height="18" viewBox="0 0 16 16" fill="none"><path d="M6 2V4.5" stroke="currentColor" stroke-linecap="round"/><path d="M10 2V4.5" stroke="currentColor" stroke-linecap="round"/><path d="M3.5 4.5H12.5V8.25C12.5 10.7353 10.4853 12.75 8 12.75C5.51472 12.75 3.5 10.7353 3.5 8.25V4.5Z" stroke="currentColor" stroke-linejoin="round"/><path d="M8 12.75V14.5" stroke="currentColor" stroke-linecap="round"/></svg><span class="capture-title">MCP连接器</span><span class="capture-version">v${packageJson.version}</span><span class="capture-state">无凭据 Mock</span></header>
155
155
  <iframe src="/mcp-connector/ui/" title="MCP连接器"></iframe>
156
156
  </section>
157
157
  <script>
package/ui/index.html CHANGED
@@ -67,7 +67,6 @@
67
67
  flex-shrink: 0;
68
68
  }
69
69
  header .logo { font-size: 18px; font-weight: 700; display: flex; align-items: center; gap: 8px; color: var(--text); }
70
- header .logo .puzzle { font-size: 22px; }
71
70
  .tabs { display: flex; gap: 0; background: var(--bg); padding: 3px; border-radius: 10px; }
72
71
  .tabs button {
73
72
  display: inline-flex; align-items: center; gap: 5px;