本教程聚焦 MCP(Model Context Protocol)里两条最常被忽视的通知原语——notifications/progress(进度通知)与 notifications/message(日志)。你将学会用 Python 官方 SDK(FastMCP)构建一个能让客户端"看得见进度、读得到日志"的长任务工具,并把它接入 Claude Code 实地观察。适合已经写过"第一个 MCP 工具"、想让工具更可观测、更易调试的中级开发者,预计上手 60–90 分钟。
uv 或 pip 均可)claude --version 可用)@mcp.tool() 与 mcp.run() 怎么跑@modelcontextprotocol/inspector)大多数入门教程只演示请求-响应:客户端调用一个工具,等待,拿到结果。这种模型在工具秒级返回时没问题,但一旦工具要做的事变长——递归扫一个万行代码库、拉一个大表、跑一段构建——客户端就只能干等。用户盯着一个没有反馈的转圈,根本不知道是卡死了还是在干活。
MCP 在 JSON-RPC 之上额外定义了两种带外通知(out-of-band notification),专门解决"长任务的黑盒问题":
| 原语 | 通知方法 | 方向 | 解决的问题 | 客户端可控项 |
|---|---|---|---|---|
| 进度 Progress | notifications/progress |
双向 | "任务跑到哪了" | 在请求里塞 progressToken 决定要不要收 |
| 日志 Logging | notifications/message |
服务器→客户端 | "服务器内部发生了什么" | 用 logging/setLevel 调最低级别 |
注意一个关键事实:这两条通道都是"通知",不是"请求"——通知没有 id,不需要对方回 result,因此服务器可以一边干活一边往外推消息,不阻塞主请求的执行。理解这一点,是后面所有代码的地基。
进度的机制比看上去精巧。它不是服务器单方面狂推,而是一个"约定"流程:
params._meta.progressToken 里塞一个自己挑的 token(字符串或整数,必须全局唯一)。notifications/progress,每次带 progress(当前值)、可选 total(总量)、可选 message(人类可读说明)。// 1. 客户端:请求里带 token
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "_meta": { "progressToken": "abc123" } }
}
// 2. 服务器:反复推进度
{
"jsonrpc": "2.0", "method": "notifications/progress",
"params": { "progressToken": "abc123", "progress": 50, "total": 100, "message": "扫描中..." }
}
规范里有几条硬约束值得记牢:
progress 值必须单调递增(哪怕总量未知)——这是客户端判断"有没有卡住"的依据;progress 和 total 可以是浮点,所以你完全可以用 0.55 表示 55%;在 FastMCP 里你不用手写 token 协商——框架在 tools/call 时自动从请求里取出 token 并在 Context 里准备好。你只要在工具里 await ctx.report_progress(progress=, total=, message=) 即可。
日志是另一条更"轻量"的通道,用来吐服务器内部的非关键信息。规范遵循 RFC 5424 的 syslog 严重级别,从轻到重一共 8 档:
| 级别 | 用途 | 在 MCP 工具里的典型场景 |
|---|---|---|
debug |
详细调试 | 进入/退出某个函数、单步细节 |
info |
常规信息 | 阶段完成、统计概览 |
notice |
显著的正常事件 | 配置变更、缓存命中 |
warning |
警告 | 跳过坏文件、用了已弃用参数 |
error |
错误 | 单次操作失败(但工具整体未崩) |
critical / alert / emergency |
严重/紧急/系统不可用 | 组件级故障、数据损坏 |
三个要点决定了日志"能不能被看见":
capabilities.logging = {}(FastMCP 默认已声明)。logging/setLevel 请求告诉服务器"我只关心 info 及以上",服务器据此过滤。默认情况下很多客户端把级别卡得较高,debug 往往收不到。FastMCP 的 Context 提供了一组语义化方法,背后都是发 notifications/message:
await ctx.debug("进入扫描循环")
await ctx.info(f"发现 {n} 个文件")
await ctx.warning(f"跳过 {name}: 权限不足")
await ctx.error(f"读取失败: {e}")
await ctx.log(level="notice", data={"hit": True, "key": "x"}) # 带结构化 data
新建目录 codebase-stats,装好官方 SDK:
mkdir codebase-stats && cd codebase-stats
python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]"
写 server.py。这个工具 analyze_codebase 会递归扫描一个目录,统计文件数、总行数、按扩展名的分布,并在扫描过程中同时上报进度和日志:
from __future__ import annotations
from pathlib import Path
from collections import Counter
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
mcp = FastMCP("codebase-stats")
EXCLUDED = {".git", "node_modules", "__pycache__", ".venv", "dist", "build"}
CODE_EXT = {".py", ".js", ".ts", ".tsx", ".go", ".rs", ".java", ".md", ".yaml", ".yml"}
@mcp.tool()
async def analyze_codebase(
path: str, ctx: Context[ServerSession, None]
) -> dict:
"""递归统计代码库:文件数、总行数、按扩展名分布;扫描中持续上报进度与日志。"""
root = Path(path).expanduser().resolve()
if not root.exists():
await ctx.error(f"路径不存在: {root}")
return {"error": f"path not found: {root}"}
# 先一次性收集待扫描清单,便于给出准确的 total
files = [
p for p in root.rglob("*")
if p.is_file()
and not any(part in EXCLUDED for part in p.parts)
and p.suffix.lower() in CODE_EXT
]
total = len(files)
await ctx.info(f"发现 {total} 个待扫描文件,开始统计")
by_ext: Counter[str] = Counter()
total_lines = 0
skipped = 0
# 每 5% 上报一次,避免刷屏
step = max(1, total // 20)
for i, p in enumerate(files, start=1):
try:
with p.open("r", encoding="utf-8", errors="replace") as f:
lines = sum(1 for _ in f)
except OSError as e:
skipped += 1
await ctx.warning(f"跳过 {p.name}: {e}")
continue
by_ext[p.suffix.lower() or "(none)"] += 1
total_lines += lines
if i == total or i % step == 0:
await ctx.report_progress(
progress=i, total=total,
message=f"已扫描 {i}/{total} 个文件",
)
await ctx.debug(f"{p.name}: +{lines} 行")
await ctx.info(f"完成:{total} 文件,{total_lines} 行,{skipped} 个跳过")
return {
"root": str(root),
"files_scanned": total - skipped,
"skipped": skipped,
"total_lines": total_lines,
"by_extension": dict(by_ext.most_common(10)),
}
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输
逐行看几个关键设计:
rglob 一遍拿到 total,进度才有可信的分母。代价是多一次遍历,但对本地磁盘通常可接受。i % step == 0 让通知频率稳定在约 20 次,既给用户连续反馈,又不违反"应做限频"的规范。ctx.warning 后跳过——错误信息走日志通道,结果里只汇总 skipped 计数。dict,FastMCP 会自动按结构化输出(structured output)生成 schema,客户端拿到的就是规整的 JSON。进度和日志是带外通知,跑 print 是看不到它们的——它们走的是 JSON-RPC 通知帧。最稳的观察方式是官方的 MCP Inspector:
npx -y @modelcontextprotocol/inspector python server.py
浏览器会打开一个图形界面。流程是:
analyze_codebase,点 List Tools 后填入一个真实目录路径;_meta);notifications/progress 与 notifications/message。如果你看到 progress 从 5%、10% 一路涨到 100%,并且每档日志按级别出现,就说明两条通道都通了。
常见问题:Inspector 里看不到日志? 多半是级别被客户端(Inspector)卡在
info以上,而你发的多是debug。在 Inspector 面板里把日志级别手动设到debug,或把代码里的ctx.debug临时换成ctx.info验证。
把服务器注册给 Claude Code(路径用绝对路径):
claude mcp add codebase-stats -- python /home/you/codebase-stats/server.py
在会话里触发:
帮我统计一下 /home/you/some-repo 的代码分布
Claude Code 会调用 analyze_codebase。此时两条通道分别这样落地:
message 文本会被用来更新"正在做什么"的提示。notifications/message 并在会话上下文里呈现。想看原始通知帧,用调试模式启动:claude --debug
--debug 会把 JSON-RPC 往来(含所有 notifications/progress 和 notifications/message)打印到终端,是排查"通知到底发没发"的第一现场。
sequenceDiagram
participant CC as Claude Code (客户端)
participant S as codebase-stats (服务器)
Note over CC,S: tools/call 带 progressToken
CC->>S: analyze_codebase(path) + _meta.progressToken
S-->>CC: notifications/message (info: 发现 N 文件)
loop 扫描中
S-->>CC: notifications/progress (i/total)
S-->>CC: notifications/message (debug: +x 行)
end
S-->>CC: notifications/message (info: 完成)
S->>CC: tools/call 结果 (结构化 JSON)
flowchart LR
A[写 server.py<br/>注入 ctx.report_progress / ctx.info] --> B[Inspector<br/>勾 progressToken 调试]
B --> C{通知都通?}
C -- 否 --> A
C -- 是 --> D[claude mcp add 注册]
D --> E[claude --debug 观察现场]
E --> F[限频 + 脱敏打磨]
progress。report_progress,扫描 10 万个文件就是 10 万条通知,会拖垮通道。用固定步长或时间窗口(比如每 200ms 至多一条)。report_progress 是 await,工具必须是 async def 且参数里带 ctx: Context[...];写成普通 def 不会注入上下文。setLevel 到 debug,要么用 Inspector 强制低级别。data。规范明确禁止凭据/PII 出现在日志里——它会被推送并可能持久化。ctx.log(level="warning", data={"file": x, "reason": y}) 比拼字符串更便于客户端过滤和展示。--debug 排查 20 分钟最值钱。notifications/progress 单调递增、完成后停止等硬约束的权威出处。capabilities.logging、RFC 5424 八级严重度、logging/setLevel 与"日志不得含密钥/PII"安全红线。Context 的 report_progress、info/debug/warning/error/log 方法签名与官方 tool_progress.py 代码片段。notifications/message,与本文 Inspector/Claude Code 观察方法互为印证。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
