MCP 入门
2026-03-24 23:58:49 # LLM

写在前面:

本文章是关于 Anthropic 公司开源协议框架 MPC 的部分学习笔记。碍于本人学识有限,部分叙述难免存在纰漏,请读者注意甄别。

参考文档:


MCP 与 FunctionCalling 的关系

什么是 FunctionCalling

Function Calling 能让 LLM 触发外部行为,是因为:它把“行为”编码成了模型可以预测的“文本结构”。

无论多复杂,LLM 本质只有一个能力:预测下一个 token。那么为什么“预测 token”可以变成“触发行为”?

  • 没有 Function Calling 时,模型只能输出:
1
今天天气是 20 度
  • 有 Function Calling 时,我们给模型一种“特殊语言”:
1
2
3
4
5
6
{
"name": "get_weather",
"arguments": {
"city": "上海"
}
}

由此可见,函数调用(Function Calling)并不是真正调用,而是一段“特定格式(格式化)的文本”。

真正的触发发生在哪里?

  • 模型做的事:输出 JSON
  • 系统做的事:解析 JSON $\to$ 调用函数 $\to$ 返回结果

所以,触发行为的不是 LLM本身,而是“系统解释/调用了 LLM 的输出”。

为什么模型“会说这种话”?

因为我们在训练或提示中做了两件事:给它工具说明(Schema)/告诉它规则,所以当 $P(\text{function call} | 输入) > P(文本 | 输入)$时,模型就会输出“特定格式的文本”。

由此可见,针对不同的模型实现相同的 Function Calling 往往需要针对性开发适配,而 MCP 的出现很好的解决了规范性的问题(协议)。

Function Calling VS. MCP

Function Calling 解决了“模型怎么调用自定义函数”,但在实际使用中还面临一些问题:

  • 多个工具组成的调用链(先查天气、再发邮件)
  • 工具参数结构的规范与自动注册
  • 不同调用方式的适配(HTTP、本地插件等)
  • 在不同模型间复用统一的工具体系

MCP 是由 Anthropic 推出的开放标准协议,旨在为大模型和外部工具之间的通信提供通用接口。它不是 Function Calling 的替代,而是对其在执行层面的进一步规范和封装,使工具系统更易接入、更易管理、更易复用

维度 Function Calling MCP
目标 让模型调用函数 标准化工具接入
面向对象 LLM 系统 / Agent / 工具生态
关注点 调用“哪个函数 + 参数” 工具“怎么被发现 + 调用”
本质 推理能力增强 通信协议

MCP 核心角色

MCP 采用客户端-服务端架构(CS),其中 MCP 宿主(Host),如 Claude Code 等 AI 应用,会建立与一个或多个 MCP 服务端的连接。MCP 宿主通过为每个 MCP 服务端创建一个 MCP 客户端来实现这一点。每个 MCP 客户端与其对应的 MCP 服务端保持专用连接。以下是 MCP 官方展示的架构:

2430457-20250512135338388-1937866079

MCP Client

  • 向 MCP Server 请求工具列表
  • 使用 HTTP 或 stdio 协议发起工具调用请求

例如,聊天应用类,提供自然语言交互服务,让用户通过对话调用 AI 能力;编码工具类,在 IDE 里调用外部应用和系统的能力;任务自动化类,帮用户自动化执行重复性任务,如数据处理、流程调度,以提升效率。

MCP Server

  • 接收 tool_calls,根据调用内容执行对应工具
  • 返回统一格式的结构化结果

例如,数据库类(如 ClickHouse、Supabase)负责数据存储、查询与管理;设计类(如 Figma、Blender)支撑设计创作、文件协作等功能;生产力工具类(如 Notion、Obsidian)提供笔记管理、知识整理等办公协作服务;支付类(如 Stripe),处理在线支付交易,支持商业场景的资金流转。

MCP 分层

MCP 由两层组成

  • 数据层:定义基于 JSON-RPC 的客户端-服务端通信协议,包括生命周期管理以及工具、资源、提示词和通知等核心原语。
  • 传输层:定义支持客户端和服务端之间数据交换的通信机制和通道,包括特定传输方式的连接建立、消息分帧和授权。

从概念上讲,数据层是内层,而传输层是外层。

数据层

数据层实现了一个基于 JSON-RPC 2.0 的交互协议,定义了消息结构和语义。该层包括

  • 生命周期管理:处理客户端和服务端之间的连接初始化、能力协商和连接终止
  • 服务端特性:使服务端能够提供核心功能,包括用于 AI 操作的“工具”、用于上下文数据的“资源”以及用于交互模板的“提示词”
  • 客户端特性:使服务端能够请求客户端从宿主 LLM 进行采样、引导用户输入以及向客户端记录日志消息
  • 实用特性:支持额外功能,如用于实时更新的“通知”和用于长时间运行操作的“进度跟踪”

