合集: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 工作流