云计算百科
云计算领域专业知识百科平台

使用 LangGraph、FastAPI、MCP 与 Docker 构建通用 AI 智能体(06):MCP 原理,工具协议、传输方式与版本边界

第五篇中,我把资料正文放到外部存储,图中保存版本引用,每次规划前重新组装有效上下文。到这里,智能体已经有了执行循环,也能管理证据,但工具仍然是项目里的本地函数。

当读取文档、搜索资料与保存报告分别由不同服务提供时,我需要重复解决几件事:服务有哪些工具,参数是什么,怎样调用,失败如何返回,以及连接何时关闭。如果每个服务都定义一套格式,Host 的适配代码就会随着工具数量一起增长。

MCP,即 Model Context Protocol,为这些交互提供统一协议。本篇先讲清协议边界,再通过真实 Python SDK 的客户端与服务端验证发现和调用。它并不替应用决定下一步,也不自动让所有工具获得执行权限。

本篇代码使用人工编写的固定教学笔记,不读真实文件,不联网搜索,不调用模型。完整源码与运行命令都在文内,另有 06_mcp_protocol/ 运行副本。

资料核对与本地验证日期:2026-11-06。分别核对 2025-11-25 与 2026-07-28 规范,使用 SDK 1.30.0 与 2.3.0 的独立环境验证,不能把这两个版本的流程混用。

一、MCP 位于模型与工具之间的什么位置

我把一个模型提出的工具调用分成两段理解。

第一段是模型接口:模型收到工具名称、描述和参数 Schema 后,返回准备调用哪个工具、使用什么参数。第二段是程序执行:应用校验调用,找到对应服务,把参数交给工具,接收结果,再按模型接口要求回传。

Function Calling 主要属于第一段,MCP 主要解决第二段中 Client 与 Server 的标准交互。Host 仍需把 MCP 工具描述转换为模型供应商接受的格式,并把模型返回的调用映射为 tools/call。MCP 的工具结果也不能不经适配直接冒充模型消息。资料 1

#mermaid-svg-OZLAzLXgdb2xSk8O{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OZLAzLXgdb2xSk8O .error-icon{fill:#552222;}#mermaid-svg-OZLAzLXgdb2xSk8O .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OZLAzLXgdb2xSk8O .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OZLAzLXgdb2xSk8O .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OZLAzLXgdb2xSk8O .marker.cross{stroke:#333333;}#mermaid-svg-OZLAzLXgdb2xSk8O svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OZLAzLXgdb2xSk8O p{margin:0;}#mermaid-svg-OZLAzLXgdb2xSk8O .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-OZLAzLXgdb2xSk8O .cluster-label text{fill:#333;}#mermaid-svg-OZLAzLXgdb2xSk8O .cluster-label span{color:#333;}#mermaid-svg-OZLAzLXgdb2xSk8O .cluster-label span p{background-color:transparent;}#mermaid-svg-OZLAzLXgdb2xSk8O .label text,#mermaid-svg-OZLAzLXgdb2xSk8O span{fill:#333;color:#333;}#mermaid-svg-OZLAzLXgdb2xSk8O .node rect,#mermaid-svg-OZLAzLXgdb2xSk8O .node circle,#mermaid-svg-OZLAzLXgdb2xSk8O .node ellipse,#mermaid-svg-OZLAzLXgdb2xSk8O .node polygon,#mermaid-svg-OZLAzLXgdb2xSk8O .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OZLAzLXgdb2xSk8O .rough-node .label text,#mermaid-svg-OZLAzLXgdb2xSk8O .node .label text,#mermaid-svg-OZLAzLXgdb2xSk8O .image-shape .label,#mermaid-svg-OZLAzLXgdb2xSk8O .icon-shape .label{text-anchor:middle;}#mermaid-svg-OZLAzLXgdb2xSk8O .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OZLAzLXgdb2xSk8O .rough-node .label,#mermaid-svg-OZLAzLXgdb2xSk8O .node .label,#mermaid-svg-OZLAzLXgdb2xSk8O .image-shape .label,#mermaid-svg-OZLAzLXgdb2xSk8O .icon-shape .label{text-align:center;}#mermaid-svg-OZLAzLXgdb2xSk8O .node.clickable{cursor:pointer;}#mermaid-svg-OZLAzLXgdb2xSk8O .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OZLAzLXgdb2xSk8O .arrowheadPath{fill:#333333;}#mermaid-svg-OZLAzLXgdb2xSk8O .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OZLAzLXgdb2xSk8O .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OZLAzLXgdb2xSk8O .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OZLAzLXgdb2xSk8O .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OZLAzLXgdb2xSk8O .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OZLAzLXgdb2xSk8O .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OZLAzLXgdb2xSk8O .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OZLAzLXgdb2xSk8O .cluster text{fill:#333;}#mermaid-svg-OZLAzLXgdb2xSk8O .cluster span{color:#333;}#mermaid-svg-OZLAzLXgdb2xSk8O div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OZLAzLXgdb2xSk8O .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OZLAzLXgdb2xSk8O rect.text{fill:none;stroke-width:0;}#mermaid-svg-OZLAzLXgdb2xSk8O .icon-shape,#mermaid-svg-OZLAzLXgdb2xSk8O .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OZLAzLXgdb2xSk8O .icon-shape p,#mermaid-svg-OZLAzLXgdb2xSk8O .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OZLAzLXgdb2xSk8O .icon-shape .label rect,#mermaid-svg-OZLAzLXgdb2xSk8O .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OZLAzLXgdb2xSk8O .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OZLAzLXgdb2xSk8O .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OZLAzLXgdb2xSk8O :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