传输层

传输层管理客户端和服务端之间的通信通道和认证。它负责处理 MCP 参与者之间的连接建立、消息分帧和安全通信。MCP 支持两种传输机制:

  • Stdio 传输:使用标准输入/输出流进行同一机器上本地进程间的直接通信,提供无网络开销的最佳性能。
    • 可流式 HTTP 传输:使用 HTTP POST 发送客户端到服务端的请求,并结合可选的服务器发送事件 (SSE) 实现流式功能。此传输方式支持远程服务器通信,并支持标准 HTTP 认证方法,包括 Bearer 令牌、API 密钥和自定义标头。MCP 建议使用 OAuth 来获取身份验证令牌。

传输层将通信细节从协议层中抽象出来,使得相同的 JSON-RPC 2.0 消息格式可以在所有传输机制中使用。

MCP 原语

MCP 原语是 MCP 中最重要的概念。它们定义了客户端和服务端可以相互提供的内容。这些原语指定了可以与 AI 应用共享的上下文信息类型以及可以执行的操作范围。MCP 定义了 服务端 可以暴露的三种核心原语:

  • 工具 (Tools):AI 应用可以调用以执行操作的可执行函数(例如:文件操作、API 调用、数据库查询)
  • 资源 (Resources):为 AI 应用提供上下文信息的数据源(例如:文件内容、数据库记录、API 响应)
  • 提示词 (Prompts):帮助构建与语言模型交互的可重用模板(例如:系统提示词、少样本示例)

每种原语类型都有关联的发现 (*/list)、获取 (*/get) 以及在某些情况下的执行 (tools/call) 方法。MCP 客户端将使用 */list 方法来发现可用的原语。

例如,客户端可以先列出所有可用工具 (tools/list) 然后执行它们。这种设计允许列表是动态的。举一个具体的例子,考虑一个提供数据库上下文的 MCP 服务端。它可以暴露用于查询数据库的“工具”,一个包含数据库模式的“资源”,以及一个包含与工具交互的少样本示例的“提示词”。

MCP 还定义了 客户端 可以暴露的原语。这些原语允许 MCP 服务端作者构建更丰富的交互。

  • 采样 (Sampling):允许服务端请求客户端 AI 应用进行语言模型补全。当服务端作者想要访问语言模型,但希望保持模型独立且不愿在 MCP 服务端中包含语言模型 SDK 时,这非常有用。他们可以使用 sampling/complete 方法请求客户端 AI 应用进行语言模型补全。
  • 引导 (Elicitation):允许服务端向用户请求额外信息。当服务端作者想要从用户那里获取更多信息或请求对某项操作的确认时,这非常有用。他们可以使用 elicitation/request 方法向用户请求额外信息。
  • 日志记录 (Logging):使服务端能够向客户端发送日志消息,用于调试和监控目的。

数据层生命周期序列

1. 初始化(生命周期管理)

MCP 从通过能力协商握手(capability negotiation handshake)进行的生命周期管理开始,客户端发送 initialize 请求以建立连接并协商支持的特性。

  • 初始化请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-05-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "example-client",
"version": "1.0.0"
}
}
}
  • 初始化响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-05-18",
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {}
},
"serverInfo": {
"name": "example-server",
"version": "1.0.0"
}
}
}

初始化过程是 MCP 生命周期管理的关键部分,具有几个重要目的

  1. 协议版本协商protocolVersion 字段(例如 "2025-05-18")确保客户端和服务端使用的是兼容的协议版本。这可以防止不同版本尝试交互时可能发生的通信错误。如果没有协商出共同兼容的版本,则应终止连接。
  2. 能力发现capabilities 对象允许各方声明它们支持哪些特性,包括它们可以处理哪些原语(工具、资源、提示词)以及是否支持通知等特性。这通过避免不支持的操作来实现高效通信。
  3. 身份交换clientInfoserverInfo 对象提供标识和版本信息,用于调试和兼容性目的。

在此示例中,能力协商展示了 MCP 原语是如何声明的:

  • 客户端能力

    • "elicitation": {} - 客户端声明它可以处理用户交互请求(可以接收 elicitation/create 方法调用)
  • 服务端能力:

    • "tools": {"listChanged": true} - 服务端支持工具原语,并且可以在工具列表更改时发送 tools/list_changed 通知
    • "resources": {} - 服务端还支持资源原语(可以处理 resources/listresources/read 方法)

初始化成功后,客户端发送通知表示已就绪

1
2
3
4
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

在初始化期间,AI 应用的 MCP 客户端管理器会建立与已配置服务端的连接,并存储它们的能力以备后用。应用使用此信息来确定哪些服务端可以提供特定类型的功能(工具、资源、提示词)以及它们是否支持实时更新。

