MCP:宿主、客户端、服务器

8 分钟读完

P3
深入解析 · 协议与互操作

模型上下文协议:宿主、客户端、服务器,以及三种原语。

MCP 的参与者模型完好地熬过了 2026-07-28 版本;它的生命周期没有。initialize 握手已经不存在了——每一次请求都在 params._meta 里重述自己的协议版本与能力,而一条打开的连接不再是一个会话。剩下的是值得学一次就够用的那部分:三种角色(宿主、客户端、服务器)、三种意图类型化的原语(资源、工具、提示),以及一个 JSON-RPC 层——在这一层里,凡是要跨调用存活的东西,如今都得有一个你自己起的名字。

STEP 1

参与者模型:宿主、客户端、服务器。

MCP——由 Anthropic 于 2024 年 11 月推出,由一份开放规范治理——定义三种角色。把它们理清就是全部心智模型:

  • 宿主(Host)。用户交互且内嵌模型的应用——IDE 助手、桌面聊天应用、智能体运行时。宿主管理模型、强制用户同意、并协调一个或多个客户端。
  • 客户端(Client)。宿主内部的连接器,与单个服务器严格一对一。若宿主连三个服务器,就跑三个客户端。客户端讲协议并隔离该服务器的连接。
  • 服务器(Server)。一个独立程序,通过协议暴露能力——工具、资源、提示。服务器包装一个系统(文件系统、数据库、SaaS API),可被任何支持 MCP 的宿主复用。
┌─ Host (the agent app) ─────────────────────┐
│  model + consent + orchestration            │
│   ├── Client A ───stdio───>  Server: files  │
│   ├── Client B ───HTTP────>  Server: github │
│   └── Client C ───HTTP────>  Server: db     │
└─────────────────────────────────────────────┘
   one client  ⇄  one server  (1:1, isolated)

客户端对服务器 1:1 隔离是有意为之:它按连接界定信任。恶意或有缺陷的服务器看不到另一个服务器的流量,宿主决定哪些服务器对某个模型上下文可见。这是"互操作问题"一文 M+N 论证的协议化表达——把每个系统包装一次为服务器,教会宿主客户端一次。有一点留到 STEP 3 展开:连接界定信任,但自 2026-07-28 起它不承载任何状态,所以"每个服务器一个客户端"是一条隔离性质,而不是一个会话。

STEP 2

三种服务器原语:资源、工具、提示。

一个 MCP 服务器可提供三类能力。区分在于谁掌握控制权:

