跳转至

MCP构建Resources

Resources 是 MCP 中另一个核心原语,用于向 LLM 提供只读上下文数据。本篇详细讲怎么设计和构建 Resources。

1. Resources 与 Tools 的本质区别

容易混淆的一点:很多人觉得 Resources 也是"返回数据",那和 Tools 有什么区别?

维度 Tools Resources
调用方 模型自动决定 用户/客户端显式选择
副作用 可有 必须无(只读)
类比 HTTP POST GET
类比文件 函数 文件
典型用法 执行操作 提供背景知识

简单理解:

  • 想让模型"做某件事" → Tool
  • 想给模型"看某些资料" → Resource

2. 基础 Resource

最简单的 Resource:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("resources-demo")

@mcp.resource("config://app")
def get_app_config() -> str:
    """获取应用配置"""
    return """{
  "name": "MyApp",
  "version": "1.0.0",
  "environment": "production"
}"""

URI config://app 是这个 Resource 的唯一标识,scheme(config://)可以自定义。

3. URI 设计

Resource 的 URI 用于让客户端引用具体某个资源。常见的 scheme 设计:

Scheme 用途 示例
file:// 本地文件 file:///project/README.md
http:// / https:// 网络资源 https://api.example.com/data
db:// 数据库 db://mydb/users/123
自定义 业务概念 wiki://team/onboarding

URI 设计原则:

  • 唯一:不同资源有不同 URI
  • 可读:人能看懂 URI 含义
  • 稳定:同一资源 URI 不变

4. 静态 Resource vs 动态 Resource

静态 Resource

URI 是固定的,调用时返回内容(可能是从文件、API 实时读取):

@mcp.resource("docs://readme")
def get_readme() -> str:
    """项目 README 文档"""
    with open("README.md", "r", encoding="utf-8") as f:
        return f.read()

动态 Resource(资源模板)

URI 包含占位符 {},可以根据参数生成不同内容:

@mcp.resource("user://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
    """获取用户档案"""
    user = db.find_user(user_id)
    if not user:
        raise ValueError(f"用户 {user_id} 不存在")
    return f"姓名: {user.name}\n邮箱: {user.email}"

客户端可以通过 user://123/profileuser://456/profile 等 URI 访问不同用户。

5. 返回不同 MIME 类型

Resource 可以指定 MIME 类型,便于客户端正确处理:

@mcp.resource("logs://app", mime_type="text/plain")
def get_logs() -> str:
    """应用日志"""
    return open("/var/log/app.log").read()

@mcp.resource("data://users", mime_type="application/json")
def get_users() -> str:
    """用户数据 JSON"""
    import json
    return json.dumps([
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"},
    ])

@mcp.resource("docs://manual", mime_type="text/markdown")
def get_manual() -> str:
    """用户手册(Markdown)"""
    return "# 用户手册\n\n..."

常见 MIME 类型:

  • text/plain:纯文本
  • text/markdown:Markdown 文档
  • application/json:JSON 数据
  • text/html:HTML
  • image/png:PNG 图片(需要返回 bytes)

6. 返回二进制内容

对于图片、PDF 等二进制资源,函数返回 bytes

@mcp.resource("image://logo", mime_type="image/png")
def get_logo() -> bytes:
    """公司 logo"""
    with open("logo.png", "rb") as f:
        return f.read()

MCP SDK 会自动 base64 编码后传输。

7. 资源列表(list 接口)

客户端调用 resources/list 时,会得到所有静态 Resource 的清单。但资源模板(带 {} 的)默认不会出现在列表里,因为参数无穷多。

可以手动列举常用的实例:

from mcp.server.fastmcp.resources import FunctionResource

# 静态注册一些重要的实例
@mcp.list_resources()
def list_my_resources():
    return [
        {
            "uri": "user://admin/profile",
            "name": "管理员档案",
            "mimeType": "text/plain",
        },
        {
            "uri": "user://guest/profile",
            "name": "访客档案",
            "mimeType": "text/plain",
        },
    ]

这样客户端能"知道"有哪些常用资源,方便用户选择。

8. 异步 Resource

涉及 I/O 操作时用异步:

import httpx

@mcp.resource("stock://{symbol}")
async def get_stock_price(symbol: str) -> str:
    """获取股票最新价格"""
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.stock.com/{symbol}")
        data = response.json()
        return f"{symbol}: ${data['price']} ({data['change']}%)"

9. 真实场景示例:项目知识库

构建一个简单的项目知识库 MCP Server,展示 Resource 的实际用法:

import os
from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("project-kb")

DOCS_DIR = Path("/Users/me/projects/myapp/docs")

# === 列出所有文档 ===
@mcp.list_resources()
def list_docs():
    docs = []
    for md_file in DOCS_DIR.rglob("*.md"):
        rel_path = md_file.relative_to(DOCS_DIR)
        docs.append({
            "uri": f"docs://{rel_path}",
            "name": str(rel_path),
            "mimeType": "text/markdown",
        })
    return docs

# === 读取单个文档 ===
@mcp.resource("docs://{path}")
def read_doc(path: str) -> str:
    """读取项目文档"""
    file_path = DOCS_DIR / path
    if not file_path.exists():
        raise ValueError(f"文档 {path} 不存在")
    if not str(file_path.resolve()).startswith(str(DOCS_DIR.resolve())):
        raise PermissionError("不允许访问目录之外的文件")
    return file_path.read_text(encoding="utf-8")

# === 提供项目元信息 ===
@mcp.resource("project://info", mime_type="application/json")
def get_project_info() -> str:
    import json
    return json.dumps({
        "name": "MyApp",
        "language": "Python",
        "framework": "FastAPI",
        "description": "示例项目",
    }, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    mcp.run()

接入 Claude Desktop 后,用户可以:

  1. 在对话中点 "📎" 附加资源
  2. 选择某个文档作为上下文
  3. 询问 "根据这份文档,怎么部署?"

10. 在 Tool 中读取 Resource

Tool 也可以反过来读取 Resource,通过 Context

from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("demo")

@mcp.resource("config://app")
def get_config() -> str:
    return '{"version": "1.0"}'

@mcp.tool()
async def show_version(ctx: Context) -> str:
    """显示当前版本"""
    config_data, _ = await ctx.read_resource("config://app")
    import json
    config = json.loads(config_data)
    return f"当前版本: {config['version']}"

这种模式下,Resource 充当配置/数据源,Tool 是基于这些数据执行操作的入口。

11. Resource 的客户端使用方式

不同客户端使用 Resource 的方式不同:

  • Claude Desktop:用户点 📎 选择资源附加到对话
  • Cursor / Cline:可以通过 @ 引用资源
  • 自研客户端:完全由开发者决定何时加载

这与 Tool 的差异:Tool 是模型自己决定何时调用,Resource 是用户/客户端决定何时加载。

12. 设计建议

1. Resource URI 要稳定

URI 是客户端引用资源的"地址",要保证一段时间内不变。否则用户保存的引用会失效。

2. 模板参数要有限

资源模板的参数空间不要太大,否则模型不知道用什么值调用。可以在描述中提示有效值范围。

3. 大资源要分页

单个 Resource 不要返回数 MB 的内容,会撑爆 LLM 上下文。考虑:

  • 拆分成多个小 Resource
  • 提供 read_doc(path, start_line, end_line) 这样的 Tool 配合

4. 安全:路径验证

涉及文件系统的 Resource 务必验证路径,防止穿越攻击:

if not str(file_path.resolve()).startswith(str(BASE_DIR.resolve())):
    raise PermissionError("非法路径")

5. 区分 Tool 和 Resource

如果一个能力既能做 Tool 又能做 Resource,问自己:用户需要"主动选择"它吗?

  • 是 → Resource(如选择某份文档)
  • 否 → Tool(如自动搜索文档)

总结

  • Resource 是只读上下文,由用户/客户端选择加载,不是模型自动调用
  • URI 设计要唯一、可读、稳定,可用自定义 scheme
  • 静态 Resource URI 固定,资源模板支持 {param} 占位符
  • mime_type 标注内容类型,二进制返回 bytes
  • @mcp.list_resources() 自定义资源列表,方便客户端展示
  • Tool 可以通过 ctx.read_resource() 反过来读取 Resource
  • 设计原则:URI 稳定、限制大小、路径安全、与 Tool 边界清晰

评论