1
2
3
4
5
6
7
# 伪代码
async with stdio_client(server_config) as (read, write):
async with ClientSession(read, write) as session:
init_response = await session.initialize()
if init_response.capabilities.tools:
app.register_mcp_server(session, supports_tools=True)
app.set_server_ready(session)

2. 工具发现(原语)

既然连接已建立,客户端就可以通过发送 tools/list 请求来发现可用的工具。此请求是 MCP 工具发现机制的基础——它允许客户端在尝试使用工具之前了解服务端上有哪些工具可用。

  • 工具列表请求:tools/list 请求很简单,不包含任何参数。
1
2
3
4
5
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
  • 工具列表响应:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "calculator_arithmetic",
"title": "Calculator",
"description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
"inputSchema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
}
},
"required": ["expression"]
}
},
{
"name": "weather_current",
"title": "Weather Information",
"description": "Get current weather information for any location worldwide",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, address, or coordinates (latitude,longitude)"
},
"units": {
"type": "string",
"enum": ["metric", "imperial", "kelvin"],
"description": "Temperature units to use in response",
"default": "metric"
}
},
"required": ["location"]
}
}
]
}
}

响应包含一个 tools 数组,提供有关每个可用工具的详尽元数据。这种基于数组的结构允许服务端同时暴露多个工具,同时保持不同功能之间的清晰界限。响应中的每个工具对象包含几个关键字段:

  • name:服务端命名空间内工具的唯一标识符。这是工具执行的主键,应遵循清晰的命名模式(例如:calculator_arithmetic 而不仅仅是 calculate
  • title:工具的人类可读显示名称,客户端可以向用户显示
  • description:关于工具用途及何时使用的详细说明
  • inputSchema:一个 JSON Schema,定义了预期的输入参数,支持类型验证并提供关于必填和可选参数的清晰文档。

AI 应用从所有连接的 MCP 服务端获取可用工具,并将它们合并到一个语言模型可以访问的统一工具注册表中。这使得 LLM 能够理解它可以执行哪些操作,并在对话期间自动生成适当的工具调用。

1
2
3
4
5
6
# 伪代码
available_tools = []
for session in app.mcp_server_sessions():
tools_response = await session.list_tools()
available_tools.extend(tools_response.tools)
conversation.register_available_tools(available_tools)

3. 工具执行(原语)

客户端现在可以使用 tools/call 方法执行工具。这演示了 MCP 原语在实践中是如何使用的:在发现可用工具后,客户端可以使用适当的参数调用它们。

  • 工具调用请求:tools/call 请求遵循结构化格式,确保客户端和服务端之间的类型安全和清晰通信。
1
2
3
4
5
6
7
8
9
10
11
12
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "weather_current",
"arguments": {
"location": "San Francisco",
"units": "imperial"
}
}
}

请求结构包含几个重要部分

  1. name:必须与发现响应中的工具名称 (weather_current) 完全匹配。这确保了服务端可以正确识别要执行哪个工具。
  2. arguments:包含由工具的 inputSchema 定义的输入参数。在本例中
    • location: “San Francisco”(必填参数)
    • units: “imperial”(可选参数,如果不指定则默认为 “metric”)
  3. JSON-RPC 结构:使用标准 JSON-RPC 2.0 格式,带有用于请求-响应关联的唯一 id
  • 工具调用相应:
1
2
3
4
5
6
7
8
9
10
11
12
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
}
]
}
}

响应展示了 MCP 灵活的内容系统

  1. content 数组:工具响应返回内容对象的数组,支持丰富的多格式响应(文本、图像、资源等)
  2. content类型:每个内容对象都有一个 type 字段。在本例中,"type": "text" 表示纯文本内容,但 MCP 支持针对不同用例的各种内容类型。
  3. 结构化输出:响应提供了可操作的信息,AI 应用可以将其用作语言模型交互的上下文。

这种执行模式允许 AI 应用动态调用服务端功能,并接收可以集成到与语言模型对话中的结构化响应。

当语言模型在对话期间决定使用工具时,AI 应用会拦截该工具调用,将其路由到适当的 MCP 服务端执行,并将结果作为对话流的一部分返回给 LLM。这使得 LLM 能够访问实时数据并在外部世界执行操作。

1
2
3
4
5
# 伪代码
async def handle_tool_call(conversation, tool_name, arguments):
session = app.find_mcp_session_for_tool(tool_name)
result = await session.call_tool(tool_name, arguments)
conversation.add_tool_result(result.content)

4. 实时更新(通知)

MCP 支持实时通知,使服务端能够告知客户端更改而无需明确请求。这是一个保持 MCP 连接同步和响应灵敏的关键特性。当服务端的可用工具发生变化时,例如新功能上线、现有工具被修改或工具暂时不可用,服务端可以主动通知已连接的客户端。

MCP 通知的主要特性

  1. 无需响应:请注意通知中没有 id 字段。这遵循了 JSON-RPC 2.0 通知语义,即不期望也不发送响应。
  2. 基于能力:此通知仅由在初始化期间在其工具能力中声明了 "listChanged": true 的服务端发送(如步骤 1 所示)。
  3. 事件驱动:服务端根据内部状态更改决定何时发送通知,使 MCP 连接具有动态性和响应性。
  • 请求
1
2
3
4
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}

收到此通知后,客户端通常会通过请求更新后的工具列表来做出反应。这创建了一个刷新循环,使客户端对可用工具的理解保持最新。

  • 响应
1
2
3
4
5
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/list"
}

此通知系统至关重要,原因如下:

  1. 动态环境:工具可能会根据服务端状态、外部依赖或用户权限而出现或消失
  2. 效率:客户端不需要轮询更改;当更新发生时它们会被通知
  3. 一致性:确保客户端始终拥有关于可用服务端能力的准确信息
  4. 实时协作:支持能够适应不断变化的上下文的响应式 AI 应用

这种通知模式不仅限于工具,还扩展到了其他 MCP 原语,实现了客户端和服务端之间全面的实时同步。

当 AI 应用收到有关工具更改的通知时,它会立即刷新其工具注册表并更新 LLM 的可用能力。这确保了正在进行的对话始终可以访问最新的工具集,并且 LLM 可以动态适应新功能。

1
2
3
4
5
6
# 伪代码
async def handle_tools_changed_notification(session):
tools_response = await session.list_tools()
app.update_available_tools(session, tools_response.tools)
if app.conversation.is_active():
app.conversation.notify_llm_of_new_capabilities()

MCP 服务器

服务器通过三个构建块提供功能

功能 说明 示例 控制方
工具 LLM 可以主动调用的函数,并根据用户请求决定何时使用。
工具可以写入数据库、调用外部 API、修改文件或触发其他逻辑。
搜索航班 发送消息 创建日历事件 模型
资源 被动数据源,为上下文提供信息的只读访问,如文件内容、数据库模式或 API 文档。 检索文档 访问知识库 读取日历 应用程序
提示 预构建的指令模板,指导模型如何使用特定的工具和资源。 规划假期 总结会议内容 起草电子邮件 用户

工具

工具(Tools)使 AI 模型能够执行操作。每个工具定义了一个具有类型化输入和输出的特定操作。模型根据上下文请求执行工具。

工具是 LLM 可以调用的架构定义接口。MCP 使用 JSON Schema 进行验证。每个工具执行一个具有明确定义输入和输出的单一操作。工具在执行前可能需要用户授权,以确保用户能够控制模型采取的行动。

协议操作:

方法 目的 返回
tools/list 发现可用工具 带有架构的工具定义数组
tools/call 执行特定工具 工具执行结果

工具定义示例

1
2
3
4
5
6
7
8
9
10
11
12
13
{
name: "searchFlights",
description: "Search for available flights",
inputSchema: {
type: "object",
properties: {
origin: { type: "string", description: "Departure city" },
destination: { type: "string", description: "Arrival city" },
date: { type: "string", format: "date", description: "Travel date" }
},
required: ["origin", "destination", "date"]
}
}

用户交互模型

工具是由模型控制的,这意味着 AI 模型可以自动发现并调用它们。然而,MCP 强调通过多种机制进行人工监督。为了安全与信任,应用程序可以通过各种机制实现用户控制,例如:

  • 在 UI 中显示可用工具,允许用户定义在特定交互中是否可以使用该工具
  • 针对单个工具执行的确认对话框
  • 预先批准某些安全操作的权限设置
  • 显示所有工具执行情况及其结果的活动日志

资源

资源提供对信息的结构化访问,AI 应用程序可以检索这些信息并将其作为上下文提供给模型。

资源公开来自文件、API、数据库或 AI 理解上下文所需的任何其他来源的数据。应用程序可以直接访问这些信息并决定如何使用——无论是选择相关部分、使用嵌入(embeddings)进行搜索,还是将其全部传递给模型。

