MCP(Model Context Protocol)[ 3 ]

MCP 高级功能

工具拦截器

接下来我们一起来学习 MCP 的高级使用功能,第一个核心能力就是工具拦截器。

之前讲 MCP 服务器时,我们分别定义过工具、资源、提示词模板,也逐一讲解了它们的用法与核心知识点,当时就提到:我们可以借助工具拦截器,对工具返回的结果做自定义更新。

当时我们写过一段拦截器代码,它的作用和我们之前学过的中间件非常相似。逻辑是:调用工具拿到返回结果result后,我们能对result里的content内容做自定义处理【多添加了一个结构化返回字段】,这套处理逻辑全部依靠工具拦截器实现。

使用方式也很简单: 拦截器必须定义为异步函数,然后将拦截器配置到客户端 MultiServerMCPClient 初始化参数里的 tool_interceptors 配置项,值是一个列表,把我们写好的拦截器函数放进列表即可;多拦截器可以依次往列表追加。绑定完成后,后续实际调用 MCP 工具时,就能通过这个拦截器,在工具执行前做前置处理、工具执行后做后置处理

以上是我们之前简单接触过的工具拦截器,当时只写了一个简易示例,让大家知道有这个功能、能修改工具返回结果。本篇我们就深入拆解,讲清楚工具拦截器真正能落地的各类业务场景,搞明白它到底具备哪些能力。

为什么需要工具拦截器?

MCP 服务器作为独立进程 运行,它们无法直接访问 LangGraph 的运行时信息 ,例如存储(store)、上下文(context)或 Agent 状态(state)。

拦截器(Interceptors)填补了这一空白,它在 MCP 工具执行期间提供了访问这些运行时上下文的途径。同时,拦截器也提供了类似中间件的控制能力:可以修改请求、实现重试逻辑、动态添加请求头,甚至完全中断执行。

核心能力 1:访问运行时上下文

当 MCP 工具在 LangChain Agent(通过 create_agent 创建)内部使用时,拦截器会接收到 ToolRuntime 上下文。 这使得我们可以访问 tool_call_id 工具调用 ID、state 状态、config 配置和 store 存储,从而实现访问用户数据、持久化信息和控制 Agent 行为等强大模式。

典型场景:向 MCP 工具调用注入用户上下文

我们先从一个典型业务场景入手:向 MCP 工具调用中注入用户上下文

python 复制代码
from fastmcp import FastMCP

mcp = FastMCP("Weather")

@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)

首先看上面基础 MCP 服务代码,这是一个简易天气 MCP 服务器,通过 8000 端口启动 HTTP 服务,服务内只定义了一个get_weather工具。 这个工具和我们之前写的工具有一处明显区别:除了必填的city城市参数,还新增了user_id参数,最终返回的结果会拼接 "用户 xxx 查询 xx 城市天气" 的文本。

这里就出现了一个关键问题【这也是上面说的为什么需要拦截器的具体体现】: MCP 服务器是独立进程运行的,它没办法直接读取 Agent、LangGraph 图里的运行时上下文信息。MCP 服务本身是独立运行的工具进程,和我们的 Agent、LangGraph 运行环境完全隔离。如果我们想把上下文里的信息(比如这里的用户 ID)传给 MCP 工具,只能通过工具入参手动传递 ;如果不主动把user_id当成参数传给工具,MCP 服务器完全无法从 Agent、Graph 的运行上下文里自动读取这个值。

清楚这个底层限制后,我们再写一段客户端调用代码,完整复现这个问题。我们先创建MultiServerMCPClient客户端实例,填入天气服务的 HTTP 地址,先运行代码测试连通性。调用client.get_tools()拉取服务端工具列表,能打印出get_weather就代表客户端和 MCP 服务连接正常。

python 复制代码
import asyncio

from langchain.agents import create_agent
from langchain_deepseek import ChatDeepSeek
from langchain_mcp_adapters.client import MultiServerMCPClient

model = ChatDeepSeek(
    model="deepseek-chat",
    temperature=0.0,
)

client = MultiServerMCPClient({
    "Weather": {
        "transport": "streamable-http",
        "url": "http://localhost:8000/mcp",
    }
})

async def main():
    tools = await client.get_tools()
    # for tool in tools:
    #     print(tool.name)
    agent = create_agent(model=model, tools=tools)
    response = await agent.ainvoke(
        {
            "messages": [{"role": "user", "content": "上海的天气如何?"}]
        }
    )

    for msg in response.get("messages"):
        msg.pretty_print()

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

连通成功后,我们基于拉取到的工具创建 Agent,调用create_agent,传入大模型名称和工具列表。创建完成后,调用agent.ainvoke()发起提问,比如让 Agent 查询上海的天气,Agent 会自主调用get_weather工具,最后打印工具返回的结果。

python 复制代码
================================ Human Message =================================

上海的天气如何?
================================== Ai Message ==================================

好的,我来查询上海的天气情况。
Tool Calls:
  get_weather (call_00_CwCiRlTKu4HfZNnLhfpO7099)
 Call ID: call_00_CwCiRlTKu4HfZNnLhfpO7099
  Args:
    city: 上海
    user_id: user
================================= Tool Message =================================
Name: get_weather

[{'type': 'text', 'text': '用户[user]查询: 上海 天气晴朗!', 'id': 'lc_ef0a668d-4d47-4325-a5a4-e6a440de7b58'}]
================================== Ai Message ==================================

上海的天气情况如下:

🌤 **上海天气:晴朗!**

目前上海天气晴好,阳光明媚,是个不错的好天气!如果您有出行计划,可以放心安排。

请问您还需要了解其他城市的天气吗?

这里会暴露出核心矛盾: Agent 本身只会自主填充提问里明确给出的city参数(上海),但工具要求的user_id参数,Agent 没办法自动填充【随机填充】。 我们的用户 ID、密钥这类信息,是存放在 Agent 的运行上下文里的,我们先定义上下文数据结构:用dataclass声明Context类,里面包含两个属性,一个user_id字符串存储用户 ID,再加一个api_key字段存储第三方接口密钥(比如高德地图密钥,这里只是用来丰富上下文结构,做演示)。

python 复制代码
@dataclass
class Context:
    user_id: str
    api_key: str

定义好上下文类后,创建 Agent 时通过context_schema参数把这个上下文结构绑定到 Agent。后续调用agent.ainvoke()的时候,我们就能手动传入自定义上下文,比如指定user_id="111"api_key="sk-xxx"

