MCP Python SDK 中间件(Middleware)实战:用 `async (ctx, call_next)` 观测、拦截与改写每条入站消息
MCP Python SDK 中间件Middleware实战用async (ctx, call_next)观测、拦截与改写每条入站消息【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk本文围绕 MCP Python SDKModel Context Protocol 官方 Python SDK的 Server 中间件机制展开讲解如何用一条形如async (ctx, call_next)的异步函数包裹服务器接收到的每一条消息从而完成耗时统计、日志观测、按调用方拒绝请求、改写参数乃至直接应答等横切能力。读完本文你将掌握低层Server与高层MCPServer两套 API 上注册中间件的方法、中间件链的执行顺序、initialize握手消息的特殊性与陷阱以及 SDK 默认附带的 OpenTelemetry 中间件的内部实现。中间件是什么一条异步函数包裹每条入站消息在 MCP Python SDK 中中间件middleware就是一条异步函数它包裹服务器接收到的每一条消息。它的签名固定为async def middleware(ctx: ServerRequestContext, call_next: CallNext) - HandlerResult: ...你把它追加到server.middleware列表即可生效——这就是中间件的全部 API。这里的ctx与你的处理器handler收到的是同一个ServerRequestContext对象ctx.method是原始方法名字符串ctx.params是尚未经过任何校验的原始参数字典。call_next(ctx)则负责执行链条的剩余部分参数校验、处理器查找、最终调用你的 handler并把结果原样返回。一个关键的设计原则是中间件列表在源码中被明确标记为“临时provisional”。在 src/mcp/server/lowlevel/server.py 中可以看到对应的 TODO 注释其签名与语义可能随 2.x 次版本发布中的 Context/middleware 重构而改变。因此官方建议用它来观测计时、日志、追踪和拒绝消息而不要把它当作服务器赖以运转的地基。两套 APIMCPServer 构造参数与低层 server.middleware中间件列表在 SDK 的两层服务器 API 上以两种方式暴露低层Server在构造后直接通过server.middleware.append(...)追加这也是下文计时示例采用的方式如果你还不熟悉Server(name, on_call_tool...)这种低层写法建议先阅读 低层 Server 指南高层MCPServer在构造时通过MCPServer(name, middleware[...])传入并通过mcp.middleware属性暴露同一份列表。从源码可以确认MCPServer.middleware属性与低层Server.middleware是同一份列表——MCPServer内部维护着一个_lowlevel_server实例其middleware属性直接透传底层列表见 src/mcp/server/mcpserver/server.py。值得注意的还有高层 API 中用户中间件的插入位置。在 src/mcp/server/mcpserver/server.py 中可以看到SDK 内置的中间件OpenTelemetry 追踪、请求状态边界RequestStateBoundary先被追加进列表用户的中间件再按传入顺序追加其后形成“最外层是 SDK 内置件、用户中间件在其内部”的嵌套结构。计时中间件实战一个完整可运行的示例下面是一个“一个服务器 一个工具 一个中间件”的完整示例它记录每条消息的处理耗时。该示例源码位于 docs_src/middleware/tutorial001.pyimport logging import time from mcp.server import Server, ServerRequestContext from mcp.server.context import CallNext, HandlerResult from mcp.types import ( CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool, ) logger logging.getLogger(__name__) async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) - ListToolsResult: return ListToolsResult( tools[ Tool( namesearch_books, descriptionSearch the catalog by title or author., input_schema{ type: object, properties: {query: {type: string}}, required: [query], }, ) ] ) async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) - CallToolResult: query (params.arguments or {})[query] return CallToolResult(content[TextContent(typetext, textfFound 3 books matching {query!r}.)]) async def log_timing(ctx: ServerRequestContext, call_next: CallNext) - HandlerResult: start time.perf_counter() try: return await call_next(ctx) finally: elapsed_ms (time.perf_counter() - start) * 1000 logger.info(%s took %.1f ms, ctx.method, elapsed_ms) server Server(Bookshop, on_list_toolson_list_tools, on_call_toolon_call_tool) server.middleware.append(log_timing)逐行拆解这个示例可以提炼出四条核心语义ctx即ServerRequestContext它由ServerRunner为每条入站消息构造携带连接级ServerSession用于服务器向客户端发起请求与通知、协议版本、原始方法名与原始参数等元数据定义见 src/mcp/server/context.py。call_next(ctx)执行链条剩余部分校验 → 处理器查找 → 你的 handler返回值原样透传响应不被改动。try/finally是刻意为之即便 handler 抛出异常耗时依然会被记录——因为失败会以call_next抛出的异常形式到达你的中间件。server.middleware.append(...)完成注册列表从外向内执行因此middleware[0]是离“线路”wire最近的那一个。运行验证两次调用为何产生三条日志连接一个客户端依次列出工具、调用一个工具你的日志中会出现三行server/discover took 18.3 ms tools/list took 0.1 ms tools/call took 0.1 ms你明明只发起了两次调用却得到三行日志。第一行server/discover是客户端在建立连接阶段自动发送的请求——发生在你提出任何业务请求之前。这正是中间件的意义所在它包裹每一条到达服务器的入站消息包括连接建立流程server/discover在传统会话legacy session上则对应initialize与notifications/initialized每条请求与通知对于通知ctx.request_id is Nonecall_next(ctx)返回None你返回的任何内容都会被丢弃。一个补充细节是在2026-07-28版本的 Streamable HTTP 路径上客户端的通知 POST 会在传输层直接收到202应答而不会被分发因此也到不了中间件——该协议修订版没有定义任何 HTTP 上的客户端到服务器通知服务器没有处理器的未知方法call_next会抛出MCPError(-32601, Method not found)该异常穿过你的中间件一路传回客户端。中间件内可以做什么观察、拒绝、改写、应答按“需要多谨慎”递增的顺序中间件内可以执行四类操作观察Observe计时、计数、打日志——即上文示例所示。这是中间件最主要的用途风险最低。拒绝Refuse不调用call_next(ctx)而是直接抛出一个MCPError那么这一条消息会收到一个 JSON-RPC 错误应答而连接保持存活下一条消息照常通过。这是服务器按调用方控制subscriptions/listen访问权限的惯用手段具体做法可参考 Abonnements订阅页面中的“决定谁可以监听”一节。改写Rewritectx是一个 dataclass因此可以用dataclasses.replace构造一个改写过参数的新上下文await call_next(dataclasses.replace(ctx, params...))这样链条后续部分收到的参数就与客户端实际发送的不同。从 src/mcp/server/runner.py 的实现看_compose_server_middleware是在调用时读取ctx上的method/params的所以中间件对ctx的改写会即时作用于后续链条——这也是这一能力能够成立的原因。但绝不要对initialize做改写客户端拿到的返回结果虽然是由你改写后的参数构建的但服务器握手阶段提交的连接状态却来自线路上的原始参数最终可能导致通信双方对协商结果各执一词。应答Answer不调用call_next(ctx)直接返回一个结果它会作为你的应答发给客户端。此时call_next会交给你“最终在线上传输的形态”而管道不会再改动你返回的内容因此整个信封都由你负责在 2026 时代的连接上这包括_meta中的serverInfo时间戳——SDK 会给 handler 的结果附加该字段但不会替你附加。initialize 的特殊性唯一的钩子与死锁陷阱initialize握手消息同样被中间件包裹而中间件是你能钩住它的唯一入口。如果你试图用add_request_handler接管它SDK 会直接拒绝ValueError: initialize is handled by the server runner and cannot be overridden; use Server.middleware to observe or wrap initialization这条报错的来源是 src/mcp/server/lowlevel/server.py 中的显式守卫add_request_handler对initialize方法直接抛出ValueError因为握手流程归 runner 所有。此外还有一个必须牢记的死锁陷阱initialize是内联处理的——在你的中间件链返回之前服务器不会读取任何其他入站消息。因此如果在处理initialize期间等待一个服务器到客户端的请求ctx.session.send_request(...)例如一次 elicitation 引导会死锁整个连接你等待的响应永远不可能被读取。而“发出即忘”的通知fire-and-forget则没有问题。默认随 SDK 附带的中间件OpenTelemetrySDK 恰好内置了一个中间件并且默认已经在你的服务器中间件列表里即每处理一条消息就发射一个 OpenTelemetry span 的那个。你不需要手动追加它大多数时候甚至感觉不到它的存在——在安装导出器exporter之前它完全是无操作no-op。其实现位于 src/mcp/server/_otel.py类OpenTelemetryMiddleware同样遵循ServerMiddleware协议在call_next的调用点包裹 span 的开启与关闭。在 src/mcp/server/lowlevel/server.py 可以看到它的默认注册方式self.middleware: list[...] [OpenTelemetryMiddleware()]。如果你想关闭它把它从列表中移除即可。完整的配置与导出方式见 OpenTelemetry 指南。与 ASGI/Starlette 中间件的对比如果你写过 ASGI 中间件会对这个形态感到熟悉Starlette 的(scope, receive, send)在这里变成了(ctx, call_next)并且它运行在传输层之后作用于解码后的 MCP 消息而非原始 HTTP 请求。两者可以叠加组合挂在streamable_http_app()上的 Starlette 中间件看到的是 HTTP而这里的中间件看到的是 MCP。在高层MCPServer的源码中也能看到这种分层的痕迹——Starlette 层的认证中间件AuthenticationMiddleware、BearerAuthBackend等用于 HTTP 认证而ctx层的中间件链用于 MCP 消息处理。源码级原理runner 如何组合中间件链从 src/mcp/server/runner.py 的实现可以看到链条的组装方式_compose_server_middleware将server.middleware列表反转遍历把每个中间件通过partial(_apply_middleware, middleware, call)包裹到前一个之上从而保证列表从外向内执行middleware[0]最靠近线路。_on_request与_on_notify共享这条链路因此请求与通知走的是同一条中间件链。另外中间件的异常处理同样被纳入考量链条内部的异常会以call_next抛出的形式让每个外层中间件依次观测到且只在整条链成功返回后才提交连接状态——这意味着中间件“否决”一条消息不会留下任何状态残留。总结中间件的形态是async (ctx, call_next) - result低层通过server.middleware.append(...)注册高层通过MCPServer(middleware[...])传入或追加到mcp.middleware它包裹每一条到达服务器的入站消息server/discover、initialize、普通请求、通知、未知方法并从外向内执行ctx.request_id is None是区分通知与请求的标志不调用call_next而是抛异常可以拒绝单条消息且连接不受影响SDK 自带的 OpenTelemetry 追踪本身就是一个中间件且默认已在列表中参考 OpenTelemetry整套中间件接口目前仍是临时 APIprovisional请用它来观测而不要在其上构建长期依赖。至此我们覆盖了所有“包裹在请求外围”的机制至于一条请求是否有权执行则由 授权Authorization 机制来决定。【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

无人机NOMA通信系统优化与MATLAB实现

无人机NOMA通信系统优化与MATLAB实现

1. 项目背景与核心价值无人机辅助的非正交多址(NOMA)蜂窝卸载是当前无线通信领域的前沿研究方向。传统蜂窝网络在热点区域容易出现过载现象,而无人机作为移动基站具有部署灵活、视距传输概率高等优势。当无人机搭载NOMA技术时,能在…

2026/9/20 8:27:42 阅读更多 →
如何实现拼多多批量抓取采集自动化?幽灵穿甲无视遮挡,隔着弹窗直接操作

如何实现拼多多批量抓取采集自动化?幽灵穿甲无视遮挡,隔着弹窗直接操作

如何实现拼多多批量抓取采集自动化?幽灵穿甲无视遮挡,隔着弹窗直接操作 电商自动化圈子里流传一句话:拼多多的批量抓取采集,是店群运营中最耗人力也最容易出错的环节。 采集竞品数据是店群运营的命脉。但各大平台的反爬系统越来越…

2026/9/20 8:26:42 阅读更多 →
如何实现拼多多批量抓取采集自动化?代码级稳定性保障,7x24跑不停不断

如何实现拼多多批量抓取采集自动化?代码级稳定性保障,7x24跑不停不断

