首页 › tutorials › MCP资源原语:开放只读数据

概述

绝大多数 MCP 教程都在教你怎么造「工具(Tools)」——让模型执行动作。但 MCP 体系里还有一个被严重低估的原语:Resources(资源)。它的定位是只读上下文:把文件、配置、数据库 schema、运行日志、文档片段等数据,用标准 URI 暴露给模型,让模型在你授权后"阅读",而不是"调用"。

本教程聚焦唯一一个面向"数据"的 MCP 原语。学完你将掌握:Resources 与 Tools 的本质区别、@mcp.resource 装饰器与 URI 模板(RFC 6570)、静态资源与动态资源模板的写法、resources/list 与 resources/read 协议握手、列表变更与订阅通知,并在最后落地一个可运行的「项目知识库」MCP 服务器。适合已经写过 Tool、想补齐全套原语的开发者。


一、前置条件

项 要求 说明
Python 3.10+ 资源函数支持类型注解即可
FastMCP pip install fastmcp 官方维护的高层 SDK,资源 API 最简洁
Claude Code 已安装并跑通过至少一个 MCP 本教程假设你用过 /mcp 命令
MCP Inspector npx @modelcontextprotocol/inspector 用于离线验证资源是否正确暴露

你应当已经理解 MCP 的客户端/服务器架构,并写过至少一个 Tool。如果还没有,先补「首个 MCP 工具」再回来——本文不重复 Tool 的基础。


二、核心认知:Resources 到底解决什么问题

2.1 一句话区分三大原语

MCP 的服务端能力被拆成三类,对应模型与"世界"互动的三种姿势:

一个高频误区是把所有东西都做成 Tool。后果是:模型会"自作主张"地反复调用,浪费 token、放大幻觉风险,而且用户失去对"哪些数据进入了上下文"的控制权。Resources 的设计哲学是 application-driven(由应用/用户驱动):哪些资源进入上下文,由宿主应用(如 Claude Code 的资源选择器、@ 提及)决定,而不是模型自己拍板。这一条决定了它的安全语义——只读、幂等、可审计。

2.2 协议层面:能力声明与四类消息

服务端若支持资源,必须在初始化握手里声明 resources 能力:

{
  "capabilities": {
    "resources": {
      "subscribe": true,
      "listChanged": true
    }
  }
}

两个子能力都是可选的:

围绕这两个开关,协议定义了一组消息:

方法 方向 作用
resources/list 客户端→服务端 列出所有静态资源(支持分页 cursor)
resources/read 客户端→服务端 按 URI 读取资源内容
resources/templates/list 客户端→服务端 列出动态资源模板
resources/subscribe 客户端→服务端 订阅某 URI 的变更
notifications/resources/updated 服务端→客户端 某资源内容变了
notifications/resources/list_changed 服务端→客户端 资源清单变了,请重新 list

读懂这张表,你就掌握了资源的全部交互面。下面把它落到代码里。


三、动手:第一个静态资源

3.1 最小可运行示例

新建 kb_server.py:

import json
from fastmcp import FastMCP

mcp = FastMCP(name="ProjectKB")

# 静态资源:固定 URI,内容在读取时求值(懒加载)
@mcp.resource("kb://manifest")
def get_manifest() -> str:
    """项目清单:名称、版本、模块入口,供模型理解项目骨架。"""
    return json.dumps({
        "name": "order-service",
        "version": "2.4.1",
        "modules": ["api", "worker", "scheduler"],
        "entry": "api/main.py"
    }, ensure_ascii=False, indent=2)

启动方式有两种,二选一:

# 方式 A:FastMCP CLI(开发期最省事)
fastmcp dev kb_server.py

# 方式 B:直接以 stdio 模式跑(Claude Code 接入用)
fastmcp run kb_server.py

3.2 预期结果

用 MCP Inspector 连上后,切到 Resources 标签页,你会看到一个 URI 为 kb://manifest 的条目。点击它,右侧返回:

{
  "contents": [
    {
      "uri": "kb://manifest",
      "mimeType": "text/plain",
      "text": "{\n  \"name\": \"order-service\", ... }"
    }
  ]
}

对应的底层 JSON-RPC 就是协议里的 resources/read:

{ "jsonrpc": "2.0", "id": 2, "method": "resources/read",
  "params": { "uri": "kb://manifest" } }

3.3 常见问题


四、动态资源模板:参数化 URI

静态资源适合「全局唯一」的数据(清单、版本)。真实世界更多是按参数取的数据:某一天的报告、某个模块的文档、某个指标的当前值。这时用资源模板(Resource Template),本质是带占位符的 URI 模板(遵循 RFC 6570)。

from pathlib import Path
import json

NOTES_DIR = Path("notes")  # 存放 markdown 文档的目录
METRICS = {                                # 模拟一个"指标数据库"
    "qps": {"value": 1280, "unit": "req/s"},
    "p99": {"value": 42, "unit": "ms"},
    "error_rate": {"value": 0.03, "unit": "%"},
}

@mcp.resource("kb://guide/{topic}")
def get_guide(topic: str) -> str:
    """按主题读取一篇指南(notes/<topic>.md)。"""
    # 安全校验:topic 只允许字母数字与短横线,杜绝路径穿越
    if not topic.replace("-", "").isalnum():
        raise ValueError(f"非法主题名: {topic}")
    path = (NOTES_DIR / f"{topic}.md").resolve()
    if not str(path).startswith(str(NOTES_DIR.resolve())):
        raise ValueError("禁止访问 notes 目录之外的文件")
    if not path.exists():
        raise FileNotFoundError(f"未找到主题: {topic}")
    return path.read_text(encoding="utf-8")

@mcp.resource("kb://metric/{name}")
def get_metric(name: str) -> str:
    """按名称读取一个运行指标。"""
    if name not in METRICS:
        raise KeyError(f"未知指标: {name}")
    return json.dumps({"name": name, **METRICS[name]}, ensure_ascii=False)

关键点:URI 里的 {topic}、{name} 与函数参数名必须同名,FastMCP 会把模板参数自动注入。模板注册后,客户端通过 resources/templates/list 拿到 uriTemplate: "kb://guide/{topic}",然后用 kb://guide/deploy 这样的具体 URI 去 read。

4.1 预期结果

Inspector 的 Resource Templates 标签会列出两个模板。在地址栏输入 kb://metric/qps,得到:

{ "name": "qps", "value": 1280, "unit": "req/s" }

输入 kb://guide/deploy(前提是 notes/deploy.md 存在)则返回该文档全文。

4.2 常见问题


五、注解:把语义告诉客户端

资源可以挂注解(Annotations),提示客户端如何呈现、如何缓存。默认情况下,资源的 readOnlyHint 和 idempotentHint 都是 true——这正反映了"只读幂等"的核心承诺。显式声明能让客户端做更好的 UI 与访问优化:

@mcp.resource(
    "kb://changelog",
    name="变更日志",
    description="最近一次发布的功能与修复清单。",
    mime_type="text/markdown",
    annotations={"readOnlyHint": True, "idempotentHint": True},
)
def get_changelog() -> str:
    return Path("CHANGELOG.md").read_text(encoding="utf-8")
注解 默认 含义
readOnlyHint true 只提供数据,不修改环境
idempotentHint true 多次读取等价于一次
meta 空 透传给客户端的自定义元数据,可用于版本号、归属等

注意:注解是建议性的,不会在协议层强制执行。它的价值在于让 Claude Code 这类客户端把只读资源和有副作用的工具分开渲染、分别授权。


六、列表变更与订阅:让数据"活"起来

很多教程止步于静态读取。但真实场景里数据会变——指标在更新、文档被改写。两个通知机制让资源具备时效性。

6.1 listChanged:清单变了就喊一声

FastMCP 在你运行时增删资源时,会自动广播 notifications/resources/list_changed,无需手写。运行期动态注册一个资源,客户端会自动刷新清单:

@mcp.tool()
def publish_runbook(name: str, content: str) -> str:
    """把一段手册发布成资源(带副作用的动作,所以用 Tool)。"""
    # 注意:这是 Tool,不是 Resource——它改变了世界
    mcp.add_resource_fn(lambda: content, uri=f"kb://runbook/{name}",
                        name=name)
    return f"已发布 {name}"

6.2 subscribe:盯住某个资源

声明 subscribe 能力后,客户端可订阅特定 URI,服务端在内容变化时推送 notifications/resources/updated:

from fastmcp import Context

@mcp.resource("kb://live-qps")
async def live_qps(ctx: Context) -> str:
    """实时 QPS(演示订阅语义)。"""
    return json.dumps({"qps": await fetch_qps()})

