深入 MCP 三大原语:Resources、Tools 和 Prompts 实战

3 阅读7分钟

合集:MCP(模型上下文协议)系列 · 中级篇(二)


前言

上一篇我们构建了可运行的 MCP Server,但只用到了最基本的 Tool 功能。MCP 真正强大的地方在于它的三大原语——Tools、Resources、Prompts——各自有着独特的设计哲学和适用场景。

本篇深入每个原语的高级特性,通过一个完整的"公司内部知识库助手"案例,展示三大原语如何协同工作。国内使用Claude Code 访问ccAiHub.com


一、三大原语的设计哲学

┌─────────────────────────────────────────┐
│                                         │
│   Resources     Tools        Prompts    │
│   (知道什么)  (能做什么)  (怎么做)   │
│                                         │
│   数据/上下文   可执行动作    工作流模板  │
│   只读为主      读写均可      结构化引导  │
│                                         │
└─────────────────────────────────────────┘

一个核心原则

  • 用 Resource 传递上下文数据(让 AI 知道)
  • 用 Tool 执行有副作用的操作(让 AI 做到)
  • 用 Prompt 提供最佳实践模板(让 AI 用好)

二、Resources 深度解析

2.1 URI 设计规范

Resources 通过 URI 标识,URI 是语义化的:

file:///path/to/file.ts         # 本地文件
postgres://host/db/table/schema # 数据库结构
memory://context/conversation   # 内存数据
http://api.example.com/data     # 远程数据
custom://myapp/config/settings  # 自定义协议

2.2 静态 vs 动态资源

静态资源:URI 是固定的

@mcp.resource("config://app/settings")
def get_settings() -> str:
    """应用配置(只读)"""
    return json.dumps({
        "version": "1.0.0",
        "environment": "production",
        "features": {"new_ui": True}
    })

动态资源:URI 包含参数

@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
    """获取指定用户的档案"""
    user = db.query(f"SELECT * FROM users WHERE id = ?", [user_id])
    if not user:
        raise ValueError(f"用户 {user_id} 不存在")
    return json.dumps(user)
@mcp.resource("documents://{category}/{doc_id}")
def get_document(category: str, doc_id: str) -> str:
    """获取指定分类下的文档"""
    doc_path = Path(f"docs/{category}/{doc_id}.md")
    if not doc_path.exists():
        raise FileNotFoundError(f"文档不存在:{category}/{doc_id}")
    return doc_path.read_text()

2.3 资源订阅(Resource Subscriptions)

允许主机监听资源变化,当资源更新时自动通知 AI:

from mcp.server.fastmcp import FastMCP
from mcp.types import Resource

mcp = FastMCP("realtime-monitor")

# 注册可订阅的资源
@mcp.resource("metrics://system/cpu", subscribe=True)
def get_cpu_metrics() -> str:
    """CPU 使用率(支持实时订阅)"""
    # 返回当前 CPU 使用率
    import psutil
    return json.dumps({
        "usage_percent": psutil.cpu_percent(interval=1),
        "timestamp": time.time()
    })
// TypeScript 实现资源变更通知
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
  const { uri } = request.params;
  
  if (uri.startsWith("metrics://")) {
    // 设置定时推送
    setInterval(async () => {
      await server.notification({
        method: "notifications/resources/updated",
        params: { uri }
      });
    }, 5000); // 每 5 秒通知一次
  }
  
  return {};
});

2.4 Resource vs Tool:何时用哪个?

需要数据         → Resource(AI 会直接读取)
需要执行操作     → Tool(AI 主动调用)
数据会频繁变化   → Resource + 订阅
操作有副作用     → Tool(更明确的调用意图)

三、Tools 深度解析

3.1 输入 Schema 的高级用法

Tools 的 inputSchema 完整支持 JSON Schema 规范:

@mcp.tool()
def search_documents(
    query: str,
    categories: list[str] | None = None,
    date_range: dict | None = None,
    max_results: int = 10,
    sort_by: str = "relevance"
) -> list[dict]:
    """
    搜索内部文档库
    
    Args:
        query: 搜索关键词
        categories: 限制搜索的文档类别列表(可选)
        date_range: 日期范围 {"start": "2024-01-01", "end": "2024-12-31"}
        max_results: 最多返回结果数,1-50 之间
        sort_by: 排序方式,"relevance" 或 "date"
    """
    # FastMCP 会自动从类型注解和 docstring 生成 JSON Schema
    ...

手动定义复杂 Schema(TypeScript):

