3797 字
约 12 分钟
16
8.2 两层结构:Data layer 与 Transport layer
2026-10-25
无标签

8.2 两层结构:Data layer 与 Transport layer

一句话结论:MCP 被明确切成两层——数据层管"说什么"(JSON-RPC 语义),传输层管"怎么送"(stdio / HTTP)。这个分层的好处是:换传输方式不改变协议语义。

读这篇之前

需要先读 8.1(MCP 解决什么问题)。这一篇是 MCP 的技术骨架。


一、官方定义

MCP 由两层组成:[核验:MCP 官方架构]

层 管什么
Data layer(数据层) 定义基于 JSON-RPC 2.0 的客户端-服务端协议,包括能力与版本发现,以及核心原语 tools / resources / prompts / notifications
Transport layer(传输层) 管理客户端与服务端之间的通信通道与认证

这个分层的价值可以用一句话说清:

数据层抽象了"语义",传输层抽象了"通信细节"——所以同一套 JSON-RPC 消息可以跑在不同的传输上。


二、数据层:三个核心原语

数据层定义了三类"服务端能提供的东西":

原语 用途 类比
tools(工具) 让 AI 执行动作 "能力"
resources(资源) 提供上下文数据 "资料"
prompts(提示模板) 提供交互模板 "话术"

tools:和 Function Calling 的对应关系

这是最常用的一个。它和 4.1 讲的 Function Calling 在形态上几乎一致:

  • 有 name、description、参数 schema
  • 调用后返回结果

区别在"谁提供":Function Calling 里工具由你的应用定义;MCP 里工具由独立运行的服务端提供。

resources:为什么需要"数据"这个原语

这原语容易和 tools 混。区别:

tools resources
是否改变状态 可能 不改变
谁发起 模型决定调用 客户端可主动读取
形态 动作 数据(文件、记录、日志)

resources 的价值:把"只读的上下文数据"和"可执行的工具"分开。这样客户端可以在需要时主动拉取数据,而不必让模型专门发一次工具调用。

prompts:模板化交互

服务端可以预置一些"提示模板",客户端选用。这让"这个数据源的最佳提问方式"能被封装起来。

第二个关键:能力与版本发现

数据层还定义了客户端如何查询服务端的协议版本、能力与身份(通过 server/discover 请求)。[核验:官方架构]

这个设计解决了一个实际问题:客户端不需要预先知道服务端支持什么,可以动态发现。


三、传输层:两种机制

MCP 支持两种传输方式:[核验:官方文档]

传输 机制 特点 典型场景
Stdio 标准输入/输出流,直接进程通信 无网络开销,性能最佳 本地进程,通常服务单个客户端
Streamable HTTP HTTP POST + 可选 SSE 流式 支持多客户端、可远程 远程服务,服务多个客户端

选型判据很简单:

你的场景 用哪个
工具和 AI 应用在同一台机器 Stdio(简单、快、无需认证)
工具在服务器上,多客户端共用 Streamable HTTP(需要认证)
需要部署成公共服务 Streamable HTTP

一个具体例子:官方的 Sentry MCP 服务端跑在 Sentry 平台上,用的就是 Streamable HTTP(因为它要服务很多客户端)。


四、为什么这个分层重要

三层好处,从浅到深:

好处一:换传输不改语义

同一套 tools/resources/prompts 定义,可以今天用 stdio 跑在本地,明天改成 HTTP 部署到服务器——协议语义完全不变。

好处二:测试更容易

你可以在本地用 stdio 测试协议逻辑,不需要搭 HTTP 服务。传输的复杂度和业务逻辑解耦。

好处三:安全边界清晰

层 安全问题
数据层 参数校验、权限(谁能调什么工具)
传输层 认证、加密、网络隔离

分开之后,两层的防护可以独立设计。 这是 2.5 讲的"执行层防线"在协议层面的体现。


五、一次完整的调用长什么样

把两层串起来:

① 客户端通过 Transport 建立连接(stdio 或 HTTP)
② 客户端发 server/discover → 得到:协议版本、支持的能力、工具清单
③ 用户在应用里提问
④ 模型决定调用某工具 → 输出 tool_calls(4.1)
⑤ 客户端把 tool_calls 转成 JSON-RPC 请求,经 Transport 发给服务端
⑥ 服务端执行,返回 JSON-RPC 响应
⑦ 客户端把结果作为 tool 消息放回上下文(1.3)
⑧ 模型继续推理

注意第 ⑤⑥ 步:这是 MCP 相对"本地函数调用"多出来的部分——多了一次网络/进程通信,换来了解耦与复用。



六、动手:一次 stdio 调用的完整报文

协议讲得再多,不如看一遍真实报文。下面是一次本地文件系统 MCP Server 的完整交互(简化)。

① 初始化与能力发现

→ {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}

← {"jsonrpc":"2.0","id":1,"result":{
     "protocolVersion":"2026-07-28",
     "capabilities":{"tools":{},"resources":{}},
     "serverInfo":{"name":"filesystem","version":"1.2.0"}
   }}

注意:客户端第一件事不是调工具,而是问对方有什么(8.4 详述)。

② 列出工具

→ {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

← {"jsonrpc":"2.0","id":2,"result":{"tools":[
     {"name":"read_file",
      "description":"读取项目内的文本文件。只读,不修改。",
      "inputSchema":{"type":"object",
        "properties":{"path":{"type":"string","description":"相对项目根目录的路径"}},
        "required":["path"]}}
   ]}}

看 description:它写着"只读,不修改"——这是 4.2 讲的"副作用声明",让模型知道风险。

③ 调用工具

→ {"jsonrpc":"2.0","id":3,"method":"tools/call",
   "params":{"name":"read_file","arguments":{"path":"src/app.ts"}}}

← {"jsonrpc":"2.0","id":3,"result":{
     "content":[{"type":"text","text":"(文件内容)"}],
     "isError":false
   }}

④ 错误怎么返回

← {"jsonrpc":"2.0","id":3,"result":{
     "content":[{"type":"text","text":"文件不存在:src/app.ts。"}],
     "isError":true
   }}

关键设计:工具执行失败用 isError: true 放在 result 里,而不是用 JSON-RPC 的 error 字段。

层 用哪个 例子
协议错误 error 字段 方法不存在、参数格式错
业务错误 result.isError 文件不存在、权限不足

为什么要分开? 因为业务错误要交给模型看(4.3 的失败回填),而协议错误是客户端代码的问题。


七、两种传输的选型:一个具体对比

维度 Stdio Streamable HTTP
启动方式 父进程 spawn 子进程 连接已有服务
延迟 最低(无网络) 有网络往返
多客户端 ❌ 通常单一 ✅ 支持
认证 不需要(同机) 需要
部署 跟着应用走 独立部署
故障影响 进程死则连接断 可重连、可负载均衡
调试 本地直接看 stderr 需要看服务端日志

一个真实的选型判断

你的工具需要访问用户的本地文件吗?
 ├─ 是 → Stdio(只有本机进程能访问用户文件)
 └─ 否 → 会被多个应用共用吗?
      ├─ 是 → Streamable HTTP
      └─ 否 → Stdio 更简单

第一条是硬约束:远程服务看不到用户本地文件,所以本地能力必须用 stdio。


八、分层设计的三个实际收益(展开)

原文提了三点,这里给出具体例子。

收益一:换传输不改语义

一个真实的迁移场景:

阶段一:本地开发,用 stdio,工具逻辑写在 server.ts 里
阶段二:团队共用,改成 HTTP 部署

要改的:只有传输层的启动代码(约 20 行)。
不用改的:所有 tool/resource/prompt 的定义与实现。

如果协议没有分层,你就要重写整个消息处理逻辑——这就是分层的价值。

收益二:测试更容易

用 stdio 测协议逻辑:起一个子进程,喂 JSON,看返回
   ↓ 不需要 HTTP 服务、不需要端口、不需要认证

这让人能在 10 秒内写一个协议层单测,而不是搭一套测试环境。

收益三:安全边界清晰

层 谁负责 具体措施
数据层 工具作者 参数校验、路径白名单(14.4)
传输层 部署者 TLS、认证、网络隔离

分开之后,两层的加固可以独立进行——这是 14.4 讲的"纵深防御"在协议层的体现。


九、动手:写一个最小 MCP Server

知道协议后,最快的学习方式是写一个 60 行的 Server。

骨架(伪代码)

启动 → 从 stdin 逐行读 JSON
     → 解析 method
     → 分发:
         server/discover → 返回 capabilities
         tools/list      → 返回工具定义数组
         tools/call      → 执行 → 返回 result
     → 写到 stdout(每行一个 JSON)

四个实现要点:

要点 说明
一行一个 JSON stdio 用换行分隔消息,不能多行
stdout 只放协议消息 日志必须走 stderr,否则客户端解析失败
不要在 stdout 打印 这是新手最常见的坑
错误也要是合法 JSON 崩溃时返回 JSON-RPC error,不要直接退出

第三个坑的具体表现

错:
  print("debug: 收到请求")     ← 打到 stdout
  print(json.dumps(response))
  → 客户端解析第一行失败

对:
  print("debug: 收到请求", file=sys.stderr)
  print(json.dumps(response))   ← stdout 只有协议消息

这是 stdio 传输最常见的事故,而且现象很误导(客户端报"JSON 解析失败",看起来像协议实现错了)。

一个最小可行的工具实现

tools/call: read_file
  1. 校验 path 是否在白名单目录内(14.4)
  2. 规范化路径(防 ../ 穿越)
  3. 读文件,超长则截断
  4. 返回 content + isError

四步缺一不可——尤其第 1、2 步,缺了就是任意文件读取漏洞。


十、常见误区

误区七:业务错误也应该用 JSON-RPC 的 error。
会让模型看不到可读的错误信息。用 result.isError + 文本。

误区八:stdio 已经过时了,应该都用 HTTP。
本地能力(访问用户文件)只能用 stdio。

误区九:分层只是代码组织。
它决定了"换传输要不要重写"。

误区十:一个 Server 只能有一种传输。
协议允许,实现上需分别处理(多数实现只支持一种)。


原有误区

误区一:MCP 就是 JSON-RPC。
JSON-RPC 只是数据层的编码方式。还有传输层、能力发现、原语定义。

误区二:远程 MCP 一定更好。
远程带来网络延迟与认证复杂度。同机部署用 stdio 更快。

误区三:三层原语里只有 tools 有用。
resources 解决"只读数据供给",prompts 解决"模板复用",各有位置。

误区四:能力发现只是形式。
它让客户端能适配未知服务端,是生态能成立的前提之一。

误区五:分层是学术洁癖。
分层的实际收益是:测试更容易、换传输不改语义、安全边界清晰。

误区六:MCP 调用比本地函数调用快。
会慢(多一次通信)。换来的是解耦与复用,不是性能。


十一、动手:一次真实的 stdio 报文往返

8.2 讲了分层,这里看完整报文,把"数据层"落到眼前。

① 发现能力

{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}

回应:

{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":"2026-07-28",
  "capabilities":{"tools":{},"resources":{}},
  "serverInfo":{"name":"filesystem","version":"1.2.0"}
}}