python 复制代码
response = await agent.ainvoke(
    {
        "messages": [{"role": "user", "content": "上海的天气如何?"}]
    },
    context=Context(user_id="111", api_key="sk-xxx"),
)

上下文已经成功传给 Agent 了,我们现在运行代码测试,看看 Agent 能不能自动把上下文里的user_id传给 MCP 工具。 打印工具返回的ToolMessage结果就能发现问题:返回文本里的user_id并不是我们传入的111,还是模型自动生成的随机默认值。

这就说明: 即便 Agent 持有我们传入的上下文,它也无法自动把上下文里的user_id匹配成工具的入参 。 根本原因是大模型没办法自主识别上下文字段的业务含义:不同开发者的命名风格完全不一样,有人存用户 ID 的字段叫user_id,有人简写为uid,还有人直接命名id,大模型无法通过字段名判断这个值是要传给工具的入参,自然不会自动把上下文里的用户 ID 塞进工具调用参数中。

简单总结: 上下文里的字段含义只有开发者自己清楚,大模型没办法自主解析、自动透传给 MCP 工具,必须由我们开发者手动处理上下文数据。

铺垫完所有背景知识,现在解决核心问题:我们已经把user_id=111存入上下文,要怎么把这个值自动传给 MCP 工具的user_id参数? 答案就是工具拦截器,这也是我们铺垫这么多背景,要给大家讲清的拦截器核心用途。

我们先看工具拦截器的标准写法: 拦截器是一个异步方法,固定接收两个参数,第一个是MCPToolCallRequest类型的request(存储本次工具调用的全部请求信息),第二个是handler回调函数,handler才是真正执行 MCP 工具调用的方法。

python 复制代码
# 导入依赖
from langchain_mcp_adapters.interceptors import MCPToolCallRequest

# 工具拦截器标准异步方法
async def custom_tool_interceptor(
    # 第一个固定参数:MCPToolCallRequest 类型,存放本次工具调用全部请求信息
    request: MCPToolCallRequest,
    # 第二个固定参数:handler 回调,负责真实执行MCP工具调用
    handler
):
    # 前置处理逻辑:工具调用前自定义操作
    print("拦截器前置:即将调用工具,请求信息:", request)

    # 调用handler,执行真实MCP工具,拿到工具返回结果
    tool_result = await handler(request)

    # 后置处理逻辑:工具调用完成后自定义加工结果
    print("拦截器后置:工具执行完毕,原始结果:", tool_result)

    # 返回最终处理后的工具结果
    return tool_result

基于这个结构,拦截器可以在调用handler之前执行前置逻辑,也可以在handler执行完、拿到结果后执行后置逻辑,最后返回处理后的结果。

回到我们的需求: 把上下文里的user_id自动注入工具参数,这段逻辑放在拦截器的前置环节处理即可。

从request中取出runtime属性,runtime就是 LangGraph 完整的运行时上下文对象,Agent 本身基于 LangGraph 运行,所以所有运行时数据都存在这里;

python 复制代码
@dataclass
class MCPToolCallRequest:
    """传递给 MCP 工具调用拦截器的工具执行请求对象。

    该工具调用请求的设计模式与 LangChain 的 ToolCallRequest 相近(扁平命名空间),
    不会将调用数据、上下文拆分嵌套到多层对象中。

    可修改字段(通过 override 方法修改,以此改变工具执行行为):
        name: 待调用的工具名称。
        args: 工具入参,键值对字典格式。
        headers: 适用于 SSE、HTTP 这类传输协议的 HTTP 请求头。

    上下文只读字段(仅用于路由分发、日志记录,不可修改):
        server_name: 处理本次工具调用的 MCP 服务标识名称。
        runtime: LangGraph 运行时上下文(可选,未在图流程内时为 None)。
    """

    name: str
    args: dict[str, Any]
    server_name: str  # 上下文只读字段:MCP 服务名称
    headers: dict[str, Any] | None = None  # 可修改字段:HTTP 请求头
    runtime: object | None = None  # 上下文只读字段:LangGraph 运行时对象(存在则赋值)

    def override(
        self, **overrides: Unpack[_MCPToolCallRequestOverrides]
    ) -> MCPToolCallRequest:
        """基于当前请求生成新请求实例,替换指定属性。

        返回全新的 `MCPToolCallRequest` 对象,仅替换传入的指定属性。
        采用不可变对象设计模式,原始 request 对象不会被改动。

        参数:
            **overrides: 关键字参数,用于指定需要覆盖修改的属性。
                支持的键名:
                - name:工具名称
                - args:工具入参字典
                - headers:HTTP 请求头

        返回:
            完成属性覆盖后的全新 MCPToolCallRequest 实例

        注意:
            server_name、runtime 这类上下文只读字段无法通过该方法覆盖修改。

        使用示例:
            ```python
            # 修改工具调用参数
            new_request = request.override(args={"value": 10})

            # 更换要调用的工具名称
            new_request = request.override(name="different_tool")
            return replace(self, **overrides)
            """

runtime内部包含我们之前定义的context、运行状态state、工具调用 ID 等全部数据,我们通过runtime.context就能拿到传入的user_id和api_key;

这里要注意代码编辑器的语法提示问题: 部分编辑器无法识别runtime.context的类型,会出现波浪警告,但官方源码明确runtime内置context属性,不影响代码实际运行;

拿到上下文里的user_id后,不能直接修改原始request对象(遵循不可变设计模式),要调用request.override()生成全新的请求对象;

重写参数时,先通过**request.args保留工具原本的全部入参(比如这里的city城市参数),再额外追加user_id字段,把从上下文读取到的用户 ID 赋值进去;

生成新的request后,把新请求传给handler执行工具调用,后置逻辑这里暂时不做处理,最后返回工具执行结果。

我们把这个自定义拦截器命名为inject_user_context,功能注释标注为 "将用户上下文注入 MCP 工具调用"。

写完拦截器后,创建MultiServerMCPClient时,通过tool_interceptors参数把拦截器绑定到客户端,完成全部配置。

【完整代码】

python 复制代码
import asyncio

from langchain.agents import create_agent
from langchain_deepseek import ChatDeepSeek
from langchain_mcp_adapters.client import MultiServerMCPClient
from dataclasses import dataclass
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from watchfiles import awatch

model = ChatDeepSeek(
    model="deepseek-chat",
    temperature=0.0,
)

@dataclass
class Context:
    user_id: str
    api_key: str

