dsh-mcp-connector 0.2.21 → 0.2.22
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 +16 -1
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/STDIO-SUPPORT.md +2 -0
- package/docs/USER-GUIDE.md +3 -3
- package/lib/constants.js +2 -0
- package/lib/index.js +299 -51
- package/lib/mcp-provision.js +5 -5
- package/lib/mcp-validation.js +25 -3
- package/package.json +1 -1
- package/ui/index.html +28 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.22] - 2026-08-26
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- 首次连接改为等待 `dsh-mcp-client` 完成 MCP `initialize` 与首次 `tools/list`;启动失败或超时不再提前保存、增加已安装数量或显示“已连接”。
|
|
12
|
+
- stdio 健康状态与工具详情改用 Host 实际注册的 `mcp__<serverName>__*` 工具;未注册工具保持“已配置”并显示可重试诊断。
|
|
13
|
+
- 手动 HTTP、免鉴权市场连接与 JSON 导入统一在落库前执行连接校验;DNS、网络、鉴权或协议失败时保留原配置且不写入无效记录。
|
|
14
|
+
- 市场 Web 请求增加连接、健康检查与工具加载超时,避免详情页无限停留在“加载中”。
|
|
15
|
+
|
|
16
|
+
### Verification
|
|
17
|
+
|
|
18
|
+
- 新增无效 HTTP 不落库、stdio Host 启动失败回滚、Host 工具注册状态和前端有限超时回归测试。
|
|
19
|
+
|
|
7
20
|
## [0.2.21] - 2026-08-25
|
|
8
21
|
|
|
9
22
|
### Documentation
|
|
@@ -359,7 +372,9 @@
|
|
|
359
372
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
360
373
|
- iframe 消息校验同源和消息来源。
|
|
361
374
|
|
|
362
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
375
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.22...HEAD
|
|
376
|
+
[0.2.22]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.21...v0.2.22
|
|
377
|
+
[0.2.21]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.20...v0.2.21
|
|
363
378
|
[0.2.20]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.19...v0.2.20
|
|
364
379
|
[0.2.19]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.18...v0.2.19
|
|
365
380
|
[0.2.18]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.17...v0.2.18
|
package/README.en.md
CHANGED
|
@@ -111,7 +111,7 @@ npm run dev:ui
|
|
|
111
111
|
|
|
112
112
|
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.
|
|
113
113
|
|
|
114
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
114
|
+
The current public version is [`dsh-mcp-connector@0.2.22`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.22](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.22).
|
|
115
115
|
|
|
116
116
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
117
117
|
|
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ npm run dev:ui
|
|
|
106
106
|
|
|
107
107
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
108
108
|
|
|
109
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
109
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.22`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.22](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.22)。
|
|
110
110
|
|
|
111
111
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
112
112
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/docs/STDIO-SUPPORT.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
> 结论:**低成本补齐**——`@deepseek-ai/dsh-mcp-client` 已原生支持 stdio,我们只需在 schema + provisioning 两层「开闸透传」。
|
|
5
5
|
|
|
6
6
|
> **实施状态(2026-08-23)**:本文方案已经落地。schema、手工配置、JSON 导入、provisioning、Registry Schema、目录安全审计、健康状态、详情提示和自动测试均已完成;下文保留实施前的代码位置与改动建议,作为架构决策记录。
|
|
7
|
+
>
|
|
8
|
+
> **就绪语义加固(2026-08-26)**:用户主动连接时显式启用 `failOnStartupError`,必须等待 Host 完成首次 `initialize + tools/list` 后才持久化;健康检查与详情页改读 Host 实际注册的 `mcp__<serverName>__*` 工具。启动失败、进程退出或超时不再提前显示“已连接”。依赖默认值仍为 `false`,下文对应代码仅作为依赖能力说明。
|
|
7
9
|
> 状态:已完成(v0.2.12)
|
|
8
10
|
|
|
9
11
|
---
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -69,7 +69,7 @@ dsh web
|
|
|
69
69
|
|
|
70
70
|
Bearer/API Key 连接器会先执行 MCP `initialize` 验证。所有 Server 通过后,凭据才会保存并出现在“已安装”中;验证失败不会写入新凭据。
|
|
71
71
|
|
|
72
|
-
stdio 市场卡片也可能显示一个或多个凭据字段,例如 API Token、区域或租户标识。卡片目录只声明字段名称及其环境变量映射;提交后,真实值仅保存到 DSH 本机连接记录,并由插件注入该 stdio 进程的 `env
|
|
72
|
+
stdio 市场卡片也可能显示一个或多个凭据字段,例如 API Token、区域或租户标识。卡片目录只声明字段名称及其环境变量映射;提交后,真实值仅保存到 DSH 本机连接记录,并由插件注入该 stdio 进程的 `env`。市场、状态页和日志不会返回这些值。插件会等待 Host 完成首次 MCP 初始化与工具同步后再保存连接;失败或超时不会增加已安装数量,卡片也不会提前显示“已连接”。
|
|
73
73
|
|
|
74
74
|
OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据发现。动态客户端注册既支持无需客户端密钥的 `none`,也支持服务商签发密钥的 `client_secret_post` 与 `client_secret_basic`。客户端密钥仅与 OAuth Grant 一同保存在 DSH 本机,用于换取、刷新和撤销 Token;插件不会要求用户把 OAuth Token 或客户端密钥复制到聊天中。
|
|
75
75
|
|
|
@@ -121,7 +121,7 @@ OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据
|
|
|
121
121
|
- **HTTP**:填写名称、HTTPS MCP URL、可选 Header 和传输方式。
|
|
122
122
|
- **stdio**:填写本机命令、参数、环境变量和可选工作目录。
|
|
123
123
|
|
|
124
|
-
stdio 进程由 `@deepseek-ai/dsh-mcp-client`
|
|
124
|
+
stdio 进程由 `@deepseek-ai/dsh-mcp-client` 管理,插件透传 `command`、`args`、`env`、`cwd`,并等待 Host 完成首次 MCP 初始化和工具同步。stdio 会以当前用户权限启动本机进程,只运行你信任的软件包和命令。手动 HTTP 配置也会在保存前执行 `initialize` 校验;验证失败时表单内容仍保留,且不会生成无效连接记录。
|
|
125
125
|
|
|
126
126
|
### 6.3 市场卡片 URL
|
|
127
127
|
|
|
@@ -165,7 +165,7 @@ OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端
|
|
|
165
165
|
|
|
166
166
|
### stdio 启动失败
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
页面会在有限时间内结束等待,并区分命令不存在、进程退出、初始化失败或启动超时。先在终端确认命令本身可执行、软件包可信、Node/运行时版本满足要求,并检查 `cwd`、参数和环境变量;随后查看 Host 日志并点击“重新检查”。不要把本机凭据写入公开 Registry descriptor。
|
|
169
169
|
|
|
170
170
|
### 如何反馈问题
|
|
171
171
|
|
package/lib/constants.js
CHANGED
|
@@ -27,6 +27,8 @@ export const DEFAULT_CATALOG_FALLBACK_URLS = [
|
|
|
27
27
|
|
|
28
28
|
/** 请求 / 刷新默认值 */
|
|
29
29
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
30
|
+
/** 首次启动 MCP Server 并完成 initialize + tools/list 的最长等待时间 */
|
|
31
|
+
export const DEFAULT_STARTUP_TIMEOUT_MS = 120_000;
|
|
30
32
|
export const DEFAULT_REFRESH_SKEW_MS = 300_000;
|
|
31
33
|
export const DEFAULT_CATALOG_TTL_MS = 3_600_000;
|
|
32
34
|
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DEFAULT_CATALOG_URL,
|
|
16
16
|
DEFAULT_CATALOG_FALLBACK_URLS,
|
|
17
17
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
18
|
+
DEFAULT_STARTUP_TIMEOUT_MS,
|
|
18
19
|
DEFAULT_REFRESH_SKEW_MS,
|
|
19
20
|
DEFAULT_CATALOG_TTL_MS,
|
|
20
21
|
} from './constants.js';
|
|
@@ -52,6 +53,8 @@ export const Config = z.object({
|
|
|
52
53
|
callbackTimeoutMs: z.number().default(300_000),
|
|
53
54
|
/** 网络请求超时(ms) */
|
|
54
55
|
requestTimeoutMs: z.number().default(DEFAULT_REQUEST_TIMEOUT_MS),
|
|
56
|
+
/** 首次启动并完成 MCP initialize + tools/list 的超时(ms) */
|
|
57
|
+
startupTimeoutMs: z.number().default(DEFAULT_STARTUP_TIMEOUT_MS),
|
|
55
58
|
/** 提前刷新阈值(ms) */
|
|
56
59
|
refreshSkewMs: z.number().default(DEFAULT_REFRESH_SKEW_MS),
|
|
57
60
|
/** 是否自动打开浏览器(false = 仅打印授权 URL) */
|
|
@@ -251,18 +254,123 @@ export async function apply(ctx, config) {
|
|
|
251
254
|
|
|
252
255
|
/* ───────────────────────── 连接记录落库 / 挂载 ───────────────────────── */
|
|
253
256
|
|
|
257
|
+
function clearRecordHealth(record) {
|
|
258
|
+
if (record?.connectorId) state.health.delete(record.connectorId);
|
|
259
|
+
}
|
|
260
|
+
|
|
254
261
|
async function persistRecord(record) {
|
|
255
|
-
|
|
256
|
-
if (record.connectorId) state.health.delete(record.connectorId);
|
|
262
|
+
// 存储成功后才更新内存,避免落盘失败却在当前进程显示为“已安装”。
|
|
257
263
|
if (config.persistSecrets) await connectionStore.put(record);
|
|
264
|
+
state.connections.set(record.key, record);
|
|
265
|
+
clearRecordHealth(record);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function restorePersistedRecord(key, original) {
|
|
269
|
+
const current = state.connections.get(key);
|
|
270
|
+
if (config.persistSecrets) {
|
|
271
|
+
if (original) await connectionStore.put(original);
|
|
272
|
+
else await connectionStore.delete(key);
|
|
273
|
+
}
|
|
274
|
+
if (original) state.connections.set(key, original);
|
|
275
|
+
else state.connections.delete(key);
|
|
276
|
+
clearRecordHealth(current);
|
|
277
|
+
clearRecordHealth(original);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function restoreProvisionedEntry(record, original) {
|
|
281
|
+
if (original) await provision(ctx, config, original, grantMap(), { failOnStartupError: false });
|
|
282
|
+
else await remove(ctx, config, record);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function provisionUntilReady(record, original, { strictStartup = true } = {}) {
|
|
286
|
+
if (!strictStartup) {
|
|
287
|
+
return provision(ctx, config, record, grantMap(), { failOnStartupError: false });
|
|
288
|
+
}
|
|
289
|
+
const startupTimeoutMs = Math.max(1_000, Number(config.startupTimeoutMs) || DEFAULT_STARTUP_TIMEOUT_MS);
|
|
290
|
+
const timeoutError = Object.assign(
|
|
291
|
+
new Error(`MCP Server 启动与工具同步超过 ${Math.ceil(startupTimeoutMs / 1000)} 秒`),
|
|
292
|
+
{ code: 'ETIMEDOUT' },
|
|
293
|
+
);
|
|
294
|
+
let timer;
|
|
295
|
+
const task = provision(ctx, config, record, grantMap(), { failOnStartupError: true });
|
|
296
|
+
const deadline = new Promise((_, reject) => {
|
|
297
|
+
timer = setTimeout(() => reject(timeoutError), startupTimeoutMs);
|
|
298
|
+
timer.unref?.();
|
|
299
|
+
});
|
|
300
|
+
try {
|
|
301
|
+
return await Promise.race([task, deadline]);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error === timeoutError) {
|
|
304
|
+
// loader 可能仍在等待 SDK 请求;它最终成功时必须恢复旧条目/移除新条目,绝不迟到落库。
|
|
305
|
+
task.then(
|
|
306
|
+
() => restoreProvisionedEntry(record, original).catch((rollbackError) => {
|
|
307
|
+
logger.error(`late startup rollback "${record.key}" failed: ${rollbackError.message}`);
|
|
308
|
+
}),
|
|
309
|
+
() => {},
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
} finally {
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function rollbackProvisionedRecords(records, originals) {
|
|
319
|
+
const failures = [];
|
|
320
|
+
for (const record of [...records].reverse()) {
|
|
321
|
+
try {
|
|
322
|
+
await restoreProvisionedEntry(record, originals.get(record.key));
|
|
323
|
+
} catch (error) {
|
|
324
|
+
failures.push(error);
|
|
325
|
+
logger.error(`connection rollback "${record.key}" failed: ${error.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return failures;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function commitConnectionRecords(records, { strictStartup = true } = {}) {
|
|
332
|
+
const originals = new Map(records.map((record) => [record.key, state.connections.get(record.key)]));
|
|
333
|
+
const staged = records.map((record) => {
|
|
334
|
+
const existing = originals.get(record.key);
|
|
335
|
+
return existing ? { ...existing, ...record, updatedAt: Date.now() } : record;
|
|
336
|
+
});
|
|
337
|
+
const provisioned = [];
|
|
338
|
+
let activeRecord;
|
|
339
|
+
try {
|
|
340
|
+
for (const record of staged) {
|
|
341
|
+
activeRecord = record;
|
|
342
|
+
await provisionUntilReady(record, originals.get(record.key), { strictStartup });
|
|
343
|
+
provisioned.push(record);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
await rollbackProvisionedRecords(provisioned, originals);
|
|
347
|
+
const wrapped = new Error(`MCP Server "${activeRecord?.serverName ?? activeRecord?.key ?? 'unknown'}" 未完成启动`, { cause: error });
|
|
348
|
+
wrapped.connectionRecord = activeRecord;
|
|
349
|
+
throw wrapped;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
for (const record of staged) await persistRecord(record);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
const rollbackErrors = await rollbackProvisionedRecords(provisioned, originals);
|
|
356
|
+
for (const record of staged) {
|
|
357
|
+
try {
|
|
358
|
+
await restorePersistedRecord(record.key, originals.get(record.key));
|
|
359
|
+
} catch (rollbackError) {
|
|
360
|
+
rollbackErrors.push(rollbackError);
|
|
361
|
+
logger.error(`connection storage rollback "${record.key}" failed: ${rollbackError.message}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const wrapped = new Error('连接记录持久化失败,已回滚 Host 条目', { cause: error });
|
|
365
|
+
wrapped.connectionRecord = activeRecord;
|
|
366
|
+
wrapped.rollbackErrors = rollbackErrors;
|
|
367
|
+
throw wrapped;
|
|
368
|
+
}
|
|
369
|
+
return staged;
|
|
258
370
|
}
|
|
259
371
|
|
|
260
|
-
async function upsertRecord(record) {
|
|
261
|
-
|
|
262
|
-
const merged = existing ? { ...existing, ...record, updatedAt: Date.now() } : record;
|
|
263
|
-
await persistRecord(merged);
|
|
264
|
-
await provision(ctx, config, merged, grantMap());
|
|
265
|
-
return merged;
|
|
372
|
+
async function upsertRecord(record, options) {
|
|
373
|
+
return (await commitConnectionRecords([record], options))[0];
|
|
266
374
|
}
|
|
267
375
|
|
|
268
376
|
function grantFailure(record) {
|
|
@@ -278,16 +386,70 @@ export async function apply(ctx, config) {
|
|
|
278
386
|
return null;
|
|
279
387
|
}
|
|
280
388
|
|
|
389
|
+
function registeredStdioTools(record) {
|
|
390
|
+
if (typeof ctx.tools?.schemas !== 'function') {
|
|
391
|
+
return { supported: false, tools: [] };
|
|
392
|
+
}
|
|
393
|
+
const prefix = `mcp__${record.serverName}__`;
|
|
394
|
+
try {
|
|
395
|
+
const tools = ctx.tools.schemas()
|
|
396
|
+
.filter((schema) => typeof schema?.name === 'string' && schema.name.startsWith(prefix))
|
|
397
|
+
.map((schema) => ({
|
|
398
|
+
name: schema.name.slice(prefix.length),
|
|
399
|
+
publicName: schema.name,
|
|
400
|
+
title: schema.title || schema.name.slice(prefix.length),
|
|
401
|
+
description: schema.description || '',
|
|
402
|
+
}));
|
|
403
|
+
return { supported: true, tools };
|
|
404
|
+
} catch (error) {
|
|
405
|
+
logger.warn(`read registered tools for "${record.serverName}" failed: ${error.message}`);
|
|
406
|
+
return { supported: true, tools: [], error };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function readinessResults(records, validationResults = []) {
|
|
411
|
+
return records.map((record, index) => {
|
|
412
|
+
const validated = validationResults[index];
|
|
413
|
+
return {
|
|
414
|
+
...(validated ?? {}),
|
|
415
|
+
ok: true,
|
|
416
|
+
kind: 'connected',
|
|
417
|
+
serverKey: record.serverKey,
|
|
418
|
+
serverName: record.serverName,
|
|
419
|
+
message: record.transport === 'stdio'
|
|
420
|
+
? 'Host 已完成 stdio MCP 初始化与首次工具同步'
|
|
421
|
+
: validated?.message,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function connectionSetupFailure(error, prefix = '连接失败') {
|
|
427
|
+
const record = error?.connectionRecord;
|
|
428
|
+
const classified = classifyConnectionError(error, { transport: record?.transport });
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
message: `${prefix}:${classified.message}。未保存连接,请修正后重试。`,
|
|
432
|
+
detail: {
|
|
433
|
+
kind: classified.kind,
|
|
434
|
+
serverKey: record?.serverKey,
|
|
435
|
+
serverName: record?.serverName,
|
|
436
|
+
...(classified.exitCode !== undefined ? { exitCode: classified.exitCode } : {}),
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
281
441
|
function healthSummary(connectorId, records, results, checkedAt = Date.now()) {
|
|
282
442
|
const enabledRecords = records.filter((record) => record.enabled !== false);
|
|
283
|
-
const availableServers = results.filter((result) => result.ok).length;
|
|
284
|
-
const
|
|
443
|
+
const availableServers = results.filter((result) => result.ok && result.kind !== 'managed').length;
|
|
444
|
+
const pendingServers = results.filter((result) => result.ok && result.kind === 'managed').length;
|
|
445
|
+
const failedServers = results.filter((result) => !result.ok).length;
|
|
285
446
|
const authFailures = results.filter((result) => !result.ok && result.kind === 'auth').length;
|
|
286
447
|
let connectionState = 'configured';
|
|
287
448
|
if (records.length === 0) connectionState = 'disconnected';
|
|
288
449
|
else if (enabledRecords.length === 0) connectionState = 'disabled';
|
|
289
|
-
else if (failedServers === 0 && availableServers > 0) connectionState = 'healthy';
|
|
290
|
-
else if (availableServers > 0) connectionState = 'degraded';
|
|
450
|
+
else if (failedServers === 0 && pendingServers === 0 && availableServers > 0) connectionState = 'healthy';
|
|
451
|
+
else if (availableServers > 0 && (failedServers > 0 || pendingServers > 0)) connectionState = 'degraded';
|
|
452
|
+
else if (pendingServers > 0 && failedServers === 0) connectionState = 'configured';
|
|
291
453
|
else if (authFailures > 0) connectionState = 'reauth';
|
|
292
454
|
else if (failedServers > 0) connectionState = 'unavailable';
|
|
293
455
|
const labels = {
|
|
@@ -301,6 +463,7 @@ export async function apply(ctx, config) {
|
|
|
301
463
|
configuredServers: records.length,
|
|
302
464
|
enabledServers: enabledRecords.length,
|
|
303
465
|
availableServers,
|
|
466
|
+
pendingServers,
|
|
304
467
|
failedServers,
|
|
305
468
|
checkedAt,
|
|
306
469
|
results,
|
|
@@ -451,7 +614,7 @@ export async function apply(ctx, config) {
|
|
|
451
614
|
state.grants.set(grantKey, { grant, timer: undefined, refreshPromise: null, needsReauth: false });
|
|
452
615
|
if (config.persistSecrets) await grantStore.put(grant);
|
|
453
616
|
|
|
454
|
-
const
|
|
617
|
+
const records = [];
|
|
455
618
|
for (const sk of authz.grantedKeys) {
|
|
456
619
|
const server = connector.servers.find((s) => s.serverKey === sk);
|
|
457
620
|
if (!server) continue;
|
|
@@ -470,9 +633,11 @@ export async function apply(ctx, config) {
|
|
|
470
633
|
createdAt: Date.now(),
|
|
471
634
|
updatedAt: Date.now(),
|
|
472
635
|
};
|
|
473
|
-
|
|
474
|
-
created.push(record.key);
|
|
636
|
+
records.push(record);
|
|
475
637
|
}
|
|
638
|
+
const committed = await commitConnectionRecords(records);
|
|
639
|
+
const created = committed.map((record) => record.key);
|
|
640
|
+
cacheHealth(connector.id, committed, readinessResults(committed));
|
|
476
641
|
scheduleRefresh(grantKey);
|
|
477
642
|
let retiredGrantCount = 0;
|
|
478
643
|
for (const previousKey of previousGrantKeys) {
|
|
@@ -486,7 +651,9 @@ export async function apply(ctx, config) {
|
|
|
486
651
|
} catch (error) {
|
|
487
652
|
logger.error(`oauth connect failed: ${error.message}`);
|
|
488
653
|
await pruneOrphanGrants();
|
|
489
|
-
return
|
|
654
|
+
return error?.connectionRecord
|
|
655
|
+
? connectionSetupFailure(error)
|
|
656
|
+
: { ok: false, message: `连接失败: ${error.message}` };
|
|
490
657
|
}
|
|
491
658
|
})();
|
|
492
659
|
state.oauthConnects.set(connector.id, promise);
|
|
@@ -518,8 +685,15 @@ export async function apply(ctx, config) {
|
|
|
518
685
|
createdAt: Date.now(),
|
|
519
686
|
updatedAt: Date.now(),
|
|
520
687
|
};
|
|
521
|
-
await
|
|
522
|
-
return {
|
|
688
|
+
const validation = await validateConnectionRecords([record], { timeoutMs: config.requestTimeoutMs });
|
|
689
|
+
if (!validation.ok) return { ...validation, detail: { connectorId: connector.id, ...validation.detail } };
|
|
690
|
+
try {
|
|
691
|
+
const [committed] = await commitConnectionRecords([record]);
|
|
692
|
+
cacheHealth(connector.id, [committed], readinessResults([committed], validation.detail.results));
|
|
693
|
+
return { ok: true, message: `已连接 "${connector.name}"`, detail: { key: committed.key } };
|
|
694
|
+
} catch (error) {
|
|
695
|
+
return connectionSetupFailure(error);
|
|
696
|
+
}
|
|
523
697
|
}
|
|
524
698
|
|
|
525
699
|
// bearer / api-key 型:目录不含密钥,引导自定义配置
|
|
@@ -574,12 +748,9 @@ export async function apply(ctx, config) {
|
|
|
574
748
|
const validation = await validateConnectionRecords(records, { timeoutMs: config.requestTimeoutMs });
|
|
575
749
|
if (!validation.ok) return { ...validation, detail: { connectorId: connector.id, ...validation.detail } };
|
|
576
750
|
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
created.push(record.key);
|
|
581
|
-
}
|
|
582
|
-
cacheHealth(connector.id, records, validation.detail.results);
|
|
751
|
+
const committed = await commitConnectionRecords(records);
|
|
752
|
+
const created = committed.map((record) => record.key);
|
|
753
|
+
cacheHealth(connector.id, committed, readinessResults(committed, validation.detail.results));
|
|
583
754
|
return {
|
|
584
755
|
ok: true,
|
|
585
756
|
message: `已配置并连接 "${connector.name}"(${created.length} 个 MCP Server)`,
|
|
@@ -587,20 +758,39 @@ export async function apply(ctx, config) {
|
|
|
587
758
|
};
|
|
588
759
|
}
|
|
589
760
|
const record = buildManualRecord(params);
|
|
590
|
-
await
|
|
591
|
-
|
|
761
|
+
const validation = await validateConnectionRecords([record], { timeoutMs: config.requestTimeoutMs });
|
|
762
|
+
if (!validation.ok) return validation;
|
|
763
|
+
const [committed] = await commitConnectionRecords([record]);
|
|
764
|
+
cacheHealth(committed.connectorId, [committed], readinessResults([committed], validation.detail.results));
|
|
765
|
+
return { ok: true, message: `已配置并连接 "${committed.name}"(serverName=${committed.serverName})`, detail: { key: committed.key, serverName: committed.serverName } };
|
|
592
766
|
} catch (error) {
|
|
593
|
-
return
|
|
767
|
+
return error?.connectionRecord
|
|
768
|
+
? connectionSetupFailure(error, '配置失败')
|
|
769
|
+
: { ok: false, message: `配置失败: ${error.message}` };
|
|
594
770
|
}
|
|
595
771
|
},
|
|
596
772
|
|
|
597
773
|
async importJson(json) {
|
|
598
774
|
try {
|
|
599
775
|
const { records, skipped } = normalizeJsonImport(json);
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
776
|
+
const validation = await validateConnectionRecords(records, {
|
|
777
|
+
timeoutMs: config.requestTimeoutMs,
|
|
778
|
+
concurrency: 4,
|
|
779
|
+
});
|
|
780
|
+
if (!validation.ok) return validation;
|
|
781
|
+
const committed = await commitConnectionRecords(records);
|
|
782
|
+
const keys = committed.map((record) => record.key);
|
|
783
|
+
const grouped = new Map();
|
|
784
|
+
for (const record of committed) {
|
|
785
|
+
if (!grouped.has(record.connectorId)) grouped.set(record.connectorId, []);
|
|
786
|
+
grouped.get(record.connectorId).push(record);
|
|
787
|
+
}
|
|
788
|
+
for (const [connectorId, connectorRecords] of grouped) {
|
|
789
|
+
const results = connectorRecords.map((record) => {
|
|
790
|
+
const index = committed.findIndex((item) => item.key === record.key);
|
|
791
|
+
return readinessResults([record], [validation.detail.results[index]])[0];
|
|
792
|
+
});
|
|
793
|
+
cacheHealth(connectorId, connectorRecords, results);
|
|
604
794
|
}
|
|
605
795
|
const extra = skipped.length ? `;跳过:${skipped.join('、')}` : '';
|
|
606
796
|
return {
|
|
@@ -609,7 +799,9 @@ export async function apply(ctx, config) {
|
|
|
609
799
|
detail: { keys, skipped },
|
|
610
800
|
};
|
|
611
801
|
} catch (error) {
|
|
612
|
-
return
|
|
802
|
+
return error?.connectionRecord
|
|
803
|
+
? connectionSetupFailure(error, '导入失败')
|
|
804
|
+
: { ok: false, message: `导入失败: ${error.message}` };
|
|
613
805
|
}
|
|
614
806
|
},
|
|
615
807
|
|
|
@@ -667,7 +859,9 @@ export async function apply(ctx, config) {
|
|
|
667
859
|
authMode: record.auth?.mode ?? 'none',
|
|
668
860
|
grant: grantInfo,
|
|
669
861
|
lastError: record.lastError ?? null,
|
|
670
|
-
connectionState: recordHealth?.ok
|
|
862
|
+
connectionState: recordHealth?.ok && recordHealth.kind !== 'managed'
|
|
863
|
+
? 'healthy'
|
|
864
|
+
: recordHealth ? health.connectionState : health?.connectionState ?? 'configured',
|
|
671
865
|
healthCheckedAt: health?.checkedAt ?? null,
|
|
672
866
|
healthMessage: recordHealth?.message ?? null,
|
|
673
867
|
});
|
|
@@ -694,7 +888,29 @@ export async function apply(ctx, config) {
|
|
|
694
888
|
for (const record of enabledRecords) {
|
|
695
889
|
const failure = grantFailure(record);
|
|
696
890
|
if (failure) immediateResults.push(failure);
|
|
697
|
-
else
|
|
891
|
+
else if (record.transport === 'stdio') {
|
|
892
|
+
const registered = registeredStdioTools(record);
|
|
893
|
+
if (registered.tools.length > 0) {
|
|
894
|
+
immediateResults.push({
|
|
895
|
+
ok: true,
|
|
896
|
+
kind: 'connected',
|
|
897
|
+
serverKey: record.serverKey,
|
|
898
|
+
serverName: record.serverName,
|
|
899
|
+
toolCount: registered.tools.length,
|
|
900
|
+
message: `Host 已注册 ${registered.tools.length} 个工具`,
|
|
901
|
+
});
|
|
902
|
+
} else {
|
|
903
|
+
immediateResults.push({
|
|
904
|
+
ok: true,
|
|
905
|
+
kind: 'managed',
|
|
906
|
+
serverKey: record.serverKey,
|
|
907
|
+
serverName: record.serverName,
|
|
908
|
+
message: registered.supported
|
|
909
|
+
? 'Host 尚未注册该 stdio Server 的工具'
|
|
910
|
+
: '当前 Host 无法读取 stdio 工具注册状态',
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
} else checkable.push(record);
|
|
698
914
|
}
|
|
699
915
|
let checkedResults = [];
|
|
700
916
|
if (checkable.length > 0) {
|
|
@@ -775,11 +991,26 @@ export async function apply(ctx, config) {
|
|
|
775
991
|
async setEnabled(key, enabled) {
|
|
776
992
|
const record = state.connections.get(key);
|
|
777
993
|
if (!record) return { ok: false, message: `连接 "${key}" 不存在` };
|
|
778
|
-
record
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
994
|
+
const next = { ...record, enabled: !!enabled, updatedAt: Date.now() };
|
|
995
|
+
if (next.enabled) {
|
|
996
|
+
try {
|
|
997
|
+
const [committed] = await commitConnectionRecords([next]);
|
|
998
|
+
if (committed.connectorId) cacheHealth(committed.connectorId, [committed], readinessResults([committed]));
|
|
999
|
+
return { ok: true, message: `已启用 "${committed.name}"` };
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
return connectionSetupFailure(error, '启用失败');
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
await disable(ctx, config, next);
|
|
1005
|
+
try {
|
|
1006
|
+
await persistRecord(next);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
await provision(ctx, config, record, grantMap(), { failOnStartupError: false }).catch((rollbackError) => {
|
|
1009
|
+
logger.error(`disable rollback "${record.key}" failed: ${rollbackError.message}`);
|
|
1010
|
+
});
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
1013
|
+
return { ok: true, message: `已停用 "${next.name}"` };
|
|
783
1014
|
},
|
|
784
1015
|
|
|
785
1016
|
async disconnect(key, signal) {
|
|
@@ -887,19 +1118,30 @@ export async function apply(ctx, config) {
|
|
|
887
1118
|
const servers = [];
|
|
888
1119
|
for (const record of records) {
|
|
889
1120
|
if (record.transport === 'stdio') {
|
|
1121
|
+
const registered = registeredStdioTools(record);
|
|
890
1122
|
const snapshot = (connector.toolsSnapshot ?? []).find((item) =>
|
|
891
1123
|
item.serverKey === record.serverKey || item.serverName === record.serverName);
|
|
1124
|
+
const liveTools = registered.tools;
|
|
892
1125
|
servers.push({
|
|
893
1126
|
serverKey: record.serverKey || record.name,
|
|
894
1127
|
serverName: record.serverName,
|
|
895
|
-
ok:
|
|
1128
|
+
ok: liveTools.length > 0,
|
|
896
1129
|
managed: true,
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1130
|
+
live: liveTools.length > 0,
|
|
1131
|
+
preview: liveTools.length === 0 && Boolean(snapshot),
|
|
1132
|
+
tools: liveTools.length > 0
|
|
1133
|
+
? liveTools
|
|
1134
|
+
: (snapshot?.tools ?? []).map((tool) => ({
|
|
1135
|
+
name: tool.name,
|
|
1136
|
+
title: tool.title || tool.name,
|
|
1137
|
+
description: tool.description || '',
|
|
1138
|
+
})),
|
|
1139
|
+
...(liveTools.length === 0 ? {
|
|
1140
|
+
errorKind: registered.supported ? 'startup' : 'host-version',
|
|
1141
|
+
error: registered.supported
|
|
1142
|
+
? 'Host 尚未注册该 stdio Server 的工具,请检查启动日志后重试'
|
|
1143
|
+
: '当前 DSH Host 不支持读取工具注册状态,请升级 Host 后重试',
|
|
1144
|
+
} : {}),
|
|
903
1145
|
});
|
|
904
1146
|
continue;
|
|
905
1147
|
}
|
|
@@ -938,7 +1180,7 @@ export async function apply(ctx, config) {
|
|
|
938
1180
|
const totalTools = servers.reduce((sum, s) => sum + (s.tools?.length || 0), 0);
|
|
939
1181
|
const availableServers = servers.filter((server) => server.ok).length;
|
|
940
1182
|
const failedServers = servers.length - availableServers;
|
|
941
|
-
const
|
|
1183
|
+
const pendingManagedServers = servers.filter((server) => server.managed && !server.ok).length;
|
|
942
1184
|
const health = cacheHealth(connectorId, records, servers.map((server) => server.ok
|
|
943
1185
|
? { ok: true, kind: 'connected', serverKey: server.serverKey, serverName: server.serverName }
|
|
944
1186
|
: { ok: false, kind: server.errorKind ?? 'network', serverKey: server.serverKey, serverName: server.serverName, message: server.error }));
|
|
@@ -948,10 +1190,16 @@ export async function apply(ctx, config) {
|
|
|
948
1190
|
ok: availableServers > 0,
|
|
949
1191
|
message: failedServers > 0
|
|
950
1192
|
? `工具加载失败:${failureSummary}${failedServers > 3 ? `;另有 ${failedServers - 3} 个 Server 不可用` : ''}${availableServers > 0 ? `;${availableServers} 个 Server 仍可用` : ''}`
|
|
951
|
-
:
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1193
|
+
: `共 ${servers.length} 个 server,${totalTools} 个工具`,
|
|
1194
|
+
detail: {
|
|
1195
|
+
connectorId,
|
|
1196
|
+
servers,
|
|
1197
|
+
totalTools,
|
|
1198
|
+
availableServers,
|
|
1199
|
+
failedServers,
|
|
1200
|
+
pendingManagedServers,
|
|
1201
|
+
connectionState: health.connectionState,
|
|
1202
|
+
},
|
|
955
1203
|
};
|
|
956
1204
|
},
|
|
957
1205
|
};
|
package/lib/mcp-provision.js
CHANGED
|
@@ -24,7 +24,7 @@ export function authHeaders(record, grants) {
|
|
|
24
24
|
return headers;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function buildEntryConfig(record, grants) {
|
|
27
|
+
export function buildEntryConfig(record, grants, { failOnStartupError = false } = {}) {
|
|
28
28
|
if (record.transport === 'stdio') {
|
|
29
29
|
return {
|
|
30
30
|
transport: 'stdio',
|
|
@@ -33,7 +33,7 @@ export function buildEntryConfig(record, grants) {
|
|
|
33
33
|
args: record.args ?? [],
|
|
34
34
|
env: record.env ?? {},
|
|
35
35
|
cwd: record.cwd || process.cwd(),
|
|
36
|
-
failOnStartupError
|
|
36
|
+
failOnStartupError,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
return {
|
|
@@ -41,7 +41,7 @@ export function buildEntryConfig(record, grants) {
|
|
|
41
41
|
serverName: record.serverName,
|
|
42
42
|
url: record.url,
|
|
43
43
|
headers: authHeaders(record, grants),
|
|
44
|
-
failOnStartupError
|
|
44
|
+
failOnStartupError,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -55,9 +55,9 @@ function hasEntry(ctx, id) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** 创建或更新一条 mcp-client 条目(幂等;serverName 不变则工具名不变) */
|
|
58
|
-
export async function provision(ctx, config, record, grants) {
|
|
58
|
+
export async function provision(ctx, config, record, grants, { failOnStartupError = false } = {}) {
|
|
59
59
|
const id = entryIdFor(config, record);
|
|
60
|
-
const entryConfig = buildEntryConfig(record, grants);
|
|
60
|
+
const entryConfig = buildEntryConfig(record, grants, { failOnStartupError });
|
|
61
61
|
const disabled = record.enabled === false;
|
|
62
62
|
if (hasEntry(ctx, id)) {
|
|
63
63
|
const existing = ctx.loader.resolve(id);
|
package/lib/mcp-validation.js
CHANGED
|
@@ -47,17 +47,22 @@ function parseMcpPayload(text, contentType = '') {
|
|
|
47
47
|
function errorChain(error) {
|
|
48
48
|
const parts = [];
|
|
49
49
|
let current = error;
|
|
50
|
-
for (let depth = 0; current && depth <
|
|
50
|
+
for (let depth = 0; current && depth < 8; depth += 1) {
|
|
51
51
|
parts.push(current.code, current.name, current.message);
|
|
52
52
|
current = current.cause;
|
|
53
53
|
}
|
|
54
54
|
return parts.filter(Boolean).join(' ');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function classifyConnectionError(error) {
|
|
57
|
+
export function classifyConnectionError(error, { transport } = {}) {
|
|
58
58
|
const text = errorChain(error);
|
|
59
59
|
if (/AbortError|timeout|timed out|ETIMEDOUT/i.test(text)) {
|
|
60
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
kind: 'timeout',
|
|
62
|
+
message: transport === 'stdio'
|
|
63
|
+
? '本地 MCP Server 启动或工具同步超时,请检查运行环境、命令和 Host 日志'
|
|
64
|
+
: '连接超时,请检查网络、VPN/专线或服务状态',
|
|
65
|
+
};
|
|
61
66
|
}
|
|
62
67
|
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(text)) {
|
|
63
68
|
return { kind: 'dns', message: '域名解析失败,请检查当前网络 DNS' };
|
|
@@ -68,6 +73,23 @@ export function classifyConnectionError(error) {
|
|
|
68
73
|
if (/ECONNREFUSED/i.test(text)) {
|
|
69
74
|
return { kind: 'refused', message: '目标服务拒绝连接,请检查 MCP URL 和服务状态' };
|
|
70
75
|
}
|
|
76
|
+
if (/ENOENT|command not found|spawn[^\n]*not found/i.test(text)) {
|
|
77
|
+
return { kind: 'process-not-found', message: '本地 MCP 启动命令不存在,请先安装所需运行时或检查 command 配置' };
|
|
78
|
+
}
|
|
79
|
+
if (/exit(?:ed)?\s+(?:with\s+)?code|process[^\n]*(?:closed|exited)|subprocess[^\n]*(?:closed|exited)/i.test(text)) {
|
|
80
|
+
const exitCode = text.match(/(?:exit(?:ed)?\s+(?:with\s+)?code|code)\s*[:=]?\s*(-?\d+)/i)?.[1];
|
|
81
|
+
return {
|
|
82
|
+
kind: 'process-exit',
|
|
83
|
+
message: `本地 MCP 进程在初始化期间退出${exitCode !== undefined ? `(退出码 ${exitCode})` : ''},请检查命令、参数和 Host 日志`,
|
|
84
|
+
...(exitCode !== undefined ? { exitCode: Number(exitCode) } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (/initial (?:connection|tool synchronization)|tool synchronization failed|initialize(?: handshake)? failed/i.test(text)) {
|
|
88
|
+
return { kind: 'startup', message: 'MCP 初始化或首次工具同步失败,请检查服务配置和 Host 日志' };
|
|
89
|
+
}
|
|
90
|
+
if (transport === 'stdio') {
|
|
91
|
+
return { kind: 'startup', message: '本地 MCP Server 启动或初始化失败,请检查命令、参数、运行环境和 Host 日志' };
|
|
92
|
+
}
|
|
71
93
|
return { kind: 'network', message: '无法连接 MCP 服务,请检查网络和 MCP URL' };
|
|
72
94
|
}
|
|
73
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"description": "General-purpose MCP connector, connection manager, plugin extension, and integration marketplace for DeepSeek Harness, initiated and maintained by Qichacha/QCC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/ui/index.html
CHANGED
|
@@ -455,6 +455,16 @@
|
|
|
455
455
|
<script>
|
|
456
456
|
(function () {
|
|
457
457
|
const API = '/mcp-connector/api';
|
|
458
|
+
const API_TIMEOUTS = {
|
|
459
|
+
connect: 145_000,
|
|
460
|
+
configure: 145_000,
|
|
461
|
+
importJson: 145_000,
|
|
462
|
+
installFromUrl: 145_000,
|
|
463
|
+
setEnabled: 130_000,
|
|
464
|
+
toolsList: 20_000,
|
|
465
|
+
healthCheck: 10_000,
|
|
466
|
+
};
|
|
467
|
+
const DEFAULT_API_TIMEOUT_MS = 30_000;
|
|
458
468
|
const $ = (sel) => document.querySelector(sel);
|
|
459
469
|
const main = $('#main');
|
|
460
470
|
const toast = $('#toast');
|
|
@@ -505,18 +515,31 @@
|
|
|
505
515
|
let promptRequestTimer = null;
|
|
506
516
|
let initialHealthCheckStarted = false;
|
|
507
517
|
|
|
508
|
-
async function call(method, params) {
|
|
518
|
+
async function call(method, params, { timeoutMs: timeoutOverride } = {}) {
|
|
519
|
+
const controller = new AbortController();
|
|
520
|
+
const timeoutMs = timeoutOverride || API_TIMEOUTS[method] || DEFAULT_API_TIMEOUT_MS;
|
|
521
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
509
522
|
try {
|
|
510
523
|
const res = await fetch(API, {
|
|
511
524
|
method: 'POST',
|
|
512
525
|
headers: { 'content-type': 'application/json' },
|
|
513
526
|
body: JSON.stringify({ method, params }),
|
|
527
|
+
signal: controller.signal,
|
|
514
528
|
});
|
|
515
529
|
const data = await res.json();
|
|
516
530
|
if (!res.ok && data.ok !== false) return { ok: false, message: `请求失败(HTTP ${res.status})` };
|
|
517
531
|
return data;
|
|
518
532
|
} catch (error) {
|
|
533
|
+
if (controller.signal.aborted) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
message: `请求超时(${Math.ceil(timeoutMs / 1000)} 秒),请检查服务状态和 Host 日志后重试`,
|
|
537
|
+
detail: { kind: 'timeout' },
|
|
538
|
+
};
|
|
539
|
+
}
|
|
519
540
|
return { ok: false, message: `无法连接 MCP 连接器服务:${error?.message || error}` };
|
|
541
|
+
} finally {
|
|
542
|
+
clearTimeout(timer);
|
|
520
543
|
}
|
|
521
544
|
}
|
|
522
545
|
|
|
@@ -676,7 +699,7 @@
|
|
|
676
699
|
return `<div class="tool-item"><div class="tool-name-row"><div class="tool-name">${esc(tool.name || tool.title || '未命名工具')}</div>${deprecated ? '<span class="tool-deprecated">已弃用</span>' : ''}</div><div class="tool-description${description ? '' : ' empty'}">${esc(description || '暂无描述')}</div></div>`;
|
|
677
700
|
}).join('')
|
|
678
701
|
: server.managed
|
|
679
|
-
? '<div class="tools-loading">stdio
|
|
702
|
+
? '<div class="tools-loading">Host 已完成 stdio 初始化,但该 Server 当前没有可展示的工具。</div>'
|
|
680
703
|
: '<div class="tools-loading">暂无工具</div>';
|
|
681
704
|
return `<div class="tools-server"><div class="tools-server-header">${esc(label)} <span class="count">· 显示 ${tools.length} / ${matched.length} 个</span></div>${items}</div>`;
|
|
682
705
|
}).filter(Boolean);
|
|
@@ -1038,7 +1061,8 @@
|
|
|
1038
1061
|
|
|
1039
1062
|
window.__mcp = {
|
|
1040
1063
|
async connect(connectorId) {
|
|
1041
|
-
const
|
|
1064
|
+
const timeoutMs = marketItems[connectorId]?.authMode === 'oauth2-pkce' ? 310_000 : undefined;
|
|
1065
|
+
const r = await call('connect', { connectorId }, { timeoutMs });
|
|
1042
1066
|
showToast(r.message, r.ok);
|
|
1043
1067
|
if (r.ok) {
|
|
1044
1068
|
await call('healthCheck', { connectorId });
|
|
@@ -1320,7 +1344,7 @@
|
|
|
1320
1344
|
}
|
|
1321
1345
|
if (detailFailureState === 'reauth' && detailConnector?.authMode === 'oauth2-pkce') {
|
|
1322
1346
|
const id = detailConnector.id;
|
|
1323
|
-
const r = await call('connect', { connectorId: id });
|
|
1347
|
+
const r = await call('connect', { connectorId: id }, { timeoutMs: 310_000 });
|
|
1324
1348
|
showToast(r.message, r.ok);
|
|
1325
1349
|
if (r.ok) {
|
|
1326
1350
|
await call('healthCheck', { connectorId: id });
|