{
  name: "analyze_pr",
  description: "分析 Pull Request 的代码变更",
  inputSchema: {
    type: "object",
    properties: {
      pr_number: {
        type: "integer",
        description: "PR 编号",
        minimum: 1
      },
      analysis_type: {
        type: "string",
        enum: ["security", "performance", "style", "all"],
        description: "分析类型",
        default: "all"
      },
      severity_threshold: {
        type: "string",
        enum: ["low", "medium", "high"],
        description: "只报告高于此严重程度的问题",
        default: "medium"
      }
    },
    required: ["pr_number"],
    additionalProperties: false
  }
}

3.2 工具注解(Tool Annotations)

注解向 AI 和主机提供工具行为的元信息:

{
  name: "delete_records",
  description: "删除数据库记录",
  inputSchema: { ... },
  // 注解:告诉主机这个工具有副作用且不可撤销
  annotations: {
    readOnlyHint: false,        // 会修改数据
    destructiveHint: true,      // 操作不可撤销
    idempotentHint: false,      // 不是幂等的
    openWorldHint: false        // 只影响本地系统
  }
}
注解含义主机可能的行为
readOnlyHint: true只读操作可自动执行,无需确认
destructiveHint: true破坏性操作要求用户明确确认
idempotentHint: true幂等操作可以安全重试
openWorldHint: true影响外部系统谨慎执行

3.3 错误处理规范

Tools 的错误分两种:

协议级错误(工具本身出问题):

@mcp.tool()
def read_file(path: str) -> str:
    if not os.path.exists(path):
        # 抛出异常 → 协议级错误
        raise FileNotFoundError(f"文件不存在:{path}")
    return open(path).read()

业务级错误(操作失败,但工具正常工作):

// 业务错误通过 isError: true 返回
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const result = await performOperation(request.params.arguments);
  
  if (!result.success) {
    return {
      content: [{
        type: "text",
        text: `操作失败:${result.errorMessage}\n建议:${result.suggestion}`
      }],
      isError: true  // 标记为业务错误,AI 会知道操作失败
    };
  }
  
  return {
    content: [{ type: "text", text: `操作成功:${result.data}` }]
  };
});

四、Prompts 深度解析

4.1 静态 Prompt

最简单的形式,固定内容的工作流模板:

@mcp.prompt()
def security_audit_prompt() -> str:
    """触发安全审计工作流"""
    return """
请执行完整的安全审计:

1. 使用 scan_dependencies 工具检查所有依赖的已知漏洞
2. 使用 check_config 工具验证安全配置
3. 使用 find_secrets 工具扫描代码中的硬编码 secrets
4. 汇总所有发现,按 CVSS 评分排序
5. 为每个问题提供修复建议和优先级

输出格式:安全审计报告(Markdown),包含执行摘要和详细问题清单。
"""

4.2 带参数的动态 Prompt

@mcp.prompt()
def code_migration_prompt(
    from_framework: str,
    to_framework: str,
    file_path: str | None = None
) -> list[dict]:
    """
    生成代码迁移指导
    
    Args:
        from_framework: 源框架(如 express, django)
        to_framework: 目标框架(如 fastapi, fastify)
        file_path: 要迁移的特定文件(可选)
    """
    scope = f"文件 {file_path}" if file_path else "整个项目"
    
    return [
        {
            "role": "user",
            "content": {
                "type": "text",
                "text": f"""
请帮我将 {scope}{from_framework} 迁移到 {to_framework}。

迁移流程:
1. 首先分析源代码结构(读取相关文件)
2. 查阅 {to_framework} 的文档了解最佳实践
3. 逐步迁移,保持功能等价
4. 更新测试用例
5. 验证迁移后功能正确

注意:
- 保持 API 接口兼容性
- 利用 {to_framework} 的特性优化性能
- 记录迁移中的重要决策
"""
            }
        }
    ]

4.3 嵌入 Resources 的 Prompt

Prompts 可以直接引用 Resources,让 AI 带着数据开始工作:

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  if (request.params.name === "weekly_report") {
    const weekNumber = request.params.arguments?.week || getCurrentWeek();
    
    return {
      description: "生成周报",
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `请根据以下数据生成第 ${weekNumber} 周的工作周报:`
          }
        },
        {
          role: "user",
          content: {
            type: "resource",
            resource: {
              uri: `metrics://weekly/${weekNumber}`,  // 嵌入 Resource!
              text: await fetchWeeklyMetrics(weekNumber),
              mimeType: "application/json"
            }
          }
        },
        {
          role: "user",
          content: {
            type: "text",
            text: `
请生成包含以下内容的周报:
- 本周关键指标摘要
- 完成的主要工作
- 遇到的挑战和解决方案
- 下周计划
格式:Markdown,适合发送给管理层。`
          }
        }
      ]
    };
  }
});

五、综合案例:公司内部知识库助手

用三大原语构建一个完整的企业知识库助手:

from mcp.server.fastmcp import FastMCP
import chromadb  # 向量数据库

mcp = FastMCP("company-kb")

# 初始化向量数据库
client = chromadb.PersistentClient(path="./kb_store")
collection = client.get_or_create_collection("documents")

# ── Resources:知识库数据 ──────────────────────

@mcp.resource("kb://stats")
def get_kb_stats() -> str:
    """知识库统计信息"""
    count = collection.count()
    return json.dumps({"total_documents": count, "last_updated": "..."})

@mcp.resource("kb://document/{doc_id}")
def get_document(doc_id: str) -> str:
    """获取指定文档全文"""
    result = collection.get(ids=[doc_id])
    if not result["documents"]:
        raise ValueError(f"文档 {doc_id} 不存在")
    return result["documents"][0]

# ── Tools:知识库操作 ──────────────────────────

@mcp.tool()
def search_knowledge_base(
    query: str,
    n_results: int = 5,
    category: str | None = None
) -> list[dict]:
    """
    语义搜索知识库
    
    Args:
        query: 搜索问题
        n_results: 返回结果数量(1-20)
        category: 按类别过滤(产品/技术/HR/财务)
    """
    where = {"category": category} if category else None
    results = collection.query(
        query_texts=[query],
        n_results=min(n_results, 20),
        where=where
    )
    
    documents = []
    for i, doc in enumerate(results["documents"][0]):
        metadata = results["metadatas"][0][i]
        documents.append({
            "id": results["ids"][0][i],
            "title": metadata.get("title", "无标题"),
            "category": metadata.get("category", "未分类"),
            "relevance_score": 1 - results["distances"][0][i],
            "excerpt": doc[:300] + "..." if len(doc) > 300 else doc
        })
    
    return documents

@mcp.tool()
def add_document(
    title: str,
    content: str,
    category: str,
    tags: list[str] | None = None
) -> str:
    """
    向知识库添加新文档
    
    Args:
        title: 文档标题
        content: 文档内容(支持 Markdown)
        category: 文档分类
        tags: 标签列表
    """
    import hashlib
    doc_id = hashlib.md5(f"{title}{content[:100]}".encode()).hexdigest()[:12]
    
    collection.add(
        ids=[doc_id],
        documents=[content],
        metadatas=[{
            "title": title,
            "category": category,
            "tags": ",".join(tags or []),
            "created_at": time.strftime("%Y-%m-%d")
        }]
    )
    
    return f"✅ 文档添加成功!ID: {doc_id}"

# ── Prompts:最佳实践模板 ──────────────────────

@mcp.prompt()
def answer_with_kb(question: str, strict: bool = False) -> str:
    """
    基于知识库回答问题的标准模板
    
    Args:
        question: 用户问题
        strict: True = 只基于知识库回答,不外部推理
    """
    strictness_note = "只能使用知识库中的信息回答,若知识库无相关内容,明确说明。" if strict else "优先使用知识库信息,必要时可补充通用知识。"
    
    return f"""
问题:{question}

请按以下步骤回答:
1. 使用 search_knowledge_base 工具搜索相关内容(至少搜索 2-3 个不同角度)
2. 如果搜到相关文档,用 kb://document/{{id}} 资源获取完整内容
3. 综合所有信息给出完整、准确的回答

要求:
- {strictness_note}
- 引用信息来源(文档标题和 ID)
- 如有相互矛盾的信息,指出差异并给出最可靠的答案
"""

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

六、能力协商:握手阶段的原理

MCP Server 启动时,会与主机进行能力协商:

// Server 声明支持的能力
{
  "capabilities": {
    "tools": {
      "listChanged": true    // 工具列表可以动态变化
    },
    "resources": {
      "subscribe": true,     // 支持资源订阅
      "listChanged": true    // 资源列表可以动态变化
    },
    "prompts": {
      "listChanged": true    // 提示模板可以动态变化
    }
  }
}

主机根据 Server 声明的能力决定:

  • 是否启用订阅功能
  • 是否缓存工具/资源列表
  • 如何展示 Prompt 模板

七、本篇小结

原语何时用关键特性
Resources提供上下文数据URI 标识、支持订阅、模板化
Tools执行操作JSON Schema、注解、错误处理
Prompts工作流引导参数化、嵌入资源、多轮对话

下一篇:MCP + RAG 实战,把向量检索和 MCP 结合,构建真正智能的企业知识库问答系统。


系列导航

  • 中级篇(一):动手构建 MCP Server:Python & TypeScript 实战
  • 中级篇(二):深入三大原语:Resources、Tools 和 Prompts ← 当前
  • 中级篇(三):MCP + RAG:构建企业知识库问答系统
  • 高级篇(一):企业级 MCP 架构:安全、认证与高可用
  • 高级篇(二):MCP OAuth 2.1 实战:标准化身份认证
  • 高级篇(三):MCP + 多智能体编排:下一代 AI 工作流