第7章 资源(Resources)开发
前言
如果说工具是MCP的"动词",那么资源就是"名词"。本章讲解如何设计和管理持久化的数据资源,让LLM能够安全地访问和订阅各类信息源。
7.1 资源的定义与设计
7.1.1 资源的本质
graph TB
A["资源 Resource"] --> B["数据特性"]
A --> C["访问特性"]
A --> D["生命周期"]
B --> B1["持久化"]
B --> B2["可寻址"]
B --> B3["版本化"]
C --> C1["可读取"]
C --> C2["可列表"]
C --> C3["可订阅"]
D --> D1["创建"]
D --> D2["更新"]
D --> D3["删除"]
E["工具 vs 资源"] --> E1["工具:动作、计算、转换"]
E --> E2["资源:数据、信息、状态"]
资源的核心特征:
| 特征 | 说明 | 例子 |
|---|---|---|
| 持久化 | 长期存在的数据 | 数据库记录、文件内容 |
| 可寻址 | 唯一的URI标识 | resources://db/customers/001 |
| 可列表 | 支持枚举和搜索 | 列出所有客户资源 |
| 可订阅 | 支持变化推送 | 客户信息更新时通知 |
| 版本化 | 追踪版本历史 | 记录修改时间和版本号 |
7.1.2 资源URI设计
graph TB
A["资源URI构造"] --> B["Scheme"]
A --> C["Authority"]
A --> D["Path"]
A --> E["Query"]
B --> B1["resources://"]
B --> B1A["MCP标准协议标记"]
C --> C1["Domain/Service"]
C --> C1A["database、filesystem等"]
D --> D1["Hierarchical Path"]
D --> D1A["/table/record/field"]
E --> E1["Filter/Options"]
E --> E1A["?version=2&format=json"]
URI设计规范:
resources://authority/path?query=value
示例:
┌─ 数据库资源
│ resources://postgres/public/customers/cust_001
│ resources://mysql/orders/2025-01
│
├─ 文件系统资源
│ resources://filesystem/home/user/documents/report.pdf
│ resources://filesystem/project/src?type=python
│
├─ API资源
│ resources://github/anthropics/mcp/issues?state=open
│
└─ 动态资源
resources://cache/sessions/session_xyz
resources://computed/analytics/sales_report?month=2025-10
URI最佳实践:
from urllib.parse import urlparse, parse_qs
class ResourceURI:
"""资源URI解析器"""
def __init__(self, uri: str):
self.uri = uri
self.parsed = urlparse(uri)
@property
def scheme(self) -> str:
"""获取协议"""
return self.parsed.scheme # resources
@property
def authority(self) -> str:
"""获取权限(服务标识)"""
return self.parsed.netloc # database, filesystem, etc
@property
def path_parts(self) -> list:
"""获取路径组件"""
return [p for p in self.parsed.path.split('/') if p]
@property
def query_params(self) -> dict:
"""获取查询参数"""
return parse_qs(self.parsed.query)
def validate(self) -> bool:
"""验证URI格式"""
if self.scheme != "resources":
raise ValueError(f"Invalid scheme: {self.scheme}")
if not self.authority:
raise ValueError("Missing authority in URI")
if not self.path_parts:
raise ValueError("Empty path in URI")
return True
# 使用示例
uri = ResourceURI("resources://db/customers/cust_001?version=2")
print(uri.authority) # db
print(uri.path_parts) # ['customers', 'cust_001']
print(uri.query_params) # {'version': ['2']}
7.1.3 资源的完整定义
{
"uri": "resources://database/customers/cust_001",
"name": "Customer Profile",
"mimeType": "application/json",
"description": "客户信息资源,包含基本信息、联系方式、消费记录等。支持版本控制和变更订阅。",
"metadata": {
"created": "2025-01-01T00:00:00Z",
"updated": "2025-10-20T12:30:00Z",
"version": "2",
"author": "system",
"size": 2048
},
"schema": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"created_at": { "type": "string", "format": "date-time" },
"orders": {
"type": "array",
"items": { "type": "object" }
}
}
},
"changeNotificationCapable": true,
"contentNotificationCapable": true
}
7.2 资源管理最佳实践
7.2.1 资源生命周期
sequenceDiagram
participant Client
participant Server
participant Storage
Client->>Server: 1. 列表资源
Server->>Storage: 查询
Storage-->>Server: 返回资源列表
Server-->>Client: 资源URIs
Client->>Server: 2. 读取资源
Server->>Storage: 获取内容
Storage-->>Server: 资源数据
Server-->>Client: 资源内容
Client->>Server: 3. 订阅变更
Server->>Server: 注册监听
Storage->>Storage: 资源更新
Server-->>Client: 推送通知
Client->>Server: 4. 取消订阅
Server->>Server: 清理监听
7.2.2 资源发现与列表
实现资源列表接口 (Python):
from typing import List, Dict, Optional
from dataclasses import dataclass
from datetime import datetime
@dataclass
class ResourceInfo:
"""资源信息"""
uri: str
name: str
mime_type: str
description: str
created_at: datetime
updated_at: datetime
version: str
size: int
class ResourceManager:
"""资源管理器"""
def __init__(self):
self.resources: Dict[str, ResourceInfo] = {}
self.subscriptions: Dict[str, List[str]] = {}
async def list_resources(self, pattern: str = None) -> List[ResourceInfo]:
"""
列出资源
Args:
pattern: 资源URI模式(支持通配符),如 "resources://db/customers/*"
Returns:
资源列表
"""
result = []
for uri, info in self.resources.items():
# 支持简单的模式匹配
if pattern and not self._matches_pattern(uri, pattern):
continue
result.append(info)
return result
def _matches_pattern(self, uri: str, pattern: str) -> bool:
"""URI模式匹配"""
import fnmatch
return fnmatch.fnmatch(uri, pattern)
async def read_resource(self, uri: str) -> str:
"""
读取资源内容
Args:
uri: 资源URI
Returns:
资源内容
Raises:
ResourceNotFoundError: 资源不存在
"""
if uri not in self.resources:
raise ResourceNotFoundError(f"Resource not found: {uri}")
# 实际应用中从存储系统读取
return await self._fetch_content(uri)
async def _fetch_content(self, uri: str) -> str:
"""从存储系统获取内容"""
# 模拟获取内容
return f"Content of {uri}"
def register_resource(self, resource: ResourceInfo):
"""注册资源"""
self.resources[resource.uri] = resource
class ResourceNotFoundError(Exception):
"""资源未找到错误"""
pass
7.2.3 版本控制
资源版本管理:
from typing import Optional
from datetime import datetime
class ResourceVersion:
"""资源版本"""
def __init__(self, version: str, content: str, timestamp: datetime = None):
self.version = version
self.content = content
self.timestamp = timestamp or datetime.now()
self.checksum = self._calculate_checksum(content)
def _calculate_checksum(self, content: str) -> str:
"""计算内容校验和"""
import hashlib
return hashlib.sha256(content.encode()).hexdigest()
class VersionedResourceManager:
"""版本化资源管理器"""
def __init__(self):
self.versions: Dict[str, List[ResourceVersion]] = {}
def add_version(self, uri: str, content: str):
"""添加新版本"""
if uri not in self.versions:
self.versions[uri] = []
# 自动生成版本号
version_num = len(self.versions[uri]) + 1
version = ResourceVersion(str(version_num), content)
self.versions[uri].append(version)
logger.info(f"Added version {version_num} to {uri}")
def get_version(self, uri: str, version: Optional[str] = None) -> ResourceVersion:
"""
获取特定版本
Args:
uri: 资源URI
version: 版本号(None则返回最新)
Returns:
资源版本
"""
if uri not in self.versions:
raise ResourceNotFoundError(f"Resource {uri} not found")
versions = self.versions[uri]
if version is None:
# 返回最新版本
return versions[-1]
# 查找特定版本
for v in versions:
if v.version == version:
return v
raise ValueError(f"Version {version} not found for {uri}")
def get_version_history(self, uri: str) -> List[Dict]:
"""获取版本历史"""
if uri not in self.versions:
return []
return [
{
"version": v.version,
"timestamp": v.timestamp.isoformat(),
"checksum": v.checksum
}
for v in self.versions[uri]
]
def compare_versions(self, uri: str, v1: str, v2: str) -> Dict:
"""比较两个版本"""
ver1 = self.get_version(uri, v1)
ver2 = self.get_version(uri, v2)
return {
"uri": uri,
"version1": v1,
"version2": v2,
"version1_checksum": ver1.checksum,
"version2_checksum": ver2.checksum,
"changed": ver1.checksum != ver2.checksum,
"version1_size": len(ver1.content),
"version2_size": len(ver2.content)
}
7.2.4 变更通知
graph TB
A["资源变更通知系统"] --> B["监听层"]
A --> C["事件层"]
A --> D["分发层"]
B --> B1["订阅管理"]
B --> B2["变更检测"]
B --> B3["通知触发"]
C --> C1["创建事件"]
C --> C2["更新事件"]
C --> C3["删除事件"]
D --> D1["推送到客户端"]
D --> D2["事件日志"]
D --> D3["重试机制"]
变更通知实现 (Python):
from enum import Enum
from asyncio import Queue
import asyncio
class ResourceChangeType(Enum):
"""资源变更类型"""
CREATED = "created"
UPDATED = "updated"
DELETED = "deleted"
RENAMED = "renamed"
class ResourceChangeEvent:
"""资源变更事件"""
def __init__(self, uri: str, change_type: ResourceChangeType,
metadata: Dict = None):
self.uri = uri
self.change_type = change_type
self.metadata = metadata or {}
self.timestamp = datetime.now()
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"uri": self.uri,
"type": self.change_type.value,
"timestamp": self.timestamp.isoformat(),
"metadata": self.metadata
}
class SubscriptionManager:
"""订阅管理器"""
def __init__(self):
self.subscriptions: Dict[str, List[Queue]] = {}
async def subscribe(self, uri_pattern: str) -> Queue:
"""
订阅资源变更
Args:
uri_pattern: 资源URI模式
Returns:
事件队列
"""
queue = Queue()
if uri_pattern not in self.subscriptions:
self.subscriptions[uri_pattern] = []
self.subscriptions[uri_pattern].append(queue)
logger.info(f"New subscription to {uri_pattern}")
return queue
def unsubscribe(self, uri_pattern: str, queue: Queue):
"""取消订阅"""
if uri_pattern in self.subscriptions:
try:
self.subscriptions[uri_pattern].remove(queue)
except ValueError:
pass
async def notify_change(self, event: ResourceChangeEvent):
"""
通知资源变更
Args:
event: 变更事件
"""
# 遍历所有订阅,检查URI是否匹配
for pattern, queues in self.subscriptions.items():
if self._matches_pattern(event.uri, pattern):
for queue in queues:
await queue.put(event.to_dict())
logger.info(f"Notified {len(queues)} subscribers about {event.uri}")
def _matches_pattern(self, uri: str, pattern: str) -> bool:
"""URI模式匹配"""
import fnmatch
return fnmatch.fnmatch(uri, pattern)
# 使用示例
async def resource_change_listener():
"""监听资源变更示例"""
manager = SubscriptionManager()
# 订阅所有客户资源的变更
queue = await manager.subscribe("resources://db/customers/*")
# 模拟资源更新
event = ResourceChangeEvent(
"resources://db/customers/cust_001",
ResourceChangeType.UPDATED,
{"updated_fields": ["email", "phone"]}
)
await manager.notify_change(event)
# 接收事件
change = await queue.get()
print(f"Resource changed: {change}")
7.3 常见资源实现
7.3.1 文件系统资源
import os
from pathlib import Path
from typing import Optional, List, Dict
class FileSystemResource:
"""文件系统资源"""
def __init__(self, root_path: str):
self.root = Path(root_path)
def uri_to_path(self, uri: str) -> Path:
"""从URI获取文件路径"""
# resources://filesystem/documents/report.pdf -> /root/documents/report.pdf
# 解析URI
from urllib.parse import urlparse
parsed = urlparse(uri)
# 提取路径部分(跳过authority)
rel_path = parsed.path
# 构建完整路径
full_path = (self.root / rel_path).resolve()
# 安全检查:防止路径遍历攻击
if not str(full_path).startswith(str(self.root.resolve())):
raise PermissionError(f"Path traversal attempt: {uri}")
return full_path
async def list_files(self, pattern: str = None) -> List[Dict]:
"""列出文件资源"""
results = []
for file_path in self.root.rglob("*"):
if file_path.is_file():
rel_path = file_path.relative_to(self.root)
uri = f"resources://filesystem/{rel_path}"
# 模式匹配
if pattern and not self._matches_pattern(str(rel_path), pattern):
continue
results.append({
"uri": uri,
"name": file_path.name,
"size": file_path.stat().st_size,
"modified": file_path.stat().st_mtime,
"type": "file"
})
return results
async def read_file(self, uri: str) -> str:
"""读取文件内容"""
file_path = self.uri_to_path(uri)
if not file_path.exists():
raise FileNotFoundError(f"File not found: {uri}")
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def _matches_pattern(self, path: str, pattern: str) -> bool:
"""模式匹配"""
import fnmatch
return fnmatch.fnmatch(path, pattern)
7.3.2 数据库资源
import sqlite3
from typing import List, Dict
class DatabaseResource:
"""数据库资源"""
def __init__(self, db_path: str):
self.db_path = db_path
self.conn = sqlite3.connect(db_path)
self.conn.row_factory = sqlite3.Row
async def list_tables(self) -> List[Dict]:
"""列出表资源"""
cursor = self.conn.cursor()
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
tables = []
for row in cursor.fetchall():
table_name = row[0]
# 获取表信息
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
row_count = cursor.fetchone()[0]
uri = f"resources://database/{table_name}"
tables.append({
"uri": uri,
"name": table_name,
"type": "table",
"rows": row_count,
"description": f"Table {table_name} with {row_count} rows"
})
return tables
async def list_records(self, table: str, limit: int = 100) -> List[Dict]:
"""列出表记录资源"""
cursor = self.conn.cursor()
try:
cursor.execute(f"SELECT * FROM {table} LIMIT ?", (limit,))
columns = [description[0] for description in cursor.description]
records = []
for row_idx, row in enumerate(cursor.fetchall()):
record_data = dict(zip(columns, row))
# 为每条记录生成URI
if 'id' in record_data:
uri = f"resources://database/{table}/{record_data['id']}"
else:
uri = f"resources://database/{table}/row_{row_idx}"
records.append({
"uri": uri,
"data": record_data,
"type": "record"
})
return records
except Exception as e:
raise PermissionError(f"Cannot query table {table}: {e}")
async def read_record(self, uri: str) -> Dict:
"""
读取单条记录
Args:
uri: 如 resources://database/customers/cust_001
Returns:
记录数据
"""
# 解析URI
parts = uri.replace("resources://database/", "").split("/")
if len(parts) < 2:
raise ValueError(f"Invalid URI: {uri}")
table = parts[0]
record_id = parts[1]
# 安全性检查:验证表名
cursor = self.conn.cursor()
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
(table,)
)
if not cursor.fetchone():
raise PermissionError(f"Table not found: {table}")
# 查询记录
cursor.execute(f"SELECT * FROM {table} WHERE id = ?", (record_id,))
row = cursor.fetchone()
if not row:
raise ResourceNotFoundError(f"Record not found: {uri}")
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
7.3.3 动态生成的资源
class DynamicResource:
"""动态生成的资源"""
async def generate_report(self, report_type: str, params: Dict) -> Dict:
"""
动态生成报告资源
Args:
report_type: 报告类型(sales, marketing, etc)
params: 报告参数
Returns:
生成的报告
"""
if report_type == "sales":
return await self._generate_sales_report(params)
elif report_type == "marketing":
return await self._generate_marketing_report(params)
else:
raise ValueError(f"Unknown report type: {report_type}")
async def _generate_sales_report(self, params: Dict) -> Dict:
"""生成销售报告"""
month = params.get("month", "2025-10")
# 模拟报告生成
return {
"uri": f"resources://computed/reports/sales_{month}",
"type": "report",
"data": {
"month": month,
"total_sales": 500000,
"order_count": 1200,
"average_order": 416.67,
"top_products": ["Product A", "Product B", "Product C"]
}
}
async def _generate_marketing_report(self, params: Dict) -> Dict:
"""生成营销报告"""
# 类似实现
pass
# 使用示例
async def dynamic_resource_example():
"""动态资源使用示例"""
resource = DynamicResource()
# 动态生成报告
report = await resource.generate_report(
"sales",
{"month": "2025-10"}
)
print(report)
# 输出:
# {
# "uri": "resources://computed/reports/sales_2025-10",
# "type": "report",
# "data": {...}
# }
7.3.4 关系型资源
class RelationalResource:
"""支持关系的资源"""
def __init__(self):
self.relations: Dict[str, List[str]] = {}
def link_resources(self, source_uri: str, target_uri: str,
relation_type: str = "related"):
"""
建立资源间的关系
Args:
source_uri: 源资源URI
target_uri: 目标资源URI
relation_type: 关系类型(related, parent, child等)
"""
key = f"{source_uri}_{relation_type}"
if key not in self.relations:
self.relations[key] = []
self.relations[key].append(target_uri)
logger.info(f"Linked {source_uri} -> {target_uri} ({relation_type})")
def get_related_resources(self, uri: str, relation_type: str = "related") -> List[str]:
"""获取相关资源"""
key = f"{uri}_{relation_type}"
return self.relations.get(key, [])
def get_resource_graph(self, uri: str, depth: int = 2) -> Dict:
"""获取资源关系图"""
graph = {
"root": uri,
"relations": {}
}
def _traverse(current_uri: str, current_depth: int):
if current_depth >= depth:
return
related = self.get_related_resources(current_uri)
if related:
graph["relations"][current_uri] = related
for related_uri in related:
_traverse(related_uri, current_depth + 1)
_traverse(uri, 0)
return graph
# 使用示例
async def relational_resource_example():
"""关系型资源使用示例"""
resource = RelationalResource()
# 建立关系
resource.link_resources(
"resources://db/customers/cust_001",
"resources://db/orders/ord_001",
relation_type="has_order"
)
# 获取关系图
graph = resource.get_resource_graph("resources://db/customers/cust_001")
print(graph)
本章总结
| 核心概念 | 关键点 |
|---|---|
| 资源定义 | 持久化、可寻址、版本化、可订阅 |
| URI设计 | 标准格式:resources://authority/path?query |
| 生命周期 | 列表→读取→订阅→接收→取消 |
| 版本控制 | 自动版本号、校验和、版本比较 |
| 变更通知 | 事件驱动、模式匹配、异步推送 |
| 文件资源 | 路径映射、访问控制、模式匹配 |
| 数据库资源 | 表映射、记录访问、SQL安全 |
| 动态资源 | 参数生成、类型多样 |
| 关系资源 | 资源链接、关系图谱 |
常见问题
Q1: 资源和工具有什么本质区别? A: 资源是持久化数据(名词),工具是动作操作(动词)。资源通常只读或少更新,工具用于计算和转换。
Q2: 如何处理大型资源的流式读取? A: 支持分页或Range请求,返回部分内容而不是一次性加载全部。
Q3: 如何确保资源访问的安全性? A: 在URI解析时进行路径验证(防止遍历)、实施权限检查、记录访问日志。
Q4: 资源版本控制应该保留多久的历史? A: 建议根据使用场景设置:关键资源保留所有版本,临时资源可以定期清理。
Q5: 支持多客户端并发订阅同一资源吗? A: 是的。使用发布-订阅模式,一个资源变更可以通知多个订阅者。
实战要点
✅ 推荐做法
- 使用标准的URI设计
- 实施版本控制
- 支持资源变更订阅
- 添加访问日志
- 提供搜索和过滤功能
- 优化大型资源的访问性能
❌ 避免的做法
- 不要直接暴露内部路径
- 不要忽视安全检查
- 不要让资源订阅无限增长
- 不要频繁变更资源结构
- 不要支持所有资源类型(定义清晰的支持列表)
延伸阅读
- 资源设计模式:spec.modelcontextprotocol.io/resources
- RESTful设计原理:restfulapi.net/
- 版本控制最佳实践:semver.org/
- 发布-订阅模式:en.wikipedia.org/wiki/Publis…
下一章预告:第8章将讲述如何开发MCP提示模板(Prompts)——包括模板设计、参数化、动态生成等高级主题。