跳到主要内容
Cowers://
全部文章
协议与通信

MCP 协议原理

MCP = Model Context Protocol(模型上下文协议),由 Anthropic 提出的开放标准。

MCP 协议原理:模型和工具之间那条标准化的线

阅读提示 本笔记面向已经理解 LLM、Prompt 和 Tool Calling(工具调用)、正在学 Agent 工程的读者。主线是:MCP 到底替代了什么 → 双方各暴露哪些能力 → 一次连接内部发生了什么 → 传输和授权怎么选 → LangChain 怎么把它接进来

📌 依据 2025-11-25 版规范specification)。MCP 迭代很快,网上大量教程停留在 2024 年的早期视角,遇到与本笔记不一致的说法,先确认它写的是哪个版本。

前置概念见 Agent概念;本篇只讲协议本身,动手写 Server 和封装企业 API 见 MCP 实战:从 Server 到企业 API 封装

需要注意的边界:MCP 解决的是「模型 ↔ 工具」,不解决「Agent ↔ Agent」,后者是另一套协议,见 A2A Agent 间通信协议

📅 2026-07-28 规范已发布,本篇讲的连接模型被大改(核验于 2026-08-13) 本篇仍按 2025-11-25 版写——那是目前绝大多数 SDK、客户端和线上 Server 实际跑的版本,作为入门理解依然成立。但 2026-07-28 版changelog)做了结构性改动,下面第三节「连接生命周期」受影响最大。开新项目前必须知道这几条:

本篇讲的(2025-11-25) 2026-07-28 改成了
initialize + notifications/initialized 握手,会话内协商一次 握手整个移除,协议变成无状态。协议版本和客户端能力改为每个请求通过 _meta 携带(io.modelcontextprotocol/protocolVersion 等)
协议级 session(Mcp-Session-Id 头) 移除。需要跨调用状态的 Server 改为自己签发句柄,当成普通工具参数传
靠握手了解对方能力 新增 server/discover,Server 必须实现,用来公布支持的协议版本、能力和身份
Server 主动发请求(sampling/createMessageelicitation/createroots/list 换成 MRTR(Multi Round-Trip Requests):Server 返回 resultType: "input_required"inputRequests,Client 重试原请求并带上 inputResponses。且 Roots / Sampling / Logging 三项已进入弃用窗口
resources/subscribe + 独立的 HTTP GET 通知流 合并成 subscriptions/listen 单条长连接,按类型显式订阅
Tasks 是核心协议里的实验特性 移出核心,改为官方扩展 io.modelcontextprotocol/taskstasks/result 换成轮询 tasks/gettasks/list 取消
pinglogging/setLevelnotifications/roots/list_changed 全部移除
OAuth 动态客户端注册(DCR)是推荐机制 DCR 已弃用,改推 Client ID Metadata Documents

哪些结论没变: 三个角色(Host/Client/Server)、Host 握有授权决策、模型不在连接上、JSON-RPC 2.0 报文形态、Tool/Resource/Prompt 三分、只有 Tool 有 inputSchema、分页、stdio 与 Streamable HTTP 两种标准传输、禁止 token passthrough、Origin 校验与只绑 127.0.0.1——本篇的主要教学价值都在这些不变的部分

读到下面第三节的 initialize 三阶段时,记住那是 2025-11-25 的形态;对照新版时把它理解成「能力协商从一次握手摊到了每个请求上」。

目录


核心概念 MCP = Model Context Protocol(模型上下文协议),由 Anthropic 提出的开放标准。

一句话:它把「模型调用外部工具」这件事,从各家框架各写一套,统一成一套基于 JSON-RPC 2.0 的客户端-服务器协议。工具提供方写一个 MCP Server,所有兼容 MCP 的模型和客户端都能连。

一、MCP 要解决的到底是什么问题?

先说清楚它不是什么:MCP 不是让模型「获得了调用工具的能力」——那个能力叫 Function Calling,模型本身就有。MCP 解决的是它上面一层的问题:同一个工具,凭什么要为每个模型、每个框架重写一遍接入代码。

官方类比是 USB-C:以前 Mini-USB、Micro-USB、Lightning 各插各的,现在一个口通吃。对应到 AI:以前 OpenAI 用 tools 参数、Anthropic 用 tool_use、每个框架又包一层自己的 Tool 抽象;现在工具方只写一次 MCP Server。

MCP 三层架构与协议分工

图从下往上读:底层 Function Calling 让模型「会说要调什么」,中间 MCP 让工具「一次编写到处可连」,最上面 Skill 是编排——决定多个工具按什么顺序组合。三层各解决一个问题,缺了 MCP 这层,程序照样能跑,只是每换一个模型就要重接一遍。

生活化对照能帮着记住边界:

比喻 缺了它会怎样
Function Calling 你「会打电话」这个能力 模型根本没法调用外部函数
MCP 统一的电话网络标准 能打,但每换个运营商就要换台电话机
Skill 知道什么时候打给谁、打完接着做什么 每个工具都能调,但组合不出复杂任务

⚠️ 架构是三个角色,不是「模型直连工具」 常见的错误图景是:大模型自己开一条连接连到 MCP Server 上,自己去调工具。实际上规范定义的是三个角色:

角色 是什么 职责
Host(宿主) LLM 应用本身——Claude Desktop、Cursor、你写的 Agent 程序 发起连接、管理会话、代表用户做授权决策
Client(客户端) Host 内部的连接器,一个 Server 配一个 Client 维护与某个 Server 的 1:1 有状态连接、做能力协商
Server(服务器) 提供能力的服务——文件系统、GitHub、高德地图 暴露 Tools / Resources / Prompts

模型根本不在这条连接上。 模型做的还是老一套:生成一个「我想调用某工具」的请求。是 Host 决定要不要真调、要不要先问用户、把哪些 Server 的哪些工具暴露给模型、结果怎么塞回上下文。

为什么这个区分很要紧: MCP 的整套安全模型建立在「Host 是可信中介」这个前提上。规范里写的「必须获得用户明确同意才能调用工具」「必须由用户批准 sampling 请求」,责任方全都是 Host——协议本身没有能力强制这些。把架构理解成「模型直连工具」,就会以为安全由协议自动保证,实际上那些检查全要你在 Host 侧自己写。

记住方向:「启动一个 MCP Server」指的是把工具跑起来,不是把模型跑起来。

二、Tool、Resource、Prompt:模型的手、眼、剧本

MCP Server 可以暴露三类东西,这是它和「一堆函数」最大的区别。

能力 比喻 语义 典型用途 对应消息
Tool(工具) 模型的手 让模型某事,有副作用 搜索、计算、调 API、写文件 tools/listtools/call
Resource(资源) 模型的眼睛 让模型某数据,只读 读规章、FAQ、日志、数据库记录 resources/listresources/read
Prompt(提示) 模型的剧本 让模型遵循某模板 代码审查、摘要生成的标准流程 prompts/listprompts/get

三者的调用方也不同,这点最容易混:

  • Tool 由模型自己决定调不调——它出现在模型的可用工具列表里;
  • Resource 通常由客户端(应用)主动读进上下文——比如 Cursor 把你打开的文件作为 Resource 塞给模型;
  • Prompt 一般由用户显式触发——比如在 Claude Desktop 里选一个斜杠命令。

怎么判断该定义成哪一类 首要判据是「谁来决定用它」,不是「有没有副作用」。 规范对三者的定性是控制方不同:Tool 是 model-controlled(模型自己决定调)、Resource 是 application-controlled(应用决定什么时候读进上下文)、Prompt 是 user-controlled(用户显式触发)。

顺着这条往下问:

  • 需要模型在运行时自己判断要不要用 → Tool。改数据、发请求、花钱这类当然是 Tool,但只读操作照样可以是 Tool——「查今天的天气」没有任何副作用,它仍然必须是 Tool,因为决定权在模型手里。
  • 是一份由应用按 URI 取、由应用决定塞不塞进上下文的数据(打开的文件、选中的表、一份规章)→ Resource。
  • 是一段「该怎么干活」的说明、由用户挑一个来用 → Prompt。

副作用只是个次级信号:有副作用的一定是 Tool,但 Tool 不都有副作用。 拿副作用当唯一判据,会把一堆本该让模型自主调用的只读查询错误地做成 Resource——而 Resource 模型根本"点"不到,结果就是工具明明写了,模型却永远不用。

实际情况是:绝大多数 Server 只实现了 Tool。Resource 和 Prompt 的客户端支持度参差不齐,写 Server 时优先把能力做成 Tool,兼容性最好。

反过来:Client 也向 Server 提供三种能力

这是本笔记旧版本完全漏掉的一半,也是很多中文资料至今没跟上的部分。能力不是单向的——Client 也能向 Server 提供功能,规范称之为 Client Features:

Client 能力 语义 谁发起 典型用途
Sampling(采样) Server 反过来请求 Client 去调模型 Server → Client Server 内部需要一次 LLM 推理,但不想自己持有 API Key 和模型配置
Elicitation(信息征询) Server 在执行中向用户要补充信息 Server → Client 参数不全、需要确认、需要用户在几个选项里选一个
Roots(根目录) Server 询问自己可以在哪些 URI / 文件边界内操作 Server → Client 文件类 Server 确认工作区范围

Sampling 的意义容易被低估:它让 Server 不需要自带模型。一个做代码分析的 Server 想让模型总结一段代码,不必自己接 OpenAI、不必管 Key 和计费,而是把请求交回 Client,由 Host 用用户已经配好的模型去跑。代价是这必须经过用户批准——规范明确要求用户能控制是否采样、发出去的提示词内容、以及 Server 能看到什么结果。

Elicitation 则直接推翻了一个流传很广的说法:

⚠️ 「工具不会反问你,所以 MCP 只能一问一答」已经过时 早期资料(也包括本笔记旧版本和很多 A2A vs MCP 的对比文章)用「会不会中途要信息」来区分 MCP 和 A2A:工具一次调用一个结果,Agent 才会反问。

这条判据现在不成立了。 MCP 从 2025 年起补上了 Elicitation(Server 执行到一半向用户请求补充信息)和 Tasks(长时任务、可轮询、有状态机,见下一节)。「同步一问一答」是常见形态,不是协议限制。

稳的判据见 A2A Agent 间通信协议你是在给自己的 Agent 接一个能力(MCP),还是把一整件事委托给一个你不掌控内部逻辑的远端主体(A2A)。

还有一层:基础工具(Utilities)

除了上面两组能力,规范还定义了一批贯穿各处的基础设施,写生产级实现时基本都要用到:

工具 作用
Tasks(任务) 把请求升级成可轮询的长时任务tools/call 带上 task 字段 → 立即返回 taskIdtasks/get 轮询 → tasks/result 取结果。状态机是 workinginput_required / completed / failed / cancelled2025-11-25 引入,标注为实验性
Progress(进度) 长操作过程中回报进度百分比
Cancellation(取消) 取消一个进行中的请求
Completion(补全) 为 Prompt / Resource 的参数提供自动补全候选
Logging(日志) Server 向 Client 发结构化日志,带日志级别
Pagination(分页) 各种 */list 用游标分页,不要假设一次能拿全

Tasks 值得单独留意:它让 MCP 也能表达「跑一个小时的批处理」这类操作,且 taskId 由接收方生成、支持 TTL 和轮询间隔建议。反过来 Server 也能给 Client 发任务化的 sampling/createMessage

三、一次 MCP 连接内部发生了什么?

MCP 是基于 JSON-RPC 2.0 的应用层协议。JSON-RPC 2.0 是一个很老的远程调用规范,只规定三种报文:

// 请求:有 id,必须回复
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
 "params": {"name": "get_weather", "arguments": {"city": "北京"}}}
 