设计提示:把"读当前值"做成 Resource(kb://live-qps),把"发布一个值"做成 Tool。这是 Resources/Tools 边界最清晰的实战体现——读用资源,写用工具。


七、实战案例:项目知识库 MCP 服务器

把前面的零件组装成一个完整服务。目录结构:

project-kb/
├── kb_server.py          # MCP 服务器
├── notes/
│   ├── deploy.md         # 部署指南
│   └── rollback.md       # 回滚手册
├── metrics.json          # 指标数据源
├── CHANGELOG.md
└── .mcp.json             # Claude Code 接入配置

kb_server.py 完整版:

import json
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP(name="ProjectKB")
ROOT = Path(__file__).parent

# ① 静态资源:项目骨架
@mcp.resource("kb://manifest")
def manifest() -> str:
    return json.dumps({
        "name": "order-service", "version": "2.4.1",
        "modules": ["api", "worker", "scheduler"],
    }, ensure_ascii=False)

# ② 静态资源:变更日志(markdown)
@mcp.resource("kb://changelog",
              mime_type="text/markdown",
              description="最近发布的功能与修复。")
def changelog() -> str:
    return (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")

# ③ 动态模板:按主题读文档(带路径穿越防护)
@mcp.resource("kb://guide/{topic}")
def guide(topic: str) -> str:
    if not topic.replace("-", "").isalnum():
        raise ValueError("非法主题名")
    p = (ROOT / "notes" / f"{topic}.md").resolve()
    if not str(p).startswith(str((ROOT / "notes").resolve())):
        raise ValueError("越界访问")
    return p.read_text(encoding="utf-8")

# ④ 动态模板:按名读指标
@mcp.resource("kb://metric/{name}")
def metric(name: str) -> str:
    data = json.loads((ROOT / "metrics.json").read_text(encoding="utf-8"))
    if name not in data:
        raise KeyError(f"未知指标: {name}")
    return json.dumps({"name": name, **data[name]}, ensure_ascii=False)

if __name__ == "__main__":
    mcp.run()  # 默认 stdio 传输

.mcp.json(放在项目根,Claude Code 启动时自动加载):

{
  "mcpServers": {
    "project-kb": {
      "command": "fastmcp",
      "args": ["run", "kb_server.py"]
    }
  }
}

在项目目录里启动 Claude Code,运行 /mcp 应看到 project-kb 已连接。然后让模型读数据——资源由你(用户)显式喂入,而非模型自己调:

提示词示例:@kb://manifest 结合 @kb://guide/deploy,说明本次部署要改哪些模块的配置。

Claude Code 会把这两个资源的内容作为上下文读进来,再基于它们作答。若想确认资源是否真被读到,可用 /context 查看当前上下文里的资源片段。

验证清单


八、工作流程图

资源从声明到被消费的完整链路:

sequenceDiagram
    participant CC as Claude Code(客户端)
    participant SRV as MCP 服务器

    Note over CC,SRV: 初始化握手
    CC->>SRV: initialize(声明期望)
    SRV-->>CC: capabilities.resources = {subscribe, listChanged}

    Note over CC,SRV: ① 发现
    CC->>SRV: resources/list(cursor 分页)
    SRV-->>CC: 静态资源清单
    CC->>SRV: resources/templates/list
    SRV-->>CC: 动态模板清单 kb://guide/{topic}

    Note over CC,SRV: ② 用户用 @ 显式选择
    CC->>SRV: resources/read(uri=kb://guide/deploy)
    SRV->>SRV: 校验 topic → 读 notes/deploy.md
    SRV-->>CC: TextResourceContents

    Note over CC,SRV: ③ 变更通知
    SRV-->>CC: notifications/resources/list_changed
    CC->>SRV: resources/list(重新拉取)

何时选 Resource、何时选 Tool,用下面这张决策图收口:

flowchart TD
    A[要给模型的东西] --> B{是动作还是数据?}
    B-- 动作/有副作用 --> T[Tool 工具]
    B-- 数据/只读 --> C{由谁决定读不读?}
    C-- 模型自主调用 --> T
    C-- 用户/应用显式喂入 --> R[Resource 资源]
    R --> D{数据是唯一的还是按参数取?}
    D-- 唯一 --> R1[静态资源 kb://manifest]
    D-- 按参数 --> R2[资源模板 kb://guide/{topic}]

九、安全与最佳实践

  1. 路径穿越是头号风险。任何把 URI 参数映射到文件系统的资源模板,都必须做 resolve() + 前缀校验,并拒绝 .. 与绝对路径。上面的 guide() 就是模板写法。
  2. 尊重只读语义。不要在资源函数里做写操作。要写,就开一个 Tool——语义干净,授权也清晰。
  3. 控制体量。单次 read 别返回超长内容;超大文件应按段切片成多个 URI(如 kb://log/2024-06-24/p1)。
  4. 二进制用 bytes。返回 bytes 会被 base64 成 BlobResourceContents,记得显式给 mime_type(如 image/png)。
  5. 幂等优先。指标类资源即使每次值不同,读取行为本身也不应有副作用——这正是 idempotentHint 的本意。

十、延伸阅读


十一、训练营适用性评估

维度 评估
适合训练营 MCP 进阶营、企业 AI 工程营、内部知识库自动化营
学员基础 已能独立写出至少一个 MCP Tool
教学时长 讲授 45 分钟 + 动手 60 分钟 + 联调 30 分钟,合计约 2.5 小时
验收标准 学员独立交付一个含 1 个静态资源 + 2 个资源模板的 KB 服务器,并通过 Inspector 与 Claude Code @ 引用双重验证
延伸作业 给资源加上 subscribe 通知;把指标数据源从 JSON 换成真实数据库只读视图

参考来源

  1. Resources — Model Context Protocol Specification 2025-06-18 — 官方资源原语规范,含能力声明、四类协议消息与消息流时序图。
  2. Resources & Templates — FastMCP 官方文档 — @mcp.resource 装饰器、URI 模板参数、返回类型与注解的权威 API 参考。
  3. MCP Resource Templates Deep Dive — ChatForest — 静态资源与参数化模板的对比实战,覆盖 RFC 6570 与通配参数。
  4. RFC 6570: URI Template — URI 模板标准,{name} 与 {+path} 保留展开的语法定义。
  5. Resources — MCP C# SDK — 跨语言实现对照,用于验证资源原语在不同 SDK 中的一致性。

[!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学通信知识星球海报-扫码加入