dsh-mcp-connector 0.2.22 → 0.2.24

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,33 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.24] - 2026-08-27
8
+
9
+ ### Added
10
+
11
+ - MCP连接器标题显示当前插件版本,并由服务端缓存检查 npm `latest` 与 GitHub Release;发现新版本时引导用户前往 DSH 插件市场安全更新。
12
+
13
+ ### Changed
14
+
15
+ - 市场页原“刷新”按钮改为“刷新连接器目录”,明确该操作仅更新 Registry 卡片,不会升级插件本身。
16
+
17
+ ### Verification
18
+
19
+ - 123 项自动测试、lint、npm 发布包白名单/敏感内容扫描和 npm/GitHub 真实版本源验收通过。
20
+
21
+ ## [0.2.23] - 2026-08-26
22
+
23
+ ### Fixed
24
+
25
+ - OAuth Grant 现在保存 Access Token 的真实过期时间,并在提前刷新遇到网络、发现端点或服务端暂时故障时采用有上限的指数退避自动恢复;只有 `invalid_grant`、`invalid_client` 等明确不可恢复错误才显示“需重新授权”。
26
+ - 启动恢复不再吞掉 Token 刷新异常;脱敏日志会记录阶段、OAuth 错误码、HTTP 状态与永久/暂时分类,且不会输出 Access Token、Refresh Token 或客户端密钥。
27
+ - `grantSharing: "issuer"` 正式生效:同账号、同 issuer/scope 的卡片共享动态客户端与 Grant,后续连接同组卡片不再重复打开授权页;重新授权会把同组已连接卡片迁移到新 Grant。
28
+ - 启动和连接前检测仍启用的 `qcc-mcp-oauth` / `qcc-legal-mcp-oauth` 及其残留 mcp-client 条目;发现同名 Server 时阻断新连接,避免旧 Token 覆盖新连接器凭据。
29
+
30
+ ### Verification
31
+
32
+ - 新增 OAuth 刷新失败分类/脱敏/退避、暂时故障自动恢复、永久失效重新授权、同 issuer 顺序与并发共享授权、旧插件冲突阻断回归测试。
33
+
7
34
  ## [0.2.22] - 2026-08-26
8
35
 
9
36
  ### Fixed
@@ -372,7 +399,9 @@
372
399
  - 外部 URL 与导入 Header 执行安全校验。
373
400
  - iframe 消息校验同源和消息来源。
374
401
 
375
- [Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.22...HEAD
402
+ [Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.24...HEAD
403
+ [0.2.24]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.23...v0.2.24
404
+ [0.2.23]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.22...v0.2.23
376
405
  [0.2.22]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.21...v0.2.22
377
406
  [0.2.21]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.20...v0.2.21
378
407
  [0.2.20]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.19...v0.2.20
package/README.en.md CHANGED
@@ -25,10 +25,11 @@ Browse and install MCP connectors from different providers in DeepSeek Harness D
25
25
  - 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.
26
26
  - Dynamic tool discovery grouped by MCP server, including descriptions, search, batched rendering, and an independent scroll region.
27
27
  - Curated prompt templates that can open a DSH conversation and prefill its draft; missing variables are requested before the prompt is sent.
28
- - Persistent connection lifecycle management: restore on restart, enable/disable, disconnect, refresh OAuth tokens, and revoke authorization. DCR client secrets are kept with the local grant only.
28
+ - 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; DCR client secrets remain local to that grant.
29
29
  - Built-in, remote, and local catalogs with `published` and `featured` controls.
30
30
  - A standalone remote Registry, allowing new marketplace cards to appear after refresh without publishing a new npm version.
31
- - Explicit, non-destructive migration of authorization from the two earlier Qichacha OAuth plugins.
31
+ - Plugin version awareness: the MCP Connector title shows the installed version, while a server-side cached check compares npm `latest` with GitHub Releases and routes available updates through the DSH plugin marketplace.
32
+ - Explicit, non-destructive migration from the two earlier Qichacha OAuth plugins, plus active-plugin conflict detection that blocks duplicate server management and credential overwrites.
32
33
 
33
34
  <!-- catalog-stats:start -->
34
35
  As of 2026-08-25, the public Registry publishes 78 connector descriptors. After merging and deduplicating them with the 4 bundled Qichacha cards, the Marketplace exposes 82 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.
@@ -111,7 +112,7 @@ npm run dev:ui
111
112
 
112
113
  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.
113
114
 
114
- The current public version is [`dsh-mcp-connector@0.2.22`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.22](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.22).
115
+ The current public version is [`dsh-mcp-connector@0.2.24`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.24](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.24).
115
116
 
116
117
  See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
117
118
 
package/README.md CHANGED
@@ -25,11 +25,12 @@
25
25
  - Prompt 模板:使用 `{{company}}` 等变量,发送前填写真实查询主体。
26
26
  - 三种接入:OAuth 2.0 PKCE、自定义 HTTP/stdio、导入 `mcpServers` JSON;也支持从连接器描述 URL 安装。OAuth 动态注册兼容公共客户端以及 `client_secret_post` / `client_secret_basic` 机密客户端。
27
27
  - 市场 Bearer/API Key 连接器先执行 MCP initialize 连通性与凭据校验,全部 HTTP Server 通过后才持久化凭据并进入“已安装”;stdio 卡片可声明多个本机凭据字段及其环境变量映射。
28
- - 生命周期管理:连接持久化、重启恢复、启停、断开、OAuth 刷新与撤销;DCR 返回的客户端密钥与 Token 一同只保存在本机。
28
+ - 生命周期管理:连接持久化、重启恢复、启停、断开、OAuth 自动刷新/退避恢复与撤销;同 issuer 卡片可共享一次授权,DCR 返回的客户端密钥与 Token 一同只保存在本机。
29
29
  - 目录运营:内置目录、远程 registry、本地覆盖,支持 `published` 上下架与 `featured` 精选。
30
30
  - 独立远程 Registry:新市场卡片合并后客户端刷新即可见,无需重新发布 npm;远程不可用时自动回退内置目录。
31
+ - 插件版本感知:MCP连接器标题显示当前版本,服务端缓存检查 npm 与 GitHub Release;发现 npm 新版本时引导到 DSH 插件市场更新。
31
32
  - Registry 工具链:Schema/唯一性/密钥审计、MCP/OAuth 无凭据探针、每周健康巡检。
32
- - 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;不删除原插件或原凭据。
33
+ - 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;检测到旧插件仍启用并管理同名 Server 时阻断重复连接,避免凭据相互覆盖。
33
34
  - 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`install_from_url`、`status`、`health_check`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
34
35
 
35
36
  <!-- catalog-stats:start -->
@@ -106,7 +107,7 @@ npm run dev:ui
106
107
 
107
108
  公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
108
109
 
109
- 当前公开版本为 [`dsh-mcp-connector@0.2.22`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.22](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.22)。
110
+ 当前公开版本为 [`dsh-mcp-connector@0.2.24`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.24](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.24)。
110
111
 
111
112
  版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
112
113
  Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
@@ -42,7 +42,8 @@ dsh web
42
42
  - **已安装**:查看已经保存到本机的连接,页签徽标显示连接数。
43
43
  - **搜索**:按名称、服务商、简介和标签查找。
44
44
  - **添加连接**:导入 JSON、手动配置 HTTP/stdio,或从市场描述 URL 安装。
45
- - **刷新**:重新拉取目录,并更新连接健康状态。
45
+ - **版本与更新提示**:标题旁显示当前插件版本。检测到 npm 有新版本时,点击更新提示前往 DSH 插件市场执行安全升级;升级后按页面提示完全重启 DSH。
46
+ - **刷新连接器目录**:重新拉取 Registry 卡片,并更新连接健康状态;该操作不会升级 MCP连接器插件本身。
46
47
 
47
48
  ## 3. 浏览市场与分类
48
49
 
@@ -63,7 +64,8 @@ dsh web
63
64
  | `连接` | OAuth 或免密连接器尚未连接 | 点击后按页面提示完成授权或连通性检查 |
64
65
  | `配置` | 需要 Bearer Token 或 API Key | 录入服务商签发的凭据并验证 |
65
66
  | `需重新授权` | OAuth 授权过期、被撤销或不可用 | 点击后重新完成 OAuth 授权 |
66
- | `已配置` | 本机已有配置,尚未完成本轮健康确认 | 点击“刷新”或进入详情检查 |
67
+ | `自动重试中` | OAuth Token 刷新遇到网络或服务端暂时故障 | 无需重新授权;保持 DSH 运行,插件会按退避策略自动恢复 |
68
+ | `已配置` | 本机已有配置,尚未完成本轮健康确认 | 点击“刷新连接器目录”或进入详情检查 |
67
69
  | `已连接` | 最近一次健康检查通过 | 可直接在会话中使用对应 MCP 工具 |
68
70
  | `部分异常` / `连接异常` | 一个或多个 Server 未通过检查 | 打开详情查看提示,核对网络、权限和服务状态 |
69
71
 
@@ -73,6 +75,8 @@ stdio 市场卡片也可能显示一个或多个凭据字段,例如 API Token
73
75
 
74
76
  OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据发现。动态客户端注册既支持无需客户端密钥的 `none`,也支持服务商签发密钥的 `client_secret_post` 与 `client_secret_basic`。客户端密钥仅与 OAuth Grant 一同保存在 DSH 本机,用于换取、刷新和撤销 Token;插件不会要求用户把 OAuth Token 或客户端密钥复制到聊天中。
75
77
 
78
+ 当市场描述声明 `grantSharing: "issuer"` 时,同一账号下相同 issuer、scope 和客户端鉴权方式的卡片共享一组 Grant。首次授权仍只启用用户点击的卡片;之后连接同组卡片会直接复用现有授权,不再重复打开 OAuth 页面。网络、OAuth 元数据发现或服务端 5xx 等暂时故障只进入“自动重试中”,明确收到 Refresh Token/客户端失效错误时才进入“需重新授权”。
79
+
76
80
  ## 5. 查看详情、Prompt 与工具
77
81
 
78
82
  点击卡片或“详情”可打开连接器详情:
@@ -137,7 +141,7 @@ stdio 进程由 `@deepseek-ai/dsh-mcp-client` 管理,插件透传 `command`、
137
141
  - 执行健康检查;
138
142
  - 断开连接并删除该连接的本机配置。
139
143
 
140
- OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端点时仍会删除 DSH 本机授权记录。连接状态发生变化后,可点击“刷新”重新检查。
144
+ OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端点时仍会删除 DSH 本机授权记录。连接状态发生变化后,可点击“刷新连接器目录”重新检查。
141
145
 
142
146
  ## 8. 安全边界
143
147
 
@@ -157,7 +161,7 @@ OAuth 断开时,插件会尽力调用服务商的撤销端点;无撤销端
157
161
 
158
162
  ### 市场里没有最新卡片
159
163
 
160
- 点击“刷新”。如果远程 Registry 暂时不可用,页面会继续显示缓存或内置目录;稍后恢复网络再刷新即可。
164
+ 点击“刷新连接器目录”。如果远程 Registry 暂时不可用,页面会继续显示缓存或内置目录;稍后恢复网络再刷新即可。
161
165
 
162
166
  ### Token/API Key 一直验证失败
163
167
 
package/lib/client.js CHANGED
@@ -19,6 +19,9 @@ window.__ModuleLoader__.load({
19
19
  ];
20
20
  const PROMPT_REQUEST_TYPE = "mcp-connector:start-session";
21
21
  const PROMPT_RESULT_TYPE = "mcp-connector:start-session-result";
22
+ const VERSION_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
23
+ const VERSION_CHECK_RETRY_MS = 5 * 60 * 1e3;
24
+ const NPM_PACKAGE_URL = "https://www.npmjs.com/package/dsh-mcp-connector";
22
25
 
23
26
  /**
24
27
  * Desktop 内置的社区插件市场也注册在 sidebar.footer.action。
@@ -86,6 +89,31 @@ window.__ModuleLoader__.load({
86
89
  .mcpConnectorMarketPanel { background: #ffffff; color: #111827; }
87
90
  .mcpConnectorMarketHeader { border-color: #e5e7eb; }
88
91
  .mcpConnectorMarketTitle { color: #111827; }
92
+ .mcpConnectorVersion {
93
+ color: #6b7280;
94
+ background: #f3f4f6;
95
+ border-radius: 999px;
96
+ padding: 2px 8px;
97
+ font-size: 12px;
98
+ font-weight: 600;
99
+ }
100
+ .mcpConnectorUpdateButton {
101
+ color: #4338ca;
102
+ background: #eef2ff;
103
+ border: 1px solid #c7d2fe;
104
+ border-radius: 8px;
105
+ padding: 5px 9px;
106
+ font: inherit;
107
+ font-size: 12px;
108
+ font-weight: 600;
109
+ cursor: pointer;
110
+ }
111
+ .mcpConnectorUpdateButton:hover,
112
+ .mcpConnectorUpdateButton:focus-visible {
113
+ background: #e0e7ff;
114
+ outline: 2px solid #6366f1;
115
+ outline-offset: 1px;
116
+ }
89
117
  .mcpConnectorMarketClose { color: #6b7280; }
90
118
  .mcpConnectorMarketClose:hover,
91
119
  .mcpConnectorMarketClose:focus-visible {
@@ -99,6 +127,10 @@ window.__ModuleLoader__.load({
99
127
  .mcpConnectorMarketPanel { background: #1b1e25; color: #f3f4f6; }
100
128
  .mcpConnectorMarketHeader { border-color: #30343d; }
101
129
  .mcpConnectorMarketTitle { color: #f3f4f6; }
130
+ .mcpConnectorVersion { color: #d1d5db; background: #30343d; }
131
+ .mcpConnectorUpdateButton { color: #c7d2fe; background: #312e81; border-color: #4f46e5; }
132
+ .mcpConnectorUpdateButton:hover,
133
+ .mcpConnectorUpdateButton:focus-visible { background: #3730a3; }
102
134
  .mcpConnectorMarketClose { color: #c4c8d0; }
103
135
  .mcpConnectorMarketClose:hover,
104
136
  .mcpConnectorMarketClose:focus-visible { background: #30343d !important; color: #ffffff; }
@@ -135,6 +167,50 @@ window.__ModuleLoader__.load({
135
167
  return mount;
136
168
  }
137
169
 
170
+ async function fetchVersionStatus() {
171
+ const response = await window.fetch("/mcp-connector/api", {
172
+ method: "POST",
173
+ headers: { "content-type": "application/json" },
174
+ body: JSON.stringify({ method: "versionStatus", params: {} })
175
+ });
176
+ const result = await response.json();
177
+ if (!response.ok || result.ok !== true || result.detail === void 0) {
178
+ throw new Error(result.message ?? `HTTP ${response.status}`);
179
+ }
180
+ return result.detail;
181
+ }
182
+
183
+ /**
184
+ * DSH 暂未公开“打开指定设置分区”服务:只依赖稳定的 sidebar.settings
185
+ * slot 和可访问文本打开插件市场,不依赖构建生成的 CSS 类名。
186
+ */
187
+ function openDshPluginMarket(actions) {
188
+ const settingsHost = document.querySelector('[data-slot="sidebar.settings"]');
189
+ const settingsTrigger = settingsHost?.querySelector?.('button[aria-haspopup="dialog"]');
190
+ if (settingsTrigger === null || settingsTrigger === void 0) {
191
+ window.open(NPM_PACKAGE_URL, "_blank", "noopener,noreferrer");
192
+ return;
193
+ }
194
+ actions.close();
195
+ window.setTimeout(() => {
196
+ settingsTrigger.click();
197
+ let attempts = 0;
198
+ const selectMarket = () => {
199
+ const marketButton = [...document.querySelectorAll('[role="dialog"] nav button')].find((button) => {
200
+ const label = button.textContent?.trim() ?? "";
201
+ return /^(\u63d2\u4ef6\u5e02\u573a|Plugin Market|Plugin Marketplace)$/i.test(label);
202
+ });
203
+ if (marketButton !== void 0) {
204
+ marketButton.click();
205
+ return;
206
+ }
207
+ attempts += 1;
208
+ if (attempts < 20) window.setTimeout(selectMarket, 50);
209
+ };
210
+ window.requestAnimationFrame(selectMarket);
211
+ }, 0);
212
+ }
213
+
138
214
  /** 创建弹框 store(使用 DSH defineStore,与社区插件市场一致)。 */
139
215
  function createMarketViewStore() {
140
216
  return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
@@ -181,6 +257,7 @@ window.__ModuleLoader__.load({
181
257
  const { wide, useStore, actions, startPromptSession: launchPromptSession } = props;
182
258
  const open = useStore((state) => state.open);
183
259
  const detailOpen = useStore((state) => state.detailOpen);
260
+ const [versionStatus, setVersionStatus] = (0, react.useState)(null);
184
261
  const frameRef = (0, react.useRef)(null);
185
262
  const closeRef = (0, react.useRef)(null);
186
263
  (0, react.useEffect)(() => {
@@ -219,6 +296,38 @@ window.__ModuleLoader__.load({
219
296
  window.addEventListener("message", onMessage);
220
297
  return () => { window.removeEventListener("message", onMessage); };
221
298
  }, [actions, launchPromptSession]);
299
+ (0, react.useEffect)(() => {
300
+ if (!open) return void 0;
301
+ let disposed = false;
302
+ let pollTimer;
303
+ const schedule = (delayMs) => {
304
+ window.clearTimeout(pollTimer);
305
+ pollTimer = window.setTimeout(check, Math.max(1e3, delayMs));
306
+ };
307
+ const check = () => {
308
+ fetchVersionStatus().then((status) => {
309
+ if (disposed) return;
310
+ setVersionStatus(status);
311
+ if (status.checking) {
312
+ schedule(1e3);
313
+ return;
314
+ }
315
+ const nextCheckAt = Date.parse(status.nextCheckAt ?? "");
316
+ const delay = Number.isFinite(nextCheckAt)
317
+ ? Math.min(VERSION_CHECK_INTERVAL_MS, nextCheckAt - Date.now())
318
+ : VERSION_CHECK_INTERVAL_MS;
319
+ schedule(delay);
320
+ }, (error) => {
321
+ console.warn("[mcp-connector] plugin update check failed:", error);
322
+ if (!disposed) schedule(VERSION_CHECK_RETRY_MS);
323
+ });
324
+ };
325
+ check();
326
+ return () => {
327
+ disposed = true;
328
+ window.clearTimeout(pollTimer);
329
+ };
330
+ }, [open]);
222
331
  (0, react.useEffect)(() => {
223
332
  if (!open) return void 0;
224
333
  closeRef.current?.focus?.();
@@ -273,7 +382,24 @@ window.__ModuleLoader__.load({
273
382
  style: { display: "flex", alignItems: "center", gap: 10 },
274
383
  children: [
275
384
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 22 }, children: "\u{1F9E9}" }),
276
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { id: "mcp-connector-market-title", className: "mcpConnectorMarketTitle", style: { fontSize: 18, fontWeight: 600 }, children: "MCP连接器" })
385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { id: "mcp-connector-market-title", className: "mcpConnectorMarketTitle", style: { fontSize: 18, fontWeight: 600 }, children: "MCP连接器" }),
386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
+ className: "mcpConnectorVersion",
388
+ title: versionStatus === null ? "正在检查插件版本" : `当前版本 v${versionStatus.installedVersion}`,
389
+ children: versionStatus === null ? "v…" : `v${versionStatus.installedVersion}`
390
+ }),
391
+ versionStatus?.updateAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
392
+ type: "button",
393
+ className: "mcpConnectorUpdateButton",
394
+ title: `npm 已发布 v${versionStatus.latestVersion}`,
395
+ onClick: () => openDshPluginMarket(actions),
396
+ children: `前往插件市场更新到 v${versionStatus.latestVersion}`
397
+ }),
398
+ versionStatus?.releasePending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
399
+ className: "mcpConnectorVersion",
400
+ title: "GitHub Release 已发布,等待 npm 同步后即可更新",
401
+ children: `v${versionStatus.release.version} 正在同步`
402
+ })
277
403
  ]
278
404
  }),
279
405
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -80,12 +80,19 @@ export async function oauthAuthorize({ connector, config, logger, signal }) {
80
80
  // 按 token 实际授权范围过滤 server;非 JWT / 无 resource claim 时 fallback 全部
81
81
  const grantedUrls = extractTokenResources(token.accessToken);
82
82
  let grantedKeys;
83
+ let grantedResources;
83
84
  if (grantedUrls) {
84
85
  const granted = new Set(grantedUrls);
85
- grantedKeys = connector.servers.filter((s) => granted.has(s.url)).map((s) => s.serverKey);
86
- if (grantedKeys.length === 0) grantedKeys = [entryServer.serverKey];
86
+ const grantedServers = connector.servers.filter((s) => granted.has(s.url));
87
+ grantedKeys = grantedServers.map((s) => s.serverKey);
88
+ grantedResources = grantedServers.map((s) => s.url);
89
+ if (grantedKeys.length === 0) {
90
+ grantedKeys = [entryServer.serverKey];
91
+ grantedResources = [entryServer.url];
92
+ }
87
93
  } else {
88
94
  grantedKeys = connector.servers.map((s) => s.serverKey);
95
+ grantedResources = connector.servers.map((s) => s.url);
89
96
  }
90
97
 
91
98
  return {
@@ -98,6 +105,7 @@ export async function oauthAuthorize({ connector, config, logger, signal }) {
98
105
  scope: connector.auth.scope,
99
106
  token,
100
107
  grantedKeys,
108
+ grantedResources,
101
109
  entryResource,
102
110
  };
103
111
  }
package/lib/constants.js CHANGED
@@ -30,6 +30,9 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
30
30
  /** 首次启动 MCP Server 并完成 initialize + tools/list 的最长等待时间 */
31
31
  export const DEFAULT_STARTUP_TIMEOUT_MS = 120_000;
32
32
  export const DEFAULT_REFRESH_SKEW_MS = 300_000;
33
+ /** OAuth 刷新暂时失败后的指数退避范围。 */
34
+ export const DEFAULT_REFRESH_RETRY_BASE_MS = 30_000;
35
+ export const DEFAULT_REFRESH_RETRY_MAX_MS = 300_000;
33
36
  export const DEFAULT_CATALOG_TTL_MS = 3_600_000;
34
37
 
35
38
  /** PKCE 约束(RFC 7636) */
@@ -0,0 +1,47 @@
1
+ /** OAuth Grant 刷新失败分类、日志脱敏与重试退避。 */
2
+
3
+ const PERMANENT_REFRESH_CODES = new Set([
4
+ 'invalid_grant',
5
+ 'invalid_token',
6
+ 'invalid_client',
7
+ 'unauthorized_client',
8
+ 'invalid_scope',
9
+ 'client_secret_expired',
10
+ 'missing_refresh_token',
11
+ ]);
12
+
13
+ /** 防止 OAuth 服务端错误描述意外回显凭据。 */
14
+ export function redactOAuthDetail(value) {
15
+ return String(value ?? '')
16
+ .replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
17
+ .replace(/([?&](?:access_token|refresh_token|client_secret|code)=)[^&#\s]*/gi, '$1[REDACTED]')
18
+ .replace(/((?:access_token|refresh_token|client_secret)\s*[=:]\s*)[^\s,;&}]+/gi, '$1[REDACTED]')
19
+ .slice(0, 500);
20
+ }
21
+
22
+ /**
23
+ * 只有明确表示 Refresh Token / 动态客户端不可恢复的 OAuth 错误才要求重新授权。
24
+ * 网络、发现端点、5xx、超时和未知服务端错误都按暂时故障自动重试。
25
+ */
26
+ export function classifyRefreshFailure(error) {
27
+ const code = typeof error?.code === 'string' && error.code
28
+ ? redactOAuthDetail(error.code).slice(0, 80)
29
+ : error?.name === 'OAuthNetworkError' ? 'network_error' : 'refresh_failed';
30
+ const httpStatus = Number.isInteger(error?.httpStatus) ? error.httpStatus : undefined;
31
+ const permanent = PERMANENT_REFRESH_CODES.has(code) || httpStatus === 401 || httpStatus === 403;
32
+ return {
33
+ permanent,
34
+ kind: permanent ? 'permanent' : 'transient',
35
+ code,
36
+ httpStatus,
37
+ message: redactOAuthDetail(error?.message || code),
38
+ };
39
+ }
40
+
41
+ /** 指数退避:base、2×、4×……,封顶 max。failureCount 从 1 开始。 */
42
+ export function refreshRetryDelay(failureCount, baseMs, maxMs) {
43
+ const base = Math.max(1_000, Number(baseMs) || 30_000);
44
+ const max = Math.max(base, Number(maxMs) || 300_000);
45
+ const exponent = Math.max(0, Math.min(20, Number(failureCount || 1) - 1));
46
+ return Math.min(max, base * (2 ** exponent));
47
+ }