dsh-mcp-connector 0.2.74 → 0.2.76
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 +29 -0
- package/README.en.md +12 -2
- package/README.md +12 -2
- package/cordis.patch.yml +3 -0
- package/docs/DESKTOP-E2E.md +21 -2
- package/lib/connection-backup.js +3 -0
- package/lib/index.js +99 -15
- package/lib/local-overrides.js +63 -0
- package/lib/mcp-provision.js +2 -0
- package/lib/schema.js +3 -0
- package/lib/status-events.js +1 -0
- package/lib/tool-catalog-cache.js +1 -1
- package/lib/tools.js +73 -4
- package/lib/web.js +2 -0
- package/package.json +4 -3
- package/ui/index.html +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/);首个公开版本从 `0.1.0` 开始。
|
|
4
4
|
|
|
5
|
+
## [0.2.76] - 2026-10-20
|
|
6
|
+
|
|
7
|
+
### Added / Fixed
|
|
8
|
+
|
|
9
|
+
- 无凭据市场 stdio 连接可在首次安装及已安装界面设置本地 argsAppend/env/cwd;保留目录来源和连接身份,支持脱敏编辑、快照恢复、失败重试及停用保持。
|
|
10
|
+
- 串行处理首次安装、编辑、停用和断开,防止迟到启动恢复过期状态;持久化与回滚双重失败保留恢复快照并返回安全提示。
|
|
11
|
+
- 修正已安装工具详情将 stdio 错标为远程服务的问题。
|
|
12
|
+
- 不开放 OAuth、凭据绑定、CLI 桥接或完整参数替换。
|
|
13
|
+
|
|
14
|
+
### Verification and known limitations
|
|
15
|
+
|
|
16
|
+
- 完整 check(含隔离真实 MCP bridge):427 passed / 4 skipped / 0 failed;89 个发布文件检查通过。
|
|
17
|
+
- macOS 隔离真实 UI 验证安装、编辑、停用/启用、工具发现、失败重试、快照恢复、脱敏导出及断开。
|
|
18
|
+
- 按维护者明确要求发布。Windows Desktop 实机、完整宿主重启持久化与生产 ToolRuntime UI 调用尚未验收;CI 不等于 Desktop 验收,#146 保持开放。详见 docs/ACCEPTANCE-146-20261020.md。
|
|
19
|
+
|
|
20
|
+
## [0.2.75] - 2026-10-17
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- 可选 `compactManagementTools` 精简管理工具模式(#161):开启后将 25 个管理工具收敛为只读发现与原有会话管理两个入口;图形界面功能不变。默认关闭以兼容旧工具名,修改后需重载插件。
|
|
25
|
+
- 统一发现入口仅允许目录、状态、缓存搜索与详情四种操作,保留工作区参数与原有渲染,不提供通用工具调用、凭据变更或授权代理。业务工具可见性、会话确认和 Host 审批沿用原机制。
|
|
26
|
+
- 教程相对 inline Markdown 链接与图片检查(#159 / #156),覆盖 Windows 路径、代码片段排除、标题与括号文件名、越界及不存在的目标;纳入 `npm run check`。不宣称覆盖引用式链接、HTML 或完整 CommonMark。
|
|
27
|
+
|
|
28
|
+
### Verification scope
|
|
29
|
+
|
|
30
|
+
- 本地完整 `npm run check`:408 项通过、4 项条件跳过、0 失败;88 个发布文件通过白名单与凭据/本机路径扫描。跳过项不计作通过。
|
|
31
|
+
- 新增工具数量、schema 大小、严格分派、结果渲染、参数拒绝、会话上下文、卸载清理和图形 API 集成回归。#163 与 #159 已通过 Node 20/22/24、Windows 和 Zod 4.1.8 CI。真实 DSH Web/Desktop 开关重载及模型调用验收仍需单独完成;#161 保留打开,精简模式默认关闭。
|
|
32
|
+
- #146 本地参数覆盖、#105 Schema 变化提醒、#83 钉钉授权等开发草案不包含在本次发布中;旧企查查凭据不做自动迁移。
|
|
33
|
+
|
|
5
34
|
## [0.2.74] - 2026-10-16
|
|
6
35
|
|
|
7
36
|
### 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-10-
|
|
88
|
+
As of 2026-10-17, the public Registry publishes 110 connector descriptors. After merging and deduplicating them with the 4 bundled Qichacha cards, the Marketplace exposes 114 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
|
|
@@ -167,6 +167,7 @@ The default bundle configuration is in `cordis.patch.yml`:
|
|
|
167
167
|
refreshSkewMs: 300000
|
|
168
168
|
openBrowser: true
|
|
169
169
|
showSidebarEntry: true
|
|
170
|
+
compactManagementTools: false
|
|
170
171
|
frameAncestors: ['self']
|
|
171
172
|
```
|
|
172
173
|
|
|
@@ -178,6 +179,15 @@ The proxy variables must be present in the running DSH process. After changing t
|
|
|
178
179
|
|
|
179
180
|
`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.
|
|
180
181
|
|
|
182
|
+
`compactManagementTools` defaults to `false` for backward compatibility. Set it to `true` in the plugin configuration and reload the plugin (or restart DSH) to expose only two management tools at the next inference boundary:
|
|
183
|
+
|
|
184
|
+
- `mcp_connector_discover`: fixed read-only actions `catalog`, `status`, `search` (requires `query`), and `detail` (requires `toolName`). Example: `{"action":"search","query":"weather","limit":5}`.
|
|
185
|
+
- `mcp_connector_session_tools`: unchanged session activation, confirmation and TTL behavior.
|
|
186
|
+
|
|
187
|
+
Use the connector UI for connection setup, authorization, import, editing, snapshots and migration. There is no arbitrary execution proxy or automatic management-tool loading. Keep this option `false` for conversation-only deployments or workflows that depend on legacy tool names. Set it back to `false` and reload to restore those names; do not run duplicate plugin instances.
|
|
188
|
+
|
|
189
|
+
This setting does not hide downstream business tools, disable connections, migrate credentials, change workspace/governance rules or bypass Host approval. Use the existing session injection mode for downstream tool visibility. Reduced tool count/schema size is not a measured token or cost saving.
|
|
190
|
+
|
|
181
191
|
`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
192
|
|
|
183
193
|
```yaml
|
|
@@ -215,7 +225,7 @@ npm run dev:ui
|
|
|
215
225
|
|
|
216
226
|
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.
|
|
217
227
|
|
|
218
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
228
|
+
The current public version is [`dsh-mcp-connector@0.2.76`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.76](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.76).
|
|
219
229
|
|
|
220
230
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
221
231
|
|
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
73
73
|
- 市场 Bearer/API Key 连接器先执行 MCP initialize 连通性与凭据校验,全部 HTTP Server 通过后才持久化凭据并进入“已安装”;stdio 卡片可声明多个本机凭据字段及其环境变量映射。
|
|
74
74
|
- 生命周期管理:连接持久化、重启恢复、启停、断开、OAuth 自动刷新/退避恢复与撤销;同 issuer 卡片共享一次授权,跨进程锁与独立原子 Grant journal 防止 Desktop/Web 并行时重复消耗 Refresh Token。
|
|
75
75
|
- 配置原地编辑:自定义和 JSON 导入连接可在“已安装”中编辑标准化 JSON 并重新连接;敏感值以本机保留标记处理,校验、启动或保存失败时原连接继续可用。
|
|
76
|
+
- 本地参数覆盖(#146,0.2.76):无凭据市场 stdio 连接支持在首次连接确认框填写“高级本地参数”,以及在已安装列表编辑 `argsAppend`、`env`、`cwd`,不修改目录或连接身份;已有值不回传浏览器,支持快照恢复和清除覆盖。首次连接留空沿用市场参数,已有连接留空保留原覆盖;启动失败保留表单可修改重试。不支持 OAuth、凭据绑定、CLI 桥接或完整参数替换;真实 Desktop/Windows 验收尚未完成。
|
|
76
77
|
- 配置备份:一键复制/下载可再次导入的脱敏 JSON;连接变更前自动保存最多 20 个本机快照,支持预览与原子恢复。凭据、本地路径和 OAuth Grant 不进入导出结果。
|
|
77
78
|
- 连接作用域:新连接可选当前 Workspace 项目或 profile 全局;支持先预览 Server/工具影响,再复制、移动或按 revision 回滚。凭据只存一份,project-only 工具由 DSH Host 强制隔离。
|
|
78
79
|
- 三层治理:Connection、Server、Tool 规则按 Tool > Server > Connection > 默认允许解析;变更先预览、按 revision 提交并可回滚,由 DSH Host 的 schema/lookup/dispatch restriction 与最终执行 Guard 真实生效。
|
|
@@ -86,7 +87,7 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
86
87
|
- 对话工具:`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
88
|
|
|
88
89
|
<!-- catalog-stats:start -->
|
|
89
|
-
截至 2026-10-
|
|
90
|
+
截至 2026-10-17,公共 Registry 已发布 110 条连接器描述;与随包的 4 张企查查卡片合并去重后,市场页可浏览 114 张卡片,覆盖企业数据、金融投资、法律合规、开发工具、办公协作、调研分析、设计创意、效率工具、其他 9 类。推荐位严格保留 4 张企查查卡片、北大法宝和 Wind,共 6 张;其他连接器按业务分类展示。Registry 可独立持续更新,实际数量以客户端刷新后的市场页签徽标和上方实时统计徽标为准。
|
|
90
91
|
<!-- catalog-stats:end -->
|
|
91
92
|
|
|
92
93
|
## 界面与演示
|
|
@@ -162,6 +163,7 @@ Bundle 默认配置位于 `cordis.patch.yml`:
|
|
|
162
163
|
refreshSkewMs: 300000
|
|
163
164
|
openBrowser: true
|
|
164
165
|
showSidebarEntry: true
|
|
166
|
+
compactManagementTools: false
|
|
165
167
|
frameAncestors: ['self']
|
|
166
168
|
```
|
|
167
169
|
|
|
@@ -173,6 +175,14 @@ Bundle 默认配置位于 `cordis.patch.yml`:
|
|
|
173
175
|
|
|
174
176
|
`showSidebarEntry` 默认为 `true`。用户可在 DSH 插件配置页覆盖该值;关闭后只隐藏侧边栏快捷入口,不停用连接器、已连接 MCP Server 或工具。
|
|
175
177
|
|
|
178
|
+
`compactManagementTools` 默认为 `false`,兼容原有对话管理工具。希望降低管理工具上下文开销时,可在插件配置中设为 `true` 并重载插件(必要时重启 DSH),下一次推理使用新工具集:
|
|
179
|
+
|
|
180
|
+
- 只注册 `mcp_connector_discover`(市场、连接状态、缓存工具搜索/详情)和 `mcp_connector_session_tools`(原有会话工具可见性管理)两个入口。
|
|
181
|
+
- 连接、授权、编辑、导入、快照、迁移等管理操作使用 **MCP连接器界面**;精简模式没有任意工具执行代理,也不自动加载全部管理工具。纯对话部署或依赖旧工具名的自动化应保持 `false`。
|
|
182
|
+
- 此开关不减少已连接服务的业务工具数量,不停用连接、不迁移凭据、不修改作用域/治理规则。业务工具需要按需可见时,仍使用原有“按会话启用”。会话确认、TTL 与 Host 审批保持不变。
|
|
183
|
+
- 例如发现入口可用 `{"action":"search","query":"天气","limit":5}`;查询连接状态可用 `{"action":"status"}`。详情使用 `action=detail` 和必填 `toolName`,市场搜索使用 `action=catalog` 与可选 `keyword`。
|
|
184
|
+
- 设回 `false` 并重载插件可恢复旧管理工具名;不要同时运行两份插件。工具数量和 schema 大小的减少不等于实测 token 或费用节省。
|
|
185
|
+
|
|
176
186
|
`frameAncestors` 默认为 `['self']`,即面板页面(`/mcp-connector/ui/`)的 CSP 只允许同源页面内嵌,是既有行为,不做任何放宽。在桌面壳(Tauri/Electron)里把面板嵌进壳窗口时,祖先源与面板自身不同源,浏览器会以 `Framing '...' violates the following Content Security Policy directive: "frame-ancestors 'self'"` 拦截并导致面板白屏;此时需要由部署方/用户显式追加外壳源:
|
|
177
187
|
|
|
178
188
|
```yaml
|
|
@@ -210,7 +220,7 @@ npm run dev:ui
|
|
|
210
220
|
|
|
211
221
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
212
222
|
|
|
213
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
223
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.76`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.76](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.76)。
|
|
214
224
|
|
|
215
225
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
216
226
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/cordis.patch.yml
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
refreshSkewMs: 300000
|
|
12
12
|
openBrowser: true
|
|
13
13
|
showSidebarEntry: true
|
|
14
|
+
# true:仅注册发现与会话两个管理入口;完整连接配置仍在界面操作。
|
|
15
|
+
# 保持 false 兼容已有对话/自动化;更改后重载插件。
|
|
16
|
+
compactManagementTools: false
|
|
14
17
|
# 面板 CSP frame-ancestors 白名单:默认仅同源可嵌入。
|
|
15
18
|
# 桌面壳(Tauri/Electron)内嵌面板时需显式追加外壳源,
|
|
16
19
|
# 例如 ['self', 'http://tauri.localhost', 'tauri://localhost']。
|
package/docs/DESKTOP-E2E.md
CHANGED
|
@@ -13,7 +13,7 @@ npm run dev:ui
|
|
|
13
13
|
|
|
14
14
|
1. 完全退出并重启 DSH Desktop;展开/收起侧栏,入口始终在“新会话”下、“工作区”上且不重复。
|
|
15
15
|
2. 切换工作区、新建会话、重建侧栏后入口仍存在;目标 slot 缺失时 footer 降级可点击。
|
|
16
|
-
3.
|
|
16
|
+
3. 打开市场,验证中文单语言(无 EN 切换按钮)、系统深浅主题、Tab 顺序、Escape 关闭与焦点回到触发按钮。
|
|
17
17
|
4. 点击未连接卡片可查看详情;点击已连接卡片可读实时工具、搜索并每次再加载 50 条。
|
|
18
18
|
5. “添加连接”依次验证手动、JSON、市场卡片;JSON 示例/粘贴可缩进格式化,错误 URL/JSON/凭据得到可行动提示。
|
|
19
19
|
6. Bearer/API Key 型市场卡片先填错误凭据:弹框内应显示鉴权/网络分类错误,不得进入「已安装」;再填正确凭据,只有卡片下所有 Server 均通过 initialize 校验后才批量安装。
|
|
@@ -21,7 +21,26 @@ npm run dev:ui
|
|
|
21
21
|
8. OAuth:授权回调、重启恢复、停用/启用、断开撤销;二级弹框蒙层与关闭按钮清晰。
|
|
22
22
|
9. 旧插件迁移先预览,明确确认后复制;旧凭据仍保留,新连接验证后再手工停用旧插件。
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## 专项验收与人工确认边界
|
|
25
|
+
|
|
26
|
+
### 本地参数覆盖(#146,0.2.76;Desktop 验收待完成)
|
|
27
|
+
|
|
28
|
+
传输层隔离验收可使用本机已安装的 Host 代码,无需修改用户 profile。将下面路径换成实际 DSH 安装目录中的 `package.json`(必须能解析 `@deepseek-ai/dsh-mcp-client` 与 `@deepseek-ai/cordis`):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
DSH_HOST_INTEGRATION_ANCHOR=/absolute/path/to/dsh/package.json node --test test/local-overrides-host.test.mjs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
不设置此变量时该测试跳过;显式指定的 Host 不可解析时应失败。测试使用真实 Cordis 生命周期与 MCP 客户端/stdio 子进程,工具注册表是隔离测试容器,不加载实际 Host profile、存储或凭据。仅启动仓库自有无网络测试服务器,验证初次启动失败、修正 `argsAppend/env/cwd` 后工具发现与无害调用、带启动保护的进程恢复、清除覆盖后恢复原始启动行为及进程回收。临时工作目录包含空格,参数含中文和 shell 特殊字符。
|
|
35
|
+
|
|
36
|
+
该证据不等同于完整 Desktop 验收,仍需在隔离 profile 完成:
|
|
37
|
+
|
|
38
|
+
- 首次连接确认框高级参数输入,失败后表单保留、修正重试;已有连接留空保留覆盖。
|
|
39
|
+
- 已安装列表编辑、敏感值保留标记往返、清除覆盖和快照恢复。
|
|
40
|
+
- 重启后参数实际生效、停用连接不启动;市场改为凭据型后旧覆盖不再自动启动。
|
|
41
|
+
- Windows Desktop 的含空格可执行文件路径、目录与工具调用;Windows CI 不能代替桌面宿主验收。
|
|
42
|
+
|
|
43
|
+
不得将此草案直接安装进现有正常工作 profile,也不得用真实付费工具替代无害调用验收。
|
|
25
44
|
|
|
26
45
|
### 面板浏览状态恢复(#135)
|
|
27
46
|
|
package/lib/connection-backup.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* 先经过本模块转换,避免 Token、Header/env 值与本地路径进入对话或 Web API。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { effectiveLocalRecord } from './local-overrides.js';
|
|
9
|
+
|
|
8
10
|
export const REDACTED_VALUE = '<REDACTED:REENTER>';
|
|
9
11
|
export const REDACTED_LOCAL_VALUE = '<REDACTED:LOCAL_VALUE>';
|
|
10
12
|
export const REDACTED_EXPORT_FORMAT = 'dsh-mcp-connector.redacted';
|
|
@@ -48,6 +50,7 @@ function portableName(record) {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
function exportPortableConnection(record) {
|
|
53
|
+
record = effectiveLocalRecord(record);
|
|
51
54
|
const redactedFields = [];
|
|
52
55
|
const base = {
|
|
53
56
|
name: portableName(record),
|
package/lib/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
editableConnectionConfiguration,
|
|
36
36
|
normalizeConnectionReconfiguration,
|
|
37
37
|
} from './connection-editor.js';
|
|
38
|
+
import { supportsLocalOverrides, editableLocalOverrides, normalizeLocalOverrides } from './local-overrides.js';
|
|
38
39
|
import { discoverServerMetadata, refreshAccessToken, revokeRefreshToken, extractTokenResources } from './oauth.js';
|
|
39
40
|
import { provision, disable, remove, entryIdFor } from './mcp-provision.js';
|
|
40
41
|
import { classifyConnectionError, validateConnectionRecords } from './mcp-validation.js';
|
|
@@ -125,6 +126,8 @@ export const Config = z.object({
|
|
|
125
126
|
account: z.string().default(DEFAULT_ACCOUNT),
|
|
126
127
|
/** 是否在 DSH 主侧边栏显示 MCP连接器快捷入口 */
|
|
127
128
|
showSidebarEntry: z.boolean().default(true),
|
|
129
|
+
/** 精简对话管理工具为发现入口 + 会话入口;旧配置默认保留全量工具。修改后重载插件。 */
|
|
130
|
+
compactManagementTools: z.boolean().default(false),
|
|
128
131
|
/**
|
|
129
132
|
* 图形化市场面板(/mcp-connector/ui/)的 CSP `frame-ancestors` 白名单。
|
|
130
133
|
*
|
|
@@ -690,6 +693,7 @@ export async function apply(ctx, config) {
|
|
|
690
693
|
}
|
|
691
694
|
|
|
692
695
|
async function provisionUntilReady(record, original, { strictStartup = true } = {}) {
|
|
696
|
+
if (record.localOverrides && !canEditLocalOverrides(record)) throw new Error('当前市场配置不再允许本地参数覆盖,请先清除覆盖');
|
|
693
697
|
if (!strictStartup) {
|
|
694
698
|
return provision(ctx, config, record, grantMap(), { failOnStartupError: false });
|
|
695
699
|
}
|
|
@@ -710,7 +714,9 @@ export async function apply(ctx, config) {
|
|
|
710
714
|
if (error === timeoutError) {
|
|
711
715
|
// loader 可能仍在等待 SDK 请求;它最终成功时必须恢复旧条目/移除新条目,绝不迟到落库。
|
|
712
716
|
task.then(
|
|
713
|
-
|
|
717
|
+
// A later disable, disconnect or edit may already own this key.
|
|
718
|
+
// Reconcile after queued mutations, using the record at execution time.
|
|
719
|
+
() => queueConnectionAction(record.key, () => restoreProvisionedEntry(record, state.connections.get(record.key))).catch((rollbackError) => {
|
|
714
720
|
logger.error(`late startup rollback "${record.key}" failed: ${rollbackError.message}`);
|
|
715
721
|
}),
|
|
716
722
|
() => {},
|
|
@@ -735,6 +741,14 @@ export async function apply(ctx, config) {
|
|
|
735
741
|
return failures;
|
|
736
742
|
}
|
|
737
743
|
|
|
744
|
+
function canEditLocalOverrides(record) {
|
|
745
|
+
if (!supportsLocalOverrides(record)) return false;
|
|
746
|
+
const descriptor = state.merged.find((item) => item.id === record.connectorId);
|
|
747
|
+
const server = descriptor?.servers?.find((item) => item.serverKey === record.serverKey);
|
|
748
|
+
return descriptor?.auth?.mode === 'none' && server?.transport === 'stdio'
|
|
749
|
+
&& !server.oauthTokenEnv && !Object.keys(server.credentialBindings ?? {}).length;
|
|
750
|
+
}
|
|
751
|
+
|
|
738
752
|
async function commitConnectionRecords(records, { strictStartup = true } = {}) {
|
|
739
753
|
const originals = new Map(records.map((record) => [record.key, state.connections.get(record.key)]));
|
|
740
754
|
const staged = records.map((record) => {
|
|
@@ -750,9 +764,10 @@ export async function apply(ctx, config) {
|
|
|
750
764
|
provisioned.push(record);
|
|
751
765
|
}
|
|
752
766
|
} catch (error) {
|
|
753
|
-
await rollbackProvisionedRecords(provisioned, originals);
|
|
767
|
+
const rollbackErrors = await rollbackProvisionedRecords(provisioned, originals);
|
|
754
768
|
const wrapped = new Error(`MCP Server "${activeRecord?.serverName ?? activeRecord?.key ?? 'unknown'}" 未完成启动`, { cause: error });
|
|
755
769
|
wrapped.connectionRecord = activeRecord;
|
|
770
|
+
wrapped.rollbackErrors = rollbackErrors;
|
|
756
771
|
throw wrapped;
|
|
757
772
|
}
|
|
758
773
|
|
|
@@ -768,7 +783,7 @@ export async function apply(ctx, config) {
|
|
|
768
783
|
logger.error(`connection storage rollback "${record.key}" failed: ${rollbackError.message}`);
|
|
769
784
|
}
|
|
770
785
|
}
|
|
771
|
-
const wrapped = new Error('连接记录持久化失败,已回滚 Host 条目', { cause: error });
|
|
786
|
+
const wrapped = new Error(rollbackErrors.length ? '连接记录持久化失败,自动回滚未完成' : '连接记录持久化失败,已回滚 Host 条目', { cause: error });
|
|
772
787
|
wrapped.connectionRecord = activeRecord;
|
|
773
788
|
wrapped.rollbackErrors = rollbackErrors;
|
|
774
789
|
throw wrapped;
|
|
@@ -807,7 +822,12 @@ export async function apply(ctx, config) {
|
|
|
807
822
|
const committed = await commitConnectionRecords(records, options);
|
|
808
823
|
return { committed, snapshot };
|
|
809
824
|
} catch (error) {
|
|
810
|
-
|
|
825
|
+
if (error.rollbackErrors?.length) {
|
|
826
|
+
// Do not destroy the last known configuration when recovery itself fails.
|
|
827
|
+
error.recoverySnapshotId = snapshot?.key ?? null;
|
|
828
|
+
} else {
|
|
829
|
+
await discardConnectionSnapshot(snapshot);
|
|
830
|
+
}
|
|
811
831
|
throw error;
|
|
812
832
|
}
|
|
813
833
|
}
|
|
@@ -1949,6 +1969,10 @@ export async function apply(ctx, config) {
|
|
|
1949
1969
|
async connect(connectorId, serverKey, signal, scopeInput = {}) {
|
|
1950
1970
|
const connector = state.merged.find((d) => d.id === connectorId);
|
|
1951
1971
|
if (!connector) return { ok: false, message: `连接器 "${connectorId}" 不存在(可先 mcp_connector_refresh_catalog 或 mcp_connector_catalog 查看)` };
|
|
1972
|
+
const hasLocalOverrides = scopeInput?.localOverrides !== undefined;
|
|
1973
|
+
if (hasLocalOverrides && connector.auth.mode !== 'none') {
|
|
1974
|
+
return { ok: false, message: '本地参数仅支持无凭据的市场 stdio 连接,不可覆盖授权配置' };
|
|
1975
|
+
}
|
|
1952
1976
|
let requestedScope;
|
|
1953
1977
|
const explicitScope = scopeInput?.scope !== undefined;
|
|
1954
1978
|
try {
|
|
@@ -2099,6 +2123,15 @@ export async function apply(ctx, config) {
|
|
|
2099
2123
|
createdAt: Date.now(),
|
|
2100
2124
|
updatedAt: Date.now(),
|
|
2101
2125
|
};
|
|
2126
|
+
if (hasLocalOverrides) {
|
|
2127
|
+
if (!canEditLocalOverrides(record)) return { ok: false, message: '当前连接不支持本地参数覆盖' };
|
|
2128
|
+
try {
|
|
2129
|
+
const existing = state.connections.get(record.key);
|
|
2130
|
+
record.localOverrides = normalizeLocalOverrides({ ...record, localOverrides: existing?.localOverrides }, scopeInput.localOverrides);
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
return { ok: false, message: error.message };
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2102
2135
|
const validation = await validateConnectionRecords([record], { timeoutMs: config.requestTimeoutMs });
|
|
2103
2136
|
if (!validation.ok) return { ...validation, detail: { connectorId: connector.id, ...validation.detail } };
|
|
2104
2137
|
try {
|
|
@@ -2106,6 +2139,7 @@ export async function apply(ctx, config) {
|
|
|
2106
2139
|
cacheHealth(connector.id, [committed], readinessResults([committed], validation.detail.results));
|
|
2107
2140
|
return { ok: true, message: `已连接 "${connector.name}"(${scopeLabel(connectionScopes.binding(committed.key), workspaceNames())})`, detail: { key: committed.key, snapshotId: snapshot?.key ?? null, scopeRevision, scopeRollbackRevision } };
|
|
2108
2141
|
} catch (error) {
|
|
2142
|
+
if (hasLocalOverrides) return { ok: false, message: '连接启动或保存失败,已尝试恢复原配置;请检查本机 Host 日志与连接状态后重试' };
|
|
2109
2143
|
return connectionSetupFailure(error);
|
|
2110
2144
|
}
|
|
2111
2145
|
}
|
|
@@ -2368,6 +2402,8 @@ export async function apply(ctx, config) {
|
|
|
2368
2402
|
kind: record.kind,
|
|
2369
2403
|
canRename: connectionDisplayNameEditable(record),
|
|
2370
2404
|
canEditConfiguration: connectionConfigurationEditable(record),
|
|
2405
|
+
canEditLocalOverrides: canEditLocalOverrides(record),
|
|
2406
|
+
hasLocalOverrides: Boolean(record.localOverrides),
|
|
2371
2407
|
serverName: record.serverName,
|
|
2372
2408
|
url: record.url,
|
|
2373
2409
|
transport: record.transport,
|
|
@@ -2610,6 +2646,37 @@ export async function apply(ctx, config) {
|
|
|
2610
2646
|
}
|
|
2611
2647
|
},
|
|
2612
2648
|
|
|
2649
|
+
async editableLocalOverrides(key) {
|
|
2650
|
+
const record = state.connections.get(key);
|
|
2651
|
+
if (!record || !canEditLocalOverrides(record)) return { ok: false, message: '仅无凭据的市场 stdio 连接支持本地参数覆盖' };
|
|
2652
|
+
return { ok: true, detail: { json: editableLocalOverrides(record) } };
|
|
2653
|
+
},
|
|
2654
|
+
|
|
2655
|
+
async applyLocalOverrides(key, json, reset = false) {
|
|
2656
|
+
const current = state.connections.get(key);
|
|
2657
|
+
if (!current || (!canEditLocalOverrides(current) && !(reset === true && current.localOverrides))) {
|
|
2658
|
+
return { ok: false, message: '当前连接不支持本地参数覆盖' };
|
|
2659
|
+
}
|
|
2660
|
+
let localOverrides;
|
|
2661
|
+
try { localOverrides = reset === true ? undefined : normalizeLocalOverrides(current, json); }
|
|
2662
|
+
catch (error) { return { ok: false, message: error.message }; }
|
|
2663
|
+
try {
|
|
2664
|
+
const next = { ...current, localOverrides, updatedAt: Date.now() };
|
|
2665
|
+
const { snapshot } = await commitWithSnapshot([next], 'local-overrides');
|
|
2666
|
+
return { ok: true, message: reset === true ? '已清除本地覆盖,恢复市场参数' : '已保存本地参数覆盖;启用的连接已重新加载', detail: { key, snapshotId: snapshot?.key ?? null } };
|
|
2667
|
+
} catch (error) {
|
|
2668
|
+
// Startup/storage errors may contain command arguments or env values.
|
|
2669
|
+
if (error.rollbackErrors?.length) return {
|
|
2670
|
+
ok: false,
|
|
2671
|
+
message: error.recoverySnapshotId
|
|
2672
|
+
? '本地参数更新失败且自动回滚未完成,已保留操作前快照;请修复本机存储或启动问题后,从配置快照恢复并核对连接状态'
|
|
2673
|
+
: '本地参数更新失败且自动回滚未完成;请检查本机存储、Host 日志与连接状态',
|
|
2674
|
+
detail: { recoveryRequired: true, snapshotId: error.recoverySnapshotId ?? null },
|
|
2675
|
+
};
|
|
2676
|
+
return { ok: false, message: '本地参数更新失败,已尝试恢复原配置;请检查本机 Host 日志与连接状态' };
|
|
2677
|
+
}
|
|
2678
|
+
},
|
|
2679
|
+
|
|
2613
2680
|
async reconfigureConnection(key, json) {
|
|
2614
2681
|
const current = state.connections.get(key);
|
|
2615
2682
|
if (!current) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
@@ -3338,6 +3405,10 @@ export async function apply(ctx, config) {
|
|
|
3338
3405
|
}
|
|
3339
3406
|
for (const [key, record] of state.connections) {
|
|
3340
3407
|
if (record.enabled !== false) {
|
|
3408
|
+
if (record.localOverrides && !canEditLocalOverrides(record)) {
|
|
3409
|
+
logger.warn(`skip provision "${key}" at startup: 当前市场配置不再允许本地参数覆盖,请先清除覆盖`);
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3341
3412
|
const failure = grantFailure(record);
|
|
3342
3413
|
if (failure) {
|
|
3343
3414
|
logger.warn(`skip provision "${key}" at startup: ${failure.message}`);
|
|
@@ -3414,19 +3485,32 @@ export async function apply(ctx, config) {
|
|
|
3414
3485
|
}
|
|
3415
3486
|
// Serialize same-record actions: a delayed reconnect must never reactivate a
|
|
3416
3487
|
// connection the user has subsequently disabled or disconnected.
|
|
3417
|
-
const connectionActions = new Set(['reconnect', 'setEnabled', 'disconnect', 'reconfigureConnection', 'renameConnection', 'setInjectionMode']);
|
|
3488
|
+
const connectionActions = new Set(['reconnect', 'setEnabled', 'disconnect', 'reconfigureConnection', 'applyLocalOverrides', 'renameConnection', 'setInjectionMode']);
|
|
3418
3489
|
const connectionQueues = new Map();
|
|
3490
|
+
async function queueConnectionAction(key, action) {
|
|
3491
|
+
const previous = connectionQueues.get(key) ?? Promise.resolve();
|
|
3492
|
+
const pending = previous.catch(() => {}).then(action);
|
|
3493
|
+
connectionQueues.set(key, pending);
|
|
3494
|
+
try { return await pending; }
|
|
3495
|
+
finally { if (connectionQueues.get(key) === pending) connectionQueues.delete(key); }
|
|
3496
|
+
}
|
|
3419
3497
|
for (const name of connectionActions) {
|
|
3420
3498
|
const action = api[name];
|
|
3421
|
-
api[name] =
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3499
|
+
api[name] = (key, ...args) => queueConnectionAction(key, () => action.call(api, key, ...args));
|
|
3500
|
+
}
|
|
3501
|
+
// First-time stdio installation must share the edit/disable/disconnect queue.
|
|
3502
|
+
// Otherwise a delayed install can overwrite a newer overlay or resurrect a removed entry.
|
|
3503
|
+
// OAuth retains its independent shared-authorization deduplication path.
|
|
3504
|
+
const connectAction = api.connect;
|
|
3505
|
+
api.connect = (connectorId, serverKey, ...args) => {
|
|
3506
|
+
const connector = state.merged.find((item) => item.id === connectorId);
|
|
3507
|
+
const server = connector?.servers.find((item) => item.serverKey === serverKey) ?? connector?.servers[0];
|
|
3508
|
+
const run = () => connectAction.call(api, connectorId, serverKey, ...args);
|
|
3509
|
+
return connector?.auth.mode === 'none' && server?.transport === 'stdio'
|
|
3510
|
+
? queueConnectionAction(`${connector.id}-${server.serverKey}`, run)
|
|
3511
|
+
: run();
|
|
3512
|
+
};
|
|
3513
|
+
const discoveryMutations = new Set(['connect', 'configure', 'importJson', 'installFromUrl', 'reconfigureConnection', 'applyLocalOverrides', 'setEnabled', 'reconnect', 'restoreSnapshot', 'migrateLegacy']);
|
|
3430
3514
|
const discoveryApi = new Proxy(api, { get(target, property) {
|
|
3431
3515
|
const value = target[property];
|
|
3432
3516
|
if (!discoveryMutations.has(property)) return value;
|
|
@@ -3442,7 +3526,7 @@ export async function apply(ctx, config) {
|
|
|
3442
3526
|
if (statusEvents.clients.size > 0) scheduleDiscovery();
|
|
3443
3527
|
}, 30_000);
|
|
3444
3528
|
discoveryInterval.unref?.();
|
|
3445
|
-
const disposeTools = registerTools(ctx, observableApi);
|
|
3529
|
+
const disposeTools = registerTools(ctx, observableApi, { compact: config.compactManagementTools === true });
|
|
3446
3530
|
|
|
3447
3531
|
/* ───────────────────────── web 半区(图形化市场)──────────────────────── */
|
|
3448
3532
|
// webServer + 新版 webStartup 或旧版 webRuntime;纯对话部署不挂载 HTTP 路由。
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Local-only overlay for credential-free catalog stdio entries. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { CUSTOM_CONNECTOR_ID, JSON_CONNECTOR_ID } from './constants.js';
|
|
4
|
+
// Do not import connection-editor: it depends on schema via the JSON parser.
|
|
5
|
+
const KEEP_EXISTING_VALUE = '<KEEP_EXISTING>';
|
|
6
|
+
const forbiddenEnv = new Set(['__proto__', 'constructor', 'prototype', 'ELECTRON_RUN_AS_NODE']);
|
|
7
|
+
|
|
8
|
+
export const localOverridesSchema = z.object({
|
|
9
|
+
argsAppend: z.array(z.string().max(8192)).max(128).optional(),
|
|
10
|
+
env: z.custom((value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
11
|
+
&& !Object.keys(value).some((key) => forbiddenEnv.has(key)))
|
|
12
|
+
.pipe(z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string().max(8192))).optional(),
|
|
13
|
+
cwd: z.string().max(8192).optional(),
|
|
14
|
+
}).strict();
|
|
15
|
+
|
|
16
|
+
export function supportsLocalOverrides(record) {
|
|
17
|
+
return record?.transport === 'stdio' && ![CUSTOM_CONNECTOR_ID, JSON_CONNECTOR_ID, 'dingtalk'].includes(record.connectorId)
|
|
18
|
+
&& Boolean(record.connectorId) && !record.auth && !record.oauthTokenEnv;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function editableLocalOverrides(record) {
|
|
22
|
+
if (!supportsLocalOverrides(record)) throw new Error('当前仅支持无凭据的市场 stdio 连接;OAuth/凭据型/CLI 桥接连接不可覆盖');
|
|
23
|
+
const values = record.localOverrides ?? {};
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
argsAppend: values.argsAppend?.length ? [KEEP_EXISTING_VALUE] : [],
|
|
26
|
+
env: Object.fromEntries(Object.keys(values.env ?? {}).map((key) => [key, KEEP_EXISTING_VALUE])),
|
|
27
|
+
cwd: values.cwd ? KEEP_EXISTING_VALUE : '',
|
|
28
|
+
}, null, 2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeLocalOverrides(record, input) {
|
|
32
|
+
if (!supportsLocalOverrides(record)) throw new Error('当前连接不支持本地参数覆盖');
|
|
33
|
+
let raw;
|
|
34
|
+
try { raw = typeof input === 'string' ? JSON.parse(input) : input; }
|
|
35
|
+
catch { throw new Error('本地参数必须是合法 JSON'); }
|
|
36
|
+
const parsed = localOverridesSchema.safeParse(raw);
|
|
37
|
+
if (!parsed.success) throw new Error('本地参数仅允许 argsAppend 字符串数组、env 字符串对象及 cwd 字符串');
|
|
38
|
+
const value = parsed.data;
|
|
39
|
+
const previous = record.localOverrides ?? {};
|
|
40
|
+
if (value.argsAppend?.length === 1 && value.argsAppend[0] === KEEP_EXISTING_VALUE) value.argsAppend = [...(previous.argsAppend ?? [])];
|
|
41
|
+
if (value.cwd === KEEP_EXISTING_VALUE) value.cwd = previous.cwd ?? '';
|
|
42
|
+
for (const [key, entry] of Object.entries(value.env ?? {})) {
|
|
43
|
+
if (['__proto__', 'constructor', 'prototype', 'ELECTRON_RUN_AS_NODE'].includes(key)) throw new Error('包含不允许覆盖的环境变量名');
|
|
44
|
+
if (entry === KEEP_EXISTING_VALUE) {
|
|
45
|
+
if (!Object.hasOwn(previous.env ?? {}, key)) throw new Error('环境变量不存在可保留的原值');
|
|
46
|
+
value.env[key] = previous.env[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (value.argsAppend?.includes(KEEP_EXISTING_VALUE)) throw new Error('argsAppend 保留标记必须单独使用');
|
|
50
|
+
if ([...(value.argsAppend ?? []), ...Object.values(value.env ?? {}), value.cwd ?? ''].some((entry) => entry.includes('\0'))) throw new Error('本地参数不可包含空字符');
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function effectiveLocalRecord(record) {
|
|
55
|
+
if (!record.localOverrides) return record;
|
|
56
|
+
const overlay = normalizeLocalOverrides(record, record.localOverrides);
|
|
57
|
+
return {
|
|
58
|
+
...record,
|
|
59
|
+
args: [...(record.args ?? []), ...(overlay.argsAppend ?? [])],
|
|
60
|
+
env: { ...(record.env ?? {}), ...(overlay.env ?? {}) },
|
|
61
|
+
cwd: overlay.cwd || record.cwd,
|
|
62
|
+
};
|
|
63
|
+
}
|
package/lib/mcp-provision.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { cliBridgeArgs } from './cli-providers.js';
|
|
7
|
+
import { effectiveLocalRecord } from './local-overrides.js';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
9
10
|
const STDIO_STARTUP_GUARD = fileURLToPath(new URL('./stdio-startup-guard.js', import.meta.url));
|
|
@@ -39,6 +40,7 @@ export function authHeaders(record, grants) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export function buildEntryConfig(record, grants, { failOnStartupError = false, startupRequestTimeoutMs } = {}) {
|
|
43
|
+
record = effectiveLocalRecord(record);
|
|
42
44
|
if (record.transport === 'stdio') {
|
|
43
45
|
const env = { ...(record.env ?? {}) };
|
|
44
46
|
if (record.auth?.mode === 'oauth' && record.oauthTokenEnv) {
|
package/lib/schema.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { assertConnectionUrl, isPrivateNetworkHost } from './util.js';
|
|
7
|
+
import { localOverridesSchema, normalizeLocalOverrides } from './local-overrides.js';
|
|
7
8
|
|
|
8
9
|
/** 连接器目录里单个 MCP server 的描述 */
|
|
9
10
|
export const serverRefSchema = z.object({
|
|
@@ -101,6 +102,7 @@ export const connectorDescriptorSchema = z.object({
|
|
|
101
102
|
|
|
102
103
|
/** 用户本机连接实例 */
|
|
103
104
|
export const connectionRecordSchema = z.object({
|
|
105
|
+
localOverrides: localOverridesSchema.optional(),
|
|
104
106
|
key: z.string().min(1),
|
|
105
107
|
connectorId: z.string().min(1),
|
|
106
108
|
kind: z.enum(['oauth', 'manual', 'json']),
|
|
@@ -345,6 +347,7 @@ export function normalizeConnectionRecord(raw) {
|
|
|
345
347
|
throw new Error(`invalid connection record: ${issues}`);
|
|
346
348
|
}
|
|
347
349
|
const data = parsed.data;
|
|
350
|
+
if (data.localOverrides) data.localOverrides = normalizeLocalOverrides(data, data.localOverrides);
|
|
348
351
|
const transport = data.transport === 'sse' ? 'streamable-http' : data.transport;
|
|
349
352
|
if (transport === 'stdio' && !data.command) {
|
|
350
353
|
throw new Error('invalid connection record: transport=stdio 时 command 必填');
|
package/lib/status-events.js
CHANGED
|
@@ -5,6 +5,7 @@ const DEFAULT_EVENT_TYPES = new Map([
|
|
|
5
5
|
['installFromUrl', 'connections'],
|
|
6
6
|
['renameConnection', 'connections'],
|
|
7
7
|
['reconfigureConnection', 'connections'],
|
|
8
|
+
['applyLocalOverrides', 'connections'],
|
|
8
9
|
['setEnabled', 'connections'],
|
|
9
10
|
['reconnect', 'connections'],
|
|
10
11
|
['setInjectionMode', 'connections'],
|
|
@@ -84,7 +84,7 @@ export function connectionToolCacheSignature(record) {
|
|
|
84
84
|
const identity = record?.transport === 'stdio'
|
|
85
85
|
? [record.transport, record.serverName, record.command, record.args ?? [], record.cwd ?? '']
|
|
86
86
|
: [record?.transport, record?.serverName, record?.url ?? ''];
|
|
87
|
-
return createHash('sha256').update(JSON.stringify(identity)).digest('hex');
|
|
87
|
+
return createHash('sha256').update(JSON.stringify(record?.localOverrides ? [identity, record.localOverrides] : identity)).digest('hex');
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function createToolCatalogRecord(record, tools, now = Date.now()) {
|
package/lib/tools.js
CHANGED
|
@@ -169,14 +169,14 @@ export function toLossless(value, seen = new Set()) {
|
|
|
169
169
|
return output;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
export function registerTools(ctx, api) {
|
|
173
|
-
const
|
|
174
|
-
const reg = (def) =>
|
|
172
|
+
export function registerTools(ctx, api, { compact = false } = {}) {
|
|
173
|
+
const definitions = [];
|
|
174
|
+
const reg = (def) => definitions.push({
|
|
175
175
|
...def,
|
|
176
176
|
async execute(args, exec) {
|
|
177
177
|
return toLossless(await def.execute(args, exec));
|
|
178
178
|
},
|
|
179
|
-
})
|
|
179
|
+
});
|
|
180
180
|
|
|
181
181
|
reg({
|
|
182
182
|
name: 'mcp_connector_catalog',
|
|
@@ -758,7 +758,76 @@ export function registerTools(ctx, api) {
|
|
|
758
758
|
},
|
|
759
759
|
});
|
|
760
760
|
|
|
761
|
+
// Keep the original definitions intact: no generic executor, direct API
|
|
762
|
+
// bypass, or change to Host dispatch/approval for downstream MCP tools.
|
|
763
|
+
const selected = compact === true
|
|
764
|
+
? [createDiscoveryTool(definitions), definitions.find((def) => def.name === 'mcp_connector_session_tools')]
|
|
765
|
+
: definitions;
|
|
766
|
+
const disposers = [];
|
|
767
|
+
try {
|
|
768
|
+
for (const def of selected) disposers.push(ctx.tools.register(def));
|
|
769
|
+
} catch (error) {
|
|
770
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
761
773
|
return () => {
|
|
762
774
|
for (const dispose of disposers) dispose();
|
|
763
775
|
};
|
|
764
776
|
}
|
|
777
|
+
|
|
778
|
+
/** Fixed read-only routes, never an arbitrary tool name or API method. */
|
|
779
|
+
function createDiscoveryTool(definitions) {
|
|
780
|
+
const routes = Object.fromEntries([
|
|
781
|
+
['catalog', 'mcp_connector_catalog'],
|
|
782
|
+
['status', 'mcp_connector_status'],
|
|
783
|
+
['search', 'mcp_connector_tool_search'],
|
|
784
|
+
['detail', 'mcp_connector_tool_detail'],
|
|
785
|
+
].map(([action, name]) => [action, definitions.find((def) => def.name === name)]));
|
|
786
|
+
const failure = (message) => ({ ok: false, message });
|
|
787
|
+
return {
|
|
788
|
+
name: 'mcp_connector_discover',
|
|
789
|
+
description: 'MCP连接器只读发现:catalog 查市场,status 查连接状态,search 搜索已连接服务的缓存工具,detail 查缓存参数。' +
|
|
790
|
+
'不会连接、配置或执行业务工具。连接/授权/编辑/迁移请使用「MCP连接器」界面;' +
|
|
791
|
+
'按会话启用的业务工具使用 mcp_connector_session_tools。其他业务调用仍走 Host 原生工具及审批。',
|
|
792
|
+
parameters: {
|
|
793
|
+
type: 'object',
|
|
794
|
+
properties: {
|
|
795
|
+
action: { type: 'string', enum: Object.keys(routes), description: 'catalog 使用 keyword/category;status 可选 workspaceId;search 必填 query;detail 必填 toolName。仅传当前操作支持的参数。' },
|
|
796
|
+
...Object.assign({}, ...Object.values(routes).map((def) => def.parameters.properties)),
|
|
797
|
+
},
|
|
798
|
+
required: ['action'],
|
|
799
|
+
additionalProperties: false,
|
|
800
|
+
},
|
|
801
|
+
output: {
|
|
802
|
+
schema: RESULT_SCHEMA,
|
|
803
|
+
render(args, value) {
|
|
804
|
+
const route = Object.hasOwn(routes, value.discoveryAction) ? routes[value.discoveryAction] : null;
|
|
805
|
+
return route ? route.output.render(args, value) : resultOutput.render(args, value);
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
async execute(input) {
|
|
809
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) ||
|
|
810
|
+
typeof input.action !== 'string' || !Object.hasOwn(routes, input.action)) {
|
|
811
|
+
return failure('action 必须是 catalog、status、search 或 detail');
|
|
812
|
+
}
|
|
813
|
+
const { action, ...args } = input;
|
|
814
|
+
const route = routes[action];
|
|
815
|
+
// The union-shaped public object cannot express action-specific required
|
|
816
|
+
// fields without provider-incompatible top-level unions. Check them here
|
|
817
|
+
// before invoking any route; do not reflect arbitrary input or secrets.
|
|
818
|
+
for (const key of route.parameters.required ?? []) {
|
|
819
|
+
if (typeof args[key] !== 'string' || !args[key].trim()) return failure(`${action} 缺少必填参数 ${key}`);
|
|
820
|
+
}
|
|
821
|
+
for (const [key, value] of Object.entries(args)) {
|
|
822
|
+
if (!Object.hasOwn(route.parameters.properties, key)) return failure('包含当前 action 不支持的参数');
|
|
823
|
+
const schema = route.parameters.properties[key];
|
|
824
|
+
if (typeof value !== schema.type || (schema.type === 'number' &&
|
|
825
|
+
(!Number.isFinite(value) || value < schema.minimum || value > schema.maximum))) {
|
|
826
|
+
return failure(`${action} 参数 ${key} 类型或范围不正确`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const result = await route.execute(args);
|
|
830
|
+
return toLossless({ ...result, discoveryAction: action });
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -52,6 +52,8 @@ const API_WHITELIST = new Map([
|
|
|
52
52
|
['renameConnection', (api, p) => api.renameConnection(p?.key, p?.name)],
|
|
53
53
|
['editableConnectionConfig', (api, p) => api.editableConnectionConfig(p?.key)],
|
|
54
54
|
['reconfigureConnection', (api, p) => api.reconfigureConnection(p?.key, p?.json)],
|
|
55
|
+
['editableLocalOverrides', (api, p) => api.editableLocalOverrides(p?.key)],
|
|
56
|
+
['applyLocalOverrides', (api, p) => api.applyLocalOverrides(p?.key, p?.json, p?.reset)],
|
|
55
57
|
['disconnect', (api, p) => api.disconnect(p?.key)],
|
|
56
58
|
['setEnabled', (api, p) => api.setEnabled(p?.key, p?.enabled !== false)],
|
|
57
59
|
['reconnect', (api, p) => api.reconnect(p?.key)],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.76",
|
|
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/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",
|
|
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 && node --check scripts/check-tutorial-links.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",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"marketing:check:live": "node scripts/check-marketing-metadata.mjs --live",
|
|
62
62
|
"storefront:check": "node scripts/check-storefront-screenshots.mjs",
|
|
63
63
|
"verify-pack": "node scripts/verify-pack.mjs",
|
|
64
|
-
"
|
|
64
|
+
"docs:tutorial-links:check": "node scripts/check-tutorial-links.mjs",
|
|
65
|
+
"check": "npm run lint && npm run docs:check && npm run docs:first-use:check && npm run marketing:check && npm run storefront:check && npm test && npm run verify-pack && npm run docs:tutorial-links:check",
|
|
65
66
|
"prepublishOnly": "npm run check"
|
|
66
67
|
},
|
|
67
68
|
"keywords": [
|
package/ui/index.html
CHANGED
|
@@ -543,6 +543,7 @@
|
|
|
543
543
|
configure: 145_000,
|
|
544
544
|
importJson: 145_000,
|
|
545
545
|
reconfigureConnection: 145_000,
|
|
546
|
+
applyLocalOverrides: 145_000,
|
|
546
547
|
installFromUrl: 145_000,
|
|
547
548
|
setEnabled: 130_000,
|
|
548
549
|
setInjectionMode: 30_000,
|
|
@@ -1572,6 +1573,7 @@
|
|
|
1572
1573
|
<span class="slider"></span>
|
|
1573
1574
|
</label>
|
|
1574
1575
|
${canEditConfiguration ? `<button class="btn ghost" type="button" onclick="window.__mcp.openEditor('${key}')">编辑配置</button>` : ''}
|
|
1576
|
+
${r.canEditLocalOverrides ? `<button class="btn ghost" type="button" onclick="window.__mcp.openLocalEditor('${key}')">本地参数</button>` : r.hasLocalOverrides ? `<button class="btn ghost" type="button" onclick="window.__mcp.submitLocalOverrides('${key}', true)">清除本地参数</button>` : ''}
|
|
1575
1577
|
${canRename ? `<button class="btn ghost" type="button" onclick="window.__mcp.openRename('${key}')">重命名</button>` : ''}
|
|
1576
1578
|
${enabled ? `<button class="btn ghost" type="button" onclick="window.__mcp.showConnectionTools('${key}')">工具</button>` : ''}
|
|
1577
1579
|
${enabled && r.connectionState !== 'healthy' ? `<button class="btn ghost" type="button" title="重新建立 Host 侧条目并复检:不改配置、凭据、范围或注入模式" onclick="window.__mcp.reconnect('${key}')">重新连接</button>` : ''}
|
|
@@ -1669,11 +1671,15 @@
|
|
|
1669
1671
|
function openConnectScope(connectorId) {
|
|
1670
1672
|
const connector = marketItems[connectorId];
|
|
1671
1673
|
if (!connector) return;
|
|
1674
|
+
const server = connector.servers?.[0];
|
|
1675
|
+
const allowLocalParameters = connector.authMode === 'none' && connectorId !== 'dingtalk'
|
|
1676
|
+
&& server?.transport === 'stdio' && !server.oauthTokenEnv && !Object.keys(server.credentialBindings || {}).length;
|
|
1672
1677
|
openModal(`连接 ${connector.name}`, `
|
|
1673
1678
|
<input id="scope-connector-id" type="hidden" value="${esc(connectorId)}" />
|
|
1674
1679
|
<div class="form-note">${connectionLabels(connector).map(esc).join(' · ')}${connector.credentialDescription ? `<p>${esc(connector.credentialDescription)}</p>` : ''}${connectorId === 'dingtalk' ? '<p>企业尚未开放 CLI 或您无法取得授权?可以取消,等待管理员处理,无需反复登录或重启。一键授权也不能绕过企业政策。本版本不会自动发起登录;工具注册不代表已获得全部业务权限。</p>' : ''}</div>
|
|
1675
1680
|
<div class="form-note"><strong>选择连接影响范围</strong>连接配置与凭据仍只保存一份;项目范围会由 Host 在工具可见性和执行层强制隔离。</div>
|
|
1676
1681
|
${scopeFieldHtml()}
|
|
1682
|
+
${allowLocalParameters ? '<details><summary>高级:首次连接前设置本地参数(可选)</summary><label for="initial-local-overrides">本地参数 JSON</label><textarea id="initial-local-overrides" rows="6" spellcheck="false" placeholder=\'{"argsAppend":[],"env":{},"cwd":""}\'></textarea><div class="field-help">留空使用市场参数(已有连接保留原覆盖)。argsAppend 追加命令参数,env 合并环境变量,cwd 设置工作目录;仅保存在本机。可用于指定浏览器路径或代理。参数可能扩大本地访问权限,请仅填写可信内容;启动失败时保留表单供修改重试。</div></details>' : ''}
|
|
1677
1683
|
<div class="inline-error" id="modal-error" tabindex="-1"></div>
|
|
1678
1684
|
<div class="m-actions"><button class="btn ghost" type="button" onclick="window.__mcp.closeModal()">取消</button><button class="btn" type="button" onclick="window.__mcp.confirmConnect()">确认连接</button></div>`);
|
|
1679
1685
|
}
|
|
@@ -1720,6 +1726,20 @@
|
|
|
1720
1726
|
<div class="m-actions"><button class="btn ghost" type="button" onclick="window.__mcp.closeModal()">取消</button><button class="btn" type="button" onclick="window.__mcp.submitReconfigure('${esc(key)}')">${action}</button></div>`);
|
|
1721
1727
|
}
|
|
1722
1728
|
|
|
1729
|
+
async function openLocalEditor(key) {
|
|
1730
|
+
const item = installedItems.find((candidate) => candidate.key === key);
|
|
1731
|
+
if (!item) return;
|
|
1732
|
+
const result = await call('editableLocalOverrides', { key });
|
|
1733
|
+
if (!result.ok) { showToast(result.message, false); return; }
|
|
1734
|
+
openModal(`本地参数 · ${item.name}`, `
|
|
1735
|
+
<div class="form-note"><strong>仅覆盖本机,无需修改市场目录</strong><code>argsAppend</code> 追加到市场参数末尾;<code>env</code> 合并环境变量;<code>cwd</code> 设置工作目录。命令、连接身份与市场默认值保持不变。参数可能扩大本地访问权限,请仅填写可信内容。</div>
|
|
1736
|
+
<label for="local-overrides-json">本地参数 JSON</label>
|
|
1737
|
+
<textarea id="local-overrides-json" rows="12" spellcheck="false">${esc(result.detail?.json || '')}</textarea>
|
|
1738
|
+
<div class="field-help"><code><KEEP_EXISTING></code> 保留本机已有值;删除字段清除该项覆盖。保存会创建快照并重新加载已启用连接,停用连接保持停用;不代表工具调用已验收。</div>
|
|
1739
|
+
<div class="inline-error" id="modal-error" tabindex="-1"></div>
|
|
1740
|
+
<div class="m-actions"><button class="btn ghost" type="button" onclick="window.__mcp.closeModal()">取消</button><button class="btn ghost" type="button" onclick="window.__mcp.submitLocalOverrides('${esc(key)}', true)">恢复市场参数</button><button class="btn" type="button" onclick="window.__mcp.submitLocalOverrides('${esc(key)}')">保存本地参数</button></div>`);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1723
1743
|
function openAddConnection(mode = 'json', preset = {}) {
|
|
1724
1744
|
const server = (preset.servers || [{}])[0] || {};
|
|
1725
1745
|
if (mode === 'manual' && preset.id && ['bearer', 'api-key'].includes(preset.authMode)) {
|
|
@@ -1831,8 +1851,9 @@
|
|
|
1831
1851
|
try { scopeParams = selectedScopeParams(); }
|
|
1832
1852
|
catch (error) { showModalError(error.message); return; }
|
|
1833
1853
|
const timeoutMs = marketItems[connectorId]?.authMode === 'oauth2-pkce' ? 310_000 : undefined;
|
|
1854
|
+
const localOverrides = $('#initial-local-overrides')?.value.trim();
|
|
1834
1855
|
setModalBusy(true, '正在连接…');
|
|
1835
|
-
const r = await call('connect', { connectorId, ...scopeParams }, { timeoutMs });
|
|
1856
|
+
const r = await call('connect', { connectorId, ...scopeParams, ...(localOverrides ? { localOverrides } : {}) }, { timeoutMs });
|
|
1836
1857
|
setModalBusy(false);
|
|
1837
1858
|
showToast(r.message, r.ok);
|
|
1838
1859
|
if (!r.ok) showModalError(r.message);
|
|
@@ -1850,6 +1871,18 @@
|
|
|
1850
1871
|
openScope(key) { openScopeManager(key); },
|
|
1851
1872
|
openRename(key) { openRenameConnection(key); },
|
|
1852
1873
|
async openEditor(key) { await openConnectionEditor(key); },
|
|
1874
|
+
async openLocalEditor(key) { await openLocalEditor(key); },
|
|
1875
|
+
async submitLocalOverrides(key, reset = false) {
|
|
1876
|
+
if (reset && !await confirmDialog({ title: '恢复市场参数', message: '清除本机追加参数、环境变量和工作目录覆盖;已启用连接将重新加载。', confirmLabel: '恢复' })) return;
|
|
1877
|
+
const json = $('#local-overrides-json')?.value || '{}';
|
|
1878
|
+
setModalBusy(true, '正在更新本地参数…');
|
|
1879
|
+
const result = await call('applyLocalOverrides', { key, json, reset });
|
|
1880
|
+
setModalBusy(false);
|
|
1881
|
+
if (!result.ok) { showToast(result.message, false); showModalError(result.message); return; }
|
|
1882
|
+
closeModal();
|
|
1883
|
+
showToast(result.message, true);
|
|
1884
|
+
await loadInstalled();
|
|
1885
|
+
},
|
|
1853
1886
|
formatEditJson() {
|
|
1854
1887
|
const input = $('#edit-connection-json');
|
|
1855
1888
|
if (!input) return;
|
|
@@ -2173,6 +2206,7 @@
|
|
|
2173
2206
|
await window.__mcp.showDetail(record.connectorId, {
|
|
2174
2207
|
id: record.connectorId, connectionKey: key, name: record.name, icon: '🔌',
|
|
2175
2208
|
authMode: record.authMode, connected: [record], connectionState: record.connectionState,
|
|
2209
|
+
servers: [{ transport: record.transport }],
|
|
2176
2210
|
description: '本机连接的工具发现与参数详情;不会执行工具。',
|
|
2177
2211
|
});
|
|
2178
2212
|
},
|