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.
- package/CHANGELOG.md +30 -1
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/STDIO-SUPPORT.md +2 -0
- package/docs/USER-GUIDE.md +6 -3
- package/lib/connectors/oauth-connector.js +10 -2
- package/lib/constants.js +5 -0
- package/lib/grant-lifecycle.js +47 -0
- package/lib/index.js +739 -116
- package/lib/mcp-provision.js +5 -5
- package/lib/mcp-validation.js +25 -3
- package/package.json +2 -2
- package/ui/index.html +33 -8
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.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
|
|
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
|
|
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 });
|