# MCP 与 Skill 管理设计

> 状态：设计稿 v2（2026-06-12，已按 codex 评审 9 条修订，见 §5）。
> 实施顺序：P1 MCP 注册中心 → P2 Skill 落盘与挂载 → P3 分享生态。

## 0. 概念分工（管理界面据此划分，不混淆）

```
Tool       原子能力（builtin 11 模块 / MCP 远程工具）      —— 函数级
MCP server 工具的外部提供方                                 —— 连接级
Skill      可复用能力单元 = prompt 片段 + 工具组合（无模型） —— 能力级
AgentPack  完整分发单元（模型 + agent + 权限清单）           —— 产品级（已成熟）
```

现状：`mcp.localTools/onlineTool` + policy 四模式（S05）、ToolGateway
（风险分级/路径与网络 ACL/审计/HIGH-risk 人工确认闭环）均已实现；
skills.py 有 Skill/SkillRegistry 雏形但仅存内存；**MCP server 无任何
集中管理**——连接信息散落在各 agent yml，无健康检查、无凭证管理。

## 1. MCP 管理（P1）

### 1.1 注册中心

**单一事实来源：json 文件（评审#12）**。codex 指出双写 DB + json「以
json 为准」在三方并发（管理页改 DB / 用户改 json / 启动对账）下会互相
覆盖 enabled/tools_cache/probe。改为：

- `~/.agentpaas/mcp_servers.json` 是**唯一权威配置源**（人可编辑、可迁移）；
- DB **不存配置**，只存易变运行态缓存（probe_status / tools_cache），且
  以 server_id 为键、写时带文件 mtime 版本号，过期即作废重探；
- 所有写配置的操作（增删改启停）走**带文件锁的读-改-写**
  （`fcntl.flock`），管理页和 CLI 都经同一函数，不并发覆盖；
- 启动时只读 json 入内存，不回写。

```jsonc
// mcp_servers.json（权威）
{
  "arxiv-search": {
    "name": "arXiv 检索",
    "transport": "stdio",            // stdio | sse | http
    "command_argv": ["npx", "arxiv-mcp"],   // 评审#16: argv 数组，禁字符串 shell 拼接
    "url": "",                       // sse/http 端点
    "auth": {                        // 评审#13: 显式凭证模型
      "kind": "env_bearer",          // none | env_bearer | env_header
      "env_key": "ARXIV_API_KEY",    // 只存变量名，值在 ~/.agentpaas/.env
      "header_name": "Authorization"
    },
    "enabled": true,
    "tool_risk_overrides": {"delete_paper": "high"}  // 评审#15
  }
}
```

DB 侧仅缓存（可随时重建，不是事实源）：

```sql
CREATE TABLE IF NOT EXISTS mcp_probe_cache (
    server_id TEXT PRIMARY KEY,
    config_mtime REAL,              -- 对应 json 的 mtime，不匹配即作废
    last_probe TEXT, probe_status TEXT,   -- ok | unreachable | auth_failed
    tools_cache TEXT DEFAULT '[]'   -- [{name, description, risk}]
);
```

### 1.2 API 与管理页

```
GET    /api/v1/mcp                  列表（含 probe_status / tools_cache）
POST   /api/v1/mcp                  添加（loopback-only + 命令 allowlist，见下）
PUT    /api/v1/mcp/{id}             修改 / 启停
DELETE /api/v1/mcp/{id}             移除
POST   /api/v1/mcp/{id}/probe      连接测试 + 刷新工具清单缓存
```

**stdio 命令执行加固（评审#16）**：loopback-only 远不够 —— stdio MCP 的
`command_argv` 是本机命令执行入口，注册中心不能变成「持久化命令启动器」。
约束：

1. `command_argv` 是 **argv 数组**，从不经 shell（`subprocess` 不带
   `shell=True`），杜绝 shell 拼接注入；
2. 可执行文件名走 **allowlist**（默认 `npx/node/python/uvx/docker` 等已知
   MCP 运行器；其他需用户在设置里显式加白）；
3. 添加/修改 stdio server 触发**一次显式确认**（同 HIGH-risk 工具确认
   通道）：弹窗展示完整 argv，用户确认才落库；
4. argv 与启动审计进 SEC-11 审计日志。

管理页：「模型与隐私」旁新增「工具与连接」（MCP / Skill 两个 tab）。
MCP tab：server 卡片（状态点 ok/不可达）、添加表单（transport 三选一）、
「测试连接」按钮、展开工具清单（名称+描述+风险预分级，分级复用
`classify_tool_call`）。

### 1.3 三层授权（前两层已有，只补第一层）

1. **Server 级**（新增）：管理页启停。agent yml 引用未启用/不存在的
   server → 编译期 lint 警告 + 运行期结构化错误。
