dsh-mcp-connector 0.2.21 → 0.2.23

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.
@@ -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: false,
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: false,
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);
@@ -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 < 4; depth += 1) {
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 { kind: 'timeout', message: '连接超时,请检查网络、VPN/专线或服务状态' };
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.21",
3
+ "version": "0.2.23",
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",
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "test": "node --test",
36
- "lint": "node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.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/oauth.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/sync-registry-stats.mjs",
36
+ "lint": "node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.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/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/sync-registry-stats.mjs",
37
37
  "registry:build": "node scripts/build-registry.mjs",
38
38
  "registry:validate": "node scripts/probe-connector.mjs catalog/catalog.json --validate-only",
39
39
  "dev:ui": "node scripts/ui-harness.mjs",
package/ui/index.html CHANGED
@@ -455,13 +455,23 @@
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');
461
471
  const searchInput = $('#search');
462
472
  const LABELS = {
463
473
  details: '详情', connect: '连接', connected: '已连接', configured: '已配置', configure: '配置',
464
- reauthorize: '需重新授权', connectionError: '连接异常', partialError: '部分异常', disabled: '已停用',
474
+ reauthorize: '需重新授权', recovering: '自动重试中', connectionError: '连接异常', partialError: '部分异常', disabled: '已停用',
465
475
  send: '发送', fillSend: '填写并发送', connectToUse: '连接后使用', try: '💬 去试试',
466
476
  connectTry: '🔗 连接后试用', configureConnect: '🔑 录入凭据并连接',
467
477
  reconfigureCredential: '🔑 重新配置凭据', loading: '加载中…',
@@ -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 工具已由 DSH Host 注册;当前市场详情没有目录快照,可直接在对话中调用。</div>'
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);
@@ -834,6 +857,7 @@
834
857
  if (state === 'configured') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('configured')}</button>`;
835
858
  if (state === 'disabled') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('disabled')}</button>`;
836
859
  if (state === 'reauth') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-warning" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('reauthorize')}</button>`;
860
+ if (state === 'recovering') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-warning" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('recovering')}</button>`;
837
861
  if (state === 'degraded') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-warning" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('partialError')}</button>`;
838
862
  if (state === 'unavailable') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-bad" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('connectionError')}</button>`;
839
863
  }
@@ -863,8 +887,8 @@
863
887
  function rowHtml(r) {
864
888
  const key = esc(r.key);
865
889
  const authMode = r.authMode === 'oauth' ? '<span class="badge auth-oauth">OAuth</span>' : `<span class="badge">${esc(r.authMode || 'manual')}</span>`;
866
- const healthLabels = { healthy: '已连接', configured: '已配置', reauth: '需重新授权', degraded: '部分异常', unavailable: '连接异常', disabled: '已停用' };
867
- const healthClass = r.connectionState === 'healthy' ? 'pass' : ['reauth', 'degraded'].includes(r.connectionState) ? 'partial' : r.connectionState === 'unavailable' ? 'fail' : '';
890
+ const healthLabels = { healthy: '已连接', configured: '已配置', reauth: '需重新授权', recovering: '自动重试中', degraded: '部分异常', unavailable: '连接异常', disabled: '已停用' };
891
+ const healthClass = r.connectionState === 'healthy' ? 'pass' : ['reauth', 'recovering', 'degraded'].includes(r.connectionState) ? 'partial' : r.connectionState === 'unavailable' ? 'fail' : '';
868
892
  const healthBadge = `<span class="badge ${healthClass}">${esc(healthLabels[r.connectionState] || '已配置')}</span>`;
869
893
  const enabled = r.enabled !== false;
870
894
  return `<div class="row" data-key="${key}">
@@ -1038,7 +1062,8 @@
1038
1062
 
1039
1063
  window.__mcp = {
1040
1064
  async connect(connectorId) {
1041
- const r = await call('connect', { connectorId });
1065
+ const timeoutMs = marketItems[connectorId]?.authMode === 'oauth2-pkce' ? 310_000 : undefined;
1066
+ const r = await call('connect', { connectorId }, { timeoutMs });
1042
1067
  showToast(r.message, r.ok);
1043
1068
  if (r.ok) {
1044
1069
  await call('healthCheck', { connectorId });
@@ -1184,7 +1209,7 @@
1184
1209
  if (!d) return;
1185
1210
  detailConnector = d;
1186
1211
  detailFailureState = d.connectionState || '';
1187
- detailNeedsReconfigure = ['reauth', 'unavailable'].includes(detailFailureState);
1212
+ detailNeedsReconfigure = ['reauth', 'recovering', 'unavailable'].includes(detailFailureState);
1188
1213
  const loadId = ++detailLoadId;
1189
1214
  $('#detail-icon').innerHTML = isImageIcon(d.icon)
1190
1215
  ? `<img src="${esc(d.icon)}" alt="" onerror="this.parentElement.innerHTML='🔌';" />`
@@ -1320,7 +1345,7 @@
1320
1345
  }
1321
1346
  if (detailFailureState === 'reauth' && detailConnector?.authMode === 'oauth2-pkce') {
1322
1347
  const id = detailConnector.id;
1323
- const r = await call('connect', { connectorId: id });
1348
+ const r = await call('connect', { connectorId: id }, { timeoutMs: 310_000 });
1324
1349
  showToast(r.message, r.ok);
1325
1350
  if (r.ok) {
1326
1351
  await call('healthCheck', { connectorId: id });