第 7 章 · 01 两层扩展与 MCP 插件客户端


文档摘要

第 7 章 · 01 两层扩展与 MCP 插件客户端 本节摘要:本节进入 Reasonix 的扩展体系。SPEC §1.4 定义了两层扩展:第一层是编译期 built-in(Go 包,通过 自注册进默认 registry),与 Reasonix 主二进制同进程;第二层是运行时外部插件(stdio JSON-RPC 子进程,MCP 兼容)。本节聚焦第二层里的 MCP 客户端 :它连接外部 MCP 服务器,把远程的 tools/prompts/resources 适配成本地的 接口,让 agent 把插件工具和 built-in 一视同仁。线协议是 JSON-RPC 2.

第 7 章 · 01 两层扩展与 MCP 插件客户端

本节摘要:本节进入 Reasonix 的扩展体系。SPEC §1.4 定义了两层扩展:第一层是编译期 built-in(Go 包,通过 init() 自注册进默认 registry),与 Reasonix 主二进制同进程;第二层是运行时外部插件(stdio JSON-RPC 子进程,MCP 兼容)。本节聚焦第二层里的 MCP 客户端 internal/plugin:它连接外部 MCP 服务器,把远程的 tools/prompts/resources 适配成本地的 tool.Tool 接口,让 agent 把插件工具和 built-in 一视同仁。线协议是 JSON-RPC 2.0,传输层有三种(stdio 子进程 / Streamable HTTP / legacy HTTP+SSE),用 transport 接口屏蔽差异,让 MCP 级逻辑(handshake / tools/list / tools/call)只写一遍。MCP 服务器在 config 的 [[plugin.servers]] 声明,贡献的工具以 mcp__<server>__<tool> 命名。cmd/reasonix-plugin-example 是参考的 MCP stdio 插件示例。本节还会点明:插件贡献的工具会进入 tool schema,进而影响第 6 章讲的 cache-stable 前缀——这是扩展体系与 Cache-first 契约的直接交汇。

内容来源:原项目源码 internal/plugin/(plugin.go、transport_stdio.go、transport_http.go、transport_sse.go)、cmd/reasonix-plugin-example/main.godocs/SPEC.md §1.4/§3.3,精读并套用体系化模板。

⚠️ 注意:Reasonix 的"MCP 客户端"指的是 Reasonix 作为客户端去连接外部 MCP 服务器(服务器是别人写的子进程)。别把方向搞反——Reasonix 不在这里"提供"MCP 服务,它"消费"MCP 服务。另外,MCP 只是两层扩展中的一种(运行时外部插件里的"只贡献工具"那种);下一节的 Extension Protocol v1 sidecar 更强(能拦截事件、贡献 Provider、提供 UI)。

学习目标

阅读完本节,你应当能够:

  1. 区分两层扩展:编译期 built-in(init() 自注册)与运行时外部插件(stdio JSON-RPC 子进程)。
  2. 说清 internal/plugin 的定位:MCP 客户端,把远程 tools/prompts/resources 适配成本地接口。
  3. 列出三种传输层(stdio / Streamable HTTP / legacy HTTP+SSE)及 transport 接口的作用。
  4. 讲清 MCP 握手流程:initialize → notifications/initialized → tools/list → tools/call。
  5. 解释 MCP 工具的命名约定 mcp__<server>__<tool>annotations.readOnlyHint 的映射。
  6. 说清"安装一个 MCP 服务器即授权它的所有工具"这条授权模型。
  7. 读懂 cmd/reasonix-plugin-example 作为最小 MCP stdio 服务器的协议实现。
  8. 解释插件工具如何影响第 6 章的 cache-stable 前缀(tool schema 是前缀的一部分)。

一、SPEC §1.4:两层扩展

Reasonix 的 SPEC 第 1.4 条把扩展体系定义成两层,原文:

4. **Two extension tiers.** Compile-time built-ins (self-register via `init()`), and runtime external plugins (stdio JSON-RPC subprocesses, MCP-compatible).

翻译:两层扩展:编译期 built-in(通过 init() 自注册),和运行时外部插件(stdio JSON-RPC 子进程,MCP 兼容)。

用图把两层展开:

两层的关键区别:

维度 编译期 built-in 运行时外部插件
形态 Go 包,编译进主二进制 外部子进程(独立可执行文件)
注册方式 init() 自注册 配置声明 + 启动子进程
进程 与 Reasonix 同进程 独立进程(stdio/HTTP 通信)
升级 重新编译 Reasonix 替换子进程,不动主二进制
信任 完全信任(同进程) 配置声明即授权(SPEC §3.3)
能力 提供 tool/provider/outputstyle 等 MCP 贡献工具;Extension 更强(下节)

第一层(built-in)在前几章已经反复遇到:internal/tool/builtin 里的文件工具、bash 工具等都是 built-in,通过 init() 注册进默认 registry;cmd/reasonix/main.go 入口 blank-import 这些包让它们的 init() 执行。第二层(外部插件)是本节和下节的主角。

第二层内部又分两种:MCP 客户端(本节,只贡献 tools/prompts/resources)和 Extension Protocol v1 sidecar(下节,能拦截事件/贡献 Provider/提供 UI)。SPEC §1.4 说的"runtime external plugins (stdio JSON-RPC subprocesses, MCP-compatible)"主要指 MCP;Extension 是 Reasonix 自己更强的协议。

二、internal/plugin:MCP 客户端

internal/plugin 的包注释把定位讲得很清楚:

// Package plugin is Reasonix's MCP client. It connects to external MCP servers and // adapts their tools to the tool.Tool interface, so the agent treats plugin // tools and built-ins uniformly. The wire protocol is JSON-RPC 2.0 in every // case; only the transport differs (stdio subprocess, Streamable HTTP, or the // legacy HTTP+SSE). A transport interface hides that difference so the MCP-level // logic — handshake, tools/list, tools/call — is written once.

翻译:internal/plugin 是 Reasonix 的 MCP 客户端。它连接外部 MCP 服务器,把它们提供的工具适配成 tool.Tool 接口,让 agent 把插件工具和 built-in 一视同仁。线协议在所有情况都是 JSON-RPC 2.0,只有传输层不同(stdio 子进程 / Streamable HTTP / legacy HTTP+SSE)。一个 transport 接口隐藏这个差异,让 MCP 级逻辑(握手 / tools/list / tools/call)只写一遍

三个关键设计。

第一,适配器模式。agent 内部只认 tool.Tool 接口(第 5 章讲过),不关心工具是 built-in 还是远程插件。internal/plugin 的工作就是把远程 MCP 工具包装成实现 tool.Tool 接口的本地对象。这样 agent 的主循环、permission、checkpoint、execute_one 等所有逻辑都不用为插件工具特判——统一处理。

第二,JSON-RPC 2.0 是线协议。不管传输层是 stdio 还是 HTTP,线协议都是 JSON-RPC 2.0(request 有 id, response 按 id 匹配;notification 无 id)。这意味着 MCP 级的逻辑(发什么请求、收什么响应、怎么解析工具列表)对所有传输都一样。

第三,transport 接口屏蔽差异transport 接口(call / notify / close)抽象了"怎么把 JSON 消息发出去、怎么收回来"。stdio transport 用子进程的 stdin/stdout,HTTP transport 用 HTTP 请求,SSE transport 用 HTTP+SSE 流。MCP 级逻辑只调 transport 接口,不关心底下是哪种。这是经典的"协议与传输分离"——同一套协议跑在多种传输上。

三、三种传输层

SPEC §3.3 和源码列出三种传输:

// stdioTransport speaks newline-delimited JSON-RPC 2.0 over a subprocess's // stdin/stdout — the MCP stdio convention (one JSON message per line, ...
传输 形态 适用场景
stdio(默认) 本地子进程,一行一个 JSON 消息走 stdin/stdout 本地工具(最常见)
Streamable HTTP HTTP 请求,流式响应 远程 MCP 服务器
legacy HTTP+SSE HTTP + Server-Sent Events(老协议) 兼容旧 MCP 服务器

