dsh-mcp-connector 0.2.6 → 0.2.8

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,46 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.8] - 2026-08-22
8
+
9
+ ### Fixed
10
+
11
+ - 工具详情改为执行完整的 Streamable HTTP MCP 会话流程:`initialize` 后保存 `Mcp-Session-Id`,发送 `notifications/initialized`,再携带会话、协议版本和凭据请求 `tools/list`,修复 QVeris 等有状态 MCP Server 返回 HTTP 400 的问题。
12
+ - 会话结束后尽力发送 `DELETE` 释放服务端资源;鉴权失败、限流、HTTP/协议错误继续按类型更新连接健康状态。
13
+
14
+ ### Registry
15
+
16
+ - 将 QVeris 工具快照校正为托管端实际返回的 8 个工具;移除当前运行时不存在的 `probe`,补充 3 个兼容旧客户端的弃用别名。
17
+ - QVeris 精选 Prompt 仅使用实际可用的 `discover`、`inspect` 和用量审计工具;未经用户明确确认仍不执行可能消耗 Credits 的 `call`/`execute_tool`。
18
+
19
+ ### Documentation
20
+
21
+ - 统一 QVerisMCP 品牌名称,记录官方 Logo 因 `same-origin` 策略需由 Registry 自托管的 Desktop 兼容方案。
22
+ - 将已于 `0.1.0`–`0.2.0` 完成的 v2 详情页实施计划标记为已完成/归档,避免与当前待办混淆。
23
+
24
+ ### Verification
25
+
26
+ - 69 项自动测试、lint 与 44 个 npm 发布文件白名单/敏感内容扫描通过。
27
+ - 使用 DSH 本机已保存的 QVeris API Key 完成真实只读验收:协议版本 `2025-03-26`、有状态会话建立成功、`tools/list` 返回 8 个工具;未执行任何付费能力调用。
28
+
29
+ ## [0.2.7] - 2026-08-22
30
+
31
+ ### Added
32
+
33
+ - 市场新增“服务商 + 接入方式”组合筛选,可快速查看 OAuth、Key/Token 或免密连接器,并可与全文搜索叠加。
34
+ - 新增 DSH 外部市场注册自动验收:每小时跟踪 PR #2633,合并后继续验证上游 YAML 与 DSH 实际 `plugins.json` 均已生效。
35
+
36
+ ### Registry
37
+
38
+ - 独立远程 Registry 已扩展为 4 张第三方卡片、12 个 Server 和 16 个 Prompt;与随包目录合并后共 8 张已发布市场卡片。
39
+ - 新增“盈米·基金投顾”与“QVeris·通用能力网络”;QVeris 默认先做免费发现、参数检查与零成本询价,付费 `call` 需用户明确确认。
40
+
41
+ ### Verification
42
+
43
+ - 自动化门禁通过 69 项测试与 43 个 npm 发布文件白名单/敏感内容扫描。
44
+ - QVeris Hosted MCP 无凭据公网探针返回预期的 HTTP 401,确认端点可达且正确要求 Bearer Key;未进行付费 `call`。
45
+ - DSH Desktop `web` profile 已精确对齐 `dsh-mcp-connector@0.2.7`;完全重启后左侧入口、中文单语界面、组合筛选和 8 张市场卡片通过,19 条连接和 4 组授权保持不变。
46
+
7
47
  ## [0.2.6] - 2026-08-21
8
48
 
9
49
  ### Documentation
@@ -147,7 +187,9 @@
147
187
  - 外部 URL 与导入 Header 执行安全校验。
148
188
  - iframe 消息校验同源和消息来源。
149
189
 
150
- [Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.6...HEAD
190
+ [Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.8...HEAD
191
+ [0.2.8]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.7...v0.2.8
192
+ [0.2.7]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.6...v0.2.7
151
193
  [0.2.6]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.5...v0.2.6
152
194
  [0.2.5]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.4...v0.2.5
153
195
  [0.2.4]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.3...v0.2.4
package/README.en.md CHANGED
@@ -15,7 +15,7 @@ Browse and install MCP connectors from different providers in DeepSeek Harness D
15
15
  ## Features
16
16
 
17
17
  - A primary sidebar entry below New Conversation and above workspaces/conversations, with a public footer-slot fallback for incompatible DSH DOM versions.
18
- - Searchable Marketplace and Installed views with catalog refresh and active health checks that distinguish Configured, Connected, Reauthorization Required, Partially Unavailable, and Connection Error states.
18
+ - Searchable Marketplace and Installed views, provider and connection-method filters, catalog refresh, and active health checks that distinguish Configured, Connected, Reauthorization Required, Partially Unavailable, and Connection Error states.
19
19
  - OAuth 2.0 Authorization Code with PKCE, API key/Bearer/unauthenticated URL configuration, and `mcpServers` JSON import.
20
20
  - Installation from a credential-free connector descriptor URL.
21
21
  - Credential and MCP initialize validation before API-key connectors are saved as installed.
@@ -26,7 +26,7 @@ Browse and install MCP connectors from different providers in DeepSeek Harness D
26
26
  - A standalone remote Registry, allowing new marketplace cards to appear after refresh without publishing a new npm version.
27
27
  - Explicit, non-destructive migration of authorization from the two earlier Qichacha OAuth plugins.
28
28
 
29
- The first four bundled cards are Qichacha connectors. The public catalog also includes PKULaw and Wind connectors; the architecture is provider-neutral.
29
+ The first four bundled cards are Qichacha connectors, followed by PKULaw and Wind. After a remote Registry refresh, Yingmi and QVeris are also available, for eight published marketplace cards in total; the architecture is provider-neutral.
30
30
 
31
31
  ## Interface and demo
32
32
 
@@ -94,12 +94,13 @@ Set `catalogUrl` to an empty string for an explicitly offline/private setup.
94
94
  npm run check
95
95
  npm run registry:build
96
96
  npm run registry:validate
97
+ npm run market:check
97
98
  npm run dev:ui
98
99
  ```
99
100
 
100
- `npm run check` performs syntax checks, automated tests, and an npm package allowlist/sensitive-content audit. Tags matching `v*` trigger GitHub Actions; the tag must match `package.json`. npm releases use Trusted Publishing through GitHub OIDC and do not require a long-lived `NPM_TOKEN`.
101
+ `npm run check` performs syntax checks, automated tests, and an npm package allowlist/sensitive-content audit. `npm run market:check` tracks the external DSH marketplace PR and live directory. Tags matching `v*` trigger GitHub Actions; the tag must match `package.json`. npm releases use Trusted Publishing through GitHub OIDC and do not require a long-lived `NPM_TOKEN`.
101
102
 
102
- The current public version is [`dsh-mcp-connector@0.2.6`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.6](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.6).
103
+ The current public version is [`dsh-mcp-connector@0.2.7`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.7](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.7).
103
104
 
104
105
  See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
105
106
 
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  ## 功能
16
16
 
17
17
  - 左侧主导航入口:目标位置为“新会话”下方、“工作区/会话列表”上方;若 DSH DOM 结构不兼容,自动回退到底部公开插槽。
18
- - 图形化市场:市场/已安装、搜索、刷新与主动健康检查;区分“已配置”、“已连接”、“需重新授权”、“部分异常”和“连接异常”。
18
+ - 图形化市场:市场/已安装、全文搜索、服务商/接入方式组合筛选、刷新与主动健康检查;区分“已配置”、“已连接”、“需重新授权”、“部分异常”和“连接异常”。
19
19
  - 图形化添加:手动 URL/鉴权、`mcpServers` JSON、连接器描述 URL 三种入口,失败时保留表单并给出修复建议。
20
20
  - 连接器详情:精选 Prompt 优先展示,点击可带入 DSH 新会话;工具按 Server 分组,支持描述、搜索和独立滚动。
21
21
  - Prompt 模板:使用 `{{company}}` 等变量,发送前填写真实查询主体。
@@ -28,7 +28,7 @@
28
28
  - 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;不删除原插件或原凭据。
29
29
  - 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`install_from_url`、`status`、`health_check`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
30
30
 
31
- 当前市场前 4 张为企查查连接器,第 5 张为“北大法宝·法律检索”,第 6 张为“Wind·股票数据”;插件架构不限定厂商。
31
+ 随包目录前 4 张为企查查连接器,另含“北大法宝·法律检索”和“Wind·股票数据”。远程 Registry 刷新后还会加入“盈米·基金投顾”和“QVerisMCP”,当前共 8 张已发布市场卡片;插件架构不限定厂商。
32
32
 
33
33
  ## 界面与演示
34
34
 
@@ -90,12 +90,13 @@ Bundle 默认配置位于 `cordis.patch.yml`:
90
90
  npm run check
91
91
  npm run registry:build
92
92
  npm run registry:validate
93
+ npm run market:check
93
94
  npm run dev:ui
94
95
  ```
95
96
 
96
- `check` 执行语法检查、自动测试和 npm 发布包白名单校验;`dev:ui` 启动不含真实凭据的本地 mock 市场。CI 使用 `--legacy-peer-deps` 安装显式测试依赖,DSH 运行期 peer 仍由 Host 提供。`v*` Tag 会触发 GitHub Actions;Tag 必须与 `package.json` 版本一致。Release 通过 npm Trusted Publishing (GitHub OIDC) 发布,不依赖长期 `NPM_TOKEN`。
97
+ `check` 执行语法检查、自动测试和 npm 发布包白名单校验;`market:check` 检查外部 DSH 市场 PR 与线上目录;`dev:ui` 启动不含真实凭据的本地 mock 市场。CI 使用 `--legacy-peer-deps` 安装显式测试依赖,DSH 运行期 peer 仍由 Host 提供。`v*` Tag 会触发 GitHub Actions;Tag 必须与 `package.json` 版本一致。Release 通过 npm Trusted Publishing (GitHub OIDC) 发布,不依赖长期 `NPM_TOKEN`。
97
98
 
98
- 当前公开版本为 [`dsh-mcp-connector@0.2.6`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.6](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.6)。
99
+ 当前公开版本为 [`dsh-mcp-connector@0.2.7`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.7](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.7)。
99
100
 
100
101
  版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
101
102
  Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
@@ -27,7 +27,18 @@ Bearer/API Key 型连接器可在卡片上点「配置」,一次填写凭据
27
27
 
28
28
  插件默认读取 `https://raw.githubusercontent.com/duhu2000/dsh-mcp-connector-registry/main/catalog.json`;远程不可用时自动回退上次缓存或随包内置目录。
29
29
 
30
- ## 4. OAuth 一键授权要求
30
+ ## 4. DSH 外部插件市场验收
31
+
32
+ `dsh-mcp-connector` 在 `awesome-dsh-plugin` 的注册 PR 为 [#2633](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/2633)。仓库每小时自动执行 `.github/workflows/market-registration.yml`:
33
+
34
+ 1. PR 未合并时记录为 `awaiting-merge`,不误报构建失败;
35
+ 2. PR 合并后检查上游 `data/plugins/duhu2000__dsh-mcp-connector.yml` 已进入 `main`;
36
+ 3. 继续检查 DSH Market 实际使用的 `https://awesome-dsh-plugin.com/plugins.json`;
37
+ 4. YAML 与线上目录均生效后状态为 `accepted`,否则在合并后明确报告目录同步未完成。
38
+
39
+ 本地可运行 `npm run market:check`查看状态;CI 使用 `--strict-after-merge` 让“已合并但尚未可搜索”成为可见的验收信号。此检查仅需 GitHub Actions 默认的只读 `GITHUB_TOKEN`,不需要新增长期密钥。
40
+
41
+ ## 5. OAuth 一键授权要求
31
42
 
32
43
  `auth.mode: "oauth2-pkce"` 不是将现有 Bearer Token 改个名称。厂商服务端必须支持:
33
44
 
@@ -67,7 +78,7 @@ Bearer/API Key 型连接器可在卡片上点「配置」,一次填写凭据
67
78
  }
68
79
  ```
69
80
 
70
- ## 5. 北大法宝当前适配结论
81
+ ## 6. 北大法宝当前适配结论
71
82
 
72
83
  北大法宝当前公开文档的主路径是在控制台生成 Access Token,然后通过 `Authorization: Bearer ...` 访问多个 MCP Server。因此:
73
84
 
@@ -89,7 +100,7 @@ Bearer/API Key 型连接器可在卡片上点「配置」,一次填写凭据
89
100
  - https://mcp.pkulaw.com/docs?doc=authentication
90
101
  - https://mcp.pkulaw.com/docs?doc=mcp-integration
91
102
 
92
- ## 6. Wind 股票数据当前适配结论
103
+ ## 7. Wind 股票数据当前适配结论
93
104
 
94
105
  万得 AIFin Market 的“万得股票数据服务”详情页公开了标准 MCP 手工配置:
95
106
 
@@ -105,3 +116,40 @@ Bearer/API Key 型连接器可在卡片上点「配置」,一次填写凭据
105
116
  参考:
106
117
 
107
118
  - https://aifinmarket.wind.com.cn/#/market?tab=mcps&detailType=mcp&detailId=wind_stock_data-0
119
+
120
+ ## 8. QVerisMCP 适配结论
121
+
122
+ QVeris Hosted MCP 提供单一 Streamable HTTP 端点 `https://mcp.qveris.ai/mcp`,
123
+ 通过 `Authorization: Bearer <QVERIS_API_KEY>` 鉴权,与本插件现有的 Bearer
124
+ 凭据表单、持久化前 `initialize` 校验和 Streamable HTTP 传输完全兼容。
125
+
126
+ 2026-08-22 使用真实 API Key 完成只读 `tools/list` 验收,QVeris 托管端实际返回
127
+ 8 个 MCP 工具:`discover`、`inspect`、`call`、`usage_history`、
128
+ `credits_ledger`,以及 3 个兼容旧客户端的弃用别名 `search_tools`、
129
+ `get_tools_by_ids`、`execute_tool`。当前运行时没有官网文档所列的 `probe`;
130
+ 市场快照以实际端点为准。官方宣传的上万项能力是经 `discover` 找到的下游能力,
131
+ 不写成上万个 MCP 工具。
132
+
133
+ 由于 `call` 可能消耗 Credits,且查询内容可能由第三方服务商处理,
134
+ 该卡片保持 `featured: false`,并执行以下产品约束:
135
+
136
+ - Prompt 默认只执行 `discover`/`inspect` 和用量审计工具;
137
+ - 未经用户明确确认,不执行可能消耗 Credits 的 `call` 或弃用别名 `execute_tool`;
138
+ - 卡片明确提醒用户不要提交未获授权的个人信息、商业秘密或敏感业务数据;
139
+ - 市场使用用户指定的 QVeris 官方 Logo;由于官网原图返回
140
+ `Cross-Origin-Resource-Policy: same-origin`,在 DSH Desktop 中直链会被浏览器拦截,
141
+ Registry 因此保留官方原图像素并自托管于
142
+ `assets/qveris-logo.png`,对外提供可跨域嵌入的 Raw GitHub URL。
143
+
144
+ 公共无凭据探针已确认端点可达并正确返回 HTTP 401;使用 DSH 本机自有 API Key
145
+ 执行完整 `initialize → notifications/initialized → tools/list` 后,服务返回
146
+ `Mcp-Session-Id`、协议版本 `2025-03-26` 和 8 个工具。验收未执行 `call` 或
147
+ `execute_tool`,没有产生付费能力调用。
148
+
149
+ 参考:
150
+
151
+ - https://qveris.ai/hosted-mcp
152
+ - https://qveris.ai/docs/mcp-server
153
+ - https://qveris.ai/pricing
154
+ - https://qveris.ai/privacy
155
+ - https://qveris.ai/terms
package/lib/index.js CHANGED
@@ -24,8 +24,9 @@ import { oauthAuthorize } from './connectors/oauth-connector.js';
24
24
  import { buildManualRecord } from './connectors/manual-connector.js';
25
25
  import { normalizeJsonImport } from './connectors/json-connector.js';
26
26
  import { discoverServerMetadata, refreshAccessToken, revokeRefreshToken } from './oauth.js';
27
- import { provision, disable, remove, entryIdFor, authHeaders } from './mcp-provision.js';
27
+ import { provision, disable, remove, entryIdFor } from './mcp-provision.js';
28
28
  import { classifyConnectionError, validateConnectionRecords } from './mcp-validation.js';
29
+ import { listMcpTools, McpHttpError } from './mcp-http.js';
29
30
  import { registerTools } from './tools.js';
30
31
  import { mountWebRoutes } from './web.js';
31
32
  import { assertSafeUrl } from './util.js';
@@ -793,70 +794,33 @@ export async function apply(ctx, config) {
793
794
 
794
795
  const servers = [];
795
796
  for (const record of records) {
796
- const headers = {
797
- 'Content-Type': 'application/json',
798
- 'Accept': 'application/json, text/event-stream',
799
- ...authHeaders(record, grantMap()),
800
- };
801
-
802
797
  try {
803
- const response = await fetch(record.url, {
804
- method: 'POST',
805
- headers,
806
- body: JSON.stringify({
807
- jsonrpc: '2.0',
808
- id: 1,
809
- method: 'tools/list',
810
- params: {},
811
- }),
812
- signal: AbortSignal.timeout(config.requestTimeoutMs),
798
+ const listed = await listMcpTools(record, {
799
+ timeoutMs: config.requestTimeoutMs,
800
+ grants: grantMap(),
813
801
  });
814
-
815
- if (!response.ok) {
816
- const error = response.status === 401 || response.status === 403
817
- ? `凭据无效、已过期或权限不足(HTTP ${response.status})`
818
- : `HTTP ${response.status}`;
819
- servers.push({
820
- serverKey: record.serverKey || record.name,
821
- serverName: record.serverName,
822
- ok: false,
823
- error,
824
- errorKind: response.status === 401 || response.status === 403 ? 'auth' : 'http',
825
- tools: [],
826
- });
827
- continue;
828
- }
829
-
830
- const contentType = response.headers.get('content-type') || '';
831
- let result;
832
- if (contentType.includes('text/event-stream')) {
833
- // Parse SSE response
834
- const text = await response.text();
835
- const lines = text.split('\n');
836
- for (const line of lines) {
837
- if (line.startsWith('data: ')) {
838
- try {
839
- result = JSON.parse(line.slice(6));
840
- break;
841
- } catch {}
842
- }
843
- }
844
- } else {
845
- result = await response.json();
846
- }
847
-
848
- const tools = result?.result?.tools ?? [];
849
802
  servers.push({
850
803
  serverKey: record.serverKey || record.name,
851
804
  serverName: record.serverName,
852
805
  ok: true,
853
- tools: tools.map((t) => ({
806
+ tools: listed.tools.map((t) => ({
854
807
  name: t.name,
855
808
  title: t.title || t.name,
856
809
  description: t.description || '',
857
810
  })),
858
811
  });
859
812
  } catch (error) {
813
+ if (error instanceof McpHttpError) {
814
+ servers.push({
815
+ serverKey: record.serverKey || record.name,
816
+ serverName: record.serverName,
817
+ ok: false,
818
+ error: error.message,
819
+ errorKind: error.kind,
820
+ tools: [],
821
+ });
822
+ continue;
823
+ }
860
824
  const classified = classifyConnectionError(error);
861
825
  servers.push({ serverKey: record.serverKey || record.name, serverName: record.serverName, ok: false, error: classified.message, errorKind: classified.kind, tools: [] });
862
826
  }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Streamable HTTP MCP 会话客户端。
3
+ *
4
+ * 详情页不能直接向有状态的 MCP Server 发送 tools/list:必须先
5
+ * initialize,保留响应的 Mcp-Session-Id,发送 notifications/initialized,
6
+ * 再在后续每个请求中携带会话头和凭据。
7
+ */
8
+ import { MCP_PROTOCOL_VERSION } from './constants.js';
9
+ import { authHeaders } from './mcp-provision.js';
10
+
11
+ const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
12
+
13
+ export class McpHttpError extends Error {
14
+ constructor(message, { kind = 'protocol', status } = {}) {
15
+ super(message);
16
+ this.name = 'McpHttpError';
17
+ this.kind = kind;
18
+ this.status = status;
19
+ }
20
+ }
21
+
22
+ async function readLimitedText(response, maxBytes = MAX_RESPONSE_BYTES) {
23
+ const declared = Number(response.headers.get('content-length'));
24
+ if (Number.isFinite(declared) && declared > maxBytes) throw new McpHttpError('MCP 响应过大');
25
+ if (!response.body) return '';
26
+ const reader = response.body.getReader();
27
+ const chunks = [];
28
+ let total = 0;
29
+ for (;;) {
30
+ const { done, value } = await reader.read();
31
+ if (done) break;
32
+ total += value.byteLength;
33
+ if (total > maxBytes) {
34
+ await reader.cancel().catch(() => {});
35
+ throw new McpHttpError('MCP 响应过大');
36
+ }
37
+ chunks.push(value);
38
+ }
39
+ const bytes = new Uint8Array(total);
40
+ let offset = 0;
41
+ for (const chunk of chunks) {
42
+ bytes.set(chunk, offset);
43
+ offset += chunk.byteLength;
44
+ }
45
+ return new TextDecoder().decode(bytes);
46
+ }
47
+
48
+ function parseMcpPayload(text, contentType = '') {
49
+ if (contentType.includes('text/event-stream')) {
50
+ for (const line of text.split(/\r?\n/)) {
51
+ if (!line.startsWith('data:')) continue;
52
+ try { return JSON.parse(line.slice(5).trim()); } catch {}
53
+ }
54
+ return undefined;
55
+ }
56
+ try { return JSON.parse(text); } catch { return undefined; }
57
+ }
58
+
59
+ function responseError(response, payload, text) {
60
+ const rawMessage = String(payload?.error?.message ?? text ?? '').replace(/\s+/g, ' ').trim().slice(0, 180);
61
+ if (response.status === 401 || response.status === 403) {
62
+ return new McpHttpError(`凭据无效、已过期或权限不足(HTTP ${response.status})`, { kind: 'auth', status: response.status });
63
+ }
64
+ if (response.status === 429) {
65
+ return new McpHttpError('服务请求过于频繁(HTTP 429),请稍后重试', { kind: 'rate-limit', status: response.status });
66
+ }
67
+ const suffix = rawMessage ? `:${rawMessage}` : '';
68
+ return new McpHttpError(`MCP 请求失败(HTTP ${response.status})${suffix}`, { kind: 'http', status: response.status });
69
+ }
70
+
71
+ function payloadError(payload) {
72
+ if (!payload?.error) return null;
73
+ const rawMessage = String(payload.error.message ?? '').replace(/\s+/g, ' ').slice(0, 180);
74
+ const kind = /unauthori[sz]ed|forbidden|invalid.?token|invalid.?key|credential|access.?denied|未授权|无权|密钥|令牌/i.test(rawMessage)
75
+ ? 'auth'
76
+ : 'protocol';
77
+ return new McpHttpError(`MCP 返回错误${rawMessage ? `:${rawMessage}` : ''}`, { kind });
78
+ }
79
+
80
+ function requestHeaders(record, grants, { sessionId, protocolVersion } = {}) {
81
+ return {
82
+ 'Content-Type': 'application/json',
83
+ Accept: 'application/json, text/event-stream',
84
+ ...authHeaders(record, grants),
85
+ ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {}),
86
+ ...(protocolVersion ? { 'MCP-Protocol-Version': protocolVersion } : {}),
87
+ };
88
+ }
89
+
90
+ async function post(record, payload, { grants, sessionId, protocolVersion, signal, fetchImpl }) {
91
+ const response = await fetchImpl(record.url, {
92
+ method: 'POST',
93
+ headers: requestHeaders(record, grants, { sessionId, protocolVersion }),
94
+ body: JSON.stringify(payload),
95
+ signal,
96
+ redirect: 'follow',
97
+ });
98
+ const text = await readLimitedText(response);
99
+ const parsed = parseMcpPayload(text, response.headers.get('content-type') ?? '');
100
+ return { response, text, payload: parsed };
101
+ }
102
+
103
+ async function closeSession(record, { grants, sessionId, protocolVersion, fetchImpl }) {
104
+ if (!sessionId) return;
105
+ try {
106
+ const response = await fetchImpl(record.url, {
107
+ method: 'DELETE',
108
+ headers: requestHeaders(record, grants, { sessionId, protocolVersion }),
109
+ redirect: 'follow',
110
+ signal: AbortSignal.timeout(3_000),
111
+ });
112
+ await response.body?.cancel().catch(() => {});
113
+ } catch {}
114
+ }
115
+
116
+ export async function listMcpTools(record, {
117
+ timeoutMs = 15_000,
118
+ fetchImpl = fetch,
119
+ grants = new Map(),
120
+ } = {}) {
121
+ if (record.transport && record.transport !== 'streamable-http') {
122
+ throw new McpHttpError(`动态工具加载暂不支持 ${record.transport} 传输`, { kind: 'protocol' });
123
+ }
124
+ const controller = new AbortController();
125
+ const timer = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
126
+ let sessionId = '';
127
+ let protocolVersion = MCP_PROTOCOL_VERSION;
128
+ try {
129
+ const initialized = await post(record, {
130
+ jsonrpc: '2.0',
131
+ id: 1,
132
+ method: 'initialize',
133
+ params: {
134
+ protocolVersion: MCP_PROTOCOL_VERSION,
135
+ capabilities: {},
136
+ clientInfo: { name: 'dsh-mcp-connector', version: '1.0' },
137
+ },
138
+ }, { grants, signal: controller.signal, fetchImpl });
139
+ if (!initialized.response.ok) throw responseError(initialized.response, initialized.payload, initialized.text);
140
+ const initPayloadError = payloadError(initialized.payload);
141
+ if (initPayloadError) throw initPayloadError;
142
+ if (!initialized.payload?.result || typeof initialized.payload.result !== 'object') {
143
+ throw new McpHttpError('MCP Server 未返回有效的 initialize 结果');
144
+ }
145
+ protocolVersion = initialized.payload.result.protocolVersion || MCP_PROTOCOL_VERSION;
146
+ sessionId = initialized.response.headers.get('mcp-session-id') ?? '';
147
+
148
+ const notification = await post(record, {
149
+ jsonrpc: '2.0',
150
+ method: 'notifications/initialized',
151
+ params: {},
152
+ }, { grants, sessionId, protocolVersion, signal: controller.signal, fetchImpl });
153
+ if (notification.response.status === 401 || notification.response.status === 403) {
154
+ throw responseError(notification.response, notification.payload, notification.text);
155
+ }
156
+
157
+ const listed = await post(record, {
158
+ jsonrpc: '2.0',
159
+ id: 2,
160
+ method: 'tools/list',
161
+ params: {},
162
+ }, { grants, sessionId, protocolVersion, signal: controller.signal, fetchImpl });
163
+ if (!listed.response.ok) throw responseError(listed.response, listed.payload, listed.text);
164
+ const listPayloadError = payloadError(listed.payload);
165
+ if (listPayloadError) throw listPayloadError;
166
+ const tools = listed.payload?.result?.tools;
167
+ if (!Array.isArray(tools)) throw new McpHttpError('MCP tools/list 未返回工具数组');
168
+ return { tools, protocolVersion, sessionful: Boolean(sessionId) };
169
+ } finally {
170
+ clearTimeout(timer);
171
+ await closeSession(record, { grants, sessionId, protocolVersion, fetchImpl });
172
+ }
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcp-connector",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "General-purpose MCP connector, connection manager, plugin extension, and integration marketplace for DeepSeek Harness, initiated and maintained by Qichacha/QCC.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -31,10 +31,11 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "test": "node --test",
34
- "lint": "node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/mcp-validation.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/oauth.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs",
34
+ "lint": "node --check lib/index.js && node --check lib/web.js && node --check lib/client.js && node --check lib/schema.js && node --check lib/catalog.js && node --check lib/prompts.js && node --check lib/probe.js && node --check lib/mcp-validation.js && node --check lib/mcp-http.js && node --check lib/migration.js && node --check lib/stores.js && node --check lib/oauth.js && node --check lib/callback-server.js && node --check lib/mcp-provision.js && node --check lib/util.js && node --check lib/constants.js && node --check lib/tools.js && node --check lib/connectors/oauth-connector.js && node --check lib/connectors/manual-connector.js && node --check lib/connectors/json-connector.js && node --check scripts/probe-connector.mjs && node --check scripts/build-registry.mjs && node --check scripts/ui-harness.mjs && node --check scripts/check-market-registration.mjs",
35
35
  "registry:build": "node scripts/build-registry.mjs",
36
36
  "registry:validate": "node scripts/probe-connector.mjs catalog/catalog.json --validate-only",
37
37
  "dev:ui": "node scripts/ui-harness.mjs",
38
+ "market:check": "node scripts/check-market-registration.mjs",
38
39
  "verify-pack": "node scripts/verify-pack.mjs",
39
40
  "check": "npm run lint && npm test && npm run verify-pack",
40
41
  "prepublishOnly": "npm run check"
package/ui/index.html CHANGED
@@ -104,6 +104,25 @@
104
104
  .stats { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
105
105
  .stats .count { font-size: 13px; color: var(--text-2); }
106
106
  .stats .count strong { color: var(--text); font-weight: 600; }
107
+ .market-filters {
108
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
109
+ margin-bottom: 14px; padding: 10px 12px;
110
+ background: var(--card); border: 1px solid var(--line); border-radius: 10px;
111
+ }
112
+ .market-filters .filter-label { color: var(--muted); font-size: 12px; flex: 0 0 auto; }
113
+ .market-filters select {
114
+ min-width: 150px; height: 34px; border: 1px solid var(--line); border-radius: 8px;
115
+ background: var(--bg); color: var(--text); padding: 0 30px 0 10px; font: inherit; font-size: 12px;
116
+ }
117
+ .market-filters select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-light); }
118
+ .filter-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
119
+ .filter-chip {
120
+ border: 1px solid var(--line); background: var(--bg); color: var(--text-2);
121
+ border-radius: 999px; padding: 5px 10px; cursor: pointer; font: inherit; font-size: 12px;
122
+ }
123
+ .filter-chip:hover { border-color: var(--accent); color: var(--accent); }
124
+ .filter-chip.active { border-color: var(--accent); background: var(--accent-light); color: var(--accent); font-weight: 600; }
125
+ .filter-reset { margin-left: auto; }
107
126
 
108
127
  /* Grid - TraeWork 风格:3 列 */
109
128
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
@@ -346,6 +365,9 @@
346
365
  .card { flex-wrap: wrap; }
347
366
  .card .actions { width: 100%; justify-content: flex-end; }
348
367
  .form-row { grid-template-columns: 1fr; gap: 0; }
368
+ .market-filters { align-items: stretch; }
369
+ .market-filters select { width: 100%; }
370
+ .filter-reset { margin-left: 0; }
349
371
  }
350
372
 
351
373
  /* Scrollbar */
@@ -447,6 +469,8 @@
447
469
  let view = 'market';
448
470
  let marketItems = {};
449
471
  let searchQuery = '';
472
+ let marketVendor = '';
473
+ let marketAuth = '';
450
474
  const PROMPT_PAGE_SIZE = 4;
451
475
  let detailPrompts = [];
452
476
  let promptPage = 0;
@@ -665,6 +689,30 @@
665
689
  || (d.tags || []).some(t => t.toLowerCase().includes(q));
666
690
  }
667
691
 
692
+ const AUTH_FILTERS = [
693
+ { value: '', label: '全部接入' },
694
+ { value: 'oauth2-pkce', label: 'OAuth' },
695
+ { value: 'credential', label: 'Key / Token' },
696
+ { value: 'none', label: '免密' },
697
+ ];
698
+
699
+ function matchesMarketFilters(d) {
700
+ if (!matchesSearch(d)) return false;
701
+ if (marketVendor && d.vendor !== marketVendor) return false;
702
+ if (marketAuth === 'credential') return ['bearer', 'api-key'].includes(d.authMode);
703
+ if (marketAuth && d.authMode !== marketAuth) return false;
704
+ return true;
705
+ }
706
+
707
+ function marketFilterHtml(items) {
708
+ const vendors = [...new Set(items.map((item) => item.vendor).filter(Boolean))]
709
+ .sort((a, b) => a.localeCompare(b, 'zh-CN'));
710
+ const vendorOptions = [`<option value="">全部服务商</option>`, ...vendors.map((vendor) => `<option value="${esc(vendor)}" ${vendor === marketVendor ? 'selected' : ''}>${esc(vendor)}</option>`)].join('');
711
+ const authButtons = AUTH_FILTERS.map((filter) => `<button class="filter-chip ${filter.value === marketAuth ? 'active' : ''}" type="button" data-auth-filter="${esc(filter.value)}" aria-pressed="${filter.value === marketAuth ? 'true' : 'false'}">${esc(filter.label)}</button>`).join('');
712
+ const active = Boolean(marketVendor || marketAuth);
713
+ return `<div class="market-filters" role="group" aria-label="市场筛选"><span class="filter-label">服务商</span><select id="market-vendor-filter" aria-label="按服务商筛选">${vendorOptions}</select><span class="filter-label">接入方式</span><div class="filter-chips" role="group" aria-label="按接入方式筛选">${authButtons}</div><button class="text-btn filter-reset" id="market-filter-reset" type="button" ${active ? '' : 'hidden'}>清除筛选</button></div>`;
714
+ }
715
+
668
716
  async function loadMarket({ checkHealth = true } = {}) {
669
717
  main.innerHTML = '<div class="empty">加载中…</div>';
670
718
  const r = await call('catalog', {});
@@ -673,10 +721,15 @@
673
721
  if (!items.length) { main.innerHTML = '<div class="empty">暂无已上架的连接器</div>'; return; }
674
722
  marketItems = {};
675
723
  items.forEach((d) => { marketItems[d.id] = d; });
676
- const filtered = items.filter(matchesSearch);
724
+ const filtered = items.filter(matchesMarketFilters);
677
725
  const count = filtered.length;
678
726
  const visible = filtered.slice(0, catalogRenderLimit);
679
- main.innerHTML = `<div class="stats"><span class="count">共 <strong>${count}</strong> 个连接器${searchQuery ? ` · 搜索「${esc(searchQuery)}」` : ''} · 点击卡片查看能力,点击「连接」完成授权</span></div><div class="grid">${visible.map(cardHtml).join('')}</div>${visible.length < count ? `<button class="tools-more" id="catalog-more" type="button">再显示 ${Math.min(60, count - visible.length)} 个(剩余 ${count - visible.length})</button>` : ''}`;
727
+ const authLabel = AUTH_FILTERS.find((filter) => filter.value === marketAuth)?.label;
728
+ const filterSummary = `${marketVendor ? ` · 服务商「${esc(marketVendor)}」` : ''}${marketAuth ? ` · 接入方式「${esc(authLabel)}」` : ''}`;
729
+ const resultHtml = visible.length > 0
730
+ ? `<div class="grid">${visible.map(cardHtml).join('')}</div>${visible.length < count ? `<button class="tools-more" id="catalog-more" type="button">再显示 ${Math.min(60, count - visible.length)} 个(剩余 ${count - visible.length})</button>` : ''}`
731
+ : '<div class="empty">没有匹配当前筛选条件的连接器</div>';
732
+ main.innerHTML = `${marketFilterHtml(items)}<div class="stats"><span class="count">共 <strong>${count}</strong> 个连接器${searchQuery ? ` · 搜索「${esc(searchQuery)}」` : ''}${filterSummary} · 点击卡片查看能力,点击「连接」完成授权</span></div>${resultHtml}`;
680
733
  if (checkHealth && !initialHealthCheckStarted && items.some((item) => item.connected?.length > 0)) {
681
734
  initialHealthCheckStarted = true;
682
735
  call('healthCheck', {}).then(() => {
@@ -1198,9 +1251,30 @@
1198
1251
 
1199
1252
  $('#add-connection').addEventListener('click', () => openAddConnection('json'));
1200
1253
  main.addEventListener('click', (event) => {
1201
- if (!event.target.closest('#catalog-more')) return;
1202
- catalogRenderLimit += 60;
1203
- loadMarket();
1254
+ const authFilter = event.target.closest('[data-auth-filter]');
1255
+ if (authFilter) {
1256
+ marketAuth = authFilter.dataset.authFilter || '';
1257
+ catalogRenderLimit = 60;
1258
+ loadMarket({ checkHealth: false });
1259
+ return;
1260
+ }
1261
+ if (event.target.closest('#market-filter-reset')) {
1262
+ marketVendor = '';
1263
+ marketAuth = '';
1264
+ catalogRenderLimit = 60;
1265
+ loadMarket({ checkHealth: false });
1266
+ return;
1267
+ }
1268
+ if (event.target.closest('#catalog-more')) {
1269
+ catalogRenderLimit += 60;
1270
+ loadMarket({ checkHealth: false });
1271
+ }
1272
+ });
1273
+ main.addEventListener('change', (event) => {
1274
+ if (event.target.id !== 'market-vendor-filter') return;
1275
+ marketVendor = event.target.value;
1276
+ catalogRenderLimit = 60;
1277
+ loadMarket({ checkHealth: false });
1204
1278
  });
1205
1279
 
1206
1280
  $('#refresh').addEventListener('click', async () => {