ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MCP Python SDK 服务端 lifespan 全解:从连接池管理到生命周期验证实战

2026/9/21 7:26:50 拓冰建站 浏览量
MCP Python SDK 服务端 lifespan 全解:从连接池管理到生命周期验证实战 MCP Python SDK 服务端 lifespan 全解从连接池管理到生命周期验证实战【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk导读本篇文章基于官方 Python SDK for Model Context Protocol仓库 pythonsd/python-sdk的文档展开系统讲解服务端lifespan生命周期机制的完整用法。真实 MCP 服务器几乎都需要在存活期间持有某个常驻资源——数据库连接池、HTTP 客户端、加载好的模型——你不希望每次请求都重新创建又希望在服务器退出时干净地关闭。lifespan 正是为此设计的官方机制。读完本文你将掌握如何用asynccontextmanager编写类型化 lifespan、如何让yield出的对象被所有 handler 共享、类型参数Context[AppContext]的威力与使用边界以及如何通过一个最小实验亲眼验证启动先于首请求、结束于 finally的生命周期时序。为什么需要 lifespan服务器级别的常驻资源绝大多数真实服务器都会持有某些存活期与服务器本身一致的资源数据库连接池、HTTP 客户端、加载到内存中的模型等。如果每次调用都新建性能与连接数都不可接受如果从不关闭又会泄漏连接与句柄。lifespan 解决的就是这个创建一次、干净关闭的问题。lifespan 的本质是一个asynccontextmanager异步上下文管理器它接收服务器实例yield出一个对象这个对象在服务器运行的整个期间对所有 handler 可见。yield之前的代码是启动逻辑yield之后的代码通常放在finally中是关闭逻辑。如果你写过 FastAPI 的lifespan这里的知识是相通的同一个装饰器、同一个yield、同一个finally。类型化 lifespan完整接线示例以下是最小但完整的示例完整源码见 docs_src/lifespan/tutorial001.py建议自下而上阅读from collections.abc import AsyncIterator from contextlib import asynccontextmanager from dataclasses import dataclass from mcp.server import MCPServer from mcp.server.mcpserver import Context class Database: classmethod async def connect(cls) - Database: return cls() async def disconnect(self) - None: ... def query(self) - int: return 3 dataclass class AppContext: db: Database asynccontextmanager async def app_lifespan(server: MCPServer) - AsyncIterator[AppContext]: db await Database.connect() try: yield AppContext(dbdb) finally: await db.disconnect() mcp MCPServer(Bookshop, lifespanapp_lifespan) mcp.tool() def count_books(genre: str, ctx: Context[AppContext]) - str: Count the books in a genre. db ctx.request_context.lifespan_context.db return f{db.query()} books in {genre!r}.逐层拆解这段代码app_lifespan是启动与关闭的全部yield之前连接Databaseyield之后在finally中断开连接。异步上下文管理器保证无论期间发生什么finally都会执行关闭逻辑绝不遗漏。AppContext是一个普通 dataclass它只是承载你设置好的一堆东西的容器。今天放一个字段db明天可以扩展成十个字段——工具函数依然只需要通过ctx.request_context.lifespan_context一处入口访问。MCPServer(Bookshop, lifespanapp_lifespan)就是全部接线工作把 lifespan 作为构造参数传入即可SDK 负责在正确时机进入和退出。工具内部通过ctx.request_context.lifespan_context拿到 yield 出的对象ctx是 SDK 注入的Context参数不参与工具的输入 schema。生命周期时序一次执行全程共享lifespan恰好执行一次服务器启动时在第一个请求之前进入服务器停止时退出。期间的所有请求共享同一个AppContext实例——这正是连接池/客户端/模型只建一次语义的来源。从源码可以印证这一点。在底层服务器实现中Server.run用async with self.lifespan(self) as lifespan_context:包裹整个消息循环见 src/mcp/server/lowlevel/server.py也就是说 lifespan 上下文以with方式包住整个连接生命周期yield 出的对象随后通过lifespan_state传递给每个请求src/mcp/server/lowlevel/server.py。如果你不传lifespanSDK 会使用默认实现——一个什么都不做、直接yield {}的异步上下文管理器见 src/mcp/server/lowlevel/server.py。这解释了文档中的关键保证lifespan 永远存在ctx.request_context.lifespan_context至少是{}绝不会是None。这也是为什么裸Context会把lifespan_context类型标注为dict[str, Any]。模型视角ctx 是 SDK 注入的不进 schema对调用方LLM来说lifespan 是完全透明的。ctx是一个Context 参数由 SDK 在调用时注入绝不会出现在工具的输入 schema 里。以count_books为例模型能看到的输入 schema 只有genre一个字段{ type: object, properties: { genre: {title: Genre, type: string} }, required: [genre], title: count_booksArguments }模型唯一能传的参数是genre。lifespan 是你的服务器内部事务与协议无关。这一点在 SDK 实现中同样成立Context.request_context属性在无活动请求时会直接抛出ValueError(Context is not available outside of a request)见 src/mcp/server/mcpserver/context.py而每个请求的request_context都携带lifespan_context字段定义见 src/mcp/server/context.py。mcp.resource()与mcp.prompt()函数同样可以接收ctx参数但它们应按下一节的原因写成不带类型参数的裸Context。ctx携带的全部内容可进一步查阅文档 Context。它真的是类型安全的Context[AppContext] 的威力再看一次注解ctx: Context[AppContext]。正是这一个类型参数让类型检查器如 mypy / pyright确信ctx.request_context.lifespan_context就是AppContext类型。于是.db能自动补全而敲出.dbb会在服务器运行之前就成为类型错误——IDE 里直接标红。反过来如果写成不带类型参数的裸Contextlifespan_context的类型就是dict[str, Any]类型检查器无法得知你的 lifespan yield 了什么。对象在运行时依然存在但你失去了编译期的全部帮助。从源码看这一设计的根基在于Context的泛型声明与LifespanContextT类型变量ServerRequestContext的lifespan_context字段是泛型的src/mcp/server/context.pyContext类自身也声明为Generic[LifespanT_co]且协变src/mcp/server/context.py因此Context[AppContext]可以安全地向下兼容为Context[object]等更宽类型。重要警告Context[AppContext] 是工具专用写法警告Context[AppContext]只适用于工具mcp.tool()函数。如果把它写到mcp.resource()或mcp.prompt()函数上该 handler 的每次调用都会失败。客户端会收到错误服务器日志中会显示原因Context is not available outside of a request在资源与提示词中请写成裸ctx: Context。你的 lifespan yield 出的对象在运行时仍然位于ctx.request_context.lifespan_context中——你放弃的只是类型参数不是对象本身。产生这一限制的原因与实现细节一致Context.request_context属性在请求上下文尚未建立时如资源/提示词 handler 的某些调用路径会抛出上述ValueError见 src/mcp/server/mcpserver/context.py。提示lifespan 永远存在lifespan永远存在。即使你不传lifespanSDK 的默认 lifespan 也会 yield 一个空dict因此ctx.request_context.lifespan_context是{}绝不会是None。裸Context将其类型标为dict[str, Any]正是因为这个默认值。你的代码可以放心地直接访问lifespan_context而无需判空——当然若你依赖自定义对象仍需通过类型参数来恢复精确类型。亲眼验证启动先于首请求关闭落于 finally启动代码在第一个请求之前运行这类论断不该靠直觉接受值得亲手验证。做法是把服务器精简到只剩生命周期本身给Database加一个connected布尔标志在connect()与disconnect()中翻转该标志添加一个报告该标志状态的工具。完整示例见 docs_src/lifespan/tutorial002.pyfrom collections.abc import AsyncIterator from contextlib import asynccontextmanager from dataclasses import dataclass from mcp.server import MCPServer from mcp.server.mcpserver import Context class Database: def __init__(self) - None: self.connected False async def connect(self) - None: self.connected True async def disconnect(self) - None: self.connected False dataclass class AppContext: db: Database database Database() asynccontextmanager async def app_lifespan(server: MCPServer) - AsyncIterator[AppContext]: await database.connect() try: yield AppContext(dbdatabase) finally: await database.disconnect() mcp MCPServer(Bookshop, lifespanapp_lifespan) mcp.tool() def database_status(ctx: Context[AppContext]) - str: Report whether the database connection is up. db ctx.request_context.lifespan_context.db return connected if db.connected else disconnected注意database放在模块级别唯一的原因是从服务器外部观察它——你可以在自己的测试或调试代码里直接读取database.connected而不需要穿过 MCP 协议。三个时间点三个值按文档的验证清单在三个时刻观察时刻database.connected说明服务器启动前False导入模块不会连接任何东西连接只发生在 lifespan 的yield之前服务器运行中True调用database_status返回connected启动代码已在首个请求之前执行完毕服务器停止后Falsefinally块运行disconnect()被调用结论很清晰工作恰好发生在你放置它的位置——yield的周围。既不在模块导入时也不在每个请求时。这正印证了 lifespan 与每次请求都初始化或导入时初始化两种模式的本质区别。总结lifespan 核心要点lifespan参数接收一个asynccontextmanager它接收服务器实例并yield出一个对象。yield之前的代码是启动其后的finally是关闭。它在服务器的整个生命周期内只执行一次而非每个请求一次。你 yield 出的对象在所有工具、资源与提示词中都可以通过ctx.request_context.lifespan_context访问。ctx: Context[AppContext]让工具中的该访问获得完整类型。资源与提示词请使用裸ContextContext[AppContext]写在资源/提示词上会导致每次调用失败。不传lifespan时默认值是一个空dict绝不会是None。接下来可以继续阅读在调用中途停下来向用户询问只有用户才知道的信息的 handler属于Elicitation询问机制参见文档 Elicitation。【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考