
在上一篇《FastAPI + MCP 实战解析》中,我们揭示了“AI 直接生成 SQL”这类野蛮接入方式带来的安全、权限、审计等系统性风险,并提出了 MCP(Model Context Protocol) 作为工程级治理方案的核心思想:让 AI 看得见能力,但摸不着系统。
然而,很多读者反馈:“道理我懂,但具体怎么用?Pydantic 怎么配合?如何做工具调用日志?权限校验放哪一层?”——这正是本文要解决的问题。
测开工程师的独特优势在于:既懂业务逻辑,又熟悉自动化与可观测性。 我们不是单纯调模型的人,而是 AI 能力的“守门人”和“调度员”。因此,本文将从 工具设计 → 权限控制 → 审计追踪 → 异常兜底 → 多环境隔离 五个维度,手把手教你构建一个 生产可用的 MCP 工具链。
MCP 的核心是 Tool Schema ——即模型能理解的“能力说明书”。而 FastAPI 生态中,Pydantic 模型天然就是 Schema 的最佳载体。
order_id="abc" 这种非法输入穿透到数据库。📌 关键认知:MCP Tool 的输入输出,必须是 Pydantic BaseModel 的子类,不能是 dict 或 str!
1from pydantic import BaseModel, Field
2from fastapi_mcp import FastApiMCP
3from fastapi import Depends
4
5class OrderQueryInput(BaseModel):
6 order_id: str = Field(..., min_length=6, max_length=32, description="订单ID,由数字和字母组成")
7
8class OrderQueryOutput(BaseModel):
9 order_id: str
10 status: str
11 pay_status: str
12 user_id: str # 注意:这里故意不返回敏感字段如手机号、地址
13
14@mcp.tool()
15def query_order_status(input: OrderQueryInput) -> OrderQueryOutput:
16 # 此处 input 已经是经过 Pydantic 校验的合法对象
17 order = db.get_order(input.order_id)
18 if not order:
19 raise ValueError("订单不存在") # MCP 会捕获并返回给模型
20 return OrderQueryOutput(
21 order_id=order.id,
22 status=order.status,
23 pay_status=order.pay_status,
24 user_id=order.user_id # 仅返回必要字段
25 )
💡 测开视角:这个
OrderQueryOutput就是我们对 AI 的“数据出口白名单”。任何不在其中的字段,AI 永远拿不到。
MCP 最大的价值之一,是 把权限判断从 Prompt 移回后端代码。但具体怎么做?
假设你的系统通过 JWT 传递用户身份,可在 Tool 中注入当前用户:
1from fastapi import Request
2
3def get_current_user(request: Request) -> str:
4 # 从 request.headers 解析 token,获取 user_id
5 return "user_123"
6
7@mcp.tool()
8def query_order_status(
9 input: OrderQueryInput,
10 current_user: str = Depends(get_current_user)
11) -> OrderQueryOutput:
12 order = db.get_order(input.order_id)
13 if order.user_id != current_user:
14 raise PermissionError("无权查看他人订单")
15 return OrderQueryOutput(...)
✅ 优势:权限逻辑与业务逻辑解耦,且可复用现有鉴权体系。 ⚠️ 注意:MCP Client 必须在调用时携带有效 Token(可通过中间件自动注入)。
如果你有多个 AI Agent(如客服 Agent、运维 Agent),可为不同角色注册不同 Tool:
1# 客服角色只能查订单状态
2customer_service_mcp = FastApiMCP(app, prefix="/mcp/customer")
3@customer_service_mcp.tool()
4def query_order_status(...): ...
5
6# 运维角色可查服务器状态
7ops_mcp = FastApiMCP(app, prefix="/mcp/ops")
8@ops_mcp.tool()
9def check_server_health(...): ...
🔒 安全边界:不同前缀对应不同 API 路由,可通过 Nginx 或网关做 RBAC 控制。
没有审计的 AI 调用等于“黑盒操作”。MCP 必须记录:
1import structlog
2
3logger = structlog.get_logger()
4
5@mcp.tool()
6def query_order_status(input: OrderQueryInput, current_user: str = Depends(get_current_user)):
7 start = time.time()
8 try:
9 order = db.get_order(input.order_id)
10 if order.user_id != current_user:
11 raise PermissionError("越权")
12 result = OrderQueryOutput(...)
13 logger.info("mcp_tool_call_success",
14 tool="query_order_status",
15 user=current_user,
16 input=input.dict(),
17 output=result.dict(),
18 duration_ms=(time.time() - start) * 1000)
19 return result
20 except Exception as e:
21 logger.error("mcp_tool_call_failed",
22 tool="query_order_status",
23 user=current_user,
24 input=input.dict(),
25 error=str(e))
26 raise
📊 建议:将日志接入 ELK 或 Grafana Loki,建立 MCP 调用大盘。
MCP Tool 必须考虑以下异常场景:
异常类型 | 处理建议 |
|---|---|
输入校验失败(Pydantic 抛出) | 自动返回错误,无需额外处理 |
业务逻辑异常(如订单不存在) | 抛出明确异常,MCP 会转为模型可读消息 |
第三方服务超时 | 设置 timeout,返回“服务暂时不可用” |
数据库连接失败 | 触发告警,返回友好提示 |
1from fastapi.responses import JSONResponse
2
3@app.exception_handler(ValueError)
4async def value_error_handler(request, exc):
5 return JSONResponse(
6 status_code=400,
7 content={"error": str(exc)}
8 )
9
10@app.exception_handler(PermissionError)
11async def permission_error_handler(request, exc):
12 return JSONResponse(
13 status_code=403,
14 content={"error": "权限不足"}
15 )
✅ 效果:无论 Tool 内抛出什么异常,MCP Client 都能收到结构化错误,而非 500。
MCP 工具链必须支持环境差异化:
环境 | 能力暴露 | 数据源 | 限流策略 |
|---|---|---|---|
开发 | 全量 Tool | Mock DB | 无限制 |
测试 | 全量 Tool | Test DB | 轻度限流 |
生产 | 白名单 Tool | Prod DB | 严格限流+熔断 |
1# config.py
2class Settings:
3 ENV: str = os.getenv("ENV", "dev")
4 MCP_TOOLS_ENABLED: list = json.loads(os.getenv("MCP_TOOLS_ENABLED", "[]"))
5
6settings = Settings()
7
8# 注册 Tool 时动态判断
9if "query_order" in settings.MCP_TOOLS_ENABLED:
10 @mcp.tool()
11 def query_order_status(...): ...
1# 仅在测试环境暴露
2if settings.ENV == "test":
3 @mcp.tool()
4 def reset_test_data():
5 """重置测试数据,供 E2E 测试使用"""
6 db.truncate_orders()
7 return {"status": "ok"}
🧪 测开价值:你的自动化测试脚本可以直接调用
reset_test_data,无需绕道 UI 或手动清理。
MCP 不只是“只读查询”,合理设计下可支持:
1@mcp.tool()
2def request_refund(input: RefundRequest):
3 # 不直接退款!而是创建审批工单
4 ticket_id = create_approval_ticket(
5 type="refund",
6 data=input.dict(),
7 requester=current_user
8 )
9 return {"ticket_id": ticket_id, "status": "pending_approval"}
✅ 符合治理原则:AI 发起请求,人类决策执行。
1# Step 1: 查订单
2order = query_order_status(order_id)
3
4# Step 2: 查支付流水
5payment = query_payment_log(order.payment_id)
6
7# Step 3: 生成排查报告
8report = generate_troubleshooting_report(order, payment)
🤖 Agent 编排:由 MCP Client(如 LangChain)协调多个 Tool 调用。
构建一个生产级 MCP 工具链,请确保做到:
MCP 不是一个框架,而是一种 工程范式。它把 AI 从“拥有系统权限的超级用户”,降级为“只能调用受限工具的普通员工”。而测开工程师,正是这套制度的 设计者、实施者和监督者。
当你下次接到“让 AI 帮我们查数据”的需求时,请不要直接拼 SQL,而是反问一句:
“我们需要暴露哪些能力?谁有权使用?如何审计?”
这才是工程化的开始。
延伸阅读: