已基于官方 FastMCP v3 文档核实 API(资源模板、工具注解、结构化输出),选定本轮主题:用 Resource Template + Tool Annotations 构建日志检索 MCP——这是已覆盖主题中尚未触及的两个新原语组合。下面是完整教程正文。
本教程带你用 FastMCP v3 从零构建一个名为 logscope 的日志检索 MCP 服务器,重点掌握两个常被忽略却至关重要的新原语:Resource Templates(资源模板)让模型按 URI 模式动态寻址海量数据;Tool Annotations(工具注解)用 readOnlyHint / destructiveHint 给模型划安全红线。配套还会用到结构化输出(Structured Output),让工具返回可被模型可靠解析的强类型结果。适合已写过基础 MCP 工具、想做出"生产级、安全可控"MCP 服务的开发者。
claude --version 可用)pip install fastmcpdestructiveHint 是"提示"而非"强制"写过入门 MCP 的人都知道 @mcp.tool 把函数变成模型可调用的工具。但只靠工具原语,做不出"安全、可发现、可被模型主动浏览"的服务。下面三个原语是分水岭。
① Resource Templates——给数据一个可寻址的 URI 模式
普通 Resource(如 data://config)是静态的,一个 URI 对应一份固定数据。当你有成千上万条目(每个用户、每天日志、每个仓库),逐个注册不现实。Resource Template 用 RFC 6570 的 URI 模板语法 {参数名},让一个注册点派生出无数资源:
@mcp.resource("repos://{owner}/{repo}/info")
客户端请求 repos://PrefectHQ/fastmcp/info 时,FastMCP 自动把 owner=PrefectHQ、repo=fastmcp 注入函数参数并执行。官方文档原话:"Resource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand."模板支持多参数、通配符路径参数({path*})和 query 参数,且与普通资源共享 name/description/mime_type/tags/annotations 配置。
② Tool Annotations——给模型和客户端的安全提示
工具注解不是描述文字,而是一组布尔/对象元数据,声明工具的副作用性质。FastMCP v3 的 ToolAnnotations 属性如下(摘自官方 Tools 文档):
| 属性 | 含义 |
|---|---|
title |
人类可读标题 |
readOnlyHint |
为真表示工具不修改环境(纯读) |
destructiveHint |
为真表示可能产生破坏性副作用(删文件、清数据) |
idempotentHint |
为真表示相同参数重复调用无额外副作用,可安全重试 |
openWorldHint |
为真表示会与"开放世界"的外部实体交互 |
meta |
透传给客户端的自定义元数据 |
注意官方强调:这些是 hints, not guarantees——服务端声明,客户端决定如何对待。Claude Code 等客户端会据此决定是否弹窗确认。这是"权限护栏"在协议层的标准化落地。
③ Structured Output——返回强类型而非裸文本
给工具加返回类型注解(如 -> SearchResult,其中 SearchResult 是 Pydantic 模型),FastMCP 会自动生成 output_schema 并把结果作为结构化 JSON 返回。模型拿到的是带字段语义的结果对象(total、hits[].line),而不是需要再解析的字符串,调用准确率显著提升。
理解这三点后,下面进入实战。
创建项目目录并安装依赖:
mkdir -p ~/logscope-mcp && cd ~/logscope-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install "fastmcp>=2.10" pydantic
python3 -c "import fastmcp; print(fastmcp.__version__)"
预期结果:最后一行打印 FastMCP 版本号(v2.10+ 或 v3.x 均可)。
常见问题:
- 报 No module named fastmcp:确认激活了 .venv(which python 应指向项目内)。
- 国内网络慢:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple "fastmcp>=2.10"。
新建 logscope_mcp.py,先把"应用+日期"两个维度的日志做成可寻址资源:
import json
from pathlib import Path
from fastmcp import FastMCP
mcp = FastMCP(name="logscope")
LOG_DIR = Path.home() / ".logscope" / "logs"
# 索引资源:列出所有应用名,供模型先"看目录"
@mcp.resource("logs://index")
def log_index() -> str:
"""列出当前可被检索的所有应用名。"""
if not LOG_DIR.exists():
return json.dumps({"apps": []}, ensure_ascii=False)
apps = sorted(p.name for p in LOG_DIR.iterdir() if p.is_dir())
return json.dumps({"apps": apps}, ensure_ascii=False)
# 资源模板:任意 app + date 组合都按需生成一个资源
@mcp.resource("logs://{app}/{date}")
def read_log(app: str, date: str) -> str:
"""读取某应用在某天的完整日志。URI 示例: logs://auth/2026-07-04"""
p = LOG_DIR / app / f"{date}.log"
if not p.exists():
raise ValueError(f"日志不存在: logs://{app}/{date}")
return p.read_text(encoding="utf-8")
if __name__ == "__main__":
mcp.run()
关键点解读:
- logs://{app}/{date} 里的 {app}、{date} 占位符与函数参数同名,FastMCP 自动注入。
- 客户端通过 resources/templates/list 发现这个模板,再对具体 URI(如 logs://auth/2026-07-04)发起 resources/read,函数才被惰性执行。
- 模型可以"先读 logs://index 再读具体某天"——这就是动态可发现性。
常见问题:
- 模板没被识别:检查 URI 里花括号前后没有多余空格(HTML 文档里看起来有空格是渲染产物,真实代码须写 logs://{app}/{date})。
- 想匹配多级路径:用通配符 logs://{app}/{path*},path* 会捕获剩余整段路径。
资源负责"喂全文",但全文太长会爆上下文。补一个精准检索工具,返回结构化结果:
from pydantic import BaseModel, Field
class LogHit(BaseModel):
line: int = Field(description="命中日志的行号,从 1 开始")
text: str = Field(description="命中行的原文")
class SearchResult(BaseModel):
app: str = Field(description="应用名")
date: str = Field(description="日期 YYYY-MM-DD")
keyword: str = Field(description="检索关键字")
total: int = Field(description="命中条数")
hits: list[LogHit] = Field(description="命中明细,最多 50 条")
@mcp.tool(annotations={"readOnlyHint": True, "idempotentHint": True})
def search_log(app: str, date: str, keyword: str) -> SearchResult:
"""在某应用某天的日志中按关键字检索,返回带行号的命中结果。"""
p = LOG_DIR / app / f"{date}.log"
hits: list[LogHit] = []
if p.exists():
for i, line in enumerate(
p.read_text(encoding="utf-8").splitlines(), start=1
):
if keyword.lower() in line.lower():
hits.append(LogHit(line=i, text=line))
if len(hits) >= 50:
break
return SearchResult(
app=app, date=date, keyword=keyword, total=len(hits), hits=hits
)
关键点解读:
- -> SearchResult 让 FastMCP 自动生成 output_schema,模型在工具列表里就能看到返回字段及其描述(description 会进 schema),调用更准。
- annotations={"readOnlyHint": True, "idempotentHint": True} 告诉客户端:这是纯读、可重试。在 Claude Code 较宽松的权限模式下,这类工具通常可免确认直接跑。
- 限定最多 50 条(if len(hits) >= 50: break)是"防上下文爆炸"的工程素养——工具返回过大照样会撑爆会话。
常见问题:
- 想用官方对象而非 dict:from fastmcp.tools import ToolAnnotations 后 annotations=ToolAnnotations(readOnlyHint=True),二者等价。
- 模型不按结构解析:确认返回的是 Pydantic 模型实例(不是 dict),FastMCP 才会生成结构化 schema。
现在加一个清空日志的危险操作,故意标 destructiveHint,观察客户端如何"拦截"它:
@mcp.tool(annotations={"destructiveHint": True, "idempotentHint": True})
def purge_log(app: str, date: str) -> dict:
"""清空某应用某天的日志内容(危险:不可恢复,会触发客户端二次确认)。"""
p = LOG_DIR / app / f"{date}.log"
if not p.exists():
return {"purged": False, "reason": "not_found"}
p.write_text("", encoding="utf-8")
return {"purged": True, "path": str(p), "bytes_cleared": p.stat().st_size}
为什么 idempotentHint 也为真:清空后再清空还是空,重复调用无额外副作用——符合幂等定义,方便客户端在确认后安全重试。
注解的真正威力:destructiveHint=True 会让 Claude Code 在调用前弹窗要求你确认,即便你处于较宽松的权限模式。这就是"在协议层而非项目层做权限"的好处——换任何尊重注解的 MCP 客户端,护栏都跟着走。
准备一份种子日志,方便看到效果:
mkdir -p ~/.logscope/logs/auth ~/.logscope/logs/payment
printf '2026-07-04 09:01 login ok user=alice\n2026-07-04 09:03 login FAIL user=bob reason=badpwd\n2026-07-04 09:10 token refresh FAIL user=alice\n' > ~/.logscope/logs/auth/2026-07-04.log
printf '2026-07-04 09:05 pay ok order=1001 amount=99\n2026-07-04 09:06 pay FAIL order=1002 reason=insufficient_funds\n' > ~/.logscope/logs/payment/2026-07-04.log
把服务器注册进 Claude Code(用项目级 .mcp.json,便于团队共享):
{
"mcpServers": {
"logscope": {
"command": "/home/gprshome/logscope-mcp/.venv/bin/python",
"args": ["/home/gprshome/logscope-mcp/logscope_mcp.py"]
}
}
}
提示:
command尽量写 venv 内 python 的绝对路径,避免 Claude Code 启动子进程时找不到激活的虚拟环境。也可用 CLI 注册:claude mcp add logscope -- /home/gprshome/logscope-mcp/.venv/bin/python /home/gprshome/logscope-mcp/logscope_mcp.py。
启动 claude,先确认服务上线:
/mcp
应看到 logscope 处于 connected,并列出 read_log、search_log、purge_log 三个工具与 logs://index 资源及 logs://{app}/{date} 模板。
预期结果:在该会话里对模型说"帮我找出 auth 应用 2026-07-04 所有含 FAIL 的日志",模型会调用 search_log(只读、免确认)并返回结构化命中(行号 2、3)。若让它"清空 payment 那天的日志",调用 purge_log 时会弹出确认——这就是 destructiveHint 生效的肉眼证据。
常见问题:
- /mcp 显示连接失败:在项目目录运行 python logscope_mcp.py 单独看报错;通常是依赖没装在该 venv 或路径写错。
- 模型只调工具不读资源:正常现象。资源需要客户端把模板暴露给模型;Claude Code 中可手动 @logs://index 附加,或让模型"先列出应用"。
flowchart TD
U[用户: 找出今天的 FAIL 日志] --> M[Claude Code / 模型]
M -->|resources/templates/list| S[logscope MCP 服务]
S -->|暴露模板 logs://app/date| M
M -->|read logs://index| S
S --> LOG[(~/.logscope/logs/)]
M -->|"call search_log readOnly=True"| S
S -->|扫描文件,返回 SearchResult| M
M -->|结构化命中行号| U
U -->|顺带: 清空 payment 日志| M
M -->|"call purge_log destructive=True"| G{客户端确认门禁}
G -->|destructiveHint 触发| CONFIRM[弹窗要求用户确认]
CONFIRM -->|同意| S
CONFIRM -->|拒绝| M
S -->|清空文件| LOG
图里最关键的是 destructiveHint → "客户端确认门禁"那一条:护栏发生在客户端侧,服务端只是声明。这也意味着——如果你写的客户端不读注解,护栏就不会自动出现,需在客户端实现相应逻辑。
| 现象 | 原因 | 解决 |
|---|---|---|
| 资源模板没出现 | URI 占位符与参数名不一致,或有多余空格 | 严格写成 logs://{app}/{date},花括号紧贴 |
| 工具返回被模型当成纯文本 | 返回了 dict/str 而非 Pydantic 模型 |
用 BaseModel 作为返回类型,触发结构化 schema |
purge_log 没让确认 |
客户端未实现注解读取 / 当前权限模式放行所有 | 在更严格权限模式测试,或确认客户端支持注解 |
| 单条日志太大撑爆上下文 | 工具/资源返回全文 | 工具侧加分页与 max_results,资源侧只给摘要 |
| 多客户端注解行为不一致 | 注解是 hint 不是强制 | 把"必须确认"逻辑同时写在客户端,而非只依赖服务端 |
Context 对象做进度上报:长任务通过 ctx.report_progress() 把百分比推给客户端。read_log 升级为异步资源(async def),对接真实的对象存储/ELK。output_schema 手写自定义输出契约,或用 ToolResult 同时返回文本 + 结构化数据 + 多媒体块。readOnlyHint 与 destructiveHint 在客户端侧的差异,并写出带结构化输出的只读工具 + 一个被正确标记的破坏性工具。@tool 装饰器、ToolAnnotations(readOnly/destructive/idempotent/openWorld)、Structured Output 与 output_schema[!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 等场景全覆盖。
