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/createMessage、elicitation/create、roots/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/tasks;tasks/result换成轮询tasks/get,tasks/list取消ping、logging/setLevel、notifications/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 要解决的到底是什么问题?
- 二、Tool、Resource、Prompt:模型的手、眼、剧本
- 三、一次 MCP 连接内部发生了什么?
- 四、stdio、SSE、Streamable HTTP 该选哪个?
- 五、langchain-mcp-adapters 到底做了什么?
- 六、常见 MCP Server 速查
- 七、调试方法与常见陷阱
- 速查表
- 复习重点
核心概念 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。
图从下往上读:底层 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/list → tools/call |
| Resource(资源) | 模型的眼睛 | 让模型读某数据,只读 | 读规章、FAQ、日志、数据库记录 | resources/list → resources/read |
| Prompt(提示) | 模型的剧本 | 让模型遵循某模板 | 代码审查、摘要生成的标准流程 | prompts/list → prompts/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 字段 → 立即返回 taskId → tasks/get 轮询 → tasks/result 取结果。状态机是 working → input_required / completed / failed / cancelled。2025-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决定要不要回复。 另外响应里result和error二选一,不会同时出现。
在这个报文格式之上,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/call、resources/read、prompts/get)、Server 反向请求(sampling/createMessage、elicitation/create、roots/list)、以及各种通知。这些没有固定顺序,在会话期间反复发生。
关于清单返回的内容,有一处常见的错误认知:
⚠️ 以为三种 list 返回的项都有
inputSchema实际情况:只有 Tool 有inputSchema。 三者的形状不一样:
类型 关键字段 Tool name、title、description、inputSchema(JSON Schema),可选outputSchema、annotationsResource uri、name、description、mimeType——没有 inputSchema,它是靠 URI 定位的,不是靠参数调用的Prompt name、description、arguments(一个简单的参数名/描述/是否必填列表)——不是 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_changed、notifications/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)、要放长超时时间。
选择规则
- 工具跑在用户自己机器上 → stdio。不用管认证,也不用部署。
- 工具是云服务、要给多个用户用 → Streamable HTTP。新项目不要再选老的 HTTP+SSE。
- 对方 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 就是这层翻译,它内部做三件事:
- 为配置里的每个 Server 建立连接(stdio 的启子进程,SSE 的建会话);
- 握手后发
tools/list拿到所有工具定义; - 把 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 def里await 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=请求;method 无 id=通知;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 |
复习重点
复习重点
- 一句话说清 MCP:它不给模型新增调用能力,而是把「外部能力怎么接进 LLM 应用」标准化,让一个 Server 适配所有兼容宿主。
- 最容易记错的架构:是 Host → Client → Server 三层,模型不在这条连接上。所有「必须经用户同意」的安全要求,责任方都是 Host——协议强制不了,得你自己实现。
- 最容易漏掉的一半:能力是双向的。Server 有 Tools/Resources/Prompts,Client 有 Sampling/Elicitation/Roots,Server 会主动向 Client 发请求(不只是通知)。
- 已经过时的三条老说法:① 「MCP 只能同步一问一答、工具不会反问」——有 Elicitation 和 Tasks 了;② 「工具清单是连接时快照」——有
list_changed通知;③ 「远程就用 HTTP+SSE」——标准是 Streamable HTTP,且不需要 HTTP/2。- 最容易被忽视的机制:模型只能看到 Tool 的
name和description,看不到实现。描述质量直接决定选工具的准确率,而选错不会报错,只会答偏。- 最容易写崩的实现细节:stdio 模式下 stdout 是协议通道,一句
print()就能让连接解析失败,且报错完全不指向那行。- 安全上最不能错的一条:禁止 token passthrough——Server 必须拒绝不是签发给自己的 token,要访问下游就走令牌交换换一个受众正确的新 token。本地 HTTP Server 还必须校验
Origin并只绑127.0.0.1,否则浏览器里任意网页都能调你的本地工具。- 边界意识:MCP 管「给我的应用接能力」,A2A 管「把活委托给别人的 Agent」。看到「多 Agent 协作」的需求要想到的是 A2A Agent 间通信协议 或进程内编排,不是 MCP。
一句话总结 MCP 的价值不在于让模型「能用工具」,而在于把这件事的接线方式定死了——能力方写一次,宿主连一次,中间那段重复劳动被彻底消掉。但别把它理解成一根从模型直插工具的线:真正的结构是 Host 代表用户,在中间握着所有授权决策,安全性全部落在这一层。
下一步动手写 Server、把企业已有 REST API 包成 MCP,见 MCP 实战:从 Server 到企业 API 封装。