6.1 Context 对象:请求级能力的统一入口 本节摘要:前面几章的工具/资源/提示词都是「被调用时执行一段函数」,但真实服务端很少这么简单——它需要在执行中读别的资源、上报进度、写日志、拿到请求头。本节讲透第一个核心机制:Context(上下文)对象。它按类型注解自动注入,提供 / / 等请求级能力。关键特性是「对模型不可见」——Context 是给你的代码用的,模型从不知道它的存在。读完本节,你能让工具从「纯函数」升级为「与请求环境交互的完整业务逻辑」。 一、为什么需要 Context 先看一个纯函数工具搞不定的场景: 纯函数工具只能基于「参数 + 全局状态」工作,拿不到「这次请求的环境信息」。
本节摘要:前面几章的工具/资源/提示词都是「被调用时执行一段函数」,但真实服务端很少这么简单——它需要在执行中读别的资源、上报进度、写日志、拿到请求头。本节讲透第一个核心机制:Context(上下文)对象。它按类型注解自动注入,提供
read_resource/report_progress/info等请求级能力。关键特性是「对模型不可见」——Context 是给你的代码用的,模型从不知道它的存在。读完本节,你能让工具从「纯函数」升级为「与请求环境交互的完整业务逻辑」。
先看一个纯函数工具搞不定的场景:
@mcp.tool() def process_order(order_id: str) -> dict: """Process an order.""" # 问题 1:我想读 config://app 资源,怎么读? config = ??? # 纯函数拿不到别的资源 # 问题 2:这个操作很慢,我想上报进度,怎么报? ??? # 纯函数没有上报通道 # 问题 3:我想记日志,写到哪? ??? # 纯函数没有日志出口 # 问题 4:我想知道是谁发起的请求(请求头),怎么拿? user = ??? # 纯函数拿不到请求头
纯函数工具只能基于「参数 + 全局状态」工作,拿不到「这次请求的环境信息」。Context 就是补这个缺口的——它把请求级的能力(读资源、报进度、写日志、拿请求头)打包成一个对象,按类型注解注入你的函数。
Context 的注入极其简洁——在函数签名上加一个 ctx: Context 参数:
from mcp.server import MCPServer from mcp.server.mcpserver import Context mcp = MCPServer("Demo") @mcp.tool() async def process_order(order_id: str, ctx: Context) -> dict: """Process an order.""" ...
SDK 看到 ctx: Context 这个类型注解,自动把当前请求的 Context 对象注入。你不需要手动构造、不需要从全局拿——它就在那里。
💡 技巧:Context 注入是「按类型注解」的——SDK 扫描函数签名,发现
ctx: Context,就注入。这与第 6.2 节的Resolve依赖解析用的是同一套「按注解填充参数」的机制。参数名不一定是ctx,但类型必须是Context。
注入的 Context 对象提供这些请求级能力:
| 方法/属性 | 作用 | 典型用法 |
|---|---|---|
read_resource(uri) |
读别的资源 | 在工具里读配置、读关联数据 |
report_progress(progress, total) |
上报进度 | 长任务告诉客户端「我做到哪了」 |
info(msg) / debug(msg) / 等 |
写日志 | 日志被 SDK 转成通知发给客户端 |
session |
访问底层会话 | 高级操作(采样、订阅等,第 7 章) |
headers |
读请求头 | 拿认证信息、客户端标识 |
request_id |
当前请求 ID | 日志追踪、链路追踪 |
lifespan_context |
生命周期上下文 | 访问启动时建立的共享资源(第 6.4 节) |
逐个看几个最常用的。
ctx.read_resource 让你在工具执行中,读取服务端自己注册的资源:
@mcp.resource("config://app") def get_config() -> dict: return {"max_items": 100, "timeout": 30} @mcp.tool() async def search(query: str, ctx: Context) -> list: """Search items, respecting config.""" # 在工具里读配置资源 config = await ctx.read_resource("config://app") max_items = config["max_items"] # 用配置限制搜索结果 results = do_search(query) return results[:max_items]
这个模式很有用——配置做成资源(应用可预加载),工具执行时再读它。工具不必把配置硬编码,而是从资源动态读取,配置变化时只需更新资源,工具自动适配。
ctx.report_progress 让长任务告诉客户端「我做到哪了」:
@mcp.tool() async def batch_process(items: list[str], ctx: Context) -> list: """Process a batch of items.""" results = [] total = len(items) for i, item in enumerate(items): result = process_one(item) results.append(result) # 上报进度 await ctx.report_progress( progress=i + 1, total=total, message=f"已处理 {i + 1}/{total}" ) return results
客户端(宿主)收到进度通知后,可在 UI 显示进度条。这对长任务体验至关重要——用户不用盯着「还在转圈」,知道「做到 60% 了」。
Context 提供标准的日志方法,日志被 SDK 转成通知发给客户端:
@mcp.tool() async def risky_op(ctx: Context) -> str: """A risky operation.""" await ctx.info("开始执行 risky_op") try: result = do_something() await ctx.debug(f"成功,结果:{result}") return result except Exception as e: await ctx.info(f"失败:{e}") # 日志通知客户端 raise
这些日志不是写到服务端的文件,而是作为通知发给客户端——客户端能看到服务端在干什么。这与传统日志(写文件)不同,更适合分布式场景。
⚠️ 注意:Context 的日志方法是「发通知给客户端」,不是写本地文件。如果你要写本地日志(运维需要),仍用 Python 标准
logging模块。两者不冲突,各司其职。
Context 最容易被忽视的特性——它对模型不可见。模型看到的工具 Schema 里,没有 ctx 参数:
你写的函数: async def process_order(order_id: str, ctx: Context) -> dict 模型看到的 Schema: { "name": "process_order", "inputSchema": { "properties": {"order_id": {"type": "string"}}, "required": ["order_id"] } } ← 注意:没有 ctx!
SDK 在生成 Schema 时,自动忽略 ctx: Context 这种类型注解的参数。模型从不构造 ctx,也不知道它存在——ctx 是 SDK 在调用前自动填充的,纯给你的代码用。
这个设计很巧妙:业务代码拿到请求级能力,但模型不被这些细节干扰。模型只需关心业务参数(order_id),实现细节(读资源、报进度)由你的代码用 Context 完成。
ctx: Context,SDK 按类型注解自动注入。read_resource、report_progress、info/debug、session、headers、request_id、lifespan_context。read_resource 让工具读别的资源,实现「配置做资源、工具动态读」的解耦。report_progress 让长任务上报进度,客户端 UI 可显示进度条。logging 模块各司其职。Context 清楚了,下一节讲第二个核心机制——Resolve 依赖解析,对模型隐藏的参数注入。