async def inject_user_context(
    request: MCPToolCallRequest,
    handler
):
    """将用户凭证注入到 MCP 工具调用中。"""
    # 从上下文中读取用户信息
    runtime = request.runtime
    user_id = runtime.context.user_id
    api_key = runtime.context.api_key

    # 使用 override() 方式创建修改后的请求(遵循不可变模式)
    modify_request = request.override(
        args={**request.args, "user_id": user_id}
    )

    return await handler(modify_request)

client = MultiServerMCPClient(
    {
        "Weather": {
            "transport": "streamable-http",
            "url": "http://localhost:8000/mcp",
        },
    },
    tool_interceptors=[inject_user_context],
)

async def main():
    tools = await client.get_tools()
    # for tool in tools:
    #     print(tool.name)
    agent = create_agent(model=model, tools=tools)
    response = await agent.ainvoke(
        {
            "messages": [{"role": "user", "content": "上海的天气如何?"}]
        },
        context=Context(user_id="111", api_key="sk-xxx"),
    )

    for msg in response.get("messages"):
        msg.pretty_print()

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

再次运行代码发起查询,观察打印的ToolMessage结果,就能看到返回文本里的用户 ID 变成了我们手动传入的111,成功实现上下文自动注入工具参数。

到这里,我们就完整实现了工具拦截器最经典的落地场景: 当 MCP 工具需要依赖 LangGraph 运行上下文的参数时,必须依靠工具拦截器做透传处理

如果不使用拦截器,只有一种替代方案:把用户 ID、密钥这类信息硬写进提示词,让大模型通过语义推断填充参数,但这种方式稳定性差、不适合生产环境;而通过context结构化传递的上下文数据,大模型无法自主解析,只能由开发者通过拦截器手动读取、重写工具请求参数。

总结这个场景的完整流程: 上下文数据仅开发者知晓业务含义,开发者通过拦截器读取runtime运行时上下文,再通过request.override()重写工具调用参数,完成数据自动注入。

讲完注入用户上下文的场景,我们再拓展runtime运行时上下文里其他可读取的属性: request.runtime除了context上下文,还能读取三大核心数据:state运行状态、store持久化存储、tool_call_id工具调用 ID。

第一,读取state运行状态。

**典型业务场景:**校验用户登录状态,未认证则屏蔽敏感 MCP 工具。

python 复制代码
import asyncio

from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware
from langchain_core.messages import ToolMessage
from langchain_deepseek import ChatDeepSeek
from langchain_mcp_adapters.client import MultiServerMCPClient
from dataclasses import dataclass
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.agents import AgentState
from typing import Any

model = ChatDeepSeek(
    model="deepseek-chat",
    temperature=0.0,
)

class State(AgentState):
    authenticated: bool


# 创建中间件,关联状态
class CustomMiddleware(AgentMiddleware):
    state_schema = State
    tools = []  # 可选:为该中间件绑定工具

    def before_model(self, state: State, runtime) -> dict[str, Any] | None:
        # 在模型调用前可以访问和修改 state
        return None

async def authenticate_user(
    request: MCPToolCallRequest,
    handler
):
    """若用户未通过身份验证,则屏蔽敏感 MCP 工具"""
    runtime = request.runtime
    state = runtime.state
    is_authenticated = state.get("authenticated", False)

    if not is_authenticated:
        # 返回错误信息,而非调用哦个工具
        return ToolMessage(
            content="需要进行身份验证,请先登录。",
            tool_call_id=runtime.tool_call_id,
        )
    return await handler(request)

client = MultiServerMCPClient(
    {
        "Weather": {
            "transport": "streamable-http",
            "url": "http://localhost:8000/mcp",
        },
    },
    tool_interceptors=[authenticate_user],
)

async def main():
    tools = await client.get_tools()
    agent = create_agent(model=model, tools=tools, middleware=[CustomMiddleware()])
    response = await agent.ainvoke(
        {
            "messages": [{"role": "user", "content": "上海的天气如何?"}]
        }
    )

    for msg in response.get("messages"):
        msg.pretty_print()

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

拦截器内通过runtime.state.get("authenticated")读取登录状态标识,如果用户未完成身份验证,不需要执行工具调用,直接手动构造ToolMessage返回提示文本 "需要进行身份验证,请先登录";同时从runtime.tool_call_id获取本次工具调用 ID,赋值给消息的tool_call_id字段,保证消息和本次工具调用一一映射,最后直接返回这条提示消息,中断工具执行流程。

python 复制代码
================================ Human Message =================================

上海的天气如何?
================================== Ai Message ==================================

好的,我来查询上海的天气情况。
Tool Calls:
  get_weather (call_00_QvFWLGoJOJTuGRFs59lX0903)
 Call ID: call_00_QvFWLGoJOJTuGRFs59lX0903
  Args:
    city: 上海
    user_id: user
================================= Tool Message =================================

需要进行身份验证,请先登录。
================================== Ai Message ==================================

抱歉,查询天气需要先进行身份验证登录。目前我无法直接获取到上海的天气信息。

建议您可以通过以下方式查看上海天气:
1. **手机天气应用**(如系统自带天气、墨迹天气等)
2. **天气网站**(如中国天气网、weather.com等)
3. **语音助手**(如小爱同学、Siri等)

如果您能提供登录信息或授权,我可以再帮您查询。请问还有其他我可以帮助您的吗?

简单来说,我们可以依靠状态字段,在拦截器内提前拦截非法工具调用,自定义返回结果。

第二,读取store持久化存储。

store是 LangGraph 的长期记忆存储,我们可以从存储中读取用户个性化偏好配置,在拦截器内重写工具参数,实现 MCP 工具调用个性化。 比如读取用户偏好的语言、结果分页条数,在override重写参数时追加langlimit等字段,工具会按照用户偏好执行查询逻辑,全部操作都在拦截器前置环节完成。

【server】

python 复制代码
from fastmcp import FastMCP

mcp = FastMCP("Weather")

@mcp.tool()
async def get_weather(
    city: str,
    user_id: str,
    # 新增可选偏好参数,兼容拦截器自动注入的个性化配置
    lang: str = "zh-CN",
    limit: int = 10,
    timezone: str = "Asia/Shanghai",
    temperature_unit: str = "celsius"
) -> str:
    """
    获取城市天气
    Args:
        city: 需要查询天气的城市名称
        user_id: 当前操作用户ID
        lang: 返回结果语言偏好
        limit: 结果展示条数限制
        timezone: 用户所在时区
        temperature_unit: 温度单位 celsius/fahrenheit
    """
    # 根据用户语言偏好返回对应文案
    if lang == "en-US":
        return f"User[{user_id}] Query: The weather in {city} is sunny! Timezone: {timezone}, Temp Unit: {temperature_unit}, Result limit: {limit}"
    return f"用户[{user_id}]查询: {city} 天气晴朗! 时区:{timezone}, 温度单位:{temperature_unit}, 展示条数限制:{limit}"