② 列工具

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

③ 调工具

{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"read_file","arguments":{"path":"src/app.ts"}}}

④ 两种错误怎么区分

错误类型 放哪 例子
协议错误 JSON-RPC error 字段 方法不存在、参数格式非法
业务错误 result.isError: true 文件不存在、权限不足

为什么必须分开? 因为业务错误要交给模型看(4.3 的失败回填),而协议错误是客户端代码的问题。混在一起,模型会看到一堆它读不懂的技术报错。

一个实现细节:一行一个 JSON

stdio 传输用换行分隔消息。所以:

错:print("debug: 收到请求")          ← 打到 stdout,破坏协议流
对:print("debug: 收到请求", file=sys.stderr)

这是 stdio Server 最常见的故障,而且现象很容易误导:客户端报"JSON 解析失败",看起来像协议实现错了。


十二、一个补充:传输层怎么选——决策表

8.2 讲了两种传输。实际选型时用这张表:

你的情况 选
工具要访问用户本地文件 stdio(远程看不到用户磁盘)
工具是多台机器共用的服务 Streamable HTTP
只是自己本机用 stdio(更简单、更快)
需要跨网络、跨组织 Streamable HTTP(要认证)
需要多客户端并发 Streamable HTTP
需要部署到容器/K8s Streamable HTTP

第一行是硬约束:远程服务访问不到用户的本地文件,所以"读本地代码库"这类能力只能用 stdio。

一个混合的常见形态

用户的 IDE 插件
  ├─ stdio → 本地 MCP Server(读项目文件、跑命令)
  └─ HTTP  → 远程 MCP Server(查公司知识库、调内部 API)

两者并存是常态,不冲突。这也是 8.5 讲的"同一 Agent 既是 Client 也是 Server"的基础。


十三、最后一条

传输层可以换,语义不能换。 这就是分层的价值——你为 stdio 写的工具定义,换成 HTTP 后一行都不用改。

判断一个 MCP 实现是否规范:看它换传输时要不要改工具定义。要改,说明分层没做对。


十四、下一篇

数据层定义了三类原语,其中 tools 最常用,但 resources 和 prompts 常被误解。

下一篇专门讲这三个原语——它们各自的语义、适用场景,以及为什么不能混用。

一句补充

分层的成本是一次性的:写对一次,之后换传输几乎零成本。 不分层的成本是持续的:每次传输变更都要重写一遍。

这就是为什么"多写一个抽象层"在协议实现里总是值得的。

记住这一点,就不会把协议写死在某一种传输上。

8.2 两层结构:Data layer 与 Transport layer
https://clxhxhhr.top/posts/4903/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。