4 minutes
MCP 进阶:Resources / Prompts / Sampling
上一章我们以 Tool 为核心写了第一个 MCP server。MCP 还有两件大杀器没用上:Resources 让 Agent 主动拉数据;Prompts 把常见对话模板做成快捷指令。再深一层,还能让 server 反过来调模型——sampling。本章打完 MCP 的完整能力包。
18.1 三原语职责回顾
Tool 模型决定调用 / 有副作用 类比"实习生执行命令"
Resource 客户端按需读取 / 无副作用 类比"调用 卡片盒"
Prompt 用户主动选择的快捷方式 类比"slash command / 应用菜单"
Sampling server 反过来调 LLM 类比"反向 function-call"
理解这四件事的发起方向是关键。混用 = bug;分开 = 协议清晰。
18.2 Resources 实战:把"工单列表"做成可拉数据
18.2.1 何时 Resources > Tools
| 判据 | Resources 适合 | Tools 适合 |
|---|---|---|
| 调用方 | harness 周期性拉、缓存 | 模型按需 invoke |
| 副作用 | 无 | 有 |
| 数据形态 | 静态文档 / 列表 / 配置 | 函数调用结果 |
| 大小 | 中等(KB-MB) | 小(KB 以内为佳) |
举几个常见 Resource 设计:
tickets://open → 当前 open 工单列表 (JSON)
tickets://3201 → 单条工单详情 (JSON)
tickets://stats/weekly → 本周统计 (JSON)
notion://spec/agent-design → 某 Notion 文档 (markdown)
18.2.2 动态与静态 URI
MCP Resource 的 URI 可以是固定(“tickets://open”)也可以带模板(“tickets://{id}")。前者用于 list_resources 一次性列出,后者用 resource templates:
from mcp.types import ResourceTemplate
@server.list_resource_templates()
async def list_templates() -> list[ResourceTemplate]:
return [
ResourceTemplate(
uriTemplate="tickets://{id}",
name="Ticket by ID",
description="按 ID 取工单",
mimeType="application/json",
)
]
@server.read_resource()
async def read(uri: str) -> str:
from urllib.parse import urlparse
p = urlparse(uri)
if p.scheme != "tickets":
raise ValueError(f"unknown scheme: {p.scheme}")
if p.path == "open":
return json.dumps([t.model_dump() for t in client.list(status="open")])
if p.netloc.isdigit():
return json.dumps(client.get(int(p.netloc)).model_dump())
raise ValueError(f"unknown resource: {uri}")
resource templates 让客户端先知道 schema 再生成 URI,省一组"动态列举"成本。
18.2.3 让 Resource 可订阅
如果数据会变(如工单被关闭),告诉客户端"哪些 resource 改了” 是更高级的用法:
from mcp.server import NotificationManager
@server.list_resources()
async def list_resources():
return resources_cache
# 当某个工单状态变化时 → 通知
async def notify_change(res_updated_uri: str):
await server.request_handlers["notifications/resources/list_changed"]({})
await server.request_handlers["notifications/resources/updated"](
{"params": {"uri": res_updated_uri}}
)
支持通知的资源对实时 dashboard 这类场景价值大。
18.3 Prompts:把"常见任务"做成快捷指令
Prompt 不是"prompt 工程的 prompt"——是 MCP 协议里的"预制对话模板"。
含义:
Prompt = 一个带 name + arguments 的对话剧本片段
作用:
在 IDE / harness 里被作为 slash command 显示给用户选用
关键:
Prompt 不直接调 LLM,它让 harness 生成一段 messages 发给 LLM
举一个:把"review 这个 PR"做成 prompt:
@server.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name="pr-review",
description="Review 当前分支的 diff",
arguments=[
types.PromptArgument(name="branch",
description="分支名",
required=True),
],
)
]
@server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> types.GetPromptResult:
if name == "pr-review":
branch = arguments["branch"]
return types.GetPromptResult(
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=(f"请 review 分支 {branch} 的 diff\n"
f"关注:\n"
f"1. 类型安全问题\n"
f"2. 安全风险\n"
f"3. 性能\n"
f"4. 测试覆盖\n"
f"输出按严重程度排序的 5 条以内问题清单")
)
)
]
)
raise ValueError(f"unknown prompt: {name}")
OpenCode 把 prompts 当 slash command 显示:
/pr-review branch=feature/agent-graph
击中 Enter 后,harness 把 prompt 注入到对话开头——这是为什么 MCP 的 prompts 跟 OpenCode 的 slash command 像孪生兄弟。
18.4 Sampling:MCP server 反过来调 LLM
最有意思的原语来了。Sampling 让 server 在执行一个 tool 时反过来向 client 请求一次 LLM 调用。
方向反转:
正常: LLM → tool_call → server.tool
Sampling: server.tool → request LLM call → client 反馈 LLM 输出给 server
用例:你的 server 接收"用户给 ticket 一段自然语言评论",需要先总结成一行摘要:
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "tickets.summarize_comment":
body = arguments["body"]
summary = await server.sample(
messages=[
{"role": "user",
"content": f"用一句话中文总结这条评论:\n\n{body}"},
],
max_tokens=50,
)
return [types.TextContent(type="text", text=summary)]
注意三件事:
- server 自身不调外部 LLM API,它通过协议请 client 帮它调——避免 server 持 API key 又是合规与成本问题
- client 可以拒绝 sampling(danger 模式 / 已达额度),server 必须有 fallback
- server.sample() 在 OpenCode / Claude Code 等 client 上是支持的,但 stdio 的简单 server 可能根本不响应你
18.5 一次实战:组合三原语
编写"工单助手" server,目标是 Agent 能:
- 用 prompt /summarize-comment 把长评论总结成一句话
- 用 tool tickets.comment 写回工单
- 用 resource tickets://{id} 给后续读取
@server.list_prompts()
async def list_prompts():
return [
types.Prompt(
name="summarize-comment",
description="总结长评论为一句,并自动写入工单",
arguments=[
types.PromptArgument(name="ticket_id", required=True),
types.PromptArgument(name="raw_body", required=True),
],
)
]
@server.get_prompt()
async def get_prompt(name, arguments):
if name != "summarize-comment":
raise ValueError
tid = int(arguments["ticket_id"])
raw = arguments["raw_body"]
summary = await server.sample(
messages=[{"role":"user",
"content": f"用一句中文总结这条评论:\n\n{raw}"}],
max_tokens=80,
)
# 把 summary 当作 internal comment 写入工单
client.comment(tid, f"[AI summary] {summary}")
return types.GetPromptResult(
messages=[types.PromptMessage(role="user",
content=types.TextContent(type="text",
text=f"已为 #{tid} 添加摘要 comment: {summary}"))]
)
模型在此并不显式 invoke 任何 tool,是 sampling 帮它把"总结"短暂外包给 LLM 一次。这是 MCP 协议最有"生态互操"特征的玩法。
18.6 Roe Tooling:用 logging / progress / cancellation
MCP 还带三个轻量原语帮你做"长任务" server:
# 告诉 client 进度
await server.request_handlers["notifications/progress"](
{"params": {"progress": 50, "total": 100, "message": "syncing 50/100"}}
)
# 告诉 client 日志
await server.send_log("info", "Synced 50 tickets from upstream")
# 让 server 处理取消 (client abort)
@server.cancel_request()
async def on_cancel(request_id: str):
# 取消 httpx 调用
...
长跑 server(如迁徙数据、批量 fetch)务必接这几个——否则 client 看着等不了,会以为 server 卡死。
18.7 安全:sampling 信任边界
Sampling 把 client 的 LLM 调用权让渡给 server,是个反信任动作。三条加固:
- Server 必须在 prompt 里"声明用途",让 client 决定是否同意
- Client 端可配 sampler policy:每次 sampling 弹 confirm,不要 auto-yes
- Server 不应通过 sampling 调用模型"修改数据" ——只用作"读取的二次加工"
OpenCode 的 mcp 配置里有 samplingPolicy 字段:deny / confirmEach / allowWithBudget。第三档配 budget,避免失控烧 token。
18.8 把 4 个原语揉成"工单助手"全部能力图
┌──────────────────────── MCP Server mcp-tickets ───────────────────────────┐
│ │
│ Tools (model 调用) Resources (client 拉取) │
│ ── tickets.list ── tickets://open │
│ ── tickets.get ── tickets://{id} (template) │
│ ── tickets.comment ── tickets://stats/weekly │
│ │
│ Prompts (user 选择) Sampling (server 借 LLM) │
│ ── /pr-review ── summarize(text) -> short text │
│ ── /summarize-comment ── classify(text, labels) -> choice │
│ │
└────────────────────────────────────────────────────────────────────────────┘
一个 server 覆盖四种使用模式,对 Agent 端开放几乎是"一项全能"。
18.9 小结
- 三原语 + sampling,发起方向各异:模型 / 客户端 / 用户 / server
- Resources 走 read-only;用 URI templates 让客户端构造动态路径
- Prompts 是 server 端的 slash command 雏形,本系列 23 Skill 设计 详谈
- Sampling 把 server 借 LLM 这件事标准化;务必加 confirm 策略 + budget
下一篇:《19 MCP 生态与未来》——MCP 之外的协同竞争、跨平台桥接、未来一年趋势。
Summary: Resources 拉数据、Prompts 是快捷指令、Sampling 反向借 LLM;四原语组合才是一个完整 MCP server。