2.1_资源深度解析

2.1 资源(Resources)深度解析

资源(Resources)是MCP的核心概念之一,它们提供了一种标准化的方式来为大语言模型提供上下文信息。在本章中,我们将深入探讨资源的设计、实现和最佳实践。

资源类型和URI模式

MCP中的资源通过URI(统一资源标识符)进行标识和访问。资源URI通常采用以下格式:

scheme://path/to/resource

常见的资源方案(schemes)

以下是一些常见的资源方案:

  1. file://: 访问文件系统中的文件
  2. data://: 访问内存或数据库中的数据
  3. http(s)://: 访问通过HTTP(S)协议可获取的资源
  4. db://: 访问数据库中的内容
  5. user://: 访问用户相关的信息
  6. doc://: 访问文档或知识库
  7. config://: 访问配置信息
  8. custom://: 自定义方案,用于特定应用场景

方案名称可以是任何有效标识符,但建议使用能清晰表达资源性质的方案名称。

URI路径部分

URI的路径部分可以包含:

  • 静态路径:file:///home/user/document.txt
  • 分层路径:doc://knowledge/science/physics
  • 查询参数:data://search?query=example&limit=10

创建静态和动态资源

在MCP中,资源可以是静态的(内容固定)或动态的(内容根据请求生成)。

静态资源

静态资源的内容在服务器启动时确定,不会随请求变化。

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("静态资源示例")

@mcp.resource("static://greeting")
def get_greeting() -> str:
    """返回一个静态问候语"""
    return "Hello, world!"

@mcp.resource("static://version")
def get_version() -> str:
    """返回应用版本信息"""
    return "v1.0.0"

动态资源

动态资源的内容会根据请求参数生成。

import datetime

@mcp.resource("dynamic://time")
def get_current_time() -> str:
    """返回当前时间"""
    return datetime.datetime.now().isoformat()

@mcp.resource("weather://{city}")
def get_weather(city: str) -> str:
    """返回指定城市的天气信息"""
    # 在实际应用中,可能会调用外部API
    weather_data = {
        "北京": "晴天,25°C",
        "上海": "多云,28°C",
        "广州": "小雨,30°C"
    }
    return weather_data.get(city, f"没有{city}的天气信息")

参数化资源路径

参数化资源路径允许通过URI传递参数,这对构建灵活的资源API非常有用。

基本参数化

最简单的参数化使用花括号{}来定义参数:

@mcp.resource("user://{user_id}")
def get_user_profile(user_id: str) -> str:
    """获取用户资料"""
    # 从数据库或其他存储中获取用户信息
    return f"用户{user_id}的资料..."

@mcp.resource("doc://{category}/{document_id}")
def get_document(category: str, document_id: str) -> str:
    """获取指定类别和ID的文档"""
    return f"{category}类别中ID为{document_id}的文档内容..."

类型化参数

参数可以具有特定类型:

@mcp.resource("product://{product_id:int}")
def get_product(product_id: int) -> str:
    """获取产品信息"""
    return f"产品{product_id}的详细信息..."

@mcp.resource("report://{year:int}/{month:int}")
def get_monthly_report(year: int, month: int) -> str:
    """获取指定月份的报告"""
    return f"{year}{month}月的月度报告..."

可选参数和默认值

资源路径也可以包含可选参数:

@mcp.resource("search://{query}/{page:int?}")
def search_results(query: str, page: int = 1) -> str:
    """搜索结果,带可选页码参数"""
    return f"搜索'{query}'的结果,第{page}页"

资源内容类型和MIME类型

MCP资源可以返回不同类型的内容,通过MIME类型指定。

文本内容

文本是最常见的资源内容类型:

@mcp.resource("text://example")
def get_text() -> str:
    """返回纯文本内容"""
    return "这是一段纯文本内容"

默认情况下,字符串返回值的MIME类型为text/plain

结构化数据

对于结构化数据,可以返回字典或列表,它们将被自动转换为JSON:

@mcp.resource("data://users")
def get_users() -> dict:
    """返回用户列表(JSON格式)"""
    return {
        "users": [
            {"id": 1, "name": "张三"},
            {"id": 2, "name": "李四"},
            {"id": 3, "name": "王五"}
        ]
    }