资源(Resources)是应用控制的上下文:宿主可读取并放入模型上下文的类文件数据——文件内容、数据库行、API 响应。每个资源有一个 URI(例如 file:///repo/README.md 或自定义协议)。资源在意图上是面向读取且无副作用的;宿主决定何时呈现它们。

工具(Tools)是模型控制的动作:模型可选择调用的函数,每个用一份 JSON Schema 输入描述(与"工具调用标准"一文同一底层)。工具可有副作用——写文件、开 PR、跑查询——故 MCP 期望在执行前由宿主居中获取用户同意。

提示(Prompts)是用户控制的模板:服务器发布的可复用、可参数化的提示/工作流片段,宿主可将其呈现为命令("总结这个 PR")并用参数展开。它们让服务器交付的是专长,而不只是原始能力。

控制轴助记:资源 = 应用选择加载什么上下文;工具 = 模型选择采取什么动作;提示 = 用户选择运行哪个工作流。同一协议,三种意图。

STEP 3

在线格式:JSON-RPC 2.0,以及那个已经消失的生命周期。

MCP 消息是 JSON-RPC 2.0:带 id 期待响应的请求、结果、错误,以及单向通知。直到 2025-11-25 版本为止,每个会话都以一次 initialize 握手开始,双方在其中交换协议版本与一个能力对象。2026-07-28 版本删掉了 initialize 与 notifications/initialized,把这场协商挪到了每一次单独的请求上:params._meta 承载 io.modelcontextprotocol/protocolVersion 与 io.modelcontextprotocol/clientCapabilities(两者都是 REQUIRED)、io.modelcontextprotocol/clientInfo(SHOULD),以及可选的 io.modelcontextprotocol/logLevel。少了必填字段的请求会得到 -32602,在 HTTP 上还配一个 400;依赖了自己没有声明过的能力的请求会得到 MissingRequiredClientCapabilityError(-32021),同样是 400。2026-07-28 版本那一篇走完整条迁移路径;就架构而言重要的是后果——请求如今自带自我描述,而连接不再承载任何协商过的上下文。

最接近替代品的是 server/discover,而它要求级别上的那点不对称正是要记住的地方。服务器必须(MUST)实现它;客户端则完全可以从不调用它,直接发出任意 RPC,若猜错了就处理 UnsupportedProtocolVersionError(-32022)。真调用它时,响应里带 supportedVersions、capabilities、可选的 instructions 字符串,以及 ttlMs 和 cacheScope——让客户端知道这份答案能缓存多久、能共享到多大范围。有一个细节已经让真实部署付出过代价:serverInfo 不是该结果的顶层字段,它住在 _meta 里的 io.modelcontextprotocol/serverInfo 下。TypeScript SDK 发的是顶层那种形状,造成的是硬连接失败而不是优雅降级——这是一条经久有效的提醒:"SDK 就是这么做的"和"规范就是这么写的"不是同一个主张。

# 1. Optional for clients, mandatory for servers: what is this server?
{"jsonrpc":"2.0","id":1,"method":"server/discover",
 "params":{"_meta":{
   "io.modelcontextprotocol/protocolVersion":"2026-07-28",
   "io.modelcontextprotocol/clientCapabilities":{}}}}

# 2. Versions, capabilities, cache hint -- and serverInfo inside _meta.
{"jsonrpc":"2.0","id":1,"result":{
   "supportedVersions":["2026-07-28","2025-11-25"],
   "capabilities":{"tools":{"listChanged":true},
                    "resources":{},"prompts":{}},
   "instructions":"Search issues before opening a PR.",
   "resultType":"complete","ttlMs":3600000,"cacheScope":"public",
   "_meta":{"io.modelcontextprotocol/serverInfo":{"name":"github","version":"2.1"}}}}

# 3. Or skip it: every request restates the handshake for itself.
{"jsonrpc":"2.0","id":2,"method":"tools/list",
 "params":{"_meta":{
   "io.modelcontextprotocol/protocolVersion":"2026-07-28",
   "io.modelcontextprotocol/clientCapabilities":{},
   "io.modelcontextprotocol/clientInfo":{"name":"my-host","version":"1.0"}}}}

有两条规则让这种"自我描述"在版本混杂的机群里还活得下去。每个 result 对象现在都带 resultType——"complete"、"input_required",或者扩展给出的值(比如 Tasks 扩展的 "task")——而客户端必须(MUST)把缺失的 resultType 当作 "complete"。后面这条就是整个向后兼容故事的一句话版本:一台从没听说过这个字段的服务器,返回的结果新客户端照样读得对。而握手当年隐含的那份状态,也随它一起没了,且没有替代品。规范要求跨请求的状态"必须由客户端在每次请求上传递的一个显式标识来引用",并用一句话堵死了最顺手的绕法:"一条打开的连接——比如一个 STDIO 进程——不是一次对话或一个会话。"stdio 子进程是一根管子;挂着不关的 HTTP 流也是一根管子。凡是要跨调用存活的东西,都需要一个你自己设计的句柄:由结果返回,再作为参数穿回来。

客户端用列举方法发现服务器提供什么,再用调用方法使用它:

server/discover   -> supportedVersions, capabilities, instructions, ttlMs
tools/list        -> [{name, description, inputSchema}, …]
tools/call        -> run a tool by name with arguments
resources/list    -> [{uri, name, mimeType}, …]
resources/read    -> fetch a resource's contents by uri
prompts/list      -> [{name, arguments}, …]
prompts/get       -> expand a prompt template with args
notifications/*   -> list_changed, progress, message, cancelled, …

因此能力发现是运行时的,不是构建时的:宿主通过调用 tools/list 得知服务器的工具,或者调一次 server/discover 并按 ttlMs 缓存那份答案,而一条 list_changed 通知告诉它集合已变。这些通知不再会不请自来地推给宿主——在 HTTP 上,客户端要靠开一条 subscriptions/listen 流、并点名自己想要哪些,才收得到。能力发现有专文;这里的要点是 MCP 把它放进了请求路径,而不是放进一个生命周期。

STEP 4

传输方式,以及安全在哪里进入。

MCP 把消息格式(JSON-RPC)与承载它的传输分离。规范定义两种传输:

  • stdio。宿主把服务器作为子进程启动,通过其标准输入/输出交换 JSON-RPC。适合本地工具(你机器上的文件系统或 Git 服务器):无网络面,生命周期绑定进程——不过那根打开的管子是传输,不是会话,它并不授予你在一旁留着状态的许可。
  • 可流式 HTTP。服务器是一个远程 HTTP 端点;客户端 POST 请求,服务器以普通 JSON 作答,或把响应以服务器发送事件流式返回。非请求触发的服务器到客户端消息,走的是客户端有意打开的那条 subscriptions/listen 流——MCP 端点上的 GET 以及它承载的那条独立流,已在 2026-07-28 被移除。这是托管的、多客户端服务器的路径,也是认证(规范将远程认证对齐到 OAuth 2 风格的授权)与传输安全所在。

有一个方向被整个移除了,值得点名,因为旧文档里到处都是它。直到 2025-11-25 为止,服务器可以在执行中途回调宿主:sampling/createMessage 请宿主代它跑一次模型补全,elicitation/create 请宿主向用户收集输入,roots/list 问哪些目录在范围内。2026-07-28 把这三个作为"服务器发起的请求"全部删掉了。能力本身以 MRTR——Multi Round-Trip Requests,多轮往返请求——的形式活了下来:它保留特性,同时把"谁握着这次调用"反转过来。服务器不再回调,而是返回 resultType: "input_required",带上一个 inputRequests 映射与一个不透明的 requestState,然后停下。客户端爱怎么解决就怎么解决——问用户、查策略、填默认值——再把原方法连同 inputResponses 重新发一次,且用一个不同的 JSON-RPC id。

# The server needs a decision. It does not call back -- it returns.
{"jsonrpc":"2.0","id":7,"result":{
   "resultType":"input_required",
   "inputRequests":{
     "confirm":{"method":"elicitation/create",
                "params":{"message":"Merge PR 42 into main?",
                          "requestedSchema":{"type":"object",
                            "properties":{"ok":{"type":"boolean"}},
                            "required":["ok"]}}}},
   "requestState":"v1.opaque.9f2c81ae"}}

# The client answers, then re-sends the same call under a DIFFERENT id.
{"jsonrpc":"2.0","id":8,"method":"tools/call",
 "params":{"name":"merge_pr","arguments":{"number":42},
           "inputResponses":{"confirm":{"action":"accept",
                                        "content":{"ok":true}}},
           "requestState":"v1.opaque.9f2c81ae",
           "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
                     "io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}

架构上的收益是:控制流反转没有了。服务器不再需要一条挂着不关的流才能在调用中途够到宿主——这恰恰是会话得以被删掉的原因;而且同意点挪到了一个更好的位置,因为宿主现在是在自己的控制流里回答一个问题,而不是在服务一个来自它并不信任的服务器的中断。任何描述服务器"回调进宿主"的文档,讲的都是被取代的那一版;现在要认的形状,是一个说自己还没结束的 result。

MCP 标准化的是通道,不是信任。被连接的服务器可返回模型将读取的内容——一个提示注入面——且一次工具调用可有真实副作用。协议的职责是让同意点显式(宿主把守工具执行,以及服务器在调用中途索要的每一项输入);授予什么由你决定。另外注意 clientInfo 与 serverInfo 都是自报的,协议从不校验。威胁模型、能力范围与来源溯源防御见"安全、对齐与智能体安全"深入探讨系列。把"服务器讲 MCP"当作描述形状,永远不是授权。

主线:MCP 是一个参与者模型(宿主/客户端/服务器)加三种意图类型化原语(资源/工具/提示)加一个 JSON-RPC 层——在这一层里,协商搭的是每一次请求的车,而不是一个生命周期的车——运行在可插拔的传输(stdio 或可流式 HTTP)之上。这就是全部架构;本系列其余部分考察它的各部分——结构化 I/O、发现、智能体间扩展——如何泛化。