每个资源都有一个唯一的 URI(例如 file:///path/to/document.md)并声明其 MIME 类型以便进行适当的内容处理。

资源支持两种发现模式:

  1. 直接资源 - 指向特定数据的固定 URI。例如:calendar://events/2024 - 返回 2024 年的日历空闲情况
  2. 资源模板 - 带有参数的动态 URI,用于灵活查询。示例
    • travel://activities/{city}/{category} - 按城市和类别返回活动
    • travel://activities/barcelona/museums - 返回巴塞罗那的所有博物馆

资源模板包含标题、描述和预期 MIME 类型等元数据,使其具有可发现性和自解释性。

协议操作:

方法 目的 返回
resources/list 列出可用的直接资源 资源描述符数组
resources/templates/list 发现资源模板 资源模板定义数组
resources/read 检索资源内容 带有元数据的资源数据
resources/subscribe 监控资源变化 订阅确认

以差旅规划为例,资源为 AI 应用程序提供了相关信息的访问权限:

  • 日历数据 (calendar://events/2024) - 检查用户空闲时间
  • 旅行文档 (file:///Documents/Travel/passport.pdf) - 访问重要文件
  • 过往行程 (trips://history/barcelona-2023) - 参考过去的旅行和偏好

AI 应用程序检索这些资源并决定如何处理它们,无论是使用嵌入或关键字搜索选择数据子集,还是直接传递原始数据给模型。在这种情况下,它向模型提供日历数据、天气信息和旅行偏好,使模型能够检查可用性、查找天气模式并参考过去的旅行偏好。

资源模板示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"uriTemplate": "weather://forecast/{city}/{date}",
"name": "weather-forecast",
"title": "Weather Forecast",
"description": "Get weather forecast for any city and date",
"mimeType": "application/json"
}

{
"uriTemplate": "travel://flights/{origin}/{destination}",
"name": "flight-search",
"title": "Flight Search",
"description": "Search available flights between cities",
"mimeType": "application/json"
}

这些模板支持灵活查询。对于天气数据,用户可以访问任何城市/日期组合的预报。对于航班,他们可以搜索任意两个机场之间的路线。当用户输入“NYC”作为 origin(始发地)机场并开始输入“Bar”作为 destination(目的地)机场时,系统可以建议“Barcelona (BCN)”或“Barbados (BGI)”。

参数补全

动态资源支持参数补全。例如:

  • weather://forecast/{city} 输入 “Par” 可能会提示 “Paris” 或 “Park City”
  • flights://search/{airport} 输入 “JFK” 可能会提示 “JFK - John F. Kennedy International”

系统帮助发现有效值,而无需用户了解精确的格式。

用户交互模型

资源是应用程序驱动的,使其在检索、处理和展示可用上下文方面具有灵活性。常见的交互模式包括:

  • 在熟悉的文件夹式结构中浏览资源的树状或列表视图
  • 用于查找特定资源的搜索和过滤界面
  • 基于启发式方法或 AI 选择的自动上下文包含或智能建议
  • 用于包含单个或多个资源的手动或批量选择界面

应用程序可以自由地通过任何符合其需求的界面模式实现资源发现。协议不强制要求特定的 UI 模式,允许包含具有预览能力的资源选择器、基于当前对话上下文的智能建议、包含多个资源的批量选择,或与现有的文件浏览器和数据资源管理器集成。

提示

提示词(Prompt)提供可重用的模板。它们允许 MCP 服务器作者为特定领域提供参数化提示,或展示如何最佳地使用该 MCP 服务器。

提示词是定义了预期输入和交互模式的结构化模板。它们由用户控制,需要显式调用而非自动触发。提示词可以是上下文感知的,引用可用的资源和工具来创建完整的工作流。与资源类似,提示词支持参数补全,以帮助用户发现有效的参数值。

协议操作:

方法 目的 返回
prompts/list 发现可用提示词 提示词描述符数组
prompts/get 检索提示词详情 带有参数的完整提示词定义

提示词为常见任务提供结构化模板。在差旅规划背景下:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "plan-vacation",
"title": "Plan a vacation",
"description": "Guide through vacation planning process",
"arguments": [
{ "name": "destination", "type": "string", "required": true },
{ "name": "duration", "type": "number", "description": "days" },
{ "name": "budget", "type": "number", "required": false },
{ "name": "interests", "type": "array", "items": { "type": "string" } }
]
}

相比于非结构化的自然语言输入,提示词系统支持:

  1. 选择“规划假期”模板
  2. 结构化输入:巴塞罗那,7天,$3000,[“海滩”, “建筑”, “美食”]
  3. 基于模板的一致工作流执行

用户交互模型

提示词由用户控制,需要显式调用。该协议给予实现者自由去设计在应用程序中感觉自然的界面。核心原则包括:

  • 易于发现可用提示词
  • 清晰描述每个提示词的功能
  • 带有验证的自然参数输入
  • 透明展示提示词的底层模板

应用程序通常通过各种 UI 模式展示提示词,例如:

  • 斜杠命令(输入 “/” 查看可用提示词,如 /plan-vacation)
  • 用于可搜索访问的操作面板 (Command palettes)
  • 常用提示词的专用 UI 按钮
  • 建议相关提示词的右键上下文菜单

MCP 客户端

MCP 客户端由宿主应用程序实例化,用于与特定的 MCP 服务器通信。宿主应用程序(如 Claude.ai 或 IDE)管理整体用户体验并协调多个客户端。每个客户端负责与一台服务器进行一次直接通信。

理解这一点非常重要:宿主是用户交互的应用程序,而客户端是实现服务器连接的协议级组件。

除了使用服务器提供的上下文外,客户端还可以向服务器提供多项功能。这些客户端功能允许服务器作者构建更丰富的交互。

功能 说明 示例
引出 信息征询 (Elicitation) 使服务器能够在交互过程中向用户请求特定信息,为服务器按需收集信息提供了一种结构化的方式。 预订旅行的服务器可能会询问用户的飞机座位偏好、房型或联系电话,以完成预订。
根目录 (Roots) 允许客户端指定服务器应关注哪些目录,通过协调机制传达预期的范围。 预订旅行的服务器可能会被授予访问特定目录的权限,并从中读取用户的日历。
采样 采样 (Sampling) 允许服务器通过客户端请求 LLM 补全,从而实现 Agent 工作流。这种方法使客户端能够完全控制用户权限和安全措施。 预订旅行的服务器可能会向 LLM 发送航班列表,并请求 LLM 为用户选择最佳航班。

引出

信息征询(Elicitation)使服务器能够在交互过程中向用户请求特定信息,从而创建更动态和响应迅速的工作流。

信息征询为服务器提供了一种按需收集必要信息的结构化方式。服务器无需预先要求所有信息,也不会在缺少数据时直接失败,而是可以暂停操作以向用户请求特定输入。这创造了更灵活的交互,使服务器能够适应用户需求,而不是遵循僵化的模式。

信息征询流程:

image-20260324214218770

该流程实现了动态信息收集。服务器可以在需要时请求特定数据,用户通过适当的 UI 提供信息,服务器则利用新获取的上下文继续处理。

信息征询组件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
method: "elicitation/requestInput",
params: {
message: "Please confirm your Barcelona vacation booking details:",
schema: {
type: "object",
properties: {
confirmBooking: {
type: "boolean",
description: "Confirm the booking (Flights + Hotel = $3,000)"
},
seatPreference: {
type: "string",
enum: ["window", "aisle", "no preference"],
description: "Preferred seat type for flights"
},
roomType: {
type: "string",
enum: ["sea view", "city view", "garden view"],
description: "Preferred room type at hotel"
},
travelInsurance: {
type: "boolean",
default: false,
description: "Add travel insurance ($150)"
}
},
required: ["confirmBooking"]
}
}
}

旅行预订服务器通过最终预订确认过程展示了信息征询的强大功能。当用户选择了理想的巴塞罗那度假套餐时,服务器在继续操作前需要收集最终确认和任何缺失的细节。服务器通过结构化请求发起预订确认征询,其中包括行程摘要(6月15-22日飞往巴塞罗那的航班、海滨酒店、总价 3,000 美元)以及任何额外偏好的字段——如座位选择、房型或旅行保险选项。随着预订的进行,服务器会征询完成预订所需的联系信息。它可能会询问航班预订的旅客详情、酒店的特殊要求或紧急联系信息。

用户交互模型

信息征询交互旨在清晰、具有上下文,并尊重用户自主权:

  • 请求呈现:客户端在显示征询请求时,会提供明确的背景信息,说明哪个服务器在请求、为什么需要该信息以及将如何使用该信息。请求消息解释了目的,而 Schema(模式)提供了结构和验证。
  • 响应选项:用户可以通过适当的 UI 控件(文本框、下拉菜单、复选框)提供请求的信息,也可以拒绝提供信息(可附带解释),或取消整个操作。客户端在将响应返回给服务器之前,会根据提供的 Schema 对其进行验证。
  • 隐私考虑:信息征询绝不会请求密码或 API 密钥。客户端会针对可疑请求发出警告,并允许用户在发送前检查数据。

根(Root)目录定义了服务器操作的文件系统边界,允许客户端指定服务器应关注的目录。

根目录是客户端向服务器传达文件系统访问边界的一种机制。它们由指示服务器可以操作的目录的文件 URI 组成,帮助服务器了解可用文件和文件夹的范围。虽然根目录传达了预期的边界,但它们并不强制执行安全限制。实际的安全性必须在操作系统级别通过文件权限和/或沙箱来强制执行。

根目录结构:

1
2
3
4
{
"uri": "file:///Users/agent/travel-planning",
"name": "Travel Planning Workspace"
}

根目录仅限文件系统路径,并且始终使用 file:// URI 方案。它们帮助服务器了解项目边界、工作区组织和可访问目录。当用户处理不同的项目或文件夹时,根目录列表可以动态更新,服务器会通过 roots/list_changed 接收边界更改通知。

处理多个客户行程的旅行代理可以受益于通过根目录来组织文件系统访问。假设一个工作区中有针对旅行规划不同方面的不同目录。客户端向旅行规划服务器提供文件系统根目录:

  • file:///Users/agent/travel-planning - 包含所有旅行文件的主工作区
  • file:///Users/agent/travel-templates - 可重复使用的行程模板和资源
  • file:///Users/agent/client-documents - 客户护照和旅行证件

当代理创建巴塞罗那行程时,表现良好的服务器会尊重这些边界——访问模板、保存新行程并在指定的根目录内引用客户文档。服务器通常通过使用根目录的相对路径或利用尊重根目录边界的文件搜索工具来访问根目录内的文件。如果代理打开了一个存档文件夹,如 file:///Users/agent/archive/2023-trips,客户端会通过 roots/list_changed 更新根目录列表。有关尊重根目录的服务器完整实现,请参阅官方服务器仓库中的 文件系统服务器

设计理念

根目录充当客户端和服务器之间的协调机制,而不是安全边界。规范要求服务器“应当 (SHOULD) 尊重根目录边界”,而不是“必须 (MUST) 强制执行”它们,因为服务器运行的代码是客户端无法控制的。当服务器值得信赖或经过审查、用户了解其建议性质,且目标是防止意外而非阻止恶意行为时,根目录的效果最好。它们在上下文范围界定(告诉服务器关注哪里)、意外预防(帮助表现良好的服务器不越界)和工作流组织(如自动管理项目边界)方面表现出色。

用户交互模型

根目录通常由宿主应用程序根据用户操作自动管理,不过某些应用程序可能会开放手动根目录管理:

  • 自动根目录检测:当用户打开文件夹时,客户端会自动将其公开为根目录。打开旅行工作区允许客户端将该目录公开为根目录,帮助服务器了解哪些行程和文档属于当前工作的范围。
  • 手动根目录配置:高级用户可以通过配置指定根目录。例如,添加 /travel-templates 用于可重复使用的资源,同时排除包含财务记录的目录。

采样

采样(Sampling)允许服务器通过客户端请求语言模型补全,在保持安全和用户控制的同时实现 Agent 行为。

采样使服务器能够在不直接集成 AI 模型或为其付费的情况下执行依赖 AI 的任务。相反,服务器可以请求已经拥有 AI 模型访问权限的客户端代其处理这些任务。这种方法使客户端能够完全控制用户权限和安全措施。由于采样请求发生在其他操作(如工具分析数据)的上下文中,并作为单独的模型调用进行处理,因此它们在不同上下文之间保持了清晰的边界,从而能够更有效地利用上下文窗口。

采样流程:

image-20260324214915687

该流程通过多个“人工干预”检查点确保安全。用户可以查看并在生成响应返回给服务器之前对其及初始请求进行修改。

请求参数示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
messages: [
{
role: "user",
content: "Analyze these flight options and recommend the best choice:\n" +
"[47 flights with prices, times, airlines, and layovers]\n" +
"User preferences: morning departure, max 1 layover"
}
],
modelPreferences: {
hints: [{
name: "claude-sonnet-4-20250514" // Suggested model
}],
costPriority: 0.3, // Less concerned about API cost
speedPriority: 0.2, // Can wait for thorough analysis
intelligencePriority: 0.9 // Need complex trade-off evaluation
},
systemPrompt: "You are a travel expert helping users find the best flights based on their preferences",
maxTokens: 1500
}

考虑一个旅行预订服务器,它有一个名为 findBestFlight 的工具,该工具使用采样来分析可用航班并推荐最佳选择。当用户询问“帮我订下个月去巴塞罗那最好的航班”时,该工具需要 AI 协助来评估复杂的权衡。该工具查询航空公司 API 并收集了 47 个航班选项。然后它请求 AI 协助分析这些选项:“分析这些航班选项并推荐最佳选择:[包含价格、时间、航空公司和经停地的 47 个航班] 用户偏好:上午出发,最多 1 次经停。”客户端发起采样请求,允许 AI 评估权衡——例如较便宜的红眼航班与更方便的上午出发航班之间的权衡。该工具利用分析结果呈现前三个推荐方案。

用户交互模型

虽然不是强制要求,但采样旨在允许人工干预控制。用户可以通过以下几种机制保持监督:

  • 审批控制:采样请求可能需要明确的用户同意。客户端可以显示服务器想要分析的内容及其原因。用户可以批准、拒绝或修改请求。
  • 透明度功能:客户端可以显示确切的提示词、模型选择和 Token 限制,允许用户在 AI 响应返回给服务器之前进行审查。
  • 配置选项:用户可以设置模型偏好,为受信任的操作配置自动审批,或要求对所有内容进行审批。客户端可能提供脱敏敏感信息的选项。
  • 安全考虑:客户端和服务器在采样期间都必须妥善处理敏感数据。客户端应实施速率限制并验证所有消息内容。人工干预设计确保服务器发起的 AI 交互在未经用户明确同意的情况下,无法危害安全或访问敏感数据。

开发

SDK

我们可以使用官方 SDK 构建 MCP 服务端和客户端。所有 SDK 均提供相同的核心功能和完整的协议支持。

Anthropic: Official SDKs for building with Model Context Protocol

Demo

这里我们使用 python 来简单演示一个 MCP 服务的开发流程。

准备工作

0. 安装 uv

首先,MCP 推荐使用 uv 来管理 Python 项目。uv 是一个现代 Python 包管理工具,简单来说,uv = 更快、更现代的 pip + virtualenv + poetry 替代品。我们来安装 uv,官方提供了不同平台的安装方式,点击跳转,这里我们使用 macOS and Linux 版本为例:

1
2
3
4
5
6
7
8
9
# Use curl to download the script and execute it with sh:
curl -LsSf https://astral.sh/uv/install.sh | sh

# 或者
# If your system doesn't have curl, you can use wget:
wget -qO- https://astral.sh/uv/install.sh | sh

# 或者可以通过 pip 工具来安装
pip install uv

安装后按照提示,我们需要将配置环境变量(以 zsh 为例)

1
2
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

然后验证 uv 是否成功安装:

1
2
uv --version
# uv 0.11.0 (1f31f0e9f 2026-03-23 aarch64-apple-darwin)

至此,uv 环境已经安装完成。

1. 环境搭建

使用 uv 安装一个任意版本的 Python(以 3.13 为例)

1
2
3
uv python install 3.13
# Installed Python 3.13.12 in 1.02s
# + cpython-3.13.12-macos-aarch64-none (python3.13)

准备开发环境(Workspace)

1
2
3
4
5
6
7
8
9
10
# 创建开发路径并切换到该目录下
mkdir -p ~/Workspace/mcp_server/demo
cd ~/Workspace/mcp_server/demo
# 使用 python 3.13 来初始化 uv 项目
uv init . --python 3.13
# 创建虚拟环境并激活
uv venv
source .venv/bin/activate
# 添加 MCP SDK 依赖
uv add "mcp[cli]" httpx

STDIO

在实现 MCP 服务器时,请务必小心处理日志:对于基于 STDIO 的服务器: 严禁写入标准输出 (stdout)。这包括:

  • Python 中的 print() 语句
  • JavaScript 中的 console.log()
  • Go 中的 fmt.Println()
  • 其他语言中类似的标准输出函数

写入 stdout 会破坏 JSON-RPC 消息并导致服务器崩溃。对于基于 HTTP 的服务器: 标准输出日志是可以的,因为它不会干扰 HTTP 响应。

最佳实践

  1. 使用写入 stderr 或文件的日志库。
  2. 对于 Python,要特别小心 - print() 默认写入 stdout。
1
2
3
4
5
6
# ❌ Bad (STDIO)
print("Processing request")

# ✅ Good (STDIO)
import logging
logging.info("Processing request")

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import asyncio
from typing import Dict, Any

from mcp.server import Server
from mcp.server.stdio import stdio_server

# 创建 MCP Server
mcp_server = Server(
name="demo-mcp-server",
version="1.0.0"
)

# =========================
# Tool 示例
# =========================
@mcp_server.tool(
name="get_weather",
description="获取模拟天气信息",
input_schema={
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
)
async def get_weather(arguments: Dict[str, Any]):
city = arguments["city"]

return {
"content": [
{
"type": "text",
"text": f"{city} 当前天气:晴,25°C"
}
]
}

# =========================
# Resource 示例
# =========================
@mcp_server.resource(
name="system_info",
description="系统基础信息"
)
async def get_system_info():
return {
"contents": [
{
"uri": "resource://system/info",
"mimeType": "application/json",
"text": '{"app": "demo", "version": "1.0.0"}'
}
]
}

# =========================
# Prompt 示例
# =========================
@mcp_server.prompt(
name="analysis_prompt",
description="通用分析模板"
)
async def analysis_prompt():
return {
"messages": [
{
"role": "system",
"content": "你是一个数据分析专家,请分析以下内容:"
}
]
}

# =========================
# 启动入口(关键)
# =========================
async def main():
async with stdio_server(mcp_server) as server:
await server.serve()


if __name__ == "__main__":
asyncio.run(main())

SSE

Todo