// 响应:id 与请求对应
{"jsonrpc": "2.0", "id": 1,
 "result": {"content": [{"type": "text", "text": "晴,25℃"}]}}
 
// 通知:没有 id,不需要回复
{"jsonrpc": "2.0", "method": "notifications/initialized"}

⚠️ 「有没有 id 就能区分请求和通知」是不完整的 问题在哪: 上面三种报文里,请求和响应都带 id——id 区分的是「通知 vs 其余两者」,不是「请求 vs 响应」。照这个规则写分发逻辑,收到响应时会把它当成请求去找 handler,然后报「未知方法」或者干脆挂掉。

正确的判别顺序:

if "method" in msg and "id" in msg:      # 请求:要回复
    ...
elif "method" in msg and "id" not in msg:  # 通知:不回复
    ...
elif "result" in msg or "error" in msg:    # 响应:按 id 匹配回原请求
    ...

记:method 决定是不是「发起方」的报文,id 决定要不要回复。 另外响应里 resulterror 二选一,不会同时出现。

在这个报文格式之上,MCP 规定了连接的三个阶段:

MCP 连接生命周期与消息流

三个阶段分别在干什么

规范定义的生命周期是 initialization(初始化)→ operation(运行)→ shutdown(关闭) 三个阶段。注意「拿清单」不是一个独立阶段——它属于 operation,只是通常发生在刚连上的时候。

