7.5 进度上报与日志:单向通知 本节摘要:第 7 章的收尾节。前面讲的引导填写、采样都是「需要回答」的交互(服务端问,客户端/用户答)。本节讲另一类——「不需要回答的单向通知」。核心是两个:进度上报( ,告诉客户端「我做到哪了」)与日志(告诉客户端「我在干什么」)。它们与引导填写形成对照:引导填写是双向的(问+答),进度与日志是单向的(只通知,不等回答)。读完本节,你能让长任务有进度反馈、让服务端行为可观测。
本节摘要:第 7 章的收尾节。前面讲的引导填写、采样都是「需要回答」的交互(服务端问,客户端/用户答)。本节讲另一类——「不需要回答的单向通知」。核心是两个:进度上报(
ctx.report_progress,告诉客户端「我做到哪了」)与日志(告诉客户端「我在干什么」)。它们与引导填写形成对照:引导填写是双向的(问+答),进度与日志是单向的(只通知,不等回答)。读完本节,你能让长任务有进度反馈、让服务端行为可观测。
先把第 7 章的交互分两类:
| 类型 | 例子 | 特征 |
|---|---|---|
| 双向交互 | 引导填写、采样、根 | 服务端问,客户端/用户答,服务端继续 |
| 单向通知 | 进度上报、日志 | 服务端通知,不等回答,继续执行 |
双向交互的特点是「暂停等回答」——服务端发出问题后,要等答案才能继续。单向通知则「说完就继续」——服务端发出通知后,不等任何回答,直接往下执行。
这个区分很重要,因为它决定了你该用哪种机制:需要用户输入才能继续→引导填写;只是告知状态→进度/日志。
进度上报让长任务告诉客户端「我做到哪了」,客户端可在 UI 显示进度条:
from mcp.server.mcpserver import Context @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): results.append(process_one(item)) # 上报进度(说完就继续,不等回答) await ctx.report_progress( progress=i + 1, total=total, message=f"已处理 {i + 1}/{total}:{item}" ) return results
ctx.report_progress 接受:
progress:当前进度数(如 5)total:总数(如 10)message:可选的人类可读描述客户端收到后,可显示「5/10,已处理 xxx」。这对长任务体验至关重要——用户不用盯着转圈,知道做到哪了。
进度上报的关键特性:不等回答。report_progress 调用后立即返回,工具继续执行下一项。这与引导填写的「暂停等回答」完全不同。
日志让服务端告诉客户端「我在干什么」,客户端可显示或记录:
@mcp.tool() async def risky_op(ctx: Context) -> str: """A risky operation.""" await ctx.info("开始执行 risky_op") # 信息级 try: await ctx.debug("正在尝试方案 A") # 调试级 result = try_plan_a() await ctx.info(f"方案 A 成功:{result}") return result except Exception as e: await ctx.warning(f"方案 A 失败:{e},转方案 B") # 警告级 result = try_plan_b() await ctx.error("方案 A 彻底失败,用了备用方案") # 错误级 return result
Context 提供标准日志方法,按级别:
| 方法 | 级别 | 何时用 |
|---|---|---|
ctx.debug(msg) |
DEBUG | 详细诊断,默认不显示 |
ctx.info(msg) |
INFO | 一般信息,正常流程 |
ctx.warning(msg) |
WARNING | 警告,非致命问题 |
ctx.error(msg) |
ERROR | 错误,但仍继续 |
这些日志作为通知发给客户端(不是写本地文件),客户端可显示给用户或记录。
这里要区分两种「日志」,它们不冲突,各司其职:
| 日志类型 | 机制 | 接收方 | 用途 |
|---|---|---|---|
| Context 日志 | ctx.info 等,发通知 |
客户端(宿主 UI) | 让用户/客户端看到服务端在干什么 |
| 本地 logging | Python logging 模块 |
服务端文件/ stdout | 运维诊断、长期归档 |
import logging logger = logging.getLogger(__name__) @mcp.tool() async def op(ctx: Context) -> str: await ctx.info("客户端可见:开始执行") # 发给客户端 logger.info("本地归档:开始执行 op") # 写本地文件 ...
💡 技巧:两种日志配合用——Context 日志让客户端实时看到服务端在干什么(用户体验),本地 logging 给运维留长期记录(运维诊断)。别用一种替代另一种,它们目标不同。
进度上报与日志在协议层都是「通知(Notification)」——一种不需要回应的消息:
请求-响应(双向): 客户端 → 服务端:请求 服务端 → 客户端:响应(必须) 通知(单向): 服务端 → 客户端:通知 (客户端不回应)
MCP 协议里,进度用 notifications/progress,日志用 notifications/message。服务端发这些通知后,客户端记录/显示,但不发回应。这就是「单向」的协议本质——通知没有响应,只流向一个方向。
这也解释了为什么进度与日志「不等回答」——协议层它们就是「发了就算」的消息,没有响应机制。
把第 7 章的三种交互放一起,帮你判断:
| 需求 | 用什么 | 例子 |
|---|---|---|
| 长任务要显示完成度 | 进度上报 | 「已处理 50/100」 |
| 要让用户看到服务端在干什么 | 日志 | 「正在连接数据库」 |
| 要用户输入才能继续 | 引导填写 | 「确认删除?」 |
| 要告知非致命问题 | 日志(warning) | 「重试了 2 次才成功」 |
判断口诀:
问:用户需要做什么吗? 需要(回答问题/确认/填值)→ 引导填写(双向) 不需要(只是看)→ 进度或日志(单向) 问:信息是「量化进度」还是「定性状态」? 量化(3/10、80%)→ 进度上报 定性(开始、成功、警告)→ 日志
写进度与日志时,几个要点:
| 要点 | 说明 |
|---|---|
| 进度要量化 | 用 progress/total,别只说「快好了」 |
| 日志要简洁 | 每条一行,说清在干什么,别刷屏 |
| 级别要准确 | 别把正常流程标 error,别把致命问题标 info |
| 别过度上报 | 进度别每项都报(太频),日志别每步都记(太吵) |
| 敏感信息别记 | 密码、token 不要进日志 |
# 好的做法:量化进度、合适频率 for i, item in enumerate(big_list): process(item) if i % 100 == 0: # 每 100 项报一次,不刷屏 await ctx.report_progress(i, total, f"已处理 {i}") # 好的做法:简洁日志、准确级别 await ctx.info("开始导入") # 正常流程,info await ctx.warning("跳过重复行:5") # 非致命,warning await ctx.error("导入失败:连接断开") # 致命,error
ctx.report_progress):量化进度,客户端显示进度条,说完就继续。ctx.info/debug/warning/error):定性状态,客户端可见,按级别过滤。第 7 章结束。你已经掌握 MCP 的全部交互能力:引导填写、采样、根、进度、日志。第 8 章转向传输层——消息怎么从客户端传到服务端。