API Gateway MCP Server
GoogleOfficialPublished by Google
- Skill
- Tool integration
- Cloud and DevOps
- Apache-2.0
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill enables the transformation of REST APIs hosted on Google Cloud API Gateway into remote Model Context Protocol (MCP) servers. By annotating OpenAPI 3.x specifications with specific extensions, the gateway can translate MCP JSON-RPC 2.0 requests into standard HTTP REST calls. The skill provides a structured workflow for migrating OpenAPI 2.0 specs to 3.x, validating configurations, securing the MCP endpoint, and integrating with tools like the Google Agent Development Kit (ADK).
What it does
- Converts REST APIs into MCP-compatible tools via OpenAPI 3.x annotations.
- Validates OpenAPI specifications for MCP compatibility using a provided Python script.
- Configures security for the MCP tools/list endpoint using JWTs or API keys.
- Facilitates integration with Google ADK McpToolset and API hub discovery.
- Provides guidance on troubleshooting common MCP JSON-RPC error codes.
- Manages the lifecycle of API Gateway resources including creation, updates, and deletion.
When to use it
- When you need to expose existing REST APIs as tools for AI agents using the Model Context Protocol.
- When migrating legacy OpenAPI 2.0 specifications to OpenAPI 3.x for MCP support.
- When you need to secure MCP tool discovery with specific authentication schemes.
- When integrating Google Cloud-hosted APIs with agent frameworks like the Google ADK.
Advantages
- Enables standard MCP communication over HTTP for Google Cloud-hosted services.
- Leverages existing API Gateway authentication and quota management policies.
- Supports automated discovery in API hub and Agent Registry.
- Provides a deterministic validation loop to catch configuration errors before deployment.
Usage examples
- “How do I annotate my OpenAPI 3.0 spec to expose a REST endpoint as an MCP tool?”
- The skill provides the necessary x-google-mcp-tool annotations and explains how to define the tool name, description, and request body properties for LLM tool selection.
- “Validate my OpenAPI spec for MCP deployment.”
- The skill guides you to run the provided scripts/validate_mcp_openapi.py script to ensure your specification meets all requirements before attempting to create an API config.
- “How can I connect my agent to the API Gateway MCP server?”
- The skill provides a Python code sample using McpToolset and StreamableHTTPConnectionParams to connect to the gateway's /mcp endpoint.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/google/api-gateway-mcp-server/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip api-gateway-mcp-server.zip -d .agents/skills
Documentation by Google
API Gateway Remote MCP Server (Public Preview)
This skill converts new or existing REST APIs on Google Cloud API Gateway into remote Model Context Protocol (MCP) servers (POST https://{default_hostname}/mcp, Public Preview). By annotating an OpenAPI 3.x (3.0.x or 3.1.x) specification with x-google-api-management.mcp and x-google-mcp-tool, API Gateway translates MCP JSON-RPC 2.0 requests (initialize, notifications/initialized, tools/list, tools/call) into standard HTTP REST requests to your backend while enforcing existing authentication and quota policies.
Attribution
MUST READ: Before executing any gcloud, REST API, or MCP command, you
MUST read Usage Attribution. For commands executed
directly in the terminal (never in generated scripts, Terraform, or user code),
prefix gcloud with
CLOUDSDK_METRICS_REQUEST_ATTRIBUTION="agent-skills/1.0 (skill:api-gateway-mcp-server)"
and append agent-skills/1.0 (skill:api-gateway-mcp-server) to User-Agent headers.
Prerequisites
Install the Google Cloud CLI and the validator dependency:
Install the Google Cloud CLI. Then update it to the latest version with
gcloud components update. An old CLI can fail when it uploads an OpenAPI 3.x API config.Install Python 3.10 or later and
PyYAMLfor the local OpenAPI MCP validator script:pip install "PyYAML>=6.0,<7.0"
Authenticate your environment:
Local CLI session:
gcloud auth loginApplication Default Credentials (SDKs / ADK):
gcloud auth application-default login
Select or create a project, confirm billing, and pick a supported gateway location:
Select an existing project (
gcloud config set project {project_id}) or create one (gcloud projects create {project_id}).Confirm that an active billing account is linked to
{project_id}. API Gateway has a free monthly allowance of API calls for each billing account, but you still need an active billing account to deploy. Calls above the allowance and network data transfer out are charged. For current prices, see API Gateway pricing.Use
us-central1as the default--locationfor gateway commands. For the other supported regions, see API Gateway deployment model.
Enable the required Google Cloud APIs:
gcloud services enable \ apigateway.googleapis.com \ servicemanagement.googleapis.com \ servicecontrol.googleapis.com \ --project={project_id}For API hub discovery (optional), enable the API hub API in the API hub host project:
gcloud services enable apihub.googleapis.com --project={hub_project_id}. To query Agent Registry (optional), also rungcloud services enable agentregistry.googleapis.com --project={project_id}.Verify the IAM roles (use the most restrictive role for each step):
To create and update the API, API configs, and gateways: API Gateway Admin (
roles/apigateway.admin) on{project_id}. To run thegcloud services enablecommands: Service Usage Admin (roles/serviceusage.serviceUsageAdmin) on that project.To reference the gateway service account in an API config: Service Account User (
roles/iam.serviceAccountUser) on{gateway_sa_name}@{project_id}.iam.gserviceaccount.com.For a Cloud Run backend: grant the gateway service account the Cloud Run Invoker role (
roles/run.invoker) on the service.To mint test ID tokens as a caller service account: Service Account Token Creator (
roles/iam.serviceAccountTokenCreator) on{caller_sa_email}.Reference: Configure the development environment.
Decision Matrix: MCP Server Configuration and Client Integration
| Task | Required Mechanism | Critical Guardrail | Detailed Reference |
|---|---|---|---|
Migrate OpenAPI 2.0 (swagger: "2.0") or author OpenAPI 3.x for MCP | Root x-google-api-management.mcp + named backends + operation-level x-google-mcp-tool | NEVER add MCP extensions to an OpenAPI 2.0 spec. Rationale: MCP is not supported for OpenAPI 2.0. Use OpenAPI 3.0.x or 3.1.x, and deploy the spec with gcloud api-gateway api-configs create. Keep all MCP operations in one OpenAPI document: if an API config has several documents, API Gateway serves the MCP tools of only one of them. | OpenAPI 2.0 to 3.x Migration |
| Optimize tool names, descriptions, and schemas for LLM tool selection | x-google-mcp-tool (name, description, false) + described requestBody properties | Every operation MUST have an operationId, with or without MCP. Every exposed tool MUST resolve to a unique name that matches [A-Za-z0-9_.-]{1,128} (x-google-mcp-tool.name, or else the operationId), a non-empty description, and a non-empty response body (HTTP 204 is unsupported). Only GET, POST, PUT, PATCH, and DELETE operations can be tools, and a gateway serves at most 1,000 tools. API Gateway rejects complex nested schemas when you create the API config, but a simple nested object body works. In OpenAPI 3.1, give each schema one type, because API Gateway also rejects a type list such as type: ["string", "null"]. Describe each property, and check each tool's inputSchema in tools/list after you deploy. | OpenAPI Agent Tool Optimizer |
Secure tools/list and hide internal/admin routes | x-google-api-management.mcp.tools-list.security (exactly one JWT or API key scheme; a JWT scheme is also listed in root security) + x-google-mcp-tool: false | tools/list is unauthenticated by default. It accepts one JWT scheme, or one API key scheme sent in the x-api-key header. API Gateway rejects a config whose JWT tools-list.security scheme is missing from the root security list, and that root entry also requires the JWT on every REST operation without its own security. If the root list already has an API key scheme, add the JWT to the same requirement object (AND): API Gateway does not support alternative (OR) requirements with an API key. An mcp object (for tools-list) also enables MCP for every eligible operation, so opt out each operation that agents must not use. x-google-mcp-tool: false hides a route from MCP only; keep REST security on admin routes. | OpenAPI 2.0 to 3.x Migration |
Connect Google ADK (McpToolset) and discover in API hub / Agent Registry | McpToolset (StreamableHTTPConnectionParams) + API hub project attachment | No separate registration step is needed. After you attach the API Gateway project to API hub, the MCP server appears in API hub as {your_api} (MCP) and in Agent Registry automatically. | ADK and API Hub Discovery |
Verify /mcp JSON-RPC lifecycle and troubleshoot -32601 / -32602 / -32000 errors | POST https://{default_hostname}/mcp with MCP-Protocol-Version: 2025-11-25 | Keep /mcp at the host root: a servers base path (such as /v2) applies to REST paths only, and {basepath}/mcp returns HTTP 404. Nest JSON request bodies under params.arguments.body ({"body": {...}}). NEVER combine x-google-api-management.mcp with x-google-model-router in one config. Rationale: API Gateway does not allow MCP and model routing in the same API config. | MCP Verification and Troubleshooting |
Execution Workflow and Validation Loop
Upgrade and annotate the OpenAPI 3.x specification:
The references in this skill contain the conversion rules: the Swagger 2.0 Construct Conversion Map covers each Swagger 2.0 construct, and the Complete Converted Example shows a finished spec with a Google ID token scheme. Use them before you search other sources.
Follow OpenAPI 2.0 to 3.x Migration and OpenAPI Agent Tool Optimizer to set
openapito a 3.0.x or 3.1.x version (the examples use3.0.4), remove unsupported OpenAPI 2.0 fields (x-google-allow), definex-google-api-management.backends(withjwtAudienceordisableAuth), configurex-google-api-management.mcp.tools-list.securitywith exactly one JWT or API key scheme (add a JWT scheme to the rootsecuritylist too, in the same requirement object as any API key scheme), authorx-google-mcp-tooldescriptions stating when and why to invoke each tool, and opt out administrative routes (x-google-mcp-tool: false).Before you deploy, give the user a short summary that states each of these points, and get approval:
MCP is not supported for OpenAPI 2.0, so you converted the spec to OpenAPI 3.x.
Which operations you exposed as tools, and which you opted out with
x-google-mcp-tool: false. Opted-out operations stay on REST.If
tools/listuses a JWT scheme, the rootsecurityentry makes every REST operation without its ownsecurityrequire the JWT. The gateway rejects REST callers without it.The MCP endpoint is
https://{default_hostname}/mcp, at the host root even when the spec sets aserversbase path.The limits that apply: only
GET,POST,PUT,PATCH, andDELETEoperations can be tools, a gateway serves at most 1,000 tools, request parameters must be primitive types, API Gateway rejects complex nested schemas and OpenAPI 3.1 type lists (such astype: ["string", "null"]) when you create the API config, tools cannot returnHTTP 204, and MCP is stateless with UTF-8 text responses only.
Give the same points when the user only asks for the converted spec or for advice, without a deploy.
Preserve exact quota metric names in
x-google-api-management.metricsacross API configs so existing gateways do not fail with HTTP 500Failed to call Service Control Quota.
Run the deterministic MCP OpenAPI validation loop before creating the API config:
Execute
scripts/validate_mcp_openapi.pyon{candidate_openapi_spec}:python3 scripts/validate_mcp_openapi.py --spec={candidate_openapi_spec}If
scripts/validate_mcp_openapi.pyexits with code1(status: "FAIL"), fix every reported error in{candidate_openapi_spec}(such as OpenAPI 2.0 syntax,x-google-allow, path-levelx-google-mcp-tool, undefined backend names, an operation without anoperationId, missing descriptions, invalid or duplicate tool names, a header parameter with the reservedx-google-prefix, atools-list.securityscheme that is neither anoauth2JWT nor anapiKeyscheme, a JWTtools-list.securityscheme missing from the rootsecuritylist, alternative (OR) security requirements with an API key, or anx-google-model-routeroperation in an MCP spec) and re-run the script until it exits with code0(status: "PASS"). If it exits with code2, fix the file path or YAML syntax error printed to stderr.Read every warning (such as an
HTTP 204response, a request parameter that is not a primitive type, or operations that anmcpobject exposes by default). Fix it, or tell the user why you keep it.
Create the API (if new), create the API config, deploy the gateway, and enable the managed service (request human approval before updating a production gateway):
gcloud api-gateway apis create {api_id} --project={project_id} gcloud api-gateway api-configs create {config_id} \ --api={api_id} \ --openapi-spec={candidate_openapi_spec} \ --backend-auth-service-account={gateway_sa_name}@{project_id}.iam.gserviceaccount.com \ --project={project_id} gcloud api-gateway gateways update {gateway_id} \ --api={api_id} \ --api-config={config_id} \ --location={region} \ --project={project_id} gcloud services enable $(gcloud api-gateway apis describe {api_id} --project={project_id} --format="value(managedService)") --project={project_id}(For a brand-new gateway, run
gcloud api-gateway gateways create {gateway_id} ...instead ofgateways update).Run one
api-configs createat a time per API. Wait for it to finish before you start the next one, because API Gateway creates only one config for an API at a time. A complex API config can take up to ten minutes to create.If clients use API keys from other projects, enable the managed service in each of those projects too. API Gateway rejects an API key unless the API is enabled in the project that created the key.
Verify the JSON-RPC 2.0 lifecycle and connect ADK / API hub:
Send every probe to
https://{default_hostname}/mcp, even when the spec sets aserversbase path.An HTML
404 Page not foundmeans that the gateway may not be deployed correctly. Check that the gateway state isACTIVEand that the last create or update finished, and retry before you change the config. During a gateway update, some requests can still reach the previous API config. A JSON 404 (The current request is not defined by this API.) means that the active config does not define the path.If
tools/listrequires a JWT, mint a Google ID token whose audience is inx-google-auth.audiences:gcloud auth print-identity-token --impersonate-service-account={caller_sa_email} --audiences={jwt_audience}. Iftools/listrequires an API key, send it in thex-api-keyheader.Run the
initialize,notifications/initialized,tools/list, andtools/callverification probes in MCP Verification and Troubleshooting.Connect Google ADK (
McpToolset) and verify automatic catalog discovery in Apigee API hub ({your_api} (MCP)) and Agent Registry using ADK and API Hub Discovery.
Pre-Deployment Checklist
[ ] Specification version is
openapi: 3.0.xor3.1.x(notswagger: "2.0"), omitsx-google-allow, and does not containx-google-model-router.[ ]
python3 scripts/validate_mcp_openapi.py --spec={candidate_openapi_spec}returnsstatus: "PASS"(exit code0).[ ] Every operation has an
operationId, and all MCP operations are in one OpenAPI document.[ ]
x-google-api-management.mcp.tools-list.securitynames exactly one scheme: anoauth2JWT scheme withx-google-auththat the rootsecuritylist also references, or anapiKeyscheme within: headerandname: x-api-key.[ ] If
tools/listuses a JWT scheme, the user knows that REST operations without their ownsecuritynow require that JWT.[ ] Administrative, health-check, and
HTTP 204endpoints are opted out withx-google-mcp-tool: false, and opted-out admin paths remain protected on REST.[ ] MCP clients call
https://{default_hostname}/mcp(not{basepath}/mcp), nesttools/callJSON request bodies insideparams.arguments.body, and sendMCP-Protocol-Version: 2025-11-25.
Resource Cleanup
To avoid ongoing charges when tearing down a test or staging environment, request human approval and delete the gateway, API config, API, and service account in dependency order:
gcloud api-gateway gateways delete {gateway_id} --location={region} --project={project_id}
gcloud api-gateway api-configs delete {config_id} --api={api_id} --project={project_id}
gcloud api-gateway apis delete {api_id} --project={project_id}
gcloud iam service-accounts delete {gateway_sa_name}@{project_id}.iam.gserviceaccount.com --project={project_id}Files
- ATTRIBUTION.mdAdded by AutoGineer862 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md16.3 KiB
- references/adk-and-api-hub-discovery.md6.6 KiB
- references/attribution.md1.9 KiB
- references/mcp-limitations-and-troubleshooting.md10.7 KiB
- references/openapi-agent-tool-optimizer.md5.5 KiB
- references/openapi-v2-to-v3-mcp-migration.md17.2 KiB
- scripts/validate_mcp_openapi.pyScript22.0 KiB
scripts/validate_mcp_openapi.py
#!/usr/bin/env python3
"""Validates an OpenAPI spec against Google Cloud API Gateway MCP rules.
Errors are rules that the API Gateway documentation states. Warnings are
documented limits that need a decision from the user.
The script checks that the specification uses OpenAPI 3.0.x or 3.1.x, omits
x-google-allow, gives each schema one type, declares
x-google-api-management.backends, references only
defined backends, uses at most one server with x-google-endpoint, gives every
operation an operationId, and places x-google-mcp-tool only at the operation
level. It checks that an MCP
specification has no x-google-model-router operation, and that each exposed
tool has a backend, a unique name that matches [A-Za-z0-9_.-]{1,128}, a
non-empty description, and no header parameter with the reserved x-google-
prefix. It checks that tools-list.security names exactly one JWT or API key
scheme, that a JWT scheme is also in the root security list, and that no
security list gives alternative (OR) requirements with an API key scheme.
It warns about HTTP 204 responses, request parameters that are not primitive
types, API key schemes that are not read from the x-api-key header, the
OpenAPI 2.0 x-google-management extension, a model routing "ai" block in an
MCP specification, operations that an mcp object exposes by default, and
operations that global MCP enablement skips because they have no backend.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from typing import Any
try:
import yaml
_HAS_YAML = True
except ImportError:
_HAS_YAML = False
_TOOL_NAME_RE = re.compile(r'^[A-Za-z0-9_.-]{1,128}$')
_OAS3_RE = re.compile(r'^3\.[01]\.\d+$')
_HTTP_METHODS = frozenset(
{'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'}
)
_SUPPORTED_MCP_METHODS = frozenset({'get', 'post', 'put', 'patch', 'delete'})
_MAX_TOOLS_PER_GATEWAY = 1000
# Keys whose values are data, not schemas. A "type" list inside them is not a
# schema type list. The check also skips vendor extensions (x-*).
_NON_SCHEMA_KEYS = frozenset(
{'const', 'default', 'enum', 'example', 'examples'}
)
def _load_spec(path: str) -> dict[str, Any]:
"""Reads the OpenAPI specification file at path as JSON or YAML."""
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
try:
data = json.loads(content)
except json.JSONDecodeError:
pass
else:
if isinstance(data, dict):
return data
if not _HAS_YAML:
raise RuntimeError(
'PyYAML is required to parse YAML files. Install with: '
'pip install "PyYAML>=6.0,<7.0"'
)
try:
data = yaml.safe_load(content)
except yaml.YAMLError as exc:
raise ValueError(f'{path} is not valid JSON or YAML: {exc}') from exc
if not isinstance(data, dict):
raise ValueError(f'{path} did not parse as a YAML/JSON mapping.')
return data
def _resolve(spec: dict[str, Any], obj: Any) -> Any:
"""Follows local '#/...' $ref pointers. Returns obj if it cannot resolve."""
seen: set[str] = set()
while isinstance(obj, dict) and isinstance(obj.get('$ref'), str):
reference = obj['$ref']
if not reference.startswith('#/') or reference in seen:
return obj
seen.add(reference)
target: Any = spec
for part in reference[2:].split('/'):
part = part.replace('~1', '/').replace('~0', '~')
if not isinstance(target, dict) or part not in target:
return obj
target = target[part]
obj = target
return obj
def _requirement_names(security: Any) -> list[set[str]]:
"""Returns the scheme names of each non-empty security requirement."""
if not isinstance(security, list):
return []
return [
{str(name) for name in requirement}
for requirement in security
if isinstance(requirement, dict) and requirement
]
def _or_with_api_key_error(
security: Any, schemes: dict[str, Any], where: str
) -> str | None:
"""Returns an error if a security list has OR requirements with an API key.
Args:
security: The value of a "security" list.
schemes: The security schemes from components.securitySchemes.
where: The location of the "security" list, for the error message.
Returns:
The error message, or None if the security list is valid.
"""
requirements = _requirement_names(security)
uses_api_key = False
for requirement in requirements:
for name in requirement:
scheme = schemes.get(name)
if isinstance(scheme, dict) and scheme.get('type') == 'apiKey':
uses_api_key = True
if len(requirements) > 1 and uses_api_key:
return (
f'The "security" list of {where} gives alternative (OR)'
' requirements, and one of them uses an API key scheme. API Gateway'
' does not support OR requirements with an API key. Put the schemes'
' in one requirement object (AND).'
)
return None
def _type_list_schemas(node: Any, location: str) -> list[tuple[str, str]]:
"""Finds the schemas whose "type" is a list of more than one type.
API Gateway rejects such a schema, for example `type: ["string", "null"]`,
when you create the API config.
Args:
node: A part of the OpenAPI specification.
location: The JSON pointer of node in the specification.
Returns:
The JSON pointer and the type list of each schema with more than one type.
"""
found: list[tuple[str, str]] = []
if isinstance(node, dict):
node_type = node.get('type')
if isinstance(node_type, list) and len(node_type) > 1:
found.append((location or '/', json.dumps(node_type)))
for key, value in node.items():
key_text = str(key)
if key_text in _NON_SCHEMA_KEYS or key_text.startswith('x-'):
continue
escaped_key = key_text.replace('~', '~0').replace('/', '~1')
found.extend(_type_list_schemas(value, f'{location}/{escaped_key}'))
elif isinstance(node, list):
for index, item in enumerate(node):
found.extend(_type_list_schemas(item, f'{location}/{index}'))
return found
def validate_mcp_spec(
spec: dict[str, Any],
) -> tuple[list[str], list[str], list[dict[str, str]]]:
"""Returns (errors, warnings, exposed_tools) for an OpenAPI spec."""
errors: list[str] = []
warnings: list[str] = []
exposed_tools: list[dict[str, str]] = []
openapi_version = str(spec.get('openapi', ''))
if 'swagger' in spec:
errors.append(
'OpenAPI 2.0 (swagger: "2.0") is not supported for API Gateway MCP.'
' Convert the specification to OpenAPI 3.0.x or 3.1.x (for example,'
' openapi: 3.0.4).'
)
elif not _OAS3_RE.match(openapi_version):
errors.append(
'Expected openapi version 3.0.x or 3.1.x (for example, 3.0.4 or'
f' 3.1.0), got "{openapi_version}".'
)
if 'x-google-allow' in spec:
errors.append(
'Extension "x-google-allow" is not supported in OpenAPI 3.x and must '
'be removed.'
)
for location, type_list in _type_list_schemas(spec, ''):
errors.append(
f'The schema at {location} has more than one type ({type_list}). API'
' Gateway rejects a type list when you create the API config. Give the'
' schema one type.'
)
if 'x-google-management' in spec:
warnings.append(
'Found the OpenAPI 2.0 extension "x-google-management". In OpenAPI'
' 3.x, move the metrics and quota settings into'
' "x-google-api-management".'
)
servers = spec.get('servers', [])
if isinstance(servers, list):
endpoint_count = sum(
1
for server in servers
if isinstance(server, dict) and 'x-google-endpoint' in server
)
if endpoint_count > 1:
errors.append(
f'Found {endpoint_count} servers with "x-google-endpoint"; API'
' Gateway permits at most one server with x-google-endpoint.'
)
management = spec.get('x-google-api-management')
if not isinstance(management, dict):
errors.append('Missing required root "x-google-api-management" mapping.')
return errors, warnings, exposed_tools
backends = management.get('backends')
backend_names: set[str] = set()
if not isinstance(backends, dict) or not backends:
errors.append(
'"x-google-api-management.backends" must define at least one named'
' backend.'
)
else:
for backend_name, backend_config in backends.items():
backend_names.add(str(backend_name))
if not isinstance(backend_config, dict) or not backend_config.get(
'address'
):
errors.append(
f'Backend "{backend_name}" is missing a required "address".'
)
components = spec.get('components')
schemes = (
components.get('securitySchemes', {})
if isinstance(components, dict)
else {}
)
if not isinstance(schemes, dict):
schemes = {}
root_or_error = _or_with_api_key_error(
spec.get('security'), schemes, 'the root'
)
if root_or_error:
errors.append(root_or_error)
mcp_config = management.get('mcp')
global_mcp_enabled = False
if isinstance(mcp_config, bool):
global_mcp_enabled = mcp_config
if mcp_config:
warnings.append(
'tools/list is unauthenticated ("mcp: true"). Configure '
'x-google-api-management.mcp.tools-list.security with a JWT or API '
'key scheme for production.'
)
elif isinstance(mcp_config, dict):
global_mcp_enabled = True
tools_list = mcp_config.get('tools-list', {})
if isinstance(tools_list, dict) and 'security' in tools_list:
tools_list_security = tools_list.get('security')
if (
not isinstance(tools_list_security, dict)
or len(tools_list_security) != 1
):
errors.append(
'"x-google-api-management.mcp.tools-list.security" must be a map '
'naming exactly one JWT or API key security scheme.'
)
else:
scheme_name = str(next(iter(tools_list_security.keys())))
scheme_def = schemes.get(scheme_name)
is_jwt = isinstance(scheme_def, dict) and (
scheme_def.get('type') == 'oauth2'
and isinstance(scheme_def.get('x-google-auth'), dict)
)
is_api_key = (
isinstance(scheme_def, dict) and scheme_def.get('type') == 'apiKey'
)
if not isinstance(scheme_def, dict):
errors.append(
'tools-list.security references unknown security scheme'
f' "{scheme_name}" not defined under components.securitySchemes.'
)
elif not is_jwt and not is_api_key:
errors.append(
f'tools-list.security scheme "{scheme_name}" must be a JWT scheme'
' (type "oauth2" with an "x-google-auth" configuration) or an API'
' key scheme (type "apiKey").'
)
elif is_api_key:
if (
scheme_def.get('in') != 'header'
or str(scheme_def.get('name', '')).lower() != 'x-api-key'
):
warnings.append(
f'tools-list.security names API key scheme "{scheme_name}",'
' but tools/list reads API keys only from the "x-api-key"'
' header. Define the scheme with "in: header" and "name:'
' x-api-key".'
)
else:
root_requirements = _requirement_names(spec.get('security'))
if not any(
scheme_name in requirement for requirement in root_requirements
):
errors.append(
f'tools-list.security names JWT scheme "{scheme_name}", but the'
' root "security" list does not reference it. API Gateway'
f' rejects this config. Add "{scheme_name}: []" to the root'
' "security" list (in the same requirement object as an API'
' key scheme, if there is one).'
)
elif all(
scheme_name in requirement for requirement in root_requirements
):
warnings.append(
f'The root "security" list requires "{scheme_name}" on every'
' REST operation that does not set its own "security". Tell the'
' user that the gateway rejects REST callers without this JWT.'
)
else:
warnings.append(
f'The root "security" list names "{scheme_name}" as one of'
' several alternatives. REST operations that do not set their'
' own "security" accept any of the alternatives.'
)
else:
warnings.append(
'x-google-api-management.mcp does not configure tools-list.security; '
'tools/list will be unauthenticated.'
)
root_backend = spec.get('x-google-backend')
if isinstance(root_backend, dict):
errors.append(
'Root "x-google-backend" must be a string referencing a backend in '
'x-google-api-management.backends, not an inline OpenAPI 2.0 object.'
)
elif (
isinstance(root_backend, str)
and backend_names
and root_backend not in backend_names
):
errors.append(
f'Root x-google-backend "{root_backend}" is not defined in '
'x-google-api-management.backends.'
)
seen_names: set[str] = set()
router_operations: list[str] = []
explicit_opt_in = False
implicit_tool_count = 0
paths = spec.get('paths', {})
if isinstance(paths, dict):
for path_str, path_item in paths.items():
if not isinstance(path_item, dict):
continue
if 'x-google-mcp-tool' in path_item:
errors.append(
f'Path "{path_str}" declares "x-google-mcp-tool" at the path level.'
' It must only be specified at the individual operation level.'
)
path_parameters = path_item.get('parameters')
for raw_method, operation in path_item.items():
method = str(raw_method).lower()
if method not in _HTTP_METHODS or not isinstance(operation, dict):
continue
label = f'{method.upper()} {path_str}'
if not str(operation.get('operationId') or '').strip():
errors.append(
f'Operation {label} has no operationId. API Gateway requires an'
' operationId on every operation, with or without MCP.'
)
mcp_tool = operation.get('x-google-mcp-tool')
if 'x-google-model-router' in operation:
router_operations.append(label)
if mcp_tool is True or isinstance(mcp_tool, dict):
explicit_opt_in = True
continue
if 'security' in operation:
operation_or_error = _or_with_api_key_error(
operation.get('security'), schemes, f'operation {label}'
)
if operation_or_error:
errors.append(operation_or_error)
operation_backend = operation.get('x-google-backend', root_backend)
if isinstance(operation.get('x-google-backend'), dict):
errors.append(
f'Operation {label} uses an inline "x-google-backend" object'
' instead of a string backend name.'
)
elif (
isinstance(operation_backend, str)
and backend_names
and operation_backend not in backend_names
):
errors.append(
f'Operation {label} references undefined backend'
f' "{operation_backend}".'
)
if mcp_tool is not None and not isinstance(mcp_tool, (bool, dict)):
errors.append(
f'Operation {label} has invalid x-google-mcp-tool type (must be'
' boolean or object).'
)
continue
if mcp_tool is False:
continue
explicit = mcp_tool is True or isinstance(mcp_tool, dict)
if explicit:
explicit_opt_in = True
elif not global_mcp_enabled:
continue
if method not in _SUPPORTED_MCP_METHODS:
if explicit:
errors.append(
f'Operation {label} explicitly opts into MCP, but HTTP method'
f' {method.upper()} is not supported (only GET, POST, PUT,'
' PATCH, DELETE).'
)
continue
if not operation_backend:
if explicit:
errors.append(
f'Operation {label} opts into MCP, but it has no'
' x-google-backend. Each exposed operation needs a backend.'
)
else:
warnings.append(
f'Operation {label} has no x-google-backend, so it is not'
' eligible as an MCP tool.'
)
continue
parameters = []
for parameter_list in (path_parameters, operation.get('parameters')):
if isinstance(parameter_list, list):
parameters.extend(
_resolve(spec, parameter) for parameter in parameter_list
)
for parameter in parameters:
if not isinstance(parameter, dict):
continue
parameter_name = str(parameter.get('name', ''))
if str(parameter.get('in', '')).lower() == 'header' and (
parameter_name.lower().startswith('x-google-')
):
errors.append(
f'Operation {label} binds a header parameter to reserved'
f' prefix "{parameter_name}".'
)
schema = _resolve(spec, parameter.get('schema'))
if isinstance(schema, dict) and (
schema.get('type') in ('array', 'object')
or 'items' in schema
or 'properties' in schema
):
warnings.append(
f'Operation {label} parameter "{parameter_name}" is not a'
' primitive type. API Gateway supports only primitive types in'
' request parameters. Change it to a primitive type, or set'
' "x-google-mcp-tool: false" on the operation.'
)
if isinstance(mcp_tool, dict) and 'name' in mcp_tool:
raw_name = mcp_tool['name']
if not isinstance(raw_name, str) or not _TOOL_NAME_RE.match(raw_name):
errors.append(
f'x-google-mcp-tool.name on {label} must be a string that'
f' matches [A-Za-z0-9_.-]{{1,128}}, got {raw_name!r}.'
)
continue
tool_name = raw_name
else:
tool_name = str(operation.get('operationId') or '')
if not tool_name:
# Reported above: every operation needs an operationId.
continue
if not _TOOL_NAME_RE.match(tool_name):
errors.append(
f'Tool name "{tool_name}" (the operationId of {label}) does not'
' match [A-Za-z0-9_.-]{1,128}. Set x-google-mcp-tool.name.'
)
if tool_name in seen_names:
errors.append(f'Duplicate MCP tool name "{tool_name}".')
seen_names.add(tool_name)
override_description = (
mcp_tool.get('description') if isinstance(mcp_tool, dict) else None
)
tool_description = str(
override_description
or operation.get('description')
or operation.get('summary')
or ''
).strip()
if not tool_description:
errors.append(
f'Exposed MCP operation {label} ("{tool_name}") has no'
' resolvable description or summary.'
)
responses = operation.get('responses', {})
if isinstance(responses, dict) and (
'204' in responses or 204 in responses
):
warnings.append(
f'Operation {label} ("{tool_name}") declares an HTTP 204'
' response. MCP does not support operations that return an empty'
' body. Return 200 with a JSON body, or set'
' "x-google-mcp-tool: false".'
)
if not explicit:
implicit_tool_count += 1
exposed_tools.append({
'name': tool_name,
'method': method.upper(),
'path': path_str,
})
if global_mcp_enabled or explicit_opt_in:
for label in router_operations:
errors.append(
f'Operation {label} uses "x-google-model-router". MCP and model'
' routing cannot be used in the same API configuration.'
)
if 'ai' in management and not router_operations:
warnings.append(
'"x-google-api-management.ai" configures model routing. MCP and model'
' routing cannot be used in the same API configuration. Remove the'
' "ai" block, or move model routing to a separate API config.'
)
if isinstance(mcp_config, dict) and implicit_tool_count:
warnings.append(
'x-google-api-management.mcp is an object, so MCP also exposes'
f' {implicit_tool_count} operation(s) that do not set'
' x-google-mcp-tool. Check exposed_tools, and set'
' "x-google-mcp-tool: false" on operations that agents must not call.'
)
if not exposed_tools:
warnings.append(
'No operations resolved as exposed MCP tools; /mcp will expose 0 tools.'
)
elif len(exposed_tools) > _MAX_TOOLS_PER_GATEWAY:
errors.append(
f'This file exposes {len(exposed_tools)} MCP tools. A gateway serves at'
f' most {_MAX_TOOLS_PER_GATEWAY:,} MCP tools.'
)
return errors, warnings, exposed_tools
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description=(
'Validate an OpenAPI specification against Google Cloud API Gateway '
'MCP Public Preview constraints.'
)
)
parser.add_argument(
'--spec',
required=True,
help='Path to the OpenAPI YAML or JSON specification file to validate.',
)
args = parser.parse_args(argv)
try:
spec = _load_spec(args.spec)
errors, warnings, exposed_tools = validate_mcp_spec(spec)
except (OSError, RuntimeError, ValueError) as exc:
print(f'Error: {exc}', file=sys.stderr)
return 2
status = 'FAIL' if errors else 'PASS'
report = {
'status': status,
'spec': args.spec,
'exposed_tool_count': len(exposed_tools),
'exposed_tools': exposed_tools,
'errors': errors,
'warnings': warnings,
}
print(json.dumps(report, indent=2))
if errors:
print(
f'MCP OpenAPI validation failed with {len(errors)} error(s).',
file=sys.stderr,
)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an MCP server, Needs an API key, Needs a command-line tool, Needs Python
Security
What our scanner noticed· Low risk
- Instructions that use the network
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Publisher
- Google (Official)
- Source
- google/skills · skills/cloud/api-gateway-mcp-server · commit 2194d3b
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Google or Google.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download api-gateway-mcp-server.zip