如何实现拼多多批量抓取采集自动化?代码级稳定性保障,7x24跑不停不断 电商这行,谁的速度快谁吃肉。拼多多的批量抓取采集,是店群运营中最耗人力也最容易出错的环节。 采集竞品数据是店群运营的命脉。但各大平台的反爬系统越来越强…

2026/9/21 10:56:50 阅读更多 →

最新新闻

STM32软件SPI驱动1.8寸TFT-LCD完整教程

STM32软件SPI驱动1.8寸TFT-LCD完整教程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 10:22:15 阅读更多 →
PCIe 5.0交换芯片如何破解AI集群GPU互联瓶颈

PCIe 5.0交换芯片如何破解AI集群GPU互联瓶颈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 10:22:15 阅读更多 →
2026跨部门协同研发管理系统选型指南:避开踩坑实战解析

2026跨部门协同研发管理系统选型指南:避开踩坑实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 10:22:14 阅读更多 →
外贸建站用什么平台好?新手入门避坑指南

外贸建站用什么平台好?新手入门避坑指南

外贸建站用什么平台好?新手入门避坑指南 网站做好了没人访问,这是90%外贸新手最崩溃的时刻。你花了几万块定制开发,页面精美得像杂志,但打开百度或谷歌搜产品,根本找不到你。别慌,这通常不是内容的问题,而是 技术选型 从一开始就错了。…

