OpenAPI 到 MCP 服务器:150 个端点、49 个 AI 工具
我们如何将 OpenAPI 规范转换为包含 49 种工具的精选 MCP 服务器。 架构转换、工具描述、注释和无状态 HTTP 传输。
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
};
这 path 和 method 字段指向现有的 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 模型解析良好
工具描述是正确选择工具的最重要的字段。 模型读取它来决定工具是否符合用户的意图。 模糊的描述会导致错误的工具选择。
我们确定了一个两句模式:
- 第一句话: 该工具的作用,以动词开头。 包括它处理的特定数据类型或格式。
- 第二句: 何时使用它,从“何时使用”开始。 这为模型提供了触发条件。
比较同一个 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<string, unknown>) => {
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<string, string> = {
'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) => {
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 服务器,这里是精简版本:
- 整理您的工具列表。 选择返回结构化数据并接受简单输入的 20-80 个端点。 跳过返回二进制数据、需要文件上传或具有深层嵌套输入模式的端点。
- 编写一个模式转换器。 将您的 OpenAPI 属性类型映射到 Zod。 继承描述、默认值和枚举值。 处理请求正文 (POST) 和查询参数 (GET) 模式。
- 写两句话的描述。 第一句:该工具的作用,以动词开头。 第二句:“使用时”+触发条件。 具体说明数据类型和格式。
- 添加注释。 标记只读工具。 标记进行外部网络调用的工具。 识别幂等操作。 排除破坏性工具,除非您有确认流程。
- 选择您的交通工具。 对远程服务器使用 Streamable HTTP,对本地 CLI 工具使用 stdio。 MCP SDK 两者都提供。
- 将工具调用路由到您现有的 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 端点,涵盖查询、文本处理、图片生成和开发者工具。免费套餐,无需信用卡。