if __name__ == "__main__":
    # 与客户端端口、传输协议完全对应
    mcp.run(transport="streamable-http", port=8000)

【client】

python 复制代码
import asyncio
from dataclasses import dataclass
from typing import Any, Optional, Unpack

from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import AgentMiddleware
from langchain_core.messages import ToolMessage
from langchain_deepseek import ChatDeepSeek
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore

model = ChatDeepSeek(
    model="deepseek-chat",
    temperature=0.0,
)


# ============ 状态定义 ============
class State(AgentState):
    authenticated: bool = False
    user_id: Optional[str] = None  # 用户ID,用于查询个性化配置


# ============ 用户偏好数据类 ============
@dataclass
class UserPreferences:
    """用户个性化偏好"""
    language: str = "zh-CN"  # 语言偏好
    limit: int = 10  # 结果分页条数
    timezone: str = "Asia/Shanghai"  # 时区
    temperature_unit: str = "celsius"  # 温度单位


# ============ 从 Store 读取用户偏好(store.get 同步,不可使用 await) ============
async def get_user_preferences_from_store(
        store: BaseStore,
        user_id: str
) -> UserPreferences:
    """
    从 LangGraph Store 读取用户个性化偏好配置
    注意:langgraph store 读写是同步方法,不能加 await
    """
    try:
        namespace = ("user_preferences",)
        key = f"user_{user_id}"
        stored_item = store.get(namespace, key)
        if stored_item and stored_item.value:
            # 外层value才是我们存的包装字典,真实偏好存在里面的"value"键
            wrapper_dict = stored_item.value
            prefs = wrapper_dict.get("value", {})
            return UserPreferences(
                language=prefs.get("language", "zh-CN"),
                limit=prefs.get("limit", 10),
                timezone=prefs.get("timezone", "Asia/Shanghai"),
                temperature_unit=prefs.get("temperature_unit", "celsius"),
            )
    except Exception as e:
        print(f"读取用户偏好失败: {e},使用默认配置")

    return UserPreferences()


# ============ 个性化 MCP 工具拦截器 ============
async def personalize_mcp_tool(
        request: MCPToolCallRequest,
        handler
):
    """
    个性化 MCP 工具调用拦截器
    功能:
    1. 从 LangGraph Store 读取用户偏好
    2. 将偏好参数注入到工具调用中
    3. 实现透明化的个性化体验
    """
    runtime = request.runtime
    state = runtime.state
    user_id = state.get("user_id", "user_12345")
    store = runtime.store
    if store:
        preferences = await get_user_preferences_from_store(store, user_id)
        personalized_args = {
            **request.args,
            # 下面这些都是工具调用的参数
            "user_id": user_id,
            "lang": preferences.language,
            "limit": preferences.limit,
            "timezone": preferences.timezone,
            "temperature_unit": preferences.temperature_unit,
        }
        personalized_request = request.override(args=personalized_args)
        print(f"用户 {user_id} 个性化配置已应用: lang={preferences.language}, limit={preferences.limit}")
        return await handler(personalized_request)

    return await handler(request)


# ============ 身份验证拦截器 ============
async def authenticate_user(
        request: MCPToolCallRequest,
        handler
):
    """若用户未通过身份验证,则屏蔽敏感的 MCP 工具"""
    runtime = request.runtime
    state = runtime.state
    is_authenticated = state.get("authenticated", False)

    if not is_authenticated:
        return ToolMessage(
            content="需要进行身份验证,请先登录。",
            tool_call_id=runtime.tool_call_id,
        )
    return await handler(request)


# ============ 用户中间件(修复before_model入参) ============
class UserMiddleware(AgentMiddleware):
    """用户中间件:在模型调用前初始化用户状态"""
    state_schema = State

    def before_model(self, state: State, runtime, config) -> dict[str, Any] | None:
        # config 为独立第三个入参,不再读取 runtime.config
        configurable = config.get("configurable", {})
        user_id = configurable.get("user_id")
        print(user_id)
        if user_id:
            return {
                "user_id": user_id,
                "authenticated": True
            }
        return None


# ============ MCP 客户端配置 ============
client = MultiServerMCPClient(
    {
        "Weather": {
            "transport": "streamable-http",
            "url": "http://localhost:8000/mcp",
        },
    },
    tool_interceptors=[
        authenticate_user,    # 1. 先校验登录身份
        personalize_mcp_tool,  # 2. 注入用户个性化参数
    ]
)


# ============ 保存用户偏好工具(修复:store.put 同步,移除 await) ============
async def save_user_preferences(
        store: BaseStore,
        user_id: str,
        preferences: dict
) -> bool:
    """保存用户偏好到 Store"""
    try:
        namespace = ("user_preferences",)
        key = f"user_{user_id}"
        # 同步方法,删除 await
        store.put(
            namespace,
            key,
            {
                "value": preferences,
                "updated_at": "2026-01-01T00:00:00Z"
            }
        )
        return True
    except Exception as e:
        print(f"保存用户偏好失败: {e}")
        return False


# ============ 写入测试用户偏好 ============
async def example_update_preferences(store: InMemoryStore):
    user_prefs = {
        "language": "en-US",
        "limit": 20,
        "timezone": "America/New_York",
        "temperature_unit": "fahrenheit"
    }
    await save_user_preferences(store, "user_12345", user_prefs)
    print("用户偏好已更新完成")


# ============ 主执行函数 ============
async def main():
    # 全局唯一内存存储实例
    mem_store = InMemoryStore()
    # 写入测试用户配置,传入有效store,无None报错
    await example_update_preferences(mem_store)

    # 加载MCP全部工具
    tools = await client.get_tools()

    # 创建Agent
    agent = create_agent(
        model=model,
        tools=tools,
        middleware=[UserMiddleware()],
        store=mem_store,
    )

    # 用户ID通过configurable传入,不会污染输入messages
    response = await agent.ainvoke(
        {
            "messages": [{"role": "user", "content": "上海的天气如何?"}]
        },
        config={
            "configurable": {
                "user_id": "user_12345"
            }
        }
    )

    # 打印完整对话消息
    for msg in response.get("messages"):
        msg.pretty_print()


