dsh-mcp-connector 0.2.36 → 0.2.37
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 +17 -1
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/catalog/catalog.json +9 -19
- package/docs/STDIO-SUPPORT.md +2 -2
- package/docs/USER-GUIDE.md +3 -3
- package/docs/screenshots/01-market-overview.jpg +0 -0
- package/docs/screenshots/02-connector-detail.jpg +0 -0
- package/docs/screenshots/03-tool-discovery.jpg +0 -0
- package/docs/screenshots/04-json-import.jpg +0 -0
- package/docs/screenshots/README.md +4 -4
- package/docs/screenshots/assets.json +6 -6
- package/lib/catalog.js +28 -1
- package/lib/connectors/oauth-connector.js +5 -5
- package/lib/index.js +11 -3
- package/lib/mcp-provision.js +6 -1
- package/lib/schema.js +7 -1
- package/package.json +1 -1
- package/registry/schema/connector.schema.json +2 -0
- package/ui/index.html +17 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.37] - 2026-09-06
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- “企查查·智能文档解析”改为 OAuth 2.0 PKCE 一键授权,一次授权同时启用远程在线 URL 与本地文件解析两个 Server;无需再手工复制 API Key。
|
|
12
|
+
- 更新产品截图和演示 GIF,同步当前 109 张市场卡片、智能文档解析名称及 OAuth 双入口说明。
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- 新增受限制的 OAuth-to-stdio 运行时注入:目录必须将 `oauthResource` 精确绑定到同连接器的 HTTPS MCP resource,Access Token 只注入子进程声明的 `oauthTokenEnv`,不写入连接记录、市场输出或日志。
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- 从旧版 API Key 单入口升级时显示“升级一键授权”;只有 OAuth 与远程/本地两个 Server 全部启动成功后才原子替换旧连接,失败时保留旧配置。
|
|
21
|
+
|
|
7
22
|
## [0.2.36] - 2026-09-06
|
|
8
23
|
|
|
9
24
|
### Changed
|
|
@@ -565,7 +580,8 @@
|
|
|
565
580
|
- 外部 URL 与导入 Header 执行安全校验。
|
|
566
581
|
- iframe 消息校验同源和消息来源。
|
|
567
582
|
|
|
568
|
-
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.
|
|
583
|
+
[Unreleased]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.37...HEAD
|
|
584
|
+
[0.2.37]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.36...v0.2.37
|
|
569
585
|
[0.2.36]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.35...v0.2.36
|
|
570
586
|
[0.2.35]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.34...v0.2.35
|
|
571
587
|
[0.2.34]: https://github.com/duhu2000/dsh-mcp-connector/compare/v0.2.33...v0.2.34
|
package/README.en.md
CHANGED
|
@@ -171,7 +171,7 @@ npm run dev:ui
|
|
|
171
171
|
|
|
172
172
|
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.
|
|
173
173
|
|
|
174
|
-
The current public version is [`dsh-mcp-connector@0.2.
|
|
174
|
+
The current public version is [`dsh-mcp-connector@0.2.37`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.37](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.37).
|
|
175
175
|
|
|
176
176
|
See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
|
|
177
177
|
|
package/README.md
CHANGED
|
@@ -167,7 +167,7 @@ npm run dev:ui
|
|
|
167
167
|
|
|
168
168
|
公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
|
|
169
169
|
|
|
170
|
-
当前公开版本为 [`dsh-mcp-connector@0.2.
|
|
170
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.37`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.37](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.37)。
|
|
171
171
|
|
|
172
172
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
173
173
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/catalog/catalog.json
CHANGED
|
@@ -136,29 +136,18 @@
|
|
|
136
136
|
"icon": "/mcp-connector/ui/assets/qcc-logo.svg",
|
|
137
137
|
"category": "效率工具",
|
|
138
138
|
"summary": "本机文件 / 在线链接 · OCR 与结构化解析",
|
|
139
|
-
"description": "
|
|
139
|
+
"description": "一次 OAuth 授权同时连接远程与本地 Agent:远程入口解析公网 PDF/图片 URL,本地入口读取对话中上传、粘贴或指定路径的本机文件,自动上传并输出 Markdown 与结构化内容。本地入口需 Node.js 20+。",
|
|
140
140
|
"tags": ["智能文档解析", "OCR", "本机文件", "在线文档", "PDF", "图片", "Markdown"],
|
|
141
141
|
"published": true,
|
|
142
142
|
"featured": true,
|
|
143
143
|
"homepage": "https://agent.qcc.com/doc",
|
|
144
144
|
"auth": {
|
|
145
|
-
"mode": "
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"credentialFields": [
|
|
152
|
-
{
|
|
153
|
-
"key": "authorization",
|
|
154
|
-
"label": "Authorization",
|
|
155
|
-
"placeholder": "Bearer YOUR_API_KEY",
|
|
156
|
-
"description": "格式为 Bearer + 空格 + 智能文档解析 API Key;OAuth 登录令牌不能用于本地文件上传。凭据仅保存在 DSH 本机。",
|
|
157
|
-
"helpLabel": "前往企查查智能文档解析获取 API Key",
|
|
158
|
-
"required": true,
|
|
159
|
-
"secret": true
|
|
160
|
-
}
|
|
161
|
-
]
|
|
145
|
+
"mode": "oauth2-pkce",
|
|
146
|
+
"issuer": "https://agent.qcc.com",
|
|
147
|
+
"scope": "mcp:tools",
|
|
148
|
+
"clientName": "DeepSeek Harness - MCP 连接器",
|
|
149
|
+
"tokenEndpointAuthMethod": "none",
|
|
150
|
+
"grantSharing": "issuer"
|
|
162
151
|
},
|
|
163
152
|
"servers": [
|
|
164
153
|
{ "serverKey": "document", "url": "https://agent.qcc.com/mcp/document/stream", "serverName": "qcc-document", "transport": "streamable-http", "headers": {} },
|
|
@@ -168,7 +157,8 @@
|
|
|
168
157
|
"transport": "stdio",
|
|
169
158
|
"command": "npx",
|
|
170
159
|
"args": ["-y", "qcc-document-mcp"],
|
|
171
|
-
"
|
|
160
|
+
"oauthResource": "https://agent.qcc.com/mcp/document/stream",
|
|
161
|
+
"oauthTokenEnv": "QCC_DOCUMENT_AUTHORIZATION"
|
|
172
162
|
}
|
|
173
163
|
],
|
|
174
164
|
"prompts": [
|
package/docs/STDIO-SUPPORT.md
CHANGED
|
@@ -152,7 +152,7 @@ const Config = z.union([
|
|
|
152
152
|
|
|
153
153
|
- stdio 是本地进程,密钥最终仍通过环境变量传递(如 `GITHUB_TOKEN`、`OPENAI_API_KEY`)。
|
|
154
154
|
- 无凭据的 stdio 连接器使用 `auth.mode: "none"`;需要用户输入的卡片使用 `bearer` 或 `api-key`,并在 `auth.credentialFields` 声明一个或多个输入字段。
|
|
155
|
-
- `servers[].credentialBindings` 只保存“环境变量名 → 凭据字段 key
|
|
155
|
+
- `servers[].credentialBindings` 只保存“环境变量名 → 凭据字段 key”的映射;OAuth stdio 只能用 `oauthResource` 引用同连接器的 HTTP resource,并通过 `oauthTokenEnv` 声明运行时 Token 环境变量。`servers[].env` 只能包含非敏感默认值。
|
|
156
156
|
- 用户输入存入本机 `ConnectionRecord.env` 并透传给 `dsh-mcp-client`,不回写目录,也不出现在 catalog/status/log 输出。
|
|
157
157
|
- Registry 探针只校验声明与命令形状,绝不执行本地 stdio 命令。
|
|
158
158
|
|
|
@@ -614,7 +614,7 @@ cwd: { type: 'string', description: 'transport=stdio 时的工作目录,默认
|
|
|
614
614
|
| stdio `command` 可执行任意本地命令 | command 来自(a)维护者审核的 catalog,或(b)用户主动 configure,风险可控;DSH 运行时沙箱机制兜底 |
|
|
615
615
|
| `env` 里的密钥泄露到日志 | dsh-mcp-client 的 `buildChildEnv` 已用 `scrubbedParentEnv()` 清理敏感父环境变量;我们持久化 env 到 storage domain 与现有 headers 鉴权一致,无新增泄露面 |
|
|
616
616
|
| `cwd` 空字符串导致 spawn 异常 | provisioning 层显式 `record.cwd \|\| process.cwd()` |
|
|
617
|
-
| 目录夹带密钥 | `auditRawDescriptor` 拒绝真实凭据字段,`auditDescriptor` 拒绝 `env` 中的 token/secret/API Key/password
|
|
617
|
+
| 目录夹带密钥 | `auditRawDescriptor` 拒绝真实凭据字段,`auditDescriptor` 拒绝 `env` 中的 token/secret/API Key/password 类变量;手工凭据只允许 `credentialBindings` 引用已声明字段,OAuth Token 只允许按同连接器 `oauthResource` 在运行时注入 `oauthTokenEnv` |
|
|
618
618
|
| 凭据映射错误或未使用 | Schema 与目录审计检查字段 key、env 名、重复映射、未知引用及未被任何 HTTP/stdio Server 使用的必填字段 |
|
|
619
619
|
|
|
620
620
|
---
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -75,14 +75,14 @@ Bearer/API Key 连接器会先执行 MCP `initialize` 验证。所有 Server 通
|
|
|
75
75
|
|
|
76
76
|
stdio 市场卡片也可能显示一个或多个凭据字段,例如 API Token、区域或租户标识。卡片目录只声明字段名称及其环境变量映射;提交后,真实值仅保存到 DSH 本机连接记录,并由插件注入该 stdio 进程的 `env`。市场、状态页和日志不会返回这些值。插件会等待 Host 完成首次 MCP 初始化与工具同步后再保存连接;失败或超时不会增加已安装数量,卡片也不会提前显示“已连接”。
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
“企查查·智能文档解析”会通过一次 OAuth 一键授权配置两个互不冲突的入口:
|
|
79
79
|
|
|
80
80
|
- `qcc-document`:远程 Agent,解析公网可访问的在线文档或图片 URL。
|
|
81
81
|
- `qcc-document-mcp`:本地 Agent,通过 `npx -y qcc-document-mcp` 读取对话中上传、粘贴或指定路径的本机文件,自动上传并提交解析;要求 Node.js 20+。
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
点击“连接”后只需完成一次企查查 OAuth 授权,插件会同时注册上述两个 Server。OAuth Access Token 仅在运行时注入本地子进程的 `QCC_DOCUMENT_AUTHORIZATION`,不会写入市场目录、连接记录、页面状态或日志;Token 刷新后本地 Agent 会自动更新。从旧版手工 API Key 配置升级时,点击“升级一键授权”,只有在新授权与两个 Server 都启动成功后才会原子替换旧连接;失败会保留原配置。
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
旧版本只配置过远程入口时,卡片会显示“升级一键授权”,不会把单入口误报为完整连接。完成 OAuth 后,插件先验证两个 Server,再原子保存;任一入口失败都不会留下半套配置。
|
|
86
86
|
|
|
87
87
|
OAuth 一键连接要求服务商支持标准 OAuth 2.1/PKCE 和公开元数据发现。动态客户端注册既支持无需客户端密钥的 `none`,也支持服务商签发密钥的 `client_secret_post` 与 `client_secret_basic`。客户端密钥仅与 OAuth Grant 一同保存在 DSH 本机,用于换取、刷新和撤销 Token;插件不会要求用户把 OAuth Token 或客户端密钥复制到聊天中。如服务商在动态客户端注册阶段返回 HTTP 403,页面会明确提示“客户端注册被拒绝、尚未进入浏览器授权”;这通常需要服务商审核或登记客户端,重试用户授权无法绕过。
|
|
88
88
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# UI 素材说明
|
|
2
2
|
|
|
3
|
-
本目录用于保存 `dsh-mcp-connector` 的公开界面截图。素材于 2026-
|
|
3
|
+
本目录用于保存 `dsh-mcp-connector` 的公开界面截图。素材于 2026-09-06 从 `v0.2.37` 源码的
|
|
4
4
|
无凭据 UI harness 采集:外层浏览器视口为 1280×720,harness 复刻产品宿主在
|
|
5
5
|
`lib/client.js` 中定义的 800px 市场面板,因此桌面端稳定为一行 2 张卡片。公开 Registry
|
|
6
|
-
快照含
|
|
6
|
+
快照含 105 条描述,与 4 张随包唯一卡片合并后市场显示 109 张。画面只展示公开市场元数据、
|
|
7
7
|
示例 Prompt 和明确标识为 Mock 的工具数据,不包含 OAuth Token、API Key、本机路径、
|
|
8
8
|
用户会话或查询结果。
|
|
9
9
|
|
|
@@ -21,8 +21,8 @@ SHA-256、尺寸与 GIF 时长。
|
|
|
21
21
|
|
|
22
22
|
| 文件 | 展示内容 |
|
|
23
23
|
|---|---|
|
|
24
|
-
| `01-market-overview.jpg` |
|
|
25
|
-
| `02-connector-detail.jpg` |
|
|
24
|
+
| `01-market-overview.jpg` | 109 张合并市场卡片、桌面两列、推荐章节、9 分类和固定分类栏;展示“企查查·智能文档解析”新名称与未配置状态 |
|
|
25
|
+
| `02-connector-detail.jpg` | 智能文档解析一次 OAuth 授权同时连接 2 个 Server:远程 Agent 解析在线 URL,本地 Agent 通过 `npx -y qcc-document-mcp` 解析本机文件 |
|
|
26
26
|
| `03-tool-discovery.jpg` | Context7 无凭据 Mock 健康状态、Mock 工具数量/描述、搜索和独立滚动区 |
|
|
27
27
|
| `04-json-import.jpg` | Streamable HTTP / stdio JSON 导入与本机凭据提示 |
|
|
28
28
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"capture": {
|
|
4
|
-
"capturedOn": "2026-
|
|
4
|
+
"capturedOn": "2026-09-06",
|
|
5
5
|
"browserViewport": {
|
|
6
6
|
"width": 1280,
|
|
7
7
|
"height": 720
|
|
@@ -13,32 +13,32 @@
|
|
|
13
13
|
"screenshots": [
|
|
14
14
|
{
|
|
15
15
|
"path": "docs/screenshots/01-market-overview.jpg",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "9c9bc1c6f9e5b247c71ce283b86895b9e971f6283dc6eabaa9dbb1c42ae408c3",
|
|
17
17
|
"width": 1280,
|
|
18
18
|
"height": 720
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "docs/screenshots/02-connector-detail.jpg",
|
|
22
|
-
"sha256": "
|
|
22
|
+
"sha256": "dbb7a1493378c30e743dd77c8a35c1779f443a2f8b720871de1efc61ee54eb27",
|
|
23
23
|
"width": 1280,
|
|
24
24
|
"height": 720
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "docs/screenshots/03-tool-discovery.jpg",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "5eeb896db75a5f141a8463f128f7733ffe89af6a447cecb2d1b19f8287a3e1ec",
|
|
29
29
|
"width": 1280,
|
|
30
30
|
"height": 720
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"path": "docs/screenshots/04-json-import.jpg",
|
|
34
|
-
"sha256": "
|
|
34
|
+
"sha256": "de0a87cc7f247fad40ac37188f5a1d026f4087571e98d64595e80f8bcd9410be",
|
|
35
35
|
"width": 1280,
|
|
36
36
|
"height": 720
|
|
37
37
|
}
|
|
38
38
|
],
|
|
39
39
|
"demo": {
|
|
40
40
|
"path": "docs/demo.gif",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "c9a84ce9c164c1b9f846f8b5a65afd8dd5c49741c9dfc530f1ad766e47b3d2b6",
|
|
42
42
|
"width": 960,
|
|
43
43
|
"height": 540,
|
|
44
44
|
"durationSeconds": 16
|
package/lib/catalog.js
CHANGED
|
@@ -43,6 +43,11 @@ export function loadBundledCatalog() {
|
|
|
43
43
|
|
|
44
44
|
/** 安全审计:目录禁止携带密钥、URL 协议白名单、header 名白名单。 */
|
|
45
45
|
export function auditDescriptor(descriptor) {
|
|
46
|
+
const oauthHttpResources = new Set(
|
|
47
|
+
descriptor.servers
|
|
48
|
+
.filter((server) => server.transport === 'streamable-http')
|
|
49
|
+
.map((server) => server.url),
|
|
50
|
+
);
|
|
46
51
|
const credentialFields = descriptor.auth.credentialFields ?? [];
|
|
47
52
|
const credentialKeys = new Set();
|
|
48
53
|
for (const field of credentialFields) {
|
|
@@ -59,7 +64,26 @@ export function auditDescriptor(descriptor) {
|
|
|
59
64
|
}
|
|
60
65
|
const bindings = server.credentialBindings ?? {};
|
|
61
66
|
if (descriptor.auth.mode === 'oauth2-pkce') {
|
|
62
|
-
|
|
67
|
+
if (Object.keys(bindings).length > 0) {
|
|
68
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth stdio server 不能同时声明 credentialBindings`);
|
|
69
|
+
}
|
|
70
|
+
if (!server.oauthResource || !server.oauthTokenEnv) {
|
|
71
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth stdio server 必须声明 oauthResource 和 oauthTokenEnv`);
|
|
72
|
+
}
|
|
73
|
+
assertSafeUrl(server.oauthResource);
|
|
74
|
+
if (!oauthHttpResources.has(server.oauthResource)) {
|
|
75
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth stdio oauthResource 必须引用同一连接器的 HTTP resource`);
|
|
76
|
+
}
|
|
77
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(server.oauthTokenEnv)) {
|
|
78
|
+
throw new Error(`connector "${descriptor.id}" oauthTokenEnv 环境变量名非法: ${server.oauthTokenEnv}`);
|
|
79
|
+
}
|
|
80
|
+
if (Object.hasOwn(server.env ?? {}, server.oauthTokenEnv)) {
|
|
81
|
+
throw new Error(`connector "${descriptor.id}" oauthTokenEnv 与 servers[].env 重复声明: ${server.oauthTokenEnv}`);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (server.oauthResource || server.oauthTokenEnv) {
|
|
86
|
+
throw new Error(`connector "${descriptor.id}" 只有 OAuth stdio server 可声明 oauthResource/oauthTokenEnv`);
|
|
63
87
|
}
|
|
64
88
|
if (descriptor.auth.mode === 'none' && Object.keys(bindings).length > 0) {
|
|
65
89
|
throw new Error(`connector "${descriptor.id}" 的 stdio credentialBindings 需要 bearer/api-key 凭据定义`);
|
|
@@ -89,6 +113,9 @@ export function auditDescriptor(descriptor) {
|
|
|
89
113
|
}
|
|
90
114
|
}
|
|
91
115
|
}
|
|
116
|
+
if (descriptor.auth.mode === 'oauth2-pkce' && oauthHttpResources.size === 0) {
|
|
117
|
+
throw new Error(`connector "${descriptor.id}" 的 OAuth 授权至少需要一个 HTTP resource`);
|
|
118
|
+
}
|
|
92
119
|
if (['bearer', 'api-key'].includes(descriptor.auth.mode)) {
|
|
93
120
|
const httpServers = descriptor.servers.filter((server) => server.transport === 'streamable-http');
|
|
94
121
|
if (httpServers.length > 0 && credentialFields[0]) referencedCredentials.add(credentialFields[0].key);
|
|
@@ -88,8 +88,8 @@ export function describeOAuthAuthorizationError(error) {
|
|
|
88
88
|
* @param {AbortSignal} [args.signal]
|
|
89
89
|
*/
|
|
90
90
|
export async function oauthAuthorize({ connector, config, logger, signal }) {
|
|
91
|
-
const entryServer = connector.servers
|
|
92
|
-
if (!entryServer) throw stagedError('resource-discovery', `connector "${connector.id}" has no
|
|
91
|
+
const entryServer = connector.servers.find((server) => server.url);
|
|
92
|
+
if (!entryServer) throw stagedError('resource-discovery', `connector "${connector.id}" has no HTTP OAuth resource`);
|
|
93
93
|
|
|
94
94
|
const protectedResource = await atOAuthStage(
|
|
95
95
|
'resource-discovery',
|
|
@@ -166,16 +166,16 @@ export async function oauthAuthorize({ connector, config, logger, signal }) {
|
|
|
166
166
|
let grantedResources;
|
|
167
167
|
if (grantedUrls) {
|
|
168
168
|
const granted = new Set(grantedUrls);
|
|
169
|
-
const grantedServers = connector.servers.filter((s) => granted.has(s.url));
|
|
169
|
+
const grantedServers = connector.servers.filter((s) => granted.has(s.oauthResource ?? s.url));
|
|
170
170
|
grantedKeys = grantedServers.map((s) => s.serverKey);
|
|
171
|
-
grantedResources = grantedServers.map((s) => s.url);
|
|
171
|
+
grantedResources = [...new Set(grantedServers.map((s) => s.oauthResource ?? s.url).filter(Boolean))];
|
|
172
172
|
if (grantedKeys.length === 0) {
|
|
173
173
|
grantedKeys = [entryServer.serverKey];
|
|
174
174
|
grantedResources = [entryServer.url];
|
|
175
175
|
}
|
|
176
176
|
} else {
|
|
177
177
|
grantedKeys = connector.servers.map((s) => s.serverKey);
|
|
178
|
-
grantedResources = connector.servers.map((s) => s.url);
|
|
178
|
+
grantedResources = [...new Set(connector.servers.map((s) => s.oauthResource ?? s.url).filter(Boolean))];
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
return {
|
package/lib/index.js
CHANGED
|
@@ -1051,7 +1051,7 @@ export async function apply(ctx, config) {
|
|
|
1051
1051
|
|
|
1052
1052
|
function oauthResourceOrigins(connector) {
|
|
1053
1053
|
return [...new Set((connector.servers ?? []).map((server) => {
|
|
1054
|
-
try { return new URL(server.url).origin; } catch { return ''; }
|
|
1054
|
+
try { return new URL(server.oauthResource ?? server.url).origin; } catch { return ''; }
|
|
1055
1055
|
}).filter(Boolean))].sort();
|
|
1056
1056
|
}
|
|
1057
1057
|
|
|
@@ -1084,6 +1084,7 @@ export async function apply(ctx, config) {
|
|
|
1084
1084
|
const servers = [];
|
|
1085
1085
|
for (const candidate of sharingConnectors) {
|
|
1086
1086
|
for (const server of candidate.servers) {
|
|
1087
|
+
if (!server.url) continue;
|
|
1087
1088
|
if (seen.has(server.url)) continue;
|
|
1088
1089
|
seen.add(server.url);
|
|
1089
1090
|
servers.push({ ...server, serverKey: `${candidate.id}:${server.serverKey}` });
|
|
@@ -1097,7 +1098,7 @@ export async function apply(ctx, config) {
|
|
|
1097
1098
|
const now = Date.now();
|
|
1098
1099
|
const records = [];
|
|
1099
1100
|
for (const connector of connectors) {
|
|
1100
|
-
for (const server of connector.servers.filter((candidate) => granted.has(candidate.url))) {
|
|
1101
|
+
for (const server of connector.servers.filter((candidate) => granted.has(candidate.oauthResource ?? candidate.url))) {
|
|
1101
1102
|
records.push({
|
|
1102
1103
|
key: `${connector.id}-${server.serverKey}`,
|
|
1103
1104
|
connectorId: connector.id,
|
|
@@ -1106,6 +1107,11 @@ export async function apply(ctx, config) {
|
|
|
1106
1107
|
serverKey: server.serverKey,
|
|
1107
1108
|
transport: server.transport,
|
|
1108
1109
|
url: server.url,
|
|
1110
|
+
command: server.command,
|
|
1111
|
+
args: server.args,
|
|
1112
|
+
env: server.env,
|
|
1113
|
+
oauthTokenEnv: server.oauthTokenEnv,
|
|
1114
|
+
cwd: server.cwd,
|
|
1109
1115
|
serverName: server.serverName,
|
|
1110
1116
|
headers: server.headers,
|
|
1111
1117
|
auth: { mode: 'oauth', grantKey },
|
|
@@ -1146,7 +1152,7 @@ export async function apply(ctx, config) {
|
|
|
1146
1152
|
continue;
|
|
1147
1153
|
}
|
|
1148
1154
|
}
|
|
1149
|
-
if (connector.servers.some((server) => entry.grant.authorizedResources.includes(server.url))) {
|
|
1155
|
+
if (connector.servers.some((server) => entry.grant.authorizedResources.includes(server.oauthResource ?? server.url))) {
|
|
1150
1156
|
return { grantKey, entry };
|
|
1151
1157
|
}
|
|
1152
1158
|
}
|
|
@@ -1757,6 +1763,8 @@ export async function apply(ctx, config) {
|
|
|
1757
1763
|
command: s.command,
|
|
1758
1764
|
args: s.args,
|
|
1759
1765
|
credentialBindings: s.credentialBindings,
|
|
1766
|
+
oauthResource: s.oauthResource,
|
|
1767
|
+
oauthTokenEnv: s.oauthTokenEnv,
|
|
1760
1768
|
transport: s.transport,
|
|
1761
1769
|
serverName: s.serverName,
|
|
1762
1770
|
})),
|
package/lib/mcp-provision.js
CHANGED
|
@@ -26,12 +26,17 @@ export function authHeaders(record, grants) {
|
|
|
26
26
|
|
|
27
27
|
export function buildEntryConfig(record, grants, { failOnStartupError = false } = {}) {
|
|
28
28
|
if (record.transport === 'stdio') {
|
|
29
|
+
const env = { ...(record.env ?? {}) };
|
|
30
|
+
if (record.auth?.mode === 'oauth' && record.oauthTokenEnv) {
|
|
31
|
+
const grant = record.auth.grantKey ? grants.get(record.auth.grantKey) : null;
|
|
32
|
+
if (grant?.accessToken) env[record.oauthTokenEnv] = `Bearer ${grant.accessToken}`;
|
|
33
|
+
}
|
|
29
34
|
return {
|
|
30
35
|
transport: 'stdio',
|
|
31
36
|
serverName: record.serverName,
|
|
32
37
|
command: record.command,
|
|
33
38
|
args: record.args ?? [],
|
|
34
|
-
env
|
|
39
|
+
env,
|
|
35
40
|
cwd: record.cwd || process.cwd(),
|
|
36
41
|
failOnStartupError,
|
|
37
42
|
};
|
package/lib/schema.js
CHANGED
|
@@ -13,6 +13,8 @@ export const serverRefSchema = z.object({
|
|
|
13
13
|
args: z.array(z.string()).optional(),
|
|
14
14
|
env: z.record(z.string()).optional(),
|
|
15
15
|
credentialBindings: z.record(z.string()).optional(),
|
|
16
|
+
oauthResource: z.string().min(1).optional(),
|
|
17
|
+
oauthTokenEnv: z.string().min(1).optional(),
|
|
16
18
|
cwd: z.string().optional(),
|
|
17
19
|
serverName: z.string().min(1),
|
|
18
20
|
transport: z.enum(['streamable-http', 'sse', 'stdio']).optional(),
|
|
@@ -110,6 +112,7 @@ export const connectionRecordSchema = z.object({
|
|
|
110
112
|
command: z.string().min(1).optional(),
|
|
111
113
|
args: z.array(z.string()).optional(),
|
|
112
114
|
env: z.record(z.string()).optional(),
|
|
115
|
+
oauthTokenEnv: z.string().min(1).optional(),
|
|
113
116
|
cwd: z.string().optional(),
|
|
114
117
|
serverName: z.string().min(1),
|
|
115
118
|
headers: z.record(z.string()).optional(),
|
|
@@ -238,6 +241,8 @@ export function normalizeConnectorDescriptor(raw) {
|
|
|
238
241
|
args: transport === 'stdio' ? (s.args ?? []) : undefined,
|
|
239
242
|
env: transport === 'stdio' ? (s.env ?? {}) : undefined,
|
|
240
243
|
credentialBindings: transport === 'stdio' ? (s.credentialBindings ?? {}) : undefined,
|
|
244
|
+
oauthResource: transport === 'stdio' ? s.oauthResource : undefined,
|
|
245
|
+
oauthTokenEnv: transport === 'stdio' ? s.oauthTokenEnv : undefined,
|
|
241
246
|
cwd: transport === 'stdio' ? (s.cwd ?? '') : undefined,
|
|
242
247
|
serverName: s.serverName,
|
|
243
248
|
transport,
|
|
@@ -334,8 +339,9 @@ export function normalizeConnectionRecord(raw) {
|
|
|
334
339
|
command: transport === 'stdio' ? data.command : undefined,
|
|
335
340
|
args: transport === 'stdio' ? (data.args ?? []) : undefined,
|
|
336
341
|
env: transport === 'stdio' ? (data.env ?? {}) : undefined,
|
|
342
|
+
oauthTokenEnv: transport === 'stdio' && data.auth?.mode === 'oauth' ? data.oauthTokenEnv : undefined,
|
|
337
343
|
cwd: transport === 'stdio' ? (data.cwd ?? '') : undefined,
|
|
338
344
|
headers: transport === 'streamable-http' ? (data.headers ?? {}) : {},
|
|
339
|
-
auth: transport === 'stdio' ? undefined : data.auth,
|
|
345
|
+
auth: transport === 'stdio' && data.auth?.mode !== 'oauth' ? undefined : data.auth,
|
|
340
346
|
};
|
|
341
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
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",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"args": { "type": "array", "items": { "type": "string" } },
|
|
58
58
|
"env": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
59
59
|
"credentialBindings": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
60
|
+
"oauthResource": { "type": "string", "minLength": 1 },
|
|
61
|
+
"oauthTokenEnv": { "type": "string", "minLength": 1, "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
60
62
|
"cwd": { "type": "string" },
|
|
61
63
|
"serverName": { "type": "string", "minLength": 1 },
|
|
62
64
|
"transport": { "enum": ["streamable-http", "stdio"] },
|
package/ui/index.html
CHANGED
|
@@ -521,7 +521,7 @@
|
|
|
521
521
|
reauthorize: '需重新授权', recovering: '自动重试中', connectionError: '连接异常', partialError: '部分异常', disabled: '已停用',
|
|
522
522
|
send: '发送', fillSend: '填写并发送', connectToUse: '连接后使用', try: '💬 去试试',
|
|
523
523
|
connectTry: '🔗 连接后试用', configureConnect: '🔑 录入凭据并连接',
|
|
524
|
-
reconfigureCredential: '🔑 重新配置凭据', completeSetup: '补充本地解析', loading: '加载中…',
|
|
524
|
+
reconfigureCredential: '🔑 重新配置凭据', completeSetup: '补充本地解析', upgradeOauth: '升级一键授权', loading: '加载中…',
|
|
525
525
|
};
|
|
526
526
|
const t = (key) => LABELS[key] || key;
|
|
527
527
|
const MCP_JSON_EXAMPLE = JSON.stringify({
|
|
@@ -1064,6 +1064,9 @@
|
|
|
1064
1064
|
if (incomplete && ['bearer', 'api-key'].includes(d.authMode)) {
|
|
1065
1065
|
return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-warning" type="button" onclick="event.stopPropagation();window.__mcp.openConfig('${id}')">${t('completeSetup')}</button>`;
|
|
1066
1066
|
}
|
|
1067
|
+
if (incomplete && d.authMode === 'oauth2-pkce') {
|
|
1068
|
+
return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn status-warning" type="button" onclick="event.stopPropagation();window.__mcp.connect('${id}')">${t('upgradeOauth')}</button>`;
|
|
1069
|
+
}
|
|
1067
1070
|
if (configured) {
|
|
1068
1071
|
if (state === 'healthy') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" disabled>${t('connected')}</button>`;
|
|
1069
1072
|
if (state === 'unknown') return `<button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('details')}</button><button class="btn ghost" type="button" onclick="event.stopPropagation();window.__mcp.showDetail('${id}')">${t('unknown')}</button>`;
|
|
@@ -1630,7 +1633,7 @@
|
|
|
1630
1633
|
$('#detail-disconnect-btn').style.display = d.connected?.length > 0 ? 'block' : 'none';
|
|
1631
1634
|
const needsCredential = ['bearer', 'api-key'].includes(d.authMode);
|
|
1632
1635
|
$('#detail-try-btn').textContent = detailNeedsReconfigure
|
|
1633
|
-
? detailFailureState === 'incomplete' ? t('completeSetup') : d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth' ? '🔐 重新授权' : needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查'
|
|
1636
|
+
? detailFailureState === 'incomplete' ? (d.authMode === 'oauth2-pkce' ? t('upgradeOauth') : t('completeSetup')) : d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth' ? '🔐 重新授权' : needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查'
|
|
1634
1637
|
: d.connected?.length > 0 ? t('try') : needsCredential ? t('configureConnect') : t('connectTry');
|
|
1635
1638
|
if (!d.connected?.length && !d.toolsSnapshot?.length) {
|
|
1636
1639
|
$('#tools-summary').textContent = '连接后查看工具';
|
|
@@ -1677,7 +1680,7 @@
|
|
|
1677
1680
|
? `待补充 ${d.missingServers?.length || 1} 个服务`
|
|
1678
1681
|
: `${detailFailureState === 'degraded' ? '部分服务可用' : '已连接'} ${d.connected?.length || 0} 个服务`;
|
|
1679
1682
|
$('#detail-vendor').textContent = d.vendor ? `${d.vendor} · ${connectedLabel}` : connectedLabel;
|
|
1680
|
-
$('#detail-try-btn').textContent = incomplete ? t('completeSetup') : t('try');
|
|
1683
|
+
$('#detail-try-btn').textContent = incomplete ? (d.authMode === 'oauth2-pkce' ? t('upgradeOauth') : t('completeSetup')) : t('try');
|
|
1681
1684
|
renderPromptPage();
|
|
1682
1685
|
detailToolServers = toolsResult.detail?.servers || [];
|
|
1683
1686
|
updateToolsSummary();
|
|
@@ -1773,6 +1776,17 @@
|
|
|
1773
1776
|
openAddConnection('manual', detailConnector);
|
|
1774
1777
|
return;
|
|
1775
1778
|
}
|
|
1779
|
+
if (detailFailureState === 'incomplete' && detailConnector?.authMode === 'oauth2-pkce') {
|
|
1780
|
+
const id = detailConnector.id;
|
|
1781
|
+
const r = await call('connect', { connectorId: id }, { timeoutMs: 310_000 });
|
|
1782
|
+
showToast(r.message, r.ok);
|
|
1783
|
+
if (r.ok) {
|
|
1784
|
+
markCommunityCtaEligible();
|
|
1785
|
+
await call('healthCheck', { connectorId: id });
|
|
1786
|
+
await refreshConnectionUi(id);
|
|
1787
|
+
}
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1776
1790
|
if (detailFailureState === 'reauth' && detailConnector?.authMode === 'oauth2-pkce') {
|
|
1777
1791
|
const id = detailConnector.id;
|
|
1778
1792
|
const r = await call('connect', { connectorId: id }, { timeoutMs: 310_000 });
|