dsh-mcp 1.0.0 → 1.2.0
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.en.md +24 -0
- package/CHANGELOG.md +24 -0
- package/README.en.md +80 -7
- package/README.md +72 -6
- package/lib/client.js +32 -7
- package/lib/index.js +19 -19
- package/lib/mcp-client.js +733 -0
- package/lib/oauth.js +155 -0
- package/lib/probe.js +20 -3
- package/lib/transport.js +4 -1
- package/package.json +2 -2
- package/src/client/McpSettingsSection.module.css +7 -7
- package/src/client/McpSettingsSection.tsx +9 -1
- package/src/client/ServerForm.module.css +7 -7
- package/src/client/ServerForm.tsx +10 -3
- package/src/client/locales.ts +2 -0
- package/src/client/mcp-store.ts +3 -1
- package/static/snapshot.en.webp +0 -0
package/CHANGELOG.en.md
CHANGED
|
@@ -7,6 +7,30 @@ All notable changes to this project are documented in this file.
|
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
8
8
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2026-08-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **OAuth authentication**: `streamable-http` servers using MCP OAuth (authorization-code + PKCE) trigger browser authorization on connect; tokens are persisted (credentials document) and refreshed automatically by the SDK (auto-renewed while active within 24h) (`lib/oauth.js`)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- OAuth token credential-ref names collided with hyphens in server ids and failed credential validation (ref names only allow `[A-Za-z_][A-Za-z0-9_]*`): refs now use a sanitized server id plus a stable short hash, avoiding illegal characters and naming collisions
|
|
19
|
+
- The interactive OAuth probe budget was raised from 90 seconds to 5 minutes: the first authorization requires browser login/approval, and slower-than-90s flows caused the probe to time out and report a false failure (the authorization had actually succeeded and tokens were saved); now the test result appears automatically once authorization completes
|
|
20
|
+
- Disabled servers no longer render two "Disabled" badges (the phase badge plus a redundant caption)
|
|
21
|
+
- The settings page primary buttons (Add/Save) and the "Connecting" badge used theme tokens that do not exist in the web shell, breaking their text color: switched to the shell's real theme tokens (`--dsw-alias-button-primary-fill` / `--dsw-alias-label-primary-foreground` / `--dsw-alias-brand-primary`)
|
|
22
|
+
|
|
23
|
+
### Improved
|
|
24
|
+
|
|
25
|
+
- While testing a streamable-http server, a hint explains that a browser authorization page may open and the result refreshes automatically after it is completed
|
|
26
|
+
- After saving a server, the list refreshes itself on a delay so "Connecting" settles to "Connected" once the mount is live
|
|
27
|
+
|
|
28
|
+
## [1.1.0] - 2026-08-15
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Tool-list stabilization**: during a same-connection re-sync (e.g. a `tools/list_changed` notification), unchanged MCP tools keep their existing registration instead of being disposed and re-registered, keeping the system-prompt tool list stable to preserve prompt-cache hits (vendored `lib/mcp-client.js` extension)
|
|
33
|
+
|
|
10
34
|
## [1.0.0] - 2026-08-15
|
|
11
35
|
|
|
12
36
|
First stable release.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@
|
|
|
7
7
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
8
8
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2026-08-16
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- **OAuth 认证支持**:`streamable-http` 服务器支持 MCP OAuth(授权码 + PKCE),连接时自动打开浏览器授权;token 持久化(凭据文档)并由 SDK 自动刷新(24 小时内活跃自动续期)(`lib/oauth.js`)
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
|
|
18
|
+
- OAuth token 凭据引用名与服务器名中的连字符冲突导致 `resolve` 校验失败(凭据引用名仅允许 `[A-Za-z_][A-Za-z0-9_]*`):引用名改为清洗后的服务器名 + 稳定哈希,避免非法字符与命名碰撞
|
|
19
|
+
- OAuth 交互授权探测预算从 90 秒提升到 5 分钟:首次授权需在浏览器完成登录/同意,慢于 90 秒会导致探测提前超时并误报连接失败(授权其实已成功、token 已保存),现授权完成后测试结果会自动展示
|
|
20
|
+
- 禁用状态的服务器不再重复展示两个「未启用」徽标(badge 与补充文案叠加)
|
|
21
|
+
- 设置页 primary 按钮(添加/保存)与「连接中」徽标使用了不存在的主题 token,导致文字颜色异常:改用 web shell 真实主题 token(`--dsw-alias-button-primary-fill` / `--dsw-alias-label-primary-foreground` / `--dsw-alias-brand-primary`)
|
|
22
|
+
|
|
23
|
+
### 优化
|
|
24
|
+
|
|
25
|
+
- 测试连接(streamable-http)期间提示浏览器授权:若弹出授权页,完成授权后返回,结果自动刷新
|
|
26
|
+
- 保存服务器后自动延迟刷新列表状态,挂载完成后「连接中」自动变为「已连接」
|
|
27
|
+
|
|
28
|
+
## [1.1.0] - 2026-08-15
|
|
29
|
+
|
|
30
|
+
### 新增
|
|
31
|
+
|
|
32
|
+
- **工具列表稳定化**:同一连接的 re-sync(如 `tools/list_changed` 通知)时,未变化的 MCP 工具保留原注册,不再反复注销/重注册,保持系统提示词工具列表稳定以提升 prompt cache 命中率(vendored `lib/mcp-client.js` 扩展)
|
|
33
|
+
|
|
10
34
|
## [1.0.0] - 2026-08-15
|
|
11
35
|
|
|
12
36
|
首个正式版本。
|
package/README.en.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
# dsh-mcp — MCP
|
|
1
|
+
# dsh-mcp — MCP management UI + tool search: stable tool list, cache hits, no context bloat
|
|
2
2
|
|
|
3
|
-
](https://dshfind.com/en/plugins/ArvinQi/dsh-mcp?ref=badge)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Why dsh-mcp?
|
|
8
|
+
|
|
9
|
+
**Problems it solves:**
|
|
10
|
+
|
|
11
|
+
- **Full tool injection burns tokens**: with multiple MCP servers, the tool count can reach hundreds, and injecting all of them every request is expensive. The `search` mode lets the model hot-inject only the tools it needs via `mcp_tool_search`, saving tokens at scale.
|
|
12
|
+
- **Re-syncs churn the tool list and break caches**: `tools/list_changed` notifications dispose and re-register same-named tools, jittering the system-prompt tool list and constantly invalidating the prompt cache. Tool-list stabilization keeps unchanged tools registered, maximizing cache hits.
|
|
13
|
+
- **No visual management entry**: server config, enable/disable, and tool toggles used to require editing files by hand. Settings → MCP brings everything into one UI.
|
|
14
|
+
|
|
15
|
+
**Highlights:**
|
|
16
|
+
|
|
17
|
+
- **Visual management**: server list / create / edit / delete / test connection / enable-disable / refresh, all in the UI
|
|
18
|
+
- **Fine-grained tool control**: expand each server to see its tools, all checked by default; uncheck to load only what you need
|
|
19
|
+
- **Two injection modes**: `search` (on-demand, token-saving) and `full` (inject everything)
|
|
20
|
+
- **Zero npm dependencies**: plugs into DeepSeek Harness internals, install and go
|
|
21
|
+
- **OAuth authentication**: for `streamable-http` servers using MCP OAuth (authorization-code + PKCE), the browser opens automatically for authorization on connect; tokens are persisted and refreshed automatically by the SDK (auto-renewed while active within 24h)
|
|
22
|
+
- **Three install paths**: npm / GitHub git source / local link; bilingual UI and docs
|
|
4
23
|
|
|
5
24
|
Migrated and merged from uncommitted MCP work in the `deepseek-harness` repository:
|
|
6
25
|
|
|
@@ -22,6 +41,10 @@ Migrated and merged from uncommitted MCP work in the `deepseek-harness` reposito
|
|
|
22
41
|
- **Tool control**:
|
|
23
42
|
- Injection mode: `search` (on-demand, default — the model hot-injects tools via `mcp_tool_search`) and `full` (inject every enabled tool each request).
|
|
24
43
|
- Expandable per-server tool list, all checked by default; unchecking a tool keeps it out of injection. Changes take effect immediately.
|
|
44
|
+
- **OAuth authentication** (host, `lib/oauth.js`): on a 401 + OAuth challenge from a `streamable-http`
|
|
45
|
+
server, runs the authorization-code + PKCE flow automatically — opens the browser, receives the
|
|
46
|
+
callback on a loopback server, persists tokens, and refreshes them on demand; test connection and
|
|
47
|
+
mounts share the same token.
|
|
25
48
|
- **Remote self-mount**: the client half mounts the `mcpManager` Remote namespace itself via `ctx.remote.$mount()` in `apply()`,
|
|
26
49
|
so no in-box package modification is required.
|
|
27
50
|
- Zero npm runtime dependencies (`@deepseek-ai/*` resolve from the DSH profiles module fallback).
|
|
@@ -33,6 +56,8 @@ dsh-mcp/
|
|
|
33
56
|
├── package.json name=dsh-mcp; dsh.client declaration; zero npm dependencies
|
|
34
57
|
├── lib/
|
|
35
58
|
│ ├── index.js host half (McpManagerService, built from mcp-manager)
|
|
59
|
+
│ ├── mcp-client.js vendored MCP client (from @deepseek-ai/dsh-mcp-client, with tool-list stability extension)
|
|
60
|
+
│ ├── oauth.js MCP OAuth client provider (authorization-code + PKCE, loopback callback, token persistence)
|
|
36
61
|
│ ├── probe.js vendored connection probe (from mcp-client/src/probe.ts)
|
|
37
62
|
│ ├── transport.js vendored transport factory (from mcp-client/src/transport.ts)
|
|
38
63
|
│ └── client.js browser half (esbuild bundle, ModuleLoader wire format)
|
|
@@ -53,15 +78,17 @@ node scripts/build.mjs
|
|
|
53
78
|
- CSS Modules are handled by an esbuild onLoad plugin: styles are injected into a
|
|
54
79
|
`<style data-plugin="dsh-mcp" data-file="…">` tag, and the module default-exports an identity class-name map.
|
|
55
80
|
|
|
56
|
-
## Install
|
|
81
|
+
## Install & Usage
|
|
82
|
+
|
|
83
|
+
### 1. Install
|
|
57
84
|
|
|
58
|
-
|
|
85
|
+
**Option 1: npm (after publishing)**
|
|
59
86
|
|
|
60
87
|
```sh
|
|
61
88
|
dsh plugin --profile web add dsh-mcp
|
|
62
89
|
```
|
|
63
90
|
|
|
64
|
-
|
|
91
|
+
**Option 2: GitHub git source**
|
|
65
92
|
|
|
66
93
|
```sh
|
|
67
94
|
dsh plugin --profile web add github:ArvinQi/dsh-mcp
|
|
@@ -69,7 +96,7 @@ dsh plugin --profile web add github:ArvinQi/dsh-mcp
|
|
|
69
96
|
dsh plugin --profile web add git+https://github.com/ArvinQi/dsh-mcp.git
|
|
70
97
|
```
|
|
71
98
|
|
|
72
|
-
|
|
99
|
+
**Option 3: local development (link)**
|
|
73
100
|
|
|
74
101
|
```sh
|
|
75
102
|
dsh plugin --profile web add link:<absolute path to this repo>
|
|
@@ -79,7 +106,7 @@ dsh plugin --profile web add link:<absolute path to this repo>
|
|
|
79
106
|
> symlink (development-only, not committed); otherwise the linked symlink is realpath-resolved and `@deepseek-ai/*`
|
|
80
107
|
> cannot be resolved.
|
|
81
108
|
|
|
82
|
-
### Registration (all install options)
|
|
109
|
+
### 2. Registration (all install options)
|
|
83
110
|
|
|
84
111
|
Append to `$DSH_HOME/profiles/web/cordis.patch.yml` (`$DSH_HOME` defaults to `~/.dsh`):
|
|
85
112
|
|
|
@@ -92,6 +119,50 @@ Append to `$DSH_HOME/profiles/web/cordis.patch.yml` (`$DSH_HOME` defaults to `~/
|
|
|
92
119
|
Then **restart `dsh web`** (client roster changes require a restart); afterwards hard-refresh the browser
|
|
93
120
|
(`Cmd/Ctrl + Shift + R`) to load the settings page.
|
|
94
121
|
|
|
122
|
+
### 3. Usage
|
|
123
|
+
|
|
124
|
+
**Open the management page**: after restart, open DSH Web → **Settings → MCP**.
|
|
125
|
+
|
|
126
|
+
**Add a server**:
|
|
127
|
+
|
|
128
|
+
1. Click **Add server**.
|
|
129
|
+
2. Fill in: server name (`serverName`, determines the tool prefix `mcp__<serverName>__`), transport
|
|
130
|
+
(`streamable-http` → URL / `stdio` → command), headers, tool-call timeout, etc.
|
|
131
|
+
3. Click **Test connection** to verify connectivity and the tool list, then **Save**.
|
|
132
|
+
|
|
133
|
+
**OAuth servers** (`streamable-http` using MCP OAuth, e.g. OAuth-protected gateway services):
|
|
134
|
+
|
|
135
|
+
- Just fill in the URL and test the connection; when the server responds with a 401 + OAuth challenge,
|
|
136
|
+
the plugin **opens the browser automatically** for authorization.
|
|
137
|
+
- Log in / approve in the browser and return to DSH; the test result refreshes automatically
|
|
138
|
+
("connection succeeded + tool count").
|
|
139
|
+
- Tokens are persisted in the credentials document (scoped by `serverName`) and refreshed automatically
|
|
140
|
+
by the MCP SDK (auto-renewed while active within 24h); after authorizing once, mounts and later
|
|
141
|
+
test connections reuse the same token — no repeated authorization.
|
|
142
|
+
- The first authorization needs browser interaction, so the test/connect wait budget is relaxed to
|
|
143
|
+
5 minutes; non-OAuth servers are unaffected and fail fast.
|
|
144
|
+
|
|
145
|
+
**Day-to-day management**:
|
|
146
|
+
|
|
147
|
+
- **Enable / disable**: row button; disabling a server unregisters all of its tools immediately.
|
|
148
|
+
- **Refresh**: re-pulls server status and the tool list (syncs new tools after a server restart).
|
|
149
|
+
- **Test connection**: available in the edit form at any time.
|
|
150
|
+
|
|
151
|
+
**Tool control (the key to saving tokens)**:
|
|
152
|
+
|
|
153
|
+
- **Injection mode**: switch at the top of the page between `search` (on-demand, default) and `full` (inject everything).
|
|
154
|
+
- In `search` mode, the model calls `mcp_tool_search` to discover and hot-inject the MCP tools it needs.
|
|
155
|
+
- **Tool toggles**: click **Expand tools** to see all tools of that server (all checked by default);
|
|
156
|
+
unchecking a tool keeps it out of injection, applied immediately without saving.
|
|
157
|
+
|
|
158
|
+
**Verifying the effect**:
|
|
159
|
+
|
|
160
|
+
- In any agent session, available tools include `mcp__<serverName>__<tool>`.
|
|
161
|
+
- In `search` mode, tools not retrieved stay out of the system prompt, saving tokens and improving
|
|
162
|
+
prompt-cache hit rate.
|
|
163
|
+
- When tool content is unchanged, `list_changed` notifications no longer dispose and re-register
|
|
164
|
+
same-named tools, keeping the tool list stable.
|
|
165
|
+
|
|
95
166
|
## Versioning notes
|
|
96
167
|
|
|
97
168
|
- The host half `lib/index.js` is a **build artifact** of mcp-manager (spec/types inlined); edit the lib files
|
|
@@ -104,3 +175,5 @@ Then **restart `dsh web`** (client roster changes require a restart); afterwards
|
|
|
104
175
|
## Changelog
|
|
105
176
|
|
|
106
177
|
See [CHANGELOG.md](CHANGELOG.md). Released under the [MIT License](LICENSE).
|
|
178
|
+
|
|
179
|
+
[](https://dshfind.com/en/plugins/ArvinQi/dsh-mcp?ref=badge)
|
package/README.md
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
# dsh-mcp — MCP
|
|
1
|
+
# dsh-mcp — MCP 管理界面 + tool search:稳定工具列表、命中缓存、不撑爆上下文
|
|
2
2
|
|
|
3
3
|
**[English](README.en.md) | 简体中文**
|
|
4
4
|
|
|
5
|
+
[](https://dshfind.com/zh/plugins/ArvinQi/dsh-mcp?ref=badge)
|
|
6
|
+
|
|
5
7
|

|
|
6
8
|
|
|
9
|
+
## 为什么用 dsh-mcp?
|
|
10
|
+
|
|
11
|
+
**解决的核心问题:**
|
|
12
|
+
|
|
13
|
+
- **MCP 工具全量注入烧 token**:接入多个 MCP 服务器后工具可达上百个,每轮全量注入开销巨大。`search` 按需检索模式让模型通过 `mcp_tool_search` 热注入所需工具,大幅节省 token。
|
|
14
|
+
- **工具列表反复更新破坏缓存**:`tools/list_changed` 通知会让同名工具被反复注销/重注册,系统提示词工具列表抖动、prompt cache 频繁失效。工具列表稳定化让未变化的工具保留原注册,最大化 cache 命中。
|
|
15
|
+
- **没有可视化管理入口**:服务器配置、启停、工具勾选全靠手工改文件。Settings → MCP 一站式可视化完成。
|
|
16
|
+
|
|
17
|
+
**功能优势:**
|
|
18
|
+
|
|
19
|
+
- **可视化管理**:服务器列表 / 新建 / 编辑 / 删除 / 测试连接 / 启停 / 刷新,全 UI 操作
|
|
20
|
+
- **工具级精细控制**:每个服务器展开工具列表,默认全选,可取消勾选只加载需要的部分
|
|
21
|
+
- **双注入模式**:`search`(按需检索,省 token)与 `full`(全量注入)
|
|
22
|
+
- **零 npm 依赖**:直接对接 DeepSeek Harness 内部能力,安装即用
|
|
23
|
+
- **OAuth 认证支持**:`streamable-http` 服务器若走 MCP OAuth(授权码 + PKCE),连接时自动打开浏览器授权;token 持久化、由 SDK 自动刷新(24 小时内活跃自动续期)
|
|
24
|
+
- **三种安装方式**:npm / GitHub git 源 / 本地 link;中英文界面与文档
|
|
25
|
+
|
|
7
26
|
## 功能
|
|
8
27
|
|
|
9
28
|
- **托管 MCP 服务器注册表**(host):持久化定义(storage-domain `mcp_servers`)、按服务器挂载
|
|
10
29
|
`@deepseek-ai/dsh-mcp-client` 实例、环境变量注入(明文入定义、secret 走 credentials)、
|
|
11
30
|
连接探测(test)。
|
|
12
31
|
- **Web 设置管理页**(client):Settings → MCP,列表/编辑/删除/测试服务器。
|
|
32
|
+
- **OAuth 认证**(host,`lib/oauth.js`):`streamable-http` 服务器遇 401 + OAuth 挑战时自动走
|
|
33
|
+
授权码 + PKCE 流程,打开浏览器授权、回环回调收码、token 持久化并按需自动刷新;
|
|
34
|
+
测试连接与挂载共用同一份 token。
|
|
13
35
|
- **Remote 自挂载**:client 半部在 `apply()` 里自行 `ctx.remote.$mount()` 挂载 `mcpManager`
|
|
14
36
|
命名空间(原实现依赖 api-remotes 的 in-box 修改,独立版不再需要任何 in-box 包改动)。
|
|
15
37
|
|
|
@@ -20,6 +42,8 @@ dsh-mcp/
|
|
|
20
42
|
├── package.json name=dsh-mcp;dsh.client 声明;零 npm dependencies
|
|
21
43
|
├── lib/
|
|
22
44
|
│ ├── index.js host 半部(McpManagerService,源自 mcp-manager 构建产物)
|
|
45
|
+
│ ├── mcp-client.js vendored MCP 客户端(源自 @deepseek-ai/dsh-mcp-client,含工具列表稳定扩展)
|
|
46
|
+
│ ├── oauth.js MCP OAuth 客户端提供者(授权码 + PKCE、回环回调、token 持久化)
|
|
23
47
|
│ ├── probe.js vendored 连接探测(源自 mcp-client/src/probe.ts)
|
|
24
48
|
│ ├── transport.js vendored 传输工厂(源自 mcp-client/src/transport.ts)
|
|
25
49
|
│ └── client.js 浏览器半部(esbuild 打包,ModuleLoader wire format)
|
|
@@ -41,15 +65,17 @@ node scripts/build.mjs
|
|
|
41
65
|
- CSS Modules 由 esbuild onLoad 插件处理:样式注入
|
|
42
66
|
`<style data-plugin="dsh-mcp" data-file="…">`,默认导出 identity 类名映射。
|
|
43
67
|
|
|
44
|
-
##
|
|
68
|
+
## 安装使用
|
|
69
|
+
|
|
70
|
+
### 1. 安装
|
|
45
71
|
|
|
46
|
-
|
|
72
|
+
**方式一:npm(发布到 npm 后)**
|
|
47
73
|
|
|
48
74
|
```sh
|
|
49
75
|
dsh plugin --profile web add dsh-mcp
|
|
50
76
|
```
|
|
51
77
|
|
|
52
|
-
|
|
78
|
+
**方式二:GitHub git 源**
|
|
53
79
|
|
|
54
80
|
```sh
|
|
55
81
|
dsh plugin --profile web add github:ArvinQi/dsh-mcp
|
|
@@ -57,7 +83,7 @@ dsh plugin --profile web add github:ArvinQi/dsh-mcp
|
|
|
57
83
|
dsh plugin --profile web add git+https://github.com/ArvinQi/dsh-mcp.git
|
|
58
84
|
```
|
|
59
85
|
|
|
60
|
-
|
|
86
|
+
**方式三:本地开发(link)**
|
|
61
87
|
|
|
62
88
|
```sh
|
|
63
89
|
dsh plugin --profile web add link:<本仓库绝对路径>
|
|
@@ -67,7 +93,7 @@ dsh plugin --profile web add link:<本仓库绝对路径>
|
|
|
67
93
|
> symlink(本机开发用,不入库),否则 `link:` 安装的 symlink 被 realpath 后无法解析
|
|
68
94
|
> `@deepseek-ai/*`。
|
|
69
95
|
|
|
70
|
-
### 注册与生效(三种方式通用)
|
|
96
|
+
### 2. 注册与生效(三种方式通用)
|
|
71
97
|
|
|
72
98
|
在 `$DSH_HOME/profiles/web/cordis.patch.yml`(`$DSH_HOME` 默认 `~/.dsh`)追加:
|
|
73
99
|
|
|
@@ -80,6 +106,44 @@ dsh plugin --profile web add link:<本仓库绝对路径>
|
|
|
80
106
|
然后**重启 `dsh web`**(client roster 变更需重启);之后浏览器硬刷新(`Cmd/Ctrl + Shift + R`)
|
|
81
107
|
加载设置页。
|
|
82
108
|
|
|
109
|
+
### 3. 使用
|
|
110
|
+
|
|
111
|
+
**打开管理页**:重启后浏览器打开 DSH Web → **设置(Settings)→ MCP**。
|
|
112
|
+
|
|
113
|
+
**添加服务器**:
|
|
114
|
+
|
|
115
|
+
1. 点击「添加服务器」
|
|
116
|
+
2. 填写:服务器名称(`serverName`,决定工具前缀 `mcp__<serverName>__`)、传输方式
|
|
117
|
+
(`streamable-http` 填 URL / `stdio` 填命令)、请求头、工具调用超时等
|
|
118
|
+
3. 点「测试连接」确认连通性与工具列表,点「保存」
|
|
119
|
+
|
|
120
|
+
**OAuth 服务器**(`streamable-http` 走 MCP OAuth,如受 OAuth 保护的网关服务):
|
|
121
|
+
|
|
122
|
+
- 只需正常填写 URL 并测试连接;服务器返回 401 + OAuth 挑战时,插件**自动打开浏览器**完成授权
|
|
123
|
+
- 在浏览器中登录/同意后返回 DSH,测试结果自动刷新(「连接成功 + 工具数」)
|
|
124
|
+
- token 持久化在凭据文档(按 `serverName` 隔离),由 MCP SDK 自动刷新(24 小时内活跃自动续期);
|
|
125
|
+
授权一次后,挂载连接与后续测试连接复用同一份 token,无需重复授权
|
|
126
|
+
- 首次授权需浏览器交互,测试/连接等待时间放宽至 5 分钟;非 OAuth 服务器不受影响,连接失败即时返回
|
|
127
|
+
|
|
128
|
+
**日常管理**:
|
|
129
|
+
|
|
130
|
+
- **启用 / 禁用**:列表行按钮,禁用后该服务器所有工具即时注销,不再注入
|
|
131
|
+
- **刷新**:重新拉取服务器状态与工具列表(服务器重启后可同步新工具)
|
|
132
|
+
- **测试连接**:编辑页可随时测试
|
|
133
|
+
|
|
134
|
+
**工具控制(省 token 的关键)**:
|
|
135
|
+
|
|
136
|
+
- **注入模式**:页面顶部切换 `search`(按需检索,默认)或 `full`(全量注入)
|
|
137
|
+
- `search` 模式下,模型需要某 MCP 工具时调用 `mcp_tool_search` 检索并热注入当前对话
|
|
138
|
+
- **工具勾选**:点「展开工具」查看该服务器全部工具(默认全选),取消勾选 = 不注入该工具,
|
|
139
|
+
即时生效,无需保存
|
|
140
|
+
|
|
141
|
+
**验证效果**:
|
|
142
|
+
|
|
143
|
+
- 在任意 agent 会话中,可用工具应包含 `mcp__<服务器名>__<工具名>`
|
|
144
|
+
- `search` 模式下未检索到的工具不占系统提示词,节省 token 并提升 prompt cache 命中率
|
|
145
|
+
- 工具内容未变化时,`list_changed` 通知不会反复注销/重注册同名工具,工具列表保持稳定
|
|
146
|
+
|
|
83
147
|
## 版本注意
|
|
84
148
|
|
|
85
149
|
- host 半部 `lib/index.js` 是 mcp-manager 的**构建产物**(spec/types 已内联),改动请直接编辑
|
|
@@ -87,3 +151,5 @@ dsh plugin --profile web add link:<本仓库绝对路径>
|
|
|
87
151
|
- 浏览器半部改 `src/client/*` 后重新 `node scripts/build.mjs`;host 半部改动无需重装
|
|
88
152
|
(link 安装直接生效)。
|
|
89
153
|
- 配置变更(bundles 增删、新插件行)需重启 `dsh web` 才进入 client roster。
|
|
154
|
+
|
|
155
|
+
[](https://dshfind.com/zh/plugins/ArvinQi/dsh-mcp?ref=badge)
|
package/lib/client.js
CHANGED
|
@@ -14905,7 +14905,9 @@ function emptyDraft() {
|
|
|
14905
14905
|
headersText: "",
|
|
14906
14906
|
env: [],
|
|
14907
14907
|
toolCallTimeoutMs: String(DEFAULT_TOOL_CALL_TIMEOUT_MS),
|
|
14908
|
-
|
|
14908
|
+
// Reject the mount by default when the startup connection fails, so a
|
|
14909
|
+
// broken server never registers stale tools.
|
|
14910
|
+
failOnStartupError: true
|
|
14909
14911
|
};
|
|
14910
14912
|
}
|
|
14911
14913
|
function draftFromServer(server) {
|
|
@@ -14969,6 +14971,20 @@ function draftToSubmission(draft) {
|
|
|
14969
14971
|
}))
|
|
14970
14972
|
};
|
|
14971
14973
|
}
|
|
14974
|
+
function failureLocaleKey(code) {
|
|
14975
|
+
switch (code) {
|
|
14976
|
+
case "MCP_SERVER_NAME_CONFLICT":
|
|
14977
|
+
return "serverNameConflict";
|
|
14978
|
+
case "MCP_SERVER_NOT_FOUND":
|
|
14979
|
+
return "notFound";
|
|
14980
|
+
case "MCP_INVALID_SPEC":
|
|
14981
|
+
return "invalidSpec";
|
|
14982
|
+
case "MCP_MOUNT_FAILED":
|
|
14983
|
+
return "mountFailed";
|
|
14984
|
+
default:
|
|
14985
|
+
return "failureTitle";
|
|
14986
|
+
}
|
|
14987
|
+
}
|
|
14972
14988
|
function createMcpManagerStore() {
|
|
14973
14989
|
return (0, import_client.defineStore)({
|
|
14974
14990
|
init: () => ({
|
|
@@ -15024,7 +15040,7 @@ function createMcpManagerStore() {
|
|
|
15024
15040
|
const style = document.createElement("style");
|
|
15025
15041
|
style.setAttribute("data-plugin", "dsh-mcp");
|
|
15026
15042
|
style.setAttribute("data-file", "ServerForm.module.css");
|
|
15027
|
-
style.textContent = ".form {\n display: flex;\n flex-direction: column;\n gap: 14px;\n width: 100%;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.titleRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.title {\n margin: 0;\n font-size: 15px;\n line-height: 22px;\n font-weight: 600;\n}\n\n.titleActions {\n display: flex;\n gap: 8px;\n}\n\nbutton {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n padding: 5px 12px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n cursor: pointer;\n}\n\nbutton:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\nbutton:focus-visible {\n outline: 2px solid var(--dsw-alias-
|
|
15043
|
+
style.textContent = ".form {\n display: flex;\n flex-direction: column;\n gap: 14px;\n width: 100%;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.titleRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.title {\n margin: 0;\n font-size: 15px;\n line-height: 22px;\n font-weight: 600;\n}\n\n.titleActions {\n display: flex;\n gap: 8px;\n}\n\nbutton {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n padding: 5px 12px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n cursor: pointer;\n}\n\nbutton:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\nbutton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\nbutton:disabled {\n cursor: not-allowed;\n opacity: 0.55;\n}\n\n.primary {\n border-color: var(--dsw-alias-button-primary-fill);\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n.primary:hover {\n background: var(--dsw-alias-button-primary-hover);\n}\n\n.danger {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.error {\n margin: 0;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.field {\n display: flex;\n flex-direction: column;\n gap: 5px;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.field input,\n.field select,\n.field textarea {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n padding: 6px 10px;\n outline: none;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n resize: vertical;\n}\n\n.field input::placeholder,\n.field textarea::placeholder {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.field input:focus-visible,\n.field select:focus-visible,\n.field textarea:focus-visible {\n border-color: var(--dsw-alias-brand-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent);\n}\n\n.envBlock {\n display: flex;\n flex-direction: column;\n gap: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n padding: 10px 12px 12px;\n margin: 0;\n}\n\n.envBlock legend {\n padding: 0 4px;\n font-size: 13px;\n line-height: 20px;\n font-weight: 600;\n}\n\n.hint,\n.muted {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.envRow {\n display: grid;\n grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.6fr) auto auto;\n align-items: center;\n gap: 8px;\n}\n\n.envRow input {\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n padding: 5px 8px;\n outline: none;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n}\n\n.secretToggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.checkRow {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n.testResult {\n margin: 0;\n font-size: 13px;\n line-height: 20px;\n}\n\n.testOk {\n color: var(--dsw-alias-state-success-primary);\n}\n\n.testFail {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n";
|
|
15028
15044
|
document.head.appendChild(style);
|
|
15029
15045
|
}
|
|
15030
15046
|
}
|
|
@@ -15034,7 +15050,7 @@ var ServerForm_default = { "form": "form", "titleRow": "titleRow", "title": "tit
|
|
|
15034
15050
|
// src/client/ServerForm.tsx
|
|
15035
15051
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
15036
15052
|
function McpServerForm(props) {
|
|
15037
|
-
const { draft, busy, testRunning, test, t, actions, injected } = props;
|
|
15053
|
+
const { draft, busy, testRunning, test, t, actions, injected, onSaved } = props;
|
|
15038
15054
|
const [saveError, setSaveError] = (0, import_react.useState)(null);
|
|
15039
15055
|
const update = (patch) => {
|
|
15040
15056
|
setSaveError(null);
|
|
@@ -15066,11 +15082,12 @@ function McpServerForm(props) {
|
|
|
15066
15082
|
try {
|
|
15067
15083
|
const failure = await injected.save(draft);
|
|
15068
15084
|
if (failure !== null) {
|
|
15069
|
-
setSaveError(failure.
|
|
15085
|
+
setSaveError(t(failureLocaleKey(failure.code)));
|
|
15070
15086
|
return;
|
|
15071
15087
|
}
|
|
15072
15088
|
await refresh();
|
|
15073
15089
|
actions.cancelEdit();
|
|
15090
|
+
onSaved?.();
|
|
15074
15091
|
} catch {
|
|
15075
15092
|
setSaveError(t("failureTitle"));
|
|
15076
15093
|
} finally {
|
|
@@ -15285,6 +15302,7 @@ function McpServerForm(props) {
|
|
|
15285
15302
|
t("enabled")
|
|
15286
15303
|
] }),
|
|
15287
15304
|
test !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: `${ServerForm_default.testResult} ${probe !== void 0 && probe.ok ? ServerForm_default.testOk : ServerForm_default.testFail}`, role: "status", children: probe !== void 0 && probe.ok ? `${t("testOk")} (${probe.tools.length} ${t("toolCount")}, ${test.elapsedMs}ms)` : probe !== void 0 && !probe.ok ? `${t("testFail")}: ${probe.message}` : t("testUnknown") }) : null,
|
|
15305
|
+
testRunning && draft.transport === "streamable-http" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: ServerForm_default.muted, role: "status", children: t("testBrowserHint") }) : null,
|
|
15288
15306
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: ServerForm_default.actions, children: [
|
|
15289
15307
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: testRunning || busy !== null, onClick: () => void runTest(), children: testRunning ? t("testRunning") : t("test") }),
|
|
15290
15308
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: ServerForm_default.primary, disabled: busy !== null || testRunning, children: busy === "save" ? t("saving") : t("save") }),
|
|
@@ -15301,7 +15319,7 @@ function McpServerForm(props) {
|
|
|
15301
15319
|
const style = document.createElement("style");
|
|
15302
15320
|
style.setAttribute("data-plugin", "dsh-mcp");
|
|
15303
15321
|
style.setAttribute("data-file", "McpSettingsSection.module.css");
|
|
15304
|
-
style.textContent = ".section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n width: 100%;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n.status,\n.failure p {\n margin: 0;\n}\n\n.status,\n.failure {\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.failure {\n display: flex;\n align-items: center;\n gap: 10px;\n color: var(--dsw-alias-state-error-primary);\n}\n\nbutton {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n padding: 5px 12px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n cursor: pointer;\n}\n\nbutton:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\nbutton:focus-visible {\n outline: 2px solid var(--dsw-alias-
|
|
15322
|
+
style.textContent = ".section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n width: 100%;\n max-width: 760px;\n color: var(--dsw-alias-label-primary);\n}\n\n.header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n.status,\n.failure p {\n margin: 0;\n}\n\n.status,\n.failure {\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.failure {\n display: flex;\n align-items: center;\n gap: 10px;\n color: var(--dsw-alias-state-error-primary);\n}\n\nbutton {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n padding: 5px 12px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n cursor: pointer;\n}\n\nbutton:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\nbutton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\nbutton:disabled {\n cursor: not-allowed;\n opacity: 0.55;\n}\n\n.primary {\n border-color: var(--dsw-alias-button-primary-fill);\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n.primary:hover {\n background: var(--dsw-alias-button-primary-hover);\n}\n\n.list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n padding: 10px 14px;\n background: var(--dsw-alias-bg-layer-3);\n}\n\n.rowMain {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n\n.rowTitle {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.serverName {\n overflow: hidden;\n font-size: 14px;\n line-height: 20px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.badge {\n display: inline-flex;\n align-items: center;\n min-height: 18px;\n border-radius: 5px;\n padding: 1px 6px;\n font-size: 11px;\n line-height: 16px;\n white-space: nowrap;\n}\n\n.badge.mounting {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n}\n\n.badge.live {\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent);\n color: var(--dsw-alias-state-success-primary);\n}\n\n.badge.failed {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);\n color: var(--dsw-alias-state-error-primary);\n}\n\n.badge.stopped {\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-tertiary);\n}\n\n.muted {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.rowMeta {\n display: flex;\n gap: 14px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n font-variant-numeric: tabular-nums;\n}\n\n.rowActions {\n flex: none;\n}\n\n.card {\n display: flex;\n flex-direction: column;\n gap: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-3);\n}\n\n.card > .row {\n border: 0;\n border-radius: 0;\n background: transparent;\n}\n\n.modeRow {\n display: flex;\n align-items: center;\n gap: 18px;\n flex-wrap: wrap;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n padding: 8px 14px;\n background: var(--dsw-alias-bg-layer-3);\n font-size: 13px;\n line-height: 20px;\n}\n\n.modeLabel {\n font-weight: 600;\n}\n\n.modeOption {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n}\n\n.modeHint {\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.toolPanel {\n border-top: 1px solid var(--dsw-alias-border-l2);\n padding: 10px 14px;\n}\n\n.toolGrid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));\n gap: 4px 14px;\n max-height: 320px;\n overflow-y: auto;\n}\n\n.toolRow {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n padding: 2px 0;\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n}\n\n.toolName {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n";
|
|
15305
15323
|
document.head.appendChild(style);
|
|
15306
15324
|
}
|
|
15307
15325
|
}
|
|
@@ -15517,7 +15535,12 @@ function McpSettingsSection(props) {
|
|
|
15517
15535
|
test: state.test,
|
|
15518
15536
|
t,
|
|
15519
15537
|
actions,
|
|
15520
|
-
injected: { save, remove, test, list }
|
|
15538
|
+
injected: { save, remove, test, list },
|
|
15539
|
+
onSaved: () => {
|
|
15540
|
+
const serverName = state.draft.serverName;
|
|
15541
|
+
timersRef.current.push(window.setTimeout(() => refreshServer(serverName), 2e3));
|
|
15542
|
+
timersRef.current.push(window.setTimeout(() => refreshServer(serverName), 6e3));
|
|
15543
|
+
}
|
|
15521
15544
|
}
|
|
15522
15545
|
);
|
|
15523
15546
|
}
|
|
@@ -15561,7 +15584,7 @@ function McpSettingsSection(props) {
|
|
|
15561
15584
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: McpSettingsSection_default.rowTitle, children: [
|
|
15562
15585
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: McpSettingsSection_default.serverName, children: server.serverName }),
|
|
15563
15586
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `${McpSettingsSection_default.badge} ${McpSettingsSection_default[server.status.phase]}`, children: t(phaseKey(server.status.phase)) }),
|
|
15564
|
-
!server.enabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: McpSettingsSection_default.muted, children: t("statusStopped") }) : null
|
|
15587
|
+
!server.enabled && server.status.phase !== "stopped" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: McpSettingsSection_default.muted, children: t("statusStopped") }) : null
|
|
15565
15588
|
] }),
|
|
15566
15589
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: McpSettingsSection_default.rowMeta, children: [
|
|
15567
15590
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: server.transport }),
|
|
@@ -15643,6 +15666,7 @@ var zh = {
|
|
|
15643
15666
|
edit: "\u7F16\u8F91",
|
|
15644
15667
|
test: "\u6D4B\u8BD5\u8FDE\u63A5",
|
|
15645
15668
|
testRunning: "\u6B63\u5728\u6D4B\u8BD5\u2026",
|
|
15669
|
+
testBrowserHint: "\u6B63\u5728\u6D4B\u8BD5\u2026\u82E5\u5F39\u51FA\u6D4F\u89C8\u5668\u6388\u6743\u9875\uFF0C\u8BF7\u5B8C\u6210\u6388\u6743\u540E\u8FD4\u56DE\uFF0C\u7ED3\u679C\u4F1A\u81EA\u52A8\u5237\u65B0\u3002",
|
|
15646
15670
|
testOk: "\u8FDE\u63A5\u6210\u529F",
|
|
15647
15671
|
testFail: "\u8FDE\u63A5\u5931\u8D25",
|
|
15648
15672
|
testUnknown: "\u672A\u77E5\u7ED3\u679C",
|
|
@@ -15715,6 +15739,7 @@ var en = {
|
|
|
15715
15739
|
edit: "Edit",
|
|
15716
15740
|
test: "Test connection",
|
|
15717
15741
|
testRunning: "Testing\u2026",
|
|
15742
|
+
testBrowserHint: "Testing\u2026 if a browser authorization page opens, complete it and return; the result refreshes automatically.",
|
|
15718
15743
|
testOk: "Connection succeeded",
|
|
15719
15744
|
testFail: "Connection failed",
|
|
15720
15745
|
testUnknown: "Unknown result",
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
|
-
import * as mcpClient from "
|
|
4
|
+
import * as mcpClient from "./mcp-client.js";
|
|
5
|
+
import { createOAuthProvider } from "./oauth.js";
|
|
5
6
|
import { probeConnection } from "./probe.js";
|
|
6
7
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
7
8
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -66,19 +67,7 @@ const mcpServersDomainSpec = defineDomain({
|
|
|
66
67
|
message: "streamable-http requires an absolute http(s) URL"
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
row.headers.forEach((header, index) => {
|
|
71
|
-
if (headerNames.has(header.name)) ctx.addIssue({
|
|
72
|
-
code: "custom",
|
|
73
|
-
path: [
|
|
74
|
-
"headers",
|
|
75
|
-
index,
|
|
76
|
-
"name"
|
|
77
|
-
],
|
|
78
|
-
message: `duplicate header '${header.name}'`
|
|
79
|
-
});
|
|
80
|
-
headerNames.add(header.name);
|
|
81
|
-
});
|
|
70
|
+
// Same-name custom headers are allowed (matches other MCP clients).
|
|
82
71
|
const envNames = /* @__PURE__ */ new Set();
|
|
83
72
|
row.env.forEach((entry, index) => {
|
|
84
73
|
if (envNames.has(entry.name)) ctx.addIssue({
|
|
@@ -124,11 +113,10 @@ function validateServerInput(server, env) {
|
|
|
124
113
|
throw new McpServerValidationError("streamable-http requires an absolute http(s) URL");
|
|
125
114
|
}
|
|
126
115
|
if (!Number.isInteger(server.toolCallTimeoutMs) || server.toolCallTimeoutMs <= 0) throw new McpServerValidationError("toolCallTimeoutMs must be a positive integer");
|
|
127
|
-
|
|
116
|
+
// Custom headers pass through unvalidated (same-name headers are allowed,
|
|
117
|
+
// matching other MCP clients); only a blank name is rejected.
|
|
128
118
|
for (const header of server.headers) {
|
|
129
119
|
if (header.name.trim().length === 0) throw new McpServerValidationError("header names must not be blank");
|
|
130
|
-
if (headerNames.has(header.name)) throw new McpServerValidationError(`duplicate header '${header.name}'`);
|
|
131
|
-
headerNames.add(header.name);
|
|
132
120
|
}
|
|
133
121
|
const envNames = /* @__PURE__ */ new Set();
|
|
134
122
|
for (const entry of env) {
|
|
@@ -477,7 +465,13 @@ let McpManagerService = (() => {
|
|
|
477
465
|
let handle;
|
|
478
466
|
try {
|
|
479
467
|
const env = await this.resolveEnv(row);
|
|
480
|
-
|
|
468
|
+
// Every server gets an OAuth provider: servers that require
|
|
469
|
+
// OAuth (authorization-code flow) trigger browser auth on
|
|
470
|
+
// connect; servers that do not are unaffected. Tokens are
|
|
471
|
+
// keyed by serverName so a test-connection authorization is
|
|
472
|
+
// reused by the mount.
|
|
473
|
+
const authProvider = createOAuthProvider(row.serverName, this.ctx);
|
|
474
|
+
handle = this.ctx.plugin(MCP_CLIENT_PLUGIN, { ...this.toClientConfig(row, env), authProvider });
|
|
481
475
|
} catch (error) {
|
|
482
476
|
entry.phase = "failed";
|
|
483
477
|
entry.error = errorText(error);
|
|
@@ -691,7 +685,13 @@ let McpManagerService = (() => {
|
|
|
691
685
|
}
|
|
692
686
|
} else if (entry.value !== void 0) env[entry.name] = entry.value;
|
|
693
687
|
const startedAt = Date.now();
|
|
694
|
-
|
|
688
|
+
// Test connection also supports OAuth: an OAuth-protected server
|
|
689
|
+
// opens the browser for authorization and the probe continues after
|
|
690
|
+
// tokens are stored (keyed by serverName, matching the mount path).
|
|
691
|
+
const probe = await probeConnection(this.toClientConfig(request.server, env), {
|
|
692
|
+
timeoutMs: this.probeTimeoutMs,
|
|
693
|
+
authProvider: createOAuthProvider(request.server.serverName, this.ctx)
|
|
694
|
+
});
|
|
695
695
|
const elapsedMs = Date.now() - startedAt;
|
|
696
696
|
return {
|
|
697
697
|
ok: true,
|