# 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: todo
- complexity: S
- files: （执行操作，无新代码——迁移在新版启动时自动执行）
- depends: T1, T2, T3, T6（需先构建 + 重启炼境运行新二进制）
- acceptance: "[CLI] claude mcp list → lian-jing-* 旧条目清零、仅剩 lian-jing；[MCP] 抽查 3 个项目（含本项目 + enterprise-system）settings.json/liangjing.json 符合新格式；新 Claude 会话经 lian-jing 实调 get_diagnostics → 返回 JSON 无 error；[HTTP] POST /mcp/group/{gid} → 404"

### ✅ 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 参数
