Руководство

Как отдать существующий REST API ИИ-агенту: объявить MCP-инструменты в OpenAPI 3.x на Google API Gateway

API уже стоит за шлюзом. Прежде чем поднимать ещё один MCP-сервер, проверьте, может ли этот документ OpenAPI стать списком инструментов.

24 сентября 2026 Google описал конкретный вход в блоге для разработчиков: публичный превью Cloud API Gateway читает уже развёрнутую спецификацию OpenAPI, принимает MCP JSON-RPC на том же шлюзе и переводит каждый вызов обратно в REST. Анонс — Turn your REST APIs into MCP tools. Поля, проверки и коды ошибок берутся из Configure Model Context Protocol, обновлённого в тот же день и по-прежнему на условиях Pre-GA. Спецификация остаётся источником документации и типов: документация, типы и клиент из OpenAPI. Если предел превью заставляет поднимать свой удалённый сервер, форма выкладки — в удалённый MCP в проде.

Когда хватает аннотации

То, что агент должен вызывать, чаще всего уже REST. Обычная заплатка — второй MCP-сервер, который заново описывает пути, аутентификацию и квоты, а затем шлёт HTTP в бэкенд. JWT, API-ключ, квота и журналы остаются на шлюзе. Агент до них просто не доходит. Этот превью убирает лишний процесс. Вы выкладываете ту же конфигурацию API, и MCP появляется на /mcp. tools/call становится соответствующим REST-запросом, по тому же пути политик и с той же квотой операции. После перекодирования бэкенд не отличает вызов от прямого REST.

Превью покрывает REST, OpenAPI 3.x и уже настроенную аутентификацию. Resources, prompts, потоковые ответы и Model Armor — в дорожной карте, не в этом выпуске. Операции с пустым телом, например HTTP 204, инструментами не становятся. Глубоко вложенный object может прийти в tools/list неполностью. Один шлюз отдаёт не больше 1000 инструментов. MCP и маршрутизацию моделей нельзя включить в одной конфигурации API. Инструмент, который не является HTTP-операцией, аннотацией не выразить. Для него пишите свой сервер, форму входа смотрите в MCP и JSON Schema.

Переключатель MCP у API Gateway — не тот, что у Apigee. Google ставит Gateway как лёгкий вход: сервис уже на Cloud Run, и нужны управление плюс вход для агента без нового стека. Жизненный цикл, более тяжёлая политика трафика и монетизация — это MCP в Apigee. Какие внешние MCP-серверы агент может вызывать на выходе, решает Agent Gateway, а не это расширение OpenAPI. Верная аннотация на неверном продукте не попадает на шлюз, который вы реально эксплуатируете.

Что уже есть Аннотировать шлюз Писать MCP-сервер
Операция уже REST, аутентификация и квота на шлюзеСначала этот превьюТолько когда упрётесь в предел превью
Нужны resources, prompts или потоковый результатСейчас нельзяДелать самим
Инструмент — не HTTP-операцияАннотация это не выразитinputSchema писать самим

Включить MCP, затем убрать операции, которые модель видеть не должна

MCP принимает только OpenAPI 3.0.x или 3.1.x. Документ Swagger 2.0 списком инструментов не станет — сначала мигрируйте. Переключатель документа — x-google-api-management.mcp. Значение true открывает каждую допустимую операцию. Допустимая — это GET, POST, PUT, PATCH или DELETE, разрешимый backend и непустое описание. Имя инструмента по умолчанию — operationId. Описание берётся из description, иначе из summary.

На операции x-google-mcp-tool — это логическое значение или объект. false исключает операцию. Объект заменяет имя и описание. Имена должны совпадать с [A-Za-z0-9_.-]{1,128} и быть уникальными во всей спецификации. getOrderStatus шаблон проходит; модели лучше показывать get_order_status. В описании пишите ситуацию вызова, а не перечень полей ответа. Эту фразу модель и читает, когда выбирает инструмент.

Как только mcp становится объектом — чтобы повесить security на tools/list — MCP включён для каждой допустимой операции. Это не режим «закрыть обнаружение и ничего не публиковать». Исключайте по одной через x-google-mcp-tool: false. Расширение допустимо только на операции. На path или в корне документа загрузка отклоняется. У каждой публикуемой операции должен находиться backend: x-google-backend на операции или значение по умолчанию на уровне документа. JWT-схему оставьте ту, что уже работает для этого API. В примере только имя orderServiceJwt, новый блок issuer не придумывается.

