5.2 设计模式与反模式
小周看了很多 MCP Server 的实现,发现好的设计有共性,坏的设计也有共性。这一节先总结 10 个常见误区,再给出 7 种好模式和 7 种坏模式。
十大误区
在学 MCP 的过程中,这些是最容易踩的坑:
| # | 误区 | 事实 | 出处 |
|---|---|---|---|
| 1 | "我用了 Claude Code,所以一定要用 MCP" | Claude Code 内置了大量工具,大部分任务不需要额外 MCP Server | 1.3 |
| 2 | "Tool Annotations 的 readOnlyHint 是可信的" | Server 可以声称只读但实际执行写操作,Annotations 只是「标签」不是「保证」 | 2.2, 4.1 |
| 3 | "在 stdio Server 中用 print() 输出调试" | print() 写入 stdout,会破坏 JSON-RPC 消息解析,必须用 stderr | 3.3 |
| 4 | "MCP 能让 AI 更聪明" | MCP 不提升 AI 能力,它只是让 AI 更容易「动手」——连接外部系统 | 1.1 |
| 5 | "MCP 会替代 OpenAPI" | MCP 和 OpenAPI 互补——MCP Server 内部可以调用 OpenAPI 定义的 API | 5.1 |
| 6 | "MCP 和 A2A 是竞争关系" | MCP 连接 Agent↔工具,A2A 连接 Agent↔Agent,两者互补 | 5.1 |
| 7 | "Protocol Error 和 Tool Execution Error 是一回事" | Protocol Error LLM 无法修正,Tool Execution Error LLM 可以看到并修正参数重试 | 2.3 |
| 8 | "远程 MCP Server 不需要安全措施" | 远程 Server 必须启用 TLS、OAuth 2.1、验证 Origin 头 | 4.1 |
| 9 | "一个 Server 应该暴露所有能力" | 单一职责——每个 Server 只做一件事,做好一件事 | 5.2 |
| 10 | "MCP 只属于 Anthropic" | MCP 已移交 Linux Foundation,是开放标准。OpenAI 也支持 MCP | 1.2 |
7 种设计模式
filesystem-server → 只负责文件读写
github-server → 只负责 GitHub API
database-server → 只负责数据库查询
all-in-one-server → 文件 + GitHub + 数据库 + 邮件 + ...
模式 1:单一职责 Server
每个 Server 只做一件事,做好一件事。
好处:易于理解、独立部署、独立扩展。
模式 2:ReadOnly 优先
优先暴露只读工具,写操作需要额外确认。
# 只读工具——安全,可直接暴露
@mcp.tool()
def search_files(pattern: str) -> str:
"""搜索文件"""
...
# 写操作——需要确认
@mcp.tool()
def delete_file(path: str) -> str:
"""删除文件(需用户确认)"""
# 通过 Elicitation 请求用户确认
...模式 3:Schema 严格验证
工具的 inputSchema 应尽可能严格——限制参数类型、范围、枚举值。
{
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "get", "search"]
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100
}
},
"required": ["action"],
"additionalProperties": false
}
}模式 4:结构化错误反馈
工具执行错误时,返回清晰的可操作反馈。
# ✅ 好的错误——LLM 能理解并修正
@mcp.tool()
def query_database(sql: str) -> str:
try:
result = db.execute(sql)
return format_result(result)
except SyntaxError as e:
# Tool Execution Error——LLM 可以修正 SQL
raise ValueError(f"SQL 语法错误:{e}。请检查 SQL 语句。")
# ❌ 坏的错误——LLM 无法修正
@mcp.tool()
def query_database(sql: str) -> str:
result = db.execute(sql) # 直接抛异常,没有有用的错误信息
return result模式 5:工具注解诚实
Tool Annotations 应如实反映工具行为。
@mcp.tool()
def search_files(pattern: str) -> str:
"""搜索文件"""
# Annotations 如实声明:只读、幂等
# (FastMCP 中通过装饰器参数设置)
...不要声称 readOnlyHint: true 但实际执行写操作。
模式 6:渐进式信息暴露
Server 根据需要暴露不同粒度的信息。
Resources(只读数据):
- todo://stats → 统计信息(轻量)
- todo://list → 列表概览(中等)
- todo://item/123 → 详细信息(完整)
Prompts(模板):
- summarize → 快速摘要
- detailed-review → 深度分析模式 7:无状态优先
工具尽量无状态,状态由 Client 或外部存储管理。
# ✅ 无状态——每次调用独立
@mcp.tool()
def search(query: str) -> str:
return db.search(query)
# ❌ 有状态——依赖上次调用的结果
last_results = []
@mcp.tool()
def search(query: str) -> str:
global last_results
last_results = db.search(query) # 状态存全局变量
return last_results7 种反模式
反模式 1:God Server
一个 Server 做所有事。
❌ 全能 Server:
- 100+ 工具
- 文件系统 + GitHub + 数据库 + 邮件 + 搜索 + ...
- 工具列表太长,LLM 难以选择
- 任何工具出问题都影响整体反模式 2:print() 调试
在 stdio Server 中用 print() 输出调试信息——这会破坏 JSON-RPC 消息解析。
# ❌ 会破坏协议
print("调试信息")
# ✅ 写入 stderr
print("调试信息", file=sys.stderr)
# ✅ 或用 logging
import logging
logger = logging.getLogger(__name__)
logger.info("调试信息")反模式 3:过度暴露
暴露不需要的工具和数据。
❌ 过度暴露:
- 数据库 Server 暴露了 DROP TABLE
- 文件 Server 暴露了 /etc/passwd
- GitHub Server 暴露了删除仓库的权限反模式 4:忽略错误处理
工具执行出错时没有有用的错误信息。
# ❌ 吞掉错误
@mcp.tool()
def risky_tool(data: str) -> str:
try:
return process(data)
except:
return "Error" # 什么错误?LLM 无法修正
# ✅ 提供可操作的错误信息
@mcp.tool()
def risky_tool(data: str) -> str:
try:
return process(data)
except ValueError as e:
raise ValueError(f"数据处理失败:{e}。请检查数据格式。")反模式 5:松散 Schema
inputSchema 太松,LLM 可能传入意外的参数。
// ❌ 太松
{ "type": "object", "properties": { "input": {} } }
// ✅ 严格
{
"type": "object",
"properties": {
"query": { "type": "string", "minLength": 1, "maxLength": 500 }
},
"required": ["query"],
"additionalProperties": false
}反模式 6:阻塞式长操作
工具执行时间过长,没有进度通知。
# ❌ 阻塞——Client 不知道进度
@mcp.tool()
def train_model(data: str) -> str:
time.sleep(300) # 5 分钟
return "训练完成"
# ✅ 分步骤 + 进度通知(如果 SDK 支持)
@mcp.tool()
async def train_model(data: str) -> str:
# 步骤 1
await notify_progress("正在预处理数据...", 1, 3)
# 步骤 2
await notify_progress("正在训练模型...", 2, 3)
# 步骤 3
await notify_progress("正在评估结果...", 3, 3)
return "训练完成"反模式 7:忽略安全注解
不设置 Tool Annotations,或者设置不诚实的注解。
# ❌ 没有注解——Client 不知道这个工具是否安全
@mcp.tool()
def delete_all() -> str:
db.delete_all()
return "已删除所有数据"
# ✅ 诚实设置注解
@mcp.tool(annotations={"destructiveHint": True, "readOnlyHint": False})
def delete_all() -> str:
db.delete_all()
return "已删除所有数据"编排模式——多 Server 协作实例
MCP 架构下 Server 之间不直接通信,由 AI(Client/Host 层)编排。来看一个真实的对话示例:
关键点:三个 Server 各司其职,AI 是编排者。 这就是 MCP 架构的优雅之处——不需要复杂的 Server 间通信协议。
检查清单
设计 MCP Server 时,用这个清单自查:
- [ ] 每个 Server 是否只做一件事?(单一职责)
- [ ] 只读工具是否标记
readOnlyHint?(注解诚实) - [ ] inputSchema 是否严格?(限制类型、范围)
- [ ] 错误信息是否可操作?(LLM 能修正)
- [ ] 是否只暴露必要的工具?(最小权限)
- [ ] stdio Server 是否没有
print()?(不污染 stdout) - [ ] 是否没有全局状态?(无状态优先)
- [ ] 长操作是否有进度通知?(非阻塞)
本节核心要点
- 7 种好模式:单一职责、ReadOnly 优先、Schema 严格、结构化错误、诚实注解、渐进暴露、无状态优先
- 7 种坏模式:God Server、print() 调试、过度暴露、忽略错误、松散 Schema、阻塞长操作、忽略注解
- 核心原则:最小权限、严格验证、诚实标注、有用反馈
练习:检查你之前写的 Todo Server,对照检查清单看看有哪些可以改进的地方。