stdio 是默认且最常见——一个 MCP 服务器就是个独立的可执行文件,Reasonix 把它当子进程启动,通过它的 stdin/stdout 收发 JSON-RPC 消息。transport_stdio.go 顶部定义了关闭预算:closeWaitBudget = 5 * time.Second(关闭时最多等 5 秒)、gracefulCloseWaitBudget = 750 * time.Millisecond(优雅关闭等 750ms)。这些常量控制 Reasonix 关闭 MCP 服务器时的超时行为——先发 shutdown 通知,等一会,还不退出就强杀。

stdio 传输有个细节:MCPProcessMode。源码:

// MCPProcessHost runs authorized stdio MCP as a trusted host process that // does not inherit the agent Bash command sandbox. This is the product // default so servers such as chrome-devtools-mcp can reach the real browser, // Keychain, LaunchServices, and local app services. MCPProcessHost MCPProcessMode = "host" // MCPProcessConfined wraps the process with sandbox.CommandArgs. Reserved for // internal managed deployments and tests; never auto-selected for user installs. MCPProcessConfined MCPProcessMode = "confined"

默认是 host 模式:授权的 stdio MCP 服务器作为受信宿主进程运行,不继承 agent 的 Bash 命令沙箱。注释举例:这样 chrome-devtools-mcp 才能访问真实浏览器、Keychain、LaunchServices、本地应用服务。confined(沙箱包裹)只给内部托管部署和测试用,绝不自动选给用户安装。这是个重要的信任决策——MCP 服务器默认是高权限的(因为要能访问系统资源)。

四、MCP 握手与工具调用流程

MCP 的线协议流程,从 plugin-example 的注释看得很清楚:

Protocol, one JSON object per line: - initialize → {protocolVersion, capabilities, serverInfo} - notifications/initialized (notification, no id) → ignored - tools/list → {tools: [{name, description, inputSchema, annotations}]} - tools/call {name, arguments} → {content: [{type:"text", text}], isError} - prompts/list → {prompts: [{name, description, arguments}]} - prompts/get {name, arguments} → {messages: [{role, content:{type,text}}]} - resources/list → {resources: [{uri, name, description, mimeType}]} - resources/read {uri} → {contents: [{uri, mimeType, text}]}

MCP 不只贡献工具,还贡献 prompts(提示模板)和 resources(资源)。完整流程:

几个要点。

第一,protocolVersion。源码 const protocolVersion = "2024-11-05"——Reasonix 在 initialize 时广告的 MCP 协议版本。这是 MCP 标准的版本协商。

第二,initialize 是 request,notifications/initialized 是 notification。initialize 有 id(等响应),initialized 是通知(无 id,发了就完)。这是 JSON-RPC 2.0 的 request/notification 区分。

第三,tools/list 只调一次(握手时),tools/call 每次调工具时。握手时 Reasonix 拿到工具列表,把它们注册进本地 registry(适配成 tool.Tool)。之后 agent 调用某工具时,Reasonix 才发 tools/call 给服务器执行。所以工具列表是"启动时拉取、缓存",工具执行是"按需调用"。

第四,日志走 stderr,stdout 专留给 JSON-RPC。plugin-example 的注释强调:"Logs go to stderr (reasonix forwards plugin stderr to the terminal); stdout is reserved for JSON-RPC so it must never carry stray prose." MCP stdio 协议要求 stdout 只能走 JSON 消息,任何杂散文字都会破坏协议解析。所以插件日志必须走 stderr,Reasonix 会把 stderr 转发到终端。

五、工具命名、readOnly 映射与授权模型

MCP 工具适配进 Reasonix 后,命名遵循约定:mcp__<server>__<tool>。plugin-example 的注释举例:

