dsh-mcp-connector 0.2.43 → 0.2.44
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 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/index.js +127 -13
- package/lib/status-events.js +1 -1
- package/lib/tool-catalog-cache.js +4 -3
- package/lib/tools.js +4 -1
- package/lib/web.js +1 -1
- package/package.json +1 -1
- package/ui/index.html +97 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.44] - 2026-09-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 启动恢复及连接成功后后台发现工具,无需先打开详情;后台最多并行发现两个连接器,同一连接的并发 HTTP 发现合并。
|
|
12
|
+
- 手动、JSON 导入和目录连接统一支持工具列表、搜索与详情,可按 connectionKey 精确定位;已安装页增加工具入口。
|
|
13
|
+
- SSE 订阅期间每 30 秒重新发现以恢复断线状态;详情读取只读快照、重连后补同步,不形成事件刷新循环。
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- 页面与 API 使用一致的搜索评分,精确工具名优先;后台发现中、读取失败与成功空列表分别展示。
|
|
18
|
+
- Schema 清理区分参数名称与关键字,保留名为 default、const、examples 的合法参数;敏感默认值及示例仍不缓存。
|
|
19
|
+
- 连接配置变更后失效旧缓存,忽略断开或重配期间返回的过期发现结果;查询继续遵守工作区可见范围。
|
|
20
|
+
|
|
21
|
+
本次不新增目标工具执行、跨连接器搜索页面或凭据分享能力。
|
|
22
|
+
|
|
7
23
|
## [0.2.43] - 2026-09-12
|
|
8
24
|
|
|
9
25
|
### Added
|
package/README.en.md
CHANGED
|
@@ -178,7 +178,7 @@ npm run dev:ui
|
|
|
178
178
|
|
|
179
179
|
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.
|
|
180
180
|
|
|
181
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
181
|
+
The current public version is [`dsh-mcp-connector@0.2.44`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.44](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.44).
|
|
182
182
|
|
|
183
183
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
184
184
|
|
package/README.md
CHANGED
|
@@ -173,7 +173,7 @@ npm run dev:ui
|
|
|
173
173
|
|
|
174
174
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
175
175
|
|
|
176
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
176
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.44`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.44](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.44)。
|
|
177
177
|
|
|
178
178
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
179
179
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/lib/index.js
CHANGED
|
@@ -166,8 +166,10 @@ export async function apply(ctx, config) {
|
|
|
166
166
|
warnedLegacyConflicts: new Set(),
|
|
167
167
|
toolInventories: new Map(), // serverName -> { observed, observedAt, tools: Set<rawName> }
|
|
168
168
|
toolCatalogCache: new Map(), // connection key -> 最近成功、安全裁剪后的工具元数据
|
|
169
|
+
toolViews: new Map(), // connection key -> last discovery outcome (memory only)
|
|
169
170
|
};
|
|
170
171
|
const statusEvents = new StatusEventHub();
|
|
172
|
+
const toolRequests = new Map();
|
|
171
173
|
|
|
172
174
|
const hostGovernance = createHostGovernanceController(ctx, {
|
|
173
175
|
getRules: () => governancePolicies.snapshot().rules,
|
|
@@ -603,6 +605,13 @@ export async function apply(ctx, config) {
|
|
|
603
605
|
async function persistRecord(record) {
|
|
604
606
|
// 存储成功后才更新内存,避免落盘失败却在当前进程显示为“已安装”。
|
|
605
607
|
if (config.persistSecrets) await connectionStore.put(record);
|
|
608
|
+
const previous = state.connections.get(record.key);
|
|
609
|
+
if (previous && (connectionToolCacheSignature(previous) !== connectionToolCacheSignature(record)
|
|
610
|
+
|| JSON.stringify(previous.auth) !== JSON.stringify(record.auth)
|
|
611
|
+
|| JSON.stringify(previous.headers) !== JSON.stringify(record.headers)
|
|
612
|
+
|| JSON.stringify(previous.env) !== JSON.stringify(record.env))) {
|
|
613
|
+
await deleteLastSuccessfulTools(record.key);
|
|
614
|
+
}
|
|
606
615
|
state.connections.set(record.key, record);
|
|
607
616
|
clearRecordHealth(record);
|
|
608
617
|
}
|
|
@@ -984,6 +993,8 @@ export async function apply(ctx, config) {
|
|
|
984
993
|
}
|
|
985
994
|
|
|
986
995
|
async function cacheLastSuccessfulTools(record, tools) {
|
|
996
|
+
// A request may finish after disconnect/reconfigure. Never resurrect old metadata.
|
|
997
|
+
if (state.connections.get(record.key) !== record || record.enabled === false) return null;
|
|
987
998
|
const entry = createToolCatalogRecord(record, tools);
|
|
988
999
|
state.toolCatalogCache.set(record.key, entry);
|
|
989
1000
|
if (config.persistSecrets) {
|
|
@@ -997,6 +1008,7 @@ export async function apply(ctx, config) {
|
|
|
997
1008
|
}
|
|
998
1009
|
|
|
999
1010
|
async function deleteLastSuccessfulTools(key) {
|
|
1011
|
+
state.toolViews.delete(key);
|
|
1000
1012
|
state.toolCatalogCache.delete(key);
|
|
1001
1013
|
if (config.persistSecrets) {
|
|
1002
1014
|
await toolCatalogStore.delete(key).catch((error) => {
|
|
@@ -1014,9 +1026,10 @@ export async function apply(ctx, config) {
|
|
|
1014
1026
|
return { entry, ...toolCatalogCacheView(entry) };
|
|
1015
1027
|
}
|
|
1016
1028
|
|
|
1017
|
-
function visibleToolCatalogEntries(workspaceId) {
|
|
1029
|
+
function visibleToolCatalogEntries(workspaceId, connectionKey) {
|
|
1018
1030
|
const entries = [];
|
|
1019
1031
|
for (const record of state.connections.values()) {
|
|
1032
|
+
if (connectionKey && record.key !== connectionKey) continue;
|
|
1020
1033
|
if (record.enabled === false) continue;
|
|
1021
1034
|
if (workspaceId && !connectionVisibleInWorkspace(connectionScopes.binding(record.key), workspaceId)) continue;
|
|
1022
1035
|
const cached = cachedToolsForRecord(record);
|
|
@@ -2644,23 +2657,38 @@ export async function apply(ctx, config) {
|
|
|
2644
2657
|
async toolSearch(input = {}) {
|
|
2645
2658
|
try {
|
|
2646
2659
|
const connectorId = String(input.connectorId ?? '').trim() || undefined;
|
|
2647
|
-
if (connectorId && !state.merged.some((connector) => connector.id === connectorId)
|
|
2660
|
+
if (connectorId && !state.merged.some((connector) => connector.id === connectorId)
|
|
2661
|
+
&& ![...state.connections.values()].some((record) => record.connectorId === connectorId
|
|
2662
|
+
&& record.enabled !== false && (!input.workspaceId || connectionVisibleInWorkspace(connectionScopes.binding(record.key), input.workspaceId)))) {
|
|
2648
2663
|
return { ok: false, message: `连接器 "${connectorId}" 不存在` };
|
|
2649
2664
|
}
|
|
2650
|
-
const items = searchToolCatalog(visibleToolCatalogEntries(input.workspaceId), {
|
|
2665
|
+
const items = searchToolCatalog(visibleToolCatalogEntries(input.workspaceId, input.connectionKey), {
|
|
2651
2666
|
query: input.query,
|
|
2652
2667
|
connectorId,
|
|
2653
2668
|
limit: input.limit,
|
|
2654
2669
|
});
|
|
2655
2670
|
const staleCount = items.filter((item) => item.stale).length;
|
|
2671
|
+
const visibleRecords = [...state.connections.values()].filter((record) => record.enabled !== false
|
|
2672
|
+
&& (!connectorId || record.connectorId === connectorId)
|
|
2673
|
+
&& (!input.connectionKey || record.key === input.connectionKey)
|
|
2674
|
+
&& (!input.workspaceId || connectionVisibleInWorkspace(connectionScopes.binding(record.key), input.workspaceId)));
|
|
2675
|
+
const pendingServers = visibleRecords.filter((record) => !cachedToolsForRecord(record)
|
|
2676
|
+
&& state.toolViews.get(record.key)?.record !== record).length;
|
|
2677
|
+
const failedServers = visibleRecords.filter((record) => {
|
|
2678
|
+
const view = state.toolViews.get(record.key);
|
|
2679
|
+
return view?.record === record && !view.server.ok;
|
|
2680
|
+
}).length;
|
|
2681
|
+
if (pendingServers > 0) scheduleDiscovery();
|
|
2656
2682
|
return {
|
|
2657
2683
|
ok: true,
|
|
2658
|
-
message: `最后成功工具缓存中找到 ${items.length} 个匹配项${staleCount ? `,其中 ${staleCount} 个已超过 24 小时` : ''}`,
|
|
2684
|
+
message: `最后成功工具缓存中找到 ${items.length} 个匹配项${staleCount ? `,其中 ${staleCount} 个已超过 24 小时` : ''}${pendingServers ? `;${pendingServers} 个服务正在后台发现` : ''}${failedServers ? `;${failedServers} 个服务最近读取失败` : ''}`,
|
|
2659
2685
|
detail: {
|
|
2660
2686
|
items,
|
|
2661
2687
|
source: 'last-success-cache',
|
|
2662
2688
|
discoveryOnly: true,
|
|
2663
2689
|
credentialsStored: false,
|
|
2690
|
+
pendingServers,
|
|
2691
|
+
failedServers,
|
|
2664
2692
|
},
|
|
2665
2693
|
};
|
|
2666
2694
|
} catch (error) {
|
|
@@ -2672,7 +2700,7 @@ export async function apply(ctx, config) {
|
|
|
2672
2700
|
try {
|
|
2673
2701
|
const connectorId = String(input.connectorId ?? '').trim() || undefined;
|
|
2674
2702
|
const serverName = String(input.serverName ?? '').trim() || undefined;
|
|
2675
|
-
const matches = findToolDetails(visibleToolCatalogEntries(input.workspaceId), {
|
|
2703
|
+
const matches = findToolDetails(visibleToolCatalogEntries(input.workspaceId, input.connectionKey), {
|
|
2676
2704
|
toolName: input.toolName,
|
|
2677
2705
|
connectorId,
|
|
2678
2706
|
serverName,
|
|
@@ -2709,15 +2737,40 @@ export async function apply(ctx, config) {
|
|
|
2709
2737
|
* 从已连接的 MCP server 动态获取工具清单
|
|
2710
2738
|
* 通过 MCP JSON-RPC 协议调用 tools/list 方法
|
|
2711
2739
|
*/
|
|
2712
|
-
async toolsList(connectorId, workspaceId) {
|
|
2740
|
+
async toolsList(connectorId, workspaceId, cachedOnly = false, connectionKey) {
|
|
2713
2741
|
const connector = state.merged.find((d) => d.id === connectorId);
|
|
2714
|
-
if (!connector) return { ok: false, message: `连接器 "${connectorId}" 不存在` };
|
|
2715
2742
|
|
|
2716
2743
|
const records = [...state.connections.values()].filter((record) => (
|
|
2717
2744
|
record.connectorId === connectorId
|
|
2745
|
+
&& (!connectionKey || record.key === connectionKey)
|
|
2718
2746
|
&& record.enabled !== false
|
|
2719
2747
|
&& (!workspaceId || connectionVisibleInWorkspace(connectionScopes.binding(record.key), workspaceId))
|
|
2720
2748
|
));
|
|
2749
|
+
if (connectionKey && !records.length) return { ok: false, message: '连接不存在、已停用或当前工作区不可见' };
|
|
2750
|
+
if (!connector && !records.length) return { ok: false, message: `连接器 "${connectorId}" 不存在或当前工作区不可见` };
|
|
2751
|
+
if (cachedOnly && records.length) {
|
|
2752
|
+
const servers = records.map((record) => {
|
|
2753
|
+
const cached = cachedToolsForRecord(record);
|
|
2754
|
+
const view = state.toolViews.get(record.key);
|
|
2755
|
+
const current = view?.record === record ? view.server : null;
|
|
2756
|
+
return {
|
|
2757
|
+
serverKey: record.serverKey || record.name, serverName: record.serverName,
|
|
2758
|
+
ok: current?.ok === true, cached: Boolean(cached),
|
|
2759
|
+
unknown: !current || current.unknown === true,
|
|
2760
|
+
pending: !current && !cached,
|
|
2761
|
+
error: current?.error || (!cached && !current ? '正在后台发现工具,请稍候' : undefined),
|
|
2762
|
+
policy: serverPolicy(record),
|
|
2763
|
+
...(cached ? { cacheObservedAt: cached.observedAt, cacheAgeMs: cached.cacheAgeMs, stale: cached.stale } : {}),
|
|
2764
|
+
tools: (cached?.entry.tools || []).map((tool) => decorateToolPolicy(record, {
|
|
2765
|
+
name: tool.name, title: tool.title || tool.name, description: tool.description || '',
|
|
2766
|
+
})),
|
|
2767
|
+
};
|
|
2768
|
+
});
|
|
2769
|
+
return { ok: servers.some((server) => server.ok), message: '最近工具发现状态', detail: {
|
|
2770
|
+
connectorId, servers, totalTools: servers.reduce((sum, server) => sum + server.tools.length, 0),
|
|
2771
|
+
source: 'last-success-cache',
|
|
2772
|
+
} };
|
|
2773
|
+
}
|
|
2721
2774
|
if (!records.length) {
|
|
2722
2775
|
const servers = (connector.toolsSnapshot ?? []).map((snapshot) => ({
|
|
2723
2776
|
serverKey: snapshot.serverKey,
|
|
@@ -2740,7 +2793,7 @@ export async function apply(ctx, config) {
|
|
|
2740
2793
|
for (const record of records) {
|
|
2741
2794
|
if (record.transport === 'stdio') {
|
|
2742
2795
|
const registered = registeredStdioTools(record);
|
|
2743
|
-
const snapshot = (connector
|
|
2796
|
+
const snapshot = (connector?.toolsSnapshot ?? []).find((item) =>
|
|
2744
2797
|
item.serverKey === record.serverKey || item.serverName === record.serverName);
|
|
2745
2798
|
const liveTools = registered.tools;
|
|
2746
2799
|
let cached = cachedToolsForRecord(record);
|
|
@@ -2795,10 +2848,14 @@ export async function apply(ctx, config) {
|
|
|
2795
2848
|
continue;
|
|
2796
2849
|
}
|
|
2797
2850
|
try {
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
grants: grantMap()
|
|
2801
|
-
|
|
2851
|
+
let request = toolRequests.get(record);
|
|
2852
|
+
if (!request) {
|
|
2853
|
+
request = listMcpTools(record, { timeoutMs: config.requestTimeoutMs, grants: grantMap() });
|
|
2854
|
+
toolRequests.set(record, request);
|
|
2855
|
+
}
|
|
2856
|
+
let listed;
|
|
2857
|
+
try { listed = await request; }
|
|
2858
|
+
finally { if (toolRequests.get(record) === request) toolRequests.delete(record); }
|
|
2802
2859
|
state.toolInventories.set(record.serverName, {
|
|
2803
2860
|
observed: true,
|
|
2804
2861
|
observedAt: Date.now(),
|
|
@@ -2847,6 +2904,14 @@ export async function apply(ctx, config) {
|
|
|
2847
2904
|
}
|
|
2848
2905
|
}
|
|
2849
2906
|
|
|
2907
|
+
if (records.some((record) => state.connections.get(record.key) !== record || record.enabled === false)) {
|
|
2908
|
+
return { ok: false, message: '连接已变更,请重新读取工具' };
|
|
2909
|
+
}
|
|
2910
|
+
records.forEach((record, index) => {
|
|
2911
|
+
if (state.connections.get(record.key) === record && record.enabled !== false) {
|
|
2912
|
+
state.toolViews.set(record.key, { record, server: servers[index] });
|
|
2913
|
+
}
|
|
2914
|
+
});
|
|
2850
2915
|
const totalTools = servers.reduce((sum, s) => sum + (s.tools?.length || 0), 0);
|
|
2851
2916
|
const availableServers = servers.filter((server) => server.ok).length;
|
|
2852
2917
|
const unknownServers = servers.filter((server) => server.unknown).length;
|
|
@@ -2982,7 +3047,53 @@ export async function apply(ctx, config) {
|
|
|
2982
3047
|
hostGovernance.refresh();
|
|
2983
3048
|
hostScopes.refresh();
|
|
2984
3049
|
|
|
2985
|
-
|
|
3050
|
+
// One background pass at a time; UI event refreshes read snapshots, never tools/list.
|
|
3051
|
+
let discoveryDisposed = false;
|
|
3052
|
+
let discoveryRunning = false;
|
|
3053
|
+
let discoveryPending = false;
|
|
3054
|
+
let discoveryTimer = null;
|
|
3055
|
+
async function discoverTools() {
|
|
3056
|
+
if (discoveryDisposed || discoveryRunning) return;
|
|
3057
|
+
discoveryRunning = true;
|
|
3058
|
+
discoveryPending = false;
|
|
3059
|
+
try {
|
|
3060
|
+
const ids = [...new Set([...state.connections.values()].filter((record) => record.enabled !== false).map((record) => record.connectorId))];
|
|
3061
|
+
let cursor = 0;
|
|
3062
|
+
await Promise.all(Array.from({ length: Math.min(2, ids.length) }, async () => {
|
|
3063
|
+
while (cursor < ids.length && !discoveryDisposed) {
|
|
3064
|
+
const id = ids[cursor++];
|
|
3065
|
+
await api.toolsList(id).catch(() => {});
|
|
3066
|
+
}
|
|
3067
|
+
}));
|
|
3068
|
+
if (!discoveryDisposed) statusEvents.publish('tools');
|
|
3069
|
+
} finally {
|
|
3070
|
+
discoveryRunning = false;
|
|
3071
|
+
if (discoveryPending) scheduleDiscovery();
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
function scheduleDiscovery() {
|
|
3075
|
+
if (discoveryDisposed) return;
|
|
3076
|
+
discoveryPending = true;
|
|
3077
|
+
if (discoveryRunning || discoveryTimer) return;
|
|
3078
|
+
discoveryTimer = setTimeout(() => { discoveryTimer = null; void discoverTools(); }, 100);
|
|
3079
|
+
discoveryTimer.unref?.();
|
|
3080
|
+
}
|
|
3081
|
+
const discoveryMutations = new Set(['connect', 'configure', 'importJson', 'installFromUrl', 'reconfigureConnection', 'setEnabled', 'restoreSnapshot', 'migrateLegacy']);
|
|
3082
|
+
const discoveryApi = new Proxy(api, { get(target, property) {
|
|
3083
|
+
const value = target[property];
|
|
3084
|
+
if (!discoveryMutations.has(property)) return value;
|
|
3085
|
+
return async (...args) => {
|
|
3086
|
+
const result = await value.apply(target, args);
|
|
3087
|
+
if (result?.ok) scheduleDiscovery();
|
|
3088
|
+
return result;
|
|
3089
|
+
};
|
|
3090
|
+
} });
|
|
3091
|
+
const observableApi = observeStatusEvents(discoveryApi, statusEvents);
|
|
3092
|
+
scheduleDiscovery();
|
|
3093
|
+
const discoveryInterval = setInterval(() => {
|
|
3094
|
+
if (statusEvents.clients.size > 0) scheduleDiscovery();
|
|
3095
|
+
}, 30_000);
|
|
3096
|
+
discoveryInterval.unref?.();
|
|
2986
3097
|
const disposeTools = registerTools(ctx, observableApi);
|
|
2987
3098
|
|
|
2988
3099
|
/* ───────────────────────── web 半区(图形化市场)──────────────────────── */
|
|
@@ -3000,6 +3111,9 @@ export async function apply(ctx, config) {
|
|
|
3000
3111
|
}
|
|
3001
3112
|
|
|
3002
3113
|
ctx.effect(() => () => {
|
|
3114
|
+
discoveryDisposed = true;
|
|
3115
|
+
clearTimeout(discoveryTimer);
|
|
3116
|
+
clearInterval(discoveryInterval);
|
|
3003
3117
|
disposeTools();
|
|
3004
3118
|
disposeWeb?.();
|
|
3005
3119
|
hostGovernance.dispose();
|
package/lib/status-events.js
CHANGED
|
@@ -111,7 +111,7 @@ export function observeStatusEvents(api, eventHub, eventTypes = DEFAULT_EVENT_TY
|
|
|
111
111
|
try {
|
|
112
112
|
return await Reflect.apply(value, target, args);
|
|
113
113
|
} finally {
|
|
114
|
-
eventHub.publish(eventType);
|
|
114
|
+
if (!(property === 'toolsList' && args[2] === true)) eventHub.publish(eventType);
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
117
|
},
|
|
@@ -23,7 +23,7 @@ function truncateUtf8(value, maxBytes) {
|
|
|
23
23
|
return output;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function sanitizeSchemaValue(value, state, depth = 0) {
|
|
26
|
+
function sanitizeSchemaValue(value, state, depth = 0, propertyMap = false) {
|
|
27
27
|
if (state.nodes >= 2_048 || depth > 10) {
|
|
28
28
|
state.truncated = true;
|
|
29
29
|
return undefined;
|
|
@@ -47,12 +47,13 @@ function sanitizeSchemaValue(value, state, depth = 0) {
|
|
|
47
47
|
const entries = Object.entries(value);
|
|
48
48
|
if (entries.length > 128) state.truncated = true;
|
|
49
49
|
for (const [rawKey, rawValue] of entries.slice(0, 128)) {
|
|
50
|
-
if (OMITTED_SCHEMA_KEYS.has(rawKey) || UNSAFE_KEYS.has(rawKey)) {
|
|
50
|
+
if ((!propertyMap && OMITTED_SCHEMA_KEYS.has(rawKey)) || UNSAFE_KEYS.has(rawKey)) {
|
|
51
51
|
if (OMITTED_SCHEMA_KEYS.has(rawKey)) state.truncated = true;
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
54
54
|
const key = truncateUtf8(rawKey, 256);
|
|
55
|
-
const child = sanitizeSchemaValue(rawValue, state, depth + 1
|
|
55
|
+
const child = sanitizeSchemaValue(rawValue, state, depth + 1,
|
|
56
|
+
!propertyMap && ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas'].includes(rawKey));
|
|
56
57
|
if (key && child !== undefined) output[key] = child;
|
|
57
58
|
}
|
|
58
59
|
return output;
|
package/lib/tools.js
CHANGED
|
@@ -607,6 +607,7 @@ export function registerTools(ctx, api) {
|
|
|
607
607
|
properties: {
|
|
608
608
|
connectorId: { type: 'string', description: '连接器 id(来自 mcp_connector_catalog)' },
|
|
609
609
|
workspaceId: { type: 'string', description: '可选;只加载在该 Workspace 可见的已连接 Server' },
|
|
610
|
+
connectionKey: { type: 'string', description: '可选;来自 status 的连接 key,限定手动、JSON 或目录安装的单条连接' },
|
|
610
611
|
},
|
|
611
612
|
required: ['connectorId'],
|
|
612
613
|
additionalProperties: false,
|
|
@@ -614,7 +615,7 @@ export function registerTools(ctx, api) {
|
|
|
614
615
|
output: toolsListOutput,
|
|
615
616
|
timeoutMs: 30_000,
|
|
616
617
|
async execute(args) {
|
|
617
|
-
return api.toolsList(args.connectorId, args.workspaceId);
|
|
618
|
+
return api.toolsList(args.connectorId, args.workspaceId, false, args.connectionKey);
|
|
618
619
|
},
|
|
619
620
|
});
|
|
620
621
|
|
|
@@ -640,6 +641,7 @@ export function registerTools(ctx, api) {
|
|
|
640
641
|
type: 'object',
|
|
641
642
|
properties: {
|
|
642
643
|
query: { type: 'string', description: '工具名称、标题或描述关键词' },
|
|
644
|
+
connectionKey: { type: 'string', description: '可选;限定单条连接 key' },
|
|
643
645
|
connectorId: { type: 'string', description: '可选;限定连接器 id' },
|
|
644
646
|
workspaceId: { type: 'string', description: '可选;仅搜索该 Workspace 可见的连接' },
|
|
645
647
|
limit: { type: 'number', minimum: 1, maximum: 100, description: '最多返回数量,默认 20' },
|
|
@@ -672,6 +674,7 @@ export function registerTools(ctx, api) {
|
|
|
672
674
|
type: 'object',
|
|
673
675
|
properties: {
|
|
674
676
|
toolName: { type: 'string', description: '精确工具名(来自 mcp_connector_tool_search)' },
|
|
677
|
+
connectionKey: { type: 'string', description: '可选;限定单条连接 key' },
|
|
675
678
|
connectorId: { type: 'string', description: '可选;用于消除同名歧义' },
|
|
676
679
|
serverName: { type: 'string', description: '可选;用于消除同名歧义' },
|
|
677
680
|
workspaceId: { type: 'string', description: '可选;仅查看该 Workspace 可见的连接' },
|
package/lib/web.js
CHANGED
|
@@ -48,7 +48,7 @@ const API_WHITELIST = new Map([
|
|
|
48
48
|
['disconnect', (api, p) => api.disconnect(p?.key)],
|
|
49
49
|
['setEnabled', (api, p) => api.setEnabled(p?.key, p?.enabled !== false)],
|
|
50
50
|
['refreshCatalog', (api) => api.refreshCatalog()],
|
|
51
|
-
['toolsList', (api, p) => api.toolsList(p?.connectorId, p?.workspaceId)],
|
|
51
|
+
['toolsList', (api, p) => api.toolsList(p?.connectorId, p?.workspaceId, p?.cachedOnly === true, p?.connectionKey)],
|
|
52
52
|
['toolSearch', (api, p) => api.toolSearch(p ?? {})],
|
|
53
53
|
['toolDetail', (api, p) => api.toolDetail(p ?? {})],
|
|
54
54
|
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.44",
|
|
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",
|
package/ui/index.html
CHANGED
|
@@ -782,7 +782,7 @@
|
|
|
782
782
|
}
|
|
783
783
|
|
|
784
784
|
function policySelect(scope, current, serverName = '', toolName = '', publicName = '', label = '') {
|
|
785
|
-
if (!detailConnector?.connected?.length) return '';
|
|
785
|
+
if (!detailConnector?.connected?.length || detailConnector.connectionKey) return '';
|
|
786
786
|
return `<select class="policy-select" data-policy-scope="${esc(scope)}" data-current-effect="${esc(current || 'inherit')}" data-server-name="${esc(serverName)}" data-tool-name="${esc(toolName)}" data-public-name="${esc(publicName)}" aria-label="${esc(label || `${scope} 策略`)}">${policyOptions(current)}</select>`;
|
|
787
787
|
}
|
|
788
788
|
|
|
@@ -799,7 +799,7 @@
|
|
|
799
799
|
|
|
800
800
|
function syncGovernanceBar() {
|
|
801
801
|
const bar = $('#governance-bar');
|
|
802
|
-
const connected = detailConnector?.connected?.length > 0;
|
|
802
|
+
const connected = detailConnector?.connected?.length > 0 && !detailConnector.connectionKey;
|
|
803
803
|
bar.hidden = !connected;
|
|
804
804
|
if (!connected) return;
|
|
805
805
|
const connectionEffect = detailGovernance?.connectionPolicy || 'inherit';
|
|
@@ -861,8 +861,34 @@
|
|
|
861
861
|
if (result.ok && detailConnector) await window.__mcp.showDetail(detailConnector.id);
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
+
function normalizeToolSearchText(value) {
|
|
865
|
+
return String(value ?? '').normalize('NFKC')
|
|
866
|
+
.replace(/([\p{Script=Han}])([A-Za-z0-9])/gu, '$1 $2')
|
|
867
|
+
.replace(/([A-Za-z0-9])([\p{Script=Han}])/gu, '$1 $2')
|
|
868
|
+
.toLocaleLowerCase('zh-CN').replace(/[^\p{Letter}\p{Number}]+/gu, ' ').trim().replace(/\s+/g, ' ');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function toolSearchScore(tool, query) {
|
|
872
|
+
const terms = query.split(' ').filter(Boolean);
|
|
873
|
+
const fields = [tool.name, tool.title, tool.description].map(normalizeToolSearchText);
|
|
874
|
+
const compactQuery = query.replace(/ /g, '');
|
|
875
|
+
const compactFields = fields.map((field) => field.replace(/ /g, ''));
|
|
876
|
+
const fieldIndex = fields.findIndex((field, index) => (
|
|
877
|
+
terms.every((term) => field.includes(term)) || compactFields[index].includes(compactQuery)
|
|
878
|
+
));
|
|
879
|
+
if (fieldIndex < 0) return -1;
|
|
880
|
+
const field = fields[fieldIndex];
|
|
881
|
+
const compact = compactFields[fieldIndex];
|
|
882
|
+
let score = fieldIndex === 0 ? 300 : fieldIndex === 1 ? 180 : 80;
|
|
883
|
+
if (field === query || compact === compactQuery) score += 1_000;
|
|
884
|
+
else if (field.startsWith(query) || compact.startsWith(compactQuery)) score += 500;
|
|
885
|
+
else if (field.includes(query) || compact.includes(compactQuery)) score += 250;
|
|
886
|
+
score -= Math.min(field.length, 200) / 1_000;
|
|
887
|
+
return score;
|
|
888
|
+
}
|
|
889
|
+
|
|
864
890
|
function renderTools() {
|
|
865
|
-
const query = $('#tools-search').value
|
|
891
|
+
const query = normalizeToolSearchText($('#tools-search').value);
|
|
866
892
|
if (detailToolServers.length === 0) {
|
|
867
893
|
updateToolsSummary();
|
|
868
894
|
$('#detail-tools').innerHTML = '<div class="tools-empty">暂无可用工具</div>';
|
|
@@ -871,17 +897,22 @@
|
|
|
871
897
|
let matchedTools = 0;
|
|
872
898
|
let renderedTools = 0;
|
|
873
899
|
let remaining = toolRenderLimit;
|
|
874
|
-
const
|
|
900
|
+
const rankedServers = query ? [...detailToolServers].sort((a, b) =>
|
|
901
|
+
Math.max(-1, ...(b.tools || []).map((tool) => toolSearchScore(tool, query)))
|
|
902
|
+
- Math.max(-1, ...(a.tools || []).map((tool) => toolSearchScore(tool, query)))) : detailToolServers;
|
|
903
|
+
const groups = rankedServers.map((server) => {
|
|
875
904
|
const label = server.serverName || server.serverKey || 'MCP Server';
|
|
876
905
|
const serverSelect = policySelect('server', server.policy?.configuredEffect, server.serverName, '', '', `${label} Server 策略`);
|
|
877
906
|
if (!server.ok && !server.cached) {
|
|
878
907
|
if (query) return '';
|
|
879
|
-
return `<div class="tools-server"><div class="tools-server-header"><span class="tools-server-copy">${esc(label)} <span class="count">·
|
|
908
|
+
return `<div class="tools-server"><div class="tools-server-header"><span class="tools-server-copy">${esc(label)} <span class="count">· ${server.pending ? '正在发现' : '加载失败'}</span></span>${serverSelect}</div><div class="tools-error">${esc(server.error || '未知错误')}</div></div>`;
|
|
880
909
|
}
|
|
881
910
|
const matched = (server.tools || []).filter((tool) => {
|
|
882
911
|
if (!query) return true;
|
|
883
|
-
return
|
|
884
|
-
}).sort((left, right) =>
|
|
912
|
+
return toolSearchScore(tool, query) >= 0;
|
|
913
|
+
}).sort((left, right) => query
|
|
914
|
+
? toolSearchScore(right, query) - toolSearchScore(left, query) || left.name.localeCompare(right.name)
|
|
915
|
+
: Number(isDeprecatedTool(left)) - Number(isDeprecatedTool(right)));
|
|
885
916
|
if (query && matched.length === 0) return '';
|
|
886
917
|
matchedTools += matched.length;
|
|
887
918
|
const tools = matched.slice(0, Math.max(0, remaining));
|
|
@@ -923,6 +954,7 @@
|
|
|
923
954
|
async function openToolDetail(serverName, toolName) {
|
|
924
955
|
if (!detailConnector || !toolName) return;
|
|
925
956
|
const result = await call('toolDetail', {
|
|
957
|
+
connectionKey: detailConnector.connectionKey,
|
|
926
958
|
connectorId: detailConnector.id,
|
|
927
959
|
serverName,
|
|
928
960
|
toolName,
|
|
@@ -1166,6 +1198,7 @@
|
|
|
1166
1198
|
</label>
|
|
1167
1199
|
${canEditConfiguration ? `<button class="btn ghost" type="button" onclick="window.__mcp.openEditor('${key}')">编辑配置</button>` : ''}
|
|
1168
1200
|
${canRename ? `<button class="btn ghost" type="button" onclick="window.__mcp.openRename('${key}')">重命名</button>` : ''}
|
|
1201
|
+
${enabled ? `<button class="btn ghost" type="button" onclick="window.__mcp.showConnectionTools('${key}')">工具</button>` : ''}
|
|
1169
1202
|
<button class="btn ghost" type="button" onclick="window.__mcp.openScope('${key}')">范围</button>
|
|
1170
1203
|
<button class="btn danger" onclick="window.__mcp.disconnect('${key}')">断开</button>
|
|
1171
1204
|
</div>`;
|
|
@@ -1708,8 +1741,17 @@
|
|
|
1708
1741
|
showToast(result.message, result.ok);
|
|
1709
1742
|
await loadInstalled();
|
|
1710
1743
|
},
|
|
1711
|
-
async
|
|
1712
|
-
const
|
|
1744
|
+
async showConnectionTools(key) {
|
|
1745
|
+
const record = installedItems.find((item) => item.key === key);
|
|
1746
|
+
if (!record) return;
|
|
1747
|
+
await window.__mcp.showDetail(record.connectorId, {
|
|
1748
|
+
id: record.connectorId, connectionKey: key, name: record.name, icon: '🔌',
|
|
1749
|
+
authMode: record.authMode, connected: [record], connectionState: record.connectionState,
|
|
1750
|
+
description: '本机连接的工具发现与参数详情;不会执行工具。',
|
|
1751
|
+
});
|
|
1752
|
+
},
|
|
1753
|
+
async showDetail(connectorId, connectionDescriptor) {
|
|
1754
|
+
const d = connectionDescriptor || marketItems[connectorId];
|
|
1713
1755
|
if (!d) return;
|
|
1714
1756
|
detailConnector = d;
|
|
1715
1757
|
const incomplete = d.connected?.length > 0 && d.connectionComplete === false;
|
|
@@ -1738,6 +1780,8 @@
|
|
|
1738
1780
|
syncGovernanceBar();
|
|
1739
1781
|
setDetailOpen(true);
|
|
1740
1782
|
$('#detail-disconnect-btn').style.display = d.connected?.length > 0 ? 'block' : 'none';
|
|
1783
|
+
if (d.connectionKey) $('#detail-disconnect-btn').style.display = 'none';
|
|
1784
|
+
$('#detail-try-btn').style.display = d.connectionKey ? 'none' : '';
|
|
1741
1785
|
const needsCredential = ['bearer', 'api-key'].includes(d.authMode);
|
|
1742
1786
|
$('#detail-try-btn').textContent = detailNeedsReconfigure
|
|
1743
1787
|
? detailFailureState === 'incomplete' ? (d.authMode === 'oauth2-pkce' ? t('upgradeOauth') : t('completeSetup')) : d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth' ? '🔐 重新授权' : needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查'
|
|
@@ -1749,7 +1793,7 @@
|
|
|
1749
1793
|
return;
|
|
1750
1794
|
}
|
|
1751
1795
|
const [toolsResult, governanceResult] = await Promise.all([
|
|
1752
|
-
call('toolsList', { connectorId, ...workspaceParams() }),
|
|
1796
|
+
call('toolsList', { connectorId, connectionKey: d.connectionKey, ...workspaceParams() }),
|
|
1753
1797
|
call('governance', {}),
|
|
1754
1798
|
]);
|
|
1755
1799
|
if (loadId !== detailLoadId || !$('#detail-overlay').classList.contains('active')) return;
|
|
@@ -1951,13 +1995,52 @@
|
|
|
1951
1995
|
});
|
|
1952
1996
|
let statusEventSource = null;
|
|
1953
1997
|
let statusRefreshTimer = null;
|
|
1998
|
+
let statusRefreshRunning = false;
|
|
1999
|
+
let statusRefreshPending = false;
|
|
2000
|
+
|
|
2001
|
+
async function refreshOpenToolDetail() {
|
|
2002
|
+
if (!detailConnector || !$('#detail-overlay').classList.contains('active')) return;
|
|
2003
|
+
const id = detailLoadId;
|
|
2004
|
+
const result = await call('toolsList', { connectorId: detailConnector.id, connectionKey: detailConnector.connectionKey, ...workspaceParams(), cachedOnly: true });
|
|
2005
|
+
if (id !== detailLoadId || !$('#detail-overlay').classList.contains('active')) return;
|
|
2006
|
+
if (!result.detail?.servers) {
|
|
2007
|
+
detailToolServers = [];
|
|
2008
|
+
renderTools();
|
|
2009
|
+
$('#tools-summary').textContent = result.message || '连接当前不可用';
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
if (result.detail?.servers) {
|
|
2013
|
+
detailToolServers = result.detail.servers;
|
|
2014
|
+
renderTools();
|
|
2015
|
+
const pending = detailToolServers.some((server) => server.pending);
|
|
2016
|
+
const unknown = detailToolServers.some((server) => server.unknown);
|
|
2017
|
+
const available = detailToolServers.filter((server) => server.ok).length;
|
|
2018
|
+
const label = pending ? '正在后台发现工具' : unknown && available === 0 ? '工具状态尚未确认'
|
|
2019
|
+
: available === detailToolServers.length ? '已连接'
|
|
2020
|
+
: available > 0 ? '部分服务可用' : '当前连接异常';
|
|
2021
|
+
detailFailureState = pending ? 'unknown' : available > 0 ? 'healthy' : 'unavailable';
|
|
2022
|
+
detailNeedsReconfigure = !pending && available === 0;
|
|
2023
|
+
$('#detail-vendor').textContent = detailConnector.vendor ? `${detailConnector.vendor} · ${label}` : label;
|
|
2024
|
+
$('#detail-try-btn').textContent = detailNeedsReconfigure ? '🔄 重新检查' : t('try');
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
1954
2027
|
|
|
1955
2028
|
function scheduleStatusRefresh() {
|
|
1956
2029
|
if (document.hidden) return;
|
|
2030
|
+
statusRefreshPending = true;
|
|
2031
|
+
if (statusRefreshRunning) return;
|
|
1957
2032
|
clearTimeout(statusRefreshTimer);
|
|
1958
|
-
statusRefreshTimer = setTimeout(() => {
|
|
1959
|
-
|
|
1960
|
-
|
|
2033
|
+
statusRefreshTimer = setTimeout(async () => {
|
|
2034
|
+
statusRefreshRunning = true;
|
|
2035
|
+
statusRefreshPending = false;
|
|
2036
|
+
try {
|
|
2037
|
+
if (view === 'market') await loadMarket({ checkHealth: false, preserveScroll: true });
|
|
2038
|
+
else await loadInstalled();
|
|
2039
|
+
await refreshOpenToolDetail();
|
|
2040
|
+
} finally {
|
|
2041
|
+
statusRefreshRunning = false;
|
|
2042
|
+
if (statusRefreshPending) scheduleStatusRefresh();
|
|
2043
|
+
}
|
|
1961
2044
|
}, 150);
|
|
1962
2045
|
}
|
|
1963
2046
|
|
|
@@ -1965,6 +2048,7 @@
|
|
|
1965
2048
|
if (typeof EventSource !== 'function' || statusEventSource) return;
|
|
1966
2049
|
const source = new EventSource('/mcp-connector/events');
|
|
1967
2050
|
statusEventSource = source;
|
|
2051
|
+
source.addEventListener('ready', scheduleStatusRefresh);
|
|
1968
2052
|
source.addEventListener('status', (event) => {
|
|
1969
2053
|
try {
|
|
1970
2054
|
const payload = JSON.parse(event.data);
|