튜토리얼
기존 REST API를 AI 에이전트에 넘기는 방법: OpenAPI 3.x로 Google API Gateway에 MCP 도구를 선언하기
API는 이미 게이트웨이 뒤에서 돕니다. MCP 서버를 하나 더 세우기 전에, 이 OpenAPI가 도구 목록이 될 수 있는지 먼저 보십시오.
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로 문서, 타입, 클라이언트를 생성하는 글에 있습니다. 프리뷰 한계 때문에 원격 서버를 직접 띄워야 하면 배치 형태는 Remote MCP를 프로덕션에 올리는 글을 보십시오.
주석만으로 충분한 때
에이전트가 호출할 능력의 대부분은 이미 REST입니다. 흔한 보완은 MCP 서버를 옆에 하나 더 세워 경로, 인증, 할당량을 다시 쓰고 백엔드로 HTTP를 보내는 것입니다. 게이트웨이의 JWT, API 키, 할당량, 로그는 그대로입니다. 에이전트가 거기까지 닿지 못할 뿐입니다. 이 프리뷰가 빼는 층이 그것입니다. 같은 API config를 배포하면 MCP는 게이트웨이의 /mcp에 나타납니다. tools/call은 해당하는 REST가 되고, 정책 경로와 그 작업의 할당량은 직접 호출과 같습니다. 변환된 요청은 백엔드에서 일반 REST와 프로그램으로 구분할 수 없습니다.
프리뷰가 다루는 범위는 REST, OpenAPI 3.x, 그리고 지금 쓰는 인증입니다. Resources, Prompts, 응답 스트리밍, Model Armor는 로드맵에 있고 이번 릴리스에는 없습니다. HTTP 204처럼 빈 본문을 돌려주는 작업은 도구가 되지 않습니다. 깊게 중첩된 object는 tools/list에서 다 보이지 않을 수 있습니다. 게이트웨이 하나는 도구 1000개가 상한입니다. 같은 API config에서 MCP와 model routing을 함께 켤 수 없습니다. HTTP 작업이 아닌 도구는 주석으로 표현할 수 없습니다. 그때는 서버를 직접 만들고, 입력 형태는 MCP와 JSON Schema를 보십시오.
API Gateway의 MCP와 Apigee의 MCP는 같은 스위치가 아닙니다. Google은 Gateway를 가벼운 입구로 둡니다. 서비스가 이미 Cloud Run에 있고, 관리와 에이전트 입구를 빨리 붙이고 싶을 때입니다. 수명 주기, 더 무거운 트래픽 정책, 수익화는 Apigee의 MCP입니다. 에이전트가 밖으로 어떤 MCP를 불러도 되는지를 묶는 것은 Agent Gateway이고, 이 OpenAPI 확장이 아닙니다. 제품을 잘못 고르면 주석이 맞아도 지금 돌아가는 게이트웨이에는 닿지 않습니다.
| 가지고 있는 것 | Gateway에 주석 | MCP 서버를 작성 |
|---|---|---|
| 작업은 이미 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 본문은 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 키로는 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 config를 만들 때 돌고, 첫 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입니다. /mcp에 POST가 아니면 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 안에서 모델이 읽는 문장입니다. 「언제 호출할지」는 그 문장에 쓰고, 삭제를 호출하지 않을 일은 명시적 opt-out으로 둡니다. 항상 있는 시스템 프롬프트는 이미 목록에 나온 도구를 취소하지 못합니다.
배포 전 확인은 세 단계면 됩니다. JSON 포매터로 스펙을 펼치고, JSON Schema 검증으로 본문 샘플과 arguments.body를 맞춘 다음, JSON 비교로 누가 opt-out을 바꿨는지 봅니다.
자주 묻는 질문
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 키로 tools/list를 보호할 수 있나요?
없습니다. 발견을 잠그려면 이미 정의한 JWT 스킴 정확히 하나를 지명합니다. API 키는 그 REST 작업이 원래 키를 요구할 때의 tools/call은 지킬 수 있습니다. 발견용 자격 증명과 호출용 자격 증명을 하나로 합치지 마십시오.
백엔드가 요청이 MCP에서 왔는지 알 수 있나요?
없습니다. 문서는 변환된 요청이 직접 REST와 프로그램으로 구분되지 않는다고 합니다. 호출자별 회계는 게이트웨이 정책에서 합니다. 서비스 안에서 덧붙인 헤더는 자체 REST 클라이언트와 부딪힐 수도 있습니다.
직접 운영하는 Remote MCP와 어떻게 고르나요?
작업이 이미 Gateway 뒤에 있고 프리뷰 한계 안이면 스펙에 주석합니다. resources, prompts, 스트림 결과, 도구 1000개 초과, 빈 본문, HTTP 작업이 아닌 도구가 필요하면 서버를 직접 만듭니다. 같은 API config에서 model routing도 필요하면 MCP와 라우팅을 함께 켤 수 없으니 config를 나눕니다.
정리와 다음 단계
9월 24일 프리뷰는 「MCP 서버를 하나 더 작성」을 기본 동작에서 선택지로 바꿨습니다. 계약은 여전히 OpenAPI 3.x입니다. 전체 스위치, 작업별 opt-out, 모델이 읽는 이름과 설명, arguments 최상위의 path와 query, body 아래의 요청 본문입니다.
올리기 전에 tools/list를 JWT로 잠그고, 204와 삭제류 작업이 목록에 새지 않았는지 확인한 다음, 같은 JSON으로 핸드셰이크, 목록, 읽기, 쓰기를 호출합니다. 프리뷰 조항은 아직 유효합니다. 한계는 배포하는 날의 문서로 다시 확인하십시오.