dsh-mcp-connector 0.2.69 → 0.2.71
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 +32 -1
- package/README.en.md +14 -1
- package/README.md +14 -1
- package/cordis.patch.yml +4 -0
- package/docs/CLI-PROVIDERS.md +2 -2
- package/docs/MARKET-REGISTRATION.md +64 -1
- package/docs/STDIO-SUPPORT.md +2 -0
- package/docs/USER-GUIDE.md +5 -3
- package/lib/catalog.js +30 -2
- package/lib/cli-providers.js +5 -3
- package/lib/client.js +23 -8
- package/lib/index.js +109 -2
- package/lib/mcp-provision.js +10 -1
- package/lib/schema.js +5 -2
- package/lib/status-events.js +1 -0
- package/lib/stdio-startup-guard.js +38 -5
- package/lib/tools.js +19 -0
- package/lib/web.js +89 -3
- package/package.json +5 -1
- package/registry/catalog.json +103 -0
- package/registry/connectors/exa-search.json +45 -0
- package/ui/index.html +188 -21
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/);首个公开版本从 `0.1.0` 开始。
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.2.71] - 2026-10-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- 目录新增 HTTP Server 的 `servers[].oauthResource`:MCP URL 需要携带查询参数(如 `?tools=`)时,OAuth 的 `resource` 授权参数、`grant.authorizedResources` 与刷新参数改用规范资源标识,实际请求仍使用带查询参数的 `url`,避免刷新被授权服务器按 `invalid_target` 拒绝。仅 `oauth2-pkce` 卡片可声明,且必须与 `url` 同源。
|
|
10
|
+
- Registry 种子新增“Exa·网络搜索”第三方卡片(`registry/connectors/exa-search.json`):OAuth 一键授权接入 Exa 官方远程 MCP,覆盖语义搜索、全文抓取、高级检索与多步研究 Agent。
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- 修复 Windows 钉钉 CLI 包 bin 字段带 ./ 前缀时无法发现原生程序;精确匹配的旧受管桥接命令在运行时更新到 0.2.71,不覆盖自定义参数。
|
|
15
|
+
- 原生确认框改为页面内对话框,取消默认聚焦且 Enter 尊重焦点,避免误确认和桌面失焦。
|
|
16
|
+
- 已安装列表状态刷新保留滚动位置,回归测试抽取失败时显式报错。
|
|
17
|
+
- 远程目录请求支持 HTTP_PROXY / HTTPS_PROXY / NO_PROXY,不修改宿主全局代理。
|
|
18
|
+
- 桌面嵌入 CSP 允许显式配置可信祖先源,默认仍为 self。
|
|
19
|
+
- 重连与同一连接的停用、删除、修改串行执行;多 Server 注册等待并行,失败明确区分保留配置与 Host 状态。
|
|
20
|
+
|
|
21
|
+
- `normalizeConnectorDescriptor` 不再静默丢弃 HTTP Server 上的 `oauthResource` / `oauthTokenEnv`:前者现在生效,后者由目录审计显式拒绝,描述文件作者不会在无任何报错的情况下被忽略。
|
|
22
|
+
- 「已安装」页为未处于“已连接”的连接提供「重新连接」:重建 Host 侧条目并在有界时间内等待 stdio 工具重新注册,不修改配置、凭据、范围或注入模式;对话工具同步新增 `mcp_connector_reconnect`。
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- 「检测旧插件」改名为「检测企查查旧授权」,并说明扫描范围与“不会删除旧插件或原凭据”。
|
|
27
|
+
|
|
28
|
+
- 桌面宿主(Electron 内嵌 DSH)下启动 stdio 连接不再必然失败:以 `process.execPath` 启动守卫时注入 `ELECTRON_RUN_AS_NODE`,守卫再把该变量从真实 server 的子进程环境中移除(0.2.70 的 #130 只覆盖了 CLI 宿主)。
|
|
29
|
+
|
|
30
|
+
## [0.2.70] - 2026-10-08
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Windows 启动恢复保护采用 cross-spawn 解析 PATH/PATHEXT 与 npm .cmd shim,修复 npx 连接手动成功但重启失败的问题(#130)。
|
|
35
|
+
- 命令缺失时返回不含命令、参数或凭据的 ENOENT 与 PATH 检查提示。
|
|
36
|
+
- 新增 Windows 真实 .cmd 子进程回归,覆盖带空格路径与特殊字符参数。
|
|
6
37
|
|
|
7
38
|
## [0.2.69] - 2026-10-08
|
|
8
39
|
|
package/README.en.md
CHANGED
|
@@ -167,12 +167,25 @@ The default bundle configuration is in `cordis.patch.yml`:
|
|
|
167
167
|
refreshSkewMs: 300000
|
|
168
168
|
openBrowser: true
|
|
169
169
|
showSidebarEntry: true
|
|
170
|
+
frameAncestors: ['self']
|
|
170
171
|
```
|
|
171
172
|
|
|
172
173
|
Set `catalogUrl` to an empty string for an explicitly offline/private setup. A custom non-default URL is used as-is and does not fall back to the public registry.
|
|
173
174
|
|
|
175
|
+
Remote catalog requests (including the fallback source) honor `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`, as well as their lowercase forms. A lowercase variable takes precedence when both forms are present; HTTPS uses `HTTP_PROXY` when `HTTPS_PROXY` is absent. Use an HTTP(S) proxy URL and configure `NO_PROXY` for destinations that should connect directly (for example, `localhost,127.0.0.1`). This works on Node.js 20+ without `--use-env-proxy` and does not change the Host's global fetch dispatcher or MCP transport settings.
|
|
176
|
+
|
|
177
|
+
The proxy variables must be present in the running DSH process. After changing them, fully exit and restart DSH Desktop or launch `dsh web` from a shell with those variables set; changing Windows user environment variables does not update an already-running process. The connector does not read OS proxy settings or infer a local proxy address.
|
|
178
|
+
|
|
174
179
|
`showSidebarEntry` defaults to `true`. Users can override it from the DSH plugin configuration page; turning it off hides only the shortcut and does not disable the connector, connected MCP servers, or tools.
|
|
175
180
|
|
|
181
|
+
`frameAncestors` defaults to `['self']`: the CSP of the panel page (`/mcp-connector/ui/`) only allows same-origin embedding, which is the existing behaviour and is not relaxed by default. When a desktop shell (Tauri/Electron) embeds the panel, the ancestor origin differs from the panel origin, so the browser blocks it with `Framing '...' violates the following Content Security Policy directive: "frame-ancestors 'self'"` and the panel renders blank. Deployers must opt in explicitly:
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
frameAncestors: ['self', 'http://tauri.localhost', 'tauri://localhost']
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This is a deliberate security trade-off, not an oversight: `frame-ancestors` cannot verify who owns the embedding origin, so the plugin never hardcodes a shell origin such as Tauri — listing one lets any page served from that origin frame the local panel. Accepted values are CSP keywords (`self` / `none` / `*`) and host-sources (`https://*.example.com`, `127.0.0.1:3080`, `tauri://localhost`, `http://tauri.localhost:1420`, optionally with scheme, port, and path). Values containing `;`, commas, spaces, or non-host-source keywords such as `'unsafe-inline'` are dropped entirely to prevent CSP header injection; if nothing valid remains the list falls back to `['self']` rather than allowing every origin. Only add origins you trust.
|
|
188
|
+
|
|
176
189
|
## Compatibility and responsibility boundary
|
|
177
190
|
|
|
178
191
|
| Area | Current boundary |
|
|
@@ -202,7 +215,7 @@ npm run dev:ui
|
|
|
202
215
|
|
|
203
216
|
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
217
|
|
|
205
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
218
|
+
The current public version is [`dsh-mcp-connector@0.2.71`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.71](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.71).
|
|
206
219
|
|
|
207
220
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
208
221
|
|
package/README.md
CHANGED
|
@@ -162,12 +162,25 @@ Bundle 默认配置位于 `cordis.patch.yml`:
|
|
|
162
162
|
refreshSkewMs: 300000
|
|
163
163
|
openBrowser: true
|
|
164
164
|
showSidebarEntry: true
|
|
165
|
+
frameAncestors: ['self']
|
|
165
166
|
```
|
|
166
167
|
|
|
167
168
|
`catalogUrl` 默认通过 jsDelivr CDN 读取公共 [dsh-mcp-connector-registry](https://github.com/duhu2000/dsh-mcp-connector-registry),支持 ETag/TTL 缓存;主源失败时自动尝试 GitHub raw 备用源,再回退到上次缓存或随包内置目录。jsDelivr 的分支 URL 可能存在缓存延迟,因此 Registry 合并后的新卡片不保证秒级出现。需要离线/私有模式时可将 `catalogUrl` 显式设为空字符串;显式配置其他目录 URL 时不会自动切换到公共备用源。
|
|
168
169
|
|
|
170
|
+
远程目录请求(包括备用源)支持 `HTTP_PROXY`、`HTTPS_PROXY`、`NO_PROXY` 及其小写形式;同时存在时小写变量优先,未设置 `HTTPS_PROXY` 时 HTTPS 请求使用 `HTTP_PROXY`。代理地址须为 HTTP(S) URL;需要直连的目标可加入 `NO_PROXY`(例如 `localhost,127.0.0.1`)。Node.js 20+ 无需添加 `--use-env-proxy`;此行为不会修改 Host 的全局 fetch dispatcher 或 MCP 传输配置。
|
|
171
|
+
|
|
172
|
+
代理变量必须存在于正在运行的 DSH 进程中。修改后请完全退出并重启 DSH Desktop,或从已设置变量的终端启动 `dsh web`;修改 Windows 用户环境变量不会更新已有进程。连接器不会读取系统代理设置,也不会猜测本机代理地址。
|
|
173
|
+
|
|
169
174
|
`showSidebarEntry` 默认为 `true`。用户可在 DSH 插件配置页覆盖该值;关闭后只隐藏侧边栏快捷入口,不停用连接器、已连接 MCP Server 或工具。
|
|
170
175
|
|
|
176
|
+
`frameAncestors` 默认为 `['self']`,即面板页面(`/mcp-connector/ui/`)的 CSP 只允许同源页面内嵌,是既有行为,不做任何放宽。在桌面壳(Tauri/Electron)里把面板嵌进壳窗口时,祖先源与面板自身不同源,浏览器会以 `Framing '...' violates the following Content Security Policy directive: "frame-ancestors 'self'"` 拦截并导致面板白屏;此时需要由部署方/用户显式追加外壳源:
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
frameAncestors: ['self', 'http://tauri.localhost', 'tauri://localhost']
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
这是有意的安全取舍,不是遗漏:`frame-ancestors` 无法验证嵌入方到底是谁,所以插件不会默认把 Tauri 之类的外壳源硬编码进去——写进白名单就等于允许任何跑在该源上的页面内嵌本机面板。取值只接受 CSP 关键字(`self` / `none` / `*`)与 host-source(`https://*.example.com`、`127.0.0.1:3080`、`tauri://localhost`、`http://tauri.localhost:1420` 等,可带 scheme、端口与路径);含 `;`、逗号、空格或 `'unsafe-inline'` 一类非 host-source 关键字的值会被整条丢弃(避免 CSP 头注入),过滤后为空则回退 `['self']`,而不是放行任意源。请只填自己信任的源。
|
|
183
|
+
|
|
171
184
|
## 兼容性与责任边界
|
|
172
185
|
|
|
173
186
|
| 项目 | 当前边界 |
|
|
@@ -197,7 +210,7 @@ npm run dev:ui
|
|
|
197
210
|
|
|
198
211
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
199
212
|
|
|
200
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
213
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.71`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.71](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.71)。
|
|
201
214
|
|
|
202
215
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
203
216
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/cordis.patch.yml
CHANGED
package/docs/CLI-PROVIDERS.md
CHANGED
|
@@ -34,7 +34,7 @@ MCP 连接器原生支持 Streamable HTTP 和 stdio MCP。对于钉钉 `dws` 这
|
|
|
34
34
|
"-y",
|
|
35
35
|
"--legacy-peer-deps",
|
|
36
36
|
"--package",
|
|
37
|
-
"dsh-mcp-connector@0.2.
|
|
37
|
+
"dsh-mcp-connector@0.2.71",
|
|
38
38
|
"--package",
|
|
39
39
|
"dingtalk-workspace-cli@1.0.61",
|
|
40
40
|
"dsh-mcp-cli-bridge",
|
|
@@ -50,7 +50,7 @@ MCP 连接器原生支持 Streamable HTTP 和 stdio MCP。对于钉钉 `dws` 这
|
|
|
50
50
|
|
|
51
51
|
Windows 从 DSH 进程 PATH 中的全局 npm 或 npx `.bin` 位置查找官方包,直接运行包内 `vendor/dws.exe`;不执行 `.cmd`/`.bat`/`.ps1`,不启用 shell。也可在启动 DSH 的环境中设置 `DSH_MCP_DINGTALK_DWS_BIN` 为原生 exe 的绝对路径(允许空格),随后重启 DSH。无需复制 exe 到 npm 前缀;复制品可能在 CLI 升级后过期。缺失程序不等于 OAuth 未登录。
|
|
52
52
|
|
|
53
|
-
升级主插件并重启后,标准受管钉钉 0.2.48/0.2.49 桥接参数会在加载时迁移到 0.2.
|
|
53
|
+
升级主插件并重启后,标准受管钉钉 0.2.48/0.2.49/0.2.55 桥接参数会在加载时迁移到 0.2.71;自定义命令和参数不自动修改,需自行更新固定桥接版本。授权信息和环境配置保持不变。
|
|
54
54
|
|
|
55
55
|
- 当前用户、通讯录搜索与用户详情
|
|
56
56
|
- 日程列表与日程详情
|
|
@@ -54,6 +54,37 @@ DCR 返回的 `client_secret` 由插件与 Access/Refresh Token 一同保存在
|
|
|
54
54
|
|
|
55
55
|
插件优先读取 RFC 8414 Authorization Server Metadata;若动态注册或撤销端点缺失,会再读取 OIDC Discovery 并仅补齐缺失字段。OAuth 元数据中的授权、Token 等标准端点始终优先。
|
|
56
56
|
|
|
57
|
+
### 5.1 MCP URL 带查询参数时的 OAuth 资源标识
|
|
58
|
+
|
|
59
|
+
MCP URL 需要携带查询参数(`?tools=`、`?tenant=` 等)时,OAuth 的 `resource` 参数**不能**跟着带上查询串:RFC 8707 允许授权服务器以 `invalid_target` 拒绝与原始授权不一致的 `resource`,一旦查询串被写进 `authorizedResources`,刷新就会失败,用户会被反复要求重新授权。
|
|
60
|
+
|
|
61
|
+
因此 OAuth 卡片用 `servers[].oauthResource` 显式声明规范资源标识,`url` 保留查询参数:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"servers": [{
|
|
66
|
+
"serverKey": "search",
|
|
67
|
+
"url": "https://mcp.example.com/mcp?tools=web_search_exa,agent_run",
|
|
68
|
+
"oauthResource": "https://mcp.example.com/mcp",
|
|
69
|
+
"serverName": "vendor-search",
|
|
70
|
+
"transport": "streamable-http"
|
|
71
|
+
}]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
约束:
|
|
76
|
+
|
|
77
|
+
- 只有 `auth.mode: "oauth2-pkce"` 的卡片可以声明,其他鉴权模式声明会被目录审计拒绝;
|
|
78
|
+
- `oauthResource` 必须与 `url` **同源**,且通过 HTTPS / 回环 HTTP 检查,防止描述文件把 Token 引到其他域;
|
|
79
|
+
- 实际请求仍使用 `url`(含查询参数),`resource` 授权参数、Token 校验、`grant.authorizedResources` 与刷新参数使用 `oauthResource`;
|
|
80
|
+
- 不声明时行为与旧版完全一致(直接用 `url` 匹配)。
|
|
81
|
+
|
|
82
|
+
为什么这个字段是必需的(2026-10-10 实测):部分授权服务器签发的 access token **不带 `resource` claim**(Exa 只有 `aud: ["https://mcp.exa.ai", "https://api.exa.ai", "https://mcp.exa.ai/mcp"]`,无 `resource`),此时插件不按 token claim 过滤,而是直接采用 `servers[].oauthResource ?? url`。因此对这类厂商,**是否声明 `oauthResource` 就决定了 `authorizedResources` 是规范值还是带查询串的 URL**;带查询串的值会在每次刷新时作为 `resource` 发给授权服务器。
|
|
83
|
+
|
|
84
|
+
> 复现提示:卡片一旦被旧版插件安装过,本机 `catalog.dynamic` 里存的就是**已削掉 `oauthResource` 的副本**,仅升级插件不会修复它(连接仍可用,只是 `authorizedResources` 继续走 `url`)。升级后需要**重新安装该卡片**(重新走一次描述 URL 安装)才会带上新字段。
|
|
85
|
+
|
|
86
|
+
已知边界:`grantSharing: "issuer"` 的卡片在启动时归并历史共享授权(`consolidatePersistedSharedGrants`)按连接记录的 `url` 比较资源。查询参数场景下 `url` 与规范 `authorizedResources` 不再相等,该归并优化会被跳过——不影响连接、刷新与撤销,只是历史多 Grant 不会被自动合并。
|
|
87
|
+
|
|
57
88
|
符合上述条件的最小描述:
|
|
58
89
|
|
|
59
90
|
```json
|
|
@@ -83,7 +114,7 @@ DCR 返回的 `client_secret` 由插件与 Access/Refresh Token 一同保存在
|
|
|
83
114
|
}
|
|
84
115
|
```
|
|
85
116
|
|
|
86
|
-
### 5.
|
|
117
|
+
### 5.2 需要本机环境变量的 stdio 卡片
|
|
87
118
|
|
|
88
119
|
目录只声明字段和映射,示例见 `registry/connectors/stdio-credential.sample.json`:
|
|
89
120
|
|
|
@@ -210,3 +241,35 @@ QVeris Hosted MCP 提供单一 Streamable HTTP 端点 `https://mcp.qveris.ai/mcp
|
|
|
210
241
|
- https://mcp.bazhuayu.com/.well-known/oauth-protected-resource
|
|
211
242
|
- https://identity.bazhuayu.com/.well-known/oauth-authorization-server
|
|
212
243
|
- https://identity.bazhuayu.com/.well-known/openid-configuration
|
|
244
|
+
|
|
245
|
+
## 10. Exa 网络搜索适配结论
|
|
246
|
+
|
|
247
|
+
Exa 官方远程 MCP 为 `https://mcp.exa.ai/mcp`(Streamable HTTP;`initialize` 返回 serverInfo `exa-search-server`,版本 3.4.2),同时支持无凭据(默认工具)、API Key(`x-api-key` 或 `Authorization: Bearer`)与 OAuth 2.0。
|
|
248
|
+
|
|
249
|
+
2026-10-09 无凭据实测:
|
|
250
|
+
|
|
251
|
+
- Protected Resource Metadata:`resource=https://mcp.exa.ai/mcp`,授权服务器 `https://auth.exa.ai`,scope `mcp:tools`,`bearer_methods_supported: ["header"]`;
|
|
252
|
+
- Authorization Server Metadata(RFC 8414):issuer 与声明值精确相等,`code_challenge_methods_supported: ["S256"]`,`token_endpoint_auth_methods_supported: ["none"]`,并发布**动态客户端注册**与**撤销**端点;
|
|
253
|
+
- 用本机 loopback 回调按插件同款报文发起 DCR,返回 `201` 与 `client_id`,满足第 5 节"OAuth 一键授权"的全部门槛;
|
|
254
|
+
- `tools/list` 在未鉴权时可用;`tools/call` 无凭据走 IP 级免费额度(官方公布 3 QPS / 150 次每日),额度用尽返回 429。
|
|
255
|
+
|
|
256
|
+
工具面与本卡片的 `?tools=` 参数(2026-10-10 用真实密钥只读 `tools/list` 复测):
|
|
257
|
+
|
|
258
|
+
- 无凭据:默认 2 个工具(`web_search_exa`、`web_fetch_exa`);
|
|
259
|
+
- **有凭据**:纯 URL 自动变成 3 个工具——`agent_run` 是"鉴权即白送",并不需要写进 `tools=`;写成 `?tools=` 精确列表后,**凭据不会向列表追加工具**(只列 2 个就恰好 2 个);
|
|
260
|
+
- 卡片 URL `?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa,agent_run` 鉴权后列出 4 个工具,相对纯 URL 的**净增益只有 `web_search_advanced_exa`**(带筛选/域名/日期/摘要/子页抓取);
|
|
261
|
+
- `tools=` 是**精确替换**语义(不是追加),未知名称会被丢弃;若整份列表都不可识别会静默回落到默认 2 个,因此该列表是硬编码白名单——Exa 新增默认工具不会自动出现在卡片里,工具改名需要按第 3 节更新 Registry(不需要重新发布 npm);
|
|
262
|
+
- 列表含 `agent_run` 时,`initialize` 阶段就要求凭据,因此该 URL 的匿名 `initialize` 返回 401——这是预期行为,公开探针仍判可达且 `oauth: pass`;若只想暴露高级检索而完全不暴露计费工具,可改用 `?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa`(3 个工具,且匿名 `initialize` 可 200);
|
|
263
|
+
- 因为 MCP URL 带查询参数,卡片必须用 `servers[].oauthResource` 声明规范资源标识(见第 5.1 节),否则带查询串的 URL 会被写进 `authorizedResources`,刷新时 `resource` 参数不一致,可能被授权服务器按 `invalid_target` 拒绝。
|
|
264
|
+
|
|
265
|
+
计费与配额:搜索按请求计费(instant $4/1k、fast/auto $7/1k),`agent_run` 每次 $0.012–$1.00 或按量计费(默认上限 $5 auto / $20 ultra),余额耗尽返回 402。卡片保持 `featured: false`:保留 `agent_run`(多步研究是该分类的差异化能力),并以示例 Prompt 强制"先确认研究范围、effort 档位与金额上限"而不是把它从工具面移除。工具 schema 不随插件版本固定,`web_search_exa` 当前要求同时提供 `query` 与 `objective`。
|
|
266
|
+
|
|
267
|
+
参考:
|
|
268
|
+
|
|
269
|
+
- https://exa.ai/docs/get-started/exa-mcp
|
|
270
|
+
- https://github.com/exa-labs/exa-mcp-server
|
|
271
|
+
- https://mcp.exa.ai/.well-known/oauth-protected-resource
|
|
272
|
+
- https://auth.exa.ai/.well-known/oauth-authorization-server
|
|
273
|
+
- https://exa.ai/docs/admin/pricing
|
|
274
|
+
- https://dashboard.exa.ai/api-keys
|
|
275
|
+
|
package/docs/STDIO-SUPPORT.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# stdio 传输支持 —— 设计文档 & 开发实现
|
|
2
2
|
|
|
3
|
+
> **Windows 命令兼容(0.2.70)**:启动恢复保护使用 cross-spawn 解析 PATH/PATHEXT 中的命令,支持 npx 等 .cmd shim;通常无需将已有配置改成 cmd /c。uvx 需要先安装并位于 DSH 进程的 PATH 中。命令不存在时显示 ENOENT 与 PATH 检查提示,不输出参数或凭据。
|
|
4
|
+
|
|
3
5
|
> 目标:补齐 stdio(本地进程)传输方式,达到与 WorkBuddy / TraeWork / Qwen Code 三家大厂传输方式全覆盖。
|
|
4
6
|
> 结论:**低成本补齐**——`@deepseek-ai/dsh-mcp-client` 已原生支持 stdio,我们只需在 schema + provisioning 两层「开闸透传」。
|
|
5
7
|
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -83,7 +83,7 @@ dsh web
|
|
|
83
83
|
| `配置` | 需要 Bearer Token 或 API Key | 录入服务商签发的凭据并验证 |
|
|
84
84
|
| `需重新授权` | OAuth 授权过期、被撤销或不可用 | 点击后重新完成 OAuth 授权 |
|
|
85
85
|
| `自动重试中` | OAuth Token 刷新遇到网络或服务端暂时故障 | 无需重新授权;保持 DSH 运行,插件会按退避策略自动恢复 |
|
|
86
|
-
| `状态未知` | 本机已有配置,但本进程尚未观察到可用性检查结果;不代表健康或异常 |
|
|
86
|
+
| `状态未知` | 本机已有配置,但本进程尚未观察到可用性检查结果;不代表健康或异常 | 点击“刷新连接器目录”,或在该连接上点“重新连接” |
|
|
87
87
|
| `已连接` | 最近一次健康检查通过 | 可直接在会话中使用对应 MCP 工具 |
|
|
88
88
|
| `部分异常` / `连接异常` | 一个或多个 Server 未通过检查 | 打开详情查看提示,核对网络、权限和服务状态 |
|
|
89
89
|
|
|
@@ -199,6 +199,8 @@ stdio 进程由 `@deepseek-ai/dsh-mcp-client` 管理,插件透传 `command`、
|
|
|
199
199
|
- 启用或停用连接;
|
|
200
200
|
- 重新授权 OAuth,或重新录入 Token/API Key;
|
|
201
201
|
- 执行健康检查;
|
|
202
|
+
- 重新连接:重建 Host 侧条目并复检,用于“状态未知”或 Host 已放弃重连的连接;它不修改配置、凭据、范围或启停状态;
|
|
203
|
+
- 检测企查查旧授权:扫描两个已退役的企查查 OAuth 插件(`qcc-dsh-mcp-oauth`、`qcc-dsh-mcp-legal-oauth`)遗留的本机授权,确认后可复制到 MCP连接器;不会删除旧插件或原凭据;
|
|
202
204
|
- 断开连接并删除该连接的本机配置。
|
|
203
205
|
|
|
204
206
|
OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端点时仍会删除 DSH 本机授权记录。连接状态发生变化后,可点击“刷新连接器目录”重新检查。
|
|
@@ -315,7 +317,7 @@ OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端
|
|
|
315
317
|
|
|
316
318
|
### stdio 启动失败
|
|
317
319
|
|
|
318
|
-
页面会在有限时间内结束等待,并区分命令不存在、进程退出、初始化失败或启动超时。先在终端确认命令本身可执行、软件包可信、Node/运行时版本满足要求,并检查 `cwd`、参数和环境变量;随后查看 Host
|
|
320
|
+
页面会在有限时间内结束等待,并区分命令不存在、进程退出、初始化失败或启动超时。先在终端确认命令本身可执行、软件包可信、Node/运行时版本满足要求,并检查 `cwd`、参数和环境变量;随后查看 Host 日志,并在该连接上点击“重新连接”(Host 放弃重连后不会自发恢复)。不要把本机凭据写入公开 Registry descriptor。
|
|
319
321
|
|
|
320
322
|
#### 1. 确认命令与运行环境
|
|
321
323
|
|
|
@@ -384,7 +386,7 @@ Windows 下 `.cmd` / `.bat` 是脚本包装器,不等同于可直接启动的
|
|
|
384
386
|
|
|
385
387
|
### 为什么显示“状态未知”
|
|
386
388
|
|
|
387
|
-
这表示插件没有足够证据确认健康或失败,常见于 DSH 刚重启、尚未执行健康检查,或当前 Host 无法读取 stdio
|
|
389
|
+
这表示插件没有足够证据确认健康或失败,常见于 DSH 刚重启、尚未执行健康检查,或当前 Host 无法读取 stdio 工具注册状态。先点击“刷新连接器目录”;若仍为未知,在该连接(或详情页)点“重新连接”重建 Host 侧条目——Host 的 mcp-client 在连续重连失败后会注销工具且不再自发重试,只有重建条目才能恢复。若重建后依旧未知,再根据诊断的 `code` 和建议查看 Host 日志或升级 Host。不要把“状态未知”理解为“已连接”。
|
|
388
390
|
|
|
389
391
|
### 如何按诊断代码排查
|
|
390
392
|
|
package/lib/catalog.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 合并键为 id,优先级:本地覆盖 > config.connectors > 远程 registry > 内置目录。
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { EnvHttpProxyAgent } from 'undici';
|
|
6
7
|
import { normalizeConnectorDescriptor } from './schema.js';
|
|
7
8
|
import { assertSafeUrl, assertSafeHeaderName } from './util.js';
|
|
8
9
|
|
|
@@ -46,7 +47,7 @@ export function auditDescriptor(descriptor) {
|
|
|
46
47
|
const oauthHttpResources = new Set(
|
|
47
48
|
descriptor.servers
|
|
48
49
|
.filter((server) => server.transport === 'streamable-http')
|
|
49
|
-
.map((server) => server.url),
|
|
50
|
+
.map((server) => server.oauthResource ?? server.url),
|
|
50
51
|
);
|
|
51
52
|
const credentialFields = descriptor.auth.credentialFields ?? [];
|
|
52
53
|
const credentialKeys = new Set();
|
|
@@ -106,6 +107,20 @@ export function auditDescriptor(descriptor) {
|
|
|
106
107
|
continue;
|
|
107
108
|
}
|
|
108
109
|
assertSafeUrl(server.url);
|
|
110
|
+
// HTTP Server 可声明 oauthResource:实际请求仍用 url(允许带查询参数,如 `?tools=`),
|
|
111
|
+
// 但 OAuth resource 标识、authorizedResources 与刷新参数使用该规范值。
|
|
112
|
+
if (server.oauthResource) {
|
|
113
|
+
if (descriptor.auth.mode !== 'oauth2-pkce') {
|
|
114
|
+
throw new Error(`connector "${descriptor.id}" 只有 oauth2-pkce 卡片可声明 oauthResource: ${server.oauthResource}`);
|
|
115
|
+
}
|
|
116
|
+
assertSafeUrl(server.oauthResource);
|
|
117
|
+
if (new URL(server.oauthResource).origin !== new URL(server.url).origin) {
|
|
118
|
+
throw new Error(`connector "${descriptor.id}" oauthResource 必须与 server.url 同源: ${server.oauthResource}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (server.oauthTokenEnv) {
|
|
122
|
+
throw new Error(`connector "${descriptor.id}" 只有 OAuth stdio server 可声明 oauthTokenEnv`);
|
|
123
|
+
}
|
|
109
124
|
for (const name of Object.keys(server.headers ?? {})) {
|
|
110
125
|
assertSafeHeaderName(name);
|
|
111
126
|
if (/authorization|token|secret|api[-_]?key/i.test(name)) {
|
|
@@ -143,12 +158,22 @@ export async function readLimitedJson(response, maxBytes = MAX_CATALOG_BYTES) {
|
|
|
143
158
|
/** 拉取远程 registry(HTTPS + TTL 缓存 + 可选 ETag) */
|
|
144
159
|
export async function fetchRemoteCatalog(catalogUrl, { requestTimeoutMs, etag } = {}) {
|
|
145
160
|
assertSafeUrl(catalogUrl);
|
|
161
|
+
// Node 20+ does not consistently enable environment proxies for global fetch.
|
|
162
|
+
// Scope the dispatcher to this request rather than changing the DSH Host's
|
|
163
|
+
// global dispatcher. Undici handles HTTPS fallback and NO_PROXY matching.
|
|
164
|
+
const httpProxy = process.env.http_proxy ?? process.env.HTTP_PROXY;
|
|
165
|
+
const httpsProxy = process.env.https_proxy ?? process.env.HTTPS_PROXY;
|
|
166
|
+
const dispatcher = httpProxy || httpsProxy ? new EnvHttpProxyAgent() : undefined;
|
|
146
167
|
const controller = new AbortController();
|
|
147
168
|
const timer = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs ?? 15_000);
|
|
148
169
|
try {
|
|
149
170
|
const headers = {};
|
|
150
171
|
if (etag) headers['If-None-Match'] = etag;
|
|
151
|
-
const response = await fetch(catalogUrl, {
|
|
172
|
+
const response = await fetch(catalogUrl, {
|
|
173
|
+
headers,
|
|
174
|
+
signal: controller.signal,
|
|
175
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
176
|
+
});
|
|
152
177
|
assertSafeUrl(response.url || catalogUrl);
|
|
153
178
|
if (response.status === 304) return { notModified: true };
|
|
154
179
|
if (!response.ok) throw new Error(`catalog fetch HTTP ${response.status}`);
|
|
@@ -158,6 +183,9 @@ export async function fetchRemoteCatalog(catalogUrl, { requestTimeoutMs, etag }
|
|
|
158
183
|
return { notModified: false, etag: response.headers.get('etag') ?? undefined, connectors };
|
|
159
184
|
} finally {
|
|
160
185
|
clearTimeout(timer);
|
|
186
|
+
// Destroy also releases unread error/oversize bodies; close could wait for
|
|
187
|
+
// those bodies indefinitely. Never destroy a dispatcher owned by the Host.
|
|
188
|
+
await dispatcher?.destroy();
|
|
161
189
|
}
|
|
162
190
|
}
|
|
163
191
|
|
package/lib/cli-providers.js
CHANGED
|
@@ -314,7 +314,9 @@ export function resolveCliExecutable(command, options = {}) {
|
|
|
314
314
|
if (!existsSync(path.join(root, 'package.json'))) continue;
|
|
315
315
|
try {
|
|
316
316
|
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
317
|
-
|
|
317
|
+
// npm tarballs preserve "./", while registry metadata can omit it.
|
|
318
|
+
// Accept only these two canonical entries; do not normalize traversal.
|
|
319
|
+
if (pkg.name !== 'dingtalk-workspace-cli' || !['bin/dws.js', './bin/dws.js'].includes(pkg.bin?.dws)) continue;
|
|
318
320
|
const realRoot = realpathSync(root);
|
|
319
321
|
const binary = realpathSync(path.join(root, 'vendor', 'dws.exe'));
|
|
320
322
|
const relative = path.relative(realRoot, binary);
|
|
@@ -443,12 +445,12 @@ export function cliBridgeArgs(record) {
|
|
|
443
445
|
const args = record.args ?? [];
|
|
444
446
|
if (!legacy[3] || record.connectorId !== 'dingtalk' || record.command !== 'npx'
|
|
445
447
|
|| JSON.stringify(args) !== JSON.stringify(legacy)) return args;
|
|
446
|
-
return args.map((arg) => arg === legacy[3] ? 'dsh-mcp-connector@0.2.
|
|
448
|
+
return args.map((arg) => arg === legacy[3] ? 'dsh-mcp-connector@0.2.71' : arg);
|
|
447
449
|
}
|
|
448
450
|
|
|
449
451
|
function argsVersion(record) {
|
|
450
452
|
const value = record.args?.[3];
|
|
451
|
-
return ['dsh-mcp-connector@0.2.48', 'dsh-mcp-connector@0.2.49'].includes(value) ? value : null;
|
|
453
|
+
return ['dsh-mcp-connector@0.2.48', 'dsh-mcp-connector@0.2.49', 'dsh-mcp-connector@0.2.55'].includes(value) ? value : null;
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
export async function handleCliBridgeRequest(message, options = {}) {
|
package/lib/client.js
CHANGED
|
@@ -631,13 +631,19 @@ window.__ModuleLoader__.load({
|
|
|
631
631
|
return `dsh plugin --profile web add --config.minimumReleaseAge=0 ${PLUGIN_PACKAGE_NAME}@${parsed.normalized}`;
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
/** 尝试写入剪贴板;失败时调用方把命令展示在页面内,不退回原生 prompt。 */
|
|
634
635
|
async function copyManualUpgradeCommand(command) {
|
|
635
|
-
|
|
636
|
-
|
|
636
|
+
const clipboard = window.navigator?.clipboard;
|
|
637
|
+
if (typeof clipboard?.writeText !== "function") return false;
|
|
638
|
+
try {
|
|
639
|
+
await clipboard.writeText(command);
|
|
637
640
|
return true;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
// 桌面壳里原生弹窗关闭后不会把键盘焦点还给页面,所以这里只记录失败,
|
|
643
|
+
// 由调用方改用页面内提示 + 可一键选中的命令。
|
|
644
|
+
console.warn("[mcp-connector] clipboard write failed:", error);
|
|
645
|
+
return false;
|
|
638
646
|
}
|
|
639
|
-
window.prompt?.("请复制以下升级命令", command);
|
|
640
|
-
return false;
|
|
641
647
|
}
|
|
642
648
|
|
|
643
649
|
function parseClientVersion(value) {
|
|
@@ -1120,6 +1126,7 @@ window.__ModuleLoader__.load({
|
|
|
1120
1126
|
const [rollbackRunning, setRollbackRunning] = (0, react.useState)(false);
|
|
1121
1127
|
const [restarting, setRestarting] = (0, react.useState)(false);
|
|
1122
1128
|
const [manualCommandCopied, setManualCommandCopied] = (0, react.useState)(false);
|
|
1129
|
+
const [manualCommandVisible, setManualCommandVisible] = (0, react.useState)(false);
|
|
1123
1130
|
const [versionCheckRequest, setVersionCheckRequest] = (0, react.useState)(0);
|
|
1124
1131
|
const [versionCheckBusy, setVersionCheckBusy] = (0, react.useState)(false);
|
|
1125
1132
|
const [versionCheckFailed, setVersionCheckFailed] = (0, react.useState)(false);
|
|
@@ -1553,6 +1560,8 @@ window.__ModuleLoader__.load({
|
|
|
1553
1560
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1554
1561
|
className: "mcpConnectorManualCommand",
|
|
1555
1562
|
title: command,
|
|
1563
|
+
// 点击即全选,方便手动复制(剪贴板不可用时的回退路径)。
|
|
1564
|
+
onClick: (event) => window.getSelection?.()?.selectAllChildren?.(event.currentTarget),
|
|
1556
1565
|
children: command
|
|
1557
1566
|
}),
|
|
1558
1567
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -1560,14 +1569,20 @@ window.__ModuleLoader__.load({
|
|
|
1560
1569
|
className: "mcpConnectorUpdateButton",
|
|
1561
1570
|
onClick: () => {
|
|
1562
1571
|
copyManualUpgradeCommand(command).then((copied) => {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1572
|
+
// 剪贴板不可用时给出页面内提示,让用户直接选中上面的命令,
|
|
1573
|
+
// 不再退回原生 prompt:桌面壳里原生弹窗关闭后不会把键盘
|
|
1574
|
+
// 焦点还给页面。成功时顺带撤掉失败提示,避免留在界面上。
|
|
1575
|
+
setManualCommandCopied(copied);
|
|
1576
|
+
setManualCommandVisible(!copied);
|
|
1567
1577
|
});
|
|
1568
1578
|
},
|
|
1569
1579
|
children: manualCommandCopied ? "已复制" : "复制升级命令"
|
|
1570
1580
|
}),
|
|
1581
|
+
manualCommandVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1582
|
+
className: "mcpConnectorUpdateStatus",
|
|
1583
|
+
"data-tone": "error",
|
|
1584
|
+
children: "无法写入剪贴板,请点击上方命令全选后手动复制"
|
|
1585
|
+
}),
|
|
1571
1586
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1572
1587
|
type: "button",
|
|
1573
1588
|
className: "mcpConnectorUpdateButton mcpConnectorUpdateSecondary",
|
package/lib/index.js
CHANGED
|
@@ -125,6 +125,17 @@ export const Config = z.object({
|
|
|
125
125
|
account: z.string().default(DEFAULT_ACCOUNT),
|
|
126
126
|
/** 是否在 DSH 主侧边栏显示 MCP连接器快捷入口 */
|
|
127
127
|
showSidebarEntry: z.boolean().default(true),
|
|
128
|
+
/**
|
|
129
|
+
* 图形化市场面板(/mcp-connector/ui/)的 CSP `frame-ancestors` 白名单。
|
|
130
|
+
*
|
|
131
|
+
* 默认 `['self']`:只有同源页面能内嵌面板,保持既有行为不变。
|
|
132
|
+
* 桌面壳(Tauri/Electron)内嵌面板时,祖先源与面板自身不同源,浏览器会以
|
|
133
|
+
* `violates ... "frame-ancestors 'self'"` 拦截并导致面板白屏;此时需显式追加
|
|
134
|
+
* 外壳源,例如 `frameAncestors: ['self', 'http://tauri.localhost', 'tauri://localhost']`。
|
|
135
|
+
* 注意这是有意的安全取舍:加入白名单的源都能内嵌本面板,请只填你自己信任的源。
|
|
136
|
+
* 非法值(含 `;`/空格等注入尝试、非 host-source 关键字)会被丢弃,全部非法则回退 `['self']`。
|
|
137
|
+
*/
|
|
138
|
+
frameAncestors: z.array(z.string()).default(['self']),
|
|
128
139
|
});
|
|
129
140
|
|
|
130
141
|
export async function apply(ctx, config) {
|
|
@@ -1015,6 +1026,21 @@ export async function apply(ctx, config) {
|
|
|
1015
1026
|
}
|
|
1016
1027
|
}
|
|
1017
1028
|
|
|
1029
|
+
/**
|
|
1030
|
+
* 等待 Host 侧注册这个 stdio server 的工具。
|
|
1031
|
+
* 重建条目后 mcp-client 需要重新 spawn 并完成 initialize/tools-list;在此期间
|
|
1032
|
+
* "没有工具" 与 "启动失败" 无法区分,所以这里只做有界的轮询,不把它当作失败。
|
|
1033
|
+
*/
|
|
1034
|
+
async function waitForStdioToolRegistration(record, timeoutMs = 5_000) {
|
|
1035
|
+
const deadline = Date.now() + timeoutMs;
|
|
1036
|
+
for (;;) {
|
|
1037
|
+
const registered = registeredStdioTools(record);
|
|
1038
|
+
if (registered.tools.length > 0) return registered.tools.length;
|
|
1039
|
+
if (Date.now() >= deadline) return 0;
|
|
1040
|
+
await new Promise((resolve) => { setTimeout(resolve, 250).unref?.(); });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1018
1044
|
async function cacheLastSuccessfulTools(record, tools) {
|
|
1019
1045
|
// A request may finish after disconnect/reconfigure. Never resurrect old metadata.
|
|
1020
1046
|
if (state.connections.get(record.key) !== record || record.enabled === false) return null;
|
|
@@ -2650,6 +2676,69 @@ export async function apply(ctx, config) {
|
|
|
2650
2676
|
return { ok: true, message: `已停用 "${next.name}"`, detail: { snapshotId: snapshot?.key ?? null } };
|
|
2651
2677
|
},
|
|
2652
2678
|
|
|
2679
|
+
/**
|
|
2680
|
+
* 重新建立一条已启用连接的 Host 条目,并复检它的连接状态。
|
|
2681
|
+
*
|
|
2682
|
+
* Host 的 mcp-client 连续重连失败后会注销工具且不再自发重试(日志:giving up after
|
|
2683
|
+
* 10 consecutive failed reconnect attempts),此前只有停用/启用或重启 Host 能恢复。
|
|
2684
|
+
* 这里用与停用/启用相同的 loader 序列触发一次真正的重连,并在有界时间内等待 stdio
|
|
2685
|
+
* 工具重新注册;不修改连接记录、凭据、范围或注入模式。
|
|
2686
|
+
*/
|
|
2687
|
+
async reconnect(key) {
|
|
2688
|
+
const record = state.connections.get(key);
|
|
2689
|
+
if (!record) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
2690
|
+
if (record.enabled === false) return { ok: false, message: `连接 "${record.name}" 已停用;请先启用再重新连接` };
|
|
2691
|
+
const failure = grantFailure(record);
|
|
2692
|
+
if (failure) return { ok: false, message: failure.message };
|
|
2693
|
+
try {
|
|
2694
|
+
await disable(ctx, config, record);
|
|
2695
|
+
if (state.connections.get(key) !== record) return { ok: false, message: '连接配置已变化,请重新检查后重试' };
|
|
2696
|
+
await provision(ctx, config, record, grantMap(), { failOnStartupError: false });
|
|
2697
|
+
} catch (error) {
|
|
2698
|
+
return connectionSetupFailure(error, '重新连接失败', '已保留连接配置,但 Host 条目可能仍停用;请检查后重新连接');
|
|
2699
|
+
}
|
|
2700
|
+
const records = [...state.connections.values()].filter((item) => item.connectorId === record.connectorId);
|
|
2701
|
+
const enabledRecords = records.filter((item) => item.enabled !== false);
|
|
2702
|
+
const stdioRecords = enabledRecords.filter((item) => item.transport === 'stdio');
|
|
2703
|
+
const counts = await Promise.all(stdioRecords.map((item) => waitForStdioToolRegistration(item)));
|
|
2704
|
+
const registeredTools = counts.reduce((total, count) => total + count, 0);
|
|
2705
|
+
if (state.connections.get(key) !== record) return { ok: false, message: '连接配置已变化,请重新检查状态' };
|
|
2706
|
+
const results = stdioRecords.map((item) => {
|
|
2707
|
+
const registered = registeredStdioTools(item);
|
|
2708
|
+
if (registered.tools.length > 0) {
|
|
2709
|
+
return { ok: true, kind: 'connected', serverKey: item.serverKey, serverName: item.serverName, toolCount: registered.tools.length, message: `Host 已注册 ${registered.tools.length} 个工具` };
|
|
2710
|
+
}
|
|
2711
|
+
return {
|
|
2712
|
+
ok: true,
|
|
2713
|
+
kind: 'managed',
|
|
2714
|
+
code: registered.error ? 'host-status-error' : registered.supported ? 'host-tools-pending' : 'host-status-unavailable',
|
|
2715
|
+
serverKey: item.serverKey,
|
|
2716
|
+
serverName: item.serverName,
|
|
2717
|
+
message: registered.error
|
|
2718
|
+
? 'Host 工具注册状态读取失败'
|
|
2719
|
+
: registered.supported ? 'Host 尚未注册该 stdio Server 的工具' : '当前 Host 无法读取 stdio 工具注册状态',
|
|
2720
|
+
};
|
|
2721
|
+
});
|
|
2722
|
+
const checkable = enabledRecords.filter((item) => item.transport !== 'stdio');
|
|
2723
|
+
if (checkable.length > 0) {
|
|
2724
|
+
const validation = await validateConnectionRecords(checkable, {
|
|
2725
|
+
timeoutMs: Math.min(config.requestTimeoutMs, 5_000),
|
|
2726
|
+
concurrency: 4,
|
|
2727
|
+
grants: grantMap(),
|
|
2728
|
+
});
|
|
2729
|
+
results.push(...validation.detail.results);
|
|
2730
|
+
}
|
|
2731
|
+
const summary = cacheHealth(record.connectorId, records, results);
|
|
2732
|
+
const healthy = summary.connectionState === 'healthy';
|
|
2733
|
+
return {
|
|
2734
|
+
ok: healthy,
|
|
2735
|
+
message: healthy
|
|
2736
|
+
? `已重新连接 "${record.name}"${registeredTools > 0 ? `:Host 已注册 ${registeredTools} 个工具` : ''}`
|
|
2737
|
+
: `已重建 "${record.name}" 的 Host 条目,但当前仍为${summary.label}:${summary.diagnostic?.message ?? '请稍后重试'}`,
|
|
2738
|
+
detail: { connectorId: record.connectorId, key: record.key, connectionState: summary.connectionState, registeredTools },
|
|
2739
|
+
};
|
|
2740
|
+
},
|
|
2741
|
+
|
|
2653
2742
|
async setInjectionMode(key, mode) {
|
|
2654
2743
|
const record = state.connections.get(key);
|
|
2655
2744
|
if (!record) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
@@ -3323,7 +3412,21 @@ export async function apply(ctx, config) {
|
|
|
3323
3412
|
discoveryTimer = setTimeout(() => { discoveryTimer = null; void discoverTools(); }, 100);
|
|
3324
3413
|
discoveryTimer.unref?.();
|
|
3325
3414
|
}
|
|
3326
|
-
|
|
3415
|
+
// Serialize same-record actions: a delayed reconnect must never reactivate a
|
|
3416
|
+
// connection the user has subsequently disabled or disconnected.
|
|
3417
|
+
const connectionActions = new Set(['reconnect', 'setEnabled', 'disconnect', 'reconfigureConnection', 'renameConnection', 'setInjectionMode']);
|
|
3418
|
+
const connectionQueues = new Map();
|
|
3419
|
+
for (const name of connectionActions) {
|
|
3420
|
+
const action = api[name];
|
|
3421
|
+
api[name] = async (key, ...args) => {
|
|
3422
|
+
const previous = connectionQueues.get(key) ?? Promise.resolve();
|
|
3423
|
+
const pending = previous.catch(() => {}).then(() => action.call(api, key, ...args));
|
|
3424
|
+
connectionQueues.set(key, pending);
|
|
3425
|
+
try { return await pending; }
|
|
3426
|
+
finally { if (connectionQueues.get(key) === pending) connectionQueues.delete(key); }
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
const discoveryMutations = new Set(['connect', 'configure', 'importJson', 'installFromUrl', 'reconfigureConnection', 'setEnabled', 'reconnect', 'restoreSnapshot', 'migrateLegacy']);
|
|
3327
3430
|
const discoveryApi = new Proxy(api, { get(target, property) {
|
|
3328
3431
|
const value = target[property];
|
|
3329
3432
|
if (!discoveryMutations.has(property)) return value;
|
|
@@ -3347,7 +3450,11 @@ export async function apply(ctx, config) {
|
|
|
3347
3450
|
if (typeof ctx.inject === 'function') {
|
|
3348
3451
|
try {
|
|
3349
3452
|
ctx.inject(['webServer', 'webRuntime'], (wctx) => {
|
|
3350
|
-
disposeWeb = mountWebRoutes(wctx, observableApi, {
|
|
3453
|
+
disposeWeb = mountWebRoutes(wctx, observableApi, {
|
|
3454
|
+
logger,
|
|
3455
|
+
eventHub: statusEvents,
|
|
3456
|
+
frameAncestors: config.frameAncestors,
|
|
3457
|
+
});
|
|
3351
3458
|
logger.info('mcp-connector web UI mounted at /mcp-connector/ui/');
|
|
3352
3459
|
});
|
|
3353
3460
|
} catch (error) {
|
package/lib/mcp-provision.js
CHANGED
|
@@ -8,6 +8,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
|
|
9
9
|
const STDIO_STARTUP_GUARD = fileURLToPath(new URL('./stdio-startup-guard.js', import.meta.url));
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 桌面宿主(Electron 内嵌 DSH)的 `process.execPath` 是可执行文件本体:只有带上
|
|
13
|
+
* `ELECTRON_RUN_AS_NODE` 才会作为 Node 运行 guard 脚本;CLI 宿主(node 进程)不需要。
|
|
14
|
+
* 该变量只服务于 guard 自身,guard 启动真实 server 前会把它移除。
|
|
15
|
+
*/
|
|
16
|
+
function isElectronRuntime() {
|
|
17
|
+
return typeof process.versions?.electron === 'string';
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export function entryIdFor(config, record) {
|
|
12
21
|
return `${config.entryPrefix}-${record.key}`;
|
|
13
22
|
}
|
|
@@ -46,7 +55,7 @@ export function buildEntryConfig(record, grants, { failOnStartupError = false, s
|
|
|
46
55
|
serverName: record.serverName,
|
|
47
56
|
command: guarded ? process.execPath : record.command,
|
|
48
57
|
args: guarded ? [STDIO_STARTUP_GUARD, '--timeout-ms', String(startupRequestTimeoutMs), '--', record.command, ...args] : args,
|
|
49
|
-
env,
|
|
58
|
+
env: guarded && isElectronRuntime() ? { ...env, ELECTRON_RUN_AS_NODE: '1' } : env,
|
|
50
59
|
cwd: record.cwd || process.cwd(),
|
|
51
60
|
failOnStartupError,
|
|
52
61
|
};
|
package/lib/schema.js
CHANGED
|
@@ -265,8 +265,11 @@ export function normalizeConnectorDescriptor(raw) {
|
|
|
265
265
|
args: transport === 'stdio' ? (s.args ?? []) : undefined,
|
|
266
266
|
env: transport === 'stdio' ? (s.env ?? {}) : undefined,
|
|
267
267
|
credentialBindings: transport === 'stdio' ? (s.credentialBindings ?? {}) : undefined,
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
// OAuth 资源标识对 HTTP 与 stdio 都有效:HTTP 用它解耦「实际请求 URL(可带查询参数)」
|
|
269
|
+
// 与「OAuth resource 标识」,避免把 `?tools=` 之类的查询串写进 authorizedResources。
|
|
270
|
+
// oauthTokenEnv 仅 stdio 有意义,这里不做静默丢弃,交给 auditDescriptor 显式报错。
|
|
271
|
+
oauthResource: s.oauthResource,
|
|
272
|
+
oauthTokenEnv: s.oauthTokenEnv,
|
|
270
273
|
cwd: transport === 'stdio' ? (s.cwd ?? '') : undefined,
|
|
271
274
|
serverName: s.serverName,
|
|
272
275
|
transport,
|