只需在 REST API 前增加一个轻量转换层,就能将它变成 MCP 服务器。该转换层注册任务级工具、校验输入、调用现有端点,并返回范围明确的结构化结果。REST API 及其数据模型无需改动。
先从一项只读任务开始。在确认现有端点边界是否适合 agent 前,不要为每个端点都生成工具。
从任务开始,而不是从端点列表开始
OpenAPI 文档很适合盘点接口,但机械地将每个端点转换为一个工具,通常会产生杂乱的工具集。Agent 要回答“订单 1842 在哪里?”时,需要的是订单状态查询操作,而不是分别调用内部查询、物流查询和状态码转换工具,再自行拼装答案。
先写下任务、完成任务所需的最少输入,以及最精简且有用的结果。然后在处理程序中将这份契约映射到一个或多个 REST 调用。可靠 MCP 工具设计指南进一步介绍了命名和工具边界。
注册类型明确的 MCP 工具
当前 MCP TypeScript SDK 提供 McpServer、registerTool 和 createMcpHandler。下面的示例将 GET /orders/:id 封装为只读的 get-order-status 工具。
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const OrderStatus = z.object({
orderId: z.string(),
status: z.enum(['processing', 'shipped', 'delivered', 'cancelled']),
updatedAt: z.string(),
});
const handler = createMcpHandler(({ authInfo }) => {
const server = new McpServer({ name: 'orders', version: '1.0.0' });
server.registerTool(
'get-order-status',
{
description: 'Get the current status of one order the caller can access.',
inputSchema: z.object({
orderId: z.string().min(1).describe('The order identifier, such as 1842'),
}),
outputSchema: OrderStatus,
},
async ({ orderId }) => {
const response = await fetch(
`${process.env.ORDERS_API_URL}/orders/${encodeURIComponent(orderId)}`,
{
headers: {
Authorization: `Bearer ${process.env.ORDERS_API_TOKEN}`,
'X-Caller-ID': authInfo?.clientId ?? 'unknown',
},
},
);
if (!response.ok) {
return {
isError: true,
content: [{
type: 'text',
text: `Order lookup failed with HTTP ${response.status}`,
}],
};
}
const order = OrderStatus.parse(await response.json());
return {
structuredContent: order,
content: [{ type: 'text', text: JSON.stringify(order) }],
};
},
);
return server;
});
export default handler;
输出 schema 为客户端提供稳定的结果结构。文本版本则兼容尚未支持结构化工具结果的客户端。MCP 工具规范建议在返回结构化内容时同时提供两种形式。
在处理程序前完成身份验证
createMcpHandler 不会代为验证 bearer token。请求到达处理程序前,应先验证调用方身份,再通过 authInfo 传入经过验证的身份。上游 REST 服务仍需授权该身份,或使用权限更窄的服务凭据。调用方 ID header 有助于追溯操作,但本身不能完成授权。
提供商 token 必须留在服务端,不能出现在工具参数、工具结果、提示词或客户端可见的错误中。对于写入工具,应将凭据限定到允许的操作,并在执行破坏性操作或付费操作前要求审批。MCP 基础设施安全指南解释了这条权限边界。
返回 agent 能据此行动的错误
HTTP 状态码本身很少能告诉 agent 下一步怎么做。应将已知故障转换为简短、稳定的原因:
| REST 故障 | MCP 结果 | Agent 操作 |
|---|---|---|
400,订单 ID 无效 |
invalid_order_id |
修正参数后再重试 |
401 或 403 |
not_authorized |
停止并请求不同的权限范围 |
404 |
order_not_found |
检查 ID 或询问用户 |
429 |
rate_limited,包含可重试时间 |
等待一次后重试 |
5xx |
upstream_unavailable |
仅在操作可以安全重试时重试 |
对于变更操作,应接收幂等键并将其转发到 REST API。如果服务端已经完成写入但响应丢失,单纯增加重试封装并不能让非幂等端点变得安全。
通过 MCP 客户端测试工具
应测试注册后的契约,而不是直接调用 REST 处理程序。覆盖一次成功调用,以及会改变 agent 行为的各类失败:
- schema 拒绝缺失或无效的输入;
- 一个调用方无法获取另一个调用方的订单;
- 上游超时返回范围明确且不含密钥的错误;
- 使用同一个幂等键重复写入时只产生一次影响;
- 破坏性操作会在审批边界停止。
SDK 的 HTTP 服务指南还要求本地服务器校验 Host 和 Origin,以防范 DNS 重绑定。其框架工厂会在 localhost 上启用这些防护;普通 Node 服务器则必须显式添加。
部署前验证一项只读工具
选择一个能够回答常见用户问题的端点。定义输入和输出 schema,通过真实 MCP 客户端调用,再检查完整的工具结果。只有授权和失败测试通过后,才能部署该工具。观察到某项任务无法通过第一个契约顺利完成时,再添加下一个工具。
如果第一个工具通过了这些检查,下方提示词会把部署交给你的 agent。今天完成一个工具已经足够。