dev-manager-tauri/.blueprint/modules/mcp-single-server.md
lanrtop e57da3aebb
All checks were successful
Push & PR Check / check (push) Successful in 1m34s
Push & PR Check / check (pull_request) Successful in 1m1s
fix(mcp-single-server): 删除登记只清本项目托管条目,沉默项目统计每条 commit 只归属一个 workspace,注入状态带项目名(U1、U2)
同路径重复登记(dev-manager-tauri 与 2c5b54ef 并存)暴露的三处问题:
- remove_project_files_mcp 按路径解析目录后无条件删 Codex 管理块与 settings 条目,
  删 UUID 那条会连带切断规范 ID 的 Codex 接入;改为只删 URL 属于本 project_id
  或不是炼境项目路由(旧格式)的条目
- silent_projects 的「ID 或路径」任一匹配让一行吃到多条登记的并集(真机 147 / 86),
  改为每条 commit 先按直接 ID、再按 buff 路径归属唯一 workspace
- get_diagnostics 注入状态只打 ID,enterprise-system 的 UUID 被误读成炼境重复登记;
  改为「名称 (`id`)」,已删除的登记标注未登记

Rules-Applied: R06,R07

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:22:25 +09:00

138 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MCP 单一服务器化
将组级挂载的 `lian-jing-{group_id}`(每产品组一个 MCP 服务器)收敛为单一全局 `lian-jing` 服务器。
## 为什么
- **摩擦随项目数复利增长,与飞轮方向相反**:每新增一个产品组 = 多一次 `claude mcp add` + 多一个授权单元 + 每个会话多一份重复工具清单(现状 12 组 × 28 工具 ≈ 300+ 工具注册)。炼境目标是"管理项目越多 → agent 辅助越强",客户端配置成本必须是 O(1)。
- **组级挂载没有真正的隔离价值**:2026-07-06 实测任意组服务器可对任意项目执行 `inject_mcp`;组归属校验只覆盖部分工具(半吊子状态)。
- **消除 group_id 身份卡问题**:单一服务器后 `liangjing.json` 只需 `project_id`,group_id 缺失/过期/移组同步这一整类问题从根上消失。
## 设计决策(2026-07-06 /architect)
### 方案选择
- A. 直接切换:加全局路由同时删旧路由 —— ❌ 所有存量会话立刻断链,故障静默
- **B. 双路由过渡(选定)**:新增全局 `/mcp` 路由,旧 `/mcp/group/:gid` 保留兼容;注入逻辑切到新名字;批量迁移后旧路由在后续版本删除
- C. 全局 alias 与组服务器并存 —— ❌ 不消除 O(N) 注册,白改
### 核心接口变更
1. **路由**(`mcp_server.rs`):新增 `POST/GET /mcp` + `POST /mcp/messages`;`process_rpc` 的组上下文改为 `Option<String>`(旧路由 `Some(gid)`,新路由 `None`)
2. **工具层**(`mcp_tools.rs`):
- `resolve_project_root(project_id)` 去组化——直查 `project_workspaces`,删除 JOIN `project_group_map` 的归属过滤
- `get_project_mentor_context` / `append_project_note` / `get_project_snapshots` 三处 membership check 删除(改为项目存在性检查)
- `list_group_projects` 新增可选参数 `group_id`:显式参数 > 挂载上下文(旧路由)> 报错提示传参;`tools_list_result` schema 同步
3. **注入层**(`mcp_inject.rs`):
- `inject_project_mcp(project_id)` 去掉 `group_id` 参数;settings.json 写入 key `"lian-jing"`、URL `http://127.0.0.1:{port}/mcp`
- 注入时顺手删除同文件中所有 `lian-jing:group-*` 旧 key(迁移内建于重注入,`reinject_all_mcp` 即批量迁移器)
- `liangjing.json` 只写 `project_id` + `mcp_port`,**主动删除**已有 `group_id` 键(避免过期数据)
- 全局注册收敛为一次 `claude mcp add lian-jing`;新增迁移函数清理旧注册——遍历 `product_groups`,对**完整 UUID 名和短 8 位名两种格式**各尝试 `claude mcp remove -s user`(幂等,失败仅记日志)
- `remove_group_member` 语义修正:项目退出某组时,仅当它不再属于任何组才移除 `"lian-jing"` key
4. **附带收益**:`inject_mcp` 不再要求项目必须属于产品组("未加入任何产品组"报错可移除),未分组项目也能接入
### 受影响文件
- 修改:`src-tauri/src/mcp_server.rs` — 新增全局路由,process_rpc 组上下文 Option 化
- 修改:`src-tauri/src/mcp_tools.rs` — resolve_project_root 去组化、3 处校验删除、list_group_projects 参数化、schema 更新、inject_mcp 分支简化
- 修改:`src-tauri/src/mcp_inject.rs` — key/URL/注册收敛、旧 key 清理、liangjing.json 瘦身、全局旧注册迁移函数
- 修改:`src-tauri/src/commands/groups.rs` — inject 调用签名、remove_group_member 条件移除
- 修改:`src/components/settings/SettingsPage.tsx:537` — URL 展示改为 `/mcp`
- 修改:`docs/ai-context/git-workflow.md` + `src-tauri/resources/onboarding/git-workflow.md.tmpl` — 寻址说明改写
- 修改:`.claude/skills/retro-catchup/SKILL.md` — 解析 `lian-jing:group-` 前缀的逻辑改为 `lian-jing`
- 修改:`.blueprint/modules/mcp-config-inject.md` — 补注记:组级 key 方案已被本模块取代
- 仓库外(执行阶段处理):`~/.claude.json` 12 条旧注册清理、~15 个项目 settings.json/liangjing.json 批量重注入、用户全局 `~/.claude/CLAUDE.md` 炼境规则段改写
### 迁移顺序(切换编排)
1. 代码落地(双路由并存,零破坏)→ cargo test + curl 冒烟新路由
2. 全局注册 `lian-jing` + 清理旧注册(新会话立即可用单服务器)
3. `reinject_all_mcp` 批量刷新存量项目(settings.json 换 key、liangjing.json 去 group_id)
4. 文档/模板/技能/用户全局规则同步
5. 抽查验证:3 个项目文件内容 + `claude mcp list` 只剩 `lian-jing` + 新会话实调 `get_ci_status` 走通
6. (后置卡,下个版本)删除旧 `/mcp/group/:gid` 路由与 `Option` 兼容层
### 风险点
- 存量会话断链 → 双路由并存规避,旧路由删除后置一个版本
- `claude mcp remove` 部分失败 → best-effort + 日志,输出手动清理命令清单兜底
- 误删用户自有 mcpServers 配置 → 只删 `lian-jing:group-` 前缀 key(沿用现有精确删除机制)
- webhook 已核实不受影响(路由 `/webhook/gitea/:project_id` 不含 group_id)
## 任务卡
### ✅ T1. 服务器层双路由:全局 /mcp + 组上下文 Option 化
- status: done
- complexity: M
- files: src-tauri/src/mcp_server.rs
- acceptance: "[HTTP] POST http://127.0.0.1:{port}/mcp (initialize) → 200 且 serverInfo.name=炼境,GET /mcp 返回 SSE 事件流(endpoint 指向 /mcp/messages)"
- done_note: 2026-07-06 完成。process_rpc 驻军测试覆盖握手路径(cargo test 全绿);HTTP 层实测归入 T4(需新二进制运行)。注:T6 随后并入同一发版,双路由过渡态只存在于中间 commit
### ✅ T2. 工具层去组化:resolve_project_root 按 project_id 直查
- status: done
- complexity: M
- files: src-tauri/src/mcp_tools.rs
- depends: T1
- acceptance: "[HTTP] 经 /mcp 调 get_project_blueprint(project_id) → 返回 manifest 内容,无'不属于产品组'错误;list_group_projects(group_id=xxx) 显式传参 → 返回成员列表;cargo test 全绿"
- done_note: 2026-07-06 完成。resolve_project_root 直查 project_workspaces;3 处组归属校验替换为 ensure_project_exists;list_group_projects schema 改 required group_id。HTTP 实测归入 T4
### ✅ T3. 注入层收敛:lian-jing 单 key + 旧配置清理 + liangjing.json 瘦身
- status: done
- complexity: M
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/commands/groups.rs, src-tauri/src/commands/projects.rs, src-tauri/src/lib.rs
- depends: T1
- acceptance: "[Tauri command] inject_project_mcp(pid) → 项目 settings.json 含 key \"lian-jing\"(URL 以 /mcp 结尾)且无 lian-jing:group-* 残留;liangjing.json 含 project_id+mcp_port 且 group_id 键被删除;remove_group_member 仅在项目不属于任何组时移除 key;cargo test 全绿"
- done_note: 2026-07-06 完成。额外实现 migrate_to_single_server 启动时一次性迁移(标记位 mcp_single_migrated,claude CLI 缺失时不落标记等重试);delete_group/remove_group_member 加"仍属其他组则保留"判断;文件实测归入 T4
### ✅ T4. 存量批量迁移执行 + 端到端验证
- status: done
- complexity: S
- files: src-tauri/src/mcp_inject.rs(迁移暴露 git-bash 探测 bug,修复见 0d1427f)
- depends: T1, T2, T3, T6
- acceptance: "[CLI] claude mcp list → lian-jing-* 旧条目清零、仅剩 lian-jing;[MCP] 抽查 3 个项目 settings.json/liangjing.json 符合新格式;经 /mcp 实调 get_diagnostics → 返回 JSON 无 error;[HTTP] POST /mcp/group/{gid} → 404"
- done_note: 2026-07-06 完成。端到端验证暴露真 bug:CLAUDE_CODE_GIT_BASH_PATH 回退路径硬编码 Program Files,Scoop 安装的 Git 导致 claude mcp add/remove 全挂(存量 bug,组级时代即存在)——修复为多位置探测 + already exists 视为成功 + 迁移标记位以注册成功为条件(0d1427f)。首次迁移的全局注册部分手动补齐(本会话 claude mcp remove ×12 + add lian-jing),文件迁移由 migrate_to_single_server 自动完成。验证:~/.claude.json 仅剩 lian-jing、3 项目(dev-manager-tauri/enterprise-system/life-memo)配置全部新格式、新路由握手 + get_diagnostics 实调通过、旧路由 404。遗留:同路径重复登记(legacy id "dev-manager-tauri" 组 0001 与 2c5b54ef 并存)导致批量重注入时 liangjing.json project_id 翻转,待用户决定是否 unregister legacy 条目
### ✅ T5. 文档/模板/技能/UI 寻址说明同步
- status: done
- complexity: S
- files: docs/ai-context/git-workflow.md, src-tauri/resources/onboarding/git-workflow.md.tmpl, .claude/skills/retro-catchup/SKILL.md, src/components/settings/SettingsPage.tsx, .blueprint/modules/mcp-config-inject.md
- depends: T3
- acceptance: "[Bash] grep -rE 'lian-jing-\\{group_id\\}|lian-jing:group-' docs/ src/ src-tauri/resources/ → 零命中(mcp-config-inject.md 保留历史注记除外);pnpm typecheck 通过"
- done_note: 2026-07-06 完成。grep 零命中、typecheck 通过;用户全局 ~/.claude/CLAUDE.md 炼境规则段已同步改为单一 lian-jing 寻址
### ✅ T6. 删除旧 /mcp/group 路由与兼容层
- status: done
- complexity: S
- files: src-tauri/src/mcp_server.rs, src-tauri/src/mcp_tools.rs
- depends: T3
- acceptance: "cargo test 全绿;grep 'mcp/group' src-tauri/src/ 零命中;[HTTP] POST /mcp/group/{gid} → 404(归入 T4 实测)"
- done_note: 2026-07-06 完成。原计划后置一个版本,经评估提前并入本次发版:迁移在新版启动时自动重写全部客户端配置,旧路由的实际消费者只剩升级前的存量会话(服务重启后本就断开),单人环境保留价值趋近于零。tools_call 签名去 Option 化,list_group_projects 改 required 参数
## 遗留清理:同路径重复登记(2026-10-03)
起因:get_diagnostics「使者回传覆盖率」里 dev-manager-tauri 出现两行(147 / 86 条 commit)。只读排查确认是 done_note 记下的遗留——同一路径 `E:\code-project\dev-manager-tauri` 登记了 `dev-manager-tauri`(2026-03-23 批量迁移,现为规范 ID:context.json / buff / 注入 / Codex URL 均指向它)与 `2c5b54ef-…`(2026-04-01 UI 导入)两条 workspace,各自绑一条 gitea_repos 指向同一仓库,推送双采集:commit 按 sha 先到先得被劈成两半(256 / 137),56 对 PR 事件双写,快照双份。诊断里两行数字不同是因为 `silent_projects` 的 `OR win_path` 连接让 UUID 那行吃到了两边并集。「MCP 注入状态」里的 `d248554e-…` 是 enterprise-system,不是重复(该节只打 ID 不打名字导致误读)。
### ✅ U1. 删除登记只清本项目的客户端托管条目
- status: done
- complexity: S
- files: src-tauri/src/mcp_inject.rs
- acceptance: "agent_verify: [cargo test mcp_inject] 同一目录的 Codex 管理块 / .claude/settings.json lian-jing 条目指向别的 project_id 时,remove 后原样保留;指向本 project_id 或不是炼境项目路由(旧格式)时照旧移除"
- done_note: 2026-10-03 完成。新增 `remove_keeps_entries_owned_by_another_project_at_same_root`;`cargo test` 全量 303 通过。运行中的旧二进制尚无此修复,U3 删除登记后须立即 inject_mcp 补回。
- 为什么:`remove_project_files_mcp` 按路径解析目录后无条件删 Codex 管理块与 settings 条目,同路径重复登记时删掉 UUID 那条会顺手切断规范 ID 的 Codex 接入(主仓库 `.codex/config.toml` 的块 URL 是 `/mcp/projects/dev-manager-tauri`)。Claude local 条目(`sync_claude_local`)与 token 本就按 project_id 匹配,不受影响。
### ✅ U2. 沉默项目每个 commit 只归属一个 workspace + 注入状态带项目名
- status: done
- complexity: S
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs
- acceptance: "agent_verify: [cargo test silent_projects] 两个 workspace 同路径、commit 分挂两个 ID 时,每条 commit 只计一次(不再出现一行等于两边并集);[cargo test diagnostics] 注入状态行显示「名称 (`id`)」,已不在登记中的 ID 标注未登记"
- done_note: 2026-10-03 完成。新增 `silent_projects_counts_each_commit_once_under_duplicate_registration`(旧 SQL 在同一夹具上得 [3, 5],新 SQL 得 [2, 3])与 `diagnostics_inject_label_shows_name_and_flags_unregistered`;`cargo test` 全量 303 通过。
- 为什么:`pw.id = cm.project_id OR pw.win_path = bb.project_path` 在同路径多登记时让一条 commit 同时匹配多个 workspace;注入状态只打 ID,人读不出 `d248554e-…` 是哪个项目。
### 🔵 U3. 存量合并:2c5b54ef 并入 dev-manager-tauri 后删除登记
- status: in_progress
- complexity: S
- files: (运维操作,无代码改动)
- acceptance: "agent_verify: [只读 SQL] project_workspaces 中 win_path=E:\code-project\dev-manager-tauri 仅 1 行;gitea_repos 指向 lanrtop/dev-manager-tauri 仅 1 行;commit_metrics/project_notes/pr_events 无 2c5b54ef 残留;[MCP] get_diagnostics 覆盖率 dev-manager-tauri 仅 1 行;verify_onboarding(dev-manager-tauri) 与操作前状态一致;主仓库 git diff .codex/config.toml 为空"
- 步骤:① 先解绑 2c5b54ef 的 Gitea(gitea_unlink_repo,删重复 webhook,停双采集)② 在线备份 DB ③ 单事务迁移:commit_metrics / project_notes / 独有 pr_events / 待办改挂规范 ID,重复 pr_events 与 UUID 快照、文档新鲜度缓存删除,space / repo 绑定 / 产品组成员 / profile 空字段补给规范 ID ④ unregister_project(2c5b54ef) → 立即 inject_mcp(dev-manager-tauri) 补回(运行中的旧二进制尚无 U1 修复)⑤ backfill_notes_archive ⑥ 删空产品组 d669f693
- 不做:「登记时拒绝同路径」归 agent-client-parity P8f(已规划含重复登记拒绝),此处不重复实现;beast-mode 同类重复(beast-mode / 940dfbb4,无 Gitea 绑定、近 30 天无活动)本卡不处理。