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 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.43`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.43](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.43).
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.43`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.43](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.43)。
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.toolsSnapshot ?? []).find((item) =>
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
- const listed = await listMcpTools(record, {
2799
- timeoutMs: config.requestTimeoutMs,
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
- const observableApi = observeStatusEvents(api, statusEvents);
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();
@@ -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.43",
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.trim().toLowerCase();
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 groups = detailToolServers.map((server) => {
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">· 加载失败</span></span>${serverSelect}</div><div class="tools-error">${esc(server.error || '未知错误')}</div></div>`;
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 `${tool.name || ''}\n${tool.title || ''}\n${tool.description || ''}`.toLowerCase().includes(query);
884
- }).sort((left, right) => Number(isDeprecatedTool(left)) - Number(isDeprecatedTool(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 showDetail(connectorId) {
1712
- const d = marketItems[connectorId];
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
- if (view === 'market') loadMarket({ checkHealth: false, preserveScroll: true });
1960
- else loadInstalled();
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);