EP01. "MCP 进阶:Sampling、Notifications 与 Roots 三个核心能力"
🔒 登录后可标记已读- 这门课接着
Introduction to Model Context Protocol,给已经在动手做正式(production)MCP Server 的工程师讲三个进阶能力:怎么让 Server 借用 Client 已经接好的 Claude(Sampling)、耗时的工具怎么让用户看到进度(Notifications)、怎么在安全边界内让 Claude 找到用户本机的文件(Roots) - 这篇笔记覆盖课程「Core MCP features」这个大分类,剩下的传输层(STDIO / StreamableHTTP)另开一篇
- 前置知识:学过
Introduction to Model Context Protocol,会用 Python SDK 写基本的 Tools/Resources/Prompts
重点内容
Sampling:让 Server 借用 Client 已经接好的 Claude
Server 想要生成文本(比如帮用户总结一段内容),有两条路:
| 做法 | 代价 |
|---|---|
| 方案一:Server 自己直接整合 Claude | 要自己的 Claude API key、自己管认证、自己承担费用、自己写整合代码 |
| 方案二:Sampling | 把生成文本这件事交给 Client——Client 本来就已经接好一条 Claude 连接了 |
流程:Server 做完自己该做的事 → 组好一个要生成文字用的 prompt → 把 sampling 请求发给 Client → Client 拿这个 prompt 去调用 Claude → Client 把生成结果传回 Server → Server 把结果整合进自己的响应里。
好处:降低 Server 自身的整合复杂度;token 费用转嫁给 Client 承担,不是 Server;Server 不需要自己的 Claude 凭证;对公开给陌生用户用的 Server 来说,能避免被无限调用、自己一直出钱。
Server 端:
@mcp.tool()
async def summarize(text_to_summarize: str, ctx: Context):
prompt = f"""
Please summarize the following text:
{text_to_summarize}
"""
result = await ctx.session.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(
type="text",
text=prompt
)
)
],
max_tokens=4000,
system_prompt="You are a helpful research assistant",
)
if result.content.type == "text":
return result.content.text
else:
raise ValueError("Sampling failed")
Client 端:要提供一个 sampling callback,负责实际去调 Claude:
async def sampling_callback(
context: RequestContext, params: CreateMessageRequestParams
):
# Call Claude using the Anthropic SDK
text = await chat(params.messages)
return CreateMessageResult(
role="assistant",
model=model,
content=TextContent(type="text", text=text),
)
建立 session 时要把这个 callback 传进去:
async with ClientSession(
read,
write,
sampling_callback=sampling_callback
) as session:
await session.initialize()
📌 Sampling 最适合公开访问、成本控制很重要的场景——把整合复杂度和账单一起转给 Client 承担,Client 反正本来就有现成的 Claude 连接和凭证。
Notifications:耗时的工具怎么让用户看到进度
Claude 调一个要跑一阵子的工具(比如做研究、处理一批数据)时,用户在结果出来之前通常什么都看不到——不知道是在正常跑还是卡死了,体验很差。
解法:Python SDK 会自动给每个 tool 函数传一个 Context 参数,里面提供两个方法:
context.info()——发日志消息给 Clientcontext.report_progress()——更新当前进度 / 总量
@mcp.tool(
name="research",
description="Research a given topic"
)
async def research(
topic: str = Field(description="Topic to research"),
*,
context: Context
):
await context.info("About to do research...")
await context.report_progress(20, 100)
sources = await do_research(topic)
await context.info("Writing report...")
await context.report_progress(70, 100)
results = await generate_report(sources)
return results
Client 端要设两种 callback:
async def logging_callback(params: LoggingMessageNotificationParams):
print(params.data)
async def print_progress_callback(
progress: float, total: float | None, message: str | None
):
if total is not None:
percentage = (progress / total) * 100
print(f"Progress: {progress}/{total} ({percentage:.1f}%)")
else:
print(f"Progress: {progress}")
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read,
write,
logging_callback=logging_callback
) as session:
await session.initialize()
await session.call_tool(
name="add",
arguments={"a": 1, "b": 3},
progress_callback=print_progress_callback,
)
logging_callback 在建立 session 时传一次就好;progress_callback 则是每次 call_tool 各自传,比较灵活。
呈现方式因应用类型而不同:CLI 应用直接把消息 print 到终端;Web 应用用 WebSocket、Server-Sent Events 或轮询把更新推给浏览器;桌面应用更新进度条/状态栏。
📌 实作这些通知完全是可选的——可以整个不理会、只挑某几类呈现、或按自己应用的需要自订呈现方式,它们纯粹是体验层的优化,不影响功能本身。
Roots:怎么在安全边界内让 Claude 找到用户的文件
没有 Roots 时,Claude 没办法搜索整个文件系统去定位一个文件。比如用户说「把 biking.mp4 转成 mov 格式」,Claude 根本不知道这个文件在 Movies/Sports 这种深层目录结构里的哪个位置。最直接但体验很差的解法是要求用户自己贴完整路径。
有 Roots 之后的流程:
- 用户提出文件操作请求
- Claude 调用
list_roots,拿到有哪些目录被授权访问 - Claude 在被授权的目录里调用
read_dir,找到目标文件 - Claude 用找到的完整路径去执行工具
用户从此只要讲文件名,不用自己记/贴完整路径。
安全边界:Roots 会限制 Server 只能碰被指定的目录——如果只授权了 Desktop,Server 就碰不到 Documents 或 Downloads。Claude 想访问授权范围外的路径时会收到错误,可以据此告知用户。
📌 SDK 不会自动帮你挡:这是个实作细节容易被忽略的坑——MCP SDK 本身不会自动强制执行这个访问限制,开发者要自己写一个校验函数(常见做法是 is_path_allowed()),在真正执行文件操作之前先检查请求的路径是否落在被授权的 Root 范围内。
好处:用户不用打完整路径;检索范围收窄在授权目录内,找文件更快;避免 Claude 误触到不该碰的敏感文件;Root 本身还能透过 tool 或 prompt 动态配置,不是写死的。
学完你会
- ✅ 分得清 Sampling 跟「Server 自己接 Claude」的差别,知道什么场景该用 Sampling 省掉凭证和费用
- ✅ 会用
context.info()/context.report_progress()给耗时工具加上进度提示,Client 端也接好对应的 callback - ✅ 理解 Roots 怎么让 Claude 在不要求用户贴完整路径的前提下,安全地在授权目录内找到文件
- ✅ 知道 SDK 不会自动帮你做 Roots 的权限校验,这一步要自己写
常见错误
- ❌ Server 想生成文字,第一反应就是自己申请一把 Claude API key——先想想能不能用 Sampling 把这件事丢给 Client,省掉一整套凭证管理 / 计费 / 整合代码
- ❌ 耗时的工具完全不做任何进度回报——用户会以为程序卡死了;哪怕只是简单加两行
context.info()/report_progress(),体验也会好很多 - ❌ 以为 Notifications 是协议要求必须实现的功能——它纯粹是体验层的可选优化,可以完全不理、也可以只挑部分类型呈现
- ❌ 用了 Roots 就以为 SDK 会自动帮你挡住越界的文件路径——SDK 不会自动强制执行这个限制,校验逻辑(比如
is_path_allowed())要自己写,不写等于没有真正的安全边界 - 💡 公开给很多陌生用户使用的 Server,优先考虑用 Sampling,避免每个用户的调用都变成自己出 token 钱
Sources
官方文档:
- Model Context Protocol: Advanced Topics(课程总览)— https://academy.claude.com/courses/model-context-protocol-advanced-topics
- Sampling — https://academy.claude.com/courses/model-context-protocol-advanced-topics/sampling
- Sampling walkthrough — https://academy.claude.com/courses/model-context-protocol-advanced-topics/sampling-walkthrough
- Log and progress notifications — https://academy.claude.com/courses/model-context-protocol-advanced-topics/log-and-progress-notifications
- Notifications walkthrough — https://academy.claude.com/courses/model-context-protocol-advanced-topics/notifications-walkthrough
- Roots — https://academy.claude.com/courses/model-context-protocol-advanced-topics/roots
- Roots walkthrough — https://academy.claude.com/courses/model-context-protocol-advanced-topics/roots-walkthrough