Serveur OpenAPI vers MCP : 150 points de terminaison, 49 outils d'IA
Comment nous avons converti une spécification OpenAPI en un serveur MCP organisé avec 49 outils. Conversion de schéma, descriptions d'outils, annotations et transport HTTP sans état.
L'API REST de Botoi compte plus de 150 points de terminaison. Lorsque nous avons construit le serveur MCP, nous en avons enregistré 49 en tant qu'outils. Pas parce que le reste ne fonctionne pas. Parce que donner 150 outils à un modèle d’IA, c’est comme donner à quelqu’un un menu de 200 pages ; ils choisiront quelque chose, mais ce ne sera pas la bonne chose.
Cet article décrit le processus complet : organiser la liste d'outils, convertir les schémas OpenAPI en objets Zod, rédiger des descriptions que les modèles d'IA analysent bien, ajouter des annotations MCP et exécuter le tout en tant que Cloudflare Worker sans état. Si vous gérez une API et souhaitez créer un serveur MCP à partir de celle-ci, voici le playbook.
Pourquoi 49 outils, pas 150
Chaque outil que vous enregistrez sur un serveur MCP est sérialisé dans la fenêtre contextuelle du modèle. Le nom de l'outil, la description et le schéma d'entrée complet consomment tous des jetons. Un manifeste de 150 outils peut graver plus de 30 000 jetons avant que l'utilisateur ne tape un seul mot.
Cela crée deux problèmes :
- Moins de jetons restants pour la conversation elle-même
- Le modèle choisit le mauvais outil plus souvent lorsque la liste est longue
Nous avons testé cela. Avec plus de 150 points de terminaison enregistrés, Claude a choisi le bon outil dès la première tentative dans environ 72 % du temps. Avec 49 outils sélectionnés, ce nombre est passé à 94 %. La liste plus petite et ciblée a rendu le modèle meilleur dans son travail.
Les critères de curation étaient simples :
- Un agent IA a-t-il besoin de cette conversation à mi-parcours ? (Recherche DNS : oui. Génération de PDF : rarement.)
- L'outil renvoie-t-il des données structurées sur lesquelles le modèle peut raisonner ? (JSON : oui. Image binaire : non.)
- Le modèle peut-il remplir les paramètres requis à partir du langage naturel ? (Nom de domaine : oui. Objets de configuration imbriqués complexes : non.)
La structure du manifeste de l'outil
Chaque outil organisé mappe un nom d'outil MCP à un chemin d'API, une méthode HTTP, une description et des annotations. Voici l'interface TypeScript :
export interface CuratedTool {
path: string;
method: 'get' | 'post';
title: string;
description: string;
annotations: {
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
openWorldHint?: boolean;
};
}
Et voici à quoi ressemblent deux entrées en pratique :
// 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 et method les champs pointent vers le point de terminaison REST existant. Le description indique au modèle quand utiliser l'outil. Le annotations indiquer au modèle comment l'outil se comporte.
Conversion des schémas OpenAPI en Zod
Le SDK MCP attend des schémas d'entrée d'outils sous forme d'objets Zod. Notre API dispose déjà d'une spécification OpenAPI 3.1 avec des définitions complètes du corps de la requête pour chaque point de terminaison. Le constructeur de schéma lit ces définitions et génère des types Zod au démarrage du serveur.
La fonction de conversion principale mappe chaque type de propriété OpenAPI à son équivalent 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;
}
Les décisions clés dans cette fonction :
enumles valeurs deviennentz.enum(), donnant au modèle un ensemble fixe d'options valides- Les champs obligatoires restent obligatoires ; les champs facultatifs obtiennent
.optional() - L'OpenAPI
descriptionreporté via.describe(), que le SDK MCP inclut dans le manifeste de l'outil - Les valeurs par défaut se propagent via
.default()
La buildZodSchema La fonction gère à la fois les points de terminaison POST (corps de la requête) et GET (paramètres de requête) :
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;
}
Cette fonction s'exécute une fois par outil lors de la création du serveur. Ça rend un appartement Record<string, z.ZodTypeAny> que le SDK MCP sérialise dans le schéma JSON pour le manifeste de l'outil.
Rédaction de descriptions d'outils Les modèles d'IA analysent bien
La description de l'outil est le champ le plus important pour une sélection correcte de l'outil. Les modèles le lisent pour décider si un outil correspond à l'intention de l'utilisateur. Des descriptions vagues conduisent à de mauvais choix d’outils.
Nous avons opté pour un modèle en deux phrases :
- Première phrase : que fait l'outil, commençant par un verbe. Incluez les types ou formats de données spécifiques qu’il gère.
- Deuxième phrase : quand l'utiliser, commençant par "Utiliser quand". Cela donne au modèle une condition de déclenchement.
Comparez ces deux descriptions pour le même outil de recherche DNS :
| Version | Description | Problème |
|---|---|---|
| Mauvaise | "Outil DNS pour rechercher des choses" | Aucun type d'enregistrement répertorié, aucune condition de déclenchement, vague |
| Bien | "Interrogez les enregistrements DNS (A, AAAA, MX, TXT, CNAME, NS) pour un domaine. À utiliser lorsque vous devez vérifier la configuration DNS ou dépanner la résolution du domaine." | Aucune |
La bonne version indique au modèle les types d'enregistrements exacts qu'il peut interroger (il sait donc que cet outil gère les recherches MX) et les situations qui devraient le déclencher (vérifications de configuration DNS, dépannage). Le modèle fait correspondre l'intention de l'utilisateur à ces mots-clés.
Annotations MCP : indiquer aux modèles comment se comportent les outils
Les annotations sont des indicateurs de métadonnées sur chaque outil. Ils n'affectent pas l'exécution. Ils indiquent au modèle à quels types d’effets secondaires il faut s’attendre.
// 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 },
}
Les quatre annotations et ce qu'elles signalent :
| Annotation | Signal | Exemple |
|---|---|---|
readOnlyHint |
Cet outil lit les données mais ne modifie jamais rien | Recherche DNS, WHOIS, vérification SSL |
destructiveHint |
Cet outil supprime ou écrase les données | Suppression de la boîte de réception Webhook (pas dans notre ensemble organisé) |
idempotentHint |
Appeler cet outil deux fois avec la même entrée produit le même résultat | Cryptage AES, déchiffrement AES |
openWorldHint |
Cet outil effectue des requêtes réseau externes | Recherche IP, métadonnées URL, détection technique |
Sur nos 49 outils, 44 portent readOnlyHint: true. Les 12 outils de recherche comportent également openWorldHint: true car ils appellent des serveurs DNS externes, des registres WHOIS ou récupèrent des pages Web en direct. Les outils de chiffrement/déchiffrement portent idempotentHint: true parce que ce sont des transformations déterministes.
Aucun de nos outils sélectionnés ne contient destructiveHint. C'était un choix délibéré. Nous avons exclu les outils tels que la suppression de la boîte de réception des webhooks et la suppression par collage de l'ensemble organisé, car les modèles d'IA ne devraient pas supprimer les données utilisateur sans de solides garde-fous.
Enregistrement des outils sur le serveur MCP
La boucle d’enregistrement relie tout ensemble. Il parcourt la liste d'outils organisée, construit le schéma Zod à partir de la spécification OpenAPI et enregistre chaque outil avec sa description et ses annotations :
// 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;
}
Lorsque le modèle appelle un outil, la fonction de gestionnaire reçoit les arguments analysés et les transmet à la route API interne. Le callApi La fonction crée une requête HTTP interne et renvoie la réponse sous forme de contenu au format 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 Le modèle permet au serveur MCP d'appeler les routes API via une référence de fonction interne au lieu de faire une requête HTTP externe. Cela évite les allers-retours sur le réseau. Le gestionnaire MCP et les routes API s'exécutent dans le même Cloudflare Worker, le routage interne est donc un appel de fonction, pas un saut HTTP.
Transport HTTP sans état sur Cloudflare Workers
MCP prend en charge deux transports : stdio (pour les processus locaux) et Streamable HTTP (pour les serveurs distants). Nous avons choisi Streamable HTTP car le serveur fonctionne sur Cloudflare Workers, qui ne prend pas en charge les processus de longue durée.
// 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);
});
Chaque demande crée un nouveau McpServer exemple. Aucun état de session ne persiste entre les requêtes. C'est très bien car chaque appel d'outil est autonome ; le modèle envoie le nom de l'outil et les arguments, et le serveur renvoie le résultat. Aucune transaction en plusieurs étapes.
La conception sans état présente trois avantages :
- Aucun stockage de session nécessaire (pas de Redis, pas de KV, pas de base de données)
- Mise à l'échelle à zéro au repos, mise à l'échelle horizontale sous charge
- Se déploie sur plus de 300 emplacements périphériques Cloudflare sans configuration
La gestion des clés API s'effectue au niveau de la couche MCP. Le client envoie la clé via X-API-Key ou Authorization: Bearer en-tête. La route MCP l'extrait et le transmet à l'appel API interne. Aucun middleware d'authentification distinct sur la route MCP elle-même.
Le playbook pour votre propre API
Si vous disposez d'une spécification OpenAPI et souhaitez créer un serveur MCP, voici la version condensée :
- Organisez votre liste d’outils. Choisissez les 20 à 80 points de terminaison qui renvoient des données structurées et acceptent des entrées simples. Ignorez les points de terminaison qui renvoient des données binaires, nécessitent des téléchargements de fichiers ou ont des schémas d'entrée profondément imbriqués.
- Écrivez un convertisseur de schéma. Mappez vos types de propriétés OpenAPI sur Zod. Reportez les descriptions, les valeurs par défaut et les valeurs d'énumération. Gérez à la fois les modèles de corps de requête (POST) et de paramètres de requête (GET).
- Écrivez des descriptions en deux phrases. Première phrase : ce que fait l'outil, en commençant par un verbe. Deuxième phrase : « Utiliser quand » + condition de déclenchement. Soyez précis sur les types et les formats de données.
- Ajoutez des annotations. Marquez les outils en lecture seule. Signaler les outils qui effectuent des appels réseau externes. Identifiez les opérations idempotentes. Excluez les outils destructeurs, sauf si vous disposez de flux de confirmation.
- Choisissez votre moyen de transport. Utilisez Streamable HTTP pour les serveurs distants, stdio pour les outils CLI locaux. Le SDK MCP fournit les deux.
- Acheminez les appels d’outils vers votre API existante. Ne réécrivez pas la logique métier. Appelez vos propres itinéraires en interne. Le serveur MCP est une fine couche d'adaptateur.
Le serveur MCP de Botoi est composé de 4 fichiers : curated-tools.ts (49 définitions d'outils), schema-builder.ts (Convertisseur OpenAPI vers Zod), server.ts (enregistrement et routage), et tools.ts (point de terminaison du manifeste public). Le tout ajoute environ 400 lignes de TypeScript à une API existante.
Essayez-le
Le serveur Botoi MCP est en ligne sur https://api.botoi.com/mcp. Connectez-le à Claude Desktop, Claude Code, Cursor ou VS Code en moins d'une minute. Voir le Documents de configuration MCP pour les extraits de configuration pour chaque client pris en charge.
Parcourez le manifeste complet de l'outil pour voir les 49 définitions d'outils avec leurs schémas et annotations. Le Documents sur l'API couvrent l'ensemble complet de plus de 150 points de terminaison REST derrière le serveur MCP.
FAQ
- Comment créer un serveur MCP à partir d'une spécification OpenAPI ?
- Analysez vos définitions de chemin OpenAPI, extrayez le schéma du corps de la requête (pour POST) ou les paramètres de requête (pour GET), convertissez chaque propriété en type Zod, puis enregistrez chaque outil sur une instance McpServer avec le schéma Zod comme inputSchema. Le SDK MCP gère le transport JSON-RPC et la découverte d'outils.
- Pourquoi ne pas exposer tous les points de terminaison de l'API en tant qu'outils MCP ?
- Les modèles d'IA ont une limite de fenêtre contextuelle. Chaque définition d'outil consomme des jetons. Un manifeste de 150 outils peut consommer plus de 30 000 jetons avant le début de la conversation. La conservation de 49 outils de grande valeur maintient le manifeste sous 8 000 jetons et améliore la précision de la sélection des outils.
- Que sont les annotations de l’outil MCP et pourquoi sont-elles importantes ?
- Les annotations sont des indications de métadonnées telles que readOnlyHint, destructiveHint, idempotentHint et openWorldHint. Ils indiquent aux modèles d’IA si un outil lit ou écrit des données, s’il contacte des services externes et s’il est sécuritaire de réessayer. Les modèles utilisent ces astuces pour planifier des flux de travail en plusieurs étapes et éviter les actions destructrices sans confirmation.
- Un serveur MCP peut-il fonctionner sur Cloudflare Workers ?
- Oui. Utilisez WebStandardStreamableHTTPServerTransport à partir du SDK MCP. Il fonctionne avec n'importe quel environnement d'exécution prenant en charge l'API de requête/réponse des normes Web. Cloudflare Workers, Deno Deploy et Vercel Edge Functions sont tous éligibles. Chaque requête crée une nouvelle instance McpServer, aucun état de session n'est donc nécessaire.
- Comment dois-je rédiger des descriptions d’outils MCP pour les modèles d’IA ?
- Commencez par un verbe. Énoncez ce que fait l’outil en une phrase. Ajoutez une deuxième phrase commençant par « Utiliser quand » qui décrit la condition de déclenchement. Ignorer les détails de mise en œuvre. Exemple : « Interroger les enregistrements DNS (A, AAAA, MX, TXT, CNAME, NS) pour un domaine. À utiliser lorsque vous devez vérifier la configuration DNS ou dépanner la résolution du domaine. »
Commencez a construire avec botoi
150+ endpoints API pour la recherche, le traitement de texte, la generation d'images et les utilitaires pour developpeurs. Offre gratuite, sans carte bancaire.