dsh-mcp-connector 0.2.75 → 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 +15 -0
- package/README.en.md +2 -2
- package/README.md +3 -2
- package/docs/DESKTOP-E2E.md +21 -2
- package/lib/connection-backup.js +3 -0
- package/lib/index.js +96 -14
- 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/web.js +2 -0
- package/package.json +1 -1
- package/ui/index.html +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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
|
+
|
|
5
20
|
## [0.2.75] - 2026-10-17
|
|
6
21
|
|
|
7
22
|
### 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
|
|
@@ -225,7 +225,7 @@ npm run dev:ui
|
|
|
225
225
|
|
|
226
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.
|
|
227
227
|
|
|
228
|
-
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).
|
|
229
229
|
|
|
230
230
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
231
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
|
## 界面与演示
|
|
@@ -219,7 +220,7 @@ npm run dev:ui
|
|
|
219
220
|
|
|
220
221
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
221
222
|
|
|
222
|
-
当前公开版本为 [`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)。
|
|
223
224
|
|
|
224
225
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
225
226
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
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';
|
|
@@ -692,6 +693,7 @@ export async function apply(ctx, config) {
|
|
|
692
693
|
}
|
|
693
694
|
|
|
694
695
|
async function provisionUntilReady(record, original, { strictStartup = true } = {}) {
|
|
696
|
+
if (record.localOverrides && !canEditLocalOverrides(record)) throw new Error('当前市场配置不再允许本地参数覆盖,请先清除覆盖');
|
|
695
697
|
if (!strictStartup) {
|
|
696
698
|
return provision(ctx, config, record, grantMap(), { failOnStartupError: false });
|
|
697
699
|
}
|
|
@@ -712,7 +714,9 @@ export async function apply(ctx, config) {
|
|
|
712
714
|
if (error === timeoutError) {
|
|
713
715
|
// loader 可能仍在等待 SDK 请求;它最终成功时必须恢复旧条目/移除新条目,绝不迟到落库。
|
|
714
716
|
task.then(
|
|
715
|
-
|
|
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) => {
|
|
716
720
|
logger.error(`late startup rollback "${record.key}" failed: ${rollbackError.message}`);
|
|
717
721
|
}),
|
|
718
722
|
() => {},
|
|
@@ -737,6 +741,14 @@ export async function apply(ctx, config) {
|
|
|
737
741
|
return failures;
|
|
738
742
|
}
|
|
739
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
|
+
|
|
740
752
|
async function commitConnectionRecords(records, { strictStartup = true } = {}) {
|
|
741
753
|
const originals = new Map(records.map((record) => [record.key, state.connections.get(record.key)]));
|
|
742
754
|
const staged = records.map((record) => {
|
|
@@ -752,9 +764,10 @@ export async function apply(ctx, config) {
|
|
|
752
764
|
provisioned.push(record);
|
|
753
765
|
}
|
|
754
766
|
} catch (error) {
|
|
755
|
-
await rollbackProvisionedRecords(provisioned, originals);
|
|
767
|
+
const rollbackErrors = await rollbackProvisionedRecords(provisioned, originals);
|
|
756
768
|
const wrapped = new Error(`MCP Server "${activeRecord?.serverName ?? activeRecord?.key ?? 'unknown'}" 未完成启动`, { cause: error });
|
|
757
769
|
wrapped.connectionRecord = activeRecord;
|
|
770
|
+
wrapped.rollbackErrors = rollbackErrors;
|
|
758
771
|
throw wrapped;
|
|
759
772
|
}
|
|
760
773
|
|
|
@@ -770,7 +783,7 @@ export async function apply(ctx, config) {
|
|
|
770
783
|
logger.error(`connection storage rollback "${record.key}" failed: ${rollbackError.message}`);
|
|
771
784
|
}
|
|
772
785
|
}
|
|
773
|
-
const wrapped = new Error('连接记录持久化失败,已回滚 Host 条目', { cause: error });
|
|
786
|
+
const wrapped = new Error(rollbackErrors.length ? '连接记录持久化失败,自动回滚未完成' : '连接记录持久化失败,已回滚 Host 条目', { cause: error });
|
|
774
787
|
wrapped.connectionRecord = activeRecord;
|
|
775
788
|
wrapped.rollbackErrors = rollbackErrors;
|
|
776
789
|
throw wrapped;
|
|
@@ -809,7 +822,12 @@ export async function apply(ctx, config) {
|
|
|
809
822
|
const committed = await commitConnectionRecords(records, options);
|
|
810
823
|
return { committed, snapshot };
|
|
811
824
|
} catch (error) {
|
|
812
|
-
|
|
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
|
+
}
|
|
813
831
|
throw error;
|
|
814
832
|
}
|
|
815
833
|
}
|
|
@@ -1951,6 +1969,10 @@ export async function apply(ctx, config) {
|
|
|
1951
1969
|
async connect(connectorId, serverKey, signal, scopeInput = {}) {
|
|
1952
1970
|
const connector = state.merged.find((d) => d.id === connectorId);
|
|
1953
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
|
+
}
|
|
1954
1976
|
let requestedScope;
|
|
1955
1977
|
const explicitScope = scopeInput?.scope !== undefined;
|
|
1956
1978
|
try {
|
|
@@ -2101,6 +2123,15 @@ export async function apply(ctx, config) {
|
|
|
2101
2123
|
createdAt: Date.now(),
|
|
2102
2124
|
updatedAt: Date.now(),
|
|
2103
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
|
+
}
|
|
2104
2135
|
const validation = await validateConnectionRecords([record], { timeoutMs: config.requestTimeoutMs });
|
|
2105
2136
|
if (!validation.ok) return { ...validation, detail: { connectorId: connector.id, ...validation.detail } };
|
|
2106
2137
|
try {
|
|
@@ -2108,6 +2139,7 @@ export async function apply(ctx, config) {
|
|
|
2108
2139
|
cacheHealth(connector.id, [committed], readinessResults([committed], validation.detail.results));
|
|
2109
2140
|
return { ok: true, message: `已连接 "${connector.name}"(${scopeLabel(connectionScopes.binding(committed.key), workspaceNames())})`, detail: { key: committed.key, snapshotId: snapshot?.key ?? null, scopeRevision, scopeRollbackRevision } };
|
|
2110
2141
|
} catch (error) {
|
|
2142
|
+
if (hasLocalOverrides) return { ok: false, message: '连接启动或保存失败,已尝试恢复原配置;请检查本机 Host 日志与连接状态后重试' };
|
|
2111
2143
|
return connectionSetupFailure(error);
|
|
2112
2144
|
}
|
|
2113
2145
|
}
|
|
@@ -2370,6 +2402,8 @@ export async function apply(ctx, config) {
|
|
|
2370
2402
|
kind: record.kind,
|
|
2371
2403
|
canRename: connectionDisplayNameEditable(record),
|
|
2372
2404
|
canEditConfiguration: connectionConfigurationEditable(record),
|
|
2405
|
+
canEditLocalOverrides: canEditLocalOverrides(record),
|
|
2406
|
+
hasLocalOverrides: Boolean(record.localOverrides),
|
|
2373
2407
|
serverName: record.serverName,
|
|
2374
2408
|
url: record.url,
|
|
2375
2409
|
transport: record.transport,
|
|
@@ -2612,6 +2646,37 @@ export async function apply(ctx, config) {
|
|
|
2612
2646
|
}
|
|
2613
2647
|
},
|
|
2614
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
|
+
|
|
2615
2680
|
async reconfigureConnection(key, json) {
|
|
2616
2681
|
const current = state.connections.get(key);
|
|
2617
2682
|
if (!current) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
@@ -3340,6 +3405,10 @@ export async function apply(ctx, config) {
|
|
|
3340
3405
|
}
|
|
3341
3406
|
for (const [key, record] of state.connections) {
|
|
3342
3407
|
if (record.enabled !== false) {
|
|
3408
|
+
if (record.localOverrides && !canEditLocalOverrides(record)) {
|
|
3409
|
+
logger.warn(`skip provision "${key}" at startup: 当前市场配置不再允许本地参数覆盖,请先清除覆盖`);
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3343
3412
|
const failure = grantFailure(record);
|
|
3344
3413
|
if (failure) {
|
|
3345
3414
|
logger.warn(`skip provision "${key}" at startup: ${failure.message}`);
|
|
@@ -3416,19 +3485,32 @@ export async function apply(ctx, config) {
|
|
|
3416
3485
|
}
|
|
3417
3486
|
// Serialize same-record actions: a delayed reconnect must never reactivate a
|
|
3418
3487
|
// connection the user has subsequently disabled or disconnected.
|
|
3419
|
-
const connectionActions = new Set(['reconnect', 'setEnabled', 'disconnect', 'reconfigureConnection', 'renameConnection', 'setInjectionMode']);
|
|
3488
|
+
const connectionActions = new Set(['reconnect', 'setEnabled', 'disconnect', 'reconfigureConnection', 'applyLocalOverrides', 'renameConnection', 'setInjectionMode']);
|
|
3420
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
|
+
}
|
|
3421
3497
|
for (const name of connectionActions) {
|
|
3422
3498
|
const action = api[name];
|
|
3423
|
-
api[name] =
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
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']);
|
|
3432
3514
|
const discoveryApi = new Proxy(api, { get(target, property) {
|
|
3433
3515
|
const value = target[property];
|
|
3434
3516
|
if (!discoveryMutations.has(property)) return value;
|
|
@@ -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/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",
|
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
|
},
|