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 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 server management plugin for DeepSeek Harness (standalone)
1
+ # dsh-mcp — MCP management UI + tool search: stable tool list, cache hits, no context bloat
2
2
 
3
- ![Settings preview](static/snapshot.webp)
3
+ [![dshfind](https://dshfind.com/api/badge/ArvinQi/dsh-mcp?lang=en)](https://dshfind.com/en/plugins/ArvinQi/dsh-mcp?ref=badge)
4
+
5
+ ![Settings preview](static/snapshot.en.webp)
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
- ### Option 1: npm (after publishing)
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
- ### Option 2: GitHub git source
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
- ### Option 3: local development (link)
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
+ [![dshfind](https://dshfind.com/api/card/ArvinQi/dsh-mcp?lang=en)](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
+ [![dshfind](https://dshfind.com/api/badge/ArvinQi/dsh-mcp?lang=zh)](https://dshfind.com/zh/plugins/ArvinQi/dsh-mcp?ref=badge)
6
+
5
7
  ![设置页预览](static/snapshot.webp)
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
- ### 方式一:npm(发布到 npm 后)
72
+ **方式一:npm(发布到 npm 后)**
47
73
 
48
74
  ```sh
49
75
  dsh plugin --profile web add dsh-mcp
50
76
  ```
51
77
 
52
- ### 方式二:GitHub git
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
- ### 方式三:本地开发(link
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
+ [![dshfind](https://dshfind.com/api/card/ArvinQi/dsh-mcp?lang=zh)](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
- failOnStartupError: false
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-state-business-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-state-business-primary);\n background: var(--dsw-alias-state-business-primary);\n color: var(--dsw-alias-label-on-accent);\n}\n\n.primary:hover {\n background: var(--dsw-alias-state-business-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-state-business-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-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";
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.message);
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-state-business-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-state-business-primary);\n background: var(--dsw-alias-state-business-primary);\n color: var(--dsw-alias-label-on-accent);\n}\n\n.primary:hover {\n background: var(--dsw-alias-state-business-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-state-business-primary) 12%, transparent);\n color: var(--dsw-alias-state-business-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";
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 "@deepseek-ai/dsh-mcp-client";
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
- const headerNames = /* @__PURE__ */ new Set();
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
- const headerNames = /* @__PURE__ */ new Set();
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
- handle = this.ctx.plugin(MCP_CLIENT_PLUGIN, this.toClientConfig(row, env));
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
- const probe = await probeConnection(this.toClientConfig(request.server, env), { timeoutMs: this.probeTimeoutMs });
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,