dsh-mcp-connector 0.2.39 → 0.2.41
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 +34 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/USER-GUIDE.md +9 -1
- package/lib/connection-editor.js +216 -0
- package/lib/index.js +108 -2
- package/lib/tools.js +57 -1
- package/lib/util.js +9 -0
- package/lib/web.js +3 -0
- package/package.json +2 -2
- package/ui/index.html +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.41] - 2026-09-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- “已安装”页为用户自定义和 JSON 导入的连接新增“编辑配置”,可修改标准化单连接 JSON 并保存后重新连接,无需先断开和删除。
|
|
12
|
+
- 新增 `mcp_connector_get_editable_config` 与 `mcp_connector_reconfigure` 对话工具,提供相同的安全编辑和原子更新能力。
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- 已有 Token/API Key、Header/env 值、stdio 参数、本地路径及可能含凭据的 URL 只以 `<KEEP_EXISTING>` 返回;提交时在本机 storage-domain 内合并,也可显式替换或删除,不会将真实值发送到页面或对话。
|
|
17
|
+
- 配置更新锁定 connection key 与 `serverName`,保留原连接范围和启停状态;更新前完成连接校验并创建快照,校验、Host 启动或持久化失败时原连接保持可用。
|
|
18
|
+
|
|
19
|
+
### Verification
|
|
20
|
+
|
|
21
|
+
- 216 项自动测试、lint、版本/营销元数据/商店截图门禁和 npm 发布包白名单/敏感内容扫描全部通过;新增覆盖敏感值保留/替换/清除、HTTP/stdio、连接身份约束、Web API、对话工具、停用状态、失败保留与快照。
|
|
22
|
+
|
|
23
|
+
## [0.2.40] - 2026-09-09
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- “已安装”页为用户自定义和 JSON 导入的 MCP 连接新增“重命名”,启用状态下也可无中断修改显示名。
|
|
28
|
+
- 新增 `mcp_connector_rename` 对话工具,便于按连接 key 修改显示名。
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- 重命名只更新本机连接元数据并创建可恢复快照;connection key、`serverName`、工具前缀、连接范围、凭据和 Host 挂载均保持不变。
|
|
33
|
+
|
|
34
|
+
### Verification
|
|
35
|
+
|
|
36
|
+
- 209 项自动测试、lint、版本/营销元数据/商店截图门禁和 npm 发布包白名单/敏感内容扫描全部通过;新增覆盖显示名校验、Web API、对话工具、界面入口、启用状态下无重挂载、脱敏导出、快照与重启持久化。
|
|
37
|
+
|
|
7
38
|
## [0.2.39] - 2026-09-08
|
|
8
39
|
|
|
9
40
|
### Fixed
|
|
@@ -607,7 +638,9 @@
|
|
|
607
638
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
608
639
|
- iframe 消息校验同源和消息来源。
|
|
609
640
|
|
|
610
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
641
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.41...HEAD
|
|
642
|
+
[0.2.41]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.40...v0.2.41
|
|
643
|
+
[0.2.40]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.39...v0.2.40
|
|
611
644
|
[0.2.39]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.38...v0.2.39
|
|
612
645
|
[0.2.38]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.37...v0.2.38
|
|
613
646
|
[0.2.37]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.36...v0.2.37
|
package/README.en.md
CHANGED
|
@@ -57,6 +57,7 @@ If the plugin helps you connect an MCP server faster, consider [starring the rep
|
|
|
57
57
|
- Dynamic tool discovery grouped by MCP server, including descriptions, search, batched rendering, and an independent scroll region.
|
|
58
58
|
- Curated prompt templates that can open a DSH conversation and prefill its draft; missing variables are requested before the prompt is sent.
|
|
59
59
|
- Persistent connection lifecycle management: restore on restart, enable/disable, disconnect, retry transient OAuth refresh failures with bounded backoff, and revoke authorization. Cards declaring issuer-level sharing reuse one grant; a cross-process lock and per-grant atomic journal prevent Desktop and Web hosts from consuming or overwriting the same rotating refresh token.
|
|
60
|
+
- In-place configuration editing for custom and JSON-imported connections: edit normalized JSON from Installed and reconnect without deleting the connection. Existing sensitive values use local keep markers, and validation, startup, or persistence failures leave the previous connection usable.
|
|
60
61
|
- Portable redacted configuration export plus up to 20 local pre-change snapshots, with preview and atomic restore. Credentials, local paths, and OAuth grants never enter the export.
|
|
61
62
|
- Project/global connection scopes: choose the current DSH Workspace or profile-global visibility when connecting, preview affected servers/tools, then copy, move, or roll back by revision. Credentials remain single-copy and project-only tools are enforced by the DSH Host.
|
|
62
63
|
- Three-layer Connection, Server, and Tool governance with Tool > Server > Connection > default-allow precedence, preview, revision-checked commits, and rollback. DSH Host restrictions narrow schemas/lookups/dispatch while a final execution guard prevents UI-only enforcement.
|
|
@@ -175,7 +176,7 @@ npm run dev:ui
|
|
|
175
176
|
|
|
176
177
|
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.
|
|
177
178
|
|
|
178
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
179
|
+
The current public version is [`dsh-mcp-connector@0.2.41`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.41](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.41).
|
|
179
180
|
|
|
180
181
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
181
182
|
|
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
57
57
|
- 三种接入:OAuth 2.0 PKCE、自定义 HTTP/stdio、导入 `mcpServers` JSON;也支持从连接器描述 URL 安装。OAuth 动态注册兼容公共客户端以及 `client_secret_post` / `client_secret_basic` 机密客户端。
|
|
58
58
|
- 市场 Bearer/API Key 连接器先执行 MCP initialize 连通性与凭据校验,全部 HTTP Server 通过后才持久化凭据并进入“已安装”;stdio 卡片可声明多个本机凭据字段及其环境变量映射。
|
|
59
59
|
- 生命周期管理:连接持久化、重启恢复、启停、断开、OAuth 自动刷新/退避恢复与撤销;同 issuer 卡片共享一次授权,跨进程锁与独立原子 Grant journal 防止 Desktop/Web 并行时重复消耗 Refresh Token。
|
|
60
|
+
- 配置原地编辑:自定义和 JSON 导入连接可在“已安装”中编辑标准化 JSON 并重新连接;敏感值以本机保留标记处理,校验、启动或保存失败时原连接继续可用。
|
|
60
61
|
- 配置备份:一键复制/下载可再次导入的脱敏 JSON;连接变更前自动保存最多 20 个本机快照,支持预览与原子恢复。凭据、本地路径和 OAuth Grant 不进入导出结果。
|
|
61
62
|
- 连接作用域:新连接可选当前 Workspace 项目或 profile 全局;支持先预览 Server/工具影响,再复制、移动或按 revision 回滚。凭据只存一份,project-only 工具由 DSH Host 强制隔离。
|
|
62
63
|
- 三层治理:Connection、Server、Tool 规则按 Tool > Server > Connection > 默认允许解析;变更先预览、按 revision 提交并可回滚,由 DSH Host 的 schema/lookup/dispatch restriction 与最终执行 Guard 真实生效。
|
|
@@ -171,7 +172,7 @@ npm run dev:ui
|
|
|
171
172
|
|
|
172
173
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
173
174
|
|
|
174
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
175
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.41`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.41](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.41)。
|
|
175
176
|
|
|
176
177
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
177
178
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -152,6 +152,8 @@ stdio 进程由 `@deepseek-ai/dsh-mcp-client` 管理,插件透传 `command`、
|
|
|
152
152
|
在“已安装”中可以:
|
|
153
153
|
|
|
154
154
|
- 查看连接状态和 Server 数量;
|
|
155
|
+
- 编辑自定义或 JSON 导入连接的标准化 JSON,并保存后重新连接;
|
|
156
|
+
- 修改自定义或 JSON 导入连接的显示名,无需先停用连接;
|
|
155
157
|
- 启用或停用连接;
|
|
156
158
|
- 重新授权 OAuth,或重新录入 Token/API Key;
|
|
157
159
|
- 执行健康检查;
|
|
@@ -159,12 +161,18 @@ stdio 进程由 `@deepseek-ai/dsh-mcp-client` 管理,插件透传 `command`、
|
|
|
159
161
|
|
|
160
162
|
OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端点时仍会删除 DSH 本机授权记录。连接状态发生变化后,可点击“刷新连接器目录”重新检查。
|
|
161
163
|
|
|
164
|
+
“重命名”只修改用户看到的连接显示名,不会改变 connection key、`serverName`、`mcp__<serverName>__*` 工具前缀、连接范围、凭据或启停状态,也不会重新挂载 MCP Server。市场连接器的名称继续由公共目录维护。
|
|
165
|
+
|
|
166
|
+
“编辑配置”会展示一条标准化的 `connections` JSON,不保证恢复最初导入文本的排版、注释或字段别名。`serverName` 与 connection key 共同标识现有连接,因此不能在此修改;如需新的工具前缀,请另建连接。Token/API Key、Header/env 值、stdio 参数、本地目录以及可能含凭据的 URL 不会返回页面,而是显示 `<KEEP_EXISTING>`:保留标记表示继续使用本机原值,替换标记可录入新值,删除对应字段可清除该项。
|
|
167
|
+
|
|
168
|
+
启用中的连接点击“保存并重连”后会先验证新配置、创建快照,再原子更新 Host 与本机记录;任何校验、启动或持久化失败都会保留原连接。停用连接可以保存配置但仍保持停用。连接范围和启停状态均不会被 JSON 静默改变。市场/OAuth 连接继续使用卡片的“配置”或“重新授权”流程。
|
|
169
|
+
|
|
162
170
|
### 7.1 配置备份与恢复
|
|
163
171
|
|
|
164
172
|
“已安装”页的“配置备份”提供两种互补能力:
|
|
165
173
|
|
|
166
174
|
- **脱敏导出**:复制或下载可携带 JSON。Token/API Key、静态 Header/env 值、stdio 参数、本地目录以及可能含凭据的 URL 会变成明确占位符;目标设备导入前必须重填。OAuth 只导出连接引用,必须从市场重新授权。
|
|
167
|
-
-
|
|
175
|
+
- **本机快照**:连接、配置、导入、重命名、启停或断开前自动保存变更范围,也可手动创建;最多保留 20 个。恢复前可查看将恢复和移除的 key,恢复过程中任一 Server 或持久化步骤失败会整体回到恢复前状态。
|
|
168
176
|
|
|
169
177
|
快照可能包含用于原样恢复的本机凭据,因此只保存在当前 profile 的 storage domain,不会通过 Web API、对话工具或日志原样返回。断开并撤销 OAuth 后,快照不能重建服务端 Grant,会明确要求重新授权。完整边界见[配置备份说明](CONFIG-BACKUP.md)。
|
|
170
178
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户自定义 / JSON 连接的安全编辑视图。
|
|
3
|
+
*
|
|
4
|
+
* 浏览器只拿到标准化 JSON 与“保留现有值”标记,已有 Token、Header/env 值、
|
|
5
|
+
* stdio 参数和本地路径始终留在 storage-domain 内。提交时再在本机合并这些值。
|
|
6
|
+
*/
|
|
7
|
+
import { CUSTOM_CONNECTOR_ID, JSON_CONNECTOR_ID } from './constants.js';
|
|
8
|
+
import { normalizeJsonImport } from './connectors/json-connector.js';
|
|
9
|
+
import { normalizeConnectionDisplayName } from './util.js';
|
|
10
|
+
|
|
11
|
+
export const KEEP_EXISTING_VALUE = '<KEEP_EXISTING>';
|
|
12
|
+
|
|
13
|
+
export function connectionConfigurationEditable(record) {
|
|
14
|
+
return [CUSTOM_CONNECTOR_ID, JSON_CONNECTOR_ID].includes(record?.connectorId)
|
|
15
|
+
&& record?.auth?.mode !== 'oauth';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function keepObjectKeys(input, path, preservedFields) {
|
|
19
|
+
const output = {};
|
|
20
|
+
for (const key of Object.keys(input ?? {}).sort()) {
|
|
21
|
+
output[key] = KEEP_EXISTING_VALUE;
|
|
22
|
+
preservedFields.push(`${path}.${key}`);
|
|
23
|
+
}
|
|
24
|
+
return output;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeUrl(record, preservedFields) {
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(record.url);
|
|
30
|
+
if (!url.username && !url.password && !url.search && !url.hash) return url.toString();
|
|
31
|
+
} catch {}
|
|
32
|
+
preservedFields.push('connections[0].url');
|
|
33
|
+
return KEEP_EXISTING_VALUE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function safeCommand(record, preservedFields) {
|
|
37
|
+
const command = String(record.command ?? '');
|
|
38
|
+
if (/^[A-Za-z0-9_.@+-]+$/.test(command)) return command;
|
|
39
|
+
preservedFields.push('connections[0].command');
|
|
40
|
+
return KEEP_EXISTING_VALUE;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 返回单连接、标准化的可编辑 JSON;不返回任何已有敏感值。 */
|
|
44
|
+
export function editableConnectionConfiguration(record) {
|
|
45
|
+
if (!connectionConfigurationEditable(record)) {
|
|
46
|
+
throw new Error('仅用户自定义或 JSON 导入的连接可编辑配置;市场连接器请使用配置或重新授权入口');
|
|
47
|
+
}
|
|
48
|
+
const preservedFields = [];
|
|
49
|
+
const item = {
|
|
50
|
+
name: record.name,
|
|
51
|
+
serverName: record.serverName,
|
|
52
|
+
transport: record.transport === 'stdio' ? 'stdio' : 'streamable-http',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (record.transport === 'stdio') {
|
|
56
|
+
item.command = safeCommand(record, preservedFields);
|
|
57
|
+
if ((record.args ?? []).length > 0) {
|
|
58
|
+
item.args = [KEEP_EXISTING_VALUE];
|
|
59
|
+
preservedFields.push('connections[0].args');
|
|
60
|
+
} else item.args = [];
|
|
61
|
+
item.env = keepObjectKeys(record.env, 'connections[0].env', preservedFields);
|
|
62
|
+
if (record.cwd) {
|
|
63
|
+
item.cwd = KEEP_EXISTING_VALUE;
|
|
64
|
+
preservedFields.push('connections[0].cwd');
|
|
65
|
+
} else item.cwd = '';
|
|
66
|
+
} else {
|
|
67
|
+
item.url = safeUrl(record, preservedFields);
|
|
68
|
+
if (record.allowInsecurePrivateNetwork === true) item.allowInsecurePrivateNetwork = true;
|
|
69
|
+
item.headers = keepObjectKeys(record.headers, 'connections[0].headers', preservedFields);
|
|
70
|
+
item.authMode = record.auth?.mode ?? 'none';
|
|
71
|
+
if (record.auth?.mode === 'bearer') {
|
|
72
|
+
item.bearerToken = KEEP_EXISTING_VALUE;
|
|
73
|
+
preservedFields.push('connections[0].bearerToken');
|
|
74
|
+
} else if (record.auth?.mode === 'api-key') {
|
|
75
|
+
item.apiKeyHeader = record.auth.apiKeyHeader || 'X-Api-Key';
|
|
76
|
+
item.apiKeyValue = KEEP_EXISTING_VALUE;
|
|
77
|
+
preservedFields.push('connections[0].apiKeyValue');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
key: record.key,
|
|
83
|
+
serverName: record.serverName,
|
|
84
|
+
normalized: true,
|
|
85
|
+
preservedFields,
|
|
86
|
+
json: JSON.stringify({ connections: [item] }, null, 2),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseInput(input) {
|
|
91
|
+
if (typeof input !== 'string') return structuredClone(input);
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(input);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new Error(`JSON 解析失败: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function singleConfig(document) {
|
|
100
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
101
|
+
throw new Error('JSON 顶层必须是对象');
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(document.connections)) {
|
|
104
|
+
if (document.connections.length !== 1) throw new Error('编辑配置时只能提交当前这一条 connection');
|
|
105
|
+
return document.connections[0];
|
|
106
|
+
}
|
|
107
|
+
if (document.mcpServers && typeof document.mcpServers === 'object' && !Array.isArray(document.mcpServers)) {
|
|
108
|
+
const entries = Object.entries(document.mcpServers);
|
|
109
|
+
if (entries.length !== 1) throw new Error('编辑配置时只能提交当前这一个 mcpServer');
|
|
110
|
+
return entries[0][1];
|
|
111
|
+
}
|
|
112
|
+
throw new Error('期望 { connections: [...] } 或 { mcpServers: {...} }');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function existingHeaderValue(record, key) {
|
|
116
|
+
const direct = Object.entries(record.headers ?? {}).find(([name]) => name.toLowerCase() === key.toLowerCase());
|
|
117
|
+
if (direct) return direct[1];
|
|
118
|
+
if (key.toLowerCase() === 'authorization' && record.auth?.mode === 'bearer') {
|
|
119
|
+
return `Bearer ${record.auth.bearerToken}`;
|
|
120
|
+
}
|
|
121
|
+
if (record.auth?.mode === 'api-key' && record.auth.apiKeyHeader?.toLowerCase() === key.toLowerCase()) {
|
|
122
|
+
return record.auth.apiKeyValue;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function replaceKeepMarkers(document, current) {
|
|
128
|
+
const item = singleConfig(document);
|
|
129
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('连接配置必须是 JSON 对象');
|
|
130
|
+
|
|
131
|
+
if (item.url === KEEP_EXISTING_VALUE) {
|
|
132
|
+
if (!current.url) throw new Error('url 没有可保留的原值,请填写新值');
|
|
133
|
+
item.url = current.url;
|
|
134
|
+
}
|
|
135
|
+
if (item.command === KEEP_EXISTING_VALUE) {
|
|
136
|
+
if (!current.command) throw new Error('command 没有可保留的原值,请填写新值');
|
|
137
|
+
item.command = current.command;
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(item.args) && item.args.length === 1 && item.args[0] === KEEP_EXISTING_VALUE) {
|
|
140
|
+
item.args = [...(current.args ?? [])];
|
|
141
|
+
}
|
|
142
|
+
if (item.cwd === KEEP_EXISTING_VALUE) item.cwd = current.cwd ?? '';
|
|
143
|
+
|
|
144
|
+
for (const [key, value] of Object.entries(item.headers ?? {})) {
|
|
145
|
+
if (value !== KEEP_EXISTING_VALUE) continue;
|
|
146
|
+
const existing = existingHeaderValue(current, key);
|
|
147
|
+
if (existing === undefined) throw new Error(`Header ${key} 没有可保留的原值,请填写新值`);
|
|
148
|
+
item.headers[key] = existing;
|
|
149
|
+
}
|
|
150
|
+
for (const [key, value] of Object.entries(item.env ?? {})) {
|
|
151
|
+
if (value !== KEEP_EXISTING_VALUE) continue;
|
|
152
|
+
if (current.env?.[key] === undefined) throw new Error(`环境变量 ${key} 没有可保留的原值,请填写新值`);
|
|
153
|
+
item.env[key] = current.env[key];
|
|
154
|
+
}
|
|
155
|
+
if (item.bearerToken === KEEP_EXISTING_VALUE) {
|
|
156
|
+
if (current.auth?.mode !== 'bearer' || !current.auth.bearerToken) throw new Error('Bearer Token 没有可保留的原值,请填写新值');
|
|
157
|
+
item.bearerToken = current.auth.bearerToken;
|
|
158
|
+
}
|
|
159
|
+
if (item.apiKeyValue === KEEP_EXISTING_VALUE) {
|
|
160
|
+
if (current.auth?.mode !== 'api-key' || current.auth.apiKeyValue === undefined || current.auth.apiKeyValue === '') {
|
|
161
|
+
throw new Error('API Key 没有可保留的原值,请填写新值');
|
|
162
|
+
}
|
|
163
|
+
item.apiKeyValue = current.auth.apiKeyValue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function findKeepMarkers(value, path = '$', output = []) {
|
|
168
|
+
if (value === KEEP_EXISTING_VALUE) output.push(path);
|
|
169
|
+
else if (Array.isArray(value)) value.forEach((item, index) => findKeepMarkers(item, `${path}[${index}]`, output));
|
|
170
|
+
else if (value && typeof value === 'object') {
|
|
171
|
+
for (const [key, item] of Object.entries(value)) findKeepMarkers(item, `${path}.${key}`, output);
|
|
172
|
+
}
|
|
173
|
+
return output;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 把编辑器 JSON 合并为一条新的 ConnectionRecord。
|
|
178
|
+
* key / serverName / connectorId / kind / enabled / scope 均由旧记录锁定。
|
|
179
|
+
*/
|
|
180
|
+
export function normalizeConnectionReconfiguration(current, input) {
|
|
181
|
+
if (!connectionConfigurationEditable(current)) {
|
|
182
|
+
throw new Error('仅用户自定义或 JSON 导入的连接可编辑配置;市场连接器请使用配置或重新授权入口');
|
|
183
|
+
}
|
|
184
|
+
const document = parseInput(input);
|
|
185
|
+
replaceKeepMarkers(document, current);
|
|
186
|
+
const unresolved = findKeepMarkers(document);
|
|
187
|
+
if (unresolved.length > 0) throw new Error(`存在无法识别的保留标记:${unresolved.slice(0, 3).join('、')}`);
|
|
188
|
+
|
|
189
|
+
const { records, skipped } = normalizeJsonImport(document);
|
|
190
|
+
if (records.length !== 1 || skipped.length > 0) throw new Error('编辑配置时必须提交且只能提交当前这一条有效连接');
|
|
191
|
+
const parsed = records[0];
|
|
192
|
+
if (parsed.serverName !== current.serverName) {
|
|
193
|
+
throw new Error(`serverName 是连接身份,不能从 "${current.serverName}" 修改为 "${parsed.serverName}";请新建连接`);
|
|
194
|
+
}
|
|
195
|
+
parsed.name = normalizeConnectionDisplayName(parsed.name);
|
|
196
|
+
if (parsed.auth?.mode === 'bearer' && !parsed.auth.bearerToken) throw new Error('Bearer Token 必填,或使用保留标记');
|
|
197
|
+
if (parsed.auth?.mode === 'api-key' && (parsed.auth.apiKeyValue === undefined || parsed.auth.apiKeyValue === '')) {
|
|
198
|
+
throw new Error('API Key 必填,或使用保留标记');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const stdio = parsed.transport === 'stdio';
|
|
202
|
+
return {
|
|
203
|
+
...parsed,
|
|
204
|
+
key: current.key,
|
|
205
|
+
connectorId: current.connectorId,
|
|
206
|
+
kind: current.kind,
|
|
207
|
+
serverKey: current.serverKey,
|
|
208
|
+
enabled: current.enabled !== false,
|
|
209
|
+
createdAt: current.createdAt,
|
|
210
|
+
updatedAt: Date.now(),
|
|
211
|
+
lastError: undefined,
|
|
212
|
+
...(stdio
|
|
213
|
+
? { url: undefined, allowInsecurePrivateNetwork: undefined, headers: {}, auth: undefined }
|
|
214
|
+
: { command: undefined, args: undefined, env: undefined, cwd: undefined }),
|
|
215
|
+
};
|
|
216
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 连接记录与 OAuth grant 持久化,重启恢复与跨 Host Token 轮换
|
|
8
8
|
* - 通过 ctx.loader 动态配置 @deepseek-ai/dsh-mcp-client 条目
|
|
9
9
|
* - 对话工具:mcp_connector_catalog / connect / configure / import_json /
|
|
10
|
-
* export_config / snapshots / install_from_url / status / set_enabled /
|
|
10
|
+
* export_config / snapshots / install_from_url / status / rename / set_enabled /
|
|
11
11
|
* disconnect / refresh_catalog / publish
|
|
12
12
|
*/
|
|
13
13
|
import { randomUUID } from 'node:crypto';
|
|
@@ -29,6 +29,11 @@ import { loadBundledCatalog, fetchRemoteCatalogWithFallback, mergeCatalog, listC
|
|
|
29
29
|
import { describeOAuthAuthorizationError, oauthAuthorize } from './connectors/oauth-connector.js';
|
|
30
30
|
import { buildManualRecord } from './connectors/manual-connector.js';
|
|
31
31
|
import { normalizeJsonImport } from './connectors/json-connector.js';
|
|
32
|
+
import {
|
|
33
|
+
connectionConfigurationEditable,
|
|
34
|
+
editableConnectionConfiguration,
|
|
35
|
+
normalizeConnectionReconfiguration,
|
|
36
|
+
} from './connection-editor.js';
|
|
32
37
|
import { discoverServerMetadata, refreshAccessToken, revokeRefreshToken, extractTokenResources } from './oauth.js';
|
|
33
38
|
import { provision, disable, remove, entryIdFor } from './mcp-provision.js';
|
|
34
39
|
import { classifyConnectionError, validateConnectionRecords } from './mcp-validation.js';
|
|
@@ -36,7 +41,7 @@ import { listMcpTools, McpHttpError } from './mcp-http.js';
|
|
|
36
41
|
import { registerTools } from './tools.js';
|
|
37
42
|
import { mountWebRoutes } from './web.js';
|
|
38
43
|
import { createVersionStatusService } from './version-status.js';
|
|
39
|
-
import { assertSafeUrl } from './util.js';
|
|
44
|
+
import { assertSafeUrl, normalizeConnectionDisplayName } from './util.js';
|
|
40
45
|
import { readLegacyGrantCandidates, planLegacyMigration, toConnectorGrant, toConnectionRecords } from './migration.js';
|
|
41
46
|
import { classifyRefreshFailure, refreshRetryDelay } from './grant-lifecycle.js';
|
|
42
47
|
import { buildHealthSummary, diagnosticForRecord } from './diagnostics.js';
|
|
@@ -575,6 +580,10 @@ export async function apply(ctx, config) {
|
|
|
575
580
|
if (record?.connectorId) state.health.delete(record.connectorId);
|
|
576
581
|
}
|
|
577
582
|
|
|
583
|
+
function connectionDisplayNameEditable(record) {
|
|
584
|
+
return connectionConfigurationEditable(record);
|
|
585
|
+
}
|
|
586
|
+
|
|
578
587
|
async function persistRecord(record) {
|
|
579
588
|
// 存储成功后才更新内存,避免落盘失败却在当前进程显示为“已安装”。
|
|
580
589
|
if (config.persistSecrets) await connectionStore.put(record);
|
|
@@ -2200,6 +2209,8 @@ export async function apply(ctx, config) {
|
|
|
2200
2209
|
name: record.name,
|
|
2201
2210
|
connectorId: record.connectorId,
|
|
2202
2211
|
kind: record.kind,
|
|
2212
|
+
canRename: connectionDisplayNameEditable(record),
|
|
2213
|
+
canEditConfiguration: connectionConfigurationEditable(record),
|
|
2203
2214
|
serverName: record.serverName,
|
|
2204
2215
|
url: record.url,
|
|
2205
2216
|
transport: record.transport,
|
|
@@ -2362,6 +2373,101 @@ export async function apply(ctx, config) {
|
|
|
2362
2373
|
};
|
|
2363
2374
|
},
|
|
2364
2375
|
|
|
2376
|
+
async renameConnection(key, name) {
|
|
2377
|
+
const record = state.connections.get(key);
|
|
2378
|
+
if (!record) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
2379
|
+
if (!connectionDisplayNameEditable(record)) {
|
|
2380
|
+
return { ok: false, message: '仅用户自定义或 JSON 导入的连接可修改显示名;市场连接器名称由目录维护' };
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
let nextName;
|
|
2384
|
+
try {
|
|
2385
|
+
nextName = normalizeConnectionDisplayName(name);
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
return { ok: false, message: `重命名失败: ${error.message}` };
|
|
2388
|
+
}
|
|
2389
|
+
if (nextName === record.name) {
|
|
2390
|
+
return {
|
|
2391
|
+
ok: true,
|
|
2392
|
+
message: `连接显示名未变更:"${record.name}"`,
|
|
2393
|
+
detail: { key: record.key, name: record.name, serverName: record.serverName, unchanged: true, snapshotId: null },
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
let snapshot;
|
|
2398
|
+
try {
|
|
2399
|
+
snapshot = await captureConnectionSnapshot('rename', [key]);
|
|
2400
|
+
const next = { ...record, name: nextName, updatedAt: Date.now() };
|
|
2401
|
+
const persisted = config.persistSecrets ? await connectionStore.put(next) : next;
|
|
2402
|
+
state.connections.set(key, persisted);
|
|
2403
|
+
return {
|
|
2404
|
+
ok: true,
|
|
2405
|
+
message: `已将连接显示名修改为 "${persisted.name}"`,
|
|
2406
|
+
detail: {
|
|
2407
|
+
key: persisted.key,
|
|
2408
|
+
name: persisted.name,
|
|
2409
|
+
serverName: persisted.serverName,
|
|
2410
|
+
enabled: persisted.enabled !== false,
|
|
2411
|
+
snapshotId: snapshot?.key ?? null,
|
|
2412
|
+
},
|
|
2413
|
+
};
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
await discardConnectionSnapshot(snapshot);
|
|
2416
|
+
return { ok: false, message: `重命名失败,已保留原显示名: ${error.message}` };
|
|
2417
|
+
}
|
|
2418
|
+
},
|
|
2419
|
+
|
|
2420
|
+
async editableConnectionConfig(key) {
|
|
2421
|
+
const record = state.connections.get(key);
|
|
2422
|
+
if (!record) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
2423
|
+
try {
|
|
2424
|
+
return {
|
|
2425
|
+
ok: true,
|
|
2426
|
+
message: `已生成 "${record.name}" 的安全可编辑配置;保留标记将在本机合并,不会返回已有敏感值`,
|
|
2427
|
+
detail: editableConnectionConfiguration(record),
|
|
2428
|
+
};
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
return { ok: false, message: `读取可编辑配置失败: ${error.message}` };
|
|
2431
|
+
}
|
|
2432
|
+
},
|
|
2433
|
+
|
|
2434
|
+
async reconfigureConnection(key, json) {
|
|
2435
|
+
const current = state.connections.get(key);
|
|
2436
|
+
if (!current) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
2437
|
+
try {
|
|
2438
|
+
const next = normalizeConnectionReconfiguration(current, json);
|
|
2439
|
+
const validation = await validateConnectionRecords([next], { timeoutMs: config.requestTimeoutMs });
|
|
2440
|
+
if (!validation.ok) {
|
|
2441
|
+
return {
|
|
2442
|
+
...validation,
|
|
2443
|
+
message: `${validation.message.replace(/。未保存连接,请修正后重试。$/, '')}。已保留原连接和配置,请修正后重试。`,
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
const { committed: [committed], snapshot } = await commitWithSnapshot([next], 'reconfigure');
|
|
2447
|
+
if (committed.connectorId && committed.enabled !== false) {
|
|
2448
|
+
cacheHealth(committed.connectorId, [committed], readinessResults([committed], validation.detail.results));
|
|
2449
|
+
}
|
|
2450
|
+
return {
|
|
2451
|
+
ok: true,
|
|
2452
|
+
message: committed.enabled === false
|
|
2453
|
+
? `已保存 "${committed.name}" 的配置;连接保持停用`
|
|
2454
|
+
: `已保存并重新连接 "${committed.name}"`,
|
|
2455
|
+
detail: {
|
|
2456
|
+
key: committed.key,
|
|
2457
|
+
name: committed.name,
|
|
2458
|
+
serverName: committed.serverName,
|
|
2459
|
+
transport: committed.transport,
|
|
2460
|
+
enabled: committed.enabled !== false,
|
|
2461
|
+
snapshotId: snapshot?.key ?? null,
|
|
2462
|
+
},
|
|
2463
|
+
};
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
return error?.connectionRecord
|
|
2466
|
+
? connectionSetupFailure(error, '更新配置失败', '已保留原连接和配置,请修正后重试')
|
|
2467
|
+
: { ok: false, message: `更新配置失败,已保留原连接和配置: ${error.message}` };
|
|
2468
|
+
}
|
|
2469
|
+
},
|
|
2470
|
+
|
|
2365
2471
|
async setEnabled(key, enabled) {
|
|
2366
2472
|
const record = state.connections.get(key);
|
|
2367
2473
|
if (!record) return { ok: false, message: `连接 "${key}" 不存在` };
|
package/lib/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 对话工具面:目录 / 连接 / 配置 / 导入 / 脱敏导出 / 快照 / URL 安装 / 状态 / 启停 / 断开 / 刷新目录 / 上下架。
|
|
2
|
+
* 对话工具面:目录 / 连接 / 配置 / 导入 / 安全编辑 / 脱敏导出 / 快照 / URL 安装 / 状态 / 启停 / 断开 / 刷新目录 / 上下架。
|
|
3
3
|
*/
|
|
4
4
|
const RESULT_SCHEMA = {
|
|
5
5
|
type: 'object',
|
|
@@ -469,6 +469,62 @@ export function registerTools(ctx, api) {
|
|
|
469
469
|
},
|
|
470
470
|
});
|
|
471
471
|
|
|
472
|
+
reg({
|
|
473
|
+
name: 'mcp_connector_rename',
|
|
474
|
+
description:
|
|
475
|
+
'修改用户自定义或 JSON 导入 MCP 连接的显示名。不改变 connection key、serverName、工具前缀、运行状态或凭据。',
|
|
476
|
+
parameters: {
|
|
477
|
+
type: 'object',
|
|
478
|
+
properties: {
|
|
479
|
+
key: { type: 'string', description: '连接 key(来自 mcp_connector_status)' },
|
|
480
|
+
name: { type: 'string', description: '新的连接显示名,最长 80 个字符' },
|
|
481
|
+
},
|
|
482
|
+
required: ['key', 'name'],
|
|
483
|
+
additionalProperties: false,
|
|
484
|
+
},
|
|
485
|
+
output: resultOutput,
|
|
486
|
+
async execute(args) {
|
|
487
|
+
return api.renameConnection(args.key, args.name);
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
reg({
|
|
492
|
+
name: 'mcp_connector_get_editable_config',
|
|
493
|
+
description:
|
|
494
|
+
'读取用户自定义或 JSON 导入连接的标准化可编辑 JSON。已有敏感值只返回 <KEEP_EXISTING> 保留标记,不会返回真实凭据、本地参数或路径。',
|
|
495
|
+
parameters: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
key: { type: 'string', description: '连接 key(来自 mcp_connector_status)' },
|
|
499
|
+
},
|
|
500
|
+
required: ['key'],
|
|
501
|
+
additionalProperties: false,
|
|
502
|
+
},
|
|
503
|
+
output: exportOutput,
|
|
504
|
+
async execute(args) {
|
|
505
|
+
return api.editableConnectionConfig(args.key);
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
reg({
|
|
510
|
+
name: 'mcp_connector_reconfigure',
|
|
511
|
+
description:
|
|
512
|
+
'校验并原子更新一条用户自定义或 JSON 导入连接。serverName 和连接身份不可修改;<KEEP_EXISTING> 在本机合并,失败时保留原连接。',
|
|
513
|
+
parameters: {
|
|
514
|
+
type: 'object',
|
|
515
|
+
properties: {
|
|
516
|
+
key: { type: 'string', description: '连接 key(来自 mcp_connector_status)' },
|
|
517
|
+
json: { type: 'string', description: '修改后的单连接 JSON,可使用 get_editable_config 返回的保留标记' },
|
|
518
|
+
},
|
|
519
|
+
required: ['key', 'json'],
|
|
520
|
+
additionalProperties: false,
|
|
521
|
+
},
|
|
522
|
+
output: resultOutput,
|
|
523
|
+
async execute(args) {
|
|
524
|
+
return api.reconfigureConnection(args.key, args.json);
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
|
|
472
528
|
reg({
|
|
473
529
|
name: 'mcp_connector_disconnect',
|
|
474
530
|
description: '断开并移除一条 MCP 连接;OAuth 连接会按引用计数撤销 refresh_token(无其他连接引用该授权时)。',
|
package/lib/util.js
CHANGED
|
@@ -52,6 +52,15 @@ export function slugServerName(input) {
|
|
|
52
52
|
return slug;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** 归一化用户可编辑的连接显示名;不用于 serverName / 工具前缀。 */
|
|
56
|
+
export function normalizeConnectionDisplayName(input) {
|
|
57
|
+
const name = String(input ?? '').trim();
|
|
58
|
+
if (!name) throw new Error('连接显示名不能为空');
|
|
59
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) throw new Error('连接显示名不能包含控制字符');
|
|
60
|
+
if ([...name].length > 80) throw new Error('连接显示名不能超过 80 个字符');
|
|
61
|
+
return name;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
function parseUrl(rawUrl) {
|
|
56
65
|
let url;
|
|
57
66
|
try {
|
package/lib/web.js
CHANGED
|
@@ -41,6 +41,9 @@ const API_WHITELIST = new Map([
|
|
|
41
41
|
['previewSnapshot', (api, p) => api.previewSnapshot(p?.snapshotId)],
|
|
42
42
|
['restoreSnapshot', (api, p) => api.restoreSnapshot(p?.snapshotId)],
|
|
43
43
|
['installFromUrl', (api, p) => api.installFromUrl(p?.url, p ?? {})],
|
|
44
|
+
['renameConnection', (api, p) => api.renameConnection(p?.key, p?.name)],
|
|
45
|
+
['editableConnectionConfig', (api, p) => api.editableConnectionConfig(p?.key)],
|
|
46
|
+
['reconfigureConnection', (api, p) => api.reconfigureConnection(p?.key, p?.json)],
|
|
44
47
|
['disconnect', (api, p) => api.disconnect(p?.key)],
|
|
45
48
|
['setEnabled', (api, p) => api.setEnabled(p?.key, p?.enabled !== false)],
|
|
46
49
|
['refreshCatalog', (api) => api.refreshCatalog()],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
4
4
|
"description": "DeepSeek Harness MCP Connector and MCP Server marketplace with over one hundred MCP connectors, continuously updated. Discover, authorize, and manage connections in one place; supports OAuth 2.0 PKCE, API keys, stdio/HTTP, mcpServers JSON import, and tool and prompt discovery. Maintained by Qichacha/QCC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
43
|
"test": "node --test",
|
|
44
|
-
"lint": "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-scopes.js && node --check lib/governance.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/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/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/check-readme-version.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",
|
|
44
|
+
"lint": "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/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/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/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/check-readme-version.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",
|
|
45
45
|
"registry:build": "node scripts/build-registry.mjs",
|
|
46
46
|
"registry:validate": "node scripts/probe-connector.mjs catalog/catalog.json --validate-only",
|
|
47
47
|
"dev:ui": "node scripts/ui-harness.mjs",
|
package/ui/index.html
CHANGED
|
@@ -394,6 +394,7 @@
|
|
|
394
394
|
.inline-error { color: var(--bad); background: var(--bad-bg); border-radius: 7px; padding: 8px 10px; margin-top: 12px; font-size: 12px; display: none; }
|
|
395
395
|
.inline-error.show { display: block; }
|
|
396
396
|
#backup-json { height: 190px; resize: vertical; font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
397
|
+
#edit-connection-json { height: 260px; resize: vertical; font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
397
398
|
.snapshot-list { border: 1px solid var(--line); border-radius: 9px; margin-top: 8px; max-height: 210px; overflow-y: auto; }
|
|
398
399
|
.snapshot-item { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-top: 1px solid var(--line); }
|
|
399
400
|
.snapshot-item:first-child { border-top: 0; }
|
|
@@ -498,6 +499,7 @@
|
|
|
498
499
|
connect: 145_000,
|
|
499
500
|
configure: 145_000,
|
|
500
501
|
importJson: 145_000,
|
|
502
|
+
reconfigureConnection: 145_000,
|
|
501
503
|
installFromUrl: 145_000,
|
|
502
504
|
setEnabled: 130_000,
|
|
503
505
|
restoreSnapshot: 145_000,
|
|
@@ -1111,6 +1113,8 @@
|
|
|
1111
1113
|
|
|
1112
1114
|
function rowHtml(r) {
|
|
1113
1115
|
const key = esc(r.key);
|
|
1116
|
+
const canRename = r.canRename === true;
|
|
1117
|
+
const canEditConfiguration = r.canEditConfiguration === true;
|
|
1114
1118
|
const authMode = r.authMode === 'oauth' ? '<span class="badge auth-oauth">OAuth</span>' : `<span class="badge">${esc(r.authMode || 'manual')}</span>`;
|
|
1115
1119
|
const healthLabels = { healthy: '已连接', unknown: '状态未知', configured: '已配置', reauth: '需重新授权', recovering: '自动重试中', degraded: '部分异常', unavailable: '连接异常', disabled: '已停用' };
|
|
1116
1120
|
const healthClass = r.connectionState === 'healthy' ? 'pass' : ['reauth', 'recovering', 'degraded'].includes(r.connectionState) ? 'partial' : r.connectionState === 'unavailable' ? 'fail' : '';
|
|
@@ -1131,6 +1135,8 @@
|
|
|
1131
1135
|
<input type="checkbox" ${enabled ? 'checked' : ''} onchange="window.__mcp.toggle('${key}', this.checked)">
|
|
1132
1136
|
<span class="slider"></span>
|
|
1133
1137
|
</label>
|
|
1138
|
+
${canEditConfiguration ? `<button class="btn ghost" type="button" onclick="window.__mcp.openEditor('${key}')">编辑配置</button>` : ''}
|
|
1139
|
+
${canRename ? `<button class="btn ghost" type="button" onclick="window.__mcp.openRename('${key}')">重命名</button>` : ''}
|
|
1134
1140
|
<button class="btn ghost" type="button" onclick="window.__mcp.openScope('${key}')">范围</button>
|
|
1135
1141
|
<button class="btn danger" onclick="window.__mcp.disconnect('${key}')">断开</button>
|
|
1136
1142
|
</div>`;
|
|
@@ -1244,6 +1250,36 @@
|
|
|
1244
1250
|
<div class="m-actions"><button class="btn ghost" type="button" onclick="window.__mcp.closeModal()">取消</button><button class="btn" type="button" onclick="window.__mcp.submitScopeChange()">预览并应用</button></div>`);
|
|
1245
1251
|
}
|
|
1246
1252
|
|
|
1253
|
+
function openRenameConnection(key) {
|
|
1254
|
+
const item = installedItems.find((candidate) => candidate.key === key);
|
|
1255
|
+
if (!item) { showToast('连接不存在,请刷新后重试', false); return; }
|
|
1256
|
+
if (item.canRename !== true) {
|
|
1257
|
+
showToast('市场连接器名称由目录维护', false);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
openModal('修改连接显示名', `
|
|
1261
|
+
<div class="form-note"><strong>只修改页面显示名</strong>当前连接不会中断;<code>serverName</code>、工具前缀、作用范围和凭据均保持不变。</div>
|
|
1262
|
+
<label for="rename-name">连接显示名(必填)</label><input id="rename-name" value="${esc(item.name)}" maxlength="80" autocomplete="off" />
|
|
1263
|
+
<label for="rename-server-name">serverName(工具前缀,不可修改)</label><input id="rename-server-name" value="${esc(item.serverName || '')}" disabled />
|
|
1264
|
+
<div class="inline-error" id="modal-error" tabindex="-1"></div>
|
|
1265
|
+
<div class="m-actions"><button class="btn ghost" type="button" onclick="window.__mcp.closeModal()">取消</button><button class="btn" type="button" onclick="window.__mcp.submitRename('${esc(key)}')">保存显示名</button></div>`);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async function openConnectionEditor(key) {
|
|
1269
|
+
const item = installedItems.find((candidate) => candidate.key === key);
|
|
1270
|
+
if (!item) { showToast('连接不存在,请刷新后重试', false); return; }
|
|
1271
|
+
const result = await call('editableConnectionConfig', { key });
|
|
1272
|
+
if (!result.ok) { showToast(result.message, false); return; }
|
|
1273
|
+
const action = item.enabled === false ? '保存配置' : '保存并重连';
|
|
1274
|
+
openModal(`编辑配置 · ${item.name}`, `
|
|
1275
|
+
<div class="form-note"><strong>安全编辑标准化 JSON</strong><code>serverName</code> 是连接身份,不可修改;原始排版、注释和字段别名不会被还原。<code><KEEP_EXISTING></code> 表示敏感值继续留在本机,可直接保留,或替换为新值;删除字段表示清除该项。</div>
|
|
1276
|
+
<div class="field-label-row"><label for="edit-connection-json">单连接 JSON</label><button class="text-btn" type="button" onclick="window.__mcp.formatEditJson()">格式化 JSON</button></div>
|
|
1277
|
+
<textarea id="edit-connection-json" rows="14" spellcheck="false">${esc(result.detail?.json || '')}</textarea>
|
|
1278
|
+
<div class="field-help">提交前会先验证新配置并创建快照。任何校验、启动或保存失败都会保留原连接;连接 key、作用范围与启停状态不变。</div>
|
|
1279
|
+
<div class="inline-error" id="modal-error" tabindex="-1"></div>
|
|
1280
|
+
<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>`);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1247
1283
|
function openAddConnection(mode = 'json', preset = {}) {
|
|
1248
1284
|
const server = (preset.servers || [{}])[0] || {};
|
|
1249
1285
|
if (mode === 'manual' && preset.id && ['bearer', 'api-key'].includes(preset.authMode)) {
|
|
@@ -1371,6 +1407,48 @@
|
|
|
1371
1407
|
if (connectorId) return window.__mcp.connect(connectorId);
|
|
1372
1408
|
},
|
|
1373
1409
|
openScope(key) { openScopeManager(key); },
|
|
1410
|
+
openRename(key) { openRenameConnection(key); },
|
|
1411
|
+
async openEditor(key) { await openConnectionEditor(key); },
|
|
1412
|
+
formatEditJson() {
|
|
1413
|
+
const input = $('#edit-connection-json');
|
|
1414
|
+
if (!input) return;
|
|
1415
|
+
try {
|
|
1416
|
+
input.value = JSON.stringify(JSON.parse(input.value), null, 2);
|
|
1417
|
+
$('#modal-error')?.classList.remove('show');
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
showModalError(`JSON 格式化失败: ${error.message}`);
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
async submitReconfigure(key) {
|
|
1423
|
+
const input = $('#edit-connection-json');
|
|
1424
|
+
if (!input?.value.trim()) { showModalError('连接配置不能为空'); return; }
|
|
1425
|
+
let json;
|
|
1426
|
+
try {
|
|
1427
|
+
json = JSON.stringify(JSON.parse(input.value), null, 2);
|
|
1428
|
+
input.value = json;
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
showModalError(`连接配置不是合法 JSON: ${error.message}`);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
setModalBusy(true, '正在校验并更新…');
|
|
1434
|
+
const result = await call('reconfigureConnection', { key, json });
|
|
1435
|
+
setModalBusy(false);
|
|
1436
|
+
if (!result.ok) { showModalError(result.message); return; }
|
|
1437
|
+
closeModal();
|
|
1438
|
+
showToast(result.message, true);
|
|
1439
|
+
await loadInstalled();
|
|
1440
|
+
},
|
|
1441
|
+
async submitRename(key) {
|
|
1442
|
+
const name = $('#rename-name')?.value.trim() || '';
|
|
1443
|
+
if (!name) { showModalError('连接显示名不能为空'); return; }
|
|
1444
|
+
setModalBusy(true);
|
|
1445
|
+
const result = await call('renameConnection', { key, name });
|
|
1446
|
+
setModalBusy(false);
|
|
1447
|
+
if (!result.ok) { showModalError(result.message); return; }
|
|
1448
|
+
closeModal();
|
|
1449
|
+
showToast(result.message, true);
|
|
1450
|
+
await loadInstalled();
|
|
1451
|
+
},
|
|
1374
1452
|
async submitScopeChange() {
|
|
1375
1453
|
if (!pendingScopeKey) return;
|
|
1376
1454
|
let target;
|