2026/9/21 9:45:18 阅读更多 →
一个服务器上有两个网站要备案两次吗?源码下载避坑指南

一个服务器上有两个网站要备案两次吗?源码下载避坑指南

一个服务器上有两个网站要备案两次吗?源码下载避坑指南 别再死磕那些丑得令人发指的模板网站了,真的,看着都尴尬。很多新手为了省事,直接去搜“源码下载”,结果装出来的页面配色像上世纪的网吧,布局挤得像早高峰的地铁,客户一眼就能看穿你的不专业。更头疼的是,当你终于搞定两个网站,准备绑上服务器时,卡在了备案…

2026/9/21 9:30:07 阅读更多 →
个人博客网页设计论文选题怎么选,3个维度避开域名服务器坑

个人博客网页设计论文选题怎么选,3个维度避开域名服务器坑

个人博客网页设计论文选题怎么选,3个维度避开域名服务器坑 域名解析报错 502,服务器内存爆满,这种“代码写得好,上线就抓瞎”的尴尬,是不是你写个人博客网页设计论文时的真实写照?很多同学在选题和实操阶段,死磕 CSS 动画或 JS 交互,却对最底层的域名绑定和服务器配置一知半解。…

2026/9/21 9:16:31 阅读更多 →

日新闻

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程 【免费下载链接】agentic-awesome-skills AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and …

2026/9/21 0:00:01 阅读更多 →
gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin拥有AI辅助的基础开发平台,企业级业务AI开发解决方案,内置mcp辅助服务,内置skills管理,…

2026/9/21 0:00:01 阅读更多 →
Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

桌面应用AI 应用插件系统 【免费下载链接】Wox A cross-platform launcher that simply works 项目地址: https://gitcode.com/gh_mirrors/wo/Wox 点击查看 免费下载 全功能插件(Full-featured Plugin)是 Wox 三类插件实现方式中能力最完整的…

2026/9/21 0:00:01 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/21 3:13:20 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/21 2:19:36 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/21 4:51:05 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/19 23:01:36 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/19 17:50:38 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →