dsh-mcp-connector 0.2.41 → 0.2.43
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 +21 -1
- package/README.en.md +5 -3
- package/README.md +5 -4
- package/docs/USER-GUIDE.md +6 -2
- package/lib/client.js +17 -1
- package/lib/index.js +187 -15
- package/lib/schema.js +22 -0
- package/lib/status-events.js +119 -0
- package/lib/stores.js +29 -2
- package/lib/tool-catalog-cache.js +228 -0
- package/lib/tools.js +71 -2
- package/lib/web.js +29 -2
- package/package.json +3 -2
- package/scripts/ui-harness.mjs +50 -0
- package/ui/index.html +81 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.43] - 2026-09-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 新增同源实时状态 SSE:连接、目录、健康、工具目录、治理和作用域发生变化后,市场页面自动刷新,无需轮询。
|
|
12
|
+
- 新增“最后一次成功工具缓存”:实时 `tools/list` 或 Host 工具观测失败时仍可查看上次成功发现的工具,并明确标注缓存时间与超过 24 小时的陈旧状态。
|
|
13
|
+
- 新增 `mcp_connector_tool_search` 与 `mcp_connector_tool_detail` 渐进式发现工具;支持按名称、标题和描述搜索,并按需读取安全裁剪后的输入参数 schema,不执行目标 MCP 工具。
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- SSE 事件仅包含通用类别、递增序号与时间;不会发送连接标识、端点、错误、参数、结果或凭据。
|
|
18
|
+
- 工具缓存按连接配置签名隔离并设置工具数、描述、schema 与总容量上限;剔除 schema 默认值和示例,断开、改配或快照恢复后清理旧缓存。
|
|
19
|
+
|
|
20
|
+
## [0.2.42] - 2026-09-11
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 示例 Prompt 的“发送”动作会在 DSH 0.1.2 优先调用 `uiWorkspace.connectWorkspace`,并兼容 DSH 0.1.1 的 `workspaces.connectWorkspace` 与 `sessions.create` 安全降级;不再因工作区导航 API 迁移报错。
|
|
25
|
+
|
|
7
26
|
## [0.2.41] - 2026-09-09
|
|
8
27
|
|
|
9
28
|
### Added
|
|
@@ -638,7 +657,8 @@
|
|
|
638
657
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
639
658
|
- iframe 消息校验同源和消息来源。
|
|
640
659
|
|
|
641
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
660
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.42...HEAD
|
|
661
|
+
[0.2.42]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.41...v0.2.42
|
|
642
662
|
[0.2.41]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.40...v0.2.41
|
|
643
663
|
[0.2.40]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.39...v0.2.40
|
|
644
664
|
[0.2.39]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.38...v0.2.39
|
package/README.en.md
CHANGED
|
@@ -54,7 +54,9 @@ If the plugin helps you connect an MCP server faster, consider [starring the rep
|
|
|
54
54
|
- OAuth 2.0 Authorization Code with PKCE, including DCR public clients and `client_secret_post` / `client_secret_basic`; API key/Bearer/unauthenticated HTTP configuration, stdio local-process configuration, and `mcpServers` JSON import.
|
|
55
55
|
- Installation from a credential-free connector descriptor URL.
|
|
56
56
|
- Credential and MCP initialize validation before HTTP API-key connectors are saved as installed, plus declarative multi-field credential-to-env bindings for marketplace stdio connectors.
|
|
57
|
-
- Dynamic tool discovery grouped by MCP server, including descriptions, search, batched rendering, and an independent scroll region.
|
|
57
|
+
- Dynamic tool discovery grouped by MCP server, including descriptions, search, parameter details, batched rendering, and an independent scroll region. When live discovery fails, the UI shows the last successful tool cache with observation-time and stale markers.
|
|
58
|
+
- Same-origin SSE updates the UI after connection, catalog, health, tool, governance, or scope changes. Event payloads contain no connection identifiers, endpoints, errors, or credentials.
|
|
59
|
+
- Progressive `mcp_connector_tool_search` and `mcp_connector_tool_detail` tools search last-success metadata and reveal a bounded, sanitized input schema on demand without executing the target MCP tool.
|
|
58
60
|
- Curated prompt templates that can open a DSH conversation and prefill its draft; missing variables are requested before the prompt is sent.
|
|
59
61
|
- Persistent connection lifecycle management: restore on restart, enable/disable, disconnect, retry transient OAuth refresh failures with bounded backoff, and revoke authorization. Cards declaring issuer-level sharing reuse one grant; a cross-process lock and per-grant atomic journal prevent Desktop and Web hosts from consuming or overwriting the same rotating refresh token.
|
|
60
62
|
- In-place configuration editing for custom and JSON-imported connections: edit normalized JSON from Installed and reconnect without deleting the connection. Existing sensitive values use local keep markers, and validation, startup, or persistence failures leave the previous connection usable.
|
|
@@ -68,7 +70,7 @@ If the plugin helps you connect an MCP server faster, consider [starring the rep
|
|
|
68
70
|
- Explicit, non-destructive migration from the two earlier Qichacha OAuth plugins, plus active-plugin conflict detection that blocks duplicate server management and credential overwrites.
|
|
69
71
|
|
|
70
72
|
<!-- catalog-stats:start -->
|
|
71
|
-
As of 2026-09-
|
|
73
|
+
As of 2026-09-11, the public Registry publishes 106 connector descriptors. After merging and deduplicating them with the 4 bundled Qichacha cards, the Marketplace exposes 110 cards across 9 business categories. Recommendations remain limited to the four Qichacha cards, PKULaw, and Wind, for 6 featured cards in total. The Registry evolves independently; the badge shown after a client refresh and the live badges above are the authoritative current counts.
|
|
72
74
|
<!-- catalog-stats:end -->
|
|
73
75
|
|
|
74
76
|
## Interface and demo
|
|
@@ -176,7 +178,7 @@ npm run dev:ui
|
|
|
176
178
|
|
|
177
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.
|
|
178
180
|
|
|
179
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
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).
|
|
180
182
|
|
|
181
183
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
182
184
|
|
package/README.md
CHANGED
|
@@ -52,7 +52,8 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
52
52
|
- 侧边栏入口可按当前 profile 隐藏;隐藏后仍可从“设置 → 插件 → 插件配置 → MCP连接器”临时打开现有连接器弹框,用完即关。
|
|
53
53
|
- 图形化市场:默认“全部”按推荐与 9 类业务分类分章节展示,每章先展示 4 张并可展开;分类栏固定可见,单分类页展示全部卡片。
|
|
54
54
|
- 图形化添加:手动 HTTP/stdio、`mcpServers` JSON、连接器描述 URL 三种入口,失败时保留表单并给出修复建议。
|
|
55
|
-
- 连接器详情:精选 Prompt 优先展示,点击可带入 DSH 新会话;工具按 Server
|
|
55
|
+
- 连接器详情:精选 Prompt 优先展示,点击可带入 DSH 新会话;工具按 Server 分组,支持描述、搜索、参数详情和独立滚动。实时发现失败时显示带时间/陈旧标记的最后成功工具缓存。
|
|
56
|
+
- 实时状态:页面通过同源 SSE 接收连接、目录、健康、工具、治理和作用域变化通知并自动刷新;事件不携带连接标识、端点、错误或凭据。
|
|
56
57
|
- Prompt 模板:使用 `{{company}}` 等变量,发送前填写真实查询主体。
|
|
57
58
|
- 三种接入:OAuth 2.0 PKCE、自定义 HTTP/stdio、导入 `mcpServers` JSON;也支持从连接器描述 URL 安装。OAuth 动态注册兼容公共客户端以及 `client_secret_post` / `client_secret_basic` 机密客户端。
|
|
58
59
|
- 市场 Bearer/API Key 连接器先执行 MCP initialize 连通性与凭据校验,全部 HTTP Server 通过后才持久化凭据并进入“已安装”;stdio 卡片可声明多个本机凭据字段及其环境变量映射。
|
|
@@ -67,10 +68,10 @@ dsh plugin --profile web add dsh-mcp-connector
|
|
|
67
68
|
- 插件版本与一键更新:版本发现独立于安装来源;页面通过 Update Provider 适配层探测安全更新能力。DSH Market API v1 是首个适配器,支持进度、稳定失败码、回滚及按宿主能力提供的重启/刷新操作;无可用 Provider 时回退到当前插件市场或 npm。
|
|
68
69
|
- Registry 工具链:Schema/唯一性/密钥审计、MCP/OAuth 无凭据探针、每周健康巡检。
|
|
69
70
|
- 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;检测到旧插件仍启用并管理同名 Server 时阻断重复连接,避免凭据相互覆盖。
|
|
70
|
-
- 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`export_config`、`snapshot`、`install_from_url`、`status`、`scope`、`health_check`、`policy`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list
|
|
71
|
+
- 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`export_config`、`snapshot`、`install_from_url`、`status`、`scope`、`health_check`、`policy`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`、`tool_search`、`tool_detail`。搜索/详情只做渐进式能力发现,不执行目标 MCP 工具。
|
|
71
72
|
|
|
72
73
|
<!-- catalog-stats:start -->
|
|
73
|
-
截至 2026-09-
|
|
74
|
+
截至 2026-09-11,公共 Registry 已发布 106 条连接器描述;与随包的 4 张企查查卡片合并去重后,市场页可浏览 110 张卡片,覆盖企业数据、金融投资、法律合规、开发工具、办公协作、调研分析、设计创意、效率工具、其他 9 类。推荐位严格保留 4 张企查查卡片、北大法宝和 Wind,共 6 张;其他连接器按业务分类展示。Registry 可独立持续更新,实际数量以客户端刷新后的市场页签徽标和上方实时统计徽标为准。
|
|
74
75
|
<!-- catalog-stats:end -->
|
|
75
76
|
|
|
76
77
|
## 界面与演示
|
|
@@ -172,7 +173,7 @@ npm run dev:ui
|
|
|
172
173
|
|
|
173
174
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
174
175
|
|
|
175
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
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
177
|
|
|
177
178
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
178
179
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -99,10 +99,12 @@ OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据
|
|
|
99
99
|
|
|
100
100
|
1. 阅读服务说明、鉴权方式、数据范围与可能产生的费用或副作用。
|
|
101
101
|
2. 在“试试这样用”中选择示例 Prompt;带变量的模板会先要求补齐查询主体等信息。
|
|
102
|
-
3. 展开“工具详情”查看 Server
|
|
102
|
+
3. 展开“工具详情”查看 Server 数量、工具名称与描述,并可搜索工具;点击“参数详情”可按需查看输入 schema;连接后还可按 Connection、Server、Tool 设置治理规则。
|
|
103
103
|
4. 点击“去试试”或 Prompt 的发送按钮,在当前工作区创建或复用空白会话并写入草稿。
|
|
104
104
|
|
|
105
|
-
连接成功后,工具按 `mcp__<serverName>__*`
|
|
105
|
+
连接成功后,工具按 `mcp__<serverName>__*` 前缀提供给模型。工具清单来自服务端,实际数量会随服务商权限和版本变化。成功发现后,插件会在当前 profile 保存一份安全裁剪的工具元数据;实时发现失败时仍显示这份“最后成功缓存”,但保持连接失败/未知状态并标注缓存时间,超过 24 小时还会显示陈旧提示。详情与“已安装”列表会展示最近一次诊断的阶段、稳定错误码、说明、建议动作和最近成功时间。
|
|
106
|
+
|
|
107
|
+
对话中可用 `mcp_connector_tool_search` 按名称、标题或描述搜索缓存,再用 `mcp_connector_tool_detail` 精确读取某个工具的参数 schema。两者只用于发现,不代理或执行 MCP 调用。
|
|
106
108
|
|
|
107
109
|
## 6. 添加自定义连接
|
|
108
110
|
|
|
@@ -233,6 +235,8 @@ OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端
|
|
|
233
235
|
|
|
234
236
|
- 健康摘要和最近成功时间目前只保留在插件进程内;重启后先显示“状态未知”,直至重新检查。
|
|
235
237
|
- HTTP 工具清单使用只读 MCP `tools/list` 发现;stdio 工具清单读取 Host 已注册工具。Host 不提供可观测状态时只能报告未知。
|
|
238
|
+
- 最后成功工具缓存只保存名称、标题、描述和安全裁剪后的输入 schema,不保存调用参数、调用结果、原始错误或凭据;断开、修改连接配置或恢复快照会清理相关缓存。
|
|
239
|
+
- 页面实时状态使用同源 SSE,事件只发送变化类别、序号和时间;断线时浏览器按 EventSource 规则自动重连,页面重新可见时主动刷新一次。
|
|
236
240
|
- 连接凭据、快照、治理规则和作用域历史都存储在当前 profile;project/global 不跨 profile 同步。
|
|
237
241
|
- Workspace 被删除后,原 project-only 绑定保持 fail closed,需手动移动到当前项目或全局。
|
|
238
242
|
- 连接级停用可逆;断开会删除本机连接,并在授权不再共享时尽力撤销 OAuth。非 OAuth 配置可用断开前快照恢复;已撤销的 OAuth 必须重新授权。
|
package/lib/client.js
CHANGED
|
@@ -999,7 +999,23 @@ window.__ModuleLoader__.load({
|
|
|
999
999
|
if (targetWorkspaceId === void 0) {
|
|
1000
1000
|
throw new Error("请先选择一个工作空间,再使用示例 Prompt");
|
|
1001
1001
|
}
|
|
1002
|
-
|
|
1002
|
+
// DSH 0.1.2 将跨 Controller 的导航能力从 workspaces 移到
|
|
1003
|
+
// uiWorkspace;0.1.1 Desktop 仍暴露旧方法。优先使用当前公开能力,
|
|
1004
|
+
// 再兼容旧版,最后以 sessions.create 保证未来移除桥接方法时仍能
|
|
1005
|
+
// 在目标 Workspace 创建原生会话。
|
|
1006
|
+
const uiWorkspace = typeof ctx.get === "function"
|
|
1007
|
+
? ctx.get("uiWorkspace")
|
|
1008
|
+
: ctx.uiWorkspace;
|
|
1009
|
+
let sessionId;
|
|
1010
|
+
if (typeof uiWorkspace?.connectWorkspace === "function") {
|
|
1011
|
+
sessionId = await uiWorkspace.connectWorkspace(targetWorkspaceId);
|
|
1012
|
+
} else if (typeof ctx.workspaces?.connectWorkspace === "function") {
|
|
1013
|
+
sessionId = await ctx.workspaces.connectWorkspace(targetWorkspaceId);
|
|
1014
|
+
} else if (typeof ctx.sessions?.create === "function") {
|
|
1015
|
+
sessionId = await ctx.sessions.create({ workspaceId: targetWorkspaceId });
|
|
1016
|
+
} else {
|
|
1017
|
+
throw new Error("当前 DSH 版本没有可用的工作区会话创建能力,请升级 DSH 后重试");
|
|
1018
|
+
}
|
|
1003
1019
|
const conversation = ctx.get("conversation");
|
|
1004
1020
|
if (conversation === void 0) {
|
|
1005
1021
|
throw new Error("DSH 对话服务尚未就绪,请稍后重试");
|
package/lib/index.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* - 连接器目录(内置 + 远程 registry + config 注入 + 本地上下架覆盖)
|
|
6
6
|
* - 三通道连接:通用 OAuth(PKCE) / 自定义配置 / 粘贴 JSON / URL 安装
|
|
7
7
|
* - 连接记录与 OAuth grant 持久化,重启恢复与跨 Host Token 轮换
|
|
8
|
+
* - 实时状态 SSE、最后成功工具缓存与渐进式工具搜索/详情
|
|
8
9
|
* - 通过 ctx.loader 动态配置 @deepseek-ai/dsh-mcp-client 条目
|
|
9
10
|
* - 对话工具:mcp_connector_catalog / connect / configure / import_json /
|
|
10
11
|
* export_config / snapshots / install_from_url / status / rename / set_enabled /
|
|
11
|
-
* disconnect / refresh_catalog / publish
|
|
12
|
+
* disconnect / refresh_catalog / publish / tools_list / tool_search / tool_detail
|
|
12
13
|
*/
|
|
13
14
|
import { randomUUID } from 'node:crypto';
|
|
14
15
|
import z from '@deepseek-ai/schemastery';
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
DEFAULT_REFRESH_RETRY_MAX_MS,
|
|
24
25
|
DEFAULT_CATALOG_TTL_MS,
|
|
25
26
|
} from './constants.js';
|
|
26
|
-
import { defineConnectorDomain, ConnectionStore, GrantStore, CatalogStore, SnapshotStore, GovernanceStore, ConnectionScopeStore } from './stores.js';
|
|
27
|
+
import { defineConnectorDomain, ConnectionStore, GrantStore, CatalogStore, SnapshotStore, GovernanceStore, ConnectionScopeStore, ToolCatalogStore } from './stores.js';
|
|
27
28
|
import { normalizeConnectorDescriptor, normalizeConnectionRecord } from './schema.js';
|
|
28
29
|
import { loadBundledCatalog, fetchRemoteCatalogWithFallback, mergeCatalog, listCatalog, auditDescriptor, auditRawDescriptor, readLimitedJson } from './catalog.js';
|
|
29
30
|
import { describeOAuthAuthorizationError, oauthAuthorize } from './connectors/oauth-connector.js';
|
|
@@ -68,6 +69,14 @@ import {
|
|
|
68
69
|
scopeTargets,
|
|
69
70
|
} from './connection-scopes.js';
|
|
70
71
|
import { installConnectorSettings } from './settings.js';
|
|
72
|
+
import {
|
|
73
|
+
connectionToolCacheSignature,
|
|
74
|
+
createToolCatalogRecord,
|
|
75
|
+
findToolDetails,
|
|
76
|
+
searchToolCatalog,
|
|
77
|
+
toolCatalogCacheView,
|
|
78
|
+
} from './tool-catalog-cache.js';
|
|
79
|
+
import { observeStatusEvents, StatusEventHub } from './status-events.js';
|
|
71
80
|
|
|
72
81
|
export const name = 'mcp-connector';
|
|
73
82
|
|
|
@@ -137,6 +146,7 @@ export async function apply(ctx, config) {
|
|
|
137
146
|
});
|
|
138
147
|
const catalogStore = new CatalogStore(domain);
|
|
139
148
|
const snapshotStore = new SnapshotStore(domain);
|
|
149
|
+
const toolCatalogStore = new ToolCatalogStore(domain);
|
|
140
150
|
const governanceStore = new GovernanceStore(domain);
|
|
141
151
|
const governancePolicies = new GovernancePolicyService(governanceStore);
|
|
142
152
|
await governancePolicies.load();
|
|
@@ -155,7 +165,9 @@ export async function apply(ctx, config) {
|
|
|
155
165
|
oauthConnects: new Map(), // sharingKey -> { promise, requestedConnectorIds }(防同 issuer 重复弹窗)
|
|
156
166
|
warnedLegacyConflicts: new Set(),
|
|
157
167
|
toolInventories: new Map(), // serverName -> { observed, observedAt, tools: Set<rawName> }
|
|
168
|
+
toolCatalogCache: new Map(), // connection key -> 最近成功、安全裁剪后的工具元数据
|
|
158
169
|
};
|
|
170
|
+
const statusEvents = new StatusEventHub();
|
|
159
171
|
|
|
160
172
|
const hostGovernance = createHostGovernanceController(ctx, {
|
|
161
173
|
getRules: () => governancePolicies.snapshot().rules,
|
|
@@ -421,6 +433,7 @@ export async function apply(ctx, config) {
|
|
|
421
433
|
});
|
|
422
434
|
}
|
|
423
435
|
scheduleRefresh(grantKey);
|
|
436
|
+
statusEvents.publish('status');
|
|
424
437
|
return next;
|
|
425
438
|
})();
|
|
426
439
|
try {
|
|
@@ -472,6 +485,7 @@ export async function apply(ctx, config) {
|
|
|
472
485
|
entry.refreshRetryAt = Date.now() + delay;
|
|
473
486
|
scheduleRefresh(grantKey, { delayMs: delay });
|
|
474
487
|
}
|
|
488
|
+
statusEvents.publish('status');
|
|
475
489
|
return failure;
|
|
476
490
|
}
|
|
477
491
|
|
|
@@ -494,6 +508,7 @@ export async function apply(ctx, config) {
|
|
|
494
508
|
logger.info(`OAuth grant ${grantKey} adopted a newer cross-process journal token`);
|
|
495
509
|
if (persisted.accessTokenExpiresAt > Date.now()) {
|
|
496
510
|
scheduleRefresh(grantKey);
|
|
511
|
+
statusEvents.publish('status');
|
|
497
512
|
return persisted;
|
|
498
513
|
}
|
|
499
514
|
}
|
|
@@ -523,6 +538,7 @@ export async function apply(ctx, config) {
|
|
|
523
538
|
try {
|
|
524
539
|
if (persisted.accessTokenExpiresAt > Date.now()) {
|
|
525
540
|
scheduleRefresh(grantKey);
|
|
541
|
+
statusEvents.publish('status');
|
|
526
542
|
return persisted;
|
|
527
543
|
}
|
|
528
544
|
return await grantStore.withRefreshLock(grantKey, () => refreshGrant(grantKey), {
|
|
@@ -609,6 +625,7 @@ export async function apply(ctx, config) {
|
|
|
609
625
|
throw wrapped;
|
|
610
626
|
}
|
|
611
627
|
state.connections.delete(record.key);
|
|
628
|
+
await deleteLastSuccessfulTools(record.key);
|
|
612
629
|
clearRecordHealth(record);
|
|
613
630
|
}
|
|
614
631
|
|
|
@@ -957,6 +974,7 @@ export async function apply(ctx, config) {
|
|
|
957
974
|
publicName: schema.name,
|
|
958
975
|
title: schema.title || schema.name.slice(prefix.length),
|
|
959
976
|
description: schema.description || '',
|
|
977
|
+
inputSchema: schema.parameters ?? {},
|
|
960
978
|
}));
|
|
961
979
|
return { supported: true, tools };
|
|
962
980
|
} catch (error) {
|
|
@@ -965,6 +983,48 @@ export async function apply(ctx, config) {
|
|
|
965
983
|
}
|
|
966
984
|
}
|
|
967
985
|
|
|
986
|
+
async function cacheLastSuccessfulTools(record, tools) {
|
|
987
|
+
const entry = createToolCatalogRecord(record, tools);
|
|
988
|
+
state.toolCatalogCache.set(record.key, entry);
|
|
989
|
+
if (config.persistSecrets) {
|
|
990
|
+
try {
|
|
991
|
+
state.toolCatalogCache.set(record.key, await toolCatalogStore.put(entry));
|
|
992
|
+
} catch (error) {
|
|
993
|
+
logger.warn(`persist tool catalog "${record.key}" failed: ${error.message}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return state.toolCatalogCache.get(record.key);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function deleteLastSuccessfulTools(key) {
|
|
1000
|
+
state.toolCatalogCache.delete(key);
|
|
1001
|
+
if (config.persistSecrets) {
|
|
1002
|
+
await toolCatalogStore.delete(key).catch((error) => {
|
|
1003
|
+
logger.warn(`delete tool catalog "${key}" failed: ${error.message}`);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function cachedToolsForRecord(record) {
|
|
1009
|
+
const entry = state.toolCatalogCache.get(record.key);
|
|
1010
|
+
if (!entry
|
|
1011
|
+
|| entry.connectorId !== record.connectorId
|
|
1012
|
+
|| entry.serverName !== record.serverName
|
|
1013
|
+
|| entry.connectionSignature !== connectionToolCacheSignature(record)) return null;
|
|
1014
|
+
return { entry, ...toolCatalogCacheView(entry) };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function visibleToolCatalogEntries(workspaceId) {
|
|
1018
|
+
const entries = [];
|
|
1019
|
+
for (const record of state.connections.values()) {
|
|
1020
|
+
if (record.enabled === false) continue;
|
|
1021
|
+
if (workspaceId && !connectionVisibleInWorkspace(connectionScopes.binding(record.key), workspaceId)) continue;
|
|
1022
|
+
const cached = cachedToolsForRecord(record);
|
|
1023
|
+
if (cached) entries.push(cached.entry);
|
|
1024
|
+
}
|
|
1025
|
+
return entries;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
968
1028
|
function readinessResults(records, validationResults = []) {
|
|
969
1029
|
return records.map((record, index) => {
|
|
970
1030
|
const validated = validationResults[index];
|
|
@@ -2168,6 +2228,7 @@ export async function apply(ctx, config) {
|
|
|
2168
2228
|
.map((key) => state.connections.get(key)?.auth?.grantKey)
|
|
2169
2229
|
.filter(Boolean));
|
|
2170
2230
|
const restored = await replaceConnectionSet(snapshot.targetKeys, snapshot.records);
|
|
2231
|
+
for (const key of snapshot.targetKeys) await deleteLastSuccessfulTools(key);
|
|
2171
2232
|
let retiredGrantCount = 0;
|
|
2172
2233
|
for (const grantKey of previousGrantKeys) {
|
|
2173
2234
|
if (await retireGrantIfUnused(grantKey, { revoke: true })) retiredGrantCount += 1;
|
|
@@ -2444,6 +2505,7 @@ export async function apply(ctx, config) {
|
|
|
2444
2505
|
};
|
|
2445
2506
|
}
|
|
2446
2507
|
const { committed: [committed], snapshot } = await commitWithSnapshot([next], 'reconfigure');
|
|
2508
|
+
await deleteLastSuccessfulTools(committed.key);
|
|
2447
2509
|
if (committed.connectorId && committed.enabled !== false) {
|
|
2448
2510
|
cacheHealth(committed.connectorId, [committed], readinessResults([committed], validation.detail.results));
|
|
2449
2511
|
}
|
|
@@ -2579,6 +2641,70 @@ export async function apply(ctx, config) {
|
|
|
2579
2641
|
return { ok: true, message: `已将 "${connector.name}" ${published ? '上架' : '下架'}` };
|
|
2580
2642
|
},
|
|
2581
2643
|
|
|
2644
|
+
async toolSearch(input = {}) {
|
|
2645
|
+
try {
|
|
2646
|
+
const connectorId = String(input.connectorId ?? '').trim() || undefined;
|
|
2647
|
+
if (connectorId && !state.merged.some((connector) => connector.id === connectorId)) {
|
|
2648
|
+
return { ok: false, message: `连接器 "${connectorId}" 不存在` };
|
|
2649
|
+
}
|
|
2650
|
+
const items = searchToolCatalog(visibleToolCatalogEntries(input.workspaceId), {
|
|
2651
|
+
query: input.query,
|
|
2652
|
+
connectorId,
|
|
2653
|
+
limit: input.limit,
|
|
2654
|
+
});
|
|
2655
|
+
const staleCount = items.filter((item) => item.stale).length;
|
|
2656
|
+
return {
|
|
2657
|
+
ok: true,
|
|
2658
|
+
message: `最后成功工具缓存中找到 ${items.length} 个匹配项${staleCount ? `,其中 ${staleCount} 个已超过 24 小时` : ''}`,
|
|
2659
|
+
detail: {
|
|
2660
|
+
items,
|
|
2661
|
+
source: 'last-success-cache',
|
|
2662
|
+
discoveryOnly: true,
|
|
2663
|
+
credentialsStored: false,
|
|
2664
|
+
},
|
|
2665
|
+
};
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
return { ok: false, message: `搜索工具失败: ${error.message}` };
|
|
2668
|
+
}
|
|
2669
|
+
},
|
|
2670
|
+
|
|
2671
|
+
async toolDetail(input = {}) {
|
|
2672
|
+
try {
|
|
2673
|
+
const connectorId = String(input.connectorId ?? '').trim() || undefined;
|
|
2674
|
+
const serverName = String(input.serverName ?? '').trim() || undefined;
|
|
2675
|
+
const matches = findToolDetails(visibleToolCatalogEntries(input.workspaceId), {
|
|
2676
|
+
toolName: input.toolName,
|
|
2677
|
+
connectorId,
|
|
2678
|
+
serverName,
|
|
2679
|
+
});
|
|
2680
|
+
if (matches.length === 0) {
|
|
2681
|
+
return { ok: false, message: `最后成功工具缓存中找不到 "${String(input.toolName ?? '').trim()}"` };
|
|
2682
|
+
}
|
|
2683
|
+
if (matches.length > 1) {
|
|
2684
|
+
return {
|
|
2685
|
+
ok: false,
|
|
2686
|
+
message: `找到 ${matches.length} 个同名工具,请补充 connectorId 或 serverName`,
|
|
2687
|
+
detail: {
|
|
2688
|
+
candidates: matches.map(({ connectorId: id, serverName: server, name }) => ({ connectorId: id, serverName: server, name })),
|
|
2689
|
+
discoveryOnly: true,
|
|
2690
|
+
},
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
ok: true,
|
|
2695
|
+
message: `工具详情:${matches[0].serverName}/${matches[0].name}${matches[0].stale ? '(缓存已超过 24 小时)' : ''}`,
|
|
2696
|
+
detail: {
|
|
2697
|
+
tool: matches[0],
|
|
2698
|
+
source: 'last-success-cache',
|
|
2699
|
+
discoveryOnly: true,
|
|
2700
|
+
credentialsStored: false,
|
|
2701
|
+
},
|
|
2702
|
+
};
|
|
2703
|
+
} catch (error) {
|
|
2704
|
+
return { ok: false, message: `读取工具详情失败: ${error.message}` };
|
|
2705
|
+
}
|
|
2706
|
+
},
|
|
2707
|
+
|
|
2582
2708
|
/**
|
|
2583
2709
|
* 从已连接的 MCP server 动态获取工具清单
|
|
2584
2710
|
* 通过 MCP JSON-RPC 协议调用 tools/list 方法
|
|
@@ -2617,13 +2743,22 @@ export async function apply(ctx, config) {
|
|
|
2617
2743
|
const snapshot = (connector.toolsSnapshot ?? []).find((item) =>
|
|
2618
2744
|
item.serverKey === record.serverKey || item.serverName === record.serverName);
|
|
2619
2745
|
const liveTools = registered.tools;
|
|
2746
|
+
let cached = cachedToolsForRecord(record);
|
|
2620
2747
|
if (liveTools.length > 0) {
|
|
2621
2748
|
state.toolInventories.set(record.serverName, {
|
|
2622
2749
|
observed: true,
|
|
2623
2750
|
observedAt: Date.now(),
|
|
2624
2751
|
tools: new Set(liveTools.map((tool) => tool.name)),
|
|
2625
2752
|
});
|
|
2753
|
+
await cacheLastSuccessfulTools(record, liveTools);
|
|
2754
|
+
cached = cachedToolsForRecord(record);
|
|
2626
2755
|
}
|
|
2756
|
+
const cachedFallback = liveTools.length === 0 ? cached : null;
|
|
2757
|
+
const displayTools = liveTools.length > 0
|
|
2758
|
+
? liveTools
|
|
2759
|
+
: cachedFallback?.entry.tools?.length > 0
|
|
2760
|
+
? cachedFallback.entry.tools
|
|
2761
|
+
: snapshot?.tools ?? [];
|
|
2627
2762
|
const diagnosticCode = registered.error
|
|
2628
2763
|
? 'host-status-error'
|
|
2629
2764
|
: registered.supported ? 'host-tools-pending' : 'host-status-unavailable';
|
|
@@ -2633,17 +2768,23 @@ export async function apply(ctx, config) {
|
|
|
2633
2768
|
ok: liveTools.length > 0,
|
|
2634
2769
|
managed: true,
|
|
2635
2770
|
live: liveTools.length > 0,
|
|
2636
|
-
|
|
2771
|
+
cached: Boolean(cachedFallback),
|
|
2772
|
+
preview: liveTools.length === 0 && !cachedFallback && Boolean(snapshot),
|
|
2637
2773
|
unknown: liveTools.length === 0,
|
|
2638
2774
|
diagnosticCode,
|
|
2639
2775
|
policy: serverPolicy(record),
|
|
2640
|
-
tools: (
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2776
|
+
tools: displayTools.map((tool) => decorateToolPolicy(record, {
|
|
2777
|
+
name: tool.name,
|
|
2778
|
+
...(tool.publicName ? { publicName: tool.publicName } : {}),
|
|
2779
|
+
title: tool.title || tool.name,
|
|
2780
|
+
description: tool.description || '',
|
|
2781
|
+
}, observedNames)),
|
|
2782
|
+
...(cachedFallback ? {
|
|
2783
|
+
cacheObservedAt: cachedFallback.observedAt,
|
|
2784
|
+
cacheAgeMs: cachedFallback.cacheAgeMs,
|
|
2785
|
+
stale: cachedFallback.stale,
|
|
2786
|
+
cacheTruncated: cachedFallback.entry.truncated === true,
|
|
2787
|
+
} : {}),
|
|
2647
2788
|
...(liveTools.length === 0 ? {
|
|
2648
2789
|
errorKind: registered.supported ? 'startup' : 'host-version',
|
|
2649
2790
|
error: registered.supported
|
|
@@ -2663,6 +2804,7 @@ export async function apply(ctx, config) {
|
|
|
2663
2804
|
observedAt: Date.now(),
|
|
2664
2805
|
tools: new Set(listed.tools.map((tool) => tool.name)),
|
|
2665
2806
|
});
|
|
2807
|
+
await cacheLastSuccessfulTools(record, listed.tools);
|
|
2666
2808
|
servers.push({
|
|
2667
2809
|
serverKey: record.serverKey || record.name,
|
|
2668
2810
|
serverName: record.serverName,
|
|
@@ -2675,6 +2817,19 @@ export async function apply(ctx, config) {
|
|
|
2675
2817
|
}, observedNames)),
|
|
2676
2818
|
});
|
|
2677
2819
|
} catch (error) {
|
|
2820
|
+
const cached = cachedToolsForRecord(record);
|
|
2821
|
+
const cachedFields = cached ? {
|
|
2822
|
+
cached: true,
|
|
2823
|
+
cacheObservedAt: cached.observedAt,
|
|
2824
|
+
cacheAgeMs: cached.cacheAgeMs,
|
|
2825
|
+
stale: cached.stale,
|
|
2826
|
+
cacheTruncated: cached.entry.truncated === true,
|
|
2827
|
+
tools: cached.entry.tools.map((tool) => decorateToolPolicy(record, {
|
|
2828
|
+
name: tool.name,
|
|
2829
|
+
title: tool.title || tool.name,
|
|
2830
|
+
description: tool.description || '',
|
|
2831
|
+
}, observedNames)),
|
|
2832
|
+
} : { tools: [] };
|
|
2678
2833
|
if (error instanceof McpHttpError) {
|
|
2679
2834
|
servers.push({
|
|
2680
2835
|
serverKey: record.serverKey || record.name,
|
|
@@ -2683,12 +2838,12 @@ export async function apply(ctx, config) {
|
|
|
2683
2838
|
error: error.message,
|
|
2684
2839
|
errorKind: error.kind,
|
|
2685
2840
|
policy: serverPolicy(record),
|
|
2686
|
-
|
|
2841
|
+
...cachedFields,
|
|
2687
2842
|
});
|
|
2688
2843
|
continue;
|
|
2689
2844
|
}
|
|
2690
2845
|
const classified = classifyConnectionError(error);
|
|
2691
|
-
servers.push({ serverKey: record.serverKey || record.name, serverName: record.serverName, ok: false, error: classified.message, errorKind: classified.kind, policy: serverPolicy(record),
|
|
2846
|
+
servers.push({ serverKey: record.serverKey || record.name, serverName: record.serverName, ok: false, error: classified.message, errorKind: classified.kind, policy: serverPolicy(record), ...cachedFields });
|
|
2692
2847
|
}
|
|
2693
2848
|
}
|
|
2694
2849
|
|
|
@@ -2697,6 +2852,7 @@ export async function apply(ctx, config) {
|
|
|
2697
2852
|
const unknownServers = servers.filter((server) => server.unknown).length;
|
|
2698
2853
|
const failedServers = servers.length - availableServers - unknownServers;
|
|
2699
2854
|
const pendingManagedServers = unknownServers;
|
|
2855
|
+
const cachedServers = servers.filter((server) => server.cached).length;
|
|
2700
2856
|
const health = cacheHealth(connectorId, records, servers.map((server) => server.ok
|
|
2701
2857
|
? { ok: true, kind: 'connected', serverKey: server.serverKey, serverName: server.serverName }
|
|
2702
2858
|
: server.unknown
|
|
@@ -2716,7 +2872,7 @@ export async function apply(ctx, config) {
|
|
|
2716
2872
|
return {
|
|
2717
2873
|
ok: availableServers > 0,
|
|
2718
2874
|
message: failedServers > 0
|
|
2719
|
-
? `工具加载失败:${failureSummary}${failedServers > 3 ? `;另有 ${failedServers - 3} 个 Server 不可用` : ''}${availableServers > 0 ? `;${availableServers} 个 Server 仍可用` : ''}`
|
|
2875
|
+
? `工具加载失败:${failureSummary}${failedServers > 3 ? `;另有 ${failedServers - 3} 个 Server 不可用` : ''}${availableServers > 0 ? `;${availableServers} 个 Server 仍可用` : ''}${cachedServers > 0 ? `;${cachedServers} 个 Server 显示最后成功缓存` : ''}`
|
|
2720
2876
|
: unknownServers > 0
|
|
2721
2877
|
? `工具状态未知:${unknownSummary}${unknownServers > 3 ? `;另有 ${unknownServers - 3} 个 Server 尚未确认` : ''}`
|
|
2722
2878
|
: `共 ${servers.length} 个 server,${totalTools} 个工具`,
|
|
@@ -2728,6 +2884,7 @@ export async function apply(ctx, config) {
|
|
|
2728
2884
|
failedServers,
|
|
2729
2885
|
unknownServers,
|
|
2730
2886
|
pendingManagedServers,
|
|
2887
|
+
cachedServers,
|
|
2731
2888
|
connectionState: health.connectionState,
|
|
2732
2889
|
lastSuccessfulAt: health.lastSuccessfulAt,
|
|
2733
2890
|
diagnostic: health.diagnostic,
|
|
@@ -2755,6 +2912,19 @@ export async function apply(ctx, config) {
|
|
|
2755
2912
|
logger.warn(`skip invalid stored connection "${key}": ${error.message}`);
|
|
2756
2913
|
}
|
|
2757
2914
|
}
|
|
2915
|
+
if (config.persistSecrets) {
|
|
2916
|
+
for (const [key, cached] of await toolCatalogStore.entries().catch(() => [])) {
|
|
2917
|
+
const record = state.connections.get(key);
|
|
2918
|
+
if (record
|
|
2919
|
+
&& cached.connectorId === record.connectorId
|
|
2920
|
+
&& cached.serverName === record.serverName
|
|
2921
|
+
&& cached.connectionSignature === connectionToolCacheSignature(record)) {
|
|
2922
|
+
state.toolCatalogCache.set(key, cached);
|
|
2923
|
+
} else {
|
|
2924
|
+
await toolCatalogStore.delete(key).catch(() => {});
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2758
2928
|
for (const [key, raw] of await grantStore.entries().catch(() => [])) {
|
|
2759
2929
|
try {
|
|
2760
2930
|
state.grants.set(key, grantRuntime(raw));
|
|
@@ -2812,7 +2982,8 @@ export async function apply(ctx, config) {
|
|
|
2812
2982
|
hostGovernance.refresh();
|
|
2813
2983
|
hostScopes.refresh();
|
|
2814
2984
|
|
|
2815
|
-
const
|
|
2985
|
+
const observableApi = observeStatusEvents(api, statusEvents);
|
|
2986
|
+
const disposeTools = registerTools(ctx, observableApi);
|
|
2816
2987
|
|
|
2817
2988
|
/* ───────────────────────── web 半区(图形化市场)──────────────────────── */
|
|
2818
2989
|
// webServer / webRuntime 仅存在于 web 部署;无此服务时静默跳过(纯对话工具场景仍可用)。
|
|
@@ -2820,7 +2991,7 @@ export async function apply(ctx, config) {
|
|
|
2820
2991
|
if (typeof ctx.inject === 'function') {
|
|
2821
2992
|
try {
|
|
2822
2993
|
ctx.inject(['webServer', 'webRuntime'], (wctx) => {
|
|
2823
|
-
disposeWeb = mountWebRoutes(wctx,
|
|
2994
|
+
disposeWeb = mountWebRoutes(wctx, observableApi, { logger, eventHub: statusEvents });
|
|
2824
2995
|
logger.info('mcp-connector web UI mounted at /mcp-connector/ui/');
|
|
2825
2996
|
});
|
|
2826
2997
|
} catch (error) {
|
|
@@ -2833,6 +3004,7 @@ export async function apply(ctx, config) {
|
|
|
2833
3004
|
disposeWeb?.();
|
|
2834
3005
|
hostGovernance.dispose();
|
|
2835
3006
|
hostScopes.dispose();
|
|
3007
|
+
statusEvents.dispose();
|
|
2836
3008
|
versionStatusService.dispose();
|
|
2837
3009
|
for (const g of state.grants.values()) if (g.timer) clearTimeout(g.timer);
|
|
2838
3010
|
});
|
package/lib/schema.js
CHANGED
|
@@ -190,6 +190,28 @@ export const connectionScopeDocumentSchema = z.object({
|
|
|
190
190
|
history: z.array(connectionScopeRevisionSchema),
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* 最近一次成功 tools/list 的安全能力快照。
|
|
195
|
+
* 这里只允许工具元数据与经过裁剪的输入 schema;调用参数、调用结果、凭据和原始错误均不入库。
|
|
196
|
+
*/
|
|
197
|
+
export const toolCatalogRecordSchema = z.object({
|
|
198
|
+
key: z.string().min(1).max(512),
|
|
199
|
+
connectorId: z.string().min(1).max(256),
|
|
200
|
+
serverKey: z.string().min(1).max(256).optional(),
|
|
201
|
+
serverName: z.string().min(1).max(256),
|
|
202
|
+
connectionSignature: z.string().regex(/^[a-f0-9]{64}$/),
|
|
203
|
+
observedAt: z.number().int().nonnegative(),
|
|
204
|
+
updatedAt: z.number().int().nonnegative(),
|
|
205
|
+
truncated: z.boolean().optional(),
|
|
206
|
+
tools: z.array(z.object({
|
|
207
|
+
name: z.string().min(1).max(512),
|
|
208
|
+
title: z.string().max(1024).optional(),
|
|
209
|
+
description: z.string().max(4096).optional(),
|
|
210
|
+
inputSchema: z.unknown().optional(),
|
|
211
|
+
schemaTruncated: z.boolean().optional(),
|
|
212
|
+
})).max(512),
|
|
213
|
+
});
|
|
214
|
+
|
|
193
215
|
/** OAuth 授权记录(通用多厂商) */
|
|
194
216
|
export const grantRecordSchema = z.object({
|
|
195
217
|
key: z.string().min(1),
|