# 设计提案:DSH 自有 JSON 配置层(替代 Claude 兼容层) [← 返回 README](../README.zh.md) | 相关:[dsh v0.1.2-rc.1 适配记录](adaptation-dsh-0.1.2-rc1.md) **状态**:已裁决并实现(v0.4.0) | **提出日期**:2026-09-09 | **目标版本**:v0.4.0(破坏性) **关联**:[dsh v0.1.2-rc.1 适配记录](adaptation-dsh-0.1.2-rc1.md)(宿主适配记录)、`AGENTS.md`(装载模型)、`CHANGELOG.md`(0.4.0) ## 0. 裁决结果(2026-09-09) | 待决问题 | 裁决 | |---|---| | §4.1 用户层语义 | **全局装载**(宿主级一条连接,不扇出),含遗留 `~/.dsh/mcp.yml` | | §4.2 JSON 写入策略 | **CLI 独占整个文件**(保留其他顶层键与键序,不保留注释/排版) | | §4.3 yml 与 json 优先序 | **yml 优先**,CLI 默认写 yml(JSON 为显式 opt-in) | | §3.5 profile 名 | 层名用动态 `dsh-profile-user`;从 loader 根 include 的 `config.path`/`ctx.baseUrl` 推导,`DSH_MCP_PROFILE` 可覆盖,解析不出则跳过该层 | | §3.3 遗留项目 `.mcp.json` | **保留**为独立低优先级只读层(`cc-project`) | | 项目侧压制 | **支持**:项目行遮蔽全局行时,只对该项目的会话 deny 全局工具 | 文件命名最终取 **`mcp.json`(无前导点)**:`.dsh/mcp.json`、`~/.dsh/mcp.json`、 `~/.dsh/profiles//mcp.json`。 ## 1. 背景与动机 现状(v0.3.1)的"Claude 风格"支持由两部分组成: - **项目层**:`/.mcp.json`(CC project scope,只读); - **用户层**:`~/.claude.json` 顶层 `mcpServers`(CC user scope,只读、默认关闭、 需 `DSH_MCP_READ_CLAUDE_USER=1`)。 `~/.claude.json` 是 CC 的单体状态文件,混存 OAuth 凭据、onboarding 状态、逐项目 会话历史(见 `src/cc-file.ts:1-23`)。读取它带来三类问题: 1. **风险面**:为拿 MCP 配置不得不打开一个含凭据的文件,靠 allowlist 摘取子树; 2. **扇出事故**:机器级配置会被并入**每个已知项目**各挂一条连接(见 §4.1 扇出语义); 3. **复杂度**:CC 每次会话重写整个文件,watcher 必须用 `mcpServers` 子树内容哈希 门控(`src/registry.ts:643-661`),否则空转对账。 **本提案的定位转变**:插件从"Claude 兼容层"转为"**DSH 自有 JSON 配置层**"—— 读 DSH 自己的 JSON 文件(方言沿用生态通行的 `mcpServers` 写法),CC 的 `/.mcp.json` 降级为遗留只读来源,`~/.claude.json` 不再读取。 ## 2. 生态调研(2026-09) | 工具 | 项目级 | 用户级 | 方言 | 关键做法 | |---|---|---|---|---| | **Cursor** | `.cursor/mcp.json`(提交进 git) | `~/.cursor/mcp.json` | `{"mcpServers":{…}}`,stdio `command`/`args`/`env`;远程 `url`/`headers`(无 `type`) | **两者合并、项目级优先**;无 profile 概念 | | **Codex** | 未落地([#2554](https://github.com/openai/codex/issues/2554)、[#23487](https://github.com/openai/codex/issues/23487)、[PR #3864](https://github.com/openai/codex/pull/3864)) | `~/.codex/config.toml` | TOML `[mcp_servers.]`:`command`/`args`/`env`/`url`/`bearer_token_env_var`/`enabled` | 有 `codex mcp add/list/get/remove` 子命令;`[profiles.*]` 配置档([#2800](https://github.com/openai/codex/issues/2800) 申请 `mcp --profile`) | | **opencode** | `opencode.json(c)`(项目根) | `~/.config/opencode/opencode.json` | 主配置 `mcp` 键:`type: local\|remote`、`command` 数组、`environment`、`enabled` | 优先序:远程 → 全局 → `OPENCODE_CONFIG` → 项目 → `.opencode/` → `OPENCODE_CONFIG_CONTENT`;支持 `{env:VAR}`/`{file:…}` | | **Claude Code** | `.mcp.json`(项目根) | `~/.claude.json`(单体状态文件) | `mcpServers`,含 `type: sse/http` | 另有 local scope `projects..mcpServers` | 来源:[Cursor MCP](https://cursor.com/help/customization/mcp.md)、 [Codex MCP Servers](https://mintlify.wiki/openai/codex/configuration/mcp-servers)、 [opencode MCP Servers](https://mintlify.wiki/anomalyco/opencode/mcp-servers)、 [opencode Config](https://mintlify.wiki/anomalyco/opencode/config)。 **结论**:Cursor 的"工具自有目录里的 `mcp.json` + 家目录同名文件、项目级优先"就是 本提案的目标形态;**没有任何一家读别的工具的状态文件**;**没有任何一家做 profile 级 MCP 文件**(Codex 的 `[profiles.*]` 最接近)。 ## 3. 提案内容与评估 ### 3.1 不读 `~/.claude.json` —— 采纳 - 生态一致;同时可**删除** `serversHash` 哈希门、`claudeUserLayerConflict` 冲突闩与 两个开关(`DSH_MCP_READ_CLAUDE_USER`、`DSH_MCP_IGNORE_CLAUDE_JSON`), 兑现 `src/cc-file.ts:35-36` 的 `TODO(v0.4)`。 - 用户层的风险从"读外来凭据文件"降为"读 DSH 自有文件"。 ### 3.2 JSON 写入 DSH 自有位置 —— 采纳,但两处需修正 **修正 A:文件命名去掉前导点。** 建议 `/.dsh/mcp.json`、`~/.dsh/mcp.json`、 `~/.dsh/profiles//mcp.json`: - Cursor 先例是目录带点、文件不带点(`.cursor/mcp.json`); - 与同目录 `mcp.yml` 命名对称; - 避免与项目根遗留 `.mcp.json` 在文档/报错中混淆。 **修正 B:JSON 没有注释,现有"受管块"机制失效(见 §4.2)。** 必须先定写入策略。 **实现利好**:JSON 是 YAML 1.2 的子集,现有 `yaml` 依赖(`parseDocument`)可直接 解析 `.json`,报错定位、`!!js` 标签拒绝、`KEY: null` 删除语义等既有能力可复用。 ### 3.3 项目根 `.mcp.json` 降为次级读取 —— 采纳,但保留独立层名 保留为**独立层**(`cc-project` 或改名 `legacy-project`),不要与新 DSH 项目层合并: `dsh-mcp list`、`.dsh/.mcp-diag.json` 的 `shadowedByYml`/`shadowedByProject` 与告警 都靠 `source` 归因。`DSH_MCP_IGNORE_MCP_JSON=1` 继续只管这一个遗留文件。 ### 3.4 CLI 增加写入格式开关 —— 采纳,建议 `flag > env > 自动探测` - 环境变量:`DSH_MCP_CLI_FORMAT=yml|json`(与 `DSH_MCP_*` 命名一致); - 显式覆盖:`--format yml|json`; - **自动探测**:目标项目已有哪个文件就写哪个;两个都存在 → 报错要求显式 `--format` (否则极易产生"yml 一条、json 一条"的双真相); - `--scope user` 需明确写 `~/.dsh/mcp.yml` 还是 `~/.dsh/mcp.json`; - 文档位置:`docs/guide/cli.md` / `docs/guide/cli.zh.md`(当时的 README CLI 小节)、`dsh-mcp --help` 打印当前生效值。 ### 3.5 层名 cc-* → dsh-* —— 采纳,但**不得硬编码 web/headless** - profile 名用户自定义(`tui`、`acp`、自建 profile 均合法),固定 `dsh-web-user`/ `dsh-headless-user` 会让其他 profile 的配置被静默忽略; - 建议层名 **`dsh-profile-user`**(诊断附 `profile: ` 属性),路径 `~/.dsh/profiles//mcp.json`(注意 **profiles 复数**)。 **profile 名可发现性(已核查)**: - 宿主环境**没有** `DSH_PROFILE`(实测宿主进程仅 `DSH_HOME`/`DSH_SESSION_ID`/ `DSH_SESSION_JSONL`/`DSH_SHELL`/`DSH_WEB_URL`); - 但可推导:`dsh-app-boot` 的 `boot()` 设 `ctx.baseUrl = dirname(absoluteConfigPath)`, profile 根配置固定为 `~/.dsh/profiles//cordis.yml` (源码常量 `PROFILE_ROOT_FILENAME = "cordis.yml"`),根 include entry 的 `config.path` 亦指向它;宿主另有 `ctx.provide("dshHomePath", …)` 服务; - **属宿主内部细节**:按此实现并保留"取不到 profile 名则只用通用用户层"的降级路径, 同时向 dsh 提 issue 要一等公民的 `DSH_PROFILE` 或 profile 服务。 ## 4. 两个必须先定的语义决策 ### 4.1 用户层/Profile 层:扇出(fan-out)还是全局(global)? | | 扇出 | 全局 | |---|---|---| | 装载 | 每条用户行并入**每个已知项目**,各挂一个 mcp-client 实例 | 宿主级只挂一个实例(profile patch 行 / bundle 层) | | 进程 | N 个项目 = N 个 stdio 子进程 / N 条 HTTP 连接 | 1 个,所有会话共享 | | cwd | 项目行=项目根;用户行=宿主 cwd(`src/registry.ts:839-841`) | 由全局行配置决定 | | 会话可见性 | 经 `denySetFor` 按会话项目 deny 其他项目 | 全局可见,无按项目隔离 | | 热重载 | 文件改动即收敛 | 需改 profile patch 并重启(`patchReload: startup`) | | 现状 | `user-yml`、`cc-user` 是此语义(`src/registry.ts:677-684` 扇出告警) | `globalNames()`(`src/index.ts:25-46`)只做冲突判定,**不装载** | **提案默认**:`dsh-user` / `dsh-profile-user` 采用**扇出**语义(与现有用户层一致、 改动最小),保留一次性扇出告警。若希望"profile 级 = 只挂一次",需要单独实现全局 装载与工具可见性,并改名为 `dsh--global` 之类以消除歧义。 ### 4.2 JSON 文件的写入策略(受管块替代方案) 现有 `src/mcp-file.ts` 的契约是 begin/end 注释标记之间为受管 YAML 列表、**标记外 逐字节保留** + 锁 + 原子写。JSON 无注释,三选一: | 方案 | 代价 | 建议 | |---|---|---| | **CLI 独占整个文件**:只认 `mcpServers` 键,写入时保留其他顶层键、格式归一化 | 手写格式/注释丢失 | ✅ v0.4 起步方案 | | **JSONC + 注释保留式编辑**(`jsonc-parser` 的 `modify`/`applyEdits`) | 新增依赖(与 CLI 零依赖现状冲突) | 若要保住"块外逐字节保留"承诺时 | | 受管键 + 用户键分离 | 语义混乱 | ✗ | ### 4.3 同项目两份原生文件的优先序 `.dsh/mcp.yml`(CLI 默认写入)与 `.dsh/mcp.json`("推荐读取路径")并存会制造两个 真相源。二选一并在文档中固定: - **A(推荐 v0.4)**:`mcp.yml` > `mcp.json`,与"默认写入 yml"一致; - **B**:`mcp.json` > `mcp.yml`,并把 CLI 默认写入目标也改为 json。 ## 5. 建议层序(待裁决) ``` 1. /.dsh/mcp.yml 原生 YAML(CLI 默认写入;面板兼容) 2. /.dsh/mcp.json 原生 JSON(dsh-project,推荐读取路径) 3. /.mcp.json 遗留 CC 项目文件(只读;cc-project/legacy-project) 4. ~/.dsh/profiles/<当前 profile>/mcp.json 用户层(dsh-profile-user,扇出) 5. ~/.dsh/mcp.yml 原生用户层(legacy user-yml) 6. ~/.dsh/mcp.json 通用用户层(dsh-user,扇出) ``` 三键影子去重(精确名 / 归一名 / 服务身份,`src/registry.ts:272-293`)可吸收跨层 重复;层数增加不改变"同服务只装高优先级一条"的既有规则,但 `dsh-mcp list` 与诊断 必须展示新层名与路径。 ## 6. 落地清单 | 项 | 内容 | |---|---| | 版本 | v0.4.0(破坏性:层名、`source` 值、两个开关移除) | | 代码 | `cc-file.ts` 拆为 `json-file.ts`(通用 JSON 读取)+ 遗留 CC 读取;`model.ts` 增 JSON 方言 schema(`mcpServers`);`cli.ts` 增 `--format`/`DSH_MCP_CLI_FORMAT` 与 JSON 写入器;`registry.ts` 删哈希门、扩层、改 `SOURCE_RANK`/`McpRowSource` | | 写入 | 先定 §4.2 策略,再实现原子写 + 锁(复用 `writeFileAtomic`/`withPatchLock`) | | 测试 | `test-cc-file.mjs` 拆为 JSON 读取 / 遗留 CC / 优先序 / 写入器;新增 profile 名推导与降级用例 | | 文档 | `docs/guide/layers.md` / `docs/guide/layers.zh.md` 层序表与 env 变量;`AGENTS.md` 目录结构与关键行为约定同步 | | 安全 | 新 JSON 文件同样只落 `${VAR}` 字面量、装载时展开;stdio 行仍是可执行代码载体;用户层保留扇出告警 | | 建议 | 本仓库 `.gitignore` 改为只忽略 `.dsh/.mcp-diag.json`,让 `mcp.yml`/`mcp.json` 可提交 | ## 7. 兼容与迁移 - 依赖 cc-user 层的用户:v0.4 起该层不存在,需把条目迁到 `~/.dsh/mcp.json` 或 `~/.dsh/profiles//mcp.json`(迁移脚本可作为 v0.4 的可选项)。 - 只写 `.mcp.json` 的项目:继续被读取(层 3),无动作。 - 诊断字段 `shadowedByYml`/`shadowedByProject`/`shadowedIdentity` 结构不变,`source` 枚举值变化;建议 v0.4 的诊断/CLI 同时接受旧值一个版本,便于脚本过渡。 ## 8. 待决问题清单 1. §4.1:`dsh-profile-user` 取扇出还是全局? 2. §4.2:JSON 写入采用 CLI 独占、还是引入 JSONC 注释保留编辑? 3. §4.3:`mcp.yml` 与 `mcp.json` 谁优先(是否同时改默认写入目标)? 4. §3.5:是否接受"从 `ctx.baseUrl`/根 include 路径推导 profile 名",还是先向 dsh 要 `DSH_PROFILE`? 5. 是否保留 `/.mcp.json` 的读取(本案默认保留为层 3)?