第 23 课:MCP 协议

紧接第 22 课《工具调用》:上一课我们讲透了「模型怎么调用一个函数」,也埋了一个伏笔——每个宿主都要为每个工具各写一套适配,接入量是 M×N。这一课把那个伏笔展开:MCP(Model Context Protocol)如何用一套协议把 M×N 压成 M+N,Host / Client / Server 三个角色怎么分工,Resources / Prompts / Tools 三类原语怎么选,怎么把一个内网系统包成 Server,传输与鉴权怎么配,以及 2026-07-28 版为什么把协议改成无状态、改完扩容简单在哪。

🎯 学完这 6 步你将能够:
① 算清接入量的账:5 个宿主 × 20 个工具的两两对接是 100 份,协议化后只需 5 + 20 = 25 份,省 75 份
② 说清 Host / Client / Server 三个角色的分工,以及一条 JSON-RPC 2.0 请求的关键字段(必填 jsonrpc / method,请求带 id;params 视方法可有;result 属响应)
③ 解释 2026-07-28 无状态化改了什么,并算出按 ttlMs 缓存 tools/list 把 1,000 次探测压到 1 次(省 999 次)
🗺 本节推理链(6 步:接入成本 → 角色与消息 → 三原语 → 包成 Server → 传输与鉴权 → 无状态化)
1先从课程主线最实际的约束出发:要让「5 个宿主 × 20 个工具 = 100 份适配」这件事不再成立,这就要求先把「每个宿主接每个工具都要各写一遍」这句乘法拆开看清楚——没有统一约定时,工具与 context 的接入量是消费方与提供方的乘积 M×N。接入成本是乘法。
↓
2既然「接入成本是乘法」,所以要有一个统一约定来替代两两私接;于是 MCP 用三层角色把它拆开——Host(宿主)发起、Client(客户端)连接、Server(服务器)提供能力,消息走 JSON-RPC 2.0 交换 context。三层角色各管一段。
↓
3既然「三层角色各管一段」,所以下一步要看 Server 到底能提供什么;于是把它的三原语拆开——Resources 只读数据、Prompts 模板、Tools 可执行函数,按「谁控制」决定谁把它们注入 context。三原语各有归属。
↓
4既然「三原语各有归属」,于是可以动手把公司内网系统包成一个 Server:声明几个 tool、写好 schema,所以宿主不必改代码就能拿到 context 与能力。把系统包成 Server。
↓
5既然「把系统包成 Server」,所以紧接着要问谁能调什么:本地用 stdio、远程用 Streamable HTTP,再用 OAuth 的 token 与用户同意划清权限边界。传输与鉴权配套。
↓
6既然接入、原语、鉴权都已就位,「传输与鉴权配套」,所以最后看协议自身的可运维性:早期有状态会话要求粘性路由与共享存储,2026-07-28 版改成无状态,请求自带路由信息、列表结果可按 cache 的 ttlMs 缓存。协议也要能横向扩。
MCPJSON-RPC 2.0Host/Client/ServerTools·Resources·PromptsStreamable HTTP无状态化
1
每接一个新工具,就要为每个宿主各写一遍适配代码——这个乘法错在哪?
提示:数一数——M 个宿主、N 个工具,两两对接一共要写多少份?有没有办法把它从乘法变成加法?
▾
知识背景
上一课《工具调用》把一次调用的四步走通了,但那是在一个宿主里。工程上还有一个乘法:没有统一约定时,M 个宿主(host,装了大模型、发起调用的 AI 应用)要为 N 个工具(tool,模型可调用的函数)各写一套适配代码,接入量是 M×N。MCP(Model Context Protocol,模型上下文协议,一个让 AI 应用与外部工具用统一格式对话的开放协议)就是为了消掉这个乘法;它明确借鉴 LSP(Language Server Protocol,语言服务器协议,把编辑器与编程语言解耦的同款思路),把两两适配变成统一约定。规范用 BCP 14(即 RFC 2119 / RFC 8174 那套 MUST / SHOULD 关键字约定)的 MUST / MUST NOT 定义硬性要求。

上一课我们把工具调用拆成了「定义 → 意图 → 执行 → 回填」四步。这一张先算最实际的一笔账:为什么每接一个新工具,就要为每个宿主各写一遍适配代码。

因为工具调用依赖宿主自己的私有约定——A 应用写的工具,B 应用没法直接拿来用。就像每家门店都要为每个品牌的会员卡各装一台刷卡机:5 家店 × 20 个品牌 = 100 台,谁也统一不了,每加一个品牌每店都要再加一台。所以下面把 M×N 这笔乘法,与它被压成 M+N 之后的差距算出来。

更直接的做法是「谁要用谁自己接」——每个宿主单独为每个工具写一份适配。只有 1 个宿主时它也够用,但成本随规模平方涨:从 5×20 加到 10×50,两两对接的份数从 100 涨到 500(约 5 倍——宿主翻倍、工具数又增至 2.5 倍,乘法叠加;而协议化后份数只从 25 涨到 60,约 2.4 倍);每新增一个工具,还要在全部宿主里各改一次。

🧮 第一笔账:M×N 与 M+N 差多少
Step 1:先把 M 与 N 定义清楚
符号含义例子
M宿主(host)数量5 个(IDE、聊天、工单系统…)
N工具(tool)数量20 个(数据库、工单、日历…)
算术:没有统一约定时,第 m 个宿主想用第 n 个工具,就要写一份它俩之间的适配——所以总接入量是 M 与 N 的乘积。
Step 2:两两对接 = 100 份,协议化 = 25 份
方案要写多少份计算
各自对接100M×N = 5 × 20
统一协议25M+N = 5 + 20
算术:M=5、N=20 时,两两对接要 100 份,统一协议只要 25 份——省掉 100 − 25 = 75 份。这就是协议的价值:把乘法变成加法。
Step 3:规模越大,差距越夸张
规模M×NM+N倍数
5 × 20100254.0×
10 × 5050060≈ 8.3×
20 × 1002,000120≈ 16.7×
算术:当 M、N 同步放大时,M×N 与 M+N 的差距从 4.0 倍拉到约 16.7 倍——乘法按规模平方涨、加法只线性涨。这正是 MCP 这类协议存在的理由。
🎯 关键认知

MCP 解决的不是模型能力问题,而是一个生态乘法:把 M×N 份点对点适配,换成 M+N 份对协议的实现。它借鉴 LSP 的成功经验——协议只规定「怎么交换 context」,不规定「交换什么业务」,所以既能接各类工具,也不会绑死在某一家。

AI 助教
👋 对 M×N 这笔账有疑问?比如「M+N 到底省了多少」「规模大了会怎样」?

告诉我你的宿主数与工具数,我帮你算这笔接入账。
2
MCP 到底是什么?Host、Client、Server 三个角色是怎么分工的,消息又是怎么走的?
提示:一个宿主可能同时连好几个服务器——先想「连接」这件事该由谁负责,再想一条请求要经过哪些角色。
▾
知识背景
MCP 的三个角色:Host(宿主,装了大模型、发起连接的 AI 应用,如 IDE、聊天工具)、Client(客户端,宿主内部为每个服务器建的一个连接器,与服务器 1 对 1)、Server(服务器,对外提供 context 与能力的程序,可以是本地进程或远程服务)。三者之间用 JSON-RPC 2.0(一种用 JSON 表达请求/响应、带 method / params / id 字段的轻量远程调用格式)通信。设计原则里有一条硬性约定:服务器不应读到整段对话,也看不到别的服务器。

上一张卡片算清了 M×N 这笔乘法。这一张回答:MCP 到底是什么,三个角色怎么分工。

因为一个宿主往往要同时连多个服务器,如果让应用自己记住每条连接的细节,它会越写越重,所以要把「连接」从应用里拆出来,交给一个专门的角色 Client。就像一家公司要对接多个供应商,会给每个供应商配一个专门的对接人,而不是让老板自己记所有细节。所以一次 tools/call 的请求,走的是「宿主 → 客户端 → 服务器」这条固定的路。

🧮 第二笔账:三个角色与一条请求
Step 1:谁是谁、数量怎么算
角色是什么数量
Host发起连接、负责编排的 AI 应用1 个宿主
Client宿主内为每个服务器建的连接器= 服务器数
Server提供 context 与能力的程序2 个(如工单 + 日历)
算术:1 个宿主接 2 个服务器,就要有 2 个 Client(与服务器 1 对 1);宿主管编排与安全,客户端只管把这一条连接维护好。
Step 2:一条 JSON-RPC 2.0 请求长什么样
字段取值含义
jsonrpc"2.0"版本必须是 2.0
methodtools/call要调的方法
params{name, arguments}传什么参数
id1请求与响应的配对号
算术:这条 tools/call 请求里 jsonrpc、method、params、id 都在,但它们的「必需性」并不一样——按 JSON-RPC 2.0 规范,jsonrpc 与 method 必填,params 可省略(MAY be omitted),id 只在「需要响应」的请求里才必填(不带 id 即为通知 notification);MCP 又在协议层规定请求 MUST 带 id。服务器回一条响应,也带 result(或 error)与同一个 id,客户端据此把结果对上号。
Step 3:一条请求其实是 2 条消息
动作消息数说明
请求1客户端 → 服务器
响应1服务器 → 客户端
合计2一次调用 2 条消息
连调 3 个工具62 × 3
算术:一次工具调用最少 2 条消息(请求 + 响应),连调 3 个工具就是 6 条。注意「消息数」是协议层的计数,不等于进模型 context 的 token——真实会话在 tools/call 前通常还有 initialize、tools/list 等消息,而真正进 context 的是工具返回的结果,两者别混为一谈。
🎯 关键认知

MCP 是一个「宿主—客户端—服务器」三层协议:宿主管编排、客户端管连接、服务器管能力。消息走 JSON-RPC 2.0(请求必填 jsonrpc / method,且带 id;params 视方法可有),每个响应用 id 配对——理解这条固定的路,后面所有工程问题(原语、鉴权、扩容)都会落到某一个角色身上。

AI 助教
👋 对三个角色有疑问?比如「Host 和 Client 为什么要拆开」「一条请求走几步」?

说清你的应用形态,我帮你对应到 Host / Client / Server。
3
Resources、Prompts、Tools——服务器能暴露的三类东西,各自解决什么、什么时候该用哪个?
提示:先别记名字,先问一句——这三类东西,分别由「谁」决定要不要用?模型、应用,还是人?
▾
知识背景
服务器能暴露三类原语(primitive,协议里最基本的可操作单元):Resources(资源,只读的数据,如文件、日志、数据库记录,由宿主应用决定怎么注入 context)、Prompts(提示模板,可复用的工作流模板,通常由用户显式触发,如斜杠命令)、Tools(工具,模型可执行的函数)。区分三者的关键不是数据形态,而是谁控制:Tools 由模型选、Resources 由应用注、Prompts 由用户挑。

上一张卡片看到 Server 通过「原语」对外提供能力。这一张把三类原语拆开,回答各自解决什么、什么时候用哪个。

因为三类原语的控制方不同,混用会让模型做出越权的事,所以要先按「谁控制」把边界划清。就像一个抽屉柜:Resources 是别人直接放进你桌面的资料(应用决定给什么)、Prompts 是你自己挑的模板卡(用户决定用哪张)、Tools 是给助手的一把钥匙(模型决定什么时候用)。所以下面用一笔 token 账,看把太多东西当 Tools 会付什么代价。

更省事的做法是「把服务器能提供的东西全写成 Tool」,反正模型都能调。但 Tool 的 schema 每次都常驻上下文:12 个能力全做成 Tool 就是 12 × 200 = 2,400 token,其中不少是只读资料,本该由应用按需注入。分出 Resources / Prompts 后,常驻的只剩 3 个 Tool = 600 token,省 1,800 token(每次请求都省)。

🧮 第三笔账:三类原语怎么选、代价差多少
Step 1:三类原语一张表看清
原语谁控制典型方法场景
Resources宿主应用resources/list、resources/read把资料注入 context
Prompts用户prompts/list、prompts/get斜杠命令
Tools模型tools/list、tools/call执行函数
区分标准只有一条:谁决定它被使用。控制方错了,轻则浪费 context 窗口,重则让模型越权去读或去写。
Step 2:一个服务器暴露 12 个能力,不代表要变成 12 个 Tool
做法暴露给模型的 tool 数其余怎么给
全部当 Tool12全部常驻 context
分好类3其余 9 个按需注入
算术:12 个能力若都写成 Tool,就有 12 份 schema 常驻 context;只把 3 个写成 Tool,另外 9 个按需注入,常驻量从 12 份降到 3 份。
Step 3:token 成本差多少
项数值
每个 tool 定义(示意)≈ 200 token
12 个 Tool12 × 200 = 2,400 token
3 个 Tool3 × 200 = 600 token
省下1,800 token
算术:按每个 tool 定义约 200 token 估(示意),12 个占 2,400 token,收成 3 个只占 600 token,省 1,800 token——而且每次请求都要带。
🎯 关键认知

三类原语的分水岭是「谁控制」:Resources 应用控制、Prompts 用户控制、Tools 模型控制。选错控制方,付出的代价是 token 与安全边界,而不是功能缺失——这也是为什么「只把该给模型的做成 Tool」既是设计原则,也是成本纪律。

AI 助教
👋 对三类原语有疑问?比如「同一个数据做成 Resource 还是 Tool」「Prompt 和系统提示啥区别」?

说清你的场景,我帮你判断该用哪一类原语。
4
动手:怎么把一个公司内部系统(比如工单查询)包成一个 MCP Server?
提示:先想清楚「对外要提供哪几个动作」,再想每个动作模型需要哪些输入——描述和参数 schema 就是模型看到的全部。
▾
知识背景
把内部系统包成 Server,代码上往往很小:用官方 SDK 声明几个 tool 函数、写好描述与 JSON Schema,SDK 负责 JSON-RPC 收发、能力协商与错误处理。Anthropic 在 2024-11-25 发布 MCP 时,同时给出了 Google Drive、Slack、GitHub、Git、Postgres、Puppeteer 等参考服务器(见官方公告《Introducing the Model Context Protocol》)。约定是:工具名、描述、参数 schema 就是模型看到的全部——服务器不参与对话编排,只负责把能力摆出来。

上一张卡片把三类原语分清了。这一张动手,把一个公司内网系统(工单查询)包成 Server。

因为服务器只需要把能力声明出来、把编排交给宿主,所以一个 server 常常只有几十行。就像给新同事做一张「工作说明卡」:写清他能做什么、要什么输入、返回什么,剩下该调谁、什么时候调,由主管来定。所以下面用四个工具把工单系统的 Server 走一遍,并算一下工作量。

🧮 第四笔账:工单 Server 的四个工具
Step 1:先定四个 tool
工具名输入作用
get_ticketid查一张工单
list_ticketsstatus、limit按状态列工单
create_tickettitle、desc建工单
update_statusid、status改状态
算术:4 个工具就覆盖了工单的查、列、建、改;每个工具都要写清 name、description 与参数 schema,这三样是模型能看到的全部。
Step 2:Server 的工作量(示意口径)
做法工作量
包 1 个 Server:4 个工具 × ≈ 30 行≈ 120 行
为 5 个宿主各写一套:4 × 5 = 20 个适配函数20 × 30 = 600 行
算术(示意):按每个工具约 30 行估,包一个 Server 约 120 行;若为 5 个宿主各写一套就是 600 行,省下 480 行。这是「示意」口径,具体随实现而异。
Step 3:这 4 个工具占多少 context
项数值
每个 tool 定义(示意)≈ 200 token
4 个工具4 × 200 = 800 token
发送时机每次请求都带进 context
算术:4 个工具约 800 token,每次请求都带进 context;这也是为什么前面强调「工具要少而精」——它直接是每次调用的固定成本。
🎯 关键认知

