博客
MCP

发布于 更新于

Shouryamaan 和 Arag

如何将任意 REST API 转换为 MCP 服务器

以 Markdown 阅读

只需在 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。今天完成一个工具已经足够。