阶段一 · initialization(初始化):Client 发 initialize 报上自己的协议版本和 client capabilities(是否支持 sampling / elicitation / roots / tasks),Server 回自己的名称、版本和 server capabilities(是否支持 tools / resources / prompts / logging / tasks,以及各自的子能力如 tools.listChanged),Client 再发一条 notifications/initialized 通知表示握手完成。

这一步是双向能力协商,不只是版本协商。协商结果直接决定后面能干什么:Server 没声明 tools.listChanged,就别指望收到工具变更通知;Client 没声明 sampling,Server 就不该发 sampling/createMessage

阶段二 · operation(运行):协商出来的所有能力都在这个阶段使用——拉清单(tools/list 等)、执行(tools/callresources/readprompts/get)、Server 反向请求(sampling/createMessageelicitation/createroots/list)、以及各种通知。这些没有固定顺序,在会话期间反复发生。

关于清单返回的内容,有一处常见的错误认知:

⚠️ 以为三种 list 返回的项都有 inputSchema 实际情况:只有 Tool 有 inputSchema 三者的形状不一样:

类型 关键字段
Tool nametitledescriptioninputSchema(JSON Schema),可选 outputSchemaannotations
Resource urinamedescriptionmimeType——没有 inputSchema,它是靠 URI 定位的,不是靠参数调用的
Prompt namedescriptionarguments(一个简单的参数名/描述/是否必填列表)——不是 JSON Schema

写通用解析代码时按「都有 inputSchema」去取,读 Resource 和 Prompt 会直接 KeyError

另外 Tool 现在还可以带 outputSchema,声明返回的结构化数据长什么样——这是后面「Tool 该返回自然语言还是 JSON」那个老问题的答案(见 MCP 实战:从 Server 到企业 API 封装)。

description 是模型选择工具的主要依据。模型看不到你的函数实现,只看到这段描述。描述写得含糊,模型就会选错工具。

阶段三 · shutdown(关闭):优雅关闭连接。stdio 传输下是关闭子进程的 stdin 并等待退出;HTTP 传输下是关闭 HTTP 连接。规范里没有专门的 shutdown 消息——靠传输层来做。

核心消息方法一览

消息方法 方向 说明
initialize C → S 握手,协商协议版本和客户端能力
initialized C → S 通知:初始化完成(无 id)
tools/list C → S 获取所有工具定义
tools/call C → S 调用指定工具
resources/list C → S 获取所有资源清单
resources/read C → S 读取指定资源内容
resources/subscribe C → S 订阅资源变更通知
prompts/list C → S 获取所有提示模板
prompts/get C → S 获取指定模板内容
notifications/tools/list_changed S → C 通知:工具清单变了,请重新拉取
notifications/resources/updated S → C 通知:某个已订阅的资源变了
sampling/createMessage S → C Server 请求 Client 代为调用模型
elicitation/create S → C Server 向用户请求补充信息
roots/list S → C Server 询问自己可操作的路径边界
tasks/get / tasks/result / tasks/cancel / tasks/list 双向 长时任务的轮询、取结果、取消、列举

注意后四行的方向是 S → C:MCP 的消息不是单向的,Server 也会主动向 Client 发请求(不只是通知)。只按「Client 发请求、Server 只回复」写实现,遇到这些消息会直接卡住。

⚠️ 以为「工具清单是连接时的快照」 错误认知: 握手时拉一次 tools/list 就完事,Server 之后新增/下线工具,已连接的 Client 无从得知。

实际情况: 协议有专门的通知机制。Server 只要在初始化时声明了 capabilities.tools.listChanged,工具集变化时就会发 notifications/tools/list_changed,Client 收到后应当重新拉取 tools/list。Resource 和 Prompt 有对应的 notifications/resources/list_changednotifications/prompts/list_changed;订阅了具体资源的还会收到 notifications/resources/updated

正确做法: 实现 Client 时要注册这几个通知的处理函数并刷新本地缓存。同时也别反过来假设一定会收到通知——Server 可能没声明这个能力,长会话里可以配合定期刷新兜底。

另外,各种 */list 都支持游标分页:响应里带 nextCursor 就说明还有下一页。别假设一次调用能拿到全部工具——工具多的 Server 会分页返回,只取第一页会导致后面的工具模型永远看不见。

四、stdio、SSE、Streamable HTTP 该选哪个?

同一套 JSON-RPC 报文,可以跑在不同的传输层上。规范定义的标准传输只有两种:stdio 和 Streamable HTTP。老的 HTTP+SSE 是被 Streamable HTTP 取代的历史方案,只在兼容既有部署时才会遇到。

维度 stdio Streamable HTTP HTTP+SSE(旧,已废弃)
地位 标准传输 标准传输 已被取代,仅作兼容
通信机制 子进程 stdin/stdout,按换行分隔消息 单一 HTTP 端点:POST 发消息;服务端可把响应升级成 SSE 流 GET 建长连接收推送 + 另一个端点 POST 发请求
部署位置 必须与 Client 同机 可远程 可远程
认证 无需(进程级隔离) 需要,见下面的授权 需要
主要代价 不支持远程;Client 要管子进程生命周期 服务端要处理会话与流的管理 要维护收发两个通道,断线重连麻烦
典型场景 本地文件系统、本地脚本工具 新建的云端服务 早期部署的云端服务

⚠️ 「Streamable HTTP 要求 HTTP/2」是错的 实际情况: Streamable HTTP 就是普通的 HTTP/1.1 + SSE,不需要 HTTP/2。它的「单端点双向」靠的是:客户端 POST 发消息,服务端可以选择用 Content-Type: text/event-stream 把响应升级成一条 SSE 流来推送后续消息;客户端也可以额外发一个 GET 开一条监听流。这些在 HTTP/1.1 上完全成立。

「要 HTTP/2」这个说法大概是和 gRPC 混了。真正需要注意的部署要求是别的:反向代理不能缓冲 SSE 响应(Nginx 要关 proxy_buffering)、要放长超时时间。

选择规则

  1. 工具跑在用户自己机器上 → stdio。不用管认证,也不用部署。
  2. 工具是云服务、要给多个用户用Streamable HTTP。新项目不要再选老的 HTTP+SSE。
  3. 对方 Server 只提供旧的 SSE 端点 → 那就用 SSE 连它,但自己写 Server 时不要沿用这个形态。

远程 Server 的授权:不能靠自己发明

一旦离开 stdio,认证授权就不再是「加个 Header 就行」的小事。规范里有专门的 Authorization 一章,基于 OAuth 2.1,几条硬性要求值得单独记住:

要求 说明
基于 OAuth 2.1 授权码流程必须配 PKCE;隐式流和密码流已被移除
支持动态客户端注册 客户端事先不知道有哪些 Server,需要能自动注册
通过元数据发现端点 用 Protected Resource Metadata / Authorization Server Metadata 找到授权服务器,而不是写死
Token 必须面向本 Server 签发 Server 必须校验 token 的受众(audience)是自己

⚠️ Token passthrough(令牌透传)是被明令禁止的 错误操作: MCP Server 收到客户端给的 access token,直接原样拿去调下游 API(比如把用户给你的 GitHub token 转手发给 GitHub)。或者反过来,Server 接受一个不是签发给自己的 token。

实际风险: 这会绕过下游服务的授权边界——下游看到的是一个合法 token,无从知道请求其实来自一个中间的 MCP Server,审计链路断裂;一旦 Server 被攻破或行为异常,攻击者手里就是一把可以直接用于下游的钥匙。同时它让「这个 token 只授权给谁用」这个约束彻底失效。

规范的要求是: Server 必须拒绝任何不是签发给自己的 token(校验 audience)。需要访问下游时,走标准的令牌交换/独立授权,拿一个面向下游、受众正确的新 token。

另外两条别忘了:

  • 校验 Origin——防 DNS rebinding 攻击。这条对本地监听 HTTP 的 Server 尤其致命:不校验的话,用户浏览器里任何一个恶意网页都能向 localhost 上你的 MCP Server 发请求,等于把本地工具的全部能力暴露给了整个互联网。
  • 本地 Server 只绑 127.0.0.1,不要绑 0.0.0.0

五、langchain-mcp-adapters 到底做了什么?

MCP 的 Tool 和 LangChain 的 BaseTool 是两套东西,中间需要一层翻译。MultiServerMCPClient 就是这层翻译,它内部做三件事:

  1. 为配置里的每个 Server 建立连接(stdio 的启子进程,SSE 的建会话);
  2. 握手后发 tools/list 拿到所有工具定义;
  3. 把 MCP 的工具定义逐字段映射成 LangChain 的 BaseTool

映射关系很直接:

MCP 字段 LangChain 字段 说明
Tool.name BaseTool.name 原样搬
Tool.description BaseTool.description 原样搬,模型靠这段选工具
Tool.inputSchema BaseTool.args_schema JSON Schema → 动态生成 Pydantic 模型

最小用法(配置字典的键名是你自取的服务名,仅用于区分来源):

from langchain_mcp_adapters.client import MultiServerMCPClient
 
client = MultiServerMCPClient({
    "weather": {                              # 服务名,自取
        "command": "python",
        "args": ["local_weather_server.py"],
        "transport": "stdio",
    },
})
tools = await client.get_tools()              # 注意:协程,必须 await

拿到的 tools 就是普通的 LangChain 工具列表,可以直接喂给 Agent:

from langchain.agents import create_agent
 
agent = create_agent(model, tools=tools, system_prompt="……")
result = await agent.ainvoke({"messages": [{"role": "user", "content": "北京天气"}]})

多个 Server 同时连时,所有工具会自动合并成一个扁平列表,模型并不知道某个工具来自哪个 Server:

client = MultiServerMCPClient({
    "weather": {"command": "python", "args": [...], "transport": "stdio"},   # 本地
    "amap":    {"url": "https://mcp.amap.com/sse?key=KEY", "transport": "sse"},  # 远程
})
tools = await client.get_tools()   # 本地 2 个 + 高德 11 个,合成一个列表

⚠️ 多 Server 混连时的工具重名 错误操作: 同时连两个 Server,它们各自都有一个叫 search 的工具。

实际结果: 合并后列表里出现两个同名工具,模型选择行为不可预测,也无法从调用记录里判断实际打到了哪个 Server。

原因: 合并是扁平的,MCP 的服务名只用于配置和连接管理,不会作为命名空间前缀加到工具名上

正确做法: 自己写 Server 时用带领域前缀的名字(amap_weather 而不是 weather);连第三方 Server 时先打印一遍 [t.name for t in tools] 确认无冲突,有冲突就分成两个 Agent 或在中间做一层改名。

六、常见 MCP Server 速查

Server 启动 / 连接方式 用途 认证
文件系统 npx @modelcontextprotocol/server-filesystem /path 读写本地文件
GitHub npx -y @modelcontextprotocol/server-github 仓库、Issue、PR 操作 GITHUB_TOKEN
高德地图 https://mcp.amap.com/sse?key=KEY 地理编码、天气、路径规划 AMAP_KEY
浏览器 npx @anthropic/mcp-server-browser 网页截图、交互
数据库 各厂商自有 Server 查询数据库 连接配置
搜索引擎 Brave Search / Google 互联网检索 API Key

注意前四行的形态差异:npx 开头的是 stdio 型(本地启进程),https 开头的是 SSE 型(连远程端点)。配置字典的写法也随之不同——前者用 command + args,后者用 url

七、调试方法与常见陷阱

MCP 的调试难点在于:报文在进程管道或 HTTP 流里,看不见。 三个基本手段:

  • 设环境变量 MCP_DEBUG=1 打印原始 JSON-RPC 报文;
  • mcp dev your_server.py 启动官方调试器,在网页里手工调工具;
  • 连接成功后先打印一遍工具清单,确认名称和描述符合预期:
tools = await client.get_tools()
for t in tools:
    print(f"{t.name}: {t.description}")

下面三个坑,前两个几乎人人都会踩一次。

⚠️ 在 stdio Server 里用 print() 调试 错误操作:@mcp.tool() 函数里加一句 print("收到请求") 看看有没有被调到。

实际结果: 连接直接崩溃,或者 Client 报「无法解析响应」,而且错误信息完全不指向你加的那行 print。

原因: stdio 传输方式下,stdout 就是 JSON-RPC 的数据通道。你 print 出来的中文字符串会和协议报文混在同一个流里,Client 拿到一行非法 JSON,解析当场失败。

正确做法:logging 输出到 stderr 或文件,stderr 不参与协议传输:

import logging, sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logging.info("收到请求")   # ✅ 安全

⚠️ 在同步函数里调 get_tools() 错误操作: 在普通的 def 函数里写 tools = client.get_tools(),然后传给 create_agent

实际结果: 不报错,但 Agent 一个工具也不会调,日志里看到工具列表长度异常,或抛出 TypeError: object coroutine can't be used ...

原因: get_tools() 是协程函数,不 await 只会得到一个 coroutine 对象,它不是列表也不是工具。

正确做法: 整条链路走异步——在 async defawait client.get_tools(),Agent 也用 await agent.ainvoke(...),最外层用 asyncio.run() 启动。

⚠️ 把 API Key 直接拼进 SSE 端点 URL 错误操作: {"url": f"https://mcp.amap.com/sse?key={amap_key}", "transport": "sse"},然后把这段连同日志一起提交或分享。

实际结果: Key 出现在应用日志、异常堆栈、ps 进程列表和任何打印过配置字典的地方,等于半公开。

原因: URL query 参数会被几乎所有环节原样记录,它从来不是放密钥的地方——只是很多 Server 出于兼容性支持了这种写法。

正确做法: Key 一律从 os.getenv() 读,不写进代码;打印配置前先脱敏;如果 Server 支持 Header 认证就改用 Header;已经泄露过的 Key 直接去控制台轮换,不要指望删日志。


速查表

