OpenAPI al servidor MCP: 150 puntos finales, 49 herramientas de IA
Cómo convertimos una especificación OpenAPI en un servidor MCP seleccionado con 49 herramientas. Conversión de esquemas, descripciones de herramientas, anotaciones y transporte HTTP sin estado.
La API REST de Botoi tiene más de 150 puntos finales. Cuando creamos el servidor MCP, registramos 49 de ellos como herramientas. No porque el resto no funcione. Porque darle a un modelo de IA 150 herramientas es como entregarle a alguien un menú de 200 páginas; escogerán algo, pero no será lo correcto.
Esta publicación recorre el proceso completo: seleccionar la lista de herramientas, convertir esquemas OpenAPI en objetos Zod, escribir descripciones que los modelos de IA analicen bien, agregar anotaciones MCP y ejecutar todo como un trabajador de Cloudflare sin estado. Si mantiene una API y desea crear un servidor MCP a partir de ella, este es el manual.
¿Por qué 49 herramientas, no 150?
Cada herramienta que registra en un servidor MCP se serializa en la ventana contextual del modelo. El nombre de la herramienta, la descripción y el esquema de entrada completo consumen tokens. Un manifiesto de 150 herramientas puede grabar más de 30.000 tokens antes de que el usuario escriba una sola palabra.
Eso crea dos problemas:
- Quedan menos tokens para la conversación en sí
- El modelo elige la herramienta equivocada con más frecuencia cuando la lista es larga
Probamos esto. Con los más de 150 puntos finales registrados, Claude eligió la herramienta correcta en el primer intento aproximadamente el 72 % de las veces. Con 49 herramientas seleccionadas, esa cifra saltó al 94%. La lista más pequeña y enfocada hizo que el modelo hiciera mejor su trabajo.
Los criterios de curación fueron simples:
- ¿Un agente de IA necesita esta conversación en mitad de una conversación? (Búsqueda de DNS: sí. Generación de PDF: rara vez).
- ¿La herramienta devuelve datos estructurados sobre los que el modelo puede razonar? (JSON: sí. Imagen binaria: no.)
- ¿Puede el modelo completar los parámetros requeridos desde el lenguaje natural? (Nombre de dominio: sí. Objetos de configuración anidados complejos: no).
La estructura del manifiesto de herramientas.
Cada herramienta seleccionada asigna el nombre de una herramienta MCP a una ruta API, un método HTTP, una descripción y anotaciones. Aquí está la interfaz de TypeScript:
export interface CuratedTool {
path: string;
method: 'get' | 'post';
title: string;
description: string;
annotations: {
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
openWorldHint?: boolean;
};
}
Y así es como se ven dos entradas en la práctica:
// 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
};
La path y method Los campos apuntan al punto final REST existente. El description le dice al modelo cuándo usar la herramienta. El annotations Dígale al modelo cómo se comporta la herramienta.
Conversión de esquemas OpenAPI a Zod
El SDK de MCP espera esquemas de entrada de herramientas como objetos Zod. Nuestra API ya tiene una especificación OpenAPI 3.1 con definiciones completas del cuerpo de solicitud para cada punto final. El generador de esquemas lee esas definiciones y genera tipos de Zod al iniciar el servidor.
La función de conversión principal asigna cada tipo de propiedad OpenAPI a su equivalente 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;
}
Las decisiones clave en esta función:
enumlos valores se conviertenz.enum(), dando al modelo un conjunto fijo de opciones válidas- Los campos obligatorios siguen siendo obligatorios; campos opcionales obtienen
.optional() - La OpenAPI
descriptionse traslada a través de.describe(), que el SDK de MCP incluye en el manifiesto de la herramienta - Los valores predeterminados se propagan a través de
.default()
La buildZodSchema La función maneja los puntos finales POST (cuerpo de la solicitud) y GET (parámetros de consulta):
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;
}
Esta función se ejecuta una vez por herramienta durante la creación del servidor. Se devuelve un piso Record<string, z.ZodTypeAny> que el SDK de MCP serializa en el esquema JSON para el manifiesto de la herramienta.
Descripciones de herramientas de escritura Los modelos de IA se analizan bien
La descripción de la herramienta es el campo más importante para la selección correcta de la herramienta. Los modelos lo leen para decidir si una herramienta coincide con la intención del usuario. Las descripciones vagas conducen a selecciones de herramientas equivocadas.
Nosotras nos decidimos por un patrón de dos oraciones:
- Primera frase: qué hace la herramienta, comenzando con un verbo. Incluya los tipos de datos o formatos específicos que maneja.
- Segunda frase: cuando usarlo, comenzando con "Usar cuando". Esto le da al modelo una condición de activación.
Compare estas dos descripciones para la misma herramienta de búsqueda de DNS:
| Versión | Descripción | Problema |
|---|---|---|
| Mala | "Herramienta DNS para buscar cosas" | No se enumeran tipos de registros, no hay condición de activación, es vago |
| Buena | "Consulta registros DNS (A, AAAA, MX, TXT, CNAME, NS) para un dominio. Úsalo cuando necesites verificar la configuración de DNS o solucionar problemas de resolución del dominio". | Ninguna |
La buena versión le dice al modelo los tipos de registros exactos que puede consultar (para que sepa que esta herramienta maneja búsquedas MX) y las situaciones que deberían activarlo (verificaciones de configuración de DNS, resolución de problemas). El modelo compara la intención del usuario con estas palabras clave.
Anotaciones MCP: decirle a los modelos cómo se comportan las herramientas
Las anotaciones son indicadores de metadatos en cada herramienta. No afectan la ejecución. Le dicen al modelo qué tipo de efectos secundarios esperar.
// 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 },
}
Las cuatro anotaciones y lo que señalan:
| Anotación | Señal | Ejemplo |
|---|---|---|
readOnlyHint |
Esta herramienta lee datos pero nunca modifica nada. | Búsqueda de DNS, WHOIS, verificación SSL |
destructiveHint |
Esta herramienta elimina o sobrescribe datos. | Eliminación de la bandeja de entrada del webhook (no en nuestro conjunto seleccionado) |
idempotentHint |
Llamar a esta herramienta dos veces con la misma entrada produce el mismo resultado | Cifrado AES, descifrado AES |
openWorldHint |
Esta herramienta realiza solicitudes de red externa. | Búsqueda de IP, metadatos de URL, detección técnica |
De nuestras 49 herramientas, 44 llevan readOnlyHint: true. Las 12 herramientas de búsqueda también llevan openWorldHint: true porque llaman a servidores DNS externos, registros WHOIS o recuperan páginas web en vivo. Las herramientas de cifrado/descifrado llevan idempotentHint: true porque son transformaciones deterministas.
Ninguna de nuestras herramientas seleccionadas lleva destructiveHint. Esa fue una elección deliberada. Excluimos herramientas como la eliminación de la bandeja de entrada de webhook y la eliminación de pegado del conjunto seleccionado porque los modelos de IA no deberían eliminar los datos del usuario sin medidas de seguridad sólidas.
Registrar herramientas en el servidor MCP
El circuito de registro une todo. Itera sobre la lista de herramientas seleccionadas, crea el esquema Zod a partir de la especificación OpenAPI y registra cada herramienta con su descripción y anotaciones:
// 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;
}
Cuando el modelo llama a una herramienta, la función del controlador recibe los argumentos analizados y los reenvía a la ruta API interna. El callApi La función crea una solicitud HTTP interna y devuelve la respuesta como contenido con formato 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) }],
};
}
La appFetcher El patrón permite que el servidor MCP llame a las rutas API a través de una referencia de función interna en lugar de realizar una solicitud HTTP externa. Esto evita viajes de ida y vuelta en la red. The MCP handler and the API routes run in the same Cloudflare Worker, so internal routing is a function call, not an HTTP hop.
Transporte HTTP sin estado en Cloudflare Workers
MCP admite dos transportes: stdio (para procesos locales) y Streamable HTTP (para servidores remotos). Elegimos Streamable HTTP porque el servidor se ejecuta en Cloudflare Workers, que no admite procesos de larga duración.
// 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);
});
Cada solicitud crea una nueva McpServer instancia. No persiste ningún estado de sesión entre solicitudes. Esto está bien porque cada llamada a una herramienta es autónoma; el modelo envía el nombre de la herramienta y los argumentos, y el servidor devuelve el resultado. Sin transacciones de varios pasos.
El diseño sin estado tiene tres ventajas:
- No se necesita almacenamiento de sesión (sin Redis, sin KV, sin base de datos)
- Escala a cero cuando está inactivo, escala horizontalmente bajo carga
- Se implementa en más de 300 ubicaciones perimetrales de Cloudflare sin configuración
El manejo de claves API ocurre en la capa MCP. El cliente envía la clave vía X-API-Key o Authorization: Bearer encabezamiento. La ruta MCP lo extrae y lo pasa a la llamada API interna. No hay middleware de autenticación independiente en la propia ruta MCP.
El manual para su propia API
Si tiene una especificación OpenAPI y desea crear un servidor MCP, aquí está la versión condensada:
- Selecciona tu lista de herramientas. Elija entre 20 y 80 puntos finales que devuelvan datos estructurados y acepten entradas simples. Omita los puntos finales que devuelven datos binarios, requieren carga de archivos o tienen esquemas de entrada anidados profundos.
- Escribe un convertidor de esquemas. Asigne sus tipos de propiedades OpenAPI a Zod. Transfiera descripciones, valores predeterminados y valores de enumeración. Maneje los patrones del cuerpo de la solicitud (POST) y de los parámetros de consulta (GET).
- Escribe descripciones de dos oraciones. Primera oración: qué hace la herramienta, comenzando con un verbo. Oración dos: "Usar cuando" + condición de activación. Sea específico sobre los tipos y formatos de datos.
- Añade anotaciones. Marcar herramientas de solo lectura. Marcar herramientas que realizan llamadas de red externa. Identificar operaciones idempotentes. Excluya las herramientas destructivas a menos que tenga flujos de confirmación.
- Elige tu transporte. Utilice Streamable HTTP para servidores remotos, stdio para herramientas CLI locales. El SDK de MCP proporciona ambos.
- Enrute llamadas de herramientas a su API existente. No reescriba la lógica empresarial. Llame a sus propias rutas internamente. El servidor MCP es una capa adaptadora delgada.
El servidor MCP de Botoi consta de 4 archivos: curated-tools.ts (49 definiciones de herramientas), schema-builder.ts (Convertidor OpenAPI a Zod), server.ts (registro y enrutamiento), y tools.ts (punto final del manifiesto público). Todo esto agrega alrededor de 400 líneas de TypeScript a una API existente.
Pruébalo
El servidor Botoi MCP está activo en https://api.botoi.com/mcp. Conéctelo a Claude Desktop, Claude Code, Cursor o VS Code en menos de un minuto. Ver el Documentos de configuración de MCP para obtener fragmentos de configuración para cada cliente compatible.
Navega por el manifiesto de herramienta completo para ver las 49 definiciones de herramientas con sus esquemas y anotaciones. El Documentos API cubre el conjunto completo de más de 150 puntos finales REST detrás del servidor MCP.
FAQ
- ¿Cómo se construye un servidor MCP a partir de una especificación OpenAPI?
- Analice las definiciones de ruta de OpenAPI, extraiga el esquema del cuerpo de la solicitud (para POST) o los parámetros de consulta (para GET), convierta cada propiedad a un tipo Zod y luego registre cada herramienta en una instancia de McpServer con el esquema Zod como inputSchema. El SDK de MCP maneja el transporte JSON-RPC y el descubrimiento de herramientas.
- ¿Por qué no exponer todos los puntos finales de API como herramientas MCP?
- Los modelos de IA tienen un límite de ventana de contexto. Cada definición de herramienta consume tokens. Un manifiesto de 150 herramientas puede consumir más de 30.000 tokens antes de que comience la conversación. La selección de hasta 49 herramientas de alto valor mantiene el manifiesto por debajo de 8000 tokens y mejora la precisión de la selección de herramientas.
- ¿Qué son las anotaciones de herramientas MCP y por qué son importantes?
- Las anotaciones son sugerencias de metadatos como readOnlyHint, destructiveHint, idempotentHint y openWorldHint. Indican a los modelos de IA si una herramienta lee o escribe datos, si contacta con servicios externos y si es seguro volver a intentarlo. Los modelos utilizan estas sugerencias para planificar flujos de trabajo de varios pasos y evitar acciones destructivas sin confirmación.
- ¿Se puede ejecutar un servidor MCP en Cloudflare Workers?
- Sí. Utilice WebStandardStreamableHTTPServerTransport del SDK de MCP. Funciona con cualquier tiempo de ejecución que admita la API de solicitud/respuesta de estándares web. Cloudflare Workers, Deno Deploy y Vercel Edge Functions califican. Cada solicitud crea una nueva instancia de McpServer, por lo que no se necesita ningún estado de sesión.
- ¿Cómo debo escribir descripciones de herramientas MCP para modelos de IA?
- Comienza con un verbo. Indique qué hace la herramienta en una oración. Agregue una segunda oración que comience con "Usar cuando" que describa la condición de activación. Omita los detalles de implementación. Ejemplo: "Consultar registros DNS (A, AAAA, MX, TXT, CNAME, NS) para un dominio. Úselo cuando necesite verificar la configuración de DNS o solucionar problemas de resolución del dominio".
Empieza a construir con botoi
150+ endpoints de API para consultas, procesamiento de texto, generacion de imagenes y utilidades para desarrolladores. Plan gratuito, sin tarjeta de credito.