Tutoriel
Confier une API REST existante à un agent IA : déclarer des outils MCP en OpenAPI 3.x sur Google API Gateway
L’API tourne déjà derrière la passerelle. Avant d’ajouter un serveur MCP, vérifiez si ce document OpenAPI peut devenir la liste d’outils.
Le 24 septembre 2026, Google a décrit une entrée concrète sur le blog développeurs : l’aperçu public de Cloud API Gateway lit la spec OpenAPI déjà déployée, accepte le JSON-RPC MCP sur la même passerelle, puis retraduit chaque appel en REST. L’annonce est Turn your REST APIs into MCP tools. Champs, contrôles et codes d’erreur suivent Configure Model Context Protocol, mis à jour le même jour et toujours sous conditions Pre-GA. La spec reste la source des docs et des types : générer documentation, types et client depuis OpenAPI. Si une limite de l’aperçu impose un serveur distant à vous, la forme de déploiement est dans mettre un MCP distant en production.
Quand l’annotation suffit
Ce qu’un agent doit appeler est le plus souvent déjà du REST. Le correctif habituel est un second serveur MCP qui réécrit chemins, auth et quotas, puis envoie du HTTP au backend. Le JWT, la clé API, le quota et les journaux restent sur la passerelle. L’agent ne les atteint simplement pas. Cet aperçu retire ce processus. Vous déployez la même config d’API, et MCP apparaît sur /mcp. tools/call devient la requête REST correspondante, sur le même chemin de politique, avec le même quota pour cette opération. Après transcodage, le backend ne distingue pas l’appel d’un REST direct.
L’aperçu couvre le REST, OpenAPI 3.x et l’auth déjà en place. Resources, prompts, streaming de réponse et Model Armor sont sur la feuille de route, pas dans cette version. Une opération qui renvoie un corps vide, comme un HTTP 204, ne devient pas un outil. Un object très imbriqué peut apparaître incomplet dans tools/list. Une passerelle sert au plus 1 000 outils. MCP et le routage de modèles ne tiennent pas dans la même config d’API. Un outil qui n’est pas une opération HTTP ne s’exprime pas par l’annotation. Écrivez alors votre serveur, et la forme d’entrée est dans MCP et JSON Schema.
L’interrupteur MCP d’API Gateway n’est pas celui d’Apigee. Google place Gateway comme rampe légère : le service est déjà sur Cloud Run, et vous voulez la gestion plus une entrée agent sans nouvelle pile. Cycle de vie, politique de trafic plus lourde et monétisation relèvent de MCP dans Apigee. Décider quels serveurs MCP externes un agent peut appeler en sortie, c’est Agent Gateway, pas cette extension OpenAPI. La bonne annotation sur le mauvais produit n’atteint pas la passerelle que vous exploitez.
| Ce que vous avez | Annoter la passerelle | Écrire un serveur MCP |
|---|---|---|
| L’opération est déjà du REST, auth et quota sur la passerelle | Commencer par cet aperçu | Seulement après une limite de l’aperçu |
| Il faut des resources, des prompts ou un résultat streamé | Pas disponible maintenant | À implémenter vous-même |
| L’outil n’est pas une opération HTTP | L’annotation ne peut pas l’exprimer | Écrire l’inputSchema vous-même |
Activer MCP, puis retirer les opérations que le modèle ne doit pas voir
MCP n’accepte qu’OpenAPI 3.0.x ou 3.1.x. Un document Swagger 2.0 ne devient pas une liste d’outils : migrez-le d’abord. L’interrupteur du document est x-google-api-management.mcp. À true, chaque opération éligible est exposée. Éligible veut dire GET, POST, PUT, PATCH ou DELETE, un backend résoluble, et une description non vide. Le nom d’outil par défaut est l’operationId. La description vient de description, sinon de summary.
Par opération, x-google-mcp-tool est un booléen ou un objet. false retire l’opération. L’objet remplace le nom et la description. Les noms doivent matcher [A-Za-z0-9_.-]{1,128} et rester uniques dans la spec. getOrderStatus passe le motif ; get_order_status est le nom que le modèle doit lire. Écrivez la description comme la situation d’appel, pas comme une reprise des champs de réponse. Cette phrase est le signal principal du choix d’outil.
Dès que mcp est un objet, pour attacher la sécurité à tools/list, MCP est ouvert pour chaque opération éligible. Cela ne veut pas dire « verrouiller la découverte et n’exposer rien ». Retirez une par une avec x-google-mcp-tool: false. L’extension n’est valide que sur une opération. Sur un path ou à la racine du document, l’envoi échoue. Chaque opération exposée a besoin d’un backend, soit x-google-backend sur l’opération, soit un défaut au niveau du document. Gardez le schéma JWT déjà en vigueur pour cette API. L’exemple ne cite que le nom orderServiceJwt ; il n’invente pas de bloc issuer.
Ce JSON est un seul contrat : MCP ouvert, un JWT nommé pour tools/list, des noms propres pour la création et la consultation, et la suppression explicitement retirée.
{
"openapi": "3.0.4",
"info": {
"title": "Order Service",
"version": "1.0.0"
},
"x-google-api-management": {
"mcp": {
"tools-list": {
"security": {
"orderServiceJwt": []
}
}
},
"backends": {
"orders-backend": {
"address": "https://orders.example.run.app"
}
}
},
"paths": {
"/orders": {
"post": {
"operationId": "createOrder",
"description": "Creates an order for a known SKU and quantity.",
"x-google-backend": "orders-backend",
"x-google-mcp-tool": {
"name": "create_order",
"description": "Create an order when the user gives a SKU and a quantity. Do not use this to check delivery status."
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["sku", "qty"],
"properties": {
"sku": { "type": "string" },
"qty": { "type": "integer" }
}
}
}
}
},
"responses": {
"201": { "description": "Created" }
}
}
},
"/orders/{orderId}": {
"get": {
"operationId": "getOrderStatus",
"description": "Returns status, carrier, and ETA for one order.",
"x-google-backend": "orders-backend",
"x-google-mcp-tool": {
"name": "get_order_status",
"description": "Look up delivery status and ETA when the user asks where an order is or when it will arrive."
},
"parameters": [
{
"name": "orderId",
"in": "path",
"required": true,
"schema": { "type": "string" }
}
],
"responses": {
"200": { "description": "Order status" }
}
},
"delete": {
"operationId": "deleteOrder",
"summary": "Cancels an order that has not shipped.",
"x-google-backend": "orders-backend",
"x-google-mcp-tool": false,
"parameters": [
{
"name": "orderId",
"in": "path",
"required": true,
"schema": { "type": "string" }
}
],
"responses": {
"200": { "description": "Cancelled" }
}
}
}
}
}
arguments n’est pas une copie à plat de l’appel REST
La passerelle reporte les arguments d’outil vers HTTP d’après le document OpenAPI. Les paramètres de path et de query deviennent des champs de premier niveau de arguments, clé égale au nom du paramètre. Les en-têtes aussi sont au premier niveau ; la passerelle les recopie sur la requête backend. On ne peut pas lier les en-têtes système réservés, ni ceux dont le nom commence par x-google-. Le corps de requête n’est pas aplati. Toute la valeur JSON se place sous une propriété nommée body. Une consultation de statut est {"orderId":"A-1042"}. Créer une commande est {"body":{"sku":"A-1042","qty":1}}.
Pour créer une commande, le corps JSON REST va sous arguments.body.
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "create_order",
"arguments": {
"body": {
"sku": "A-1042",
"qty": 1
}
}
}
}
Les modèles ratent cette couche plus souvent que les autres. Des arguments invalides reviennent en HTTP 200 avec le code JSON-RPC -32602. Beaucoup de clients traitent tout ce qui n’est pas 200 comme un échec de transport, donc la passerelle laisse les erreurs de protocole sur 200. Un échec métier du backend reste une réponse JSON-RPC réussie, avec result.isError à true et le corps backend dedans. Avant de toucher la spec, séparez trois couches : transport (401, 403, 405, 413), protocole (200 plus error.code), application (200 plus isError).
Cet appel oublie l’enveloppe body. sku et qty sont au premier niveau, la passerelle refuse les arguments.
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "create_order",
"arguments": {
"sku": "A-1042",
"qty": 1
}
}
}
Un object très imbriqué peut être incomplet dans tools/list. Quand le modèle ne voit pas tout le corps, il invente des champs. Gardez courte la couche montrée au modèle : champs obligatoires dans required, ensembles fermés dans enum. La poignée de main est initialize, avec protocolVersion égal à la chaîne documentée 2025-11-25. Ensuite, envoyez MCP-Protocol-Version sur chaque requête. Sans l’en-tête, la passerelle retombe sur 2025-03-26. Un notifications/initialized réussi est un HTTP 202 sans corps de résultat JSON-RPC.
Découvrir un outil et l’appeler, ce sont deux verrous
initialize et notifications/initialized ne sont pas authentifiés. tools/list ne l’est pas non plus par défaut. C’est commode en développement et, en production, cela publie noms d’outils et formes d’entrée à quiconque atteint /mcp. La doc demande de pointer mcp.tools-list.security vers exactement un schéma JWT sous components.securitySchemes. Une clé API ne protège pas tools/list. Nommer plusieurs schémas, ou une clé API, fait échouer l’envoi.
tools/call ignore le verrou de découverte. Il reprend ce que l’opération REST exige déjà. Si l’opération veut une clé API, l’appel la veut aussi. Si elle veut un JWT, l’appel veut un JWT. Verrouiller la découverte avec un JWT n’autorise pas l’appel. Un en-tête x-api-key sur la connexion ne sert que les opérations qui utilisent déjà une clé. Une fois la liste verrouillée, la requête de liste a aussi besoin d’un Bearer. Rangez les deux secrets à part.
Les journaux restent les métriques d’API Gateway. On distingue le trafic MCP du REST ordinaire par le chemin qui se termine par /mcp, ou par une métrique que vous ajoutez. Le backend ne reçoit pas d’en-tête magique « ceci vient d’un agent ». Le quota par appelant se fait dans la politique de la passerelle, avant le transcodage. Une fois la requête dans le service, elle est mêlée au REST direct.
| Méthode | Qui peut appeler par défaut | Secret accepté dans l’aperçu |
|---|---|---|
| initialize, notifications/initialized | Tout le monde | Pas d’auth |
| tools/list | Tout le monde, tant que vous ne verrouillez pas | Exactement un JWT si vous verrouillez |
| tools/call | Comme l’opération REST | Clé API ou JWT, selon l’opération |
Les échecs qui apparaissent à l’envoi de la spec
Le contrôle a lieu à la création de la config d’API, pas au premier tools/call. Une opération sans description résoluble est refusée. Ce texte peut venir de description, de summary ou de x-google-mcp-tool.description. Noms en double, nom hors motif, extension au mauvais endroit, méthode hors des cinq verbes : l’envoi échoue. Un HTTP 204 ne devient jamais un outil. Écrivez vous-même x-google-mcp-tool: false pour que « non exposé » soit une décision dans la spec, pas une surprise dans la liste.
Mettez les codes de protocole dans le runbook. -32700 avec HTTP 400 : le corps n’est pas du JSON. -32600 avec HTTP 200 : du JSON, mais pas une requête JSON-RPC valide, il manque jsonrpc, method ou un id requis. -32601 : méthode hors périmètre, par exemple ping, resources ou prompts. -32602 : mauvaise version de protocole, initialize sans protocolVersion chaîne, nom d’outil inconnu, ou arguments invalides. Vérifiez d’abord l’enveloppe body. -32000 : réponse trop grande, ou corps backend impossible à analyser. Un corps HTTP brut trop gros est un 413. Tout sauf POST sur /mcp est un 405.
401 et 403 restent des statuts HTTP et portent WWW-Authenticate, qui pointe vers les métadonnées de la ressource protégée. Ce n’est pas le même incident qu’un objet d’arguments faux. Si un client a mis en cache un ancien nom d’outil, -32602 Unknown tool veut dire : vérifier le déploiement d’abord, puis vider le cache. L’aperçu est fourni tel quel. Avant de traiter le MCP de la passerelle comme l’entrée, lancez initialize, tools/list, une lecture avec paramètre de path et une écriture qui envoie body, sur la même spec.
Traiter le JSON OpenAPI comme le contrat à relire
Noms d’outils, descriptions, forme sous body, et opérations mises à false vivent dans un seul JSON. Relisez la spec formatée. Confirmez que openapi est 3.0 ou 3.1, cherchez les descriptions vides, puis comparez la liste x-google-mcp-tool: false aux opérations que le produit veut vraiment exposer. Entre deux déploiements, le diff montre qui a réactivé la suppression.
Quand le modèle choisit le mauvais outil, changez la description de l’outil avant le prompt de session. La description est la phrase dans tools/list. Mettez « quand appeler » dans cette phrase, et exprimez « ne pas appeler la suppression » par un retrait explicite. Un prompt système toujours présent n’annule pas un outil déjà listé.
Trois contrôles suffisent avant le déploiement. Avec le formateur JSON étalez la spec, avec la validation JSON Schema comparez un échantillon de corps à arguments.body, et avec le diff JSON voyez qui a changé un retrait.
Questions fréquentes
Peut-on activer MCP directement avec OpenAPI 2.0 ?
Non. L’aperçu n’accepte qu’OpenAPI 3.0.x et 3.1.x. Migrez d’abord Swagger 2.0, puis ajoutez un backend, une description non vide et l’extension MCP. Les convertisseurs laissent souvent l’ancien emplacement des extensions. Remontez les backends au niveau document dans x-google-api-management.backends et référencez-les.
Une clé API peut-elle protéger tools/list ?
Non. Verrouiller la découverte exige exactement un schéma JWT déjà défini. Une clé API peut encore protéger un tools/call précis si l’opération REST exige déjà la clé. Gardez le secret de découverte et le secret d’appel séparés.
Le backend peut-il voir qu’une requête vient de MCP ?
Non. La doc dit qu’une requête transcodée est indistinguable d’un REST direct. Faites la comptabilité par appelant dans la politique de la passerelle. Un en-tête ajouté dans le service peut aussi entrer en collision avec vos propres clients REST.
Quand cela remplace un serveur MCP distant que vous exploitez ?
Si l’opération est déjà derrière API Gateway et dans les limites de l’aperçu, annotez la spec. Construisez votre serveur s’il vous faut des resources, des prompts, un résultat streamé, plus de 1 000 outils, un corps vide, ou un outil qui n’est pas une opération HTTP. Si la même config d’API a aussi besoin du routage de modèles, MCP et routage ne peuvent pas être actifs ensemble : séparez la config.
À retenir
L’aperçu du 24 septembre rend optionnel « écrire un autre serveur MCP ». Le contrat reste OpenAPI 3.x : interrupteur de document, retrait par opération, nom et description écrits pour le modèle, path et query en haut de arguments, corps de requête sous body.
Avant la mise en ligne, verrouillez tools/list sur un JWT, confirmez que les 204 et les suppressions ne sont pas entrés dans la liste, puis lancez poignée de main, liste, lecture et écriture sur ce même JSON. Les conditions d’aperçu s’appliquent encore. Revérifiez les limites sur la doc avec laquelle vous déployez.