用户目标

Host:图、策略、上下文与模型适配

模型:提出工具调用

程序:名称、参数、权限、预算与确认

MCP Client:发现与调用

MCP Server:工具实现

结果校验、外存与证据引用

模型接口中的工具调用 ID,用于匹配工具结果消息。MCP JSON-RPC 请求 ID,用于匹配 Client 与 Server 的请求和响应。二者位于不同接口,应用应建立关联,不能认为它们天然是同一个标识。

同理,业务 task_id、LangGraph 检查点 thread_id 和旧版 MCP HTTP 会话 ID 也不是一回事。后续可以把它们记录在执行日志中,但不能用一个传输会话标识证明任务归属。

二、Host、Client 与 Server 的分工

角色本系列中的对应部分主要职责
Host 智能体应用 管理模型、图、上下文、用户授权与工具策略
Client Host 内的 MCP 接入组件 面向配置的 Server 执行协议交互与连接管理
Server 提供资料或文件能力的程序 暴露工具、资源、模板,并执行服务端业务校验

Host 可以为多个 Server 创建各自的 Client。Client 是应用中的组件,不一定是另一个独立服务;Server 则可以作为本地子进程运行,也可以通过 HTTP 提供远程能力。资料 1

工具从项目中移到服务端后,校验仍然有两层。Host 判断本次任务能否使用这个工具,Server 判断调用方能否访问具体资源。即使 Host 已检查路径,服务端也应校验自己的最终路径与访问范围。

“发现了工具”只代表程序拿到了描述,不代表已经授权模型调用。工具描述和行为注解来自服务端,需要按信任边界处理;readOnlyHint 等注解不能替代执行策略。资料 2

这个区分也解释了第五篇记忆模块怎样继续发挥作用:工具结果先校验、写入外存并生成引用,然后由上下文组装器选取片段。接入 MCP 不会自动解决结果过大、旧摘要或跨用户读取。

三、Tools、Resources 与 Prompts

能力表达什么常见方法本系列的应用方向
Tools 可执行操作 tools/list、tools/call 搜索资料、读取文档、保存报告
Resources 可读取的数据资源 resources/list、resources/read 固定文档、资料说明、已有产物
Prompts 可复用的提示模板 prompts/list、prompts/get 报告结构或任务引导模板

官方概念通常把 Tools 描述为模型控制、Resources 描述为应用控制、Prompts 描述为用户控制。这是推荐的交互组织方式,协议并不强制应用必须采用某一种 UI。资料 1资料 2

读取同一份资料,可以被设计成按 URI 读取的 Resource,也可以被设计成带过滤条件的 Tool。我会按业务契约选择,而不是仅凭“读取”两个字分类。Resource URI 也不是访问许可,读取时仍要验证身份与范围。

prompts/get 返回模板消息,不表示这些消息已经执行模型调用。工具返回的 structuredContent 是服务端产出的结构化数据,也不同于模型的 Structured Outputs。客户端发现某个能力后,还要确认具体方法、参数和结果形式是否受支持。

本篇只实现一个 Tool,用它观察协议。资料服务、Resources、Prompts 与模型适配留到后续扩展,避免第一次接入就同时引入过多业务。

四、请求、结果与错误怎样对应

MCP 使用 JSON-RPC 2.0。请求包含 id、method 与可选参数,响应使用相同 ID 返回结果或错误。通知没有 ID,也不应收到单独的响应。字符串中的换行需要 JSON 转义;stdio 的消息边界本身使用换行分隔。资料 3资料 6

例如,一个 tools/call 会携带工具名称与参数对象。对于 2026-07-28,下面是包含必要元数据的完整请求示意:

{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "read_note",
"arguments": {"source_id": "control_flow"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "example-client",
"version": "1.0.0"
}
}
}
}

clientInfo 用来描述客户端实现,不是经过认证的用户身份。租户和用户范围必须来自可信凭据或服务配置,不能因为客户端把名字写进 _meta 就信任它。

工具调用失败需要分开处理。协议层错误,例如不支持的方法或版本,进入 JSON-RPC error;工具运行中的业务失败可以进入工具结果,并设置 isError: true。因此 HTTP 成功返回,甚至收到了 result,都不能直接推导出业务成功。资料 2

工具结果中的 content 可以包含文本、图片、资源链接等多种块,不能假定永远只有一段字符串。本例返回文本块,块内是教学函数返回字典的 JSON;它没有声明输出 Schema,也没有返回 structuredContent。

新版结果使用 resultType 区分完成与需要补充输入等状态。complete 表示请求已完成,不代表业务必然成功,还要看 isError。input_required 表示需要按 MRTR,即多轮请求模式,收集输入后再次请求;它不是可直接写入最终报告的资料。资料 3

五、初始化握手与逐请求元数据是两套流程

5.1 2025-11-25:先初始化,再运行

旧版 Client 先发送 initialize,包含支持的协议版本、Client 能力和实现信息。Server 返回采用的版本、自己的能力与实现信息。Client 检查兼容性后发送 notifications/initialized,再进入正常操作。资料 4

Server:旧版或兼容实现

Client:2025-11-25

Server:旧版或兼容实现

Client:2025-11-25

#mermaid-svg-Z6rt3BuNBnERwsTy{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z6rt3BuNBnERwsTy .error-icon{fill:#552222;}#mermaid-svg-Z6rt3BuNBnERwsTy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z6rt3BuNBnERwsTy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z6rt3BuNBnERwsTy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z6rt3BuNBnERwsTy .marker.cross{stroke:#333333;}#mermaid-svg-Z6rt3BuNBnERwsTy svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z6rt3BuNBnERwsTy p{margin:0;}#mermaid-svg-Z6rt3BuNBnERwsTy .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z6rt3BuNBnERwsTy text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Z6rt3BuNBnERwsTy .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Z6rt3BuNBnERwsTy .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Z6rt3BuNBnERwsTy .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Z6rt3BuNBnERwsTy .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Z6rt3BuNBnERwsTy #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Z6rt3BuNBnERwsTy .sequenceNumber{fill:white;}#mermaid-svg-Z6rt3BuNBnERwsTy #sequencenumber{fill:#333;}#mermaid-svg-Z6rt3BuNBnERwsTy #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Z6rt3BuNBnERwsTy .messageText{fill:#333;stroke:none;}#mermaid-svg-Z6rt3BuNBnERwsTy .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z6rt3BuNBnERwsTy .labelText,#mermaid-svg-Z6rt3BuNBnERwsTy .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Z6rt3BuNBnERwsTy .loopText,#mermaid-svg-Z6rt3BuNBnERwsTy .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Z6rt3BuNBnERwsTy .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Z6rt3BuNBnERwsTy .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Z6rt3BuNBnERwsTy .noteText,#mermaid-svg-Z6rt3BuNBnERwsTy .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Z6rt3BuNBnERwsTy .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z6rt3BuNBnERwsTy .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z6rt3BuNBnERwsTy .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z6rt3BuNBnERwsTy .actorPopupMenu{position:absolute;}#mermaid-svg-Z6rt3BuNBnERwsTy .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Z6rt3BuNBnERwsTy .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z6rt3BuNBnERwsTy .actor-man circle,#mermaid-svg-Z6rt3BuNBnERwsTy line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Z6rt3BuNBnERwsTy :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

