首页 › tutorials › 造日志检索MCP:动态资源配工具注解

Round 3 教程:Claude Code MCP 服务器开发

已基于官方 FastMCP v3 文档核实 API(资源模板、工具注解、结构化输出),选定本轮主题:用 Resource Template + Tool Annotations 构建日志检索 MCP——这是已覆盖主题中尚未触及的两个新原语组合。下面是完整教程正文。


概述

本教程带你用 FastMCP v3 从零构建一个名为 logscope 的日志检索 MCP 服务器,重点掌握两个常被忽略却至关重要的新原语:Resource Templates(资源模板)让模型按 URI 模式动态寻址海量数据;Tool Annotations(工具注解)用 readOnlyHint / destructiveHint 给模型划安全红线。配套还会用到结构化输出(Structured Output),让工具返回可被模型可靠解析的强类型结果。适合已写过基础 MCP 工具、想做出"生产级、安全可控"MCP 服务的开发者。

前置条件

三个你必须补上的新原语

写过入门 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),而不是需要再解析的字符串,调用准确率显著提升。

理解这三点后,下面进入实战。

步骤 1:搭好运行环境

创建项目目录并安装依赖:

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"。

步骤 2:用 Resource Template 暴露按日日志

新建 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* 会捕获剩余整段路径。

步骤 3:加一个"只读 + 结构化输出"的搜索工具

资源负责"喂全文",但全文太长会爆上下文。补一个精准检索工具,返回结构化结果:

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。

步骤 4:加一个"破坏性"工具,看注解如何变成护栏

现在加一个清空日志的危险操作,故意标 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 客户端,护栏都跟着走。

步骤 5:注册到 Claude Code 并验证

准备一份种子日志,方便看到效果:

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 不是强制 把"必须确认"逻辑同时写在客户端,而非只依赖服务端

延伸阅读

训练营适用性评估

参考来源

  1. Resources & Templates — FastMCP 官方文档 — Resource Template、RFC 6570 URI 模板、通配符与 query 参数的权威说明
  2. Tools — FastMCP 官方文档 — @tool 装饰器、ToolAnnotations(readOnly/destructive/idempotent/openWorld)、Structured Output 与 output_schema
  3. MCP Developer Guide 2026 — Lushbinary — 2026 年 MCP 构建、部署、安全集成的完整开发者指南
  4. PrefectHQ/fastmcp — GitHub — FastMCP(v3)源码与最新特性,含 ResourcesAsTools 等变换
  5. Model Context Protocol Specification — 工具注解、资源模板、结构化输出的协议级定义

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入