
最近折腾 AI(人工智能)编程代理的 API(应用程序接口)测试,踩了几个坑:这活儿明明有确定答案,模型却总想自己“理解”一遍。于是把规则校验、响应比对、类型生成这些任务全塞给模型,结果它说得头头是道,真正出错时连个报错都指不出来。
后来我换了个思路:有些 API 工作根本不需要判断,只需要严格执行规则。比如按照 JSON(JavaScript 对象表示法)Schema 校验响应、比较两份 JSON、执行 JSONPath 查询,或者检查 OpenAPI 文件里的 operationId 是否重复,这些任务都只有一个正确答案,交给代码执行比让模型猜靠谱得多。
MCP(模型上下文协议)的价值就在这里:让编程代理直接调用确定的工具,而不是凭空判断。下面用一个免费、公开的 MCP Server(模型上下文协议服务)连接编辑器,并实际跑一遍它提供的 API 测试工具。文中所有输出都来自对线上服务的真实调用。
服务地址是 https://testingapis.com/mcp。它无状态、只读,不需要注册,也不需要 API Key(密钥),通信协议为 Streamable HTTP(可流式传输的 HTTP)。它与 testingapis.com 上对应的浏览器工具运行同一套代码,每个结果末尾还会附上对应页面链接,方便你手工复核。
| 工具 | 用途 |
|---|---|
validate_openapi |
找出会导致 OpenAPI 3.0/3.1 和 Swagger 2.0 工具链出错的问题 |
validate_json_schema |
按照 Schema 校验 JSON,支持 draft 4 到 2020-12,并给出每个错误的 JSONPath |
generate_json_schema |
根据示例响应生成 draft 2020-12 Schema |
json_to_types |
把 JSON 转换为 TypeScript、Go、Python(Pydantic)、Java、C#、Kotlin 或 Rust 类型 |
compare_json |
按数据比较两份 JSON,忽略对象键顺序,并可选是否忽略数组顺序 |
query_jsonpath |
执行符合 RFC(请求评论)9535 的 JSONPath 查询,支持过滤器 |
curl_to_code |
把 cURL 命令转换为 Python、fetch、axios、Go、Java、C# 或 PHP 代码 |
convert_api_spec |
在 OpenAPI、Swagger、Postman、cURL、HAR(HTTP 归档)和 .http 之间转换 |
explain_cron |
用自然语言解释 cron 表达式,并列出接下来几次运行时间 |
get_test_data |
从公开 REST(表述性状态转移)API 获取商品、文章或待办事项等真实感测试数据 |
除 get_test_data 外,所有工具都只处理传入的文本。get_test_data 会向公开 API 发起一次 GET(读取)请求。
Claude Code
claude mcp add --transport http testingapis https://testingapis.com/mcp
Cursor:将下面配置加入 ~/.cursor/mcp.json,也可以放在项目的 .cursor/mcp.json 中。
{ "mcpServers": { "testingapis": { "url": "https://testingapis.com/mcp" } }
}
VS Code 的 Copilot Agent Mode:将下面配置加入 .vscode/mcp.json,也可以在命令面板中运行 MCP: Add Server。
{ "servers": { "testingapis": { "type": "http", "url": "https://testingapis.com/mcp" } }
}
Claude Desktop 和 claude.ai:进入 Settings → Connectors → Add custom connector,然后粘贴服务地址。
ChatGPT、Windsurf 以及其他支持远程 MCP Server 的客户端,填入同一个地址即可,不需要身份认证。
这些工具不需要你手动调用。直接用自然语言提问,比如“检查这个响应是否符合 Schema”“为什么我的接口规范无法导入”,代理会自行选择工具。真正值得关注的是返回结果,所以下面直接展示每次调用的实际输出。
要求获取一条示例商品数据,会调用 get_test_data:
GET https://api.testingapis.com/api/products/5 { "id": 5, "title": "7-Port USB-C Hub", "price": 34.99, "description": "Aluminium USB-C hub with HDMI, SD card reader, and 100W passthrough.", "category": "electronics", "image": "https://picsum.photos/seed/prod5/400/400", "rating": { "rate": 4.2, "count": 141 }, "inStock": true, "createdAt": "2025-06-20T16:20:00Z"
}
接着调用 json_to_types,传入 language: "typescript" 和 rootName: "Product":
export interface Product { id: number; title: string; price: number; description: string; category: string; image: string; rating: Rating; inStock: boolean; createdAt: string;
} export interface Rating { rate: number; count: number;
}
嵌套对象会获得独立命名的类型。如果传入数组,工具会读取每一项,因此某些元素中缺失的字段会被标记为可选,而不是必填。
这正是工具比模型靠谱的地方。先用 generate_json_schema 并设置 strict: true,把上面的商品响应转换成同时拒绝未知属性的 Schema。然后我故意把数据改坏三处:价格从数字变成字符串、删除 category,再增加一个 discount 字段,最后交给 validate_json_schema:
3 errors:
- $ (required): Missing the required property "category".
- $.price (type): Expected number, got string.
- $.discount (additionalProperties): "discount" isn't one of the allowed properties.
"34.99" 代替 34.99,正是模型阅读两份 JSON 时容易含糊带过的地方,校验器不会。
下面是一份小型接口规范,包含两个在复制、粘贴接口操作时很容易犯的错误:
openapi: 3.0.3
info: title: Orders version: "1.0"
paths: /orders/{orderId}: get: operationId: getOrder responses: "200": description: The order /orders: get: operationId: getOrder responses: "200": description: All orders
validate_openapi 返回:
Orders (OpenAPI 3.0.3, 2 operations): 2 errors, 1 warnings.
- ERROR at /paths/~1orders~1{orderId}/get: The path has {orderId}, but GET doesn't declare a path parameter "orderId".
- ERROR at /paths/~1orders/get/operationId: operationId "getOrder" is also used by GET /orders/{orderId}; it must be unique.
- WARNING at /: No servers: tools will send requests to the host the spec was loaded from.
每个问题都带有 JSON Pointer,代理可以直接定位到对应位置进行修改。OpenAPI 规范要求 operationId 唯一,而那些按照 operationId 命名的代码生成器,也无法生成两个都叫 getOrder 的方法。
使用 compare_json 并设置 ignoreArrayOrder: true,比较订单更新前后的数据。两份数据的键顺序不同,标签数组也反了:
{ "id": 42, "status": "paid", "tags": ["gift", "express"], "total": 59.99 }
{ "total": 59.99, "status": "shipped", "tags": ["express", "gift"], "id": 42, "trackingNo": "1Z999" }
2 differences:
- changed $.status: "paid" → "shipped"
- added $.trackingNo: "1Z999"