目录
[1. MCP 概述](#1. MCP 概述)
[1.1 为什么需要 MCP](#1.1 为什么需要 MCP)
[1.2 MCP 是什么](#1.2 MCP 是什么)
[2. MCP 传输方式](#2. MCP 传输方式)
[3. MCP 服务器](#3. MCP 服务器)
[3.1 自定义服务器](#3.1 自定义服务器)
[3.2 FastMCP 服务器类](#3.2 FastMCP 服务器类)
[3.3 工具接口](#3.3 工具接口)
[3.4 资源接口](#3.4 资源接口)
[3.5 提示词接口](#3.5 提示词接口)
[3.6 MCP 社区服务器](#3.6 MCP 社区服务器)
[4. MCP 客户端](#4. MCP 客户端)
[4.1 FastMCP Client vs. MultiServerMCPClient](#4.1 FastMCP Client vs. MultiServerMCPClient)
[4.2 MultiServerMCPClient](#4.2 MultiServerMCPClient)
[MultiServerMCPClient 为什么设计为异步](#MultiServerMCPClient 为什么设计为异步)
[session: 有状态会话管理](#session: 有状态会话管理)
[4.3 加载工具](#4.3 加载工具)
[4.4 MCP 工具结构化内容处理](#4.4 MCP 工具结构化内容处理)
[4.5 追加工具结果到对话历史](#4.5 追加工具结果到对话历史)
[4.6 加载资源](#4.6 加载资源)
[4.7 加载提示词](#4.7 加载提示词)
[4.8 调用三方服务器](#4.8 调用三方服务器)
[4.9 客户端示例(同时连接本地数学服务器和远程天气服务器)](#4.9 客户端示例(同时连接本地数学服务器和远程天气服务器))
[4.10 有状态会话](#4.10 有状态会话)
[6. 高级功能](#6. 高级功能)
[6.1 工具拦截器](#6.1 工具拦截器)
[6.1.1 访问运行时上下文](#6.1.1 访问运行时上下文)
[6.1.2 状态更新与流程控制 (Commands)](#6.1.2 状态更新与流程控制 (Commands))
[6.1.3 自定义拦截器](#6.1.3 自定义拦截器)
[6.2 进度通知](#6.2 进度通知)
[6.2.1 构建 MCP 服务器](#6.2.1 构建 MCP 服务器)
[6.2.2 构建 MCP 客户端](#6.2.2 构建 MCP 客户端)
[6.3 日志记录](#6.3 日志记录)
[6.4 引导式输入](#6.4 引导式输入)
1. MCP 概述
1.1 为什么需要 MCP
- 每个服务有独立的 API 风格(REST、gRPC、XML...)
- 模型需要硬编码工具定义(函数名、参数 Schema)
- 缺乏动态发现能力,多平台兼容困难
MCP 的作用:它并非替代 Tool Calling,而是为 Tool Calling 提供标准化运行时
- **调用流程:**模型发出 Tool Calling(指令) → 这个指令不再直接发给五花八门的第三方API,而是发给 MCP Client → MCP Client 根据协议将指令路由给对应的 MCP Server(运行时) → MCP Server 负责执行真正的API调用,并将结果标准化返回
- **标准化体现在:**MCP 定义了工具发现、调用的接口以及上下文采样的标准
- **运行时体现在:**它是一个常驻的后台服务,负责管理工具的连接生命周期、维护会话状态、处理上下文压缩,甚至支持多轮工具间的协同编排
- Tool Calling = 模型决策"调哪个工具、传什么参数"
- MCP = 协议层,规定"如何描述工具、如何发起调用、如何传递结果、如何管理会话"
1.2 MCP 是什么
MCP(Model Context Protocol,模型上下文协议) 是一个开放标准,允许 AI 应用(如 Claude、ChatGPT)通过统一的接口连接到数据源(文件、数据库)、工具 (搜索引擎、计算器)和工作流(专用提示词)

核心价值:
- 对开发者:一次编写 MCP Server,多个 AI 应用可复用
- 对 AI 应用:快速集成海量工具,增强能力
- 对用户:获得真正能"办事"的智能助手
2. MCP 传输方式

MCP 采用的是"宿主--客户端--服务器"三层架构, 支持两种客户端‑服务器通信机制:
| 传输方式 | 特点 | 适用场景 |
|---|---|---|
| stdio | 客户端将服务器作为子进程启动,通过标准输入/输出通信 有状态(子进程持续存在) | 本地工具、简单配置 |
| HTTP(streamable‑http) | 基于 HTTP 请求,支持自定义请求头。适合远程部署 | 远程服务器云环境 |
| 场景 | transport 正确写法 | 说明 |
|---|---|---|
客户端 MultiServerMCPClient |
"http" 或 "streamable_http" |
两者等价,官方都支持 |
服务端 FastMCP.run() |
"streamable-http"(连字符) |
MCP SDK 规范 |
| JSON 配置文件(Cursor 等) | "streamable_http"(下划线) |
MCP 规范 |
from langchain_mcp_adapters.client import MultiServerMCPClient
# ============================================================
# MultiServerMCPClient 多服务器配置
# 字典的每个 key 是自定义的服务器名称(用于标识和区分不同服务)
# 每个 value 是该服务器的连接参数
# ============================================================
client = MultiServerMCPClient({
# ----------------------------------------------------------
# 服务器 1:天气服务(远程 HTTP 连接)
# 使用 streamable_http 传输协议,适用于远程部署的生产环境
# ----------------------------------------------------------
"weather": {
# 传输协议类型:
# - "stdio" → 本地子进程,通过标准输入/输出通信
# - "streamable_http" → 远程 HTTP,支持双向通信、断线重连(推荐生产用)
# - "sse" → Server-Sent Events,远程单向推送
"transport": "streamable_http",
# MCP 服务器的 URL 地址
# 需要确保该服务已启动并监听在对应端口上
"url": "http://localhost:8000/mcp",
# 可选:自定义 HTTP 请求头,常用于鉴权、链路追踪等
"headers": {
"Authorization": "Bearer YOUR_TOKEN" # 替换为实际的 Token
},
# 可选:自定义 httpx.Auth 对象,用于更复杂的认证方案
# 例如 OAuth2、Digest Auth 等
# "auth": custom_auth_object,
},
# ----------------------------------------------------------
# 服务器 2:数学计算服务(本地 stdio 子进程连接)
# 客户端会以子进程方式启动该服务,通过标准输入/输出通信
# ----------------------------------------------------------
"math": {
# 使用 stdio 传输:客户端启动一个子进程来运行 MCP 服务器
# 适合本地开发、单机部署、对启动速度有要求的场景
"transport": "stdio",
# 启动子进程的命令
# 建议使用 sys.executable 确保使用当前 Python 解释器
"command": "python",
# 传给命令的参数列表
# 务必使用绝对路径,避免相对路径在不同工作目录下找不到文件
"args": ["/path/to/your_server.py"],
},
})
3. MCP 服务器
3.1 自定义服务器
LangChain 中,对于 MCP 服务器的创建,可以使用 FastMCP 库,参考
| 特性 | FastMCP | 原生 MCP Python SDK |
|---|---|---|
| 开发哲学 | 高层、Pythonic,屏蔽底层细节 | 底层、灵活,提供对协议的完全控制 |
| 上手复杂度 | 极低。一行代码初始化服务器 | 较高。需要手动处理初始化、协议处理器等 |
| 工具注册 | 装饰器驱动 (@mcp.tool),声明式 |
命令式,需要手动编写注册逻辑和 Schema |
| Schema 生成 | 自动。从函数签名和类型提示生成 | 手动。需要定义 Pydantic 模型 |
| 代码量 | 减少约 70% 的样板代码 | 更冗长 |
| 最佳场景 | 常规用例,追求快速开发和原型验证 | 需要底层控制、自定义传输或构建 SDK 工具时 |
MCP 服务器通过三种构件暴露能力:
| 构件 | 控制方 | 读写 | 典型用途 | 示例 |
| Tools(工具) | 模型 | 可写 | 执行操作:查询、写入、API 调用 | 搜索航班、创建日历事件 |
| Resources(资源) | 应用 | 只读 | 提供上下文数据:文件内容、数据库记录 | 帮助文档、用户配置 |
| Prompts(提示词) | 用户 | 只读 | 预置指令模板,指导模型使用工具和资源 | 数据分析模板、角色设定 |
|---|
import json
from fastmcp import FastMCP
# 1. 创建 FastMCP 服务器实例
# 参数:服务器名称,用于标识和调试,会在协议握手时传递给客户端。
mcp = FastMCP("MyServer")
# 2. 定义工具(Tools)
# 工具是客户端(如 LLM)可调用的"动作",用于执行操作或访问外部系统。
# @mcp.tool() 装饰器将普通函数转换为 MCP 工具,自动从函数签名和文档字符串生成 Schema。
# 注意:建议写成 @mcp.tool()(带括号),以便日后传递 name、description 等可选参数。
@mcp.tool()
def multiply(a: float, b: float) -> float:
"""
将两个浮点数相乘并返回结果。
:param a: 第一个乘数
:param b: 第二个乘数
:return: 乘积
"""
return a * b
# 3. 定义资源(Resources)
# 资源是只读的"数据源",由服务器主动提供给客户端,客户端无法修改。
# 资源函数必须返回以下三种类型之一:
# - str → 作为 TextResourceContents 发送,默认 MIME 类型为 text/plain
# - bytes → Base64 编码后作为 BlobResourceContents 发送(需指定合适的 mime_type)
# - ResourceResult → 完全控制内容、MIME 类型和元数据(详见 FastMCP 文档)
# 对于结构化数据(如 dict/list),需使用 json.dumps() 序列化为 JSON 字符串。
# @mcp.resource(uri="...") 用于指定资源的唯一标识符,客户端通过该 URI 读取资源。
@mcp.resource(uri="data://config")
def get_config() -> str:
"""
获取服务器配置信息(模拟数据)。
:return: JSON 格式的配置字符串
"""
config = {"topic": "dark", "version": "1.0"}
return json.dumps(config)
# 4. 定义提示词模板(Prompts)
# 提示词是可重用的消息模板,用于指导 LLM 的交互行为。
# 它们由用户(或上层应用)选择并填充参数,最终生成完整的系统/用户消息。
# @mcp.prompt 装饰器将一个函数注册为提示词模板,函数参数会成为模板的占位符。
@mcp.prompt()
def analyze_data(data_points: list[float]) -> str:
"""
生成一个用于分析数据的提示词模板。
:param data_points: 浮点数列表,待分析的数据点
:return: 完整的提示词字符串
"""
formatted_data = ", ".join(str(point) for point in data_points)
return f"请对这些数据点进行分析: {formatted_data}"
# 5. 程序入口:仅当直接运行该脚本时才启动服务器
if __name__ == "__main__":
# 启动 MCP 服务器,并指定传输方式为 HTTP(Streamable HTTP)
# 服务端必须使用连字符 "streamable-http"(注意:客户端适配器使用下划线 "streamable_http")
# 此模式会启动一个异步 HTTP 服务器,监听 8000 端口(默认)。
# 可额外指定 host 和 port,例如:mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)
mcp.run(transport="streamable-http")
3.2 FastMCP 服务器类
FastMCP 类是每个 FastMCP 应用程序的核心,充当工具、资源和提示词的容器,负责管理与 MCP 客户端的通信并编排整个服务器的生命周期
- 构造函数参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
str |
是 | 服务器的人类可读名称,用于在客户端应用或日志中标识 |
instructions |
str |
否 | 服务器使用说明,帮助客户端理解服务器用途和可用功能 |
version |
str |
否 | 版本字符串,默认为 FastMCP 库版本 |
website |
str |
否 | 服务器信息页面的 URL,在客户端应用中显示(v2.13.0+) |
icons |
list |
否 | 服务器图标列表,帮助用户视觉识别(v2.13.0+) |
auth |
AuthProvider |
否 | 用于保护 HTTP 传输的身份验证提供者 |
lifespan |
AsyncContextManager |
否 | 服务器启动和关闭逻辑的异步上下文管理器函数 |
tools |
list |
否 | 要添加到服务器的工具列表 (或可转换为工具的函数) |
include_tags |
list[str] |
否 | 只暴露至少匹配一个标签的组件 |
exclude_tags |
list[str] |
否 | 隐藏匹配任一标签的组件 |
on_duplicate_tools |
str |
否 | 处理重复工具注册的策略 |
on_duplicate_resources |
str |
否 | 处理重复资源注册的策略 |
on_duplicate_prompts |
str |
否 | 处理重复提示词注册的策略 |
strict_input_validation |
bool |
否 | False(默认)使用 Pydantic 灵活校验;True 使用 JSON Schema 严格校验(v2.11.0+) |
include_fastmcp_meta |
bool |
否 | 是否在响应的 _fastmcp 命名空间中包含 FastMCP 元数据(v2.11.0+) |
- 核心方法
| 方法 | 说明 |
|---|---|
run |
启动服务器,支持 stdio(默认)、streamable-http 等传输方式 |
add_tool |
以编程方式添加工具(替代装饰器) |
add_resource |
以编程方式添加资源 |
add_prompt |
以编程方式添加提示词 |
- run :启动 FastMCP 服务器,进入事件循环并开始处理客户端请求
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
transport |
str |
否 | "stdio" |
传输协议: stdio -- 标准输入/输出,适合本地集成 streamable-http -- HTTP 流式传输,适合远程部署 |
host |
str |
否 | "127.0.0.1" |
绑定地址(仅 HTTP/SSE 传输) 设为 "0.0.0.0" 监听所有接口 |
port |
int |
否 | 8000 |
监听端口(仅 HTTP/SSE 传输) |
path |
str |
否 | "/mcp" |
服务端点路径(仅 HTTP/SSE 传输) |
log_level |
str |
否 | "INFO" |
日志级别,如 "DEBUG"、"WARNING" |
server |
FastMCP |
否 | 当前实例 | 允许显式指定服务器对象(高级用法) |
返回值:无(None)。该方法会阻塞当前线程直到服务器停止
# Stdio 模式(默认)
mcp.run()
# HTTP 模式,自定义端口
mcp.run(transport="streamable-http", host="0.0.0.0", port=9000)
# 开启调试日志
mcp.run(transport="stdio", log_level="DEBUG")
- 在 stdio 模式下,host、port、path 参数无效
- 在 HTTP 模式下,服务器使用 Uvicorn 作为 ASGI 服务器,确保已安装 uvicorn
- 该方法是阻塞的,位于 mcp.run() 之后的代码不会执行(除非服务器被中断)
- 若需要在启动前后执行初始化/清理逻辑,可使用 lifespan 参数
add_tool:以编程方式添加工具
将 Python 函数或可调用对象注册为 MCP 工具,无需使用 @mcp.tool 装饰器
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
fn |
Callable |
是 | 要注册为工具的函数(同步或异步) 其签名和文档字符串用于生成工具 Schema |
name |
str |
否 | 工具名称(不提供则使用函数名) |
description |
str |
否 | 工具描述(不提供则使用函数文档字符串) |
title |
str |
否 | 人类可读的显示标题(v2.13.0+) |
tags |
set[str] |
否 | 分类标签,用于组筛选 |
meta |
dict |
否 | 自定义元数据,附加到工具定义 |
icons |
list |
否 | 图标列表(v2.13.0+) |
enabled |
bool |
否 | True(默认)是否启用该工具 |
strict |
bool |
否 | False(默认)使用 Pydantic 灵活校验; True 使用 JSON Schema 严格校验(v2.11.0+) |
on_duplicate |
str |
否 | 处理重名工具的策略:"error"(抛出异常)、"warn"(警告并替换)、"replace"(静默替换)、"ignore"(保留原有) |
返回值:None(注册过程抛出异常则失败)
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
mcp.add_tool(add, name="my_add", description="自定义加法工具")
- 该方式与装饰器 @mcp.tool 完全等效,适合动态注册(如循环注册多个工具)
- 函数必须是无状态的,且不支持 *args / **kwargs
- 若 fn 是 async 函数,将正确处理异步调用
add_resource():以编程方式添加资源
注册一个只读数据源(资源),客户端可通过 URI 读取
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
fn |
Callable |
是 | 返回资源数据的函数(同步或异步)。可接受 Context 参数 |
uri |
str |
是 | 资源的唯一标识符,支持 URI 模板(如 "users://{user_id}") |
name |
str |
否 | 资源名称(默认使用函数名) |
description |
str |
否 | 资源描述(默认使用函数文档字符串) |
mime_type |
str |
否 | 资源的 MIME 类型。若未提供,则从返回值推断(str → text/plain,bytes → application/octet-stream) |
tags |
set[str] |
否 | 标签 |
annotations |
dict |
否 | 资源注解,如 {"readOnlyHint": True} |
on_duplicate |
str |
否 | 处理重复资源 URI 的策略(同 add_tool) |
返回值:None
def get_config():
return '{"version":"1.0"}'
mcp.add_resource(get_config, uri="config://settings", mime_type="application/json")
# URI: "users://{user_id}"
def get_user(user_id: str) -> str:
return f"User {user_id}"
mcp.add_resource(get_user, uri="users://{user_id}")
- 返回值支持 str、bytes 或 ResourceResult(高级控制)
- 对于动态资源(URI 模板),函数参数名必须与模板变量匹配
add_prompt:以编程方式添加提示词
注册一个可重用的提示词模板,客户端可通过名称获取
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
fn |
Callable |
是 | 生成提示词内容的函数(同步或异步)。其参数作为提示词模板的占位符 |
name |
str |
否 | 提示词名称(默认使用函数名) |
description |
str |
否 | 提示词描述(默认使用函数文档字符串) |
tags |
set[str] |
否 | 标签 |
on_duplicate |
str |
否 | 处理重复名称的策略(同 add_tool) |
返回值:None
def greeting(name: str) -> str:
return f"Hello, {name}! How can I help?"
mcp.add_prompt(greeting, name="greet_user")
- 函数返回值必须是 str 或 listMessage(高级用法,可包含多轮消息)
- 客户端调用时会传入参数,函数生成最终提示词内容
- 提示词可以包含占位符,如 {data_points},客户端填充后使
3.3 工具接口
FastMCP 通过检查函数签名和类型注解自动将 Python 函数转换为 MCP 工具
- @mcp.tool 装饰器参数
| 参数 | 类型 | 说明 |
|---|---|---|
name |
str |
工具名称(默认使用函数名) |
description |
str |
工具描述(若不提供则使用函数文档字符串) |
title |
str |
人类可读的显示标题(v2.13.0+) |
tags |
set[str] |
分类标签,用于组织/筛选 |
meta |
dict |
自定义元数据 |
icons |
list |
工具图标列表(v2.13.0+) |
enabled |
bool |
是否启用工具 |
- 工具函数支持的模式
| 模式 | 说明 | 示例 |
|---|---|---|
| 同步函数 | 普通 Python 函数 | @mcp.tool def add(a:int,b:int)->int |
| 异步函数 | async def 支持 I/O 操作 |
@mcp.tool async def fetch(url:str)->dict |
| 带 Field 描述 | 使用 Pydantic Field 添加参数描述和校验 |
limit: int = Field(ge=1, le=100) |
| 带 Context | 注入 Context 对象访问生命周期状态 |
@mcp.tool async def query(ctx: Context, sql:str) |
| 错误处理 | 抛出 ToolError 返回友好错误 |
raise ToolError("Cannot divide by zero") |
不支持 *args 或 **kwargs 可变参数
3.4 资源接口
资源是只读数据源,由服务器主动提供给客户端读取
- @mcp.resource 参数
| 参数 | 说明 |
|---|---|
uri |
必填 。资源的唯一标识符,支持 URI 模板(如 users://{user_id}/profile) |
annotations |
可选注解,如 readOnlyHint、idempotentHint |
- 资源函数支持的返回类型
| 返回类型 | 说明 |
|---|---|
str |
作为 TextResourceContents 发送,默认 MIME 类型 text/plain |
bytes |
Base64 编码后作为 BlobResourceContents 发送 |
ResourceResult |
完全控制内容、MIME 类型和元数据 |
- 资源模式
| 模式 | 说明 | 示例 |
|---|---|---|
| 静态资源 | 固定 URI,返回固定数据 | @mcp.resource("config://settings") |
| 动态资源 | URI 模板,参数动态匹配 | @mcp.resource("users://{user_id}/profile") |
| 多参数资源 | 支持多个 URI 参数 | @mcp.resource("repos://{owner}/{repo}/info") |
| 异步资源 | 支持 async def |
@mcp.resource("db://users") async def all_users(ctx) |
3.5 提示词接口
提示词是可重用的消息模板,用于指导 LLM 交互
- @mcp.prompt 装饰器
| 参数 | 说明 |
|---|---|
name |
提示词名称(默认使用函数名) |
description |
提示词描述(默认使用函数文档字符串) |
- 提示词函数
| 模式 | 说明 |
|---|---|
| 同步函数 | 返回 str 作为提示词内容 |
| 异步函数 | 支持 async def |
| 带参数 | 函数参数成为提示词的占位符 |
3.6 MCP 社区服务器
- MCP 官方参考服务器:https://github.com/modelcontextprotocol/servers
- 魔搭社区 MCP:https://modelscope.cn/mcp
- 阿里百炼 MCP 市场:https://bailian.console.aliyun.com/cn-beijing?tab=mcp#/mcp-market
| 资源 | 定位 | 适用人群 | 获取成本 |
|---|---|---|---|
| 官方参考服务器 | 开源参考实现 | 开发者自建 | 免费,需自运维 |
| 魔搭 MCP | 模型生态集成 | 国内 AI 开发者 | 部分免费/部分付费 |
| 百炼 MCP 市场 | 企业级托管服务 | 企业用户 | 付费,有 SLA |
4. MCP 客户端
4.1 FastMCP Client vs. MultiServerMCPClient
| 特性维度 | FastMCP 原生客户端 (fastmcp.Client) |
LangChain 适配客户端 (MultiServerMCPClient) |
|---|---|---|
| 定位用途 | 通用、程序化的 MCP 客户端。用于与任何 MCP 服务器进行确定性、受控的交互 | LangChain/LangGraph 生态的专用适配器。旨在将 MCP 服务器无缝接入 LangChain 智能体 |
| 核心功能 | 直接调用 MCP 服务器的工具(call_tool)、资源和提示 |
自动发现并将 MCP 工具转换为 LangChain 的 BaseTool 对象,供 Agent 使用 |
| 集成对象 | 独立使用,或作为更高级系统的构建块 | 与 langchain.agents、langgraph.prebuilt 等模块深度集成 |
| 会话管理 | 通过 async with client: 上下文管理器显式管理连接生命周期 |
默认无状态 :每次工具调用都创建全新的 ClientSession,用后即焚 |
| 应用场景 | 测试 MCP 服务器、构建需要可靠 MCP 交互的确定性应用 | 在 LangChain/LangGraph 应用中,让智能体能够调用一个或多个 MCP 服务器上的工具 |
4.2 MultiServerMCPClient
MultiServerMCPClient 是 langchain-mcp-adapters 库的核心类,可以把它理解为一个多服务器客户端管理器。它的主要职责是:根据配置连接到多个 MCP 服务器,自动发现它们提供的工具,并转换成 LangChain 智能体能直接使用的工具列表
1. 异步执行要求(强制性)
- 强制异步运行:MultiServerMCPClient 及其所有核心方法(get_tools、session、get_prompt 等)均为异步 API
- 入口规范:在脚本或测试环境中,必须使用 asyncio.run(main()) 作为程序入口来驱动事件循环;在 FastAPI 等 Web 框架中,对应的路由或启动事件必须声明为 async def
MultiServerMCPClient 为什么设计为异步
- 第一,底层 I/O 本质决定了异步是必选项。 它的通信基于子进程 STDIO 读写和 HTTP 长连接,全部是阻塞型 I/O 操作。同步模型下线程被阻塞挂起,CPU 空等;异步通过 await 挂起协程,释放事件循环去处理其他请求,这是解决 I/O 密集型高并发问题的标准方案
- 第二,并发能力有质的差异。 异步协程内存开销仅 KB 级别,单线程即可支撑数千并发连接;同步多线程每个线程占用 ~8MB 内存,且 GIL 下的线程切换开销巨大。配合 asyncio.gather 还能实现多工具并行调用,总耗时从 T1+T2+T3 降至 max(T1,T2,T3)
- 第三,与 LangGraph 架构深度耦合。 LangGraph 的 astream/ainvoke 是原生异步的,异步设计保证在节点中直接 await MCP 调用,既避免阻塞事件循环导致流式输出卡顿,又让 LLM 推理与工具执行交替进行,实现真正的流式体验。同步方案只能通过 asyncio.to_thread 做脏活,徒增开销且破坏状态一致性
2. 默认行为:严格无状态(高并发首选)
- 瞬建瞬毁机制:默认情况下,通过 get_tools() 转换生成的工具,在每次被 Agent 实际调用时,均会临时创建一个全新的 ClientSession → 执行 call_tool → 立即关闭并清理会话
- 适用场景:适合纯函数式工具(如查询天气、计算数学、调用 REST API),无需在服务端维持任何跨轮次上下文
- 性能提醒:高并发场景下,频繁握手(尤其是 STDIO 模式反复 fork 子进程)存在性能开销。若无状态工具调用极为频繁,建议评估是否切换为有状态长连接
3. 有状态会话管理(显式控制生命周期)
- 当需要维护跨工具调用的上下文(如分页游标、临时文件句柄、多轮累积数据)时,必须放弃默认无状态模式,转而使用显式会话管理
- 实现方式:使用 client.session("服务器名称") 上下文管理器
- 工具加载:在该上下文内部,需使用 load_mcp_tools(session) 加载工具(而非之前的 client.get_tools())
- 生命周期控制:会话存活时间由 async with 块的范围决定;退出块时自动关闭连接并清理子进程
构造函数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
connections |
`dict[str, Connection] | None` | None |
callbacks |
`Callbacks | None` | None |
tool_interceptors |
`list[ToolCallInterceptor] | None` | None |
tool_name_prefix |
bool |
False |
若为 True,工具名以服务器名加下划线作为前缀,避免多服务器同名工具冲突 |
handle_tool_errors |
bool |
True |
若为 True(默认),MCP 工具执行错误以 ToolMessage(status="error") 返回,让 Agent 自行修正而非崩溃。若为 False,直接抛出 ToolException。注意:传输层故障或内容转换错误始终会抛出异常,不受此参数控制 |
- connections 的连接配置结构
connections 字典中每个条目的值根据传输类型不同而不同:
| 传输类型 | 配置字段 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| stdio (本地子进程) | transport |
"stdio" |
是 | 固定值,告诉客户端通过**标准输入输出(stdin/stdout)**建立双向通信。 |
command |
str |
是 | 启动可执行文件的命令。可以是系统命令(如 node、python、npx),必须确保在系统 PATH 中或使用绝对路径。 |
|
args |
list[str] |
否 | 传给命令的参数列表。注意 :不要把 command 和 args 写反,python 是 command,["main.py"] 是 args |
|
env |
dict |
否 | 环境变量 。用于传递密钥(如 API_KEY)或覆盖默认配置 |
|
cwd |
str |
否 | 工作目录。指定子进程的运行路径。如果脚本里引用了相对路径的配置文件,这个字段就至关重要 | |
timeout |
int |
否 | 启动超时时间(毫秒)。如果子进程启动超过设定时间未就绪,客户端会终止该进程并报错,默认通常为 5000ms | |
| HTTP / Streamable-HTTP (远程通信) | transport |
"http" / "streamable_http" |
是 | streamable-http 意味着支持流式响应 |
url |
str |
是 | 服务器端点 | |
headers |
dict |
否 | 请求头。用于鉴权或指定内容类型 | |
method |
str |
否 | 请求方法,默认通常是 POST。在纯 HTTP模式下可能需要指定 GET 或 POST |
|
timeout |
int |
否 | 请求超时时间(秒/毫秒,视具体实现而定)。因为涉及网络波动,建议设置比 stdio 更长的超时(如 30s) | |
reconnect_interval |
int |
否 | 重连间隔(毫秒)。针对 SSE 长连接意外断开后的自动重试策略 |
- 核心方法
| 方法 | 说明 |
|---|---|
session |
连接到指定 MCP 服务器并返回初始化后的会话 用于需要显式控制会话生命周期 或维持状态的场景 |
get_tools |
从所有(或指定)已连接的服务器获取 LangChain 兼容的工具列表 默认无状态:每次调用都创建新会话、执行工具、然后清理 |
get_prompt |
从指定 MCP 服务器获取指定名称的提示词 |
get_resources |
从指定服务器获取资源,转换为 LangChain 的 Blob 对象 |
session: 有状态会话管理
| 维度 | 详细说明 |
|---|---|
| 完整签名 | async session(server_name: str, *, auto_initialize: bool = True) -> AsyncIterator[ClientSession] |
| 参数详解 | • server_name (必选):str,必须匹配 connections 字典中的键名 • auto_initialize (可选,仅限关键字):bool,默认为 True 若为 True,自动完成 MCP 协议的 initialize 握手和 initialized 通知 若为 False,会话建立后处于未初始化状态,需手动调用 session.initialize() |
| 返回值 | AsyncIterator[ClientSession]:异步迭代器(上下文管理器) 产出 mcp.client.session.ClientSession 原生对象, 可直接调用其 list_tools()、call_tool()、read_resource() 等方法 |
| 内部机制 | 1. 根据 connections 配置建立底层传输(STDIO子进程 / HTTP连接) 2. 若 auto_initialize=True,发送 MCP 初始化握手并等待服务端能力协商 3. 产出活会话(yield) 4. 退出 async with 块时,自动关闭传输连接并清理子进程(STDIO模式) |
| 使用场景 | • 需要保持连接状态 :如服务器端维护对话记忆、文件句柄、游标位置 • 批量顺序调用 :在同一个会话中多次调用 call_tool,避免多次握手的网络开销 • 需要订阅服务器通知(如进度更新、日志推送) |
| 关键陷阱 | • MultiServerMCPClient 本身不实现 __aenter__,不能直接 async with client as c, 必须通过 client.session() 显式获取 • 若 auto_initialize=False 且忘记手动初始化,后续 call_tool 会抛出 RuntimeError • 此方法不自动转换 LangChain 工具,需配合 load_mcp_tools(session) 使用 |
4.3 加载工具
| 维度 | 详细说明 |
|---|---|
| 完整签名 | `async get_tools(server_name: str |
| 参数详解 | • server_name (可选):`str |
| 返回值 | list[BaseTool]:LangChain 的 StructuredTool 对象列表 每个工具自动适配 async 调用,支持 LCEL 的 .invoke() / .ainvoke() 接口 |
| 内部机制 | 1. 为每个目标服务器建立临时会话 2. 调用 session.list_tools() 发现所有工具定义 3. 将 MCP 工具转换为 StructuredTool,并包装调用逻辑 4. 关键行为 :工具被实际 invoke 时, 每次调用都创建一个全新的会话 → 执行 call_tool → 立即关闭会话(无状态) |
| 使用场景 | • 标准 ReAct Agent / LangGraph 智能体 的工具绑定 • 工具为纯函数式 (无副作用的查询、计算、API 调用) • 不需要在服务器端维持任何连接级状态 |
| 关键陷阱 | • 默认无状态 :如果服务器工具依赖上下文(如"上次查询的下一页"),会导致数据错乱或丢失 • 每次工具调用都重新建立传输连接(尤其是 STDIO 模式会反复 fork 子进程),高并发下性能堪忧 。如需高性能,建议结合 session() 自行实现连接池或复用长连接 • 工具签名自动由 MCP 的 inputSchema 推导,但复杂嵌套 JSON Schema 可能转换不完美(需手动校验) |
| 项目 | 说明 |
|---|---|
| 函数签名 | async def load_mcp_tools( `session: ClientSession |
| 功能 | 连接到一个 MCP 服务器,获取其所有可用工具,并将它们转换为 LangChain 的 BaseTool 对象列表 |
| 核心参数 | session (ClientSession | None): 一个已激活的 MCP 客户端会话。如果提供此参数,则 connection 必须为 None connection (Connection | None): 连接配置对象。如果提供此参数,函数会为每次工具调用创建一个临时会话,适用于无状态工具。 |
| 可选参数 | callbacks (Callbacks | None): 用于处理通知和事件的可选回调函数 tool_interceptors (list[ToolCallInterceptor] | None): 工具调用拦截器列表,用于在工具执行前后插入自定义逻辑 server_name (str | None): 工具所属服务器的名称,主要用于标识和日志记录 tool_name_prefix (bool): 当为 True 且提供了 server_name 时,工具名会以 server_name 为前缀(例如 "weather_search"),避免多服务器工具名冲突 |
| 返回值 | list[BaseTool]: 一个包含 LangChain BaseTool 对象的列表,可直接用于 LangChain 的 Agent |
| 关键转换 | 该函数主要完成三项转换工作: 1. Schema 转换 :将 MCP 工具的输入模式转换为 LangChain 的 Pydantic 模型 2. 执行桥接 :将 LangChain 的工具调用桥接为 MCP 的 call_tool 请求 3. 结果转换 :将 MCP 的返回内容转换为 LangChain 的 ToolMessage 格式 |
对比:MultiServerMCPClient.get_tools() |
load_mcp_tools 适用于连接单个服务器 的场景。而 MultiServerMCPClient.get_tools() 则用于同时连接和管理多个 MCP 服务器,并聚合所有工具 |
4.4 MCP 工具结构化内容处理
| 维度 | 详细说明 |
|---|---|
| 核心接口/属性 | ToolMessage.artifact["structured_content"] |
| 标准示例流程 | 1. await agent.ainvoke(...) 执行 Agent 2. 遍历 response["messages"] 过滤 ToolMessage 实例 3. 检查 message.artifact 是否存在 4. 读取 message.artifact["structured_content"] 获取字典数据 |
| 关键陷阱 | • artifact 字段仅在 MCP 服务端返回 structuredContent 时才有值,若服务端仅返回纯文本 content,则 artifact 为 None。 • 默认情况下,LLM 看不到 artifact 中的结构化数据(仅开发者可访问),若需模型读取该 JSON,必须使用下方的拦截器方案。 |
from langchain.messages import ToolMessage
# ============================================================
# 注意:以下代码必须在 异步函数(async def) 中执行,
# 因为 agent.ainvoke 是一个异步方法。
# ============================================================
weather_response = await agent.ainvoke(
{
"messages": [
# 构造用户消息,role='user' 表示来自用户的输入
{"role": "user", "content": "上海的天气怎么样?"}
]
}
)
# 从返回结果中提取消息列表(agent 的响应包含多轮消息)
messages = weather_response["messages"]
# 遍历所有消息,查找工具调用返回的结构化数据
for message in messages:
# 判断条件:
# 1. 消息类型是 ToolMessage(即工具执行后返回的消息)
# 2. 该消息有 artifact 属性(artifact 通常存放工具返回的原始结构化数据)
if isinstance(message, ToolMessage) and message.artifact:
# 从 artifact 字典中提取 "structured_content" 字段
# 该字段一般是 JSON 或字典,便于机器解析
structured_data = message.artifact["structured_content"]
# 打印结构化数据(在实际开发中可进一步处理,如存储或返回给前端)
print(structured_data)
4.5 追加工具结果到对话历史
| 维度 | 详细说明 |
|---|---|
| 核心接口/参数 | MultiServerMCPClient(..., tool_interceptors=[async_func]) |
| 底层原理 | 拦截器在工具执行后、结果返回给 Agent 前触发。通过检查 result.structuredContent,将 JSON 字符串强制追加至 result.content(TextContent 列表),使结构化数据"伪装"成普通文本,从而让 LLM 在后续推理中能直接看到并理解 JSON 内容。 |
| 示例流程 | 1. 定义 append_structured_content 拦截器 2. 实例化 MultiServerMCPClient 时传入 tool_interceptors=[append_structured_content] 3. 获取工具并创建 Agent 4. 调用 Agent 后,ToolMessage.content 将同时包含原始文本 + JSON 字符串,LLM 可直接基于 JSON 继续推理 |
| 关键陷阱 | • 拦截器会永久改变 ToolMessage.content,若追加的 JSON 过长,可能撑爆 LLM 上下文窗口,需评估截断策略。 • 拦截器顺序敏感:多个拦截器按列表顺序执行,前一个的输出是后一个的输入。 |
import json
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from mcp.types import TextContent
from langchain_mcp_adapters import MultiServerMCPClient
# ============================================================
# 2. 定义工具调用拦截器(Interceptor)
# 作用:在工具执行后,将其返回的结构化内容(structuredContent)
# 以纯文本形式追加到响应的 content 列表中,方便最终输出。
# ============================================================
async def append_structured_content(request: MCPToolCallRequest, handler):
"""
拦截器函数,用于处理 MCP 工具调用的请求和响应。
参数:
request: MCPToolCallRequest 对象,包含调用的工具名和参数。
handler: 下一个处理器(通常是实际执行工具的函数),必须 await 调用。
返回:
修改后的 result 对象,其 content 字段中增加了结构化内容的文本表示。
"""
# 3. 调用实际的处理函数,获取工具执行结果
result = await handler(request)
# 4. 如果结果中包含结构化内容(如 JSON 对象),则将其转换为 JSON 字符串
# 并作为一个新的 TextContent 追加到响应的 content 列表中
if result.structuredContent:
# 将结构化内容序列化为 JSON 字符串
structured_json = json.dumps(result.structuredContent, ensure_ascii=False)
# 构造一个 TextContent 对象(类型为 'text')
text_content = TextContent(type="text", text=structured_json)
# 追加到原始 content 列表末尾
result.content += [text_content]
# 5. 返回修改后的结果,供上层使用
return result
# ============================================================
# 6. 主异步函数:创建 MCP 客户端,获取工具,构建 Agent 并执行查询
# ============================================================
async def main():
"""
主逻辑:
- 创建 MultiServerMCPClient,并注册工具拦截器。
- 从客户端获取所有可用工具(MCP 工具转换为 LangChain 工具)。
- 使用这些工具构建一个 Agent
- 向 Agent 发送用户消息,获取响应并打印。
"""
# 7. 创建多服务器 MCP 客户端
# 配置参数(...)需要填写实际的 MCP 服务器配置,例如:
# {
# "server1": {"transport": "stdio", "command": "python", "args": ["server.py"]},
# "server2": {"transport": "http", "url": "http://localhost:8000/mcp"}
# }
# 同时传入 tool_interceptors 列表,包含我们定义的拦截器函数
client = MultiServerMCPClient(
{...}, # 请替换为实际的服务器配置字典
tool_interceptors=[append_structured_content] # 注册拦截器
)
# 8. 获取客户端提供的所有工具(自动将 MCP 工具适配为 LangChain 工具)
tools = await client.get_tools()
# 9. 创建 Agent
# 假设 create_agent 来自 langchain.agents 或自定义函数,
# 第一个参数是 LLM 模型名称(如 "gpt-5-mini"),第二个参数是工具列表
agent = create_agent(
"gpt-5-mini", # 使用 OpenAI 的 GPT-5-mini 模型(实际请根据可用模型调整)
tools # 绑定工具,使 Agent 能够调用它们
)
# 10. 使用异步方式调用 Agent,传入用户消息
weather_response = await agent.ainvoke(
{
"messages": [
# 用户消息,角色为 'user',内容是询问上海天气
{"role": "user", "content": "上海的天气怎么样?"}
]
}
)
# 11. 打印 Agent 的完整响应(包含最终回答以及工具调用信息等)
print(weather_response)
# ============================================================
# 12. 程序入口:使用 asyncio 运行主协程
# ============================================================
if __name__ == "__main__":
import asyncio
asyncio.run(main())
4.6 加载资源
- Blob 对象(统一数据容器)
| 维度 | 详细说明 |
|---|---|
| 底层原理 | LangChain 的 Blob 是对文件/二进制数据的统一抽象。MCP 适配器将 ReadResourceResult 中的 contents(文本或 Base64 二进制)统一封装为 Blob,屏蔽文本与二进制的读取差异,并提供惰性解码(as_string 按需转换) |
| 属性方法详解 | • metadata:dict,包含 uri(资源标识符)及其他服务端元数据 • mimetype:str,如 text/plain、image/png、application/json • as_string():解码为 str(适用于文本类资源) • as_bytes():返回 bytes(适用于图片/二进制文件) |
| 关键陷阱 | • 若对二进制资源调用 as_string,会抛出解码异常或产生乱码,调用前务必检查 mimetype • 大文件资源会全量加载至内存,MCP 协议暂不支持流式读取,需自行评估内存压力 |
将 Python 对象(如字典、列表)转换为符合 JSON 格式的字符串
json.dumps()
| 参数名 | 类型 | 是否必需 | 默认值 | 作用说明 |
|---|---|---|---|---|
obj |
Any | 是 | 无 | 需要转换的 Python 对象 |
ensure_ascii |
bool | 否 | True |
中文显示关键 。若为 True,非 ASCII 字符(如中文)会被转为 \uXXXX 转义符;若为 False,则保留原始中文 |
indent |
int/str | 否 | None |
格式化缩进。传入数字(如 2 或 4)会让生成的字符串带有换行和缩进,便于人眼阅读 |
sort_keys |
bool | 否 | False |
是否对字典的 键(Key) 按字母顺序排序 |
separators |
tuple | 否 | (', ', ': ') |
分隔符设置。用于控制键值对之间、键与值之间的分隔符号 |
skipkeys |
bool | 否 | False |
键类型过滤。若为 True,会跳过键不是基础类型(str/int/float等)的键值对,不报错;否则抛出 TypeError |
default |
function | 否 | None |
自定义序列化。当遇到无法序列化的对象(如 datetime 或自定义类)时,调用此函数处理 |
- 客户端直接加载 Resources(client.get_resources)
| 维度 | 详细说明 |
|---|---|
| 完整签名 | `async get_resources(server_name: str |
| 底层原理 | 建立临时会话 → 若未传 uris,调用 list_resources 获取所有 URI 再逐条 read_resource;若传入 uris,直接批量调用 read_resource → 转换为 Blob 列表 → 立即关闭会话(默认无状态) |
| 参数说明 | • server_name:必选(官方强烈建议显式传入,避免多服务器歧义) • uris:可选。传入则只读取指定资源;不传则读取服务器上所有可读资源(依赖服务端实现,行为不可靠) |
| 标准示例 | blobs = await client.get_resources("MyServer") blobs = await client.get_resources("MyServer", uris=["data://config"]) |
| 关键陷阱 | • uris 为空时的行为非标准 :部分 MCP 服务端支持返回全部资源,部分直接报错,生产环境务必显式传入 uris • 每次调用均触发握手,高频场景下有性能开销 |
- 基于 Session 手动控制加载(load_mcp_resources)
| 维度 | 详细说明 |
|---|---|
| 完整签名 | `async load_mcp_resources(session: ClientSession, uris: list[str] |
| 底层原理 | 与 client.get_resources 逻辑一致(调用 list_resources / read_resource),但会话生命周期由调用方显式控制 。配合 async with client.session("Server") as session: 使用,可在同一长连接中分批读取多个资源,避免重复握手的网络开销 |
| 参数说明 | • session:由 client.session() 产出的 ClientSession 实例 • uris:同 get_resources,可选 |
| 标准示例 | python<br>async with client.session("MyServer") as session:<br> all_blobs = await load_mcp_resources(session)<br> specific_blobs = await load_mcp_resources(session, uris=["data://config"])<br> |
| 适用场景 | • 需要在一个会话中顺序读取多个资源 (如加载全套配置文件) • 需要配合 session 做事务性读取(如读完资源 A 再读资源 B,确保来自同一服务端快照) |
| 关键陷阱 | • 必须确保 session 处于活动状态(async with 块内),退出块后 Blob 对象仍可独立使用(数据已加载至内存),但不可再次发起读取请求 • 与 get_tools 不同,load_mcp_resources 不自动缓存,重复调用会重复拉取 |
4.7 加载提示词
| 维度 | 详细说明 |
| 完整签名 | async get_prompt(server_name: str, prompt_name: str, *, arguments: dict[str, Any] | None = None) -> list[HumanMessage | AIMessage] |
| 参数详解 | • server_name (必选):目标服务器名称 • prompt_name (必选):MCP 服务端定义的提示词唯一标识符 • arguments (可选,仅限关键字):dict[str, Any],用于填充提示词模板中的动态变量 |
| 返回值 | list[BaseMessage]:LangChain 消息列表。MCP 的 PromptMessage 被转换为对应的 HumanMessage、AIMessage 或 SystemMessage(取决于 role 字段) |
| 内部机制 | 1. 创建临时会话连接至目标服务器 2. 调用 session.get_prompt(prompt_name, arguments) 获取原始 MCP 提示结构 3. 遍历 messages 数组,将每个 PromptMessage 的 content 和 role 映射为 LangChain 消息对象 4. 关闭会话并返回消息列表 |
| 使用场景 | • 动态加载系统提示词 :将 System Prompt 托管在 MCP 服务端,便于业务方热更新而无需重新部署 Agent 代码 • 多租户个性化提示 :根据 arguments 传入不同租户 ID,拉取对应的约束或风格提示 • 将复杂提示词(含多轮对话范例)从代码中剥离,便于运维管理 |
| 关键陷阱 | • MCP 标准中提示词可以是多轮消息 (例如用户开场白+助手范例),返回的 list 需要与后续调用链正确拼接,不可直接当作 System Prompt 丢掉 Role 信息 • 若 arguments 缺失必填字段,MCP 服务端会返回错误(通常是 InvalidParams),但客户端只会抛异常,需在调用前做好参数校验 • 此方法不缓存结果,每次调用均建立新连接并拉取全量数据,高频场景建议自行增加 @lru_cache 或 Redis 缓存 |
|---|
| 维度 | 详细说明 |
|---|---|
| 完整签名 | `async load_mcp_prompt(session: ClientSession, prompt_name: str, *, arguments: dict[str, Any] |
| 参数详解 | • session:ClientSession,由 client.session("服务器名称") 上下文管理器产出 • prompt_name:str,同 get_prompt • arguments:`dict[str, Any] |
| 返回值 | list[BaseMessage],结构与 get_prompt 完全一致。 |
| 底层原理 | 与 get_prompt 核心逻辑相同(调用 MCP prompts/get),但会话生命周期由调用方显式控制 。会话在 async with client.session(...): 块内保持活动状态,可在此长连接中顺序执行多次 load_mcp_prompt 或配合 load_mcp_tools / load_mcp_resources 混合使用。 |
| 适用场景 | • 需在一个会话中批量加载多个提示 (减少握手开销) • 需配合有状态服务端(如服务端记录提示加载次数或上下文) • 需在同一会话中混用 Tools + Resources + Prompts |
| 关键陷阱 | • 必须处于 async with client.session(...): 块内,退出后会话关闭,不可再调用 load_mcp_prompt • 若在会话块内抛出异常,需自行捕获并处理,否则会话可能无法正常关闭导致资源泄露 • 与 get_prompt 不同,此方式不会自动关闭会话,需由开发者确保块内逻辑正确退出 |
4.8 调用三方服务器
import asyncio
import json
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.prompts import load_mcp_prompt
from langchain_mcp_adapters.tools import load_mcp_tools
async def main():
client = MultiServerMCPClient(
{
"time": {
"command": "python",
"args": ["-m", "mcp_server_time"],
"transport": "stdio",
}
},
)
async with client.session("time") as session:
tools = await load_mcp_tools(session)
agent = create_agent("gpt-5-mini", tools)
response = await agent.ainvoke(
{
"messages": [
{
"role": "user",
"content": (
"北京时间的17:50分,对应的英国时间是?"
)
}
]
}
)
print(response)
if __name__ == "__main__":
asyncio.run(main())
4.9 客户端示例(同时连接本地数学服务器和远程天气服务器)
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
async def main():
client = MultiServerMCPClient({
"Math": {
"transport": "stdio",
"command": "python",
"args": ["/path/to/math_server.py"],
},
"Weather": {
"transport": "streamable-http",
"url": "http://localhost:8000/mcp",
}
})
tools = await client.get_tools()
agent = create_agent("gpt-5-mini", tools)
math_resp = await agent.ainvoke({"messages": [{"role": "user", "content": "(3+5)*12 等于多少?"}]})
weather_resp = await agent.ainvoke({"messages": [{"role": "user", "content": "上海天气怎么样?"}]})
print(math_resp, weather_resp)
asyncio.run(main())
4.10 有状态会话
默认 MultiServerMCPClient 每次工具调用都会新建会话,无状态
对于需要维护上下文的服务器(如对话记忆),可使用 client.session() 手动管理:
async with client.session("Weather") as session:
tools = await load_mcp_tools(session)
agent = create_agent("gpt-5-mini", tools)
resp1 = await agent.ainvoke(...) # 复用同一会话
resp2 = await agent.ainvoke(...)
6. 高级功能
6.1 工具拦截器
MCP 服务器作为独立进程运行,与 LangGraph 的运行时信息(如 state、store、上下文等)隔离,而拦截器(Interceptors)属于客户端,可在工具使用前后访问 LangGraph 的运行时上下文、修改工具请求参数、注入动态认证头、实现重试/降级策略,甚至在特定条件下返回 Command 直接控制 LangGraph 的图流程走向,从而以类似中间件的非侵入式方式,对 MCP 工具调用进行精细化管控
MCPToolCallRequest
MCPToolCallRequest 代表了传递给拦截器的一次 MCP 工具调用请求
| 字段名 | 类型 | 类别 | 描述 |
|---|---|---|---|
name |
str |
可修改 | 要调用的 MCP 工具的名称 |
args |
dict[str, Any] |
可修改 | 传递给工具的参数,以键值对形式提供 |
headers |
dict[str, Any] | None |
可修改 | 用于适用传输方式的 HTTP 请求头 |
server_name |
str |
上下文 (只读) | 处理此工具调用的 MCP 服务器的名称, 可用于路由或日志记录 |
runtime |
object | None |
上下文 (只读) | 当在 LangGraph 图中执行时,此字段包含 LangGraph 的运行时上下文,用于访问状态、存储等。在 LangGraph 环境外为 None |
6.1.1 访问运行时上下文
from fastmcp import FastMCP
mcp = FastMCP("Weather")
# 新增 user_id 参数
@mcp.tool()
async def get_weather(city: str, user_id: str) -> str:
"""获取天气"""
return f"用户{user_id}查询:{city} 天气晴朗!"
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=8000)
import asyncio
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
@dataclass
class Context:
user_id: str
api_key: str
async def inject_user_context(request: MCPToolCallRequest, handler):
runtime = request.runtime
user_id = runtime.context.user_id
api_key = runtime.context.api_key
# 注入参数
modified_request = request.override(
args={**request.args, "user_id": user_id}
)
return await handler(modified_request)
async def main():
client = MultiServerMCPClient(
{
"Weather": {
"transport": "streamable-http",
"url": "http://localhost:8000/mcp",
}
},
tool_interceptors=[inject_user_context]
)
tools = await client.get_tools()
agent = create_agent(
"gpt-5-mini",
tools,
context_schema=Context,
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "上海的天气怎么样?"}]},
context={"user_id": "user_123", "api_key": "sk-..."}
)
print(result)
if __name__ == "__main__":
asyncio.run(main())
- **(双星号):在字典字面量 {} 中,用于合并字典。它将 request.args 中的每一项(Key-Value)展开,追加到新字典里
- *(单星号):在字典字面量 {} 中,用于解包可迭代对象的元素。遍历字典默认只返回键(Key),所以解出来的是 Key 的集合
6.1.2 状态更新与流程控制 (Commands)
拦截器可以返回 Command 对象,用于:
- 更新 Agent State(状态)
- 控制 LangGraph 图的执行流向
这在跟踪任务进度、在 Agent 间切换或提前结束执行时非常有用
-
标记任务完成并切换 Agent
from langchain.agents import AgentState, create_agent
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.messages import ToolMessage
from langgraph.types import Commandasync def handle_task_completion(
request: MCPToolCallRequest,
handler,
):
"""
当工具为 submit_order 时,标记任务完成并跳转到 summary_agent。
"""
# 先执行原始工具调用
result = await handler(request)# 如果是 submit_order 工具,则返回跳转命令 if request.name == "submit_order": return Command( update={ # 将工具结果添加到消息列表,并更新自定义状态字段 "messages": [result] if isinstance(result, ToolMessage) else [], "task_status": "completed", }, goto="summary_agent", # 指定跳转的目标节点 ) # 其他情况正常返回结果 return result -
工具执行成功后提前结束 Agent 运行
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langgraph.types import Commandasync def end_on_success(
request: MCPToolCallRequest,
handler,
):
"""
当 mark_complete 工具被调用时,结束整个 Agent 运行。
"""
# 先执行原始工具调用
result = await handler(request)# 如果是 mark_complete 工具,则直接终止图执行 if request.name == "mark_complete": return Command( update={"messages": [result], "status": "done"}, goto="__end__", # 特殊值,表示终止执行 ) # 其他情况正常返回结果 return result
6.1.3 自定义拦截器
基本拦截器模式
一个拦截器接收 request 和 handler 两个参数。你可以在调用 handler 前后执行逻辑,或者完全跳过它(不调用 handler)
-
核心逻辑:在调用 handler 前后执行自定义逻辑(如日志记录)
-
必须返回:await handler(request) 的结果
-
典型用途:日志记录、性能监控
async def logging_interceptor(request, handler):
print(f"Calling: {request.name}")
result = await handler(request)
print(f"Returned: {result}")
return result
修改请求参数(不可变模式)
-
关键方法:使用 request.override() 创建修改后的副本
-
原则:不要直接修改原始 request 对象
-
典型用途:参数转换、默认值注入、敏感信息脱敏
async def double_args_interceptor(request, handler):
modified_args = {k: v * 2 for k, v in request.args.items()}
modified_request = request.override(args=modified_args)
return await handler(modified_request)
运行时动态修改 HTTP 请求头
-
能力:根据工具名称或请求内容动态生成认证信息
-
典型用途:多租户鉴权、动态 Token 刷新
async def auth_header_interceptor(request, handler):
token = get_token_for_tool(request.name)
modified_request = request.override(
headers={"Authorization": f"Bearer {token}"}
)
return await handler(modified_request)
错误处理与重试机制
-
策略:指数退避(Exponential Backoff)
-
关键参数:max_retries、delay(基础等待时间)
-
注意:所有重试失败后抛出最后一个异常
async def retry_interceptor(request, handler, max_retries=3, delay=1.0):
for attempt in range(max_retries):
try:
return await handler(request)
except Exception as e:
if attempt == max_retries - 1:
raise e
await asyncio.sleep(delay * (2 ** attempt))
错误降级处理
-
核心:捕获特定异常,返回兜底值而不抛出
-
典型用途:缓存回退、优雅降级、提示用户
async def fallback_interceptor(request, handler):
try:
return await handler(request)
except TimeoutError:
return f"Tool {request.name} timed out. Using cached data."
except ConnectionError:
return f"Cannot connect to {request.name} service."
组合多个拦截器(洋葱模型)
-
执行顺序:从外到内进入,从内到外返回
-
注册方式:tool_interceptors=outer, inner
-
执行流程:
outer: before → inner: before → [实际工具执行] → inner: after → outer: after
client = MultiServerMCPClient(
server_configs,
tool_interceptors=[outer_interceptor, inner_interceptor]
)
6.2 进度通知
- 核心价值:客户端可订阅 MCP 服务器在长时任务中的进度更新,实现实时反馈
- 前置条件:服务端必须主动发送进度通知,客户端被动接收
- 实现方式:在 FastMCP 中,通过工具函数内的 Context 对象发送进度信息
- 适用场景:文件处理、大型数据集查询、模型推理等耗时操作,提升用户体验
6.2.1 构建 MCP 服务器
- Context
Context 是 FastMCP 为工具函数提供的上下文对象,封装了 MCP 协议中的进度报告、日志记录、用户交互等功能。它在服务端工具执行期间作为通信中枢,实现与客户端的双向交互
**获取方式:**直接在工具函数签名中添加 ctx: Context 参数,FastMCP 会在调用时自动注入
from fastmcp import Context
async def my_tool(..., ctx: Context) -> str:
await ctx.report_progress(...)
return "done"
- Context 功能
| 功能分类 | 方法/属性 | 说明 |
|---|---|---|
| 进度报告 | report_progress(progress, total, message) |
向客户端发送进度更新,用于长时任务反馈 |
| 日志记录 | debug(), info(), warning(), error() |
向客户端发送不同级别的日志消息 |
| 用户交互 | elicit(message, schema) |
请求客户端用户提供结构化输入(如表单填写) |
| 资源访问 | read_resource(uri) |
读取服务器注册的资源内容 |
| LLM 采样 | sample(messages) |
请求客户端 LLM 生成文本(用于联合推理) |
| 会话标识 | session_id, request_id, client_id |
获取当前会话/请求的标识符,用于追踪和关联 |
- report_progress
该方法用于在长时操作中向客户端发送进度通知,使用户获得实时反馈
| 参数 | 类型 | 说明 |
|---|---|---|
progress |
float |
当前进度值(如已处理行数) |
total |
`float | None` |
message |
`str | None` |
await ctx.report_progress(progress=50, total=100, message="正在处理第50项...")
-
完整示例(处理大文件时周期性报告进度)
import asyncio
from fastmcp import FastMCP, Context1. 创建服务器实例
mcp = FastMCP("file_server")
2. 定义工具函数,通过 ctx 报告进度
@mcp.tool()
async def process_large_file(file_path: str, ctx: Context) -> str:
"""
模拟处理大文件,分阶段发送进度更新。
实际应用中,进度通知应在真正的耗时循环中调用。
"""
total = 1000.0 # 文件总行数# 阶段1:读取头部 await ctx.report_progress(125, total, "正在读取 CSV 头部...") await asyncio.sleep(0.1) # 模拟耗时操作 # 阶段2:转换数据 await ctx.report_progress(372, total, "正在转换第 1000 行...") await asyncio.sleep(0.1) # 阶段3:写入结果 await ctx.report_progress(728, total, "正在写入结果文件...") await asyncio.sleep(0.1) # 阶段4:完成 await ctx.report_progress(1000, total, "处理完成") await asyncio.sleep(0.1) return f"文件 {file_path} 已成功处理。"3. 运行服务器
if name == "main":
mcp.run(transport="streamable-http", port=8000)
6.2.2 构建 MCP 客户端
Callbacks
Callbacks 是 langchain-mcp-adapters 库中用于注册 MCP 协议回调函数的容器类,主要用于接收来自 MCP 服务器的进度通知、日志消息、用户交互请求等异步事件
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
on_progress |
Callable |
选填 | 进度更新回调函数,接收来自服务端的进度通知 |
on_log |
Callable |
选填 | 日志消息回调函数,接收服务端发送的日志信息 |
on_elicit |
Callable |
选填 | 用户交互回调函数,处理服务端发起的 elicit 请求 |
on_sample |
Callable |
选填 | LLM 采样回调函数,处理服务端发起的 sample 请求 |
on_start |
Callable |
选填 | 工具调用开始时的回调函数 |
on_end |
Callable |
选填 | 工具调用结束时的回调函数 |
on_error |
Callable |
选填 | 工具调用发生异常时的回调函数 |
- 注册进度回调
客户端通过 Callbacks(on_progress=...) 注册进度回调函数,从而订阅 MCP 服务器的进度更新
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
async def on_progress(progress: float, total: float | None, message: str | None, context: CallbackContext):
"""处理来自 MCP 服务器的进度更新"""
pass
client = MultiServerMCPClient(
{"file_server": {"transport": "http", "url": "http://localhost:8000/mcp"}},
callbacks=Callbacks(on_progress=on_progress), # 重点:注册进度回调
)
- 回调函数参数详解
| 参数 | 类型 | 说明 |
|---|---|---|
progress |
float |
当前进度值(由服务器定义,通常为已处理单元数量或比例) |
total |
`float | None` |
message |
`str | None` |
context |
CallbackContext |
当前调用上下文元数据(服务器名称、工具名称等) |
-
CallbackContext 结构
class CallbackContext:
server_name: str # 发送该进度通知的 MCP 服务器名称
tool_name: str # 当前正在执行的工具名称(仅在工具调用期间有效) -
完整客户端示例
import asyncio
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent定义进度回调
async def on_progress(
progress: float,
total: float | None,
message: str | None,
context: CallbackContext,
):
if total:
percent = progress / total * 100
print(f" {context.tool_name} 已完成 {percent:.1f}%:{message}")
else:
print(f" {context.tool_name} 进度 {progress}:{message}")async def main():
client = MultiServerMCPClient(
{
"file_server": {
"transport": "http",
"url": "http://localhost:8000/mcp",
}
},
callbacks=Callbacks(on_progress=on_progress) # 注册进度回调
)tools = await client.get_tools() agent = create_agent("gpt-5-mini", tools) result = await agent.ainvoke({ "messages": [{"role": "user", "content": "请处理大文件 report.csv"}] }) print(result)if name == "main":
asyncio.run(main()) -
单向推送:进度通知由服务端主动向客户端发送,客户端仅能接收和展示进度信息,无法通过回调干预工具执行
-
上下文标识:CallbackContext 提供了 server_name 和 tool_name 字段,便于在多服务、多工具场景下区分进度信息来源
-
服务端扩展能力:服务端 Context 除了进度报告外,还支持日志记录、用户交互(elicit)、资源访问(read_resource)、LLM 采样(sample)等 MCP 高级功能
6.3 日志记录
- MCP 支持服务端在工具执行过程中向客户端发送多级别日志通知
- 服务器通过 ctx.info()、ctx.debug()、ctx.warning()、ctx.error() 发送日志
- 日志通过服务端 Context 对象发送,客户端通过 Callbacks 注册回调接收
服务端实现(发送日志)
from fastmcp import FastMCP, Context
mcp = FastMCP("FetchData")
@mcp.tool()
async def fetch_data(query: str, ctx: Context) -> str:
"""数据获取工具,执行过程中发送不同级别的日志。"""
await ctx.info(f"开始处理查询: {query}") # 信息级别
await ctx.debug("正在连接数据源...") # 调试级别
await ctx.warning("数据源响应较慢,请稍候...") # 警告级别
await ctx.info(f"查询 '{query}' 处理完成")
return f"结果:您查询的 '{query}' 没有匹配数据。"
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=8000)
| 方法 | 级别 | 说明 |
|---|---|---|
ctx.debug(message) |
debug |
调试信息,用于开发排查 |
ctx.info(message) |
info |
常规操作信息 |
ctx.warning(message) |
warning |
警告信息(非致命问题) |
ctx.error(message) |
error |
错误信息(操作失败但可恢复) |
-
客户端实现(接收日志)
import asyncio
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
from mcp.types import LoggingMessageNotificationParams定义日志回调函数
async def on_logging_message(
params: LoggingMessageNotificationParams,
context: CallbackContext,
):
"""处理来自 MCP 服务器的日志消息。"""
print(f"[{context.server_name}] {params.level}: {params.data}")async def main():
client = MultiServerMCPClient(
{
"fetch_data": {
"transport": "http",
"url": "http://localhost:8000/mcp",
}
},
callbacks=Callbacks(on_logging_message=on_logging_message), # 关键配置
)tools = await client.get_tools() agent = create_agent("gpt-5-mini", tools) result = await agent.ainvoke({ "messages": [{"role": "user", "content": "查询123订单的详细数据"}] }) print(result)if name == "main":
asyncio.run(main()) -
回调参数详解
| 参数 | 类型 | 说明 |
|---|---|---|
params |
LoggingMessageNotificationParams |
日志消息参数对象,包含以下字段 |
params.level |
str |
日志级别(debug、info、warning、error) |
params.data |
str |
日志消息内容 |
context |
CallbackContext |
调用上下文元数据,包含 server_name 和 tool_name |
6.4 引导式输入
Elicitation(引导式输入) 是 MCP 协议中一项允许服务器在工具执行过程中向用户请求额外输入的机制,无需在调用前一次性提供所有参数
- 服务端实现(发起引导请求)
使用 ctx.elicit(message, response_type) 方法向客户端发起引导请求,并定义期望返回的数据结构
from pydantic import BaseModel
from fastmcp import Context, FastMCP
server = FastMCP("Profile")
# 定义用户期望返回的数据结构
class UserDetails(BaseModel):
email: str
age: int
@server.tool()
async def create_profile(name: str, ctx: Context) -> str:
"""创建用户资料,缺失信息通过 elicitation 询问"""
# 发起引导请求
result = await ctx.elicit(
message=f"请为 {name} 的个人资料提供详细信息:",
response_type=UserDetails, # 期望返回的数据结构
)
# 根据用户响应进行不同处理
if result.action == "accept" and result.data:
return f"为 {name} 创建了个人资料: email={result.data.email}, age={result.data.age}"
elif result.action == "decline":
return f"用户拒绝了。为 {name} 创建了最简信息资料。"
else: # cancel
return "个人资料创建已取消。"
关键方法:ctx.elicit(message, response_type)
| 参数 | 类型 | 说明 |
|---|---|---|
message |
str |
向用户展示的提示信息 |
response_type |
BaseModel |
期望用户返回的数据结构(Pydantic 模型) |
返回对象:
| 属性 | 类型 | 说明 |
|---|---|---|
result.action |
str |
用户响应动作(accept / decline / cancel) |
result.data |
`BaseModel | None` |
- 客户端实现(处理引导请求)
客户端通过向 MultiServerMCPClient 提供 on_elicitation 回调来处理服务器的引导请求
import asyncio
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
from mcp.shared.context import RequestContext
from mcp.types import ElicitRequestParams, ElicitResult
async def on_elicitation(
mcp_context: RequestContext,
params: ElicitRequestParams,
context: CallbackContext,
) -> ElicitResult:
"""处理来自 MCP 服务器的引导请求"""
# 实际应用中,此处应根据 params.message 和 params.requestedSchema 提示真实用户输入
return ElicitResult(
action="accept",
content={"email": "user@example.com", "age": 25},
)
async def main():
client = MultiServerMCPClient(
{
"profile": {
"url": "http://localhost:8000/mcp",
"transport": "http",
}
},
callbacks=Callbacks(on_elicitation=on_elicitation), # 注册回调
)
tools = await client.get_tools()
agent = create_agent("gpt-5-mini", tools)
result = await agent.ainvoke({
"messages": [{"role": "user", "content": "创建小明的个人资料"}]
})
print(result)
if __name__ == "__main__":
asyncio.run(main())
- 回调参数详解
| 参数 | 类型 | 说明 |
|---|---|---|
mcp_context |
RequestContext |
MCP 请求上下文,包含当前会话和请求信息 |
params |
ElicitRequestParams |
引导请求参数对象 |
params.message |
str |
服务端发送的提示信息 |
params.requestedSchema |
dict |
期望返回的数据结构(JSON Schema 格式) |
context |
CallbackContext |
调用上下文元数据(server_name、tool_name) |
- 响应动作详解
| 动作 | 含义 | 使用示例 |
|---|---|---|
| accept | 用户提供了有效输入 | ElicitResult(action="accept", content={"email": "user@example.com", "age": 25}) |
| decline | 用户拒绝提供所请求的信息 | ElicitResult(action="decline") |
| cancel | 用户完全取消当前操作 | ElicitResult(action="cancel") |