包一个 Server 的套路只有三步:选好对外暴露的 tool、给每个 tool 写清描述与 schema、把真实系统的调用接进去。宿主那边一行都不用改,这就是 M+N 的好处——接入成本从「每个宿主一套」变成「一个 Server 一份」。

AI 助教
👋 对包 Server 有疑问?比如「一个 Server 要写多少代码」「工具描述怎么写」?

贴出你的内网接口,我帮你拆成几个 tool。
5
谁能调什么?认证、权限与传输方式(stdio 还是 HTTP)该怎么选?
提示:先看服务器跑在哪——同一台机器的进程里,还是外面的网络里?信任边界不同,选法就不同。
▾
知识背景
传输有两种:stdio(标准输入输出,服务器作为宿主的子进程、走本机管道的本地传输)与 Streamable HTTP(一种可流式返回的 HTTP 传输,服务器是远程服务,用 SSE 把结果增量推给客户端)。安全上:工具代表任意代码执行,工具描述不可全信(除非来自可信服务器);远程服务器用 OAuth 2.1(一套授权协议)鉴权,请求带 access token(访问令牌);宿主在调用任何工具前必须取得用户同意。2026-07-28 版还强化了授权侧:新增 RFC 9207 的发行方(issuer)校验,并以 CIMD(客户端元数据文档)取代 DCR(动态客户端注册)。

上一张卡片把工单 Server 跑起来了。这一张处理「谁能调什么」,也就是传输与鉴权的配套选择。

因为本地与远程的信任边界不同,所以传输方式与鉴权强度要一起选,不能只看方便。就像同一栋楼的门禁:本楼员工刷工牌就进(本地 stdio 直接继承宿主进程的权限),外来访客要登记换证(远程 HTTP 走 OAuth token)。所以下面用一笔账,比较两种传输的成本。

🧮 第五笔账:stdio 还是 HTTP
Step 1:两种传输的适用面
维度stdioStreamable HTTP
服务器位置本机子进程远程服务
信任边界继承宿主权限需独立鉴权
常见场景文件、Git日历、SaaS
选择标准是「服务器在哪、归不归你管」——本机的直接用,远程的先建立信任。归属不同,传输与鉴权都要跟着变。
Step 2:鉴权成本(示意)
传输每次调用的鉴权次数1,000 次调用
stdio0(继承进程权限)0
Streamable HTTP1(校验 token)1,000
算术(示意):本地继承权限,额外鉴权 0 次;远程每次请求都要校验一次 access token,1,000 次调用 = 1,000 次校验(示意,可用连接复用或缓存降低)。
Step 3:往返与权限最小化
项数值
stdio 的网络往返0 次
HTTP 每请求往返1 次
1,000 次调用多出1,000 次
权限最小化只授予该工具必需的范围
算术:stdio 无网络往返(0 次),HTTP 每请求 1 次,1,000 次调用就多 1,000 次。权限上再叠加一条铁律:最小权限——远程 Server 只拿它那个工具必需的范围。
🎯 关键认知

传输与鉴权是一对:本地 stdio 靠进程权限、几乎零额外成本;远程 Streamable HTTP 必须配 OAuth token、最小权限与用户同意。工具即任意代码执行,这一层不能省——描述不可全信,调用前必须取得用户同意。

AI 助教
👋 对传输与鉴权有疑问?比如「什么时候必须上 HTTP」「本地 Server 还要不要沙箱」?

