dsh-mcp 1.9.0 → 1.11.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,7 +7,47 @@ 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
- ## [Unreleased]
10
+ ## [1.11.0] - 2026-09-13
11
+
12
+ ### Added
13
+
14
+ - **Declared MCP servers from `cordis.patch.yml` are read and shown**: the Settings page now also lists the `@deepseek-ai/dsh-mcp-client` rows the composition mounts natively — the profile layer (`$DSH_HOME/profiles/<profile>/cordis.patch.yml`) and the machine-wide layer (`$DSH_HOME/cordis.patch.yml`, which outranks the profile layer) — badged as a "cordis declaration" with the file it came from, read-only. Parsing reuses the composition's own YAML dialect (`!!js` scalars render as their source text); a missing file is not an error and a malformed one degrades to a page diagnostic instead of breaking the manager
15
+ - **Declarations win, so no duplicate mount**: when a `serverName` is already declared by a patch layer, the manager no longer mounts the same-named stored row (two mounts under one name collide in the tool registry and roll back that server's whole generation); the page explains the conflict, and mount ownership follows live patch reloads (`web`/`desktop` reload live)
16
+ - **OAuth boundary hint**: a declaration cannot carry an OAuth provider (the native client takes static config only). For a streamable-http declaration with no static `Authorization` header the page explains that browser authorization requires adding the server in the plugin and disabling the declared row
17
+ - **Take over / give back (declarations gain OAuth)**: the Settings page offers "Take over" on a declared server — the plugin writes an id-targeted `disabled: true` into its own **managed block** in `cordis.patch.yml` (a `.dsh-mcp.bak` backup is taken before the first write; atomic, idempotent, reversible, and confined to that block), so the declaration releases the `serverName` and the plugin mounts a managed row instead — which enables **OAuth authorization, managed credentials, `${VAR}` header substitution and connection tests**. A hard mount failure (e.g. `failOnStartupError: true` with a failing connection) rolls back automatically: the managed block is removed, the row returns to mirror state, and the failure is reported; "Give back" removes the block, hands the mount back to the declaration and deletes the managed row. A declaration without an explicit `id`, or one containing a `!!js` expression, cannot be taken over (`MCP_ADOPT_NO_ID` / `MCP_ADOPT_JS_EXPR`)
18
+ - **Safe degradation when the hot reload does not commit (`pendingTakeover`)**: if the native tools have not unregistered within 5 seconds of writing the disable block (that profile's patch hot reload did not commit — a sibling entry failing its re-apply rolls the whole generation back), the plugin **registers the takeover but does not mount**: the managed row is stored and marked `pendingTakeover`, and the next dsh start mounts it; "Give back" likewise reports that a restart is needed when the reload does not commit. Independently, a mount is always skipped when same-named `mcp__<server>__` tools already exist without a local mount — the plugin never fights a still-mounted declaration for one name (that collides in the registry and rolls one side back)
19
+ - **Declaration reads now report the EFFECTIVE state**: layers are replayed through `applyEntryPatches`, so the disable block this plugin writes and any cross-layer (profile → machine-wide) id override are reflected; a taken-over declaration reads as disabled while attribution stays with the layer that declared it
20
+
21
+ ### Notes
22
+
23
+ - **Declarations are imported into storages as mirror rows**: at startup (and on every list refresh) each declaration is imported into the storage domain with id `cordis:<rowId>` plus provenance (`origin: cordis`, `declaredIn`, `declaredRowId`). Rules: **import only** — a row this manager created (`origin: plugin`) is never overwritten; a live mirror is refreshed from the patch layer; a mirror whose declaration disappeared is **removed automatically** (a mirror is only a copy of a declaration, so an ownerless one must not pile up in the list; rows an earlier build marked `stale` are cleaned up too); a declaration containing a `!!js` expression is **skipped** (its value cannot be resolved outside the Loader) and shown from the file with the reason. Mirrors are **never mounted** (the composition owns the mount), so they cannot fight the native row for the same tools
24
+ - **Tool search already covers declared servers, with no change needed**: the injection layer handles the whole tool set by `mcp__` prefix, so declared tools join search/hot injection and per-tool switches, and appear in the `mcp-tool-control` server list
25
+
26
+ ### Changed
27
+
28
+ - **`allowBrowserOnMount` now defaults to `true`**: mounting an OAuth server that needs authorization opens the browser to complete it (previously `false`, which made such mounts fail silently and register no tools); set it to `false` under the dsh-mcp entry to keep mounts browser-free
29
+
30
+ ### Fixed
31
+
32
+ - **A save dropped the declaration provenance**: `upsert`/`upsertJson` rebuilt the row without `declaredIn`/`declaredRowId`, so an adopted server lost its "came from a declaration" marker after one disable/enable or edit — the "Give back" button disappeared and give-back failed with "no declaration source". Saves now keep the provenance, and startup **repairs** a lost source from the ids in this manager's own managed block (only ids this plugin disabled)
33
+ - **Disable → enable could no longer remount (stuck showing disabled)**: the "same-named tools already exist, skip mounting" guard mistook tools lingering from this manager's OWN previous mount for a foreign owner. The manager now remembers the ids it has mounted and waits for its own tools to unregister before mounting again; composition-owned declarations are still skipped
34
+ - **Declared-server rows overflowed**: the source file path now takes its own wrapping line and the badge/action areas wrap, so narrow widths no longer push the card apart
35
+ - **The JSON editor materialized declarations into managed rows**: its document was seeded from the WHOLE list (including `source: cordis`), so saving wrote a declaration back as an ordinary server — producing a "managed row + declaration with the same name" conflict (shown as disabled, with a toggle that did nothing). The editor now serializes **only plugin-owned rows**, and the host adds a backstop: a JSON document never rewrites a declaration mirror (`origin: cordis`) and never deletes a mirror or a taken-over row (`declaredIn`); skipped entries are reported as "skipped declared N" in the editor notice
36
+ - **A declaration-served row no longer reads as "disabled"**: the composition currently serves its tools, yet the page showed the mount phase (`stopped` → "Disabled") with an enable/disable button that could not take effect. It now shows a "Served by declaration" / "Takeover pending restart" badge and hides the toggle
37
+ - **Giving a declaration back left it "connecting" with no tools (`needsPlugin`)**: declaration mounts run on the composition's native client, which has **no OAuth** (authorization-code + PKCE and token storage are plugin features) and **no `${VAR}` / bare-name placeholder substitution** (also a plugin feature), so a declaration that needs plugin authentication can never connect once released. The plugin now detects them — streamable-http without an `Authorization` header (an OAuth candidate), or a header value that matches a `global_env` placeholder — reports them as **failed with the reason** instead of forever-connecting, renders `status.error` on the page, uses a stronger confirmation for "Give back", and returns an explanatory warning. Taking the server over again restores its tools
38
+ - **The release pipeline was blocked by a test import error**: this package has zero runtime dependencies and resolves `@deepseek-ai/*` and `js-yaml` from the DSH installation, which public CI does not have — the three test files added in 1.11.0 import them at load time, so `npm test` failed and the publish job was skipped. `npm test` now adapts to the environment (`scripts/test.mjs`): with no DSH module closure it skips those files and says so, while a local checkout runs them all
39
+
40
+ ## [1.10.0] - 2026-09-02
41
+
42
+ ### Added
43
+
44
+ - **Actionable diagnosis when the host half is missing**: when the server-list load fails because of an HTTP 404 on `/api/mcpManager/*` (plugin host half not registered, or client/host version mismatch), the page now shows a troubleshooting hint next to the raw error (verify the `cordis.patch.yml` row → restart `dsh web` → hard-refresh → upgrade both sides); the README troubleshooting section is updated as well
45
+
46
+ ### Fixed
47
+
48
+ - **Host-side copy internationalization**: the settings UI was fully bilingual but the host half (`lib/index.js`/`lib/oauth.js`/`lib/mcp-client.js`) kept model-visible copy and OAuth errors hardcoded in Chinese. The `mcp_tool_search` description/parameter docs, search-result text, the injected `mcp-tool-control` system prompt, and the OAuth authorization/callback page copy now follow the DSH `locale.preference` from the settings document (new `lib/host-locales.js` table; falls back to Chinese — the previous behavior — when the preference cannot be read)
49
+ - **OAuth decisions no longer key on message text**: the tool-call OAuth preflight used `error.message.includes("授权")` to decide whether to re-throw the link-carrying error — translating the message would silently break OAuth error propagation. It now keys on a stable error code `MCP_OAUTH_REQUIRED` (`error.code`), decoupling control flow from display text
50
+ - **Static-credential servers are no longer mistaken for OAuth**: only streamable-http servers configured for authorization-code + PKCE and WITHOUT a static `Authorization` request header get an OAuth provider; a static-token server facing a 401 now reports the authentication failure instead of starting a browser authorization flow
11
51
 
12
52
  ## [1.9.0] - 2026-08-28
13
53
 
package/CHANGELOG.md CHANGED
@@ -7,7 +7,47 @@
7
7
  格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
8
8
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
9
9
 
10
- ## [Unreleased]
10
+ ## [1.11.0] - 2026-09-13
11
+
12
+ ### 新增
13
+
14
+ - **读取 `cordis.patch.yml` 的声明式 MCP 服务器**:设置页现在同时展示由组合原生挂载的 `@deepseek-ai/dsh-mcp-client` 行——profile 级 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 与机器级 `$DSH_HOME/cordis.patch.yml`(后者按组合层级覆盖前者),标记为「cordis 声明」并只读展示其来源文件。解析复用组合自身的 YAML 方言(`!!js` 表达式按源码文本展示),文件缺失不算异常、格式错误降级为页面诊断,不影响 manager 运行
15
+ - **声明优先,避免同名双挂载**:若某 `serverName` 已在 patch 层声明并由组合挂载,本插件不再挂载存储 domain 中的同名行(同名两处挂载会撞工具注册表并让整代工具回滚),设置页对该行给出冲突说明;patch 层热更新(`web`/`desktop` 为 live reload)后挂载归属自动跟随
16
+ - **OAuth 边界提示**:声明行无法携带 OAuth provider(原生客户端只接受静态配置)。对没有静态 `Authorization` 头的 streamable-http 声明行,设置页提示:如需浏览器授权,请在插件中新增同名服务器并停用声明行
17
+ - **接管 / 释放(声明式服务器获得 OAuth 能力)**:设置页对声明式服务器提供「接管」——插件在 `cordis.patch.yml` 的**受管块**内写入 id-targeted `disabled: true`(首次写入前生成 `.dsh-mcp.bak` 备份;原子替换、幂等、可逆、只动自己那一块),声明行因此让出该 `serverName`,随后由插件挂载同名行,从而启用 **OAuth 授权、凭据托管、`${VAR}` 头替换、测试连接**等插件能力。挂载**硬失败**(如 `failOnStartupError: true` 且连接失败)会自动回滚:移除受管块、行退回镜像状态并返回失败原因;「释放」则移除受管块、把挂载交还声明行并删除插件行。缺少显式 `id` 或含 `!!js` 的声明不可接管,返回明确错误码(`MCP_ADOPT_NO_ID` / `MCP_ADOPT_JS_EXPR`)
18
+ - **热重载未提交时安全降级(`pendingTakeover`)**:写入停用块后若原生工具在 5 秒内没有注销(该 profile 的 patch 热重载未提交——某个兄弟条目重建失败会让整代回滚),插件**登记接管但暂不挂载**:管理行落库并标记 `pendingTakeover`,重启 dsh 后由插件挂载;「释放」同样在热重载未提交时提示需要重启。此外,挂载前若发现同名 `mcp__<server>__` 工具已存在且不是本插件挂载的,一律跳过挂载——任何情况下都不会与仍在挂载的声明行争抢同名工具(那会撞注册表并让一方整代回滚)
19
+ - **声明读取升级为「生效态」**:按层回放 id-targeted patch(`applyEntryPatches`),因此本插件写入的停用块、以及跨层(profile → 机器级)的同 id 覆盖都会被正确反映;被接管的声明显示为已停用,归属仍记在声明它那一层
20
+
21
+ ### 说明
22
+
23
+ - **声明式服务器导入 storages(镜像行)**:启动(以及每次刷新列表)时把声明行导入存储 domain,id 为 `cordis:<rowId>`,并带来源元数据(`origin: cordis`、`declaredIn`、`declaredRowId`)。规则:**只导入不覆盖**——本插件自己创建的行(`origin: plugin`)永不改写;同源镜像按 patch 内容刷新;声明从文件消失的镜像**自动删除**(镜像只是声明的副本,无主后不应堆积在列表里;早期版本留下的 `stale` 行也会被一并清理);含 `!!js` 表达式的声明**跳过导入**(值无法在 Loader 之外还原),仅在页面按文件原样展示并标注原因。镜像行**永不挂载**(挂载归组合),因此不会与原生行争抢同名工具
24
+ - **tool search 无需改动即覆盖声明式服务器**:注入层按 `mcp__` 前缀处理整个工具集,声明式服务器的工具自动进入检索/热注入与单工具开关,并计入 `mcp-tool-control` 的服务器列表
25
+
26
+ ### 变更
27
+
28
+ - **`allowBrowserOnMount` 默认值改为 `true`**:启动挂载 OAuth 服务器时若需要授权会直接打开浏览器完成授权(此前默认 `false`,表现为挂载静默失败、工具不注册);仍可在 dsh-mcp 条目下显式设为 `false` 关闭该行为
29
+
30
+ ### 修复
31
+
32
+ - **保存会丢掉声明来源**:`upsert`/`upsertJson` 重建行时没有保留 `declaredIn`/`declaredRowId`,导致已接管的服务器在禁用/启用或编辑一次后失去"来自声明"标记——「释放」按钮消失、释放报"没有声明来源"。现在保存会保留来源;并且启动时会根据**本插件受管块里的 id** 自动补回丢失的来源(只针对本插件自己停用过的 id)
33
+ - **禁用→启用后无法重新挂载(一直显示未启用)**:新增的"同名工具已存在则跳过挂载"护栏会把本插件**上一次挂载残留**的工具误判为外部占用。现在按 id 记录"本插件挂载过",并在挂载前等待自身残留工具注销;由组合挂载的声明式占用仍会被正确跳过
34
+ - **声明行信息排版溢出**:来源文件路径改为独占一行并允许任意位置换行,标题徽标与操作按钮区允许换行,窄宽度下不再挤出卡片
35
+ - **JSON 编辑器会把声明式条目物化成插件行**:编辑器文档此前由**完整列表**(含 `source: cordis`)生成,保存时把声明行当普通服务器写回,于是出现"同号插件行 + 声明行"的冲突态(页面显示未启用、启停无效)。现在编辑器**只序列化插件自有的行**;宿主侧同时加了兜底——JSON 文档不会改写声明镜像(`origin: cordis`),也不会删除镜像或已接管行(`declaredIn`),被跳过的条数在编辑器提示里显示为「跳过声明式 N」
36
+ - **由声明提供的行不再误显示为「未启用」**:这类行当前由组合提供工具,页面此前显示挂载阶段(`stopped` → "未启用")并给出无效的启停按钮。现在显示「由声明提供」/「待重启接管」徽标,并隐藏启停按钮
37
+ - **释放后"一直连接中、没有工具"(`needsPlugin`)**:声明式挂载由组合的原生客户端完成,而它**既没有 OAuth**(授权码+PKCE、token 托管都是插件的功能)**也不解析 `${VAR}`/裸变量名占位符**(也是插件的功能),因此"需要插件认证"的声明一旦释放就必然连不上。现在插件会识别这类声明——streamable-http 且没有 `Authorization` 头(OAuth 候选),或 header 值命中 `global_env` 的占位符——在页面上标记为**挂载失败并给出原因**(不再假装"连接中"),`status.error` 也会在页面显示;「释放」对这类行改用更强的确认文案,并在结果里返回说明性告警。要恢复工具只需重新「接管」
38
+ - **发布流水线被测试导入错误阻断**:本包零运行时依赖,`@deepseek-ai/*` 与 `js-yaml` 来自 DSH 安装;公共 CI 没有该安装,而 1.11.0 新增的 3 个测试文件在**导入期**就引用它们,导致 `npm test` 失败、发布 job 被跳过。现在 `npm test`(`scripts/test.mjs`)按环境自适应:缺少 DSH 模块闭包时自动跳过这些文件并说明,本机则跑全量
39
+
40
+ ## [1.10.0] - 2026-09-02
41
+
42
+ ### 新增
43
+
44
+ - **设置页对 host 缺失给出可操作诊断**:服务器列表加载失败若源于 `/api/mcpManager/*` HTTP 404(插件 host 半未注册或 client/host 版本不匹配),页面除原始错误外额外显示排查指引(核对 `cordis.patch.yml` 注册行 → 重启 `dsh web` → 硬刷新 → 同步升级),README 故障排查同步补充
45
+
46
+ ### 修复
47
+
48
+ - **host 端文案国际化**:设置页双语早已齐备,但 host 半部(`lib/index.js`/`lib/oauth.js`/`lib/mcp-client.js`)的模型可见文案与 OAuth 错误一直是硬编码中文——`mcp_tool_search` 描述与参数说明、检索结果文案、注入的 `mcp-tool-control` 系统提示、OAuth 授权/回调页文案现均按 DSH `locale.preference`(settings 文档)选择 zh/en(新增 `lib/host-locales.js` 文案表;读不到时回退中文,与旧行为一致)
49
+ - **OAuth 判定不再依赖文本匹配**:`mcp-client` 的工具授权预检曾用 `error.message.includes("授权")` 决定是否重抛带链接的错误,翻译消息会静默破坏 OAuth 错误传播——现统一改用稳定的错误码 `MCP_OAUTH_REQUIRED`(`error.code`),控制流与显示文案解耦
50
+ - **静态凭据服务器不再被误当作 OAuth**:只有配置了 OAuth 授权码 + PKCE 且**没有**静态 `Authorization` 请求头的 streamable-http 服务器才挂 OAuth provider;带静态 token 的服务器遇到 401 时直接呈现认证失败,不再触发浏览器授权流程
11
51
 
12
52
  ## [1.9.0] - 2026-08-28
13
53
 
package/README.en.md CHANGED
@@ -50,8 +50,37 @@ Migrated and merged from uncommitted MCP work in the `deepseek-harness` reposito
50
50
  mounts share the same token.
51
51
  - **Remote self-mount**: the client half mounts the `mcpManager` Remote namespace itself via `ctx.remote.$mount()` in `apply()`,
52
52
  so no in-box package modification is required.
53
+ - **Declared servers are read too** (host, `lib/cordis-servers.js`): `@deepseek-ai/dsh-mcp-client` rows
54
+ declared in the patch layers are listed read-only on the Settings page (see below).
53
55
  - Zero npm runtime dependencies (`@deepseek-ai/*` resolve from the DSH profiles module fallback).
54
56
 
57
+ ### Declared servers (`cordis.patch.yml`) and precedence
58
+
59
+ DSH can declare MCP servers directly in the composition: **one row per server**, `name: '@deepseek-ai/dsh-mcp-client'`,
60
+ in the profile layer `$DSH_HOME/profiles/<profile>/cordis.patch.yml` or the machine-wide layer
61
+ `$DSH_HOME/cordis.patch.yml` (machine-wide applies to every profile and overrides the profile layer per row id).
62
+
63
+ ```yaml
64
+ - insert:
65
+ - id: mcp-github
66
+ name: '@deepseek-ai/dsh-mcp-client'
67
+ config:
68
+ transport: stdio # or streamable-http
69
+ serverName: github
70
+ command: npx
71
+ args: ['-y', '@modelcontextprotocol/server-github']
72
+ ```
73
+
74
+ Since 1.11.0 those declarations appear in Settings → MCP, badged "cordis declaration", read-only, with the file they came from:
75
+
76
+ - **Declarations win**: when a `serverName` is already declared (and not `disabled`), the manager does not mount the same-named stored row — two mounts under one name collide in the tool registry and roll back that server's whole generation; the page explains the conflict.
77
+ - **Read-only**: declared servers cannot be enabled/disabled or edited here; edit `cordis.patch.yml` instead (`web`/`desktop` reload live; `headless`/`sdk` apply it on the next start).
78
+ - **Imported into storages as mirrors**: at startup and on every refresh the declarations are imported into the storage domain (id `cordis:<rowId>`, with `origin/declaredIn/declaredRowId`), **import only** — a row created in the plugin is never overwritten; a mirror whose declaration disappeared is **removed automatically** (a mirror is only a copy of a declaration, so an ownerless one must not pile up); a declaration containing a `!!js` expression is skipped and flagged with the reason (its value only resolves inside the Loader). Mirrors are never mounted — the composition owns the mount.
79
+ - **Take over / give back**: clicking "Take over" makes the plugin write an id-targeted `disabled: true` into its own **managed block** in `cordis.patch.yml` (a `.dsh-mcp.bak` backup is taken before the first write; atomic, idempotent, reversible, confined to that block). The declaration releases the `serverName` and the plugin mounts its own row instead — enabling **OAuth authorization, managed credentials, `${VAR}` header substitution and connection tests**. A hard mount failure rolls back automatically (the block is removed and the row returns to mirror state) and reports why; "Give back" hands the mount back to the declaration and deletes the managed row. A declaration without an explicit `id`, or one containing a `!!js` expression, cannot be taken over.
80
+ - **OAuth / placeholder limit (`needsPlugin`)**: a declaration can carry neither an OAuth provider nor `${VAR}` / bare-name placeholder substitution — only the plugin does those. Such a declaration can therefore only work while the plugin owns the mount: the page reports it as **failed with the reason** (not "connecting"), "Give back" makes its tools disappear (the confirmation and the result warning both say so), and taking it over again restores them.
81
+ - **Safe degradation when the hot reload does not commit**: if the native tools have not unregistered within 5 seconds of writing the disable block (that profile's patch hot reload did not commit — a sibling entry failing its re-apply rolls the whole generation back), the plugin **registers the takeover without mounting** (the managed row is marked `pendingTakeover`); the next dsh start mounts it, and "Give back" reports the same need for a restart. A mount is also always skipped when same-named `mcp__<server>__` tools already exist without a local mount, so the plugin never fights a still-mounted declaration for one name.
82
+ - **Tool search covers them**: search/hot injection and per-tool switches work by `mcp__` prefix across the whole tool set, so declared tools need no extra configuration.
83
+
55
84
  ## Structure
56
85
 
57
86
  ```
@@ -59,6 +88,8 @@ dsh-mcp/
59
88
  ├── package.json name=dsh-mcp; dsh.client declaration; zero npm dependencies
60
89
  ├── lib/
61
90
  │ ├── index.js host half (McpManagerService, built from mcp-manager)
91
+ │ ├── cordis-servers.js reads natively declared MCP servers from the patch layers (1.11.0)
92
+ │ ├── patch-writer.js managed-block writer: disables a declaration on takeover (backup/atomic/idempotent)
62
93
  │ ├── mcp-client.js vendored MCP client (from @deepseek-ai/dsh-mcp-client, with tool-list stability extension)
63
94
  │ ├── oauth.js MCP OAuth client provider (authorization-code + PKCE, loopback callback, token persistence)
64
95
  │ ├── probe.js vendored connection probe (from mcp-client/src/probe.ts)
@@ -81,6 +112,19 @@ node scripts/build.mjs
81
112
  - CSS Modules are handled by an esbuild onLoad plugin: styles are injected into a
82
113
  `<style data-plugin="dsh-mcp" data-file="…">` tag, and the module default-exports an identity class-name map.
83
114
 
115
+ ## Test
116
+
117
+ ```sh
118
+ npm test
119
+ ```
120
+
121
+ - `npm test` adapts to the environment: the files that import the DSH module closure
122
+ (`@deepseek-ai/*`, `js-yaml`) — `cordis-servers`, `patch-writer`, `takeover` — are **skipped when
123
+ no local DSH installation resolves** (as in public CI), while a local checkout with the profile's
124
+ `node_modules` in reach runs the whole suite; the run says which files it skipped.
125
+ - When you add a test file that needs the DSH closure, add it to `NEEDS_DSH_CLOSURE` in
126
+ `scripts/test.mjs`.
127
+
84
128
  ## Install & Usage
85
129
 
86
130
  ### 1. Install
@@ -160,7 +204,12 @@ Check in order:
160
204
  - Check the `dsh web` process log for `mcp-manager` initialization errors;
161
205
  - After upgrading the plugin, restart and **hard-refresh** so the old client bundle does not
162
206
  mix with the new host (typical symptom: `client api: ... 404` or `env is not iterable` — both
163
- come from mixing versions).
207
+ come from mixing versions);
208
+ - An error shaped like `transport failure for /api/mcpManager/list: HTTP 404` means the host did
209
+ not register the `mcpManager` service: usually the plugin host half is not active (missing
210
+ cordis.patch.yml row / wrong profile) or the client and host versions disagree. Verify the
211
+ registration row per Q1, confirm the install targets the `web` profile, restart, and
212
+ hard-refresh; if it persists, upgrade both `dsh web` and the plugin to the latest versions.
164
213
 
165
214
  **Q3: MCP tools do not show up in an agent session?**
166
215
 
@@ -169,6 +218,16 @@ Check in order:
169
218
  so tools not searched are absent from the system prompt by design; switch to "Full injection"
170
219
  to verify.
171
220
 
221
+ **Q4: A server with an `Authorization` header still asks for OAuth / fails to mount?**
222
+
223
+ - When an `Authorization` (static bearer/token) header is configured, dsh-mcp does NOT treat the
224
+ server as OAuth: the authorization-code + PKCE flow is enabled only for servers WITHOUT a static
225
+ `Authorization` header, so a 401 is never mistaken for an OAuth challenge that opens the browser.
226
+ For a static-token server, make sure the request headers are correct;
227
+ - If an HTTPS intranet host reports `fetch failed` / `unable to verify the first certificate`, the
228
+ host Node does not trust the internal CA: start `dsh web` with `NODE_OPTIONS=--use-system-ca`
229
+ (or add the root cert to `NODE_EXTRA_CA_CERTS`), then restart the host and hard-refresh.
230
+
172
231
  **Add a server**:
173
232
 
174
233
  1. Click **Add server** (the form expands inline above the list).
package/README.md CHANGED
@@ -37,6 +37,51 @@
37
37
  测试连接与挂载共用同一份 token。
38
38
  - **Remote 自挂载**:client 半部在 `apply()` 里自行 `ctx.remote.$mount()` 挂载 `mcpManager`
39
39
  命名空间(原实现依赖 api-remotes 的 in-box 修改,独立版不再需要任何 in-box 包改动)。
40
+ - **读取声明式服务器**(host,`lib/cordis-servers.js`):把 patch 层里原生声明的
41
+ `@deepseek-ai/dsh-mcp-client` 行一并展示到设置页(只读,见下节)。
42
+
43
+ ### 声明式服务器(`cordis.patch.yml`)与优先级
44
+
45
+ DSH 原生支持在组合里直接声明 MCP 服务器:**一行一台**,`name: '@deepseek-ai/dsh-mcp-client'`,
46
+ 放在 profile 级 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 或机器级
47
+ `$DSH_HOME/cordis.patch.yml`(机器级对所有 profile 生效,且按层级覆盖 profile 级同 id 行)。
48
+
49
+ ```yaml
50
+ - insert:
51
+ - id: mcp-github
52
+ name: '@deepseek-ai/dsh-mcp-client'
53
+ config:
54
+ transport: stdio # 或 streamable-http
55
+ serverName: github
56
+ command: npx
57
+ args: ['-y', '@modelcontextprotocol/server-github']
58
+ ```
59
+
60
+ 1.11.0 起,这些声明会出现在 Settings → MCP 列表中,标记「cordis 声明」,只读展示来源文件。
61
+ 规则:
62
+
63
+ - **声明优先**:同一 `serverName` 若已被 patch 层声明(且未 `disabled`),插件不再挂载存储里
64
+ 的同名行——同名双挂载会撞工具注册表并让该服务器的工具整代回滚,页面会给出冲突说明。
65
+ - **只读**:声明式服务器在本页不可启停/编辑,改配置请直接改 `cordis.patch.yml`(`web`/`desktop`
66
+ 为 live reload,改完即生效;`headless`/`sdk` 等下次启动生效)。
67
+ - **导入 storages(镜像行)**:启动与每次刷新会把声明导入存储 domain(id `cordis:<rowId>`,
68
+ 带 `origin/declaredIn/declaredRowId` 来源标记),**只导入不覆盖**——插件自建行永不被改写;
69
+ 声明消失的镜像**自动删除**(镜像只是声明的副本,无主后不应堆积);含 `!!js` 表达式的声明**跳过导入**并在页面标注
70
+ 原因(值只能在 Loader 内求值)。镜像行**永不挂载**,挂载始终归组合。
71
+ - **接管 / 释放**:点击「接管」后,插件在 `cordis.patch.yml` 的**受管块**内写入 id-targeted `disabled: true`
72
+ (首次写入前生成 `.dsh-mcp.bak` 备份;原子替换、幂等、可逆,只动该块),声明让出 `serverName`,改由插件挂载
73
+ 同名行——由此启用 **OAuth 授权、凭据托管、`${VAR}` 头替换、测试连接**。挂载**硬失败**会自动回滚(移除受管块、
74
+ 行退回镜像)并返回失败原因;「释放」把挂载交还声明行并删除插件行。缺少显式 `id` 或含 `!!js` 的声明不可接管。
75
+ - **OAuth / 占位符限制(`needsPlugin`)**:声明行本身既不能携带 OAuth provider,也不能解析
76
+ `${VAR}`/裸变量名占位符——这两件事只有插件会做。因此这类声明**只能由插件挂载**:页面会把它们标为
77
+ **挂载失败并说明原因**(不是"连接中"),「释放」会让工具消失(确认框与结果告警都会提示),恢复只需
78
+ 重新「接管」。
79
+ - **热重载未提交时的安全降级**:写入停用块后若原生工具 5 秒内没有注销(该 profile 的 patch 热重载未提交,
80
+ 例如某个兄弟条目重建失败导致整代回滚),插件会**登记接管但暂不挂载**(管理行标记 `pendingTakeover`),
81
+ 重启 dsh 后由插件挂载;「释放」在同样情况下会提示需要重启。此外,挂载前若发现同名 `mcp__<server>__`
82
+ 工具已存在且不是本插件挂载的,一律跳过——任何情况下都不会与仍在挂载的声明行争抢同名工具。
83
+ - **tool search 覆盖**:检索/热注入与单工具开关按 `mcp__` 前缀处理整个工具集,声明式服务器的
84
+ 工具天然纳入,无需额外配置。
40
85
 
41
86
  ## 结构
42
87
 
@@ -45,6 +90,8 @@ dsh-mcp/
45
90
  ├── package.json name=dsh-mcp;dsh.client 声明;零 npm dependencies
46
91
  ├── lib/
47
92
  │ ├── index.js host 半部(McpManagerService,源自 mcp-manager 构建产物)
93
+ │ ├── cordis-servers.js 读取 patch 层原生声明的 MCP 服务器(1.11.0)
94
+ │ ├── patch-writer.js 受管块写入器:接管时在 patch 文件里停用声明行(备份/原子/幂等)
48
95
  │ ├── mcp-client.js vendored MCP 客户端(源自 @deepseek-ai/dsh-mcp-client,含工具列表稳定扩展)
49
96
  │ ├── oauth.js MCP OAuth 客户端提供者(授权码 + PKCE、回环回调、token 持久化)
50
97
  │ ├── probe.js vendored 连接探测(源自 mcp-client/src/probe.ts)
@@ -68,6 +115,17 @@ node scripts/build.mjs
68
115
  - CSS Modules 由 esbuild onLoad 插件处理:样式注入
69
116
  `<style data-plugin="dsh-mcp" data-file="…">`,默认导出 identity 类名映射。
70
117
 
118
+ ## 测试
119
+
120
+ ```sh
121
+ npm test
122
+ ```
123
+
124
+ - `npm test` 会自动适配环境:依赖 DSH 模块闭包(`@deepseek-ai/*`、`js-yaml`)的测试文件
125
+ (`cordis-servers` / `patch-writer` / `takeover`)在**缺少本地 DSH 安装时会被跳过**(公共 CI
126
+ 即如此),本机(profile 的 `node_modules` 可达)则跑全量,并在输出里说明跳过了哪些文件。
127
+ - 新增依赖 DSH 闭包的测试文件时,记得加进 `scripts/test.mjs` 的 `NEEDS_DSH_CLOSURE` 列表。
128
+
71
129
  ## 安装使用
72
130
 
73
131
  ### 1. 安装
@@ -142,7 +200,11 @@ dsh plugin --profile web add link:<本仓库绝对路径>
142
200
 
143
201
  - 确认 `dsh web` 进程日志中 `mcp-manager` 没有初始化错误;
144
202
  - 若升级过插件,请重启后**硬刷新**,避免旧 client bundle 与新版 host 不匹配
145
- (典型现象:操作报 `client api: ... 404` 或 `env is not iterable`,都是新旧版本混用所致)。
203
+ (典型现象:操作报 `client api: ... 404` 或 `env is not iterable`,都是新旧版本混用所致);
204
+ - 报错形如 `transport failure for /api/mcpManager/list: HTTP 404` 表示宿主端没有注册
205
+ `mcpManager` 服务:多半是插件 host 半未生效(漏了 cordis.patch.yml 注册行/装错 profile)或
206
+ client 与 host 版本不一致。请按 Q1 核对注册行、确认安装到了 `web` profile、重启后硬刷新;
207
+ 仍不行则把 `dsh web` 与插件版本都升到最新再试。
146
208
 
147
209
  **Q3:MCP 工具没有出现在 agent 会话里?**
148
210
 
@@ -150,6 +212,16 @@ dsh plugin --profile web add link:<本仓库绝对路径>
150
212
  - 注入模式为「按需检索」时,模型会通过 `mcp_tool_search` 检索后热注入,未检索到的工具不在
151
213
  系统提示词中属正常现象;可切换到「全量注入」验证。
152
214
 
215
+ **Q4:服务器配置了 Authorization 头却提示需要 OAuth 授权 / 挂载失败?**
216
+
217
+ - 只要在请求头里配置了 `Authorization`(静态 Bearer/token),dsh-mcp 就不会把它当作 OAuth
218
+ 服务器:真正的 OAuth(授权码 + PKCE)只对**没有静态 Authorization 头**的服务器启用,避免
219
+ 401 被误当成 OAuth 挑战而打开浏览器授权。若你连的是需要静态 token 的服务器,确认请求头
220
+ 正确即可;
221
+ - 若 https 内网域名报 `fetch failed` / `unable to verify the first certificate`,是宿主 Node
222
+ 不信任公司内网 CA:用 `NODE_OPTIONS=--use-system-ca` 启动 `dsh web`(或把根证书加入
223
+ `NODE_EXTRA_CA_CERTS`),再重启宿主与硬刷新浏览器。
224
+
153
225
  **添加服务器**:
154
226
 
155
227
  1. 点击「添加服务器」(表单在列表上方就地展开)