initialize(版本、能力、clientInfo)

InitializeResult(版本、能力、serverInfo)

notifications/initialized

tools/list

工具描述与可选分页游标

tools/call

工具结果或 JSON-RPC 错误

如果 Server 不支持请求中的版本,会返回它支持的另一个版本;Client 不支持响应版本时应断开。本篇旧版 SDK 的 session.initialize() 封装了这段流程,无需调用方再重复发送 initialized 通知。

5.2 2026-07-28:每个请求声明协议信息

新版没有初始化协商握手。每个请求都在 params._meta 中声明协议版本与相关 Client 能力,Server 独立处理请求。clientInfo 是可选实现信息。Server 必须实现 server/discover,但纯新版 Client 不必先发现才能发起其他请求。资料 3资料 5

Server:新版处理路径

Client:2026-07-28

Server:新版处理路径

Client:2026-07-28

#mermaid-svg-4ggtckYaiU4zYsnx{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4ggtckYaiU4zYsnx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4ggtckYaiU4zYsnx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4ggtckYaiU4zYsnx .error-icon{fill:#552222;}#mermaid-svg-4ggtckYaiU4zYsnx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4ggtckYaiU4zYsnx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4ggtckYaiU4zYsnx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4ggtckYaiU4zYsnx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4ggtckYaiU4zYsnx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4ggtckYaiU4zYsnx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4ggtckYaiU4zYsnx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4ggtckYaiU4zYsnx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4ggtckYaiU4zYsnx .marker.cross{stroke:#333333;}#mermaid-svg-4ggtckYaiU4zYsnx svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4ggtckYaiU4zYsnx p{margin:0;}#mermaid-svg-4ggtckYaiU4zYsnx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4ggtckYaiU4zYsnx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4ggtckYaiU4zYsnx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4ggtckYaiU4zYsnx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4ggtckYaiU4zYsnx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4ggtckYaiU4zYsnx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4ggtckYaiU4zYsnx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4ggtckYaiU4zYsnx .sequenceNumber{fill:white;}#mermaid-svg-4ggtckYaiU4zYsnx #sequencenumber{fill:#333;}#mermaid-svg-4ggtckYaiU4zYsnx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4ggtckYaiU4zYsnx .messageText{fill:#333;stroke:none;}#mermaid-svg-4ggtckYaiU4zYsnx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4ggtckYaiU4zYsnx .labelText,#mermaid-svg-4ggtckYaiU4zYsnx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4ggtckYaiU4zYsnx .loopText,#mermaid-svg-4ggtckYaiU4zYsnx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4ggtckYaiU4zYsnx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4ggtckYaiU4zYsnx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4ggtckYaiU4zYsnx .noteText,#mermaid-svg-4ggtckYaiU4zYsnx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4ggtckYaiU4zYsnx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4ggtckYaiU4zYsnx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4ggtckYaiU4zYsnx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4ggtckYaiU4zYsnx .actorPopupMenu{position:absolute;}#mermaid-svg-4ggtckYaiU4zYsnx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4ggtckYaiU4zYsnx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4ggtckYaiU4zYsnx .actor-man circle,#mermaid-svg-4ggtckYaiU4zYsnx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4ggtckYaiU4zYsnx :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

server/discover(逐请求 _meta,可选发现)

supportedVersions、能力与实现信息

tools/list(本次请求 _meta)

resultType=complete,工具列表

tools/call(参数与本次请求 _meta)

complete / input_required / error

收到 UnsupportedProtocolVersionError,错误码 -32022,表示对端认识新版交互但不接受该版本。Client 应从返回的支持列表选择共同版本,不能看到错误就直接退回旧版握手。资料 5

支持新旧两代的 Client,在 stdio 上可以先用 server/discover 探测。收到新版结果或可识别的新版错误就继续新版处理;其他错误或合理超时才按规范回退。旧 Server 的未知方法错误不一定固定为某一个错误码。

新版协议的“无状态”是请求处理语义:必要信息不依赖先前连接交互。它不表示服务端不能使用数据库、缓存或长期任务;需要跨请求保留的业务状态,应通过显式标识关联。stdio 子进程与连接也不应直接代表某一次业务会话。

六、stdio、Streamable HTTP 与旧 HTTP+SSE

6.1 stdio:子进程的标准流

Client 启动 Server 子进程,用 stdin 发送消息,从 stdout 读取消息。消息按行分隔,日志写 stderr。一个普通 print() 如果写到 Server 的 stdout,就可能破坏协议帧。资料 6

stdio 适合先验证本地工具服务,但不自动构成权限沙箱。子进程具有什么文件和网络权限,仍由启动环境及操作系统约束。关闭时先关闭输入,等待退出,必要时再终止进程。

6.2 Streamable HTTP:不要只记住“支持 SSE”

项目2025-11-252026-07-28
主要消息入口 单一 MCP 路径,POST 发消息 单一 MCP 路径,每条请求单独 POST
初始化 initialize 握手 逐请求元数据,没有该握手
返回形式 JSON 或 SSE JSON 或请求范围的 SSE
GET 流 可用 GET 打开;不提供时返回 405 移除 GET 流,订阅通过 subscriptions/listen 请求
协议会话 可分配 MCP-Session-Id 移除协议层会话
断流与取消 断开本身不应视为取消 关闭对应响应流是该请求的取消信号
SSE 重连 可选事件重放与 Last-Event-ID 不支持该恢复机制

表中的差别来自对应版本的传输规范,不能把旧版恢复与会话机制直接套到新版。资料 7资料 8

新版 HTTP 请求除了 JSON-RPC 正文,还需要携带 MCP-Protocol-Version、Mcp-Method,调用工具时携带对应的 Mcp-Name。正文是元数据的依据,镜像到头中的值必须一致。Accept 要声明同时支持 application/json 与 text/event-stream,只实现 JSON 接收的通用客户端不足以覆盖规范。资料 8

SSE 是消息承载方式,不等于模型 token 流。这里的 SSE 数据可以是 JSON-RPC 进度通知、订阅通知或最终响应。以后 FastAPI 向浏览器发送的任务事件流,会有自己的事件 ID、鉴权与重连契约,不能与 MCP 传输混为一谈。

新版断流的协议语义也不等于外部写入已经撤销。如果保存动作已经提交,取消不能让它自动消失;副作用幂等与结果未知留到第十二篇处理。

6.3 旧 HTTP+SSE 是另一种布局

2024-11-05 的 HTTP+SSE 使用 SSE 接收入口和 POST 发送入口。Client 先 GET 建立 SSE 连接,Server 用 endpoint 事件告知后续 POST 的地址。Streamable HTTP 则在单一 MCP 路径处理消息,返回可以是 JSON 或 SSE。资料 9

因此,看到 text/event-stream 无法判断正在使用哪一种传输。需要同时检查协议版本、入口方法和握手流程。本篇不实现旧 HTTP+SSE 自动回退,也没有验证其互通。

七、SDK 版本与协议日期要分别记录

协议日期描述消息约定,SDK 版本描述某一语言实现。安装了最新 SDK,不代表所有连接都会使用最新协议。

本地检查与运行得到:

Python SDK版本注册表本例客户端入口本例服务端入口
mcp==1.30.0 从 2024-11-05 到 2025-11-25 的四个版本 ClientSession.initialize() FastMCP
mcp==2.3.0 上述四个版本,加 2026-07-28 Client,默认 mode="auto" MCPServer

注册表说明实现声明的范围,不等于本篇实测了每个日期、每种传输。这里实际使用的是 2025-11-25 和 2026-07-28,验证 stdio 互通与新版 HTTP 调用。

2.x 的迁移还涉及 Python 字段名改为 snake_case、FastMCP 改名为 MCPServer、传输配置移动,以及 HTTP 客户端依赖从 httpx 变为 httpx2。本篇使用 model_dump(by_alias=True) 输出协议字段,所以仍能看到 inputSchema、isError 等线上的名称。资料 10

还有一个容易误判的细节:新版 SDK 解析旧结果时可以补上默认 resultType="complete"。打印出的 SDK 对象不是原始网络报文,不能仅凭这个字段判断 Server 在线上使用了新版协议。本地验证因此另外检查了原始 stdio 消息。

八、完整实现:观察发现与调用

将下面七个文件保存在同一个目录。两个依赖文件用于两个独立虚拟环境,不能安装到同一个环境中覆盖彼此。本地使用 Python 3.13.13,示例入口使用 Python 3.11 及以上的 asyncio.timeout()。

06_mcp_protocol/
├── teaching_notes.py
├── modern_server.py
├── legacy_server.py
├── modern_client.py
├── legacy_client.py
├── requirements-modern.txt
└── requirements-legacy.txt

8.1 teaching_notes.py:共享的只读教学函数

"""固定教学笔记,只用于验证协议;不执行文件或网络访问。"""

def read_note(source_id: str) –> dict:
"""读取固定教学笔记,当前只接受 source_id=control_flow。"""
if not isinstance(source_id, str) or not 1 <= len(source_id) <= 64:
raise ValueError("来源标识必须是 1—64 字符的字符串。")
if source_id != "control_flow":
raise ValueError("教学来源不存在。")
return {
"source_id": source_id, "revision": 1,
"content": "教学笔记:计划决定下一步,程序检查预算,观察用于更新证据缺口。",
"fixture": True,
}

8.2 modern_server.py:新版与兼容服务端

"""MCP SDK 2.3.0 服务端;支持新版与兼容旧版连接。"""

import argparse

from mcp.server.mcpserver import MCPServer

from teaching_notes import read_note

server = MCPServer("protocol-notes", version="1.0.0")
server.tool()(read_note)

def main() –> None:
parser = argparse.ArgumentParser()
parser.add_argument("–transport", choices=["stdio", "streamable-http"], default="stdio")
parser.add_argument("–port", type=int, default=8766)
args = parser.parse_args()
if args.transport == "stdio":
# stdout 是协议通道,不在这里打印业务日志。
server.run(transport="stdio")
else:
server.run(transport="streamable-http", host="127.0.0.1", port=args.port)

if __name__ == "__main__":
main()

8.3 legacy_server.py:旧版服务端

"""MCP SDK 1.30.0 服务端,使用初始化握手。"""

from mcp.server.fastmcp import FastMCP

from teaching_notes import read_note

server = FastMCP("protocol-notes")
server.tool()(read_note)

if __name__ == "__main__":
server.run(transport="stdio")

8.4 modern_client.py:自动发现与版本回退

"""SDK 2.3.0 客户端:自动识别协议年代,确定性调用只读工具。"""

import argparse
import asyncio
import json
import sys
from importlib.metadata import version
from pathlib import Path

from mcp import Client, StdioServerParameters

async def inspect_server(server, source_id: str) –> dict:
# 一次连接中复用客户端;超时退出后由上下文管理器释放传输。
async with Client(server, read_timeout_seconds=10) as client:
if client.server_capabilities.tools is None:
raise RuntimeError("服务端未声明 tools 能力。")
tools, cursor = [], None
for _ in range(4):
page = await client.list_tools(cursor=cursor)
tools.extend(page.tools)
cursor = page.next_cursor
if cursor is None:
break
if cursor is not None:
raise RuntimeError("工具列表超过教学页数预算。")
matching = [tool for tool in tools if tool.name == "read_note"]
if len(matching) != 1:
raise RuntimeError("允许的只读工具不存在或名称重复。")
tool = matching[0]
# 教学客户端只调用固定允许的工具,没有让模型选择动作。
result = await client.call_tool("read_note", {"source_id": source_id})
return {
"sdk_version": version("mcp"), "protocol_version": client.protocol_version,
"tool": tool.model_dump(by_alias=True, exclude_none=True),
"result": result.model_dump(by_alias=True, exclude_none=True),
}

async def run(args) –> dict:
if args.url:
server = args.url
else:
server_file = args.server_file or str(Path(__file__).with_name("modern_server.py"))
server = StdioServerParameters(command=args.server_python, args=[server_file])
async with asyncio.timeout(20):
return await inspect_server(server, args.source_id)

def main() –> None:
parser = argparse.ArgumentParser()
parser.add_argument("–url", help="例如 http://127.0.0.1:8766/mcp")
parser.add_argument("–server-python", default=sys.executable)
parser.add_argument("–server-file")
parser.add_argument("–source-id", default="control_flow")
result = asyncio.run(run(parser.parse_args()))
print(json.dumps(result, ensure_ascii=False))