if __name__ == "__main__":
    # 仅执行main,删除独立调用example_update_preferences的代码,消除NoneType报错
    asyncio.run(main())
python 复制代码
用户偏好已更新完成
user_12345
用户 user_12345 个性化配置已应用: lang=en-US, limit=20
user_12345
================================ Human Message =================================

上海的天气如何?
================================== Ai Message ==================================

好的,我来查询上海的天气信息。
Tool Calls:
  get_weather (call_00_YANDvig3eXJNO0zNFAUT8068)
 Call ID: call_00_YANDvig3eXJNO0zNFAUT8068
  Args:
    city: 上海
    user_id: user
================================= Tool Message =================================
Name: get_weather

[{'type': 'text', 'text': 'User[user_12345] Query: The weather in 上海 is sunny! Timezone: America/New_York, Temp Unit: fahrenheit, Result limit: 20', 'id': 'lc_a3976391-18de-4aac-b2df-4238e13b6a47'}]
================================== Ai Message ==================================

上海的天气情况如下:

🌤️ **天气状况:晴天**

不过目前返回的信息中没有显示具体的温度数值。如果您需要更详细的天气信息(如温度、湿度、风力等),请告诉我,我可以进一步为您查询!

第三,读取tool_call_id工具调用 ID。

除了上面身份校验场景用来映射返回消息,还能实现接口限流、调用次数统,可以用来限制昂贵的 MCP 工具调用次数。

python 复制代码
# 拦截器
async def interceptor(request: MCPToolCallRequest, handler):
    """限制昂贵的 MCP 工具调用的次数。"""
    runtime = request.runtime
    tool_call_id = runtime.tool_call_id
    # 检查速率限制(简化示例)
    if is_rate_limited(request.name):
        return ToolMessage(
            content="速率限制已超。请稍后再试。",
            tool_call_id=tool_call_id,
        )
    result = await handler(request)
    # 工具调用成功记录
    log_tool_execution(tool_call_id, request.name, success=True)
    return result

拦截器内先获取工具名称,匹配限流规则,如果当前工具调用超出频率限制,直接构造ToolMessage提示 "速率限制已超,请稍后再试";工具调用正常执行完毕后,用tool_call_id记录本次调用日志,留存调用记录方便后续排查。

以上三类场景核心逻辑一致: 读取runtime里的运行时数据,在工具执行前做自定义业务判断,按需重写请求参数、或者直接拦截工具调用、自定义返回消息。

可以获取的元素 使用场景举例
state 若用户未通过身份验证,则屏蔽敏感的 MCP 工具
store 使用 store 的偏好设置来个性化 MCP 工具的调用操作。
Tool call ID 限制昂贵的 MCP 工具调用的次数。

到这里,我们就讲完了工具拦截器的第一个核心能力:访问 LangGraph 运行时上下文 ,依靠runtime读取contextstatestoretool_call_id等数据,自定义改造工具调用的完整链路。

核心能力 2:状态更新与流程控制 (Commands)

接下来我们继续学习工具拦截器的第二个核心能力:状态更新与流程控制

先给大家拆解这句话的含义: 上面我们写的拦截器,所有操作都属于前置处理 ------ 也就是在工具真正执行之前,读取context上下文、state运行状态这类信息。

但拦截器不止能做前置逻辑,还能在工具执行完成后做后置操作,下面讲解后置逻辑对应的两大能力。

我们先做知识回顾: 之前讲过拦截器的设计思路和中间件高度相似,参数结构、返回规则都是统一的。之前讲 MCP 工具调用MCPToolCallRequest时提到过,拦截器的返回值只有两种合法选择,二选一:第一种是返回ToolMessage工具消息,第二种是返回Command命令对象。拦截器最终返回什么对象,直接决定了后置环节的执行逻辑。

第一种返回类型ToolMessage我们上面已经实操过,它的作用是自定义工具返回的内容、拦截原有工具执行流程;第二种全新的返回类型Command命令,它包含两大核心功能:更新 Agent 运行状态、控制图执行流程跳转。

Command 对象功能 1:更新 Agent 运行状态

先讲Command里的update参数,它的作用是更新 LangGraph 图里的运行状态。上面我们已经知道,在拦截器的request.runtime里可以读取当前state状态;既然能读取状态,自然也支持主动修改、更新状态,这个修改操作就必须依靠Command对象的update字段完成。

这里有一个前置前提:想要更新状态,当前创建 Agent 时必须绑定了状态结构。分两种情况:

  • 默认状态: 框架内置的AgentState,无需自定义,开箱即用;

  • 自定义状态: 需要我们提前用dataclassTypedDict自己定义一套 State 数据结构,创建 Agent 时通过参数绑定,才能在拦截器中修改自定义字段。

举个实操场景: 工具执行完毕后,我们想给状态里的消息列表追加一条记录、或者修改任务进度字段,就可以在Commandupdate中传入键值对,框架会自动覆盖 / 追加到全局 State 里。

【server】

python 复制代码
from fastmcp import FastMCP
mcp = FastMCP("OrderServer")

@mcp.tool()
async def submit_order(goods_id: str, user_id: str) -> str:
    """提交订单"""
    return f"订单提交成功!用户:{user_id}, 商品:{goods_id}"

if __name__ == "__main__":
    mcp.run(transport="stdio")

【client】

python 复制代码
import asyncio
from typing import Any, Optional

from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import AgentMiddleware
from langchain.messages import ToolMessage
from langchain_deepseek import ChatDeepSeek
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langgraph.types import Command
from langgraph.store.memory import InMemoryStore

model = ChatDeepSeek(
    model="deepseek-chat",
    temperature=0.0,
)

# ====================== 1. 自定义图状态 ======================
class CustomState(AgentState):
    authenticated: bool = False
    task_status: str = "pending"  # pending / completed / failed

# ====================== 2. 修复后的Command拦截器 ======================
async def handle_task_update_interceptor(
    request: MCPToolCallRequest,
    handler
) -> ToolMessage | Command:
    """
    自动填充user_id到工具参数
    下单完成仅更新task_status,不手动修改messages,避免消息上下文断裂报错
    """
    tool_response = await handler(request)

    if request.name == "submit_order":
        return Command(
            # 更新状态
            update={
                "messages": [ToolMessage(content=tool_response.content[-1].text, tool_call_id=request.runtime.tool_call_id)],
                "task_status": "completed",
            },
        )
    return tool_response

# ====================== 用户中间件 ======================
class UserMiddleware(AgentMiddleware):
    state_schema = CustomState