返回的字典或列表会被序列化为JSON,MIME类型为application/json

二进制数据

对于二进制数据,可以使用bytes类型:

@mcp.resource("image://logo")
def get_logo() -> bytes:
    """返回logo图片数据"""
    with open("logo.png", "rb") as f:
        return f.read()

二进制数据的默认MIME类型为application/octet-stream

自定义MIME类型

可以通过注解或额外返回值指定MIME类型:

@mcp.resource("custom://data")
def get_custom_data() -> tuple:
    """返回自定义MIME类型的数据"""
    data = "<svg>...</svg>"
    mime_type = "image/svg+xml"
    return data, mime_type

资源错误处理

资源处理过程中可能会遇到各种错误,MCP提供了标准化的错误处理机制。

捕获和返回错误

最简单的方式是在资源函数中捕获异常并返回适当的错误信息:

@mcp.resource("error://demo/{id}")
def get_with_error_handling(id: str) -> str:
    """演示错误处理的资源"""
    try:
        # 可能引发异常的代码
        if id == "invalid":
            raise ValueError("无效的ID")
        
        # 正常处理...
        return f"资源ID {id} 的内容"
    
    except ValueError as e:
        return f"错误: {e}"
    except Exception as e:
        return f"未知错误: {e}"

使用专用异常类

MCP提供了专用的异常类,用于表示不同类型的资源错误:

from mcp.errors import ResourceNotFoundError, ResourcePermissionError

@mcp.resource("protected://{resource_id}")
def get_protected_resource(resource_id: str) -> str:
    """获取受保护的资源"""
    
    # 检查资源是否存在
    if resource_id not in available_resources:
        raise ResourceNotFoundError(f"资源{resource_id}不存在")
    
    # 检查权限
    if not has_permission(resource_id):
        raise ResourcePermissionError(f"没有访问资源{resource_id}的权限")
    
    # 正常处理...
    return available_resources[resource_id]

状态码和错误元数据

对于更复杂的错误处理,可以返回包含状态码和错误详情的结构:

from mcp.types import ErrorContent

@mcp.resource("advanced://error")
def get_with_advanced_error() -> any:
    """演示高级错误处理"""
    try:
        # 业务逻辑...
        raise Exception("示例错误")
    
    except Exception as e:
        error = ErrorContent(
            status_code=500,  # HTTP风格的状态码
            reason="Internal Server Error",
            detail=str(e),
            help_text="请联系管理员解决此问题"
        )
        return error

进阶示例:文件系统浏览器

下面是一个更复杂的实例,实现一个文件系统浏览器,展示了如何将上述概念应用到实际场景:

import os
import json
from typing import Dict, List, Optional
from mcp.server.fastmcp import FastMCP
from mcp.errors import ResourceNotFoundError

mcp = FastMCP("文件系统浏览器")

# 定义基础目录,出于安全考虑限制访问范围
BASE_DIR = "/path/to/allowed/directory"

@mcp.resource("fs://list/{path:?}")
def list_directory(path: str = "") -> str:
    """列出目录内容
    
    参数:
        path: 相对于基础目录的路径,默认为根目录
    
    返回:
        目录内容的JSON格式字符串
    """
    full_path = os.path.normpath(os.path.join(BASE_DIR, path))
    
    # 安全检查:确保路径仍在BASE_DIR内
    if not full_path.startswith(BASE_DIR):
        raise ResourcePermissionError("无权访问该路径")
    
    if not os.path.exists(full_path):
        raise ResourceNotFoundError(f"路径不存在: {path}")
    
    if not os.path.isdir(full_path):
        raise ValueError(f"路径不是目录: {path}")
    
    # 获取目录内容
    entries = []
    with os.scandir(full_path) as it:
        for entry in it:
            entry_info = {
                "name": entry.name,
                "path": os.path.join(path, entry.name),
                "is_dir": entry.is_dir(),
                "size": entry.stat().st_size if not entry.is_dir() else None,
                "modified": entry.stat().st_mtime
            }
            entries.append(entry_info)
    
    # 排序:先目录后文件,按名称排序
    entries.sort(key=lambda e: (not e["is_dir"], e["name"].lower()))
    
    # 构建返回结果
    result = {
        "current_path": path,
        "parent_path": os.path.dirname(path) if path else None,
        "entries": entries
    }
    
    return json.dumps(result, indent=2)

