dsh-mcp-connector 0.2.40 → 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 +18 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/USER-GUIDE.md +5 -0
- package/lib/connection-editor.js +216 -0
- package/lib/index.js +58 -3
- package/lib/tools.js +38 -1
- package/lib/web.js +2 -0
- package/package.json +2 -2
- package/ui/index.html +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
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
|
+
|
|
7
23
|
## [0.2.40] - 2026-09-09
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -622,7 +638,8 @@
|
|
|
622
638
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
623
639
|
- iframe 消息校验同源和消息来源。
|
|
624
640
|
|
|
625
|
-
[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
|
|
626
643
|
[0.2.40]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.39...v0.2.40
|
|
627
644
|
[0.2.39]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.38...v0.2.39
|
|
628
645
|
[0.2.38]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.37...v0.2.38
|
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,7 @@ stdio 进程由 `@deepseek-ai/dsh-mcp-client` 管理,插件透传 `command`、
|
|
|
152
152
|
在“已安装”中可以:
|
|
153
153
|
|
|
154
154
|
- 查看连接状态和 Server 数量;
|
|
155
|
+
- 编辑自定义或 JSON 导入连接的标准化 JSON,并保存后重新连接;
|
|
155
156
|
- 修改自定义或 JSON 导入连接的显示名,无需先停用连接;
|
|
156
157
|
- 启用或停用连接;
|
|
157
158
|
- 重新授权 OAuth,或重新录入 Token/API Key;
|
|
@@ -162,6 +163,10 @@ OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端
|
|
|
162
163
|
|
|
163
164
|
“重命名”只修改用户看到的连接显示名,不会改变 connection key、`serverName`、`mcp__<serverName>__*` 工具前缀、连接范围、凭据或启停状态,也不会重新挂载 MCP Server。市场连接器的名称继续由公共目录维护。
|
|
164
165
|
|
|
166
|
+
“编辑配置”会展示一条标准化的 `connections` JSON,不保证恢复最初导入文本的排版、注释或字段别名。`serverName` 与 connection key 共同标识现有连接,因此不能在此修改;如需新的工具前缀,请另建连接。Token/API Key、Header/env 值、stdio 参数、本地目录以及可能含凭据的 URL 不会返回页面,而是显示 `<KEEP_EXISTING>`:保留标记表示继续使用本机原值,替换标记可录入新值,删除对应字段可清除该项。
|
|
167
|
+
|
|
168
|
+
启用中的连接点击“保存并重连”后会先验证新配置、创建快照,再原子更新 Host 与本机记录;任何校验、启动或持久化失败都会保留原连接。停用连接可以保存配置但仍保持停用。连接范围和启停状态均不会被 JSON 静默改变。市场/OAuth 连接继续使用卡片的“配置”或“重新授权”流程。
|
|
169
|
+
|
|
165
170
|
### 7.1 配置备份与恢复
|
|
166
171
|
|
|
167
172
|
“已安装”页的“配置备份”提供两种互补能力:
|
|
@@ -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
|
@@ -22,8 +22,6 @@ import {
|
|
|
22
22
|
DEFAULT_REFRESH_RETRY_BASE_MS,
|
|
23
23
|
DEFAULT_REFRESH_RETRY_MAX_MS,
|
|
24
24
|
DEFAULT_CATALOG_TTL_MS,
|
|
25
|
-
CUSTOM_CONNECTOR_ID,
|
|
26
|
-
JSON_CONNECTOR_ID,
|
|
27
25
|
} from './constants.js';
|
|
28
26
|
import { defineConnectorDomain, ConnectionStore, GrantStore, CatalogStore, SnapshotStore, GovernanceStore, ConnectionScopeStore } from './stores.js';
|
|
29
27
|
import { normalizeConnectorDescriptor, normalizeConnectionRecord } from './schema.js';
|
|
@@ -31,6 +29,11 @@ import { loadBundledCatalog, fetchRemoteCatalogWithFallback, mergeCatalog, listC
|
|
|
31
29
|
import { describeOAuthAuthorizationError, oauthAuthorize } from './connectors/oauth-connector.js';
|
|
32
30
|
import { buildManualRecord } from './connectors/manual-connector.js';
|
|
33
31
|
import { normalizeJsonImport } from './connectors/json-connector.js';
|
|
32
|
+
import {
|
|
33
|
+
connectionConfigurationEditable,
|
|
34
|
+
editableConnectionConfiguration,
|
|
35
|
+
normalizeConnectionReconfiguration,
|
|
36
|
+
} from './connection-editor.js';
|
|
34
37
|
import { discoverServerMetadata, refreshAccessToken, revokeRefreshToken, extractTokenResources } from './oauth.js';
|
|
35
38
|
import { provision, disable, remove, entryIdFor } from './mcp-provision.js';
|
|
36
39
|
import { classifyConnectionError, validateConnectionRecords } from './mcp-validation.js';
|
|
@@ -578,7 +581,7 @@ export async function apply(ctx, config) {
|
|
|
578
581
|
}
|
|
579
582
|
|
|
580
583
|
function connectionDisplayNameEditable(record) {
|
|
581
|
-
return
|
|
584
|
+
return connectionConfigurationEditable(record);
|
|
582
585
|
}
|
|
583
586
|
|
|
584
587
|
async function persistRecord(record) {
|
|
@@ -2207,6 +2210,7 @@ export async function apply(ctx, config) {
|
|
|
2207
2210
|
connectorId: record.connectorId,
|
|
2208
2211
|
kind: record.kind,
|
|
2209
2212
|
canRename: connectionDisplayNameEditable(record),
|
|
2213
|
+
canEditConfiguration: connectionConfigurationEditable(record),
|
|
2210
2214
|
serverName: record.serverName,
|
|
2211
2215
|
url: record.url,
|
|
2212
2216
|
transport: record.transport,
|
|
@@ -2413,6 +2417,57 @@ export async function apply(ctx, config) {
|
|
|
2413
2417
|
}
|
|
2414
2418
|
},
|
|
2415
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
|
+
|
|
2416
2471
|
async setEnabled(key, enabled) {
|
|
2417
2472
|
const record = state.connections.get(key);
|
|
2418
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',
|
|
@@ -488,6 +488,43 @@ export function registerTools(ctx, api) {
|
|
|
488
488
|
},
|
|
489
489
|
});
|
|
490
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
|
+
|
|
491
528
|
reg({
|
|
492
529
|
name: 'mcp_connector_disconnect',
|
|
493
530
|
description: '断开并移除一条 MCP 连接;OAuth 连接会按引用计数撤销 refresh_token(无其他连接引用该授权时)。',
|
package/lib/web.js
CHANGED
|
@@ -42,6 +42,8 @@ const API_WHITELIST = new Map([
|
|
|
42
42
|
['restoreSnapshot', (api, p) => api.restoreSnapshot(p?.snapshotId)],
|
|
43
43
|
['installFromUrl', (api, p) => api.installFromUrl(p?.url, p ?? {})],
|
|
44
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)],
|
|
45
47
|
['disconnect', (api, p) => api.disconnect(p?.key)],
|
|
46
48
|
['setEnabled', (api, p) => api.setEnabled(p?.key, p?.enabled !== false)],
|
|
47
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,
|
|
@@ -1112,6 +1114,7 @@
|
|
|
1112
1114
|
function rowHtml(r) {
|
|
1113
1115
|
const key = esc(r.key);
|
|
1114
1116
|
const canRename = r.canRename === true;
|
|
1117
|
+
const canEditConfiguration = r.canEditConfiguration === true;
|
|
1115
1118
|
const authMode = r.authMode === 'oauth' ? '<span class="badge auth-oauth">OAuth</span>' : `<span class="badge">${esc(r.authMode || 'manual')}</span>`;
|
|
1116
1119
|
const healthLabels = { healthy: '已连接', unknown: '状态未知', configured: '已配置', reauth: '需重新授权', recovering: '自动重试中', degraded: '部分异常', unavailable: '连接异常', disabled: '已停用' };
|
|
1117
1120
|
const healthClass = r.connectionState === 'healthy' ? 'pass' : ['reauth', 'recovering', 'degraded'].includes(r.connectionState) ? 'partial' : r.connectionState === 'unavailable' ? 'fail' : '';
|
|
@@ -1132,6 +1135,7 @@
|
|
|
1132
1135
|
<input type="checkbox" ${enabled ? 'checked' : ''} onchange="window.__mcp.toggle('${key}', this.checked)">
|
|
1133
1136
|
<span class="slider"></span>
|
|
1134
1137
|
</label>
|
|
1138
|
+
${canEditConfiguration ? `<button class="btn ghost" type="button" onclick="window.__mcp.openEditor('${key}')">编辑配置</button>` : ''}
|
|
1135
1139
|
${canRename ? `<button class="btn ghost" type="button" onclick="window.__mcp.openRename('${key}')">重命名</button>` : ''}
|
|
1136
1140
|
<button class="btn ghost" type="button" onclick="window.__mcp.openScope('${key}')">范围</button>
|
|
1137
1141
|
<button class="btn danger" onclick="window.__mcp.disconnect('${key}')">断开</button>
|
|
@@ -1261,6 +1265,21 @@
|
|
|
1261
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>`);
|
|
1262
1266
|
}
|
|
1263
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
|
+
|
|
1264
1283
|
function openAddConnection(mode = 'json', preset = {}) {
|
|
1265
1284
|
const server = (preset.servers || [{}])[0] || {};
|
|
1266
1285
|
if (mode === 'manual' && preset.id && ['bearer', 'api-key'].includes(preset.authMode)) {
|
|
@@ -1389,6 +1408,36 @@
|
|
|
1389
1408
|
},
|
|
1390
1409
|
openScope(key) { openScopeManager(key); },
|
|
1391
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
|
+
},
|
|
1392
1441
|
async submitRename(key) {
|
|
1393
1442
|
const name = $('#rename-name')?.value.trim() || '';
|
|
1394
1443
|
if (!name) { showModalError('连接显示名不能为空'); return; }
|