引言
最近MCP大火,本文尝试揭开它神秘的面纱。文章较长,分为上下两篇。
架构
MCP协议遵循客户端-主机-服务器架构,其中一个主机应用运行多个客户端实例,每个客户端实例维护了和服务器建立的独立的连接。
- Host: 希望通过MCP访问数据的程序,比如一个聊天应用程序。
- Client: 与服务器保持1:1连接(会话)的客户端,Host通过这个Client连接不同的Server提供的功能。
- Server:通过MCP公开特定功能的轻量级程序,比如上图的Server可以访问文档(Wiki)、谷歌搜索和代码执行等。
通过这种架构可以把一些敏感信息,如访问内部系统的密钥放到Server端,避免泄露的风险。
Host
Host进程充当容易和协调器:
- 创建和管理多个客户端实例
- 控制客户端连接权限和生命周期
- 执行安全策略和同意要求
- 处理用户授权
- 协调LLM的集成和采样
- 管理跨客户端的上下文聚合
Client
每个客户端由主机创建并维护一个独立的服务器连接:
- 为每个服务器建立一个有状态会话
- 处理协议协商和能力交换
- 双向路由协议消息
- 管理订阅和通知
- 维护服务器之间的安全边界
Server
服务器提供专门的上下文和功能:
- 通过MCP原语暴露资源、工具和提示
- 独立运作并专注于特定职责
- 通过客户端接口请求采样
- 可以是本地进程,也可以是远程服务
协议层
协议层负责消息封装、请求与响应的关联,以及高级通信模式的处理。
class Session(BaseSession[RequestT, NotificationT, ResultT]):
async def send_request(
self,
request: RequestT,
result_type: type[Result]
) -> Result:
"""
发送请求并等待响应。如果响应包含错误抛出McpError
"""
async def send_notification(
self,
notification: NotificationT
) -> None:
"""发送不需要响应的单向通知"""
async def _received_request(
self,
responder: RequestResponder[ReceiveRequestT, ResultT]
) -> None:
"""处理来自对方的请求"""
async def _received_notification(
self,
notification: ReceiveNotificationT
) -> None:
"""处理来自对方的通知"""
传输层
传输层处理客户端和服务器之间的实际通信,现在定义了两种传输机制:
- Stdio
- 使用标准输入/输出进行通信
- 适用于本地进程
- 带SSE的HTTP(后续版本升级为Streamable HTTP)
- 使用 Server-Sent Events (SSE) 进行服务器向客户端的消息传输
- 使用 HTTP POST 进行客户端向服务器的消息传输
所有传输方式均使用 JSON-RPC 2.0 进行消息交换。
带SSE的HTTP
SSE(Server-Sent Events)是一种基于 HTTP 的单向通信机制,允许服务端持续不断地向客户端推送数据。它通过保持一个 HTTP 长连接,将多个事件以“流”的方式发送给客户端,适合用于实时消息推送、进度更新、通知等场景。客户端连接后,会一直监听服务端传来的消息,直到连接关闭或中断。
SSE的特点是:
- 单向: 服务器 → 客户端
- 基于HTTP
- 通用性
- 调试更简单
- 传输格式为文本(UTF-8)
- 内置自动重连
现在大多数大模型(如 OpenAI、Claude、Gemini、Mistral 等)在支持流式响应(streaming response)时,通常是通过 SSE(Server-Sent Events)协议来实现的。
一旦启用 stream
,返回的 HTTP 响应就变成了 Content-Type: text/event-stream
,也就是 SSE 格式:
返回数据大概像这样:
data: {"id": "...", "choices": [{"delta": {"content": "你"}}]}
data: {"id": "...", "choices": [{"delta": {"content": "好"}}]}
data: {"id": "...", "choices": [{"delta": {"content": ","}}]}
data: {"id": "...", "choices": [{"delta": {"content": "我"}}]}
data: {"id": "...", "choices": [{"delta": {"content": "是"}}]}
...
data: [DONE]
再来看一下带SSE的HTTP,在该模式中,服务端作为独立进程运行。服务端必须提供两个端点:
- SSE端点(
/sse
):用于客户端建立连接并接收服务端消息; - HTTP POST端点(
/messages/
):用于客户端发送消息给服务端;
当客户端连接后,服务端必须发送一个 endpoint
事件,其中包含客户端后续用来发送消息的 URI。之后所有客户端消息必须通过 HTTP POST 请求发送到该端点。
服务端的消息通过 SSE 的 message
事件发送,其内容以 JSON 编码并包含在事件数据中。
这就是为什么在底层API实现中需要暴露这两个端点:
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message),
],
)
术语 & 流程
服务器功能
服务器通过 MCP 提供语言模型上下文的基本构建块。这些原语使客户端、服务器和语言模型之间能够进行丰富的交互:
- Prompts(提示):预定义的模板或指令,用于引导语言模型的交互
- Resources(资源):提供给模型的结构化数据或内容,以增加上下文信息
- Tools(工具):可执行的函数,使模型能够执行操作或获取信息
每种原语可按以下控制层级进行归类:
原语 | 控制方式 | 描述 | 示例 |
---|---|---|---|
Prompts | 用户控制 | 由用户选择触发的交互式模板 | 斜杠命令、菜单选项 |
Resources | 应用控制 | 由客户端管理并附加的上下文数据 | 文件内容、Git 历史 |
Tools | 模型控制 | 暴露给 LLM 以执行操作的函数 | API POST 请求、文件写入 |
我们这里重点关注工具能力。
工具调用
MCP允许服务器暴露工具,以供LLM使用。工具使LLM能与外部系统交互,例如查询数据库、调用API等。每个工具有唯一的名称、包含描述其模式的元数据。
工具定义包含:
name
: 工具的唯一标识符description
:工具功能描述inputSchema
:JSON Schema,定义预期参数annotations
:可选,描述工具行为
支持工具的服务器必须声明工具能力:
{
"capabilities": {
"tools": {
"listChanged": true
}
}
}
listChanged
告诉服务器是否会在可用工具列表更改时发送通知。
获取工具列表
客户端发送tools/list
请求来发现可用工具:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": "optional-cursor-value"
}
}
响应:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather",
"description": "获取指定位置的当前天气信息",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称或邮政编码"
}
},
"required": ["location"]
}
}
],
"nextCursor": "next-page-cursor"
}
}
调用工具
客户端发送tools/call
请求来调用工具:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "深圳"
}
}
}
响应:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "深圳当前天气:\n温度:18°C\n天气情况:晴转多云"
}
],
"isError": false
}
}
工具列表变更通知
若服务器声明了listChanged
能力,则当可用工具列表更改时,服务器应发送通知:
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
完整流程:
客户端功能
客户端可以实现额外功能来丰富连接的MCP服务器。
Roots
MCP为客户端提供了一种向服务器公开文件系统"根"的标准化方法,定义服务器可以访问客户端哪些目录和文件。
Sampling
MCP为服务器提供的一种标准化方法,可以让服务器通过客户端从LLM中请求采样(完成或生成)。使得服务器也能够利用AI功能。
能力协商
MCP采用基于能力的协商系统,客户端和服务器在初始化时明确声明其支持的功能。
- 服务器声明其支持的能力,如资源订阅、工具支持和提示模板等。
- 客户端声明其支持的能力,如采样支持和通知处理。
- 双方在整个会话过程中必须遵守已声明的能力。
- 可以通过协议扩展来协商额外的功能。
每种能力都解锁特定的协议功能,例如:
- 服务器功能需要在服务器能力声明中公开
- 发送资源订阅通知要求服务器声明支持订阅
- 工具调用要求服务器声明工具能力
- 采样需要客户端在能力声明中明确支持
这种能力协商机制确保客户端和服务器能明确理解各自的支持功能,同时保持协议的可扩展性。
生命周期
MCP为客户端与服务器之间的连接定义了一个严谨的生命周期:
- 初始化:能力协商与版本协议确认
- 运行:正常的协议通信
- 关闭:连接的优雅关闭
初始化
初始化阶段是客户端与服务器的第一步交互。客户端与服务器需要:
- 确定协议版本的兼容性
- 交换并协商能力
- 共享实现细节
客户端必须通过发送initialize
请求启动该阶段,其中包含: 支持的协议版本、客户端能力、客户端实现信息。
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
}
}
}
服务器必须响应其自身的能力和信息:’
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"logging": {},
"prompts": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "ExampleServer",
"version": "1.0.0"
}
}
}
成功初始化后,客户端必须发送initialized
通知,表明其已准备好进行正常操作:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
版本协商
在initialize
请求中,客户端必须发送其支持的协议版本,通常选择支持的最新版本。
如果服务器支持请求的协议版本,则必须以相同版本响应;否则服务器必须以其支持的其他协议版本响应。
如果客户端不支持服务器响应的协议版本,则应断开连接。
能力协商
客户端和服务器的能力决定了会话期间可用的可选协议功能:
分类 | 能力 | 描述 |
---|---|---|
Client | roots | 提供文件系统root的能力,即定义服务器可以拥有哪些目录和文件的访问权限。 |
Client | sampling | 支持LLM采样请求,使服务器能通过客户端请求LLM采样(补全或生成),让服务器能利用客户端的AI功能。 |
Client | experimental | 支持非标准实验性功能 |
Server | prompts | 提供提示词模板 |
Server | resources | 提供可读的资源 |
Server | tools | 暴露可调用的工具 |
Server | logging | 发送结构化的日志消息 |
Server | experimental | 支持非标准实验性功能 |
运行
在运行阶段,客户端和服务器根据协商的能力进行消息交换。
双方应该:
- 遵守协商的协议版本
- 仅使用已协商成功的能力
关闭
在关闭阶段,一方会优雅地终止协议连接,通过底层传输机制指示连接终止。
常见问题
MCP和Function Calling的区别是什么
模型上下文协议和函数调用都是扩展LLM功能的方法,尽管它们的目标类似,但在架构、范围和实现方式上存在显著差异。
函数调用:
- 目的: 允许LLM根据用户提示调用预定义的函数或API,从而获取数据或执行特定操作。
- 机制: 开发者定义一组特定参数的函数,供LLM使用。当处理提示词时,LLM判断是否需要调用函数,并生成必要的参数以执行该函数。
- 局限:
- 供应商特定实现: 不同的LLM提供商可能有不同的函数调用实现方式,导致不一致和集成挑战。
- 紧密耦合: 函数通常和LLM应用程序紧密集成,降低了适应或扩展的灵活性。
模型上下文协议:
- 目的: 旨在标准化AI应用与外部工具、数据源和系统之间的交互,为此类集成提供统一的协议。
- 机制: MCP采用客户端-主机-服务器架构:
- 服务器:通过标准化API公开工具、资源和提示。
- 客户端:驻留在AI应用程序中,管理与MCP服务器的连接,促进LLM与外部系统之间的通信。
- 主机:用户交互的应用程序,包含MCP客户端以连接各种服务器。
- 优势:
- 组件解耦: 通过将工具与LLM代理分离,MCP允许更模块化和灵活的集成。
- 标准化: 提供连接AI模型到各种数据源和工具的一致一致,减少定制的需要。
- 可扩展性:便于集成多个工具,而不会导致进程复杂性成倍增长。
- 安全:调用工具的API_KEY可以放到服务端,避免客户端泄露的风险。
参考
- https://modelcontextprotocol.io/
- https://spec.modelcontextprotocol.io/specification/2025-03-26/
- https://github.com/sidharthrajaram/mcp-sse