dsh-plugin-mcp-apps 0.0.0-stage → 0.5.7
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/LICENSE +21 -0
- package/README.md +139 -2
- package/cordis.patch.yml +9 -0
- package/lib/auth.js +61 -0
- package/lib/client.js +1069 -0
- package/lib/index.js +706 -0
- package/lib/oauth.js +239 -0
- package/package.json +83 -4
- package/scripts/install-remote.sh +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stuxx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,140 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-mcp-apps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
通用 MCP 连接器 + MCP Apps 工作台。
|
|
4
|
+
|
|
5
|
+
- **连接应用**页(左侧栏「技能」下方,`order: 3`):添加 / 编辑 / 启停任意 MCP 服务器,
|
|
6
|
+
支持粘贴 Claude Code 的 `servers.json`(`{"mcpServers": {...}}`)或表单逐项填写。
|
|
7
|
+
- **传输**:Streamable HTTP、SSE、stdio(本地进程)。
|
|
8
|
+
- **鉴权**:三种模式
|
|
9
|
+
- 无鉴权;
|
|
10
|
+
- API 参数(`header` / `query` / `body` 三个位置,任意多个,每个连接一套;保存后值以
|
|
11
|
+
`••••` 回显,留空表示沿用旧值);
|
|
12
|
+
- OAuth 2.0(浏览器授权码 + PKCE):客户端注册方式可选**自动注册(DCR)**、
|
|
13
|
+
**CIMD**(用本插件的元数据文档地址当 `client_id`)、或**手填 `client_id` /
|
|
14
|
+
`client_secret`**。
|
|
15
|
+
- **启停**:关闭的连接不向模型注册任何工具(`ctx.tools.register` 的 disposer 全部执行),
|
|
16
|
+
模型下一轮就看不到它;重新启用会重新连接并注册。
|
|
17
|
+
- **MCP Apps**:带 `_meta.ui.resourceUri`(`ui://…`)的工具,其调用完成后自动在右侧
|
|
18
|
+
「工作台」标签页里渲染对应 App;聊天里只留一行 `【已开启工作台】`。右侧栏不可用时
|
|
19
|
+
改为在对话内联渲染,并给出 `ui://` 资源地址。
|
|
20
|
+
|
|
21
|
+
## 行为
|
|
22
|
+
|
|
23
|
+
| 场景 | 聊天里的工具行 | 右侧栏 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 带 `ui://` 资源的工具调用完成 | `【已开启工作台】` + 「在右侧打开」按钮 | 自动展开一个 `mcp-app` 标签页,渲染该工具对应的 App |
|
|
26
|
+
| 同一会话再次调用 | 同上(不重复开标签页) | 复用同一个标签页,按新的 `navigation.revision` 重挂一遍 App |
|
|
27
|
+
| 右侧栏打不开(无会话 / 服务缺席 / 注册失败) | 「工作台未能打开,改为在对话内渲染。」+ **MCP 资源地址**(`ui://…`)+ 内联 App + 原始结果文本 | 无 |
|
|
28
|
+
| 刷新页面后重渲染的历史调用 | 「历史调用」+「在右侧打开」按钮(不自动开) | 无 |
|
|
29
|
+
| 其它(不带 UI 的)工具 | 完全不变,走通用行 | 无 |
|
|
30
|
+
|
|
31
|
+
宿主侧(`lib/index.js`)另外注册一段系统提示词:这些工具成功后只回一行
|
|
32
|
+
`【已开启工作台】`,不复述金额与明细;用户要看数据请到右侧工作台。提示词**需要重启
|
|
33
|
+
宿主**才生效。
|
|
34
|
+
|
|
35
|
+
## OAuth 2.0
|
|
36
|
+
|
|
37
|
+
1. 连接的鉴权模式选「OAuth 2.0」,可填 `scope`;点保存后在卡片上点「授权登录」。
|
|
38
|
+
2. 宿主按 MCP 规范做 RFC 9728 资源元数据发现 → RFC 8414 授权服务器发现 → 动态客户端注册
|
|
39
|
+
(服务器不支持时改填 `client_id` / `client_secret`)→ 生成 PKCE(S256)授权地址,返回给页面。
|
|
40
|
+
3. 页面打开授权页;服务器回调到**本页同源**的 `/api/mcp-apps/oauth/callback`
|
|
41
|
+
(地址按发起授权时的 `Host` / `X-Forwarded-Proto` 推导,所以走反向代理也成立)。
|
|
42
|
+
4. 客户端的三种来源:
|
|
43
|
+
- `auto`(默认):服务器支持动态注册时自动注册;
|
|
44
|
+
- `cimd`:插件在 **`https://<本页域名>/api/mcp-apps/oauth/client-metadata.json`**
|
|
45
|
+
发布 CIMD 文档(免鉴权 GET,内容随请求域名生成),文档里的 `client_id` 就是该地址,
|
|
46
|
+
`redirect_uris` 指向同源回调。适用于只支持 URL 形式 client_id
|
|
47
|
+
(`client_id_metadata_document_supported`)或不允许动态注册的服务器;需要 HTTPS 访问。
|
|
48
|
+
- `manual`:填服务器预先分配的 `client_id` / `client_secret`。
|
|
49
|
+
切换注册方式会丢弃旧的客户端注册(DCR 的 client id 不能当作 CIMD 用),已取到的令牌保留、可继续用。
|
|
50
|
+
5. 回调用 `state` 校验(PKCE + state 双保险,128 位随机),换到的 access/refresh token
|
|
51
|
+
只写进宿主 `$DSH_HOME/mcp-apps/connections.json`(0600),随后自动重连并注册工具。
|
|
52
|
+
6. 令牌过期由 MCP SDK 用 refresh_token 续期;「清除授权」会断开连接并丢弃令牌
|
|
53
|
+
(客户端注册信息保留,下次直接重新授权)。
|
|
54
|
+
|
|
55
|
+
回调页是一个极简 HTML,授权成功后自动 `window.close()` 并通知打开它的页面刷新列表。
|
|
56
|
+
|
|
57
|
+
## 数据与安全
|
|
58
|
+
|
|
59
|
+
- 连接配置存在 `$DSH_HOME/mcp-apps/connections.json`(权限 `0600`),宿主重启后自动
|
|
60
|
+
重连所有启用的连接;首次运行会写入一条 `mcp-stuxx(示例)`(默认关闭,填好 token 再启用)。
|
|
61
|
+
- **凭据只在宿主进程里**(API 参数与 OAuth 令牌都是):浏览器从不接触 token,也不直接连
|
|
62
|
+
MCP 端点。页面与工作台都通过同源接口 `/api/mcp-apps/*` 走宿主代理
|
|
63
|
+
(`tools/call`、`resources/read`)。OAuth 回调是唯一免 cookie 的路由,靠 `state` 防伪。
|
|
64
|
+
- 该接口要求浏览器携带 DSH 会话 cookie;仅本机回环地址放行,便于 `curl` 自测。
|
|
65
|
+
- 工具的模型可见名是 `mcp__<连接 id>__<原始工具名>`(超长/非法字符会追加哈希),
|
|
66
|
+
与内置 `@deepseek-ai/dsh-mcp-client` 的 `mcp__<serverName>__*` 命名空间互不干扰。
|
|
67
|
+
|
|
68
|
+
## 限制
|
|
69
|
+
|
|
70
|
+
- `body` 位置的鉴权参数会被合并进每个 JSON-RPC 请求体的顶层字段;只对标准 JSON-RPC
|
|
71
|
+
服务器有意义,对严格校验请求字段的服务器可能无效。
|
|
72
|
+
- 图片 / 音频内容块在文本投影里降级为占位说明,不写入附件存储。
|
|
73
|
+
- 单连接工具目录上限 500 条(仅影响 `/state` 展示)。
|
|
74
|
+
- 「工作台是否真的打开」只有浏览器知道,模型拿不到这个信号;失败兜底由工具行渲染,
|
|
75
|
+
而不是由模型生成。
|
|
76
|
+
|
|
77
|
+
## 安装(作为 DSH 插件)
|
|
78
|
+
|
|
79
|
+
本包是一个完整插件:`main` 是宿主半边(连接器 + 系统提示词段),`dsh.client` 让 Web 端
|
|
80
|
+
加载 `./client` 作为浏览器半边,`dsh.bundle.patch` 指向包内自带的 `cordis.patch.yml` ——
|
|
81
|
+
profile 按 `dsh.profile.bundles` 顺序叠加它,插件就会被组装进来。
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npm pack # 产出 dsh-plugin-mcp-apps-<ver>.tgz(再挪进 dist/)
|
|
85
|
+
sh scripts/install.sh /abs/path/to/dsh-plugin-mcp-apps-<ver>.tgz web
|
|
86
|
+
docker restart dsh # 或你惯用的重启方式
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`scripts/install.sh` 做三件事(每步都可重复执行):
|
|
90
|
+
|
|
91
|
+
1. `dsh plugin --profile <profile> add <tgz>` —— 官方转发器,把包放进 profile 的 `node_modules`;
|
|
92
|
+
2. 把包名追加到 `<profile>/package.json` 的 `dsh.profile.bundles`;
|
|
93
|
+
3. 删掉 profile 里旧的手工 `- insert:` 条目,避免同一插件被组装两次。
|
|
94
|
+
|
|
95
|
+
### 装到其他设备
|
|
96
|
+
|
|
97
|
+
`scripts/install-remote.sh` 是同样的三步,但接受 npm 名、git 地址或 tgz:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
# 1) 从 npm(发布后)
|
|
101
|
+
sh scripts/install-remote.sh dsh-plugin-mcp-apps web
|
|
102
|
+
# 2) 直接从 git(仓库公开时用 https,私有仓库用 ssh 并配好密钥)
|
|
103
|
+
sh scripts/install-remote.sh https://github.com/xiaoqin422/dsh-plugin-mcp-apps web
|
|
104
|
+
# 3) 手工拷 tgz
|
|
105
|
+
sh scripts/install-remote.sh ./dsh-plugin-mcp-apps-0.5.6.tgz web
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
profile 不存在时先 `dsh --profile <name> --from-default-profile web --dump-config >/dev/null`
|
|
109
|
+
生成一份,再跑安装脚本。装完都要重启 dsh 并硬刷新页面。
|
|
110
|
+
|
|
111
|
+
前置条件:DSH `>= 0.2.1-rc.2`;右侧工作台需要
|
|
112
|
+
`@deepseek-ai/dsh-client-ui-sidebar-right`(或任意提供 `sidebarRight` 的包)处于启用状态;
|
|
113
|
+
没有它时工具行退化为内联渲染,插件本身不会挂。
|
|
114
|
+
|
|
115
|
+
## 卸载
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
dsh plugin --profile web remove dsh-plugin-mcp-apps
|
|
119
|
+
# 再从 <profile>/package.json 的 dsh.profile.bundles 里删掉这一项,重启
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 开发
|
|
123
|
+
|
|
124
|
+
- 回归测试:
|
|
125
|
+
- `node test/auth-injection.test.mjs` —— header / query / body 三个位置的注入、已有参数
|
|
126
|
+
与 Accept 的保留、裸 token 自动补 `Bearer`。
|
|
127
|
+
- `node test/row-fallback.test.cjs` —— 迷你 React 跑真实 bundle,覆盖页面与侧栏入口注册、
|
|
128
|
+
`/state` 驱动的工具行认领与释放、工作台打开、内联兜底。
|
|
129
|
+
- `node test/oauth-flow.test.cjs` —— 起一个 mock 授权服务器 + MCP 资源服务器,跑完整
|
|
130
|
+
OAuth 链路(发现 → 注册 → PKCE 跳转 → 换令牌 → 带令牌调工具 → 按范围清理凭据),
|
|
131
|
+
以及 CIMD 分支(服务器声明支持 URL 形式 client_id 时,`client_id` 就是元数据文档地址、
|
|
132
|
+
不发生动态注册)。
|
|
133
|
+
- 手动验证:`node -e 'require("./test/mock-oauth.cjs").startMock().then(m=>console.log(m.base))'`
|
|
134
|
+
后把该地址 `/mcp` 作为连接加进来,走一遍「授权登录」。
|
|
135
|
+
- 鉴权注入:`lib/auth.js` 独立成模块,可直接 `import { authFetch }` 用假 fetch 验证
|
|
136
|
+
header / query / body 三个位置以及已有 header 的保留;OAuth 的存储对接在 `lib/oauth.js`
|
|
137
|
+
的 `RecordOAuthProvider`。
|
|
138
|
+
- 生效方式:客户端 bundle 改动刷新页面即可(必要时硬刷新);宿主半边的改动要重启 dsh。
|
|
139
|
+
调试期间可直接改 `/data/profiles/web/node_modules/dsh-plugin-mcp-apps/lib/*` 后再
|
|
140
|
+
重新打包安装,保持一致。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Bundle patch layer for dsh-plugin-mcp-apps.
|
|
2
|
+
#
|
|
3
|
+
# The profile loads this package's `main` as the host half (a plain Cordis
|
|
4
|
+
# plugin: it only contributes the workbench system-prompt section), and the
|
|
5
|
+
# `dsh.client` declaration in package.json makes the web app load `./client`
|
|
6
|
+
# as the browser half (the renderer itself).
|
|
7
|
+
- insert:
|
|
8
|
+
- id: dsh-plugin-mcp-apps
|
|
9
|
+
name: dsh-plugin-mcp-apps
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth parameter injection for MCP HTTP transports.
|
|
3
|
+
*
|
|
4
|
+
* Header and query parameters are applied to every request; body parameters are
|
|
5
|
+
* merged into each outgoing JSON-RPC message. Kept in its own module so the
|
|
6
|
+
* injection rules can be tested without booting a harness.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-plugin-mcp-apps/auth
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a fetch that applies the connection's auth parameters in place.
|
|
13
|
+
* Header and query parameters are set per request; body parameters are merged
|
|
14
|
+
* into each outgoing JSON-RPC message body.
|
|
15
|
+
* @param record - the connection record.
|
|
16
|
+
* @param base - underlying fetch implementation.
|
|
17
|
+
* @returns an auth-aware fetch.
|
|
18
|
+
*/
|
|
19
|
+
export function authFetch(record, base) {
|
|
20
|
+
const params = Array.isArray(record.auth?.params) ? record.auth.params : [];
|
|
21
|
+
const headerParams = params.filter((param) => param && param.in === "header" && param.name);
|
|
22
|
+
const queryParams = params.filter((param) => param && param.in === "query" && param.name);
|
|
23
|
+
const bodyParams = params.filter((param) => param && param.in === "body" && param.name);
|
|
24
|
+
return async (input, init = {}) => {
|
|
25
|
+
// The transport may hand over a string, a URL, or a Request; normalize first.
|
|
26
|
+
const request = new Request(input, init);
|
|
27
|
+
const target = new URL(request.url);
|
|
28
|
+
for (const param of queryParams) target.searchParams.set(param.name, String(param.value ?? ""));
|
|
29
|
+
const nextHeaders = new Headers(request.headers);
|
|
30
|
+
for (const param of headerParams) {
|
|
31
|
+
let value = String(param.value ?? "");
|
|
32
|
+
// A bare token in an Authorization header is never valid HTTP auth:
|
|
33
|
+
// default to the Bearer scheme instead of silently sending a 401.
|
|
34
|
+
if (param.name.toLowerCase() === "authorization" && value !== "" && !/^(bearer|token|basic|digest)\s/i.test(value)) value = "Bearer " + value;
|
|
35
|
+
nextHeaders.set(param.name, value);
|
|
36
|
+
}
|
|
37
|
+
for (const [name, value] of Object.entries(record.headers ?? {})) nextHeaders.set(name, String(value));
|
|
38
|
+
let nextBody;
|
|
39
|
+
if (request.method !== "GET" && request.method !== "HEAD" && request.body !== null) {
|
|
40
|
+
nextBody = await request.clone().text();
|
|
41
|
+
if (bodyParams.length > 0 && typeof nextBody === "string") {
|
|
42
|
+
try {
|
|
43
|
+
const message = JSON.parse(nextBody);
|
|
44
|
+
if (message && typeof message === "object" && !Array.isArray(message)) {
|
|
45
|
+
for (const param of bodyParams) message[param.name] = param.value;
|
|
46
|
+
nextBody = JSON.stringify(message);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
/* not JSON: leave the body untouched */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return base(new Request(target.toString(), {
|
|
54
|
+
method: request.method,
|
|
55
|
+
headers: nextHeaders,
|
|
56
|
+
body: nextBody,
|
|
57
|
+
signal: request.signal
|
|
58
|
+
}));
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|