概念 结论 一句话说明
MCP 全称 Model Context Protocol Anthropic 提出的开放标准;本篇按 2025-11-25 版,2026-07-28 已改为无状态(见开头版本提示框)
底层协议 JSON-RPC 2.0 method+id=请求;methodid=通知;result/error=响应
三个角色 Host → Client → Server Host 是宿主应用并握有授权决策;一个 Server 配一个 Client;模型不在连接上
Server 能力 Tool / Resource / Prompt 做事 / 读数据 / 套模板;实际以 Tool 为主
Client 能力 Sampling / Elicitation / Roots Server 反向请求:借模型 / 向用户要信息 / 问路径边界
基础工具 Tasks(实验性)/ Progress / Cancellation / Completion / Logging / Pagination 长时任务、进度、取消、补全、日志、分页
生命周期 initialization → operation → shutdown 拉清单属于 operation,不是独立阶段
谁有 inputSchema 只有 Tool Resource 靠 uri,Prompt 用 arguments 列表
清单会变吗 notifications/tools/list_changed,需 Server 声明 tools.listChanged
分页 */list 支持游标 响应带 nextCursor 就还有下一页,别只取第一页
标准传输 stdio / Streamable HTTP 旧的 HTTP+SSE 已被取代,新项目别选
Streamable HTTP 要 HTTP/2 吗 不要 HTTP/1.1 + SSE 即可;注意反代关 buffering
远程授权 OAuth 2.1 授权码流必须配 PKCE,支持动态注册与元数据发现
安全红线 禁止 token passthrough 必须校验 token 受众;必须校验 Origin;本地只绑 127.0.0.1
模型选工具的依据 Tool 的 description 写含糊就会选错,且不报错
桥接库 langchain-mcp-adapters inputSchema → 动态 Pydantic args_schema
多 Server 工具扁平合并 服务名不做命名空间,重名要自己防
调试开关 MCP_DEBUG=1 / mcp dev 打印原始报文 / 网页手工调工具
stdio 头号禁忌 不能 print 到 stdout stdout 是协议通道,日志走 stderr

复习重点

复习重点

  1. 一句话说清 MCP:它不给模型新增调用能力,而是把「外部能力怎么接进 LLM 应用」标准化,让一个 Server 适配所有兼容宿主。
  2. 最容易记错的架构:是 Host → Client → Server 三层,模型不在这条连接上。所有「必须经用户同意」的安全要求,责任方都是 Host——协议强制不了,得你自己实现。
  3. 最容易漏掉的一半:能力是双向的。Server 有 Tools/Resources/Prompts,Client 有 Sampling/Elicitation/Roots,Server 会主动向 Client 发请求(不只是通知)。
  4. 已经过时的三条老说法:① 「MCP 只能同步一问一答、工具不会反问」——有 Elicitation 和 Tasks 了;② 「工具清单是连接时快照」——有 list_changed 通知;③ 「远程就用 HTTP+SSE」——标准是 Streamable HTTP,且不需要 HTTP/2
  5. 最容易被忽视的机制:模型只能看到 Tool 的 namedescription,看不到实现。描述质量直接决定选工具的准确率,而选错不会报错,只会答偏。
  6. 最容易写崩的实现细节:stdio 模式下 stdout 是协议通道,一句 print() 就能让连接解析失败,且报错完全不指向那行。
  7. 安全上最不能错的一条禁止 token passthrough——Server 必须拒绝不是签发给自己的 token,要访问下游就走令牌交换换一个受众正确的新 token。本地 HTTP Server 还必须校验 Origin 并只绑 127.0.0.1,否则浏览器里任意网页都能调你的本地工具。
  8. 边界意识:MCP 管「给我的应用接能力」,A2A 管「把活委托给别人的 Agent」。看到「多 Agent 协作」的需求要想到的是 A2A Agent 间通信协议 或进程内编排,不是 MCP。

一句话总结 MCP 的价值不在于让模型「能用工具」,而在于把这件事的接线方式定死了——能力方写一次,宿主连一次,中间那段重复劳动被彻底消掉。但别把它理解成一根从模型直插工具的线:真正的结构是 Host 代表用户,在中间握着所有授权决策,安全性全部落在这一层。

下一步动手写 Server、把企业已有 REST API 包成 MCP,见 MCP 实战:从 Server 到企业 API 封装