教程

现有 REST API 如何交给 AI Agent:用 OpenAPI 3.x 在 Google API Gateway 上声明 MCP 工具

接口已经在网关后面跑着。Agent 要调它,先看这份 OpenAPI 能不能直接变成工具,再决定要不要另写一台 MCP Server。

2026 年 9 月 24 日,Google 在开发者博客给出一条可落地的入口:Cloud API Gateway 公共预览按你已经部署的 OpenAPI,在同一条网关上接收 MCP 的 JSON-RPC,再转成原来的 REST。公告见 Turn your REST APIs into MCP tools。字段、校验和错误码以当天更新的 Configure Model Context Protocol 为准,页首写明仍受 Pre-GA 条款约束。规格继续当文档和类型的源,见 用 OpenAPI 生成文档、类型和 Client。预览限制让你仍要自建远程服务时,部署形状见 Remote MCP 如何进生产。

何时注解就够了

能被 Agent 调用的能力,多数已经是 REST。常见补法是旁边再起一台 MCP Server,把路径、鉴权和配额重写一遍,再对后端发 HTTP。网关上的 JWT、API Key、配额和日志还在,Agent 只是走不到。这条预览省的就是这一层:你仍部署原来的 API config,MCP 出现在网关的 /mcp。tools/call 被转成对应的 REST,策略路径与直接调接口相同,同一操作共用一份配额。转码后的请求和普通 REST 在后端无法用程序区分。

预览覆盖的是 REST、OpenAPI 3.x,以及你现有的鉴权。Resources、Prompts、响应流式和 Model Armor 在路线图上,当前没有。返回空 body 的操作(HTTP 204)不会变成工具。深层嵌套的 object 在 tools/list 里可能展示不全。一个网关最多 1000 个工具。同一份 API config 不能同时打开 MCP 和 model routing。工具若根本不是某个 HTTP 操作,注解也接不住,仍要自建 Server,入参 Schema 怎么写见 MCP 与 JSON Schema。

API Gateway 和 Apigee 的 MCP 不是同一个开关。Google 把 Gateway 放在轻量入口:服务已经在 Cloud Run 上,想尽快加上管理和 Agent 入口。要生命周期、更重的流量策略和变现,走 Apigee 的 MCP。出站要管 Agent 去调哪些外部 MCP,那是 Agent Gateway,不是这份 OpenAPI 扩展。产品选错,注解写对也落不到你正在用的网关。

你手里的东西 注解进 Gateway 另写 MCP Server
操作已是 REST,鉴权和配额在网关先走这条预览撞上预览限制时再写
需要 resources、prompts 或流式结果现在做不到自己实现
工具并不是一个 HTTP 操作接不住自己写 inputSchema

全局打开,再把不该给模型的接口关掉

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 一旦写成对象,为了给 tools/list 配 security,效果同样是全局打开,不是「只锁发现、不暴露接口」。不想全暴露,就逐个写 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 不是同一层

网关按 OpenAPI 把工具参数映射回 HTTP。path 和 query 变成 arguments 的顶层字段,键就是参数名。header 也在顶层,网关再写进后端请求头。系统保留头,以及以 x-google- 开头的头,不能拿来绑定。请求体不摊平:整个 JSON 放在名为 body 的属性里。查询订单是 {"orderId":"A-1042"}。创建订单是 {"body":{"sku":"A-1042","qty":1}}。

创建订单时,REST 的 JSON body 要放在 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 指向 components.securitySchemes 里恰好一个 JWT。API Key 不能保护 tools/list。点了多个 scheme,或点了 API Key,上传规格时就会失败。

tools/call 不看这把发现锁。它复用该 REST 操作自己的鉴权:操作要 API Key 就还要 API Key,要 JWT 就还要 JWT。发现改成 JWT,不代表调用已经通过。连接头里的 x-api-key 只服务那些本来就用 Key 的操作。列表请求若已上锁,还要另带 Bearer。两套凭证分开存放。

日志仍是 API Gateway 的指标。区分 MCP 与普通 REST,看路径是否落在 /mcp,或自己加自定义指标。后端没有一个魔法头写着「这是 Agent」。调用方配额要在转码前的网关策略里做。请求进了服务再猜来源,已经和直接 REST 混在一起。

方法 默认谁能调 预览里能用的凭证
initialize、notifications/initialized任何人不鉴权
tools/list默认任何人要锁就只能点名一个 JWT
tools/call与该 REST 操作相同API Key 或 JWT,看操作自己

上传规格时就会失败的条件

校验发生在创建 API config 时,不是等第一次 tools/call。没有非空描述的操作会被拒绝:description、summary,或 x-google-mcp-tool.description,至少要有一段。工具名重复、正则不符、扩展写错位置、方法不在五种之内,都是上传失败。HTTP 204 不会变成工具。与其等列表里找不到,不如在规格里写上 x-google-mcp-tool: false,让「不暴露」是你的决定。

协议错误码写进 runbook。-32700 且 HTTP 400:body 不是 JSON。-32600 且 HTTP 200:是 JSON,但不是合法 JSON-RPC,缺 jsonrpc、method 或该有的 id。-32601:方法不在范围内,例如 ping、resources、prompts。-32602:协议版本不对、initialize 缺 protocolVersion、工具名不存在,或参数不合法,先查是不是忘了 body。-32000:响应过大,或后端响应解析不了。原始 HTTP body 过大是 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 的清单和产品要暴露的清单。两次部署之间用对比看是谁把删除接口又打开了。

模型选错工具时,先改工具描述,再改会话提示词。描述是 tools/list 里模型读到的那句。把「何时调用」写进描述,把不该调用的删除写成显式退出。系统提示词每次都在,挡不住一份已经列出去的工具。

部署前三步就够:用JSON 格式化把规格摊开,用JSON Schema 校验对一下请求体样例和 arguments.body,再用JSON 对比看这次谁改动了退出标志。

常见问题

OpenAPI 2.0 能直接打开 MCP 吗?

不能。预览只接受 OpenAPI 3.0.x 和 3.1.x。Swagger 2.0 要先迁到 3.x,再补 backend、非空描述和 MCP 扩展。转换工具常常留下旧的扩展位置,backend 要从操作内嵌改成文档级 x-google-api-management.backends 再引用。

API Key 能保护 tools/list 吗?

不能。发现接口要锁,只能点名一个已经定义的 JWT。API Key 仍然可以保护具体的 tools/call,前提是那个 REST 操作本来就要求 Key。发现和调用的凭证不要合成一个。

后端能分辨请求来自 MCP 吗?

不能。文档写明转码后的请求与直接 REST 无法用程序区分。要按调用方记账,在网关策略里做。服务里临时加的头,也可能和你自己的 REST 客户端撞车。

和自建 Remote MCP 怎么选?

操作已经在 Gateway 后面,又落在预览能力里,就注解规格。需要 resources、prompts、流式结果、超过 1000 个工具、空 body,或工具不是 HTTP 操作,就自建 Server。同一份 API config 还要做 model routing 时,MCP 和路由不能同时开,只能拆 config。

总结与下一步

9 月 24 日这条预览把「再写一台 MCP Server」从默认动作改成可选项。合同仍是 OpenAPI 3.x:全局开关、逐操作退出、给模型看的名字和描述、path 与 query 在 arguments 顶层、请求体放在 body 下。

上线前锁住 tools/list 的 JWT,确认 204 和删除类操作没有漏进列表,再用同一份 JSON 打握手、列表、读和写。预览条款还在,限制以文档当天的版本为准。