Then reasonix surfaces its tools as "mcp__example__echo" / "mcp__example__wordcount", its prompt as the "/mcp__example__review" slash command, and its resource as the "@example:doc://style-guide" reference.

也就是说,一个名为 example 的 MCP 服务器,提供的 echo 工具在 Reasonix 里叫 mcp__example__echo。这种 <前缀>__<服务器>__<工具> 的命名避免了不同服务器的同名工具冲突,也让 permission 规则能按服务器前缀匹配。

readOnly 映射。SPEC §3.3:

- A tool's MCP `annotations.readOnlyHint` maps to `Tool.ReadOnly()`. It defaults

MCP 工具的 annotations.readOnlyHint(MCP 标准里的一个提示字段)映射到 Reasonix 的 Tool.ReadOnly() 方法。这个 ReadOnly() 在第 5 章 02 节讲过——它决定工具是否触发 checkpoint 快照(非只读工具才拍快照)。所以 MCP 工具通过这个映射,自然融入 Reasonix 的 checkpoint 体系:声明了 readOnlyHint 的 MCP 工具不会触发文件快照。

授权模型:安装即授权。SPEC §3.3 / 安全模型:

- **MCP authorization.** Installing an MCP server authorizes all of its tools;

安装一个 MCP 服务器,就授权了它的所有工具。这是个"全有或全无"的授权——你不能"只安装但只允许其中某几个工具"(那是 permission 规则的事,不是 MCP 授权)。安装动作本身就是信任声明。结合上一节的 MCPProcessHost 默认(不继承沙箱),MCP 服务器默认有相当高的权限,所以 Reasonix 在 install preview / reasonix plugin show / capability diagnostics / Desktop installer 里都会显示 MCP 服务器的启动命令供用户审查。

💡 契约要点:MCP 客户端的设计核心是"适配 + 统一":用 transport 接口屏蔽传输差异(stdio/HTTP/SSE),用 JSON-RPC 2.0 统一线协议,用 tool.Tool 接口让插件工具和 built-in 一视同仁。agent 主循环、permission、checkpoint 都不用为 MCP 工具特判。代价是:安装即全授权、默认不沙箱——MCP 服务器是高信任实体。理解这套"统一接口 + 高信任"的取舍,才能用好 MCP。

六、cmd/reasonix-plugin-example:参考 MCP stdio 服务器

cmd/reasonix-plugin-example 是个可运行的参考 MCP stdio 服务器,它存在的目的注释写得很直接:

// Command reasonix-plugin-example is a reference Reasonix plugin: a minimal MCP stdio // server speaking newline-delimited JSON-RPC 2.0 on stdin/stdout. It exists to // document the contract end-to-end (the protocol the internal/plugin client // drives) and to give users a working example to copy.

翻译:它是个最小 MCP stdio 服务器,在 stdin/stdout 上说换行分隔的 JSON-RPC 2.0。它存在是为了端到端地文档化契约(internal/plugin 客户端驱动的协议),并给用户一个可工作的示例去复制

它在 reasonix.toml 里的配置方式:

[[plugins]] name = "example" command = "reasonix-plugin-example"

配好后 Reasonix 把它的工具暴露成 mcp__example__echo / mcp__example__wordcount,prompt 暴露成 /mcp__example__review slash 命令,resource 暴露成 @example:doc://style-guide 引用。

main.go 的主循环很简单:从 stdin 一行行读 JSON-RPC,按 method 分派(initialize / tools/list / tools/call / prompts/list / prompts/get / resources/list / resources/read),把响应写回 stdout。version 通过 -ldflags "-X main.version=..." 注入,在 initialize 的 serverInfo 里报告——这样 Reasonix(和人)能看到跑的是哪个构建。这个示例是学习 MCP stdio 协议的最佳起点,因为它把契约用最小可运行代码完整演示了一遍。

七、MCP 工具与 cache-stable 前缀的交汇

本节最后,把 MCP 和第 6 章的 Cache-first 契约连起来。这一点容易被忽略,但很重要。