Этот JSON — один контракт: MCP включён, для tools/list назван один JWT, у создания и поиска свои имена, удаление явно исключено.

{
  "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 — не плоская копия REST-вызова

Шлюз отображает аргументы инструмента обратно в HTTP по документу OpenAPI. Параметры path и query становятся полями верхнего уровня arguments, ключ — имя параметра. Заголовки тоже на верхнем уровне; шлюз переносит их в запрос к бэкенду. Нельзя привязать зарезервированные системные заголовки и заголовки, имя которых начинается с x-google-. Тело запроса не расплющивается. Всё JSON-значение лежит в свойстве с именем body. Запрос статуса — {"orderId":"A-1042"}. Создание заказа — {"body":{"sku":"A-1042","qty":1}}.

При создании заказа JSON-тело REST кладётся под arguments.body.

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "create_order",
    "arguments": {
      "body": {
        "sku": "A-1042",
        "qty": 1
      }
    }
  }
}

Модели пропускают этот слой чаще остальных. Неверные аргументы возвращаются как HTTP 200 с кодом JSON-RPC -32602. Многие клиенты считают любой не-200 сбоем транспорта, поэтому ошибки протокола остаются на 200. Прикладная ошибка бэкенда — это всё ещё успешный JSON-RPC, с result.isError равным true и телом бэкенда внутри. Прежде чем менять спецификацию, разделите три слоя: транспорт (401, 403, 405, 413), протокол (200 плюс error.code), приложение (200 плюс isError).

В этом вызове нет обёртки body. sku и qty лежат на верхнем уровне, шлюз отклоняет аргументы.

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "create_order",
    "arguments": {
      "sku": "A-1042",
      "qty": 1
    }
  }
}

Глубоко вложенный object может отобразиться в tools/list неполностью. Если модель не видит всё тело, она выдумывает поля. Слой, который вы показываете модели, держите коротким: обязательные поля в required, замкнутые наборы в enum. Рукопожатие — это initialize, protocolVersion равен документированной строке 2025-11-25. Дальше на каждый запрос ставьте MCP-Protocol-Version. Без заголовка шлюз откатывается к 2025-03-26. Успешный notifications/initialized — это HTTP 202 без тела результата JSON-RPC.

Найти инструмент и вызвать его — разные замки

initialize и notifications/initialized без аутентификации. tools/list по умолчанию тоже. В разработке это удобно, в проде это публикует имена инструментов и форму входа любому, кто достучался до /mcp. Документация требует, чтобы mcp.tools-list.security указывал ровно на одну JWT-схему в components.securitySchemes. API-ключ tools/list не защищает. Несколько схем или API-ключ роняют загрузку.

tools/call этот замок обнаружения не смотрит. Он повторяет то, что REST-операция уже требует. Если операции нужен API-ключ, вызову тоже нужен ключ. Если нужен JWT — вызову нужен JWT. Закрыть обнаружение через JWT не значит авторизовать вызов. Заголовок x-api-key на соединении обслуживает только те операции, которые ключ уже используют. Когда список закрыт, запросу списка нужен ещё и Bearer. Два секрета храните отдельно.

Журналы остаются метриками API Gateway. MCP-трафик от обычного REST отличает путь, который кончается на /mcp, или своя метрика. Бэкенд не получает волшебный заголовок «это пришло от агента». Квота по вызывающему ставится в политике шлюза, до перекодирования. Внутри сервиса запрос уже смешан с прямым REST.

Метод Кто может вызвать по умолчанию Какой секрет принимает превью
initialize, notifications/initializedКто угодноБез аутентификации
tools/listКто угодно, пока не закроетеРовно один JWT, если закрываете
tools/callКак у этой REST-операцииAPI-ключ или JWT, как написано у операции

Ошибки, которые видны уже при загрузке спецификации

Проверка идёт при создании конфигурации API, а не на первом tools/call. Операция без разрешимого описания отклоняется. Текст может прийти из description, summary или x-google-mcp-tool.description. Повтор имени, имя вне шаблона, расширение не на том месте и метод вне пяти глаголов — это сбой загрузки. HTTP 204 инструментом не станет. Напишите x-google-mcp-tool: false сами, чтобы «не публиковать» было решением в спецификации, а не сюрпризом в списке.

