教程

現有 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 打握手、列表、讀和寫。預覽條款還在,限制以文檔當天的版本爲準。