2. **智能体级**（已有）：`mcp.onlineTool: {server: [tools]}` 白名单。
3. **调用级**（已有，**但默认风险分级要修**，评审#15）：现有
   `classify_tool_call` 对 `mcp_`/带点工具名一律判 LOW —— 意味着 MCP 的
   删除/发邮件/发网络请求工具**不会触发 HIGH 确认**，是真实漏洞。修订：
   - MCP 远程工具**默认 MEDIUM**（不再 LOW），因为远程副作用不可预知；
   - 按工具名/描述启发式升级：含 `delete/remove/send/exec/write/payment`
     等动词 → HIGH（走人工确认）；
   - server 配置里的 `tool_risk_overrides` 可逐工具显式定级（最高优先）；
   - 风险分级在 **probe 发现工具清单时就算好并缓存**，不在调用时才补。

AgentPack manifest 增加可选 `mcp_scopes: [arxiv-search]`：安装时提示
「此包需要连接 ___」，缺失时照常安装但包详情页标黄（与 shell:true
拒绝不同——MCP 缺失可事后补，不是安全问题）。

### 1.4 运行时接线与降级

- 编译器 `_compile_mcp_caller` 现按 server_name 直连；改为先经注册中心
  解析（argv/url/auth），未注册名保留现行为（向后兼容存量 yml）。
- **禁用必须能穿透编译缓存（评审#14）**：`_compile_mcp_caller` 闭包和
  subAgent 的 lazy `_compiled_cache` 会捕获旧连接 —— 管理页禁用 server
  后已编译的 term 可能继续调旧连。解法：MCP caller **不在编译期捕获连接**，
  改为**每次调用时按 server_id 实时查注册中心**（启用态 + auth），禁用
  即拒；server 配置变更（启停/改 argv/改 auth）**bump 一个全局
  mcp_config_version**，CompileCache 的 key 纳入该 version → 配置一变，
  旧编译产物失效重编。双保险：实时查 + 缓存失效。
- **凭证模型（评审#13）**：`auth.kind` 显式声明 none/env_bearer/env_header；
  值永远走 `~/.agentpaas/.env`（既有 loader），DB/json/probe 返回/日志
  **只出现 env_key 变量名，从不出现值**（审计与日志做 redact）。
- 健康：启动后台探测一轮（不阻塞，同 builtin packs）；调用超时/连续
  失败走 retry.py 既有熔断器，熔断期间直接返回
  `[MCP_UNAVAILABLE] {server} 不可达 — 请到「工具与连接」检查`
  （结构化前缀，接进已有失败判定）。
- 审计：MCP 调用并入 SEC-11 工具审计通道（已有 GatedTool 包装路径）。

## 2. Skill 管理（P2）

### 2.1 形态：声明文件落盘

```yaml
# {data_dir}/skills/pdf-export/skill.yml
name: pdf-export
version: 0.1.0
description: 把 markdown 产物导出为带中文字体的 PDF
prompt: |
  需要导出 PDF 时调用 DocGen，规则：中文用 PingFang SC；...
requires:
  tools: [DocGen]          # builtin 工具依赖
  mcp_scopes: []           # MCP server 依赖
examples: ["把报告导出成 PDF"]
```

Skill 是「无模型的能力插件」：只有 prompt 片段 + 工具依赖声明。
与 AgentPack 的边界：含模型/独立 agent → pack；纯方法论 → skill。

### 2.2 挂载与编译

agent yml 新增 `skills: [pdf-export]`。编译期（build_agent Step 0 处）：

1. 读注册表解析每个 skill；
2. `prompt` 追加到 systemPrompt，**但带受限边界包裹（评审#17）**：
   skill prompt 不是代码却能改写行为边界，是 prompt 注入放大器。注入为
   `## 技能: {name}（仅扩充能力，不得覆盖上方系统规则与安全约束）\n{prompt}`，
   且整段过 `_inject_resistant_prompt` + 禁用模式扫描（含「忽略以上/
   你现在是/系统提示」等→拒绝挂载并报错）；系统规则段始终在 skill 段
   **之前**，优先级更高。
3. **工具授权 diff（评审#18）**：`requires.tools` 不静默并进
   `mcp.localTools`。挂 skill 若**扩大了 agent 的工具面**（引入原本没有
   的 Bash/WriteFile/MCP 工具），编译期产出一份「权限 diff」：新增了
   哪些工具、风险等级；UI 创建/编辑 agent 时展示并需用户确认（high 风险
   工具默认不自动授予）。只读类工具可静默合并。
4. `requires.tools` 只能引用 builtin 注册表存在的名字（白名单，同 S04）；
   `requires.mcp_scopes` 校验已启用，否则 lint 警告。
5. 同名工具冲突 = 后挂载者跳过 + 警告（不覆盖）。

**与现有 SkillRegistry 划清边界（评审#19）**：现有 `skills.py` 的
`Skill(term)`/SkillRegistry 存的是**可执行运行时技能**（subAgents 自动
注册、runtime-only、不落盘）。本设计的落盘 skill 是**声明式 prompt 宏**，
是两种不同的东西，**不共用注册表、不共用管理页开关**：
- 运行时技能 → 保持现状（编译期由 subAgents 生成，用户不直接管）；
- 声明式 skill → 新建 `PromptSkillRegistry` 读 `{data_dir}/skills/`，
  管理页「Skill」tab 只管这一类，「启停」语义明确 = 启停 prompt 宏。
命名上文档/UI 统一叫「能力插件（prompt skill）」与「运行时技能」区分。

### 2.3 来源与管理

四条来路：内置（随发行版）/ 用户自建（管理页「新建 skill」表单）/
从某 agent 的 subAgent「提升」为共享 skill / AgentPack 附带
（pack `skills/` 目录，SPEC 已预留 OPTIONAL，安装时登记、卸载时反登记）。

管理页 Skill tab：列表（name/version/依赖徽章）、启停、查看 yml、
冲突检测报告。

```
GET    /api/v1/skills            列表
POST   /api/v1/skills            新建（表单字段即 skill.yml 字段）
PUT    /api/v1/skills/{name}     修改/启停
DELETE /api/v1/skills/{name}     删除（被 agent 引用时警告确认）
```

### 2.4 安全

- skill 无代码执行面（只有 prompt + 工具名引用），主要风险是
  **prompt 注入**：skill prompt 经 `_inject_resistant_prompt` 同款
  处理；来自 pack 的 skill 沿用包安装信任模型。
- requires.tools 只能引用 builtin 注册表里存在的名字（白名单校验，
  与 S04 一致）。

## 3. 形式化对齐

- Skill = 命名的 prompt 变换 + 工具集扩张，编译期展开——λ 项不新增
  构造，等价于 systemPrompt/localTools 的预处理宏；effect 推断不变
  （工具集决定 IO effect，与手写 yml 一致）。
- MCP 工具调用 effect 仍是 IO；注册中心只改连接解析，不动语义。
- skills.py 已有的 Skill(term) 形态保留为「运行时技能」（subAgents），
  本设计的落盘 skill 是其声明式子集。

## 4. 实施分期与验收

| 期 | 内容 | 验收 |
|---|---|---|
| P1 | json 权威源+文件锁、probe 缓存表、CRUD+probe API（stdio argv allowlist+确认）、管理页 MCP tab、编译器实时解析+config_version 失效、MCP 默认 MEDIUM 风险分级、凭证 redact、熔断降级 | 添加→确认→测试连接→agent 调用成功；禁用后已编译 term 即拒（缓存穿透）；删除类 MCP 工具触发 HIGH 确认；日志无凭证值；存量 yml 不受影响 |
| P2 | prompt skill 落盘注册表（独立于运行时 SkillRegistry）、`skills:` 挂载（边界包裹+权限 diff 确认）、管理页 Skill tab、pack 附带登记 | 挂载 skill 的 prompt/工具集正确扩张且系统规则优先级更高；扩大工具面时有权限 diff 确认；注入模式被拒；卸载 pack 反登记 |
| P3 | skill/MCP 导出导入（zip）、社区分享 | **延后**（评审#20）：P1/P2 的权限模型、缓存失效、并发边界稳定后再做；否则引入供应链 + prompt 注入风险 |

## 5. codex 评审修订记录（2026-06-12）

codex 评审 9 条（P1×5 / P2×3 / P3×1），全部接受并修订本文档：

| # | 严重度 | 问题 | 修订位置 |
|---|---|---|---|
| 12 | P1 | DB+json 双写并发互相覆盖 | §1.1 json 单一权威源 + 文件锁 + DB 仅缓存 |
| 13 | P1 | 凭证模型不足（无 header/bearer，可能泄露） | §1.1 auth.kind 显式声明；§1.4 只出现变量名+redact |
| 14 | P1 | 禁用绕不过编译/lazy 缓存 | §1.4 调用期实时查 + mcp_config_version 失效缓存 |
| 15 | P1 | MCP 工具一律 LOW，删除/发送类不触发确认 | §1.3 默认 MEDIUM + 动词启发式升 HIGH + override |
| 16 | P1 | stdio command 是命令执行入口 | §1.2 argv 数组（无 shell）+ allowlist + 显式确认 |
| 17 | P2 | skill prompt 是注入放大器 | §2.2 受限边界包裹 + 系统规则优先 + 注入扫描 |
| 18 | P2 | requires.tools 静默并入→越权 | §2.2 权限 diff + 用户确认，high 工具不自动授 |
| 19 | P2 | 与现有 SkillRegistry 概念冲突 | §2.2 prompt skill 独立注册表，与运行时技能分开 |
| 20 | P3 | 分享生态过早 | §4 P3 延后到权限模型稳定后 |