Коды протокола держите в runbook. -32700 и HTTP 400: тело — не JSON. -32600 и HTTP 200: это JSON, но не корректный JSON-RPC, нет jsonrpc, method или нужного id. -32601: метод вне набора, например ping, resources или prompts. -32602: неверная версия протокола, в initialize нет строкового protocolVersion, неизвестное имя инструмента или неверные аргументы. Сначала проверьте обёртку body. -32000: ответ слишком большой или тело бэкенда не разобрать. Слишком большое сырое тело HTTP — это 413. Всё, кроме POST на /mcp, — это 405.

401 и 403 остаются HTTP-статусами и несут WWW-Authenticate со ссылкой на метаданные защищённого ресурса. Это другой инцидент, не ошибка объекта аргументов. Если клиент закэшировал старое имя инструмента, -32602 Unknown tool значит: сначала сверить выкладку, потом сбросить кэш. Превью даётся как есть. Прежде чем считать MCP шлюза входом, прогоните по той же спецификации initialize, tools/list, одно чтение с параметром пути и одну запись с body.

Считать OpenAPI JSON контрактом, который вы ревьюите

Имена инструментов, описания, форма под body и операции со значением false живут в одном JSON. Ревьюьте отформатированную спецификацию. Убедитесь, что openapi — 3.0 или 3.1, найдите пустые описания, затем сверьте список x-google-mcp-tool: false с операциями, которые продукт действительно хочет публиковать. Между двумя выкладками diff показывает, кто снова включил удаление.

Если модель выбирает не тот инструмент, сначала меняйте описание инструмента, потом промпт сессии. Описание — это фраза внутри tools/list. «Когда вызывать» пишите в эту фразу, а «не вызывать удаление» выражайте явным исключением. Системный промпт, который всегда на месте, не отменяет инструмент, уже попавший в список.

Перед выкладкой хватает трёх проверок. Форматирование JSON раскладывает спецификацию, проверка JSON Schema сверяет образец тела с arguments.body, а сравнение JSON показывает, кто изменил исключение.

Частые вопросы

Можно ли включить MCP прямо на OpenAPI 2.0?

Нет. Превью принимает только OpenAPI 3.0.x и 3.1.x. Сначала переведите Swagger 2.0, затем добавьте backend, непустое описание и расширение MCP. Конвертеры часто оставляют старое место расширений. Поднимите backend на уровень документа в x-google-api-management.backends и ссылайтесь оттуда.

Может ли API-ключ защитить tools/list?

Нет. Чтобы закрыть обнаружение, нужно назвать ровно одну уже определённую JWT-схему. API-ключ по-прежнему может защищать конкретный tools/call, если эта REST-операция и так требует ключ. Секрет обнаружения и секрет вызова не склеивайте.

Может ли бэкенд понять, что запрос пришёл из MCP?

Нет. В документации перекодированный запрос программно неотличим от прямого REST. Учёт по вызывающему делайте в политике шлюза. Заголовок, который вы добавите внутри сервиса, может столкнуться и с вашими собственными REST-клиентами.

Когда это заменяет удалённый MCP-сервер, который вы ведёте сами?

Если операция уже за API Gateway и укладывается в пределы превью — аннотируйте спецификацию. Свой сервер нужен, когда требуются resources, prompts, потоковый результат, больше 1000 инструментов, пустое тело или инструмент, который не является HTTP-операцией. Если той же конфигурации API нужна ещё и маршрутизация моделей, MCP и маршрутизацию вместе не включить: разделите конфигурацию.

Итог и следующий шаг

Превью от 24 сентября делает «написать ещё один MCP-сервер» необязательным шагом. Контракт по-прежнему OpenAPI 3.x: переключатель документа, исключение по операции, имя и описание для модели, path и query на верхнем уровне arguments, тело запроса под body.

Перед выпуском закройте tools/list на JWT, убедитесь, что 204 и операции удаления не попали в список, затем прогоните рукопожатие, список, чтение и запись по тому же JSON. Условия превью всё ещё действуют. Пределы сверьте с документацией того дня, когда выкладываете.