# ====================== MCP客户端 ======================
client = MultiServerMCPClient(
    {
        "OrderServer": {
            "transport": "stdio",
            "command": "python",
            "args": ["E:/pythonPlace/WorkSpace/LangChain V1/MCP/状态更新【server】.py"]
        }
    },
    tool_interceptors=[
        handle_task_update_interceptor,
    ]
)

# ====================== 主函数 ======================
async def main():
    tools = await client.get_tools()
    agent = create_agent(
        model=model,
        tools=tools,
        middleware=[UserMiddleware()],
        store=InMemoryStore(),
        state_schema=CustomState
    )

    # prompt携带user_id,保证模型触发submit_order工具
    response = await agent.ainvoke(
        {
            "messages": [{"role": "user", "content": "我的用户id是user_666,帮我提交订单,商品ID: 10086"}],
        },
    )

    print("===== 更新后的全局运行状态 =====")
    final_state = response
    print(f"任务状态 task_status: {final_state.get('task_status')}")
    print(f"消息列表长度: {len(final_state.get('messages', []))}")
    for msg in final_state.get("messages"):
        msg.pretty_print()

if __name__ == "__main__":
    asyncio.run(main())
python 复制代码
===== 更新后的全局运行状态 =====
任务状态 task_status: completed
消息列表长度: 4
================================ Human Message =================================

我的用户id是user_666,帮我提交订单,商品ID: 10086
================================== Ai Message ==================================

好的,我来帮您提交订单。
Tool Calls:
  submit_order (call_00_sfmtqkdqeVAvtmo9gTMM4118)
 Call ID: call_00_sfmtqkdqeVAvtmo9gTMM4118
  Args:
    goods_id: 10086
    user_id: user_666
================================= Tool Message =================================
Name: submit_order

订单提交成功!用户:user_666, 商品:10086
================================== Ai Message ==================================

订单已成功提交!以下是订单信息:

- **用户ID**:user_666
- **商品ID**:10086
- **状态**:✅ 提交成功

请问还有其他需要帮忙的吗?

那我们可以直接改状态吗?

绝对不能直接修改 request.runtime.state["xxx"],强制要求用 Command 更新状态,分三层底层原理讲清楚

底层设计 :LangGraph 的 State 是不可变 (Immutable) 数据结构

LangGraph 图运行时的 runtime.state 并不是普通字典,而是冻结、只读的快照对象:

  1. 每一轮图执行节点(包括拦截器、Agent、工具调用)拿到的 state 都是当前执行快照副本
  2. 你写 request.runtime.state["task_status"] = "completed" 试图原地赋值,要么直接抛「对象不可修改」异常,要么修改仅作用于本地临时副本,不会同步到全局图状态
  3. 所有对图全局状态的变更,必须产出一份「状态变更描述」交给图调度器,由调度器统一合并、生成全新的下一轮状态快照,这就是 Command(update={}) 的作用。

简单类比: runtime.state = 只读试卷,只能看不能涂写; Command(update={...}) = 答题卡,你填写要修改的字段,交给系统统一更新整张试卷。

**拦截器执行时机特殊:**直接改 runtime.state 生命周期失效

MCP 工具拦截器的执行时序:

拦截器触发 → 执行 await handler(request) 调用 MCP 工具;

拦截器 return 数据,才会把变更提交给 LangGraph 调度器;

如果你在拦截器中间写 runtime.state["task_status"] = "xxx"

  • 只是修改了拦截器函数内的局部临时 state 副本;

  • 函数执行完毕后,这个局部修改直接销毁,主图流程完全感知不到;

  • 下一个节点、下一轮工具调用读取 runtime.state 依旧是旧值。

Command拦截器的返回值载体 : 框架会识别拦截器返回 Command 对象,提取里面 update 字典,调度器统一合并进全局状态,永久生效到下一轮图执行。

图流程调度器需要感知状态变更,实现链路联动

LangGraph 不止单纯存数据,还依靠状态做:

  1. 分支跳转、多 Agent 切换Command(goto="xxx"));

  2. 节点条件判断(比如 task_status == completed 就跳转到总结节点);

  3. 状态持久化、日志记录、中间件回调;

如果你原地偷偷修改 runtime.state

  • 调度器无法捕获本次状态变更,不会触发分支判断、不会记录变更日志;

  • 无法配合 goto 实现流程跳转,状态更新和图执行流程割裂;

只有通过 Command 返回变更,调度器才能一次性拿到「要改哪些字段 + 跳转到哪个节点」完整指令,统一调度整张图的后续走向。

方式 1:直接赋值(无效 / 报错)

python 复制代码
async def bad_interceptor(request: MCPToolCallRequest, handler):
    runtime = request.runtime
    # 错误写法:试图原地修改state
    runtime.state["task_status"] = "completed"
    result = await handler(request)
    # 直接返回工具结果,无Command
    return result

现象:

  • 要么抛出 TypeError: cannot assign to State field

  • 无报错时,后续节点读取 runtime.state["task_status"] 依旧是 pending,修改完全丢失。

方式 2:Command 标准写法(全局永久生效)

python 复制代码
async def good_interceptor(request: MCPToolCallRequest, handler):
    runtime = request.runtime
    result = await handler(request)
    if request.name == "submit_order":
        # 通过Command告知调度器需要更新的字段
        return Command(
            update={"task_status": "completed", "messages": [result]}
            # 还可以搭配goto控制图跳转,原地修改做不到
            # goto="summary_agent"
        )
    return result

现象: 下一轮任意节点、拦截器读取 runtime.state["task_status"] 都是 completed,变更全局生效,同时支持流程跳转。


补充:那中间件 before_model 为什么可以直接返回字典改状态?

你之前写的 UserMiddleware.before_model 返回 {"user_id":"xxx","authenticated":True} 看起来像直接修改,本质逻辑和 Command 完全同源:

  • 中间件的返回字典 = 简化版 Command.update;

  • 底层框架会自动把返回字典封装成内部变更指令,交给调度器合并状态;

  • 拦截器不支持直接返回字典,MCP 工具拦截器规范强制只能返回:ToolMessage / 原始工具结果 / Command 对象,因此只能靠 Command 传递状态变更。


Command 对象功能 2:goto 流程跳转控制

Command第二个核心属性是goto,专门用来控制 LangGraph 图的执行流向,实现节点跳转,分两种使用场景:

场景 1:goto="end" 提前终止整个 Agent 流程

官方文档标注,给goto赋值特殊值"__end__",就能直接跳转到 LangGraph 的终止节点,整个图的执行流程直接结束,不再继续走后续逻辑,和我们之前学 LangGraph 时设置终止节点的原理完全一致。

我这里给大家实测踩坑结果: 我按照官方示例编写代码测试这个能力时,直接返回Command(goto="__end__")会直接报错。

python 复制代码
openai.BadRequestError: Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message)", 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_request_error'}}
During task with name 'model' and id '4a72354b-27c5-2de4-173b-bbf4ef710377'

报错信息提示: 缺少匹配的ToolMessage工具消息。【其实我们注意上面我们也是加上了对应的ToolMessage + tool_call_id,否则一样的报错】

我们来拆解报错根源: 当前 Graph 的消息队列里,已经存在用户输入的HumanMessage、负责决策调用工具的AIMessage,流程走到拦截器时,原本预期会执行 MCP 工具、生成一条对应的ToolMessage存入消息列表。但我们直接用goto跳到结束节点,跳过了工具执行,消息列表里缺少和本次工具调用匹配的ToolMessage,状态结构不完整,框架校验失败抛出异常。

解决方案:更新状态补充 ToolMessage

想要消除报错,必须在Commandupdate参数里手动补一条ToolMessage写入消息列表,补齐状态数据:

  1. request.runtime.tool_call_id获取本次工具调用唯一 ID;

  2. update中给messages消息列表追加一条手动构造的ToolMessage

  3. ToolMessage必须绑定刚才拿到的tool_call_id,和之前 AIMessage 里的工具调用 ID 一一对应,完成消息匹配。

python 复制代码
return Command(
    # 更新状态
    update={
        "messages": [ToolMessage(content=tool_response.content[-1].text, tool_call_id=request.runtime.tool_call_id)],
        "task_status": "completed",
    },
    goto="__end__"
)

补齐消息后代码不会再报错,状态更新功能可以正常生效。但我们继续运行验证goto="__end__"终止能力,发现流程并不会直接结束: 代码执行顺序依旧是:HumanMessageAIMessage → 我们手动补充的ToolMessage → 大模型再次发起新一轮工具决策,重新进入拦截器逻辑,循环往复,完全没有触发终止。【打印结果跟上面的 Command 对象功能 1:更新 Agent 运行状态打印结果一样】

结合实测结论给大家说明: 截至我写这篇博客当前,goto="__end__"提前结束流程的官方写法存在框架 Bug,无法正常生效。或许后面大家可以自行拉取最新代码测试,如果新版本修复了 Bug,这个写法就能正常使用;即便目前无法生效,我们也要掌握官方标准用法,作为知识点记下来。

场景 2:goto 跳转到其他子 Agent(多智能体场景)

除了终止流程,goto还支持跳转到 Graph 里其他自定义节点,最典型的落地场景就是多 Agent 协同系统

这块内容需要等我们后面完整学习多智能体章节才能彻底吃透,这里先简单铺垫概念: 复杂业务场景不会只靠单个 Agent 完成全部任务,通常会搭建一套多智能体架构:

  • **1 个主路由 Agent:**负责接收用户问题,判断任务类型,做路由分发;

  • **多个子任务 Agent(A/B/C):**分别处理不同细分业务。

在这套架构里,我们就可以在工具拦截器返回Command,通过goto="子Agent节点名"手动控制流程跳转到指定子 Agent,实现任务路由分发。这个属于高阶拓展能力,大家先记住拦截器支持跨 Agent 跳转这个特性即可。

python 复制代码
if request.name == "submit_order":
    return Command(
        update={
            # 将工具结果添加到消息列表,并更新自定义状态字段
            "messages": [result] if isinstance(result, ToolMessage) else [],
            "task_status": "completed",
        },
        goto="summary_agent", # 指定跳转的目标节点
    )

总结: 拦截器可以返回 Command 对象,用于更新 Agent State 或控制 LangGraph 图的执行流向。这在跟踪任务进度、在 Agent 间切换或提前结束执行时非常有用。

工具拦截器第二个核心能力「状态更新与流程控制」整体分为两块:

1. 状态更新: 依靠Command.update修改全局 State,实测功能稳定可用;

2. 流程控制: 依靠Command.goto实现节点跳转,分两种用法

  • **goto="__end__":**官方推荐提前终止流程,但当前版本存在 Bug 无法生效;

  • **goto="其他节点名称":**多智能体架构下,跳转到其他子 Agent 完成任务分发。

到这里,工具拦截器的两大核心能力就全部讲解完毕: 第一是访问运行时上下文(前置处理),第二是通过返回Command实现状态更新、流程跳转(后置流程控制)。

编写自定义拦截器:模式与最佳实践

接下来我们一起学习,手动编写 MCP 工具拦截器时的各类通用最佳实践,下面内容都是知识点总结,我们逐条梳理理解即可。

基本拦截器模式

首先明确拦截器固定入参:所有拦截器统一接收两个参数,分别是requesthandler

  • request 本次 MCP 工具调用的请求对象,类型为MCPToolCallRequest,存储本次工具调用全部请求信息;

  • **handler:**回调函数,调用它才会真正发起 MCP 工具请求。

基于这个基础结构,第一种通用实践:全链路日志打印

我们可以在调用handler执行工具之前打印前置日志,记录工具名称、入参;在handler执行完成拿到返回结果后打印后置日志,记录工具返回内容。依靠日志完整记录工具调用全流程,方便后续调试、排查问题。

所以我们可以在调用 handler 前后执行逻辑,或者完全跳过它。

python 复制代码
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest

async def logging_interceptor(
    request: MCPToolCallRequest,
    handler,
):
    """在执行前后记录工具调用日志。"""
    print(f"[calling tool] {request.name} with args: {request.args}")
    result = await handler(request)
    print(f"[finished tool] {request.name}: {result}")
    return result

# 将拦截器列表传递给客户端
client = MultiServerMCPClient(
    {
        "math": {"transport": "stdio", "command": "python", "args": ["./math_tool_server.py"]},
    },
    tool_interceptors=[logging_interceptor],
)
修改请求参数

第二种核心实践: 修改工具调用的请求内容,统一使用request.override()方法,可修改两类内容:

  • 工具本身的业务入参(函数参数);

  • 远程 HTTP 类型 MCP 服务的 HTTP 请求头。

这里有一条硬性注意事项:严格遵循不可变设计模式,禁止直接修改原始request对象

不能直接写request.args["xxx"] = xxx这种代码修改原始请求,原始对象只读 ;必须调用override()生成全新的请求实例,在新实例上覆盖参数、请求头,再把新请求传给handler执行。

python 复制代码
async def double_args_interceptor(
    request: MCPToolCallRequest,
    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)

# 原始调用:add(a=2, b=3) → 实际执行:add(a=4, b=6)
根据运行时的具体内容修改 HTTP 请求头

很多线上 MCP 服务会做接口鉴权,需要在 HTTP 请求头携带 Token、密钥等凭证,我们可以用拦截器统一处理鉴权头,实现全量工具统一权限管控,分两种业务场景给大家区分:

MCP 工具自身自带鉴权逻辑: MCP 服务内部会校验请求头里的 Token,校验通过才执行工具逻辑。这种场景下,我们需要在拦截器通过override自动追加鉴权请求头,给每一次工具调用带上合法凭证,完成服务端鉴权校验。

**MCP 工具本身无鉴权逻辑,但业务要求所有工具调用必须受保护:**即便工具本身不校验请求头,业务侧要求 Agent 侧统一管控访问权限,也可以依靠拦截器实现全局权限拦截:

  • 在拦截器内从request.runtime读取运行时statecontext上下文,取出当前用户身份、权限标识;

  • 增加权限判断逻辑,如果用户无对应工具访问权限,直接返回ToolMessage告知用户无权限,不执行handler、不发起工具调用;

  • 如果校验通过,再正常调用handler执行工具。

这种方案的优势: 不用改造每一个 MCP 服务,在 Agent 客户端一层统一做权限拦截,全局管控所有工具的访问权限。如果工具本身自带鉴权、同时拦截器又追加鉴权头,会形成双重校验,安全性更高。

python 复制代码
async def auth_header_interceptor(
    request: MCPToolCallRequest,
    handler,
):
    """根据被调用的工具名称动态添加相应的 header。"""
    token = get_token_for_tool(request.name) # 假设这是获取令牌的自定义函数
    modified_request = request.override(
        headers={"Authorization": f"Bearer {token}"}
    )
    return await handler(modified_request)
错误处理与重试机制

**第三种生产级实践:**异常捕获、自动重试、错误降级。 调用远程 MCP 工具时很容易出现网络超时、连接失败、服务报错等异常,拦截器可以统一处理异常逻辑:

**重试机制:**捕获工具调用抛出的异常,设置最大重试次数、重试间隔;循环发起调用,调用成功则直接返回结果;如果达到最大重试次数依旧报错,再进入降级逻辑。

**错误降级兜底:**区分不同异常类型做差异化返回:超时异常、连接异常分别返回对应的友好提示文本,把报错信息封装成正常的返回内容,不会直接抛出崩溃中断 Agent 流程。即便工具调用失败,Agent 依旧能正常向下执行,只是返回异常说明给大模型,避免整个智能体任务直接中断。

使用拦截器捕获工具执行时的异常,并实现健壮的重试逻辑。

python 复制代码
import asyncio

async def retry_interceptor(
    request: MCPToolCallRequest,
    handler,
    max_retries: int = 3,
    delay: float = 1.0,
):
    """重试失败的工具调用,采用指数退避策略。"""
    last_exception = None
    for attempt in range(max_retries):
        try:
            return await handler(request)
        except Exception as e:
            last_exception = e
            print(f"工具 {request.name} 调用失败 (尝试 {attempt+1}/{max_retries}): {e}")
            await asyncio.sleep(delay * (2 ** attempt))
    raise last_exception
错误降级处理

捕获特定异常后,可以不抛出错误,而是返回一个兜底值,保证流程继续。

python 复制代码
async def fallback_interceptor(
    request: MCPToolCallRequest,
    handler,
):
    """工具执行失败时返回一个兜底响应。"""
    try:
        return await handler(request)
    except TimeoutError:
        return f"工具 {request.name} 超时。请稍后再试。"
    except ConnectionError:
        return f"无法连接到 {request.name} 服务。使用缓存数据。"
组合多个拦截器:洋葱模型

第四种实践: 多个拦截器同时配置时,执行顺序遵循洋葱模型(和我们之前学的多层中间件执行规则完全一致)。

举个例子: 客户端配置tool_interceptors=[outer_interceptor, inner_interceptor],外层拦截器在前,内层拦截器在后:

  • **执行前置逻辑顺序:**外层拦截器前置逻辑 → 内层拦截器前置逻辑;

  • 执行核心逻辑: 调用handler执行真实 MCP 工具;

  • **执行后置逻辑顺序:**内层拦截器后置逻辑 → 外层拦截器后置逻辑。

简单流程数字示意:outer_before → inner_before → 工具执行 handler → inner_after → outer_after。 大家配置多个拦截器时,一定要按照业务优先级排序,需要最先执行、最后收尾的逻辑放在列表最外层。

python 复制代码
async def outer_interceptor(request, handler):
    print("outer: before")
    result = await handler(request)
    print("outer: after")
    return result

async def inner_interceptor(request, handler):
    print("inner: before")
    result = await handler(request)
    print("inner: after")
    return result

# 客户端配置
client = MultiServerMCPClient(
    {...},
    tool_interceptors=[outer_interceptor, inner_interceptor],
)

# 执行顺序:outer:before → inner:before → 实际工具执行 → inner:after → outer:after

总结

最后我们梳理三大核心能力,把全部知识点汇总:

访问运行时上下文: 通过request.runtime读取contextstatestoretool_call_id等运行时数据,实现用户上下文注入、身份校验、限流等前置业务逻辑;

状态更新与流程控制: 拦截器可返回Command对象,通过update更新 Agent 全局状态,通过goto控制流程图跳转,支持提前终止任务、多 Agent 路由分发;

自定义拦截器标准化编写规范: 统一入参request, handler;使用override修改请求;增加日志、鉴权、重试、降级等通用逻辑;多拦截器遵循洋葱模型排序执行。

核心功能 关键点与原文描述
访问运行时上下文 通过 request.runtime 访问 stateconfigstorecontexttool_call_id;原文案例:向工具注入用户上下文
状态更新与流程控制 返回 Command 对象更新 Agent 状态;goto 跳转节点;goto="__end__" 提前终止执行;原文案例:任务完成跳转、成功提前结束
编写自定义拦截器 基本结构:async def func(request, handler): ... 修改请求:使用 request.override() 错误处理:捕获异常、重试、兜底降级 组合执行:洋葱模型按列表顺序嵌套执行

到这里,MCP 工具拦截器全部核心知识点、实操案例、生产最佳实践就全部讲解完毕,大家可以结合不同业务场景,套用上面的规范和思路开发拦截器。