最近MCP大火,本文尝试揭开它神秘的面纱。文章较长,分为上下两篇。
MCP协议遵循客户端-主机-服务器架构,其中一个主机应用运行多个客户端实例,每个客户端实例维护了和服务器建立的独立的连接。
通过这种架构可以把一些敏感信息,如访问内部系统的密钥放到Server端,避免泄露的风险。
Host进程充当容易和协调器:
每个客户端由主机创建并维护一个独立的服务器连接:
服务器提供专门的上下文和功能:
协议层负责消息封装、请求与响应的关联,以及高级通信模式的处理。
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:
"""处理来自对方的通知"""
传输层处理客户端和服务器之间的实际通信,现在定义了两种传输机制:
所有传输方式均使用 JSON-RPC 2.0 进行消息交换。
SSE(Server-Sent Events)是一种基于 HTTP 的单向通信机制,允许服务端持续不断地向客户端推送数据。它通过保持一个 HTTP 长连接,将多个事件以“流”的方式发送给客户端,适合用于实时消息推送、进度更新、通知等场景。客户端连接后,会一直监听服务端传来的消息,直到连接关闭或中断。
SSE的特点是:
现在大多数大模型(如 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
):用于客户端建立连接并接收服务端消息;/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 | 应用控制 | 由客户端管理并附加的上下文数据 | 文件内容、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 |
支持非标准实验性功能 |
运行
在运行阶段,客户端和服务器根据协商的能力进行消息交换。
双方应该:
关闭
在关闭阶段,一方会优雅地终止协议连接,通过底层传输机制指示连接终止。
模型上下文协议和函数调用都是扩展LLM功能的方法,尽管它们的目标类似,但在架构、范围和实现方式上存在显著差异。
函数调用:
模型上下文协议: