跳转到内容
Integration

OpenAPI 到 MCP 服务器:150 个端点、49 个 AI 工具

| 9 min read

我们如何将 OpenAPI 规范转换为包含 49 种工具的精选 MCP 服务器。 架构转换、工具描述、注释和无状态 HTTP 传输。

Laptop showing code in a dark development environment
Photo by Clement Helardot on Unsplash

Botoi 的 REST API 拥有 150 多个端点。 当我们构建MCP服务器时,我们将其中的49个注册为工具。 不是因为其余的不起作用。 因为给 AI 模型 150 个工具就像给某人一份 200 页的菜单; 他们会选择一些东西,但这不会是正确的。

这篇文章将介绍整个过程:整理工具列表、将 OpenAPI 模式转换为 Zod 对象、编写 AI 模型解析良好的描述、添加 MCP 注释,以及作为无状态 Cloudflare Worker 运行整个过程。 如果您维护一个 API 并希望从中构建一个 MCP 服务器,那么这就是剧本。

为什么是 49 种工具,而不是 150 种

您在 MCP 服务器中注册的每个工具都会序列化到模型的上下文窗口中。 工具名称、描述和完整输入模式都会消耗令牌。 在用户输入单个单词之前,包含 150 个工具的清单可以燃烧 30,000 多个代币。

这会产生两个问题:

  • 对话本身剩下的令牌更少
  • 当列表很长时,模型更容易选择错误的工具

我们对此进行了测试。 注册了所有 150 多个端点后,Claude 在第一次尝试时大约 72% 的情况下选择了正确的工具。 凭借 49 个精选工具,这一数字跃升至 94%。 更小、更集中的列表使模型能够更好地完成工作。

策展标准很简单:

  • 人工智能代理需要这种中间对话吗? (DNS 查找:是。PDF 生成:很少。)
  • 该工具是否返回模型可以推理的结构化数据? (JSON:是。二进制图像:否。)
  • 模型能否从自然语言中填写所需的参数? (域名:是。复杂嵌套配置对象:否。)

工具清单结构

每个精选工具都将 MCP 工具名称映射到 API 路径、HTTP 方法、描述和注释。 这是 TypeScript 界面:

export interface CuratedTool {
  path: string;
  method: 'get' | 'post';
  title: string;
  description: string;
  annotations: {
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    openWorldHint?: boolean;
  };
}

以下是两个条目在实践中的样子:

// curated-tools.ts
export const CURATED_TOOLS: Record<string, CuratedTool> = {
  lookup_dns: {
    path: '/v1/dns/lookup',
    method: 'post',
    title: 'DNS Lookup',
    description:
      'Query DNS records (A, AAAA, MX, TXT, CNAME, NS) for a domain. ' +
      'Use when you need to check DNS configuration or troubleshoot domain resolution.',
    annotations: { readOnlyHint: true, openWorldHint: true },
  },
  dev_hash: {
    path: '/v1/hash',
    method: 'post',
    title: 'Hash Text',
    description:
      'Generate a hash (MD5, SHA-1, SHA-256, SHA-512) of input text. ' +
      'Use for checksums, data integrity, or fingerprinting.',
    annotations: { readOnlyHint: true },
  },
  // ... 47 more tools
};

pathmethod 字段指向现有的 REST 端点。 这 description 告诉模型何时使用该工具。 这 annotations 告诉模型该工具的行为方式。

将 OpenAPI 模式转换为 Zod

MCP SDK 期望工具输入架构为 Zod 对象。 我们的 API 已经有了 OpenAPI 3.1 规范,其中包含每个端点的完整请求正文定义。 模式构建器读取这些定义并在服务器启动时生成 Zod 类型。

核心转换函数将每个 OpenAPI 属性类型映射到其 Zod 等效项:

// schema-builder.ts
import { z } from 'zod';
import { paths } from '../../openapi-paths';

