Files
NotesAgentic/docs/development/MCP-Bridge与Plugin-Host开发说明.md

12 KiB
Raw Permalink Blame History

MCP Bridge 与 Plugin Host 开发说明

更新日期:2026-09-02。本文记录第二阶段阶段 C 已实现的本地 stdio MCP Bridge、隔离 Plugin Host、Tool Contract 转换和离线测试方式。阶段 D 的 Plugin Command 与 Settings 已在其独立开发说明中落地。

1. 目标与实现状态

阶段 C 的目标是让外部 MCP Server 进入既有 Plugin、Tool、Permission、Agent 和 Trace 链路,同时避免 Agent Runtime、前端或 Benchmark 直接依赖 MCP 原始消息。

当前链路:

Plugin Manifest
→ Plugin Runtime
→ 独立 stdio MCP Server 进程
→ initialize / capability negotiation
→ tools/list 分页发现与校验
→ NotesAgent ToolDefinition
→ Tool Registry / Permission Manager
→ Agent Runtime / Agent Trace

已经实现:

  • 本地 stdio 子进程启动、关闭和异常退出检测;
  • UTF-8、换行分隔的 JSON-RPC 2.0 消息;
  • initialize、协议版本与 tools capability 协商;
  • notifications/initialized
  • 分页 tools/list
  • tools/call、业务错误与 JSON-RPC 错误转换;
  • 超时和 notifications/cancelled
  • Tool 命名空间、JSON Schema、权限和 Manifest 集合校验;
  • Host 状态查询、重启和异常后的 Tool 自动注销;
  • stderr 隔离、环境变量裁剪、消息及结果大小限制;
  • 无网络、无密钥的确定性 MCP Fixture。

实现依据为 MCP 官方 Lifecycle 2025-11-25Transports 2025-11-25Tools 2025-11-25

2. 代码位置

backend/app/extensions/mcp.py
    stdio 进程、JSON-RPC、MCP 生命周期、发现、调用和 Host 状态

backend/app/extensions/runtime.py
    Plugin Manifest、权限、MCP Tool 批量注册/回滚和生命周期集成

backend/app/agent/tools.py
    内部 Tool 参数校验、结构化执行错误和线程安全 Registry

backend/extensions/fixtures/mcp-echo/
    确定性 stdio MCP Server 与 Plugin Manifest

3. Plugin Manifest

MCP Plugin 的后端配置示例:

id: example-mcp
name: Example MCP
version: 1.0.0
permissions:
  - notes.read
contributes:
  tools:
    - example-mcp.search
backend:
  type: mcp
  transport: stdio
  command: uvx
  args: [--isolated, --from, example-mcp==1.2.3, example-mcp]
  startup_timeout_seconds: 60
  tool_timeout_seconds: 30

约束:

  • 阶段 C 只接受 type: mcptransport: stdio
  • 命令和参数通过数组直接传给 subprocess.Popen,不经过 Shell
  • Python 包形式的 MCP Server 推荐使用 uvx --isolated --from <package>==<version> <command>,固定版本并与 NotesAgent 项目环境隔离;
  • Plugin 包内自带且不需要第三方依赖的 Python 脚本可以使用 python server.pyNode、Rust 等 Server 继续使用各自受控启动器,因此 Host 不强制所有 MCP 都经过 uvx
  • PATH 中的 executable 使用名称,例如 uvxpythonnode
  • manifest 中带目录的 executable 必须解析到 Plugin 包内部;
  • contributes.tools 使用 <plugin_id>.<remote_name>
  • 安装阶段只读 Manifest,不启动第三方进程;
  • 完成用户授权后,enable 才启动 Host。

4. 生命周期

4.1 启动

启用 MCP Plugin 时依次执行:

  1. 检查 Plugin 声明权限是否全部获得授权;
  2. 检查 Manifest 声明的 Tool ID 是否与现有 Registry 冲突;
  3. 启动独立 stdio Server
  4. 发送 initialize
  5. 校验协商版本和 tools capability
  6. 发送 notifications/initialized
  7. 分页读取 tools/list
  8. 校验全部 Tool
  9. 确认发现集合与 Manifest 完全一致;
  10. 将完整集合注册到 Tool Registry
  11. Plugin 和 Host 进入 ready

任何步骤失败都会注销本轮已注册 Tool、关闭子进程并把 Plugin 标记为 error,不会留下半启用状态。

4.2 停止与异常退出

停用、卸载或应用关闭时,先注销 Tool,再关闭 stdin,等待 Server 正常退出。超时后依次 terminate 和 kill。

Server 异常退出、stdout 出现非 JSON-RPC 内容或发送超大协议消息时:

  • 未完成请求返回 PLUGIN_HOST_UNAVAILABLE
  • Host 进入 unhealthy
  • Plugin 进入 error
  • 对应 Tool 从 Registry 中立即注销;
  • 用户可以调用 Host Restart 接口重新协商和发现。

Server 发送 notifications/tools/list_changed 时不会直接信任新集合。当前实现先把 Host 标记为不健康并注销旧 Tool,要求通过 Restart 重新执行完整发现与校验。

5. Tool Contract 转换

MCP Tool

{
  "name": "search",
  "description": "Search notes",
  "inputSchema": { "type": "object", "properties": {} },
  "_meta": { "notesagent/permission": "notes.read" }
}

进入系统后转换为:

{
  "name": "example-mcp.search",
  "description": "Search notes",
  "parameters": { "type": "object", "properties": {} },
  "permission": "notes.read",
  "source": "plugin"
}

转换规则:

  • 远端名称必须能转换为合法且稳定的项目 Tool ID;
  • inputSchema 必须是有效的 object JSON Schema
  • additionalPropertiespatternProperties 等动态字段先由完整 JSON Schema 校验,Pydantic 参数载体不会再次误拒绝合法字段;
  • _meta.notesagent/permission 必须属于项目已知权限;
  • Tool 权限必须同时出现在 Plugin Manifest 中;
  • Agent 仍通过 Tool Registry 执行参数校验、Permission、超时和 Trace
  • MCP structuredContent 存在时映射为内部 output;否则保留为受控 content 数组;
  • MCP isError: true 映射为 MCP_TOOL_CALL_FAILED
  • 结果超过 256 KiB 映射为 MCP_TOOL_RESULT_TOO_LARGE

6. 隔离与安全边界

当前隔离是“独立进程 + 协议边界”,不是完整的操作系统沙箱。

uvx 解决的是 Python 工具依赖隔离:它等价于 uv tool run,在 uv 缓存中使用可丢弃的独立虚拟环境。它不会限制 Server 读取用户文件、访问网络、创建子进程或调用系统 API,因此不能代替安全沙箱。当前开发模式下,首次 enable 尚未缓存的包可能访问包索引,因此示例使用 60 秒启动上限;生产实现不得依赖该行为,必须在用户确认后的安装/更新阶段预取和验证固定版本,运行阶段只启动已准备好的环境。

已经执行的保护:

  • 第三方模块不 import 到 AI Core
  • 子进程 cwd 固定为 Plugin 包目录;
  • 不使用 Shell 拼接命令;
  • 不把 Provider API Key、APP_DB_PATH、Vault 路径和其他宿主环境变量传入子进程;
  • stderr 与 JSON-RPC stdout 分离,stderr 不进入 API 和 Agent Trace
  • stdout 只能发送合法 MCP JSON-RPC
  • stdout 在读取完整行前即应用有界读取,单条协议消息上限 2 MiB;stderr 也按固定大小分块读取;
  • 单次 Tool Result 上限 256 KiB
  • MCP Tool 不绕过 Permission Manager 和 Agent Tool Timeout。
  • 调用被 Agent 取消时,同时通知 Server 并唤醒本地 pending Queue,阻塞线程不会继续占用线程池直至远端超时。

当前尚未提供容器、受限系统账户、seccomp、Windows AppContainer 或 macOS Sandbox,因此 Plugin 进程仍具有当前操作系统用户授予的一般文件访问能力。正式社区插件分发或“一键安装”前必须完成以下安全门槛:

  • 由 Tauri/Rust Host 统一启动进程并提供平台级文件、网络、子进程和资源配额限制;
  • 安装/更新时完整展示 executable 与全部参数,明确警告并要求用户主动确认;
  • 固定包来源和版本,增加包哈希/签名与可信发布者校验;
  • 默认禁止访问 Vault、凭据和宿主环境,只通过声明 Permission 与受控 Host API 授权;
  • 关闭 Host 时终止完整进程树,不只结束直接子进程。

在这些门槛完成前,当前 MCP Host 只适用于内置 Fixture、团队可信插件和开发联调;不得把它描述为可以安全执行任意社区代码。上述安装确认要求遵循 MCP SEP-1024uvx 行为依据 uv 官方 Using tools 文档。

后端通过 APP_ENVIRONMENT 强制该边界:只有 development 可以启动当前未沙箱化的 MCP Host;其他环境返回 403 MCP_TRUST_APPROVAL_REQUIRED,且不会创建进程或注册 Tool。后续 Tauri/Rust Host 提供沙箱与绑定完整命令摘要的可信许可后,再替换此临时门禁。

7. Host API

GET  /api/plugins/{plugin_id}/host
POST /api/plugins/{plugin_id}/host/restart

状态响应包含:

plugin_id
backend_type / transport
status
tools_count
started_at / last_seen_at
protocol_version
server_name / server_version
error

状态值:

stopped
starting
ready
unhealthy
error

Restart 返回 202 OperationResponse。接口返回前已完成本地 Host 重启和 Tool 重新发现;message 中给出最终 Host 状态。Restart 只用于运行中或异常 Host;用户主动停用、尚未启用或等待授权的 Plugin 返回 409 PLUGIN_HOST_UNAVAILABLE,必须通过 Enable 明确启动。

8. 离线 Fixture

Fixture 位于:

backend/extensions/fixtures/mcp-echo

它提供:

  • mcp-fixture.echo:返回 structuredContent
  • mcp-fixture.fail:返回 isError: true
  • mcp-fixture.sleep:验证超时和取消;
  • mcp-fixture.large:验证结果大小上限;
  • mcp-fixture.environment:验证宿主 Secret/路径没有进入子进程;
  • mcp-fixture.exit:验证异常退出、Tool 注销和 Restart。
  • mcp-fixture.command:作为 Plugin Command 专用 MCP Target,验证 Context 裁剪、Secret 传递和与 Agent Tool 的隔离。

Fixture 的 tools/list 使用两页响应,用于覆盖分页发现。测试还会启动缺少 tools capability、返回无效 Schema/initialize result,以及输出超长无换行 stdout 的变体。

9. 验证

cd backend
uv run python -m compileall -q app
uv run pytest

cd ../frontend
pnpm test
pnpm type-check
pnpm build

阶段 C 新增测试覆盖:

  • initialize、版本和 capability negotiation
  • 分页 tools/list 与命名空间映射;
  • Permission、JSON Schema 与 Contribution 集合;
  • Tool 成功、业务错误、结果过大和超时;
  • Agent 取消后 pending 等待线程及时释放;
  • additionalProperties 动态参数保持 JSON Schema 语义;
  • Agent Runtime 调用 MCP Tool 并写入正式 Trace
  • Secret/Vault 环境隔离;
  • Server 异常退出、Tool 注销和 Host Restart
  • 缺少 capability、无效 initialize result、无效 MCP Schema 和超长无换行 stdout
  • disabled Plugin 不会被 Host Restart 隐式重新启用;
  • OpenAPI 发布 Host 状态和重启路径。

10. 当前边界与后续阶段

阶段 C 不包含:

  • Streamable HTTP MCP transport
  • Resources、Prompts、Sampling、Elicitation 和 MCP Tasks
  • Plugin Command 与 Settings Contribution
  • Secret Reference 注入;
  • Plugin Registry 持久化、签名与社区来源校验;
  • 操作系统级沙箱;
  • 一键安装前的完整命令展示与确认 UI
  • Tool 列表热更新的无中断替换。

阶段 D 已在当前 Plugin Runtime 上增加 Command、Settings、Secret Contract 和命名空间 Storage,且未修改 Agent 使用内部 Tool Contract 的原则。实现细节见《Plugin Command 与 Settings 开发说明》。