@mcp.resource("fs://file/{path}")
def get_file_content(path: str) -> tuple:
    """获取文件内容
    
    参数:
        path: 相对于基础目录的文件路径
    
    返回:
        文件内容和MIME类型
    """
    full_path = os.path.normpath(os.path.join(BASE_DIR, path))
    
    # 安全检查
    if not full_path.startswith(BASE_DIR):
        raise ResourcePermissionError("无权访问该文件")
    
    if not os.path.exists(full_path):
        raise ResourceNotFoundError(f"文件不存在: {path}")
    
    if not os.path.isfile(full_path):
        raise ValueError(f"路径不是文件: {path}")
    
    # 确定MIME类型
    # 简化版,实际应用中可使用mimetypes模块或更复杂的逻辑
    mime_type = "text/plain"
    extension = os.path.splitext(path)[1].lower()
    
    mime_map = {
        ".txt": "text/plain",
        ".html": "text/html",
        ".htm": "text/html",
        ".json": "application/json",
        ".xml": "application/xml",
        ".png": "image/png",
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".gif": "image/gif",
        ".pdf": "application/pdf"
    }
    
    mime_type = mime_map.get(extension, "application/octet-stream")
    
    # 文本文件直接读取文本内容
    text_mimes = ["text/plain", "text/html", "application/json", "application/xml"]
    
    if mime_type in text_mimes:
        try:
            with open(full_path, "r", encoding="utf-8") as f:
                content = f.read()
            return content, mime_type
        except UnicodeDecodeError:
            # 如果文本解码失败,退回到二进制模式
            mime_type = "application/octet-stream"
    
    # 二进制文件读取为bytes
    with open(full_path, "rb") as f:
        content = f.read()
    
    return content, mime_type

@mcp.resource("fs://info/{path}")
def get_file_info(path: str) -> str:
    """获取文件或目录的详细信息
    
    参数:
        path: 相对于基础目录的路径
    
    返回:
        文件或目录信息的JSON格式字符串
    """
    full_path = os.path.normpath(os.path.join(BASE_DIR, path))
    
    # 安全检查
    if not full_path.startswith(BASE_DIR):
        raise ResourcePermissionError("无权访问该路径")
    
    if not os.path.exists(full_path):
        raise ResourceNotFoundError(f"路径不存在: {path}")
    
    # 获取基本信息
    stat_info = os.stat(full_path)
    is_dir = os.path.isdir(full_path)
    
    info = {
        "name": os.path.basename(path) or path,
        "path": path,
        "is_dir": is_dir,
        "size": stat_info.st_size if not is_dir else None,
        "created": stat_info.st_ctime,
        "modified": stat_info.st_mtime,
        "accessed": stat_info.st_atime,
        "permissions": stat_info.st_mode & 0o777
    }
    
    # 对于目录,添加内容计数
    if is_dir:
        contents = os.listdir(full_path)
        info["item_count"] = len(contents)
        info["dir_count"] = sum(1 for item in contents if os.path.isdir(os.path.join(full_path, item)))
        info["file_count"] = sum(1 for item in contents if os.path.isfile(os.path.join(full_path, item)))
    
    return json.dumps(info, indent=2)

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

这个文件系统浏览器示例展示了:

  1. 参数化资源路径
  2. 错误处理
  3. 不同内容类型的处理
  4. 安全检查
  5. 复杂逻辑的资源实现

小结

在本章中,我们深入探讨了MCP资源的各个方面:

  • 资源URI的设计和模式
  • 静态和动态资源的创建
  • 参数化资源路径的实现
  • 资源内容类型和MIME类型
  • 资源错误处理机制
  • 通过文件系统浏览器示例,展示了复杂资源的实现

资源是MCP的核心组件之一,掌握其设计和实现技巧将使您能够构建出功能丰富、灵活且易于使用的MCP服务。在下一章中,我们将探讨MCP的另一个核心概念:工具(Tools)的高级用法。

使用 Hugo 构建
主题 StackJimmy 设计