dsh-oauth-mcp 0.0.2 → 0.0.3
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/README.md +192 -54
- package/client/index.js +21 -0
- package/index.js +31 -0
- package/lib/build-info.js +2 -2
- package/lib/mcp/reveal.js +264 -0
- package/lib/mcp/tool-registry.js +20 -2
- package/lib/tools/reveal.js +137 -0
- package/locale/en.json +2 -0
- package/locale/zh-CN.json +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,102 +12,216 @@
|
|
|
12
12
|
|
|
13
13
|
授权过的远端工具以 `mcp_oauth__<serverId>__<tool>` 的名字出现。
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## 目录
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
1. [安装](#1-安装)
|
|
18
|
+
2. [添加服务器](#2-添加服务器)
|
|
19
|
+
3. [授权](#3-授权)
|
|
20
|
+
4. [查看状态](#4-查看状态)
|
|
21
|
+
5. [使用远端工具](#5-使用远端工具)
|
|
22
|
+
6. [改配置](#6-改配置)
|
|
23
|
+
7. [开发](#7-开发)
|
|
24
|
+
8. [发布](#8-发布)
|
|
25
|
+
9. [遇到问题怎么办](#9-遇到问题怎么办)
|
|
26
|
+
10. [详细文档](#10-详细文档)
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
## 1. 安装
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## 安装
|
|
30
|
+
前置条件:已经装好并运行过 DSH 桌面应用。
|
|
24
31
|
|
|
25
32
|
### 从 npm 装
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```
|
|
34
|
+
1. 在 DSH 的终端里执行:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
dsh plugin --profile desktop add dsh-oauth-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. 看命令的返回值:
|
|
41
|
+
|
|
42
|
+
- `application: applied` —— 已经装上并生效,往下走
|
|
43
|
+
- `application: restart-required` —— 改动要下次启动才生效,重启 DSH
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
3. 打开「设置 → OAuth 授权管理」,确认这一节存在。
|
|
32
46
|
|
|
33
|
-
|
|
47
|
+
看不到这一节时,先确认第 2 步的返回值,重启一次;还不行看[遇到问题怎么办](#9-遇到问题怎么办)。
|
|
34
48
|
|
|
35
49
|
### 从源码目录装
|
|
36
50
|
|
|
37
|
-
要改代码就用这条路径。仓库用 pnpm,版本由 package.json 的 `packageManager`
|
|
51
|
+
要改代码就用这条路径。仓库用 pnpm,版本由 package.json 的 `packageManager` 字段固定。
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
cd <本插件目录>
|
|
41
|
-
pnpm install
|
|
42
|
-
```
|
|
53
|
+
1. 装依赖:
|
|
43
54
|
|
|
44
|
-
|
|
55
|
+
```sh
|
|
56
|
+
cd <本插件目录>
|
|
57
|
+
pnpm install
|
|
58
|
+
```
|
|
45
59
|
|
|
46
|
-
|
|
60
|
+
2. 用宿主的插件安装入口装载(`plugin_manager install_bundle`,`target` 填本目录的绝对路径),命令行等价写法是:
|
|
47
61
|
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
```sh
|
|
63
|
+
dsh plugin add <本插件目录的绝对路径>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
3. 看返回值。需要重启就重启 DSH(见上面第 2 步)。
|
|
67
|
+
|
|
68
|
+
profile 里这个包是指向源码目录的链接,改完源码只要重启客户端,不用重新装载。
|
|
69
|
+
|
|
70
|
+
### 从打包文件装
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
pnpm pack # 产出 dsh-oauth-mcp-<版本号>.tgz
|
|
74
|
+
dsh plugin add ./dsh-oauth-mcp-<版本号>.tgz
|
|
51
75
|
dsh plugin list | grep oauth # 确认装上了
|
|
52
76
|
```
|
|
53
77
|
|
|
54
|
-
|
|
78
|
+
## 2. 添加服务器
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
1. 打开 设置 →「OAuth 授权管理」,点「添加服务器」。
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
2. 在厂商下拉里选一家。内置 40 家实测过的远程 MCP 服务器,选中之后服务器地址、ID、名称、默认权限范围、授权服务器地址自动填好。
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
3. 勾选权限范围,保存。
|
|
61
85
|
|
|
62
|
-
|
|
86
|
+
下拉里选「自定义」就全部手填。ID 留空会按地址自动生成。
|
|
63
87
|
|
|
64
|
-
|
|
88
|
+
也可以直接在地址框里输入厂商地址,插件只自动填 ID,不动名称和权限范围。
|
|
65
89
|
|
|
66
|
-
|
|
90
|
+
服务器行右侧的胶囊标签是配置状态:「已启用」表示插件会尝试自动连接这台服务器,「已停用」表示插件跳过它。
|
|
67
91
|
|
|
68
|
-
|
|
92
|
+
## 3. 授权
|
|
69
93
|
|
|
70
|
-
|
|
94
|
+
1. 在会话里发起授权:
|
|
71
95
|
|
|
72
|
-
```
|
|
73
|
-
/oauth-mcp authorize <serverId | 服务器地址>
|
|
74
|
-
```
|
|
96
|
+
```text
|
|
97
|
+
/oauth-mcp authorize <serverId | 服务器地址>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
写地址时插件会当场把它加入服务器列表,命中已知厂商时还会补上默认权限范围与授权服务器地址。
|
|
101
|
+
|
|
102
|
+
2. 命令结果是一张卡片,卡片里有可点的授权链接,一行可以复制的纯 URL。
|
|
103
|
+
|
|
104
|
+
3. 在浏览器里打开链接。同意动作发生在厂商自己的网页上,插件不代点。
|
|
75
105
|
|
|
76
|
-
|
|
106
|
+
4. 授权完成后插件跳回 `http://127.0.0.1:39271/oauth-mcp/callback`,页面显示成功提示。页面上看不到任何令牌、code 或 state。
|
|
77
107
|
|
|
108
|
+
回调端口被占用时插件自动降级到 `host-web` 或 `manual-only`。`manual-only` 模式下把浏览器地址栏里的整条回调 URL 粘回来完成授权:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
/oauth-mcp complete <serverId> <回调 URL>
|
|
78
112
|
```
|
|
113
|
+
|
|
114
|
+
## 4. 查看状态
|
|
115
|
+
|
|
116
|
+
```text
|
|
79
117
|
/oauth-mcp status
|
|
80
118
|
```
|
|
81
119
|
|
|
82
120
|
`status` 报告每台服务器是否已授权、过期时间、有没有刷新令牌、是否已连接、工具数。令牌本身不显示。
|
|
83
121
|
|
|
84
|
-
|
|
122
|
+
看授权后拿到哪些工具:
|
|
85
123
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
| --- | --- |
|
|
90
|
-
| [docs/usage.md](docs/usage.md) | 字段表、命令与工具参考、回调模式、令牌存储、错误码、故障排查 |
|
|
91
|
-
| [docs/vendors.md](docs/vendors.md) | 40 家远程 MCP 服务器的 OAuth 元数据实测表 |
|
|
92
|
-
| [docs/verification.md](docs/verification.md) | 12 步手工验收清单 |
|
|
93
|
-
| [docs/contract.md](docs/contract.md) | 模块级接口契约 |
|
|
94
|
-
| [docs/review-2026-11-07.md](docs/review-2026-11-07.md) | 六个方面的评审记录:已改的问题与剩下的清单 |
|
|
95
|
-
| [DESIGN.md](DESIGN.md) | 设计全文与未决问题 |
|
|
96
|
-
| [TASKS.md](TASKS.md) | 任务与验收记录 |
|
|
124
|
+
```text
|
|
125
|
+
/oauth-mcp tools
|
|
126
|
+
```
|
|
97
127
|
|
|
98
|
-
|
|
128
|
+
命令的完整用法:
|
|
99
129
|
|
|
130
|
+
```text
|
|
131
|
+
/oauth-mcp status [serverId] 查看每台服务器的状态
|
|
132
|
+
/oauth-mcp tools [serverId] 查看授权后拿到的工具
|
|
133
|
+
/oauth-mcp authorize <serverId | url> 授权一台服务器
|
|
134
|
+
/oauth-mcp complete <serverId> <url> 用回执 URL 完成授权
|
|
135
|
+
/oauth-mcp disconnect <serverId> 断开连接(凭据保留)
|
|
136
|
+
/oauth-mcp cancel <serverId> 取消进行中的授权
|
|
137
|
+
/oauth-mcp help 列出这些用法
|
|
100
138
|
```
|
|
101
|
-
cd <本插件目录>
|
|
102
|
-
pnpm install
|
|
103
|
-
pnpm test # 等价于 node --test "test/**/*.test.js"
|
|
104
|
-
```
|
|
105
139
|
|
|
106
|
-
|
|
140
|
+
## 5. 使用远端工具
|
|
141
|
+
|
|
142
|
+
授权过的服务器,它的工具以 `mcp_oauth__<serverId>__<tool>` 注册成宿主工具。
|
|
143
|
+
|
|
144
|
+
例如授权了 `linear`,远端的 `list_issues` 工具在宿主里叫 `mcp_oauth__linear__list_issues`。
|
|
145
|
+
|
|
146
|
+
用的时候有两种写法:直接描述要做的事,agent 会自己挑合适的工具;或者点名调用,名字从 `/oauth-mcp tools` 里复制。
|
|
147
|
+
|
|
148
|
+
默认前缀 `mcp_oauth` 与宿主自带的 `mcp__<server>__<tool>` 不冲突。
|
|
149
|
+
|
|
150
|
+
### 惰性工具
|
|
107
151
|
|
|
108
|
-
|
|
152
|
+
授权 3 家服务器可能有 70 多个远端工具。全部挂在模型的工具表上,每一步都要背着它们的定义 —— 与本次任务无关的那部分也在里面。
|
|
109
153
|
|
|
110
|
-
|
|
154
|
+
默认开着惰性工具(`lazyTools`):
|
|
155
|
+
|
|
156
|
+
1. 工具表里只有一个入口工具 `mcp_oauth`,远端工具的名字模型都看不到。
|
|
157
|
+
2. agent 调用 `mcp_oauth`(可以带 `{"server":"notion"}` 指定一台,不带就列全部)。
|
|
158
|
+
3. 答案里是服务器列表和它公开的工具名。
|
|
159
|
+
4. 下一步起这些工具进入工具表,可以直接调用。揭示是当轮生效的。
|
|
160
|
+
|
|
161
|
+
agent 凭记忆硬调没揭示的工具会被拦下,理由里写明要先调用 `mcp_oauth`。
|
|
162
|
+
|
|
163
|
+
揭示状态按会话隔离:同一个会话里揭示一次就一直有效,换个会话回到没揭示的状态,不落盘。
|
|
164
|
+
|
|
165
|
+
实测(DSH 桌面版 0.2.2-rc.1,授权了 notion 与 context7):
|
|
166
|
+
|
|
167
|
+
| | 工具表里的工具数 | 远端工具数 |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| `lazyTools` 关 | 132 | 79 |
|
|
170
|
+
| `lazyTools` 开 | 54 | 0 |
|
|
171
|
+
|
|
172
|
+
面板里可以随时关掉这个开关,关掉后远端工具一直挂在工具表上。
|
|
173
|
+
|
|
174
|
+
## 6. 改配置
|
|
175
|
+
|
|
176
|
+
面板能改的字段只有 `servers`、`defaultScopes`、`autoConnect`、`lazyTools` 四个,写完立即生效,插件不重启。
|
|
177
|
+
|
|
178
|
+
`callbackMode`、`callbackPort`、`callbackPath`、`toolPrefix` 在面板里只读展示,要改去宿主的配置文件(设置 → 通用设置 →「打开配置文件」,改 profile 里 `dsh-oauth-mcp` 条目的 `config:`),保存后宿主重启本插件。
|
|
179
|
+
|
|
180
|
+
| 配置项 | 默认值 | 说明 |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| `callbackMode` | `dedicated-port` | 回调接收方式:`dedicated-port` / `host-web` / `manual-only` |
|
|
183
|
+
| `callbackPort` | `39271` | 回环监听端口;被占用时自动降级 |
|
|
184
|
+
| `callbackPath` | `/oauth-mcp/callback` | 回调路径 |
|
|
185
|
+
| `toolPrefix` | `mcp_oauth` | 远端工具的前缀,最终工具名为 `<prefix>__<serverId>__<tool>` |
|
|
186
|
+
| `authorizationTimeoutMs` | `300000` | 一次授权请求的有效期 |
|
|
187
|
+
| `defaultScopes` | `[]` | 服务器没有单独配置权限范围时使用的默认值 |
|
|
188
|
+
| `autoConnect` | `true` | 插件启动时自动连接已启用的服务器 |
|
|
189
|
+
| `servers` | `[]` | 服务器清单 |
|
|
190
|
+
| `lazyTools` | `true` | 远端工具只在揭示之后进工具表 |
|
|
191
|
+
|
|
192
|
+
改配置不影响令牌。令牌存在 `ctx.credentials` 里,不在插件配置里。但已建立的 MCP 会话会断开,并在下次连接时重建。
|
|
193
|
+
|
|
194
|
+
GitHub、Zendesk、Slack、PagerDuty、MongoDB Atlas、Render、Shopify、Atlassian 八家要多做一步,见 [docs/vendors.md](docs/vendors.md)。
|
|
195
|
+
|
|
196
|
+
完整的插件配置项表和服务器条目表见 [docs/usage.md](docs/usage.md#2-配置)。
|
|
197
|
+
|
|
198
|
+
## 7. 开发
|
|
199
|
+
|
|
200
|
+
仓库有一步构建。浏览器那一半是浏览器以传统脚本方式加载的,import 不了同目录文件,所以 `client/index.js` 里的组件是生成出来的副本。
|
|
201
|
+
|
|
202
|
+
1. 改完 `client/panel.js`、`client/toolview.js`、`client/commandview.js` 或 `lib/profiles/known-profiles.js` 后重新生成:
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
pnpm run build
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
它把厂商表、浏览器副本和语言文件一起重新生成,且是幂等的。`test/unit/client-sync.test.js` 会检查副本与来源逐行一致。
|
|
209
|
+
|
|
210
|
+
2. 跑测试:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
cd <本插件目录>
|
|
214
|
+
pnpm install
|
|
215
|
+
pnpm test # 等价于 node --test "test/**/*.test.js"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
仓库记录的最近一次全量运行是 **660 例**,全绿。用例分单元、协议契约、端到端、宿主集成、冒烟五类,外加发布清单与仓库卫生两条守卫。端到端有两种:`test/e2e/full-flow.test.js` 走协议层,`test/e2e/host-flow.test.js` 走完整链路。
|
|
219
|
+
|
|
220
|
+
`node --test test/` 在 Node v24 上不可用:`--test` 的位置参数按 glob 解析,`test/` 会被当成模块去加载。
|
|
221
|
+
|
|
222
|
+
插件本体是纯 JavaScript,宿主直接吃 `index.js`,运行时只依赖 `@deepseek-ai/schemastery`。
|
|
223
|
+
|
|
224
|
+
## 8. 发布
|
|
111
225
|
|
|
112
226
|
版本号写在 `package.json` 的 `version` 字段里。
|
|
113
227
|
|
|
@@ -116,3 +230,27 @@ pnpm test # 等价于 node --test "test/**/*.test.js"
|
|
|
116
230
|
CI 用 pnpm 装依赖(`pnpm install --frozen-lockfile`),每次 push 和 PR 都会在 Node 22 与 Node 24 上跑一遍测试套件。
|
|
117
231
|
|
|
118
232
|
npm 发布那一步需要 GitHub 仓库里配一个 `NPM_TOKEN` secret。
|
|
233
|
+
|
|
234
|
+
## 9. 遇到问题怎么办
|
|
235
|
+
|
|
236
|
+
| 现象 | 原因 | 处理 |
|
|
237
|
+
| --- | --- | --- |
|
|
238
|
+
| 重启后设置页没有「OAuth 授权管理」 | 装载返回 `restart-required`,改动要下次启动才生效 | 重启 DSH 客户端;还不行就重新装载一次 |
|
|
239
|
+
| 面板里服务器列表是空的 | 客户端脚本没连上宿主的服务 | 硬刷新浏览器(`Ctrl+Shift+R`) |
|
|
240
|
+
| 改了代码但行为没变 | 宿主没有热重载 | 重启 DSH,然后在浏览器里硬刷新 |
|
|
241
|
+
| 状态里报 `credential_service_unavailable` | 宿主还没提供凭据服务 | 重启 DSH |
|
|
242
|
+
| 端口 39271 被占用 | 回环端口不可用 | 插件自动降级到 `host-web` 或 `manual-only` |
|
|
243
|
+
|
|
244
|
+
更细的排查见 [docs/usage.md](docs/usage.md#9-故障排查)。
|
|
245
|
+
|
|
246
|
+
## 10. 详细文档
|
|
247
|
+
|
|
248
|
+
| 文档 | 内容 |
|
|
249
|
+
| --- | --- |
|
|
250
|
+
| [docs/usage.md](docs/usage.md) | 字段表、命令与工具参考、回调模式、令牌存储、错误码、故障排查 |
|
|
251
|
+
| [docs/vendors.md](docs/vendors.md) | 40 家远程 MCP 服务器的 OAuth 元数据实测表 |
|
|
252
|
+
| [docs/verification.md](docs/verification.md) | 12 步手工验收清单 |
|
|
253
|
+
| [docs/contract.md](docs/contract.md) | 模块级接口契约 |
|
|
254
|
+
| [docs/review-2026-11-07.md](docs/review-2026-11-07.md) | 六个方面的评审记录:已改的问题与剩下的清单 |
|
|
255
|
+
| [DESIGN.md](DESIGN.md) | 设计全文与未决问题 |
|
|
256
|
+
| [TASKS.md](TASKS.md) | 任务与验收记录 |
|
package/client/index.js
CHANGED
|
@@ -91,6 +91,9 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
'panel.global.toolPrefix': '工具名前缀',
|
|
92
92
|
'panel.global.defaultScopes': '默认权限范围',
|
|
93
93
|
'panel.global.autoConnect': '启动时自动连接',
|
|
94
|
+
'panel.global.lazyTools': '惰性工具',
|
|
95
|
+
'panel.global.lazyToolsHint':
|
|
96
|
+
'远端工具只有在 agent 调用 mcp_oauth 揭示之后才进工具表,这样每一步就不必背着它们的定义。关掉则一直挂着。',
|
|
94
97
|
'panel.global.restartHint': '回调方式、回调端口、回调路径与工具名前缀在宿主的配置文件里改(设置 → 通用设置 → 打开配置文件),保存后宿主会重启本插件。',
|
|
95
98
|
'panel.servers.vendor': '厂商',
|
|
96
99
|
'panel.servers.vendor.custom': '自定义(手动填写地址)',
|
|
@@ -182,6 +185,9 @@ window.__ModuleLoader__.load({
|
|
|
182
185
|
'panel.global.toolPrefix': 'Tool name prefix',
|
|
183
186
|
'panel.global.defaultScopes': 'Default scopes',
|
|
184
187
|
'panel.global.autoConnect': 'Connect on startup',
|
|
188
|
+
'panel.global.lazyTools': 'Lazy tools',
|
|
189
|
+
'panel.global.lazyToolsHint':
|
|
190
|
+
'The remote tools enter the tool table only after the agent reveals them with the mcp_oauth tool, so no step carries their definitions. Turn it off to keep them in the table all the time.',
|
|
185
191
|
'panel.global.restartHint':
|
|
186
192
|
'Callback mode, callback port, callback path and the tool name prefix are edited in the host configuration file (Settings → General → Open configuration file); the host restarts this plugin afterwards.',
|
|
187
193
|
'panel.servers.vendor': 'Vendor',
|
|
@@ -3312,6 +3318,21 @@ function createPanelComponent(React, options = {}) {
|
|
|
3312
3318
|
}),
|
|
3313
3319
|
h('span', { style: style.muted }, t('panel.global.autoConnect')),
|
|
3314
3320
|
),
|
|
3321
|
+
h(
|
|
3322
|
+
'label',
|
|
3323
|
+
{ className: 'dsh-oauth-mcp-panel-check', style: style.check },
|
|
3324
|
+
h('input', {
|
|
3325
|
+
type: 'checkbox',
|
|
3326
|
+
checked: config.lazyTools !== false,
|
|
3327
|
+
onChange: (event) => void writeField('lazyTools', event.target.checked),
|
|
3328
|
+
}),
|
|
3329
|
+
h('span', { style: style.muted }, t('panel.global.lazyTools')),
|
|
3330
|
+
),
|
|
3331
|
+
h(
|
|
3332
|
+
'div',
|
|
3333
|
+
{ className: 'dsh-oauth-mcp-panel-hint', style: style.muted },
|
|
3334
|
+
t('panel.global.lazyToolsHint'),
|
|
3335
|
+
),
|
|
3315
3336
|
h(
|
|
3316
3337
|
'div',
|
|
3317
3338
|
{ className: 'dsh-oauth-mcp-panel-hint', style: style.muted },
|
package/index.js
CHANGED
|
@@ -34,9 +34,11 @@ import { createMcpSession } from './lib/mcp/session.js'
|
|
|
34
34
|
import { createAuthFetch } from './lib/mcp/auth-fetch.js'
|
|
35
35
|
import { createToolRegistry } from './lib/mcp/tool-registry.js'
|
|
36
36
|
import { createToolExecutor } from './lib/mcp/tool-executor.js'
|
|
37
|
+
import { createRevealState } from './lib/mcp/reveal.js'
|
|
37
38
|
import { createOauthMcpService } from './lib/service.js'
|
|
38
39
|
import { createAuthorizeTool } from './lib/tools/authorize.js'
|
|
39
40
|
import { createStatusTool } from './lib/tools/status.js'
|
|
41
|
+
import { createRevealTool } from './lib/tools/reveal.js'
|
|
40
42
|
import { renderStatusTable, renderScopeDetails, renderToolTable } from './lib/view-model.js'
|
|
41
43
|
import { redactText } from './lib/util/redact.js'
|
|
42
44
|
import { readConfigArray, readConfigField } from './lib/util/config.js'
|
|
@@ -95,6 +97,7 @@ export const Config = z.object({
|
|
|
95
97
|
autoConnect: z.boolean().default(true).volatile(),
|
|
96
98
|
servers: z.array(ServerSchema).default([]).volatile(),
|
|
97
99
|
locale: z.string().default('').volatile(),
|
|
100
|
+
lazyTools: z.boolean().default(true).volatile(),
|
|
98
101
|
})
|
|
99
102
|
|
|
100
103
|
/**
|
|
@@ -238,10 +241,18 @@ export async function apply(ctx, config) {
|
|
|
238
241
|
disconnect: (serverId) => disconnectServer(serverId),
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
// The lazy exposure state. While it is on, the remote tools are only offered
|
|
245
|
+
// to the model after the model asked for them through the entry tool.
|
|
246
|
+
const reveal = createRevealState({
|
|
247
|
+
enabled: config.lazyTools !== false,
|
|
248
|
+
toolPrefix: config.toolPrefix,
|
|
249
|
+
})
|
|
250
|
+
|
|
241
251
|
const toolRegistry = createToolRegistry({
|
|
242
252
|
tools: ctx.tools,
|
|
243
253
|
toolPrefix: config.toolPrefix,
|
|
244
254
|
logger,
|
|
255
|
+
admissionFor: (serverId) => reveal.admission(serverId),
|
|
245
256
|
callTool: async (serverId, remoteToolName, args, options) => {
|
|
246
257
|
const connection = connectionMap.get(serverId)
|
|
247
258
|
if (!connection?.executor) {
|
|
@@ -435,6 +446,26 @@ export async function apply(ctx, config) {
|
|
|
435
446
|
)
|
|
436
447
|
dispose(ctx.tools.register(createStatusTool({ service, logger, config, settings })))
|
|
437
448
|
|
|
449
|
+
// --- the lazy exposure -----------------------------------------------------
|
|
450
|
+
// While the lazy exposure is on, the remote tools are not in the model's
|
|
451
|
+
// tool table until the model asks for them through the `mcp_oauth` entry
|
|
452
|
+
// tool. The entry tool itself is always in the table: it is the only hint
|
|
453
|
+
// the model gets that the remote tools exist.
|
|
454
|
+
dispose(ctx.tools.register(createRevealTool({ reveal, service, locale: () => config.locale })))
|
|
455
|
+
|
|
456
|
+
// A model may remember a tool name from an earlier step and call it even
|
|
457
|
+
// though the tool is not in this step's table. The guard turns that call
|
|
458
|
+
// into an explanation instead of an opaque failure.
|
|
459
|
+
if (typeof ctx.tools.guard === 'function') {
|
|
460
|
+
try {
|
|
461
|
+
dispose(ctx.tools.guard((exec) => reveal.guardFor(exec)))
|
|
462
|
+
} catch (guardError) {
|
|
463
|
+
logger?.warn?.('dsh-oauth-mcp: the tool guard did not register', {
|
|
464
|
+
error: String(guardError?.message ?? guardError),
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
438
469
|
// --- the host command ------------------------------------------------------
|
|
439
470
|
if (commands?.register) {
|
|
440
471
|
dispose(
|
package/lib/build-info.js
CHANGED
|
@@ -12,13 +12,13 @@ import { createTranslator } from './i18n/index.js'
|
|
|
12
12
|
export const PLUGIN_ID = 'dsh-oauth-mcp'
|
|
13
13
|
|
|
14
14
|
/** The version of the plugin. It is reported to every server it talks to. */
|
|
15
|
-
export const PLUGIN_VERSION = '0.0.
|
|
15
|
+
export const PLUGIN_VERSION = '0.0.3'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Short marker of the current source revision. Bump it whenever the behaviour
|
|
19
19
|
* of the user facing surfaces (commands, tools, panel, callback pages) changes.
|
|
20
20
|
*/
|
|
21
|
-
export const BUILD_STAMP = '2026-11-08-
|
|
21
|
+
export const BUILD_STAMP = '2026-11-08-lazy-tools'
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* One line that names the version and the build of the running code.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy exposure of the remote MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* The problem this module solves: every tool of every authorized server used to
|
|
5
|
+
* sit in the model's tool table for the whole session. A profile that
|
|
6
|
+
* authorized three servers carried thirty-odd remote tool schemas in every
|
|
7
|
+
* step, whether the task touched the desktop or not.
|
|
8
|
+
*
|
|
9
|
+
* The host already has the primitive for this. `@deepseek-ai/dsh-tools` lets a
|
|
10
|
+
* registration carry a `when` condition:
|
|
11
|
+
*
|
|
12
|
+
* ```js
|
|
13
|
+
* tools.register(definition, { when: (context) => boolean })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The condition is evaluated once per model step, before the step's tool table
|
|
17
|
+
* is built. A tool whose condition returns `false` is not in that step's tool
|
|
18
|
+
* table at all — the model neither sees its schema nor knows its name. The
|
|
19
|
+
* condition receives `{ agent, modelSelection, signal }`, so the decision can
|
|
20
|
+
* be per session.
|
|
21
|
+
*
|
|
22
|
+
* This module owns the state behind that decision:
|
|
23
|
+
*
|
|
24
|
+
* * {@link createRevealState} keeps, per agent, the set of servers whose tools
|
|
25
|
+
* the model has asked for. The state lives in a `WeakMap` keyed by the live
|
|
26
|
+
* agent, so a new session starts over and a finished agent is collected.
|
|
27
|
+
* * {@link RevealState#admission} produces the `when` condition for one
|
|
28
|
+
* server.
|
|
29
|
+
* * {@link RevealState#guard} produces the dispatch veto. The `when` condition
|
|
30
|
+
* already keeps an unrevealed tool out of the tool table, but the model may
|
|
31
|
+
* remember a name from an earlier step and call it anyway; the guard turns
|
|
32
|
+
* that call into an explanation instead of an opaque failure.
|
|
33
|
+
*
|
|
34
|
+
* The reveal is per session on purpose. A user who authorized three servers
|
|
35
|
+
* does not want to re-reveal them in every new conversation, but a task that
|
|
36
|
+
* never touches a remote server should not pay for their schemas either. The
|
|
37
|
+
* compromise this module takes: the state is per agent, and the entry tool
|
|
38
|
+
* reveals everything on request.
|
|
39
|
+
*
|
|
40
|
+
* @module dsh-oauth-mcp/mcp/reveal
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The state of the reveal, for every agent that asked for tools.
|
|
45
|
+
*/
|
|
46
|
+
export class RevealState {
|
|
47
|
+
/**
|
|
48
|
+
* The revealed servers per agent.
|
|
49
|
+
*
|
|
50
|
+
* @type {WeakMap<object, Set<string>>}
|
|
51
|
+
*/
|
|
52
|
+
#agents = new WeakMap()
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The servers that were revealed to every agent.
|
|
56
|
+
*
|
|
57
|
+
* A server revealed here is offered to every session. The plugin uses it
|
|
58
|
+
* for the servers the user marked as always available; nothing sets it yet,
|
|
59
|
+
* but the code path exists so the semantics do not change later.
|
|
60
|
+
*
|
|
61
|
+
* @type {Set<string>}
|
|
62
|
+
*/
|
|
63
|
+
#global = new Set()
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether lazy exposure is on at all.
|
|
67
|
+
*
|
|
68
|
+
* When it is off every remote tool is admitted, exactly as before this
|
|
69
|
+
* module existed. The setting belongs to the plugin configuration.
|
|
70
|
+
*
|
|
71
|
+
* @type {boolean}
|
|
72
|
+
*/
|
|
73
|
+
#enabled
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The prefix of the public tool names, used to recognise them in a dispatch.
|
|
77
|
+
*
|
|
78
|
+
* @type {string}
|
|
79
|
+
*/
|
|
80
|
+
#toolPrefix
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the state.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} [options] The options.
|
|
86
|
+
* @param {boolean} [options.enabled=true] Whether lazy exposure is on.
|
|
87
|
+
* @param {string} [options.toolPrefix='mcp_oauth'] The prefix of the public tool names.
|
|
88
|
+
*/
|
|
89
|
+
constructor({ enabled = true, toolPrefix = 'mcp_oauth' } = {}) {
|
|
90
|
+
this.#enabled = enabled !== false
|
|
91
|
+
this.#toolPrefix = typeof toolPrefix === 'string' && toolPrefix.length > 0 ? toolPrefix : 'mcp_oauth'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether lazy exposure is on.
|
|
96
|
+
*
|
|
97
|
+
* @returns {boolean} True when the tools are revealed on demand.
|
|
98
|
+
*/
|
|
99
|
+
get enabled() {
|
|
100
|
+
return this.#enabled
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The servers revealed to one agent.
|
|
105
|
+
*
|
|
106
|
+
* @param {object | undefined | null} agent The agent.
|
|
107
|
+
* @returns {Set<string>} The revealed server ids.
|
|
108
|
+
*/
|
|
109
|
+
#revealedFor(agent) {
|
|
110
|
+
if (!agent || typeof agent !== 'object') return this.#global
|
|
111
|
+
let revealed = this.#agents.get(agent)
|
|
112
|
+
if (!revealed) {
|
|
113
|
+
revealed = new Set(this.#global)
|
|
114
|
+
this.#agents.set(agent, revealed)
|
|
115
|
+
}
|
|
116
|
+
return revealed
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether one server is revealed to one agent.
|
|
121
|
+
*
|
|
122
|
+
* @param {object | undefined | null} agent The agent.
|
|
123
|
+
* @param {string} serverId The server id.
|
|
124
|
+
* @returns {boolean} True when the tools of that server may be offered.
|
|
125
|
+
*/
|
|
126
|
+
isRevealed(agent, serverId) {
|
|
127
|
+
if (!this.#enabled) return true
|
|
128
|
+
if (this.#global.has(serverId)) return true
|
|
129
|
+
return this.#revealedFor(agent).has(serverId)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reveal servers to one agent.
|
|
134
|
+
*
|
|
135
|
+
* @param {object | undefined | null} agent The agent.
|
|
136
|
+
* @param {string[]} serverIds The servers to reveal.
|
|
137
|
+
* @returns {string[]} The server ids that are revealed afterwards.
|
|
138
|
+
*/
|
|
139
|
+
reveal(agent, serverIds) {
|
|
140
|
+
if (!agent || typeof agent !== 'object') return []
|
|
141
|
+
const revealed = this.#revealedFor(agent)
|
|
142
|
+
const added = []
|
|
143
|
+
for (const serverId of serverIds) {
|
|
144
|
+
if (typeof serverId !== 'string' || serverId.length === 0) continue
|
|
145
|
+
if (revealed.has(serverId)) continue
|
|
146
|
+
revealed.add(serverId)
|
|
147
|
+
added.push(serverId)
|
|
148
|
+
}
|
|
149
|
+
return added
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Reveal every server to one agent.
|
|
154
|
+
*
|
|
155
|
+
* @param {object | undefined | null} agent The agent.
|
|
156
|
+
* @param {string[]} serverIds Every known server id.
|
|
157
|
+
* @returns {string[]} The server ids that are revealed afterwards.
|
|
158
|
+
*/
|
|
159
|
+
revealAll(agent, serverIds) {
|
|
160
|
+
return this.reveal(agent, serverIds)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The `when` condition for the tools of one server.
|
|
165
|
+
*
|
|
166
|
+
* The condition is what the host evaluates per model step. It returns true
|
|
167
|
+
* when the tools of that server may be offered to the model.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} serverId The server id.
|
|
170
|
+
* @returns {(context: { agent?: unknown }) => boolean} The condition.
|
|
171
|
+
*/
|
|
172
|
+
admission(serverId) {
|
|
173
|
+
if (!this.#enabled) return () => true
|
|
174
|
+
return (context) => this.isRevealed(context?.agent, serverId)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The dispatch veto for the tools of one server.
|
|
179
|
+
*
|
|
180
|
+
* The veto runs when a tool is about to be dispatched. It refuses a call to
|
|
181
|
+
* a server that was not revealed and explains how to reveal it. It returns
|
|
182
|
+
* `undefined` for a call it allows, which is what the host expects.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} serverId The server id.
|
|
185
|
+
* @returns {(exec: { name?: string, agent?: unknown }) => string | undefined} The veto.
|
|
186
|
+
*/
|
|
187
|
+
guard(serverId) {
|
|
188
|
+
if (!this.#enabled) return () => undefined
|
|
189
|
+
return (exec) => {
|
|
190
|
+
if (this.isRevealed(exec?.agent, serverId)) return undefined
|
|
191
|
+
return this.#refusal(serverId)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The refusal the guard gives for a server that was not revealed.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} serverId The server id.
|
|
199
|
+
* @returns {string} The message.
|
|
200
|
+
*/
|
|
201
|
+
#refusal(serverId) {
|
|
202
|
+
return (
|
|
203
|
+
`The tools of the MCP server "${serverId}" are not revealed in this session. ` +
|
|
204
|
+
`Call the "${REVEAL_TOOL_NAME}" tool with {"server":"${serverId}"} first, ` +
|
|
205
|
+
'then call the tool again.'
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The dispatch veto for every remote tool of the plugin.
|
|
211
|
+
*
|
|
212
|
+
* The host calls this before every tool dispatch. It recognises the remote
|
|
213
|
+
* tools by their public name and refuses the ones whose server was not
|
|
214
|
+
* revealed; every other tool of the host passes through untouched.
|
|
215
|
+
*
|
|
216
|
+
* @param {{ name?: string, agent?: unknown }} exec The pending call.
|
|
217
|
+
* @returns {string | undefined} The refusal, or undefined when the call is allowed.
|
|
218
|
+
*/
|
|
219
|
+
guardFor(exec) {
|
|
220
|
+
if (!this.#enabled) return undefined
|
|
221
|
+
const serverId = this.#serverIdOf(exec?.name)
|
|
222
|
+
if (serverId === undefined) return undefined
|
|
223
|
+
if (this.isRevealed(exec?.agent, serverId)) return undefined
|
|
224
|
+
return this.#refusal(serverId)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The server a public tool name belongs to.
|
|
229
|
+
*
|
|
230
|
+
* The public name is `<prefix>__<serverId>__<toolName>`, so the server id
|
|
231
|
+
* is the middle part. A name that does not have that shape is not a remote
|
|
232
|
+
* tool of this plugin.
|
|
233
|
+
*
|
|
234
|
+
* @param {unknown} name The public tool name.
|
|
235
|
+
* @returns {string | undefined} The server id, or undefined.
|
|
236
|
+
*/
|
|
237
|
+
#serverIdOf(name) {
|
|
238
|
+
if (typeof name !== 'string') return undefined
|
|
239
|
+
if (!name.startsWith(`${this.#toolPrefix}__`)) return undefined
|
|
240
|
+
const rest = name.slice(this.#toolPrefix.length + 2)
|
|
241
|
+
const separator = rest.indexOf('__')
|
|
242
|
+
if (separator <= 0) return undefined
|
|
243
|
+
return rest.slice(0, separator)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The name of the tool that reveals the remote tools.
|
|
249
|
+
*
|
|
250
|
+
* @type {string}
|
|
251
|
+
*/
|
|
252
|
+
export const REVEAL_TOOL_NAME = 'mcp_oauth'
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build the reveal state of the plugin.
|
|
256
|
+
*
|
|
257
|
+
* @param {object} [options] The options.
|
|
258
|
+
* @param {boolean} [options.enabled=true] Whether lazy exposure is on.
|
|
259
|
+
* @param {string} [options.toolPrefix='mcp_oauth'] The prefix of the public tool names.
|
|
260
|
+
* @returns {RevealState} The state.
|
|
261
|
+
*/
|
|
262
|
+
export function createRevealState(options = {}) {
|
|
263
|
+
return new RevealState(options)
|
|
264
|
+
}
|
package/lib/mcp/tool-registry.js
CHANGED
|
@@ -108,10 +108,11 @@ function buildDescription(serverId, remoteToolName, remoteDescription) {
|
|
|
108
108
|
* @param {object} [options.logger] The host logger; every call is optional.
|
|
109
109
|
* @param {(serverId: string, remoteToolName: string, args: object, callOptions: { timeoutMs?: number, signal?: AbortSignal }) => Promise<unknown>} [options.callTool] The injected executor that performs `tools/call`.
|
|
110
110
|
* @param {number} [options.timeoutMs] The timeout of one tool call, in milliseconds.
|
|
111
|
+
* @param {(serverId: string) => ((context: { agent?: unknown }) => boolean)} [options.admissionFor] The per-server condition that decides whether the tools of that server are offered to the model. The host evaluates it once per model step; a tool whose condition is false is not in that step's tool table.
|
|
111
112
|
* @returns {object} The registry: `sync`, `unregisterServer`, `dispose`, `hostToolName`.
|
|
112
113
|
*/
|
|
113
114
|
export function createToolRegistry(options = {}) {
|
|
114
|
-
const { tools, logger, callTool } = options
|
|
115
|
+
const { tools, logger, callTool, admissionFor } = options
|
|
115
116
|
const toolPrefix = typeof options.toolPrefix === 'string' && options.toolPrefix.length > 0
|
|
116
117
|
? options.toolPrefix
|
|
117
118
|
: DEFAULT_TOOL_PREFIX
|
|
@@ -121,6 +122,23 @@ export function createToolRegistry(options = {}) {
|
|
|
121
122
|
throw new TypeError('createToolRegistry requires the host tool service.')
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The admission condition of one server.
|
|
127
|
+
*
|
|
128
|
+
* Without a condition every tool of every server is offered to the model,
|
|
129
|
+
* which is the behaviour before the lazy exposure existed. With one, the
|
|
130
|
+
* host evaluates it per model step and leaves the tools of a server that
|
|
131
|
+
* was not revealed out of that step's tool table.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} serverId The server id.
|
|
134
|
+
* @returns {(context: { agent?: unknown }) => boolean} The condition.
|
|
135
|
+
*/
|
|
136
|
+
function admission(serverId) {
|
|
137
|
+
if (typeof admissionFor !== 'function') return () => true
|
|
138
|
+
const condition = admissionFor(serverId)
|
|
139
|
+
return typeof condition === 'function' ? condition : () => true
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
/** Registrations per server: `serverId` → `remoteToolName` → registration. */
|
|
125
143
|
const registrations = new Map()
|
|
126
144
|
/** Public name → the registration that owns it. */
|
|
@@ -239,7 +257,7 @@ export function createToolRegistry(options = {}) {
|
|
|
239
257
|
|
|
240
258
|
let disposer
|
|
241
259
|
try {
|
|
242
|
-
disposer = tools.register(definition)
|
|
260
|
+
disposer = tools.register(definition, { when: admission(serverId) })
|
|
243
261
|
} catch (registerError) {
|
|
244
262
|
error(`dsh-oauth-mcp: the host refused the tool "${name}".`, registerError)
|
|
245
263
|
return
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `mcp_oauth` agent tool: the entry point of the lazy exposure.
|
|
3
|
+
*
|
|
4
|
+
* While lazy exposure is on, the remote tools of the authorized servers are
|
|
5
|
+
* not in the model's tool table. This tool is the way in: the model calls it
|
|
6
|
+
* to learn which servers are authorized and which tools they expose, and the
|
|
7
|
+
* tools it asks for become callable from the next step on.
|
|
8
|
+
*
|
|
9
|
+
* The tool is the only one the plugin keeps in the table unconditionally, so
|
|
10
|
+
* it is also the tool that tells the model that the others exist. Its
|
|
11
|
+
* description has to carry that job on its own: the model sees this tool and
|
|
12
|
+
* nothing else.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-oauth-mcp/tools/reveal
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { REVEAL_TOOL_NAME } from '../mcp/reveal.js'
|
|
18
|
+
|
|
19
|
+
export { REVEAL_TOOL_NAME }
|
|
20
|
+
|
|
21
|
+
/** JSON Schema of the value the tool resolves with. */
|
|
22
|
+
const REVEAL_OUTPUT_SCHEMA = Object.freeze({
|
|
23
|
+
type: 'array',
|
|
24
|
+
description: 'The servers whose tools were revealed, and the tools they expose.',
|
|
25
|
+
items: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
serverId: { type: 'string', description: 'The identifier of the revealed MCP server.' },
|
|
29
|
+
label: { type: 'string', description: 'The human readable label of the server.' },
|
|
30
|
+
authorized: { type: 'boolean', description: 'Whether the server has a usable grant.' },
|
|
31
|
+
connected: { type: 'boolean', description: 'Whether the MCP session is established.' },
|
|
32
|
+
tools: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: { type: 'string' },
|
|
35
|
+
description:
|
|
36
|
+
'The public names of the tools of this server, for example "mcp_oauth__notion__search". They are callable from the next step on.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['serverId', 'label', 'authorized', 'connected', 'tools'],
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The description of the entry tool.
|
|
45
|
+
*
|
|
46
|
+
* It is written for a model that has no other MCP tool in its table: it says
|
|
47
|
+
* what the tool is for, when to call it, and what comes back.
|
|
48
|
+
*
|
|
49
|
+
* @returns {string} The description.
|
|
50
|
+
*/
|
|
51
|
+
function buildDescription() {
|
|
52
|
+
return (
|
|
53
|
+
'Reveal the tools of the OAuth protected MCP servers of this profile. ' +
|
|
54
|
+
'The tools of those servers are not in your tool table until you ask for them, ' +
|
|
55
|
+
'so that every step does not carry their schemas. ' +
|
|
56
|
+
'Call it with {"server":"<serverId>"} to reveal one server, ' +
|
|
57
|
+
'or without arguments to list the authorized servers. ' +
|
|
58
|
+
'The answer lists the server and the names of its tools; those names are callable from the next step on.'
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the entry tool.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} options The options.
|
|
66
|
+
* @param {import('../mcp/reveal.js').RevealState} options.reveal The reveal state.
|
|
67
|
+
* @param {{ status: (serverId?: string) => Promise<object[]>, listToolNames: (serverId: string) => string[] }} options.service The plugin service.
|
|
68
|
+
* @param {() => string} [options.locale] The current locale.
|
|
69
|
+
* @returns {object} The tool definition.
|
|
70
|
+
*/
|
|
71
|
+
export function createRevealTool({ reveal, service, locale }) {
|
|
72
|
+
return {
|
|
73
|
+
name: REVEAL_TOOL_NAME,
|
|
74
|
+
description: buildDescription(),
|
|
75
|
+
parameters: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
server: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description:
|
|
81
|
+
'The id of the MCP server to reveal, for example "notion". Leave it out to list the authorized servers.',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
output: { schema: REVEAL_OUTPUT_SCHEMA, render: (value) => value },
|
|
86
|
+
execute: async (args, exec) => {
|
|
87
|
+
const agent = exec?.agent
|
|
88
|
+
const requested = typeof args?.server === 'string' ? args.server.trim() : ''
|
|
89
|
+
|
|
90
|
+
const rows = await service.status(requested || undefined)
|
|
91
|
+
if (rows.length === 0) {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: requested
|
|
96
|
+
? `No MCP server with the id "${requested}" is configured in this profile.`
|
|
97
|
+
: 'No MCP server is configured in this profile. Add one in the settings, then authorize it.',
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Reveal what the model asked for. Without a server id every authorized
|
|
103
|
+
// server is revealed: the model asked what is there, and the answer is
|
|
104
|
+
// more useful than another round trip.
|
|
105
|
+
const wanted = rows.filter((row) => row.authorized).map((row) => row.serverId)
|
|
106
|
+
const target = requested ? rows.filter((row) => row.serverId === requested) : rows
|
|
107
|
+
const revealIds = target.filter((row) => row.authorized).map((row) => row.serverId)
|
|
108
|
+
reveal.reveal(agent, revealIds)
|
|
109
|
+
|
|
110
|
+
const lines = []
|
|
111
|
+
for (const row of target) {
|
|
112
|
+
const tools = service.listToolNames(row.serverId)
|
|
113
|
+
lines.push(
|
|
114
|
+
[
|
|
115
|
+
`${row.serverId}: ${row.label}`,
|
|
116
|
+
row.authorized ? 'authorized' : 'not authorized yet',
|
|
117
|
+
row.connected ? 'connected' : 'not connected',
|
|
118
|
+
tools.length > 0 ? `tools: ${tools.join(', ')}` : 'no tools yet',
|
|
119
|
+
].join(' — '),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
if (requested && revealIds.length === 0) {
|
|
123
|
+
lines.push(`The server "${requested}" is not authorized yet, so its tools were not revealed.`)
|
|
124
|
+
}
|
|
125
|
+
if (wanted.length === 0 && !requested) {
|
|
126
|
+
lines.push('No server is authorized yet. Use the oauth-mcp command to authorize one.')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: lines.join('\n'),
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
}
|
package/locale/en.json
CHANGED
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"panel.global.toolPrefix": "Tool name prefix",
|
|
46
46
|
"panel.global.defaultScopes": "Default scopes",
|
|
47
47
|
"panel.global.autoConnect": "Connect on startup",
|
|
48
|
+
"panel.global.lazyTools": "Lazy tools",
|
|
49
|
+
"panel.global.lazyToolsHint": "The remote tools enter the tool table only after the agent reveals them with the mcp_oauth tool, so no step carries their definitions. Turn it off to keep them in the table all the time.",
|
|
48
50
|
"panel.global.restartHint": "Callback mode, callback port, callback path and the tool name prefix are edited in the host configuration file (Settings → General → Open configuration file); the host restarts this plugin afterwards.",
|
|
49
51
|
"panel.servers.vendor": "Vendor",
|
|
50
52
|
"panel.servers.vendor.custom": "Custom (type the address yourself)",
|
package/locale/zh-CN.json
CHANGED
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"panel.global.toolPrefix": "工具名前缀",
|
|
46
46
|
"panel.global.defaultScopes": "默认权限范围",
|
|
47
47
|
"panel.global.autoConnect": "启动时自动连接",
|
|
48
|
+
"panel.global.lazyTools": "惰性工具",
|
|
49
|
+
"panel.global.lazyToolsHint": "远端工具只有在 agent 调用 mcp_oauth 揭示之后才进工具表,这样每一步就不必背着它们的定义。关掉则一直挂着。",
|
|
48
50
|
"panel.global.restartHint": "回调方式、回调端口、回调路径与工具名前缀在宿主的配置文件里改(设置 → 通用设置 → 打开配置文件),保存后宿主会重启本插件。",
|
|
49
51
|
"panel.servers.vendor": "厂商",
|
|
50
52
|
"panel.servers.vendor.custom": "自定义(手动填写地址)",
|
package/package.json
CHANGED