6.3 Context 与 Resolve 的分工 本节摘要:前两节分别讲了 Context 和 Resolve,本节把它们放一起,用对照表与场景判断讲透分工。两者都解决「让工具拿到请求环境信息」,但定位不同:Context 是 SDK 提供的通用请求对象(方法固定、覆盖读资源/报进度/写日志等通用操作),Resolve 是你自定义的业务依赖注入(完全灵活、覆盖当前用户/数据库会话等业务依赖)。读完本节,你在任何场景都能果断判断「该用 Context、Resolve、还是两者结合」,不再混淆。
本节摘要:前两节分别讲了 Context 和 Resolve,本节把它们放一起,用对照表与场景判断讲透分工。两者都解决「让工具拿到请求环境信息」,但定位不同:Context 是 SDK 提供的通用请求对象(方法固定、覆盖读资源/报进度/写日志等通用操作),Resolve 是你自定义的业务依赖注入(完全灵活、覆盖当前用户/数据库会话等业务依赖)。读完本节,你在任何场景都能果断判断「该用 Context、Resolve、还是两者结合」,不再混淆。
先把两者的核心定位钉死:
| 维度 | Context | Resolve |
|---|---|---|
| 本质 | SDK 提供的请求对象 | 你写的解析函数 |
| 内容 | 固定方法集(读资源、报进度等) | 你定义的任意依赖 |
| 类比 | 工具箱(里面的工具 SDK 定好了) | 工厂(生产你想要的东西) |
| 覆盖范围 | 通用请求操作 | 业务特定依赖 |
一句话总结:Context 是「SDK 给你的现成工具箱」,Resolve 是「你自己开的工厂」。
把两者的方方面面放一起对照:
| 维度 | Context | Resolve |
|---|---|---|
| 注入方式 | ctx: Context 类型注解 |
Annotated[T, Resolve(fn)] |
| 来源 | SDK 内置 | 你写的解析函数 |
| 注入时机 | 工具执行时 | 工具执行前 |
| 一个参数 vs 多个 | 一个对象(ctx),含多个方法 |
多个参数,每个一个依赖 |
| 能力 | 固定(read_resource、report_progress 等) | 完全自由(你写函数) |
| 可见性 | 对模型不可见 | 对模型不可见 |
| 适合 | 通用请求操作 | 业务依赖 |
| 复用性 | 全局统一对象 | 每个依赖一个解析函数 |
两者都有一个关键共性:对模型不可见。模型从不知道 Context 或 Resolve 参数的存在,它们都是 SDK 在工具执行前后填充的。
把常见场景列出来,标注该用哪个:
| 场景 | 用什么 | 理由 |
|---|---|---|
| 在工具里读别的资源 | Context | ctx.read_resource 是现成方法 |
| 上报长任务进度 | Context | ctx.report_progress 是现成方法 |
| 写日志通知客户端 | Context | ctx.info 等是现成方法 |
| 拿当前用户 | Resolve | 业务依赖,需自定义解析 |
| 拿数据库会话 | Resolve | 业务依赖,需自定义解析 |
| 拿配置对象 | Resolve 或 Context | 可用 ctx.read_resource,也可 Resolve 封装 |
| 读请求头做认证 | Resolve | 把认证封装成依赖,工具不重复 |
| 访问启动时建立的连接池 | Resolve 或 Context | Resolve 注入连接池,或 ctx.lifespan_context |
| 触发引导填写 | Resolve 返回 Elicit | 现代协议的推荐方式 |
判断口诀:
问:这个需求是「通用请求操作」还是「业务特定依赖」? 通用请求操作 → Context(用现成方法) 业务特定依赖 → Resolve(写解析函数) 两者都有 → 结合用(函数同时有 ctx 和 Resolve 参数)
实战中最常见的是两者结合——一个工具同时用 Context(通用操作)和 Resolve(业务依赖):
from typing import Annotated from mcp.server import MCPServer from mcp.server.mcpserver import Context from mcp.shared import resolve mcp = MCPServer("Shop") # 业务依赖:当前用户 async def get_current_user(ctx: Context) -> User: token = ctx.headers["authorization"] return verify_token(token) # 业务依赖:数据库会话 async def get_db(ctx: Context) -> Session: return open_session() @mcp.tool() async def search_my_orders( query: str, # 业务参数(模型可见) ctx: Context, # 通用操作 user: Annotated[User, Resolve(get_current_user)], # 业务依赖 db: Annotated[Session, Resolve(get_db)], # 业务依赖 ) -> dict: """Search current user's orders.""" # 用 Context 的通用能力 config = await ctx.read_resource("config://app") await ctx.report_progress(1, 2, "正在查询") # 用 Resolve 注入的业务依赖 orders = db.search_orders(user.id, query) await ctx.report_progress(2, 2, "完成") return {"orders": orders[:config["max_results"]]}
模型看到的 Schema 只有 query 一个参数:
{ "name": "search_my_orders", "inputSchema": { "properties": {"query": {"type": "string"}}, "required": ["query"] } }
模型只关心业务参数,所有实现细节(Context 操作、Resolve 依赖)都被隐藏。这是「关注点分离」做到极致的样子。
当工具同时用 Context 和 Resolve 时,执行顺序很重要:
1. 请求到达,SDK 构造 Context 对象 2. SDK 运行所有 Resolve 解析函数(并行或按依赖顺序) - get_current_user(ctx) → User - get_db(ctx) → Session 3. 把解析结果填进工具的 Resolve 参数 4. 调用工具函数,传入 ctx + 业务参数 + Resolve 注入值 5. 工具执行,返回结果
关键点:Resolve 解析函数能访问 Context——它们的签名是 async def fn(ctx: Context) -> T。所以 Resolve 依赖可以基于 Context 的信息(如 ctx.headers)来解析依赖。这就是「Resolve 构建在 Context 之上」的关系。
💡 技巧:理解了「Resolve 解析函数能用 ctx」,你就能写出强大的依赖链——比如「从 ctx.headers 拿 token,验证后返回 User」,这整个逻辑封装在解析函数里,工具只享受结果。
最后澄清几个常见误区:
| 误区 | 真相 |
|---|---|
| 「Context 和 Resolve 二选一」 | 错,两者可结合,各管一摊 |
| 「Resolve 替代 Context」 | 错,Resolve 不能读资源、报进度,这些是 Context 的固定方法 |
| 「业务依赖也用 Context 装」 | 可以但别扭,Context 没有扩展点装业务对象,Resolve 才是正路 |
| 「Resolve 参数模型能看到」 | 错,Resolve 参数对模型不可见,与 Context 一样 |
| 「解析函数不能异步」 | 错,解析函数可以是 async,且通常是(要读 IO) |
分工清楚了,下一节讲生命周期上下文——服务端启动时建立的共享资源。