MCP应用测试与调试:生产级实战指南
覆盖MCP全链路的测试策略:工具逻辑、输入校验、外部集成、认证授权、超时重试、并发请求及可观测性建设,并给出具体测试方法。
完整中文译文
MCP 应用在本地演示时运行完美,到了生产环境却可能出问题。
一个工具可能返回了错误的数据。一个外部 API 可能超时。一个阻塞函数可能冻结事件循环。一个租户的过期凭证可能导致反复失败。模型也可能会选错工具或生成无效的参数。
除非从一开始就把测试和可观测性构建到应用中,否则这些问题很难诊断。
本文我们将探讨在真实用户依赖这些应用之前,测试和调试 MCP 应用的实际方法。
应该测试什么?
MCP 应用通常包含多个移动部件:
User
↓
AI Client
↓
MCP Server
↓
Tool
↓
External API, Database, or Service
任何一层都可能发生故障。
完整的测试策略应该覆盖:
- 外部集成
- 认证和授权
- 日志、指标和链路追踪
只测试 Python 函数是不够的。还需要验证从客户端到外部服务的完整请求行为。
1. 从单元测试开始
单元测试一次验证应用程序的一个小部分。
假设一个 MCP server 暴露了一个天气工具:
@mcp.tool()
def get_weather(city: str):
if not city.strip():
raise ValueError("City is required")
return weather_client.get(city)
一个基本测试可以验证空输入被拒绝。
import pytest
def test_get_weather_rejects_empty_city():
with pytest.raises(ValueError):
get_weather("")
另一个测试可以验证预期响应:
def test_get_weather_returns_result(mocker):
mocker.patch(
"weather_client.get",
return_value={"city": "Toronto", "temperature": 24}
)
result = get_weather("Toronto")
assert result["city"] == "Toronto"
assert result["temperature"] == 24
有用的单元测试应该覆盖:
- 预期输出结构
- 边界条件
- 错误处理
保持工具小而专注。窄粒度的工具比执行多个不相关操作的工具更容易测试。
2. Mock 外部服务
MCP 工具通常依赖 API、数据库、云平台和第三方服务。
在每次测试中调用真实服务会使测试套件:
- 难以复现
- 依赖互联网访问
取而代之,mock 外部依赖。
def test_customer_lookup(mocker):
mocker.patch(
"customer_api.get_customer",
return_value={
"id": "cust-104",
"status": "active"
}
)
result = get_customer("cust-104")
assert result["status"] == "active"
还应该测试失败响应。
def test_customer_api_timeout(mocker):
mocker.patch(
"customer_api.get_customer",
side_effect=TimeoutError()
)
result = get_customer("cust-104")
assert result["error"] == "service_unavailable"
不要只测试成功响应。
生产系统会以多种方式失败。测试应该反映这一点。
3. 添加集成测试
单元测试确认单个函数工作正常。
集成测试确认多个组件协同工作。
对于 MCP 应用,集成测试可能验证:
Client request
↓
MCP server receives request
↓
Tool is discovered
↓
Tool executes
↓
Structured response is returned
一个有用的集成测试应该检查:
- 服务器是否正确启动
- 预期工具是否已注册
- 参数是否正确解析
- 认证是否被执行
- 响应是否符合预期 schema
- 失败是否以受控格式返回
def test_weather_tool_integration(mcp_client):
result = mcp_client.call_tool(
"get_weather",
{"city": "Toronto"}
)
assert result["city"] == "Toronto"
assert "temperature" in result
在隔离环境中使用测试凭证和测试数据运行集成测试。
永远不要让自动化测试指向生产资源。
4. 测试工具选择
一个工具可能正确工作,但在错误的时间被选中。
例如,用户可能问:
Explain how weather forecasts are created.
模型应该从概念上回答,而不是调用实时天气工具。
但当用户问:
What is the weather in Toronto today?
工具应该被使用。
创建一小套评估 prompt。
不要期望工具选择在每种情况下都完美。
对于每个评估 prompt,记录:
- 是否选择了正确的工具
- 是否避免了不必要的工具
- 参数是否准确
- 危险操作是否被拒绝
- 最终答案是否反映了工具结果
这些评估可以添加到 CI 中,这样 prompt、模型或工具描述的更改不会悄无声息地降低可靠性。
5. 测试超时和重试
外部服务最终会变慢或不可用。
每个外部调用都应该有超时。
response = api_client.get(
"/orders",
timeout=5
)
没有超时,请求可能会无限等待。
重试可以帮助处理临时故障,但必须有限制。
for attempt in range(3):
try:
return call_provider()
except TimeoutError:
if attempt == 2:
raise
超时和重试设计应确保:
- 请求在配置的超时后停止
- 永久错误不重试
- 避免重复操作
- 向用户返回安全的错误
对非幂等操作要特别小心。
重试读请求通常比重试以下操作更安全:
create_order
send_payment
delete_resource
send_email
重复的写操作可能产生重复或意外的结果。
6. 检测阻塞调用
最难解决的生产故障之一是进程仍在运行但应用停止响应。
这可能发生在同步工作阻塞异步事件循环时。
常见罪魁祸首包括:
- 同步 API 客户端
- 大文件操作
- 阻塞数据库调用
- 长时间运行的第三方 SDK 函数
容器在进程级别可能看起来仍然健康,但健康检查端点和用户请求可能停止响应。
将阻塞工作移出事件循环。
import asyncio
result = await asyncio.to_thread(
blocking_client.generate_embedding,
text
)
还可以监控事件循环响应能力。
import asyncio
import time
async def monitor_event_loop():
while True:
start = time.monotonic()
await asyncio.sleep(1)
delay = time.monotonic() - start - 1
if delay > 10:
logger.error(
"Event loop delay detected",
extra={"delay_seconds": delay}
)
对于难以排查的挂起,当事件循环无响应时,一个单独的看门狗线程可以捕获线程堆栈跟踪。
这将未解释的冻结变成团队可以调查的东西。
有用信号包括:
- 健康检查响应时间
- 线程池饱和度
- 工具执行时长
进程存活并不总是意味着应用健康。
7. 测试并发和租户隔离
MCP server 可能对一个用户正确工作,但在并发流量下失败。
负载测试应该模拟多个用户同时调用工具。
一个基本的并发测试可能如下:
import asyncio
async def run_request(client, city):
return await client.call_tool(
"get_weather",
{"city": city}
)
async def test_concurrent_requests(client):
results = await asyncio.gather(
run_request(client, "Toronto"),
run_request(client, "Vancouver"),
run_request(client, "Calgary"),
)
assert len(results) == 3
多租户系统还需要故障隔离。
假设一个租户的提供商密钥过期并收到反复的 401 响应。
该故障不应减少每个租户的服务容量。
使用以下维度跟踪错误:
tenant_id
tool_name
provider
error_type
租户隔离确保:
- 租户 A 无法访问租户 B 的数据
- 一个租户的速率限制不会阻塞其他租户
- 一个租户的无效凭证保持隔离
- 熔断器在正确范围内操作
- 并发控制不会将每个错误视为全局错误
8. 使用结构化日志和链路追踪
当工具失败时,像这样的消息不太有用:
Something went wrong.
结构化日志使故障更容易搜索和关联。
{
"correlation_id": "req-72a91",
"tenant_id": "tenant-18",
"tool_name": "get_customer",
"duration_ms": 842,
"status": "failed",
"error_type": "timeout"
}
有用字段包括:
correlation_id关联整个请求的生命周期tenant_id隔离多租户环境中的问题tool_name识别问题工具duration_ms测量性能error_type对错误进行分类
分布式链路追踪可以显示完整请求路径:
User Request
↓
AI Client
↓
MCP Server
↓
Tool
↓
External API
这有助于回答以下问题:
- 请求在哪里变慢了?
- 哪个服务返回了错误?
- 工具被调用了一次以上吗?
- 故障影响了一个租户还是所有人?
不要记录秘密、访问令牌、私人客户记录或完整的敏感 prompt。
9. 将测试添加到 CI/CD
测试在自动运行时最有价值。
一个基本的流水线可能包括:
Code commit
↓
Static checks
↓
Unit tests
↓
Integration tests
↓
Security tests
↓
Container build
↓
Deployment to test environment
↓
Smoke tests
一个简单的 GitHub Actions job 可能如下:
name: Test MCP Application
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest
当关键测试失败时,生产部署应该停止。
部署后,运行 smoke 测试以验证:
- 服务器可访问
- 健康检查响应
- 安全的测试工具可以执行
- 日志和指标正在生成
实用测试清单
在发布 MCP 应用之前,确认:
- 每个工具都有单元测试
- 无效输入被拒绝
- 外部 API 在测试中被 mock
- 超时和重试被测试
- 认证和权限被验证
- 工具选择 prompt 被评估
- 危险操作需要批准
- 集成测试覆盖完整请求路径
- 并发请求已测试
- 租户故障保持隔离
- 事件循环响应能力被监控
- 日志包含关联 ID
- 敏感信息不被记录
- 测试在 CI/CD 中自动运行
- 部署后运行 smoke 测试
测试 MCP 应用不仅仅是检查工具是否返回预期结果。
还需要知道应用在以下情况下如何表现:
- 模型选错了工具
- 多个用户同时发送请求
- 阻塞函数冻结了事件循环
- 一个租户开始产生反复失败
最有用的测试关注真实故障场景,而不仅仅是 happy path。
当工具小、输入被验证、依赖被 mock、故障可观测时,调试变得容易得多。
本文是我的五部分 MCP 系列文章的完结篇,涵盖了从理解 MCP 到构建、部署、保护、测试和操作基于 MCP 的应用的全过程。
本文完成了我对 MCP 的系列探讨:
Model Context Protocol (MCP) Servers Explained: A Complete Beginner's Guide
Building Your First AI Agent with MCP: A Step-by-Step Guide
Productionizing an MCP-Based AI Agent with Docker, Kubernetes, CI/CD, and Observability
Securing MCP Servers: 7 Essential Controls for Production
Testing and Debugging MCP Applications: A Practical Production Guide
我定期分享我在 AI 工程、MCP、DevOps、云基础设施、Kubernetes 和站点可靠性工程方面的学习心得。
LinkedIn: Connect with me on LinkedIn
对于你来说,测试或调试 MCP 最困难的问题是什么?