if __name__ == "__main__":
main()

8.5 legacy_client.py:显式初始化

"""SDK 1.30.0 客户端:初始化、发现和调用,不尝试新版发现流程。"""

import argparse
import asyncio
import json
import sys
from datetime import timedelta
from importlib.metadata import version
from pathlib import Path

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def run(args) –> dict:
server_file = args.server_file or str(Path(__file__).with_name("legacy_server.py"))
parameters = StdioServerParameters(command=args.server_python, args=[server_file])
async with asyncio.timeout(20):
async with stdio_client(parameters) as (read, write):
async with ClientSession(
read, write, read_timeout_seconds=timedelta(seconds=10),
) as session:
# initialize() 同时完成版本、能力交换与 initialized 通知。
initialized = await session.initialize()
if initialized.capabilities.tools is None:
raise RuntimeError("服务端未声明 tools 能力。")
tools, cursor = [], None
for _ in range(4):
page = await session.list_tools(cursor=cursor)
tools.extend(page.tools)
cursor = page.nextCursor
if cursor is None:
break
if cursor is not None:
raise RuntimeError("工具列表超过教学页数预算。")
matching = [tool for tool in tools if tool.name == "read_note"]
if len(matching) != 1:
raise RuntimeError("允许的只读工具不存在或名称重复。")
result = await session.call_tool("read_note", {"source_id": args.source_id})
return {
"sdk_version": version("mcp"),
"protocol_version": initialized.protocolVersion,
"tool": matching[0].model_dump(by_alias=True, exclude_none=True),
"result": result.model_dump(by_alias=True, exclude_none=True),
}

def main() –> None:
parser = argparse.ArgumentParser()
parser.add_argument("–server-python", default=sys.executable)
parser.add_argument("–server-file")
parser.add_argument("–source-id", default="control_flow")
result = asyncio.run(run(parser.parse_args()))
print(json.dumps(result, ensure_ascii=False))

if __name__ == "__main__":
main()

8.6 requirements-modern.txt:新版环境

mcp==2.3.0

8.7 requirements-legacy.txt:旧版环境

mcp==1.30.0

客户端复用同一连接完成能力检查、分页发现与调用,限制每次读取等待时间和总运行时间。页数预算不是完整的响应体大小限制;这段程序适用于固定教学 Server。接入不受信任的服务时,仍需增加工具数量、Schema 复杂度与消息体预算,不能直接把收到的列表全部发给模型。

这些 CLI 参数是可信本地运行配置。服务端路径、Python 解释器和 HTTP URL 不应开放给模型任意选择。HTTP 示例仅绑定回环地址,没有生产鉴权,不应直接暴露为公网服务。

九、运行步骤与实际兼容结果

在保存七个文件的目录打开终端:

python3 -m venv .venv-modern
python3 -m venv .venv-legacy
.venv-modern/bin/python -m pip install -r requirements-modern.txt
.venv-legacy/bin/python -m pip install -r requirements-legacy.txt

分别运行同版本组合:

.venv-modern/bin/python modern_client.py
.venv-legacy/bin/python legacy_client.py

再运行跨版本组合:

.venv-modern/bin/python modern_client.py \\
–server-python .venv-legacy/bin/python \\
–server-file legacy_server.py

.venv-legacy/bin/python legacy_client.py \\
–server-python .venv-modern/bin/python \\
–server-file modern_server.py

ClientServer本地 stdio 结果实际协议
SDK 2.3.0 SDK 2.3.0 的本例 Server 成功发现并调用 2026-07-28
SDK 1.30.0 SDK 1.30.0 的本例 Server 成功初始化并调用 2025-11-25
SDK 2.3.0,自动模式 SDK 1.30.0 的本例 Server 探测后回退成功 2025-11-25
SDK 1.30.0 SDK 2.3.0 的兼容 Server 按旧版路径成功 2025-11-25

最后一行验证的是支持旧版行为的 Server,不能推导旧 Client 能连接只实现新版的 Server。新版 Client 连旧 Server 时,stderr 可能出现旧 Server 不认识 server/discover 的校验日志;应结合最终协议版本、结果与退出码判断回退是否成功。

结果 JSON 中,tool 是实际发现的工具描述,result.content 包含教学笔记。用不存在的来源检查业务失败:

.venv-modern/bin/python modern_client.py –source-id missing
.venv-legacy/bin/python legacy_client.py –source-id missing

两种 SDK 的本例 Server 都返回 isError: true。示例客户端负责展示结果,所以这个业务失败仍可正常退出;实际 Host 应把该字段转换为失败观察,不能仅以进程退出码判断任务完成。

HTTP 需要两个终端。在第一个终端启动:

.venv-modern/bin/python modern_server.py \\
–transport streamable-http –port 8766

第二个终端调用:

.venv-modern/bin/python modern_client.py –url http://127.0.0.1:8766/mcp

调用完成后,在服务端终端使用 Ctrl+C 停止服务。若端口已占用,修改 –port 并同步修改 URL。此处 Server 独立运行,HTTP Client 退出不会替调用方结束服务进程。

本地验证共完成 16 组集成检查:四组 SDK 互通、两种业务错误返回、原始新版 stdio 发现及调用、版本错误支持列表、子进程释放、新版 HTTP 调用与响应类型,以及三条 HTTP 头校验路径。原始正常 HTTP 调用实际返回 application/json,本篇未验证 SSE 进度、订阅、断流取消或重连。

额外 HTTP 检查观察到:新版头配旧版正文被新版路径以 400 / -32020 拒绝;缺少 Mcp-Method 也返回 400 / -32020。反过来,旧版头配新版正文,在本例兼容实现中先进入旧会话路径,返回 400 / -32600,提示缺少会话 ID。这是具体实现的路由诊断结果,不应简化成“任何版本冲突都返回同一个错误码”。

这轮使用真实 SDK、真实子进程和本机 HTTP,没有用协议替身代替传输。Python 为 3.13.13,两个环境的 Pydantic 为 2.14.3、AnyIO 为 4.16.0;旧环境 HTTPX 为 0.28.1,新环境 HTTPX2 为 2.16.0。依赖文件锁定 SDK 版本,传递依赖可能随安装时间变化。

这些结果不证明真实模型能够选对工具,也不证明所有外部 Server 都兼容。生产认证、路径权限、消息限额、网络故障、持久任务与写入幂等均不在本轮验收范围。

十、接入已有智能体时保留哪些边界

接入第四篇图时,我会让工具执行节点调用 MCP 适配器,把协议层结果转为既有观察结构,而不让每个节点都直接操作传输。读取型结果校验后交给第五篇外存,状态继续保存版本引用。工具发现和连接管理按服务生命周期组织,不为每轮规划重复启动 Server。

工具注册使用受信任的服务配置标识与工具名建立映射,避免不同 Server 的同名 search 相互覆盖。Server 自报的名称不保证全局唯一,不能单独作为消歧或授权依据。资料 2

下一篇将把这层接入落实为资料与文档工具服务:定义输入输出、适配工具描述、验证路径与参数,并处理工具失败和连接生命周期。读者此时需要记住的工程约束是:先明确正在使用哪一版协议,再选择匹配的 SDK API 和传输行为。

参考资料

以下协议结论来自对应版本的官方规范;兼容结果来自本文所列 SDK 的本地运行,不将“规范要求”写成“所有实现均已支持”。

  • MCP Architecture,2026-07-28:角色、分层、能力与逐请求模型。
  • MCP Tools,2026-07-28:工具发现、调用、结果、名称与信任边界。
  • MCP Base Protocol,2026-07-28:消息类型、结果类型、无状态与必要元数据。
  • MCP Lifecycle,2025-11-25:旧版初始化、版本与能力交换。
  • MCP Versioning,2026-07-28:逐请求版本与新旧兼容策略。
  • MCP stdio,2026-07-28:按行消息、取消与子进程生命周期。
  • MCP Transports,2025-11-25:旧版 Streamable HTTP、会话与事件恢复。
  • MCP Streamable HTTP,2026-07-28:新版请求、头字段、订阅与取消。
  • MCP Transports,2024-11-05:旧 HTTP+SSE 的两个入口与 endpoint 事件。
  • Python SDK v1 → v2 Migration:服务端改名、客户端自动模式、字段和依赖迁移。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 使用 LangGraph、FastAPI、MCP 与 Docker 构建通用 AI 智能体(06):MCP 原理,工具协议、传输方式与版本边界
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!