function mapPropertyToZod(
  prop: OpenApiProperty,
  isRequired: boolean
): z.ZodTypeAny {
  let schema: z.ZodTypeAny;

  if (prop.enum && prop.enum.length > 0) {
    schema = z.enum(prop.enum as [string, ...string[]]);
  } else {
    switch (prop.type) {
      case 'number':
      case 'integer':
        schema = z.number();
        break;
      case 'boolean':
        schema = z.boolean();
        break;
      case 'array':
        schema = z.array(z.string());
        break;
      case 'object':
        schema = z.record(z.unknown());
        break;
      default:
        schema = z.string();
        break;
    }
  }

  if (prop.description) {
    schema = schema.describe(prop.description);
  }
  if (prop.default !== undefined) {
    schema = schema.default(prop.default);
  }
  if (!isRequired) {
    schema = schema.optional();
  }

  return schema;
}

该函数中的关键决策:

  • enum 价值观成为 z.enum(),为模型提供一组固定的有效选项
  • 必填字段仍为必填字段; 可选字段获取 .optional()
  • 开放API description 结转通过 .describe(),MCP SDK 包含在工具清单中
  • 默认值通过传播 .default()

buildZodSchema 函数处理 POST(请求正文)和 GET(查询参数)端点:

export function buildZodSchema(
  apiPath: string,
  method: 'get' | 'post'
): Record<string, z.ZodTypeAny> {
  const operation = getOperation(apiPath, method);
  if (!operation) return {};

  // POST: read from requestBody schema
  if (method === 'post') {
    const schema = operation.requestBody
      ?.content?.['application/json']?.schema;
    if (!schema?.properties) return {};

    const required = new Set(schema.required ?? []);
    const result: Record<string, z.ZodTypeAny> = {};

    for (const [key, prop] of Object.entries(schema.properties)) {
      result[key] = mapPropertyToZod(prop, required.has(key));
    }
    return result;
  }

  // GET: read from query parameters
  const params = operation.parameters;
  if (!params || params.length === 0) return {};

  const result: Record<string, z.ZodTypeAny> = {};
  for (const param of params) {
    if (param.in !== 'query') continue;
    const prop: OpenApiProperty = {
      type: param.schema?.type ?? 'string',
      description: param.description ?? param.schema?.description,
      default: param.schema?.default,
      enum: param.schema?.enum,
    };
    result[param.name] = mapPropertyToZod(prop, param.required === true);
  }
  return result;
}

在服务器创建期间,每个工具都会运行一次此函数。 它返回一个平面 Record<string, z.ZodTypeAny> MCP SDK 序列化为工具清单的 JSON 架构。

编写工具描述 AI 模型解析良好

工具描述是正确选择工具的最重要的字段。 模型读取它来决定工具是否符合用户的意图。 模糊的描述会导致错误的工具选择。

我们确定了一个两句模式:

  1. 第一句话: 该工具的作用,以动词开头。 包括它处理的特定数据类型或格式。
  2. 第二句: 何时使用它,从“何时使用”开始。 这为模型提供了触发条件。

比较同一个 DNS 查找工具的这两个描述:

版本 描述 问题
坏的 “用于查找内容的 DNS 工具” 没有列出记录类型,没有触发条件,模糊
好的 “查询域的 DNS 记录(A、AAAA、MX、TXT、CNAME、NS)。当您需要检查 DNS 配置或对域解析进行故障排除时使用。” 没有任何

好的版本告诉模型它可以查询的确切记录类型(因此它知道该工具处理 MX 查找)以及应该触发它的情况(DNS 配置检查、故障排除)。 该模型将用户意图与这些关键词进行匹配。

MCP 注释:告诉模型工具的行为方式

注释是每个工具上的元数据标志。 它们不影响执行。 他们告诉模型预期会出现什么样的副作用。

// Read-only tool that hits an external service
lookup_dns: {
  annotations: { readOnlyHint: true, openWorldHint: true },
}

// Encryption tool: no external calls, same input = same output
security_encrypt: {
  annotations: { idempotentHint: true },
}

四个注释及其含义:

注解 信号 例子
readOnlyHint 该工具读取数据但从不修改任何内容 DNS 查找、WHOIS、SSL 检查
destructiveHint 此工具删除或覆盖数据 Webhook 收件箱删除(不在我们精选的集合中)
idempotentHint 使用相同的输入调用此工具两次会产生相同的结果 AES 加密、AES 解密
openWorldHint 该工具发出外部网络请求 IP 查找、URL 元数据、技术检测

在我们的 49 种工具中,有 44 种携带 readOnlyHint: true。 12个查找工具还带有 openWorldHint: true 因为它们调用外部 DNS 服务器、WHOIS 注册表或获取实时网页。 加密/解密工具携带 idempotentHint: true 因为它们是确定性的转换。

我们精选的工具均不包含 destructiveHint。 这是一个深思熟虑的选择。 我们从精选集中排除了 webhook 收件箱删除和粘贴删除等工具,因为人工智能模型不应在没有强大防护措施的情况下删除用户数据。

在 MCP 服务器上注册工具

注册循环将所有内容联系在一起。 它迭代精选的工具列表,根据 OpenAPI 规范构建 Zod 架构,并使用其描述和注释注册每个工具:

// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CURATED_TOOLS } from './curated-tools';
import { buildZodSchema } from './schema-builder';

function createMcpServer(apiKey: string | undefined, env: Env) {
  const server = new McpServer(
    { name: 'botoi', version: '1.0.0' },
    { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() }
  );

  for (const [toolName, tool] of Object.entries(CURATED_TOOLS)) {
    const zodSchema = buildZodSchema(tool.path, tool.method);

    server.registerTool(toolName, {
      title: tool.title,
      description: tool.description,
      inputSchema: zodSchema,
      annotations: tool.annotations,
    }, async (args: Record&lt;string, unknown&gt;) =&gt; {
      return callApi(tool.path, tool.method, args, apiKey, env);
    });
  }

  return server;
}

当模型调用工具时,处理函数接收解析后的参数并将它们转发到内部 API 路由。 这 callApi 函数构建内部 HTTP 请求并将响应作为 MCP 格式的内容返回:

async function callApi(
  path: string,
  method: string,
  body: unknown,
  apiKey: string | undefined,
  env: Env
) {
  const headers: Record&lt;string, string&gt; = {
    'Content-Type': 'application/json',
  };
  if (apiKey) headers['X-API-Key'] = apiKey;

  const req = new Request(\`http://internal\$\{path}\`, {
    method: method.toUpperCase(),
    headers,
    body: method === 'post' ? JSON.stringify(body) : undefined,
  });

  const res = appFetcher
    ? await appFetcher(req, env)
    : await fetch(req);

  const json = await res.json();
  if (!json.success) {
    return {
      content: [{ type: 'text', text: JSON.stringify(json.error, null, 2) }],
      isError: true,
    };
  }
  return {
    content: [{ type: 'text', text: JSON.stringify(json.data, null, 2) }],
  };
}

appFetcher 模式允许 MCP 服务器通过内部函数引用调用 API 路由,而不是发出外部 HTTP 请求。 这避免了网络往返。 MCP 处理程序和 API 路由在同一 Cloudflare Worker 中运行,因此内部路由是函数调用,而不是 HTTP 跃点。

Cloudflare Workers 上的无状态 HTTP 传输

MCP 支持两种传输:stdio(用于本地进程)和 Streamable HTTP(用于远程服务器)。 我们选择 Streamable HTTP 因为服务器在 Cloudflare Workers 上运行,它不支持长时间运行的进程。

// Hono route handler
import { WebStandardStreamableHTTPServerTransport }
  from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';

app.all('/mcp', async (c) =&gt; {
  const apiKey =
    c.req.header('X-API-Key') ||
    c.req.header('Authorization')?.replace('Bearer ', '');

  const server = createMcpServer(apiKey, c.env);
  const transport = new WebStandardStreamableHTTPServerTransport();
  await server.connect(transport);
  return transport.handleRequest(c.req.raw);
});

每个请求都会创建一个新的 McpServer 实例。 请求之间没有持续的会话状态。 这很好,因为每个工具调用都是独立的; 模型发送工具名称和参数,服务器返回结果。 没有多步骤交易。

无状态设计具有三个优点:

  • 不需要会话存储(没有Redis、没有KV、没有数据库)
  • 空闲时缩放至零,负载下水平缩放
  • 以零配置部署到 300 多个 Cloudflare 边缘站点

API 密钥处理发生在 MCP 层。 客户端通过以下方式发送密钥 X-API-Key 或者 Authorization: Bearer 标头。 MCP 路由提取它并将其传递给内部 API 调用。 MCP 路由本身没有单独的身份验证中间件。

您自己的 API 的剧本

如果您有 OpenAPI 规范并想要构建 MCP 服务器,这里是精简版本:

  1. 整理您的工具列表。 选择返回结构化数据并接受简单输入的 20-80 个端点。 跳过返回二进制数据、需要文件上传或具有深层嵌套输入模式的端点。
  2. 编写一个模式转换器。 将您的 OpenAPI 属性类型映射到 Zod。 继承描述、默认值和枚举值。 处理请求正文 (POST) 和查询参数 (GET) 模式。
  3. 写两句话的描述。 第一句:该工具的作用,以动词开头。 第二句:“使用时”+触发条件。 具体说明数据类型和格式。
  4. 添加注释。 标记只读工具。 标记进行外部网络调用的工具。 识别幂等操作。 排除破坏性工具,除非您有确认流程。
  5. 选择您的交通工具。 对远程服务器使用 Streamable HTTP,对本地 CLI 工具使用 stdio。 MCP SDK 两者都提供。
  6. 将工具调用路由到您现有的 API。 不要重写业务逻辑。 内部调用自己的路由。 MCP 服务器是一个薄适配器层。

Botoi的MCP服务器有4个文件: curated-tools.ts (49 个工具定义), schema-builder.ts (OpenAPI 到 Zod 转换器), server.ts (注册和路由),以及 tools.ts (公共清单端点)。 整个过程向现有 API 添加了大约 400 行 TypeScript。

尝试一下

Botoi MCP 服务器上线于 https://api.botoi.com/mcp。 一分钟内即可将其连接到 Claude Desktop、Claude Code、Cursor 或 VS Code。 请参阅 MCP 设置文档 每个受支持的客户端的配置片段。

浏览 完整的工具清单 查看所有 49 个工具定义及其架构和注释。 这 API文档 覆盖 MCP 服务器后面的 150 多个 REST 端点的完整集合。

FAQ

如何根据 OpenAPI 规范构建 MCP 服务器?
解析您的 OpenAPI 路径定义,提取请求正文架构(对于 POST)或查询参数(对于 GET),将每个属性转换为 Zod 类型,然后使用 Zod 架构将 McpServer 实例上的每个工具注册为 inputSchema。 MCP SDK 处理 JSON-RPC 传输和工具发现。
为什么不将所有 API 端点公开为 MCP 工具?
人工智能模型有上下文窗口限制。 每个工具定义都会消耗令牌。 在对话开始之前,包含 150 个工具的清单可以消耗 30,000 多个令牌。 精选多达 49 个高价值工具,使清单中的令牌数量保持在 8,000 个以下,并提高工具选择的准确性。
什么是 MCP 工具注释以及它们为何重要?
注释是元数据提示,例如 readOnlyHint、 DestructiveHint、idempotHint 和 openWorldHint。 它们告诉人工智能模型工具是否读取或写入数据、是否联系外部服务以及重试是否安全。 模型使用这些提示来规划多步骤工作流程,并避免未经确认的破坏性操作。
MCP 服务器可以在 Cloudflare Workers 上运行吗?
是的。 使用 MCP SDK 中的 WebStandardStreamableHTTPServerTransport。 它适用于任何支持 Web 标准请求/响应 API 的运行时。 Cloudflare Workers、Deno Deploy 和 Vercel Edge Functions 均符合资格。 每个请求都会创建一个新的 McpServer 实例,因此不需要会话状态。
我应该如何为AI模型编写MCP工具描述?
从动词开始。 用一句话说明该工具的作用。 添加第二个以“Use when”开头的句子,描述触发条件。 跳过实施细节。 示例:“查询域的 DNS 记录(A、AAAA、MX、TXT、CNAME、NS)。当您需要检查 DNS 配置或对域解析进行故障排除时使用。”

开始使用 botoi 构建

150+ 个 API 端点,涵盖查询、文本处理、图片生成和开发者工具。免费套餐,无需信用卡。