第7章 资源(Resources)开发

72 阅读10分钟

第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设计
  • 实施版本控制
  • 支持资源变更订阅
  • 添加访问日志
  • 提供搜索和过滤功能
  • 优化大型资源的访问性能

❌ 避免的做法

  • 不要直接暴露内部路径
  • 不要忽视安全检查
  • 不要让资源订阅无限增长
  • 不要频繁变更资源结构
  • 不要支持所有资源类型(定义清晰的支持列表)

延伸阅读


下一章预告:第8章将讲述如何开发MCP提示模板(Prompts)——包括模板设计、参数化、动态生成等高级主题。