第 6 章 01 节讲过:工具 schema 是 cache-stable 前缀的一部分(base prompt + tools schema + memory 索引)。MCP 服务器贡献的工具,适配后会进 Reasonix 的 tool registry,序列化进 tools schema,从而成为前缀的一部分。

这意味着两件事。

第一,启用/禁用一个 MCP 服务器会改变前缀字节,触发一次 cache miss。因为 MCP 工具进/出 registry,tools schema 变了,前缀变了。所以 Reasonix 在 PLUGIN_PACKAGES.md 里提到:"Imported servers default to auto_start=false; users connect them on demand so startup does not change the provider-visible tool schema."(导入的服务器默认 auto_start=false;用户按需连接,这样启动不会改变 provider 可见的工具 schema。)这是个有意识的缓存友好设计——MCP 服务器默认不自动连,避免每次启动都因为连了一堆 MCP 而让 tool schema 变化。

第二,MCP 工具的 description / inputSchema 任何字节变化都会让前缀变化。所以 MCP 服务器的工具描述最好是稳定的(不要每次 tools/list 返回不同描述)。这也是为什么 internal/agent/cache_shape.gonormalizeToolSchemas 要先排序再 hash——保证"相同工具集"产生相同前缀指纹,不受注册顺序干扰。

所以扩展体系和 Cache-first 契约不是割裂的:扩展贡献的工具直接进前缀,扩展的任何变动都会影响缓存。这也是为什么第 6 章 01 节的 scripts/check-cache-impact.shinternal/plugin/* 列为 cache-sensitive 路径——改 plugin 包可能影响 tool schema,进而影响前缀。理解这条联系,你才算看清 Reasonix 各章节之间的耦合。

本节要点回顾

  1. 两层扩展(SPEC §1.4):编译期 built-in(init() 自注册,同进程)vs 运行时外部插件(stdio JSON-RPC 子进程);外部插件里又分 MCP(本节,只贡献工具)和 Extension(下节,更强)。
  2. internal/plugin 是 MCP 客户端:连接外部 MCP 服务器,把远程 tools/prompts/resources 适配成本地 tool.Tool,让 agent 一视同仁。
  3. 三种传输:stdio(默认,本地子进程,一行一个 JSON)/ Streamable HTTP(远程)/ legacy HTTP+SSE(兼容旧);transport 接口屏蔽差异,JSON-RPC 2.0 统一线协议。
  4. MCPProcessMode:默认 host(不继承沙箱,让 chrome-devtools-mcp 这类能访问真实系统资源);confined 仅内部/测试用。
  5. 握手流程:initialize(request)→ notifications/initialized(notification)→ tools/list / prompts/list / resources/list(拉取目录)→ 之后 tools/call 按需执行;protocolVersion 2024-11-05;日志走 stderr,stdout 专留 JSON-RPC。
  6. 命名与映射:工具叫 mcp__<server>__<tool>;MCP annotations.readOnlyHint 映射到 Tool.ReadOnly()(决定是否触发 checkpoint)。
  7. 授权模型:安装即全授权(不能只授权部分工具);MCP 默认高权限,install preview / plugin show / diagnostics 都显示启动命令供审查。
  8. plugin-example:可运行的最小 MCP stdio 服务器,端到端文档化契约,供复制;[[plugins]] 配置。
  9. 与 Cache-first 的交汇:MCP 工具进 tools schema → 是前缀一部分;启用/禁用 MCP 服务器或工具描述字节变化都会触发 cache miss;故导入服务器默认 auto_start=false,且 internal/plugin/* 是 cache-sensitive 路径。

下一节,我们看比 MCP 更强的扩展:Extension Protocol v1 sidecar(internal/extension,约 20K 行)。它除了贡献工具,还能拦截运行时事件(interceptor)、贡献流式 Provider、提供结构化 UI。我们会讲清 Extension 与 MCP 的能力边界差异、17 个冻结的拦截钩子点、替换策略槽(slot),以及 Plugin Manifest v1 版本化插件包。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U