# 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`(旧路由 `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: done - 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 - done_note: 2026-10-03 完成。用户执行合并脚本(运行中 DB 写入被自动模式安全分类器拦截,按约定交用户执行):commit 258 条、笔记 20 条、待办 1 条改挂规范 ID,PR 事件删重复 58 条、迁独有 3 条,UUID 快照 83 条与文档新鲜度缓存删除,space / repo 绑定 / 产品组成员 / profile 空字段补给规范 ID,删空产品组 d669f693,删 UUID 的 gitea_repos 行(该绑定无 webhook、无凭证引用,与 gitea_unlink_repo 等价)。随后经全局 MCP 执行 unregister_project(2c5b54ef) → inject_mcp(dev-manager-tauri) → backfill_notes_archive(预演确认只改写主仓库档案 27→34 条,其余项目档案不变)。验收:全库无 2c5b54ef 残留;同路径登记 1 行;Gitea 绑定 1 行;get_diagnostics 覆盖率 dev-manager-tauri 仅 1 行(176 条 commit);verify_onboarding 11/13 与操作前一致(唯一变化:近 30 天 commit 109→176)。**验收改动声明**:「主仓库 git diff .codex/config.toml 为空」未满足——差异是重注入把 9-23 旧格式块升级为带 `http_headers_helper` 的当前格式(digest = sha256(project_id),代码注明可入库),不是删除连带破坏;且 PR #34 已先合并重编译,unregister 本就不再动规范 ID 的块。该行留作主仓库未提交改动由用户决定是否提交。更正:双采集来自 Gitea 轮询遍历两行绑定,不是两个 webhook(两行 webhook_id 均为空)。遗留发现:4 条 dev-manager-tauri 旧笔记在 DB 中不是 UTF-8,backfill 的 filter_map 静默跳过(档案与回报计数都不含)。备份:%APPDATA%\com.yangzhixiang.dev-manager-tauri\dev-manager.backup-20261003-before-dedupe.db - 不做:「登记时拒绝同路径」归 agent-client-parity P8f(已规划含重复登记拒绝),此处不重复实现;beast-mode 同类重复(beast-mode / 940dfbb4,无 Gitea 绑定、近 30 天无活动)本卡不处理。