说清你的部署环境,我帮你定传输与鉴权方案。
6
MCP 早期要维护「会话」,为什么 2026-07-28 版把它改成无状态?改完部署和扩容变简单在哪?
提示:先想「有会话」在部署上意味着什么,再想实例从 1 台加到 10 台时,会多出哪些麻烦。
▾
知识背景
早期 MCP 走 HTTP+SSE(2024-11-05 版);2025-03-26 版起改用 Streamable HTTP,并且是有状态的:连接先做 initialize / initialized 握手、协商能力,服务器再发一个 Mcp-Session-Id,后续每个请求都必须带上它——于是客户端被钉在发会话的那台实例上,横向扩容要粘性路由加共享会话存储。2026-07-28 版把核心改成无状态:移除握手与会话,每个请求自带 protocolVersion、clientInfo 与 clientCapabilities(放在 _meta 里),方法名与工具名走 Mcp-Method / Mcp-Name 头、便于网关路由与鉴权,新增 server/discover,tools/list 等列表结果可按服务器给的 ttlMs 缓存(cache)。同时 roots / sampling / logging 被正式弃用(deprecated,仍可用,至少 12 个月过渡期)。

上一张卡片解决了「谁能调什么」。这一张看协议自己的演进:为什么要无状态化。

因为有状态要求请求必须回到原来那台实例、状态还要外置到共享存储,所以实例一多,扩容就卡在这两件事上。就像一家店要求「必须找上次接待你的那个店员」:人一多,排班和交接全乱了;改成「谁都能接」,才排得开班。所以下面用一笔账对比两种部署方式,并看缓存能省多少探测。

更直接的做法是「保留会话、用粘性路由把请求钉回原实例」。单实例时没问题,但实例从 1 台加到 10 台,就要求负载均衡记住每个会话落在哪台、并把会话状态外置到共享存储——扩容从「加机器」变成「迁会话」。无状态化把这两条约束都去掉,所以加实例不再需要迁移。

🧮 第六笔账:有状态 vs 无状态
Step 1:有状态强加两条约束
约束含义后果
粘性路由请求必须回到原实例负载不均、扩容要迁会话
共享会话存储状态外置到统一存储多一层组件与故障点
算术:这两条把「加机器」从插上就能用,变成要迁移、要同步;实例翻倍时,会话重分布与共享存储的压力都随之上升。
Step 2:无状态后,扩容变成「加实例」
项有状态无状态(2026-07-28)
路由依据会话 ID(粘性)Mcp-Method / Mcp-Name 头
负载均衡需粘性普通轮询即可
tools/list每次会话拉取可按 ttlMs 缓存(cache)
算术:请求自带路由信息,一台普通轮询负载均衡就能分发;tools/list 还能按服务器给的 ttlMs 缓存复用——扩容从「迁移会话」变成「直接加实例」。
Step 3:缓存 tools/list 省掉多少次往返
项每次调用都拉按 ttlMs 缓存(cache)
1,000 次工具调用1,000 次额外 tools/list1 次
额外往返占比每次多 1 次几乎为 0
算术(示意:假设客户端每次都重新探测一次列表):若每次调用都探测一次列表,1,000 次调用就多 1,000 次往返;按 ttlMs 缓存后只需 1 次(1,000 → 1,省 999 次)。无状态让任意实例都能答,可缓存让重复探测不再重复。
🎯 关键认知

无状态化不是功能缩水,而是把可扩展性从应用层挪回协议层:协议无状态、应用仍可有状态。配套三件事同向——请求自带路由头(Mcp-Method / Mcp-Name)、列表结果可缓存(ttlMs)、正式弃用政策。注意:roots / sampling / logging 在 2026-07-28 版被弃用,其中 Sampling(客户端的「服务器反过来请客户端调模型」能力)官方建议迁移到直接对接 LLM provider API;原先「服务端发起请求」这类机制则改由 Multi Round-Trip Requests(MRTR)承载。

AI 助教
👋 对无状态化有疑问?比如「无状态了上下文放哪」「ttlMs 缓存会不会不新鲜」?

说清你的部署方式,我帮你判断该走有状态还是无状态。