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/profile、user://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:HTMLimage/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 后,用户可以:
- 在对话中点 "📎" 附加资源
- 选择某个文档作为上下文
- 询问 "根据这份文档,怎么部署?"
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 务必验证路径,防止穿越攻击:
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 边界清晰