12.4 中间件:拦截整条流水线 本节摘要:本节讲横切关注点的解决方案——中间件(Middleware)。日志、鉴权、限流、指标这些需求不该散落在每个处理器里。中间件让你在请求/通知流水线上叠加拦截层,可任意组合。本节讲透中间件的链式执行模型、它与第 11 章 Bearer 中间件的关系、以及如何写一个自定义中间件。读完本节,你能把横切关注点从业务代码里彻底分离。 一、为什么需要中间件 先看一个「没有中间件」的痛点。假设每个工具都要记日志、查权限、限流: 每个工具都重复写日志、鉴权、限流——代码重复、难维护、易漏。中间件解决这个——把这些逻辑抽成可叠加的层,统一应用。
本节摘要:本节讲横切关注点的解决方案——中间件(Middleware)。日志、鉴权、限流、指标这些需求不该散落在每个处理器里。中间件让你在请求/通知流水线上叠加拦截层,可任意组合。本节讲透中间件的链式执行模型、它与第 11 章 Bearer 中间件的关系、以及如何写一个自定义中间件。读完本节,你能把横切关注点从业务代码里彻底分离。
先看一个「没有中间件」的痛点。假设每个工具都要记日志、查权限、限流:
# 反模式:每个工具重复写横切逻辑 @mcp.tool() async def op1(ctx): log("开始 op1") # 重复 if not check_auth(ctx): # 重复 raise PermissionError() if rate_limited(ctx): # 重复 raise RateLimitError() return do_op1() @mcp.tool() async def op2(ctx): log("开始 op2") # 又重复 if not check_auth(ctx): # 又重复 ...
每个工具都重复写日志、鉴权、限流——代码重复、难维护、易漏。中间件解决这个——把这些逻辑抽成可叠加的层,统一应用。
中间件构成一条「链」,请求依次穿过每一层:
请求到达 │ ▼ ┌─────────────┐ │ 日志中间件 │ ← 记录每个请求 └─────────────┘ │ ▼ ┌─────────────┐ │ 鉴权中间件 │ ← 校验令牌 └─────────────┘ │ ▼ ┌─────────────┐ │ 限流中间件 │ ← 检查频率 └─────────────┘ │ ▼ ┌─────────────┐ │ 业务处理器 │ ← 实际工具 └─────────────┘ │ ▼ 返回响应,反向穿过各层 (鉴权记录、日志记录完成时间等)
每层中间件可以:
中间件用 ServerMiddleware 定义:
from mcp.server import MCPServer from mcp.shared.middleware import ServerMiddleware, MiddlewareContext class LoggingMiddleware(ServerMiddleware): """记录每个请求的中间件。""" async def on_request(self, ctx: MiddlewareContext, next): """前置:记录开始;调用 next 往下传;后置:记录耗时。""" start = time.time() method = ctx.method print(f"[开始] {method}") try: result = await next(ctx) # 调用下一层(或业务) elapsed = time.time() - start print(f"[完成] {method} ({elapsed:.3f}s)") return result except Exception as e: elapsed = time.time() - start print(f"[失败] {method} ({elapsed:.3f}s): {e}") raise # 挂到服务端 mcp = MCPServer("Demo", middleware=[LoggingMiddleware()])
关键点:
next(ctx):调用下一层。不调就不往下传(如鉴权失败时)next 之前的代码(如记开始时间)next 之后的代码(如记耗时)try/except 包住 next,能捕获业务异常中间件适合各种「横切关注点」:
| 中间件 | 干什么 | 典型逻辑 |
|---|---|---|
| 日志 | 记录请求/响应 | 记开始/完成/耗时 |
| 鉴权 | 校验权限 | 查 token,失败返 401 |
| 限流 | 控制频率 | 超频返 429 |
| 指标 | 收集指标 | 计数/直方图 |
| 缓存 | 缓存响应 | 命中直接返回,不调 next |
| 追踪 | 分布式追踪 | 创建 span,记录 |
第 11 章的 Bearer 中间件就是鉴权中间件的具体实现——它拦截请求、校验 Bearer 令牌、失败返 401。理解了通用中间件模型,你就理解了 Bearer 中间件的工作原理。
多个中间件可任意组合,按顺序执行:
mcp = MCPServer( "Demo", middleware=[ LoggingMiddleware(), # 第 1 层:日志(最外层) AuthMiddleware(), # 第 2 层:鉴权 RateLimitMiddleware(), # 第 3 层:限流 MetricsMiddleware(), # 第 4 层:指标 ] )
执行顺序:请求从左到右穿过(日志→鉴权→限流→指标→业务),响应反向。顺序很重要:
顺序建议(从外到内): 日志(最外,记录所有) → 鉴权(失败的不该进限流) → 限流(过鉴权但超频的挡掉) → 指标(统计最终到业务的) → 业务
💡 技巧:鉴权中间件要在限流之前。否则未授权的请求也会消耗限流配额,导致恶意请求打满限流、合法用户被挡。这个顺序细节体现了中间件设计的考量。
大多数中间件都调 next 往下传。但缓存中间件是个特例——命中时直接返回,不调 next:
class CacheMiddleware(ServerMiddleware): async def on_request(self, ctx, next): key = make_cache_key(ctx) cached = self.cache.get(key) if cached is not None: return cached # 命中,直接返回(不调 next) result = await next(ctx) # 未命中,往下传 self.cache.set(key, result) # 存缓存 return result
这个「短路」能力让中间件非常强大——它不只是「观察」,还能「决定」是否继续。缓存、鉴权(失败时短路)、限流(超限时短路)都用这个能力。
第 12.3 节的扩展与本节的中间件,分工不同:
| 维度 | 扩展 | 中间件 |
|---|---|---|
| 干什么 | 定义新方法 | 拦截现有方法 |
| 增加协议能力 | 是(新方法) | 否(只处理现有) |
| 改变请求行为 | 否 | 是(可拦截/短路) |
| 例子 | batch/call_tools |
日志/鉴权/限流 |
简单说:扩展「加新功能」,中间件「管现有功能」。两者互补,常配合用——扩展定义新方法,中间件统一拦截(包括拦截扩展方法)。
ServerMiddleware 子类 + on_request(ctx, next),调 next 往下传。next(ctx) 是关键:不调就短路(如鉴权失败)。中间件清楚了,最后一节讲 Apps 与 OpenTelemetry。