Gemini Enterprise with Agent Gateway all-traffic VPC egress and Cloud NAT static IP to external MCP server

1. Introduction

This Codelab explores static IP egress connectivity for Gemini Enterprise using Agent Gateway in agent-to-anywhere (egress) mode. You will configure a Gemini Enterprise app to invoke a custom Model Context Protocol (MCP) server, protected by a strict source IP allowlist and OAuth 2.0 authentication, by routing all outbound traffic through Agent Gateway by using a connectivity template configuration. A Private Service Connect (PSC) network attachment connects Agent Gateway to the VPC network and Cloud NAT provides a stable external IP address for internet egress traffic.

By default, outbound connections from Gemini Enterprise to public internet endpoints originate from dynamic, shared Google IP address pools that cannot be pinned to a single customer-owned IP allowlist rule. Pairing Agent Gateway with an AgentConnectivityTemplate in ALL_TRAFFIC mode routes all outbound agent traffic into the VPC network, where Cloud NAT translates internet-bound connections to a reserved static external IPv4 address under your control while preserving centralized MCP tool governance through Agent Registry and Identity-Aware Proxy (IAP).

What you build

  • Agent Connectivity Template (vpcEgress: ALL_TRAFFIC) and Agent Gateway operating in egress (agent-to-anywhere) mode with Agent Registry endpoint verification
  • VPC network, PSC network attachment, Cloud Router, and Cloud NAT gateway bound to a reserved static external IPv4 address
  • External streamable HTTP MCP Resource Server (mcp-sprockets) fronted by Caddy TLS, restricted by firewall rules strictly to the Cloud NAT static IP, and verifying user-delegated Google OAuth 2.0 Bearer tokens against https://oauth2.googleapis.com/tokeninfo
  • Google Auth Platform OAuth 2.0 consent screen and Web application OAuth 2.0 client credentials (accounts.google.com and oauth2.googleapis.com/token)
  • Agent Registry MCP server catalog entry and tool specification (toolspec.json)
  • Identity-Aware Proxy (IAP) authorization extension and Gateway authorization policy for Agent Gateway
  • Gemini Enterprise app bound to Agent Gateway and connected to an OAuth 2.0 custom MCP server data store imported from Agent Registry

figure1

Fig 1. Codelab architecture

What you learn

  • How to configure an AgentConnectivityTemplate with vpcEgress: ALL_TRAFFIC to route all outbound Agent Gateway traffic into a VPC network
  • How to reserve a static external IPv4 address and bind it to Cloud NAT on a PSC network attachment subnet
  • How to deploy an external streamable HTTP MCP Resource Server that verifies Google OAuth 2.0 Bearer tokens (oauth2.googleapis.com/tokeninfo) and restrict its ingress firewall strictly to the Cloud NAT static IP (${NAT_EXT_IP}/32)
  • How to configure Google Identity OAuth 2.0 client credentials and register an MCP server and its tool schema in Agent Registry
  • How to connect a registered OAuth 2.0 MCP server to Gemini Enterprise and route user-delegated tool calls through Agent Gateway
  • How to validate static IP translation, end-user OAuth 2.0 identity verification (authenticated_user), and gateway policy enforcement using CLI commands and Cloud Logging

What you need

  • A Google Cloud project with billing enabled
  • An active Gemini Enterprise license or 30-day trial
  • IAM permissions to provision networking services, Organization Policies, Gemini Enterprise, and Agent Platform resources
  • A POSIX-compatible shell (bash or zsh) with Google Cloud CLI (gcloud), curl, and jq installed

This concludes the introduction portion... next on to the Concepts section.

2. Concepts

Deployment sequence

This Codelab deploys the networking and governance infrastructure first so static IP egress paths and gateway controls are operational before registering and connecting MCP tools with Gemini Enterprise:

  1. Network infrastructure: Reserve static external IPv4 addresses and provision the egress VPC, PSC attachment subnet, PSC network attachment, Cloud Router, Cloud NAT gateway, and external MCP network firewall rules.
  2. Agent Connectivity Template & Agent Gateway: Deploy the AgentConnectivityTemplate (vpcEgress: ALL_TRAFFIC) and regional AgentGateway in egress mode (AGENT_TO_ANYWHERE) with Agent Registry integration (registries).
  3. Authorization policies: Configure the IAP authorization extension (AuthzExtension), Gateway authorization policy (AuthzPolicy), and IAM v3 Unified Access Policy (AccessPolicy and PolicyBinding).
  4. Deploy & register MCP server: Deploy the external mcp-sprockets OAuth 2.0 Resource Server behind Caddy TLS, verify its Google OAuth 2.0 tokeninfo enforcement, lock down its ingress firewall strictly to the Cloud NAT static IP (${NAT_EXT_IP}/32), and register the service and tool specifications (get_sprockets and get_sprocket_by_id) in Agent Registry.
  5. Gemini Enterprise app & OAuth 2.0 client: Configure the Google Identity provider, trial license, and OAuth 2.0 consent screen and Web application client credentials (OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET), create the Gemini Enterprise app (Engine), and bind outbound egress to Agent Gateway (agentGatewaySetting).
  6. Import custom MCP data connector: Create and activate the REGISTRY_MCP data connector (:setUpDataConnector) configured with auth_type: "OAUTH" to link the registered MCP server backing data store to the Gemini Enterprise app.
  7. Validate: Complete the one-time user OAuth 2.0 consent flow, invoke MCP tools through the Gemini Enterprise assistant (confirming both egress_source_ip and authenticated_user), and verify policy enforcement and static IP NAT translation in Cloud Logging.

Central IdP + OAuth-protected MCP Resource Server architecture

In enterprise deployments, identity management and tool execution follow the standard OAuth 2.0 separation of roles between a centralized Identity Provider (Authorization Server) and a protected MCP Resource Server:

OAuth 2.0 Role

Component in This Codelab

Responsibility

Resource Owner

End User (${USER_IDENTITY})

Signs in to the Gemini Enterprise web app and grants consent during the one-time 3-legged OAuth 2.0 authorization flow.

OAuth 2.0 Client

Gemini Enterprise custom_mcp Data Connector

Holds OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET, exchanges the authorization code at oauth2.googleapis.com/token, vaults the user OAuth 2.0 tokens, and presents Authorization: Bearer [access_token] on every outbound MCP tool call.

Authorization Server

Google Identity OAuth 2.0 (accounts.google.com & oauth2.googleapis.com/token)

Authenticates the user during the browser consent redirect and issues the OAuth 2.0 access and refresh tokens to Gemini Enterprise.

Resource Server

External mcp-sprockets Server (https://mcp.[MCP_EXT_IP].sslip.io/mcp)

Enforces Layer 3 network identity using its VPC firewall (${NAT_EXT_IP}/32) and Layer 7 user identity by verifying the Authorization: Bearer [access_token] header against https://oauth2.googleapis.com/tokeninfo.

Because the browser consent redirect (accounts.google.com) and the back-channel token exchange (oauth2.googleapis.com/token) occurs against Google managed OAuth 2.0 endpoints, the external MCP server firewall only ever receives runtime MCP JSON-RPC requests (POST /mcp) from Agent Gateway by way of Cloud NAT, allowing its ingress firewall to be locked down strictly to ${NAT_EXT_IP}/32.

Discovery Engine resource model

A Gemini Enterprise app (represented as an Engine resource in the Discovery Engine API) is the central orchestration layer and conversational interface for end users. It manages user chat sessions, grounds generative models on enterprise data, and coordinates dynamic tool execution.

Gemini Enterprise apps interact with data and systems through data stores:

  • Knowledge data stores: Ingest and index static content (eg, Cloud Storage, Google Drive, BigQuery) for retrieval-augmented generation (RAG).
  • Data connectors (action providers): Connect to dynamic third-party or custom APIs. A custom MCP server data store exposes tools defined by the Model Context Protocol (MCP), enabling the model to dynamically call external functions during a conversation.

Gemini Enterprise egress

Gemini Enterprise routes custom MCP server tool requests to Agent Gateway when both agentGatewaySetting on the Engine and use_agent_gateway_egress: true on the DataConnector are configured.

figure2

Fig 2. Gemini Enterprise egress architecture

The Gemini Enterprise app organizes tool routing across four key areas:

  1. Widget (default_search_widget_config):
    • Serves the web client interface. The widget receives prompts from the user, triggers the one-time OAuth 2.0 user consent redirect when a tool requires authorization, and initiates chat sessions with the underlying engine.
  2. Core Assistant (assistants/default_assistant/agents/default/core_assistant):
    • The root conversational reasoning agent within the engine. When evaluating a user query, the Core Assistant inspects available tools and delegates execution to the synthesized Agent Gateway sub-agent.
  3. Data Store and Data Connector:
    • DataStore: Provisioned inside a dedicated Collection when :setUpDataConnector runs, it links (dataStoreIds) the imported Agent Registry tool schemas (get_sprockets, get_sprocket_by_id), argument types, and agent instructions to the Gemini Enterprise Engine.
    • DataConnector: Manages the REGISTRY_MCP action connection (createBapConnection: true) to the remote MCP server (instance_uri), resolves the Agent Registry MCP server resource (registry_mcp_server_name), attaches the user vaulted OAuth 2.0 Bearer token (Authorization: Bearer [access_token]), and enables Agent Gateway egress (use_agent_gateway_egress: true).
  4. Agent Identity, Agent Registry, and Agent Gateway:
    • When the Data Connector dispatches the outbound tool call, it routes traffic to the gateway specified in agentGatewaySetting. The Core Assistant mints a SPIFFE identity token asserting its workload identity: principal://agents.global.org-.../agents/default/core_assistant.
    • Agent Gateway integrates with Agent Registry using the registries field to dynamically resolve destination endpoints and registered tool schemas, evaluates authorization policies using IAP, and forwards permitted requests (preserving the user Authorization: Bearer header) into the VPC network using AgentConnectivityTemplate.

Gateway VPC connectivity and static IP NAT

Agent Gateway supports VPC network connectivity using an AgentConnectivityTemplate resource referenced by agentConnectivityTemplate on the gateway. The template egressNetworkConfig defines how traffic enters your VPC across a PSC network attachment:

  • PRIVATE_RANGES_ONLY (default): Routes only RFC 1918, RFC 6598, and Class E private IP ranges through the PSC network attachment. Internet-bound traffic bypasses your VPC and exits directly through Google default internet egress.
  • ALL_TRAFFIC (used in this Codelab): Routes all outbound traffic from Agent Gateway—including public internet and external partner MCP endpoints—into your VPC subnet through the PSC network attachment (10.10.10.2).

When operating in ALL_TRAFFIC mode, the PSC attachment subnet requires two configurations:

  • Private Google Access (--enable-private-ip-google-access): Ensures internal calls to Google Cloud APIs (*.googleapis.com) continue to route privately within the Google network.
  • Cloud Router and Cloud NAT (MANUAL_ONLY): Internet-bound traffic entering the PSC attachment subnet follows the default route (0.0.0.0/0) through Cloud NAT, which performs Source Network Address Translation (SNAT) to your reserved static external IPv4 address (${NAT_EXT_IP}). External firewalls can therefore restrict inbound access strictly to ${NAT_EXT_IP}/32.

Limitations & requirements

  • StreamableHTTP only: The legacy Server-Sent Events (SSE) transport is not supported. MCP servers must use StreamableHTTP.
  • Org policy override: You must explicitly override the organization policy for Custom MCP data stores before registering the data connector.
  • Regional alignment: Gemini Enterprise enforces regional alignment between the app location, Agent Gateway, and Agent Registry:

Gemini Enterprise app location

Agent Gateway & NAT location

Agent Registry location

global

us-central1

us-central1, us, or global

us

us-central1

us-central1 or us

eu

europe-west1

europe-west1 or eu

This concludes the concepts portion... next on to the Setup section.

3. Setup

Required IAM roles

The following roles are required to complete this Codelab:

Domain

Required IAM roles

Project & IAM

roles/orgpolicy.policyAdmin
roles/resourcemanager.projectIamAdmin
roles/iam.accessPolicyAdmin
roles/serviceusage.serviceUsageAdmin
roles/iam.serviceAccountUser

Networking & Gateway

roles/networkservices.admin
roles/networksecurity.admin
roles/serviceextensions.admin
roles/iap.admin
roles/compute.networkAdmin
roles/compute.securityAdmin

Gemini Enterprise & Registry

roles/discoveryengine.admin
roles/agentregistry.admin

Workloads

roles/compute.instanceAdmin.v1 (or roles/compute.admin)

Observability

roles/logging.viewer

Or use a broad basic role like roles/owner combined with roles/orgpolicy.policyAdmin (since roles/owner alone cannot modify organization policies).

Access your project

This Codelab uses a single Google Cloud project. Configuration steps use gcloud CLI and Linux shell commands.

Start by accessing your Google Cloud project command line:

Set your Project ID

# set target project id
gcloud config set project SET_YOUR_PROJECT_ID_HERE

Authenticate session

# login to gcloud cli
gcloud auth login
# login for gcloud api
gcloud auth application-default login

Set shell environment variables

# set custom var for slug (eg, "foo") and region preference
export SLUG="foo"
export REGION="us-central1"
export ZONE="us-central1-a"

echo ${SLUG}
echo ${REGION}
echo ${ZONE}
# create project vars (automatic)
export PROJ_ID=$(gcloud config list --format="value(core.project)")
export PROJ_NO=$(gcloud projects describe ${PROJ_ID} --format="value(projectNumber)")
export ORG_ID=$(gcloud projects get-ancestors ${PROJ_ID} --format="value(id)" | tail -n 1)
export USER_IDENTITY=$(gcloud config get-value account)

echo ${PROJ_ID}
echo ${PROJ_NO}
echo ${ORG_ID}
echo ${USER_IDENTITY}
# create resource vars for agent platform (automatic)
export AGW_NAME="agw-${SLUG}-${REGION}-ata"
export AGW_URI="projects/${PROJ_ID}/locations/${REGION}/agentGateways/${AGW_NAME}"
export MCP_NAME="mcp-sprockets"

echo ${AGW_NAME}
echo ${AGW_URI}
echo ${MCP_NAME}
# create resource vars for gemini enterprise (automatic)
export GE_LOCATION="global"
export GE_APP_ORG_NAME="${SLUG}, Inc."
export GE_APP_DISPLAY_NAME="${SLUG} ${GE_LOCATION} app"
export GE_APP_NAME="app-${SLUG}-${GE_LOCATION}"
export GE_APP_INIT="${GE_APP_NAME}_$(date +%s)"

echo ${GE_LOCATION}
echo ${GE_APP_ORG_NAME}
echo ${GE_APP_DISPLAY_NAME}
echo ${GE_APP_NAME}
echo ${GE_APP_INIT}

Set agent identity trust domains

The if-then-else statement checks if the project belongs to an organization in order to set the correct trust domain for the principal agent identities.

# set var for trust domain
if [[ -n "${ORG_ID}" ]]; then
  export TRUST_DOMAIN="agents.global.org-${ORG_ID}.system.id.goog"
else
  export TRUST_DOMAIN="agents.global.proj-${PROJ_NO}.system.id.goog"
fi

echo "trust domain: ${TRUST_DOMAIN}"

Set billing and quota project

# set cli quota project
gcloud config set billing/quota_project ${PROJ_ID}
# set api quota project
gcloud auth application-default set-quota-project ${PROJ_ID}

Create local directory for config files

# create config folder
mkdir -p cfg

If running a self-managed install of the Google Cloud SDK (ie, outside of Cloud Shell), update the components to the latest version.

# update gcloud cli
gcloud components update

Enable API services

# enable google cloud apis
gcloud services enable \
  agentregistry.googleapis.com \
  aiplatform.googleapis.com \
  cloudapiregistry.googleapis.com \
  cloudresourcemanager.googleapis.com \
  compute.googleapis.com \
  discoveryengine.googleapis.com \
  dns.googleapis.com \
  iam.googleapis.com \
  iap.googleapis.com \
  logging.googleapis.com \
  monitoring.googleapis.com \
  networkconnectivity.googleapis.com \
  networksecurity.googleapis.com \
  networkservices.googleapis.com \
  orgpolicy.googleapis.com \
  serviceextensions.googleapis.com \
  serviceusage.googleapis.com

Organization policies

Default Google Cloud managed organization policy constraints and sandbox policies can restrict features used in this Codelab:

  • compute.vmExternalIpAccess: Restricts external IPv4 addresses on Compute Engine VMs in locked-down sandbox organizations.
  • discoveryengine.managed.disableCustomMcpServerConnector: Restricts creation of data connectors that use a custom MCP server (custom_mcp) as a data source (enforced by default).
  • discoveryengine.managed.allowedEgressFqdns: Restricts outbound egress domains (instance_uri FQDNs) for data connectors when VPC Service Controls (VPC-SC) is active or the project is listed in enforcedProjects.
  • discoveryengine.managed.allowedDataSources: Restricts allowed data connector types (dataSource) when VPC-SC is active or the project is listed in enforcedProjects.

Override any inherited organization policy restrictions at the project level.

Disable custom MCP constraint

# disable data connector constraint (allow custom mcp servers)
gcloud org-policies set-policy "/dev/stdin" <<EOF
name: projects/${PROJ_ID}/policies/discoveryengine.managed.disableCustomMcpServerConnector
spec:
  rules:
  - enforce: false
EOF

Disable conditional data connector constraints

# disable egress fqdn constraint on project
gcloud org-policies set-policy "/dev/stdin" <<EOF
name: projects/${PROJ_ID}/policies/discoveryengine.managed.allowedEgressFqdns
spec:
  rules:
  - enforce: false
EOF
# disable allowed data sources constraint on project
gcloud org-policies set-policy "/dev/stdin" <<EOF
name: projects/${PROJ_ID}/policies/discoveryengine.managed.allowedDataSources
spec:
  rules:
  - enforce: false
EOF

Disable access policy constraint

# disable iam v3 constraint (allow v3 access policies)
gcloud org-policies set-policy /dev/stdin << EOF
name: projects/${PROJ_NO}/policies/iam.managed.disableAccessPolicyBinding
spec:
  rules:
  - enforce: false
EOF

Allow external IP addresses

# allow external ip address for vms on project
gcloud org-policies set-policy "/dev/stdin" <<EOF
name: projects/${PROJ_ID}/policies/compute.vmExternalIpAccess
spec:
  rules:
  - allowAll: true
EOF

Verify organization policies

Check that compute.vmExternalIpAccess is set to allowAll: True and the other policies are enforce: False:

# verify effective org policy constraints on project
for policy in \
  discoveryengine.managed.disableCustomMcpServerConnector \
  discoveryengine.managed.allowedEgressFqdns \
  discoveryengine.managed.allowedDataSources \
  iam.managed.disableAccessPolicyBinding \
  compute.vmExternalIpAccess; do
  gcloud org-policies describe ${policy} \
    --project=${PROJ_ID} \
    --effective \
    --format="flattened[no-pad](name.basename(),spec.rules[0])"
done

Verify service agent bindings (precautionary)

In a new project, Google Cloud automatically provisions the Agent Gateway service agent and grants it roles/agentgateway.serviceAgent when networkservices.googleapis.com is first enabled. If you are reusing an existing project where prior cleanup may have removed default service agent bindings, run the following commands as a fail-safe to ensure the identity and role binding are intact:

# ensure network services service account has been created
gcloud beta services identity create \
  --service=networkservices.googleapis.com \
  --project="${PROJ_ID}"

# ensure network services service account has service agent roles applied
gcloud projects add-iam-policy-binding "${PROJ_ID}" \
  --member="serviceAccount:service-${PROJ_NO}@gcp-sa-agentgateway.iam.gserviceaccount.com" \
  --role="roles/agentgateway.serviceAgent"

This concludes the setup portion... next on to the Network section.

4. Network

In this section, you reserve the static external IPv4 addresses, create the internal egress VPC network and isolated external VPC network, provision the PSC network attachment, configure Cloud NAT, and create firewall rules.

Reserve static external IP addresses

Reserve two regional static external IPv4 addresses for Cloud NAT and the external MCP server VM:

# reserve static external ipv4 address for cloud nat
gcloud compute addresses create ip-${SLUG}-nat \
  --region=${REGION}

# reserve static external ipv4 address for the mcp server
gcloud compute addresses create ip-${SLUG}-mcp \
  --region=${REGION}

Verify static external IP addresses

Gemini Enterprise requires remote MCP endpoints to use HTTPS with a publicly trusted TLS certificate. Rather than registering a custom domain for this lab, you use sslip.io, a free wildcard DNS service that resolves any hostname containing an IP address (such as mcp.[IP].sslip.io) directly back to that IP. Because Certificate Authorities like Let's Encrypt do not issue certificates to bare IP addresses, this sslip.io hostname gives the Caddy reverse proxy a valid domain name to automatically provision a trusted TLS certificate for the MCP server.

Export both IPv4 addresses and construct the sslip.io domain name and URL for the external MCP server:

# export static ip addresses and set vars for domain
export NAT_EXT_IP=$(gcloud compute addresses describe ip-${SLUG}-nat \
  --region=${REGION} \
  --format="value(address)")

export MCP_EXT_IP=$(gcloud compute addresses describe ip-${SLUG}-mcp \
  --region=${REGION} \
  --format="value(address)")

export MCP_DOMAIN="mcp.${MCP_EXT_IP}.sslip.io"
export MCP_URL="https://${MCP_DOMAIN}/mcp"

echo "cloud nat external ip    : ${NAT_EXT_IP}"
echo "mcp server external ip   : ${MCP_EXT_IP}"
echo "mcp url                  : ${MCP_URL}"

Example output:

cloud nat external ip    : 203.0.113.10
mcp server external ip   : 203.0.113.20
mcp url                  : https://mcp.203.0.113.20.sslip.io/mcp

Create internal network

When the Agent Gateway connectivity template is configured to use an egressNetworkConfig with a networkAttachment, the subnet hosting the PSC network attachment must be at least a /28 CIDR block and have Private Google Access (--enable-private-ip-google-access) enabled.

Create a global VPC network for internal Agent Gateway egress:

# create vpc network
gcloud compute networks create vnet-${SLUG}-internal --subnet-mode=custom

# create subnet for agent gateway psc na
gcloud compute networks subnets create subnet-${REGION}-agw \
  --network=vnet-${SLUG}-internal \
  --range=10.10.10.0/28 \
  --region=${REGION} \
  --enable-private-ip-google-access

Create internal firewall rules

Create a firewall policy to allow all egress traffic with logging enabled. This will be used to monitor traffic egressing from Agent Gateway to the internal VPC network. Cloud NGFW supports both Essentials and Standard tiers for network security and traffic monitoring.

# create fw policy
gcloud compute network-firewall-policies create fw-policy-${SLUG}-internal --global
# create fw policy rule
gcloud compute network-firewall-policies rules create 1001 \
  --description="allow all out and log" \
  --firewall-policy=fw-policy-${SLUG}-internal \
  --global-firewall-policy \
  --action=allow \
  --direction=EGRESS \
  --layer4-configs=all \
  --dest-ip-ranges=0.0.0.0/0 \
  --enable-logging
# bind fw policy to network
gcloud compute network-firewall-policies associations create \
  --name=fw-policy-bind-${SLUG}-internal \
  --firewall-policy=fw-policy-${SLUG}-internal \
  --network=vnet-${SLUG}-internal \
  --global-firewall-policy

Create PSC network attachment

Create a Private Service Connect (PSC) network attachment configured to automatically accept connections from Agent Gateway. The network attachment establishes the consumer VPC network side of the connection to securely link with the Agent Gateway producer side for outbound egress traffic. For additional information on subnet requirements and IP range specifications, see Configure VPC connectivity.

# create psc network attachment
gcloud compute network-attachments create psc-na-${REGION}-agw \
  --region=${REGION} \
  --subnets=subnet-${REGION}-agw \
  --connection-preference=ACCEPT_AUTOMATIC

Verify PSC network attachment

# show psc network attachment details
gcloud compute network-attachments describe psc-na-${REGION}-agw --region=${REGION}

Retrieve the resource URI of the PSC network attachment and store it in the PSC_NA_URI environment variable. This URI will be referenced in the Agent Gateway configuration (networkConfig.egress.networkAttachment) to provision the PSC Interface for network egress into the VPC network:

# fetch psc network attachment uri
export PSC_NA_URI=$(gcloud compute network-attachments describe psc-na-${REGION}-agw \
  --region=${REGION} \
  --format="value(selfLink.scope(v1))")
echo ${PSC_NA_URI}

Create Cloud NAT

Cloud NAT is used to translate the source IP address of Agent Gateway egress traffic to the reserved public IP address for routing over public internet.

Only sources from Agent Gateway egress network attachment subnet are defined as eligible to use Cloud NAT.

# create router for nat
gcloud compute routers create cr-nat-${SLUG}-${REGION} \
  --network=vnet-${SLUG}-internal \
  --asn=16550 \
  --region=${REGION}

# create nat
gcloud compute routers nats create nat-${SLUG}-${REGION} \
  --router=cr-nat-${SLUG}-${REGION} \
  --region=${REGION} \
  --nat-custom-subnet-ip-ranges="subnet-${REGION}-agw" \
  --nat-external-ip-pool="ip-${SLUG}-nat" \
  --enable-logging \
  --log-filter=ALL

Verify Cloud NAT

Check Cloud NAT status and the expected natIP source is configured:

# verify cloud nat configuration
gcloud compute routers nats describe nat-${SLUG}-${REGION} \
  --router=cr-nat-${SLUG}-${REGION} \
  --region=${REGION} \
  --format="yaml(name,natIps,subnetworks.name)"

Example output:

name: nat-foo-us-central1
natIps:
- https://www.googleapis.com/compute/v1/projects/[PROJ_ID]/regions/us-central1/addresses/ip-foo-nat

Create external network

Create a separate, isolated VPC network to host the external MCP server VM.

# create vpc network
gcloud compute networks create vnet-${SLUG}-external --subnet-mode=custom

# create subnet for mcp server
gcloud compute networks subnets create subnet-${REGION}-mcp \
  --network=vnet-${SLUG}-external \
  --range=240.240.240.0/24 \
  --region=${REGION} \
  --enable-private-ip-google-access

Create external firewall rules

Create two ingress firewall rules on the external VPC network:

  • 2001: Allows inbound TCP port 80 so Let's Encrypt can complete the ACME HTTP-01 domain validation challenge for ${MCP_DOMAIN}.
  • 2002: Initially allows inbound TCP port 443 from 0.0.0.0/0 to verify TLS readiness and OAuth 2.0 tokeninfo enforcement from local shell before locking it down strictly to ${NAT_EXT_IP}/32.
# create fw policy
gcloud compute network-firewall-policies create fw-policy-${SLUG}-external --global
# create fw policy rules
gcloud compute network-firewall-policies rules create 2001 \
  --description="allow in any http" \
  --firewall-policy=fw-policy-${SLUG}-external \
  --global-firewall-policy \
  --action=allow \
  --direction=INGRESS \
  --layer4-configs=tcp:80 \
  --src-ip-ranges=0.0.0.0/0 \
  --dest-ip-ranges=240.240.240.0/24 \
  --enable-logging

gcloud compute network-firewall-policies rules create 2002 \
  --description="allow in any https" \
  --firewall-policy=fw-policy-${SLUG}-external \
  --global-firewall-policy \
  --action=allow \
  --direction=INGRESS \
  --layer4-configs=tcp:443 \
  --src-ip-ranges=0.0.0.0/0 \
  --dest-ip-ranges=240.240.240.0/24 \
  --enable-logging
# bind fw policy to network
gcloud compute network-firewall-policies associations create \
  --name=fw-policy-bind-${SLUG}-external \
  --firewall-policy=fw-policy-${SLUG}-external \
  --network=vnet-${SLUG}-external \
  --global-firewall-policy

This concludes the network portion... next on to the Agent Gateway section.

5. Agent Gateway

In this section, you deploy the Agent Connectivity Template with vpcEgress: ALL_TRAFFIC, deploy the regional Agent Gateway, and delegate authorization to Identity-Aware Proxy (IAP), and define IAM Unified Access Policy rules to govern outbound network requests.

Create connectivity template

The connectivity template spec defines:

  • egressNetworkConfig.networkAttachment: Points to the PSC network attachment, connecting Agent Gateway into your VPC network.
  • egressNetworkConfig.vpcEgress: Set to ALL_TRAFFIC so all outbound agent connections (including public internet MCP endpoints) will route into the VPC network and exit using Cloud NAT source IP address translation.

Create and import the connectivity template configuration file:

# create connectivity template config file (vpcEgress: ALL_TRAFFIC)
cat > cfg/act-${AGW_NAME}.yaml << EOF
accessPath: AGENT_TO_ANYWHERE
name: projects/${PROJ_NO}/locations/${REGION}/agentConnectivityTemplates/act-${AGW_NAME}
deploymentModel: CENTRALIZED
egressNetworkConfig:
  networkAttachment: ${PSC_NA_URI}
  vpcEgress: ALL_TRAFFIC
EOF
# import connectivity template file (create template)
gcloud network-services agent-connectivity-templates import act-${AGW_NAME} \
  --source="cfg/act-${AGW_NAME}.yaml" \
  --location=${REGION}

Create Agent Gateway

The Agent Gateway spec defines:

  • agentConnectivityTemplate: Points to the connectivity template, so the gateway can use the PSC network attachment for VPC connectivity.
  • registries: Associates the gateway with the regional Agent Registry to resolve cataloged MCP endpoints and tool schemas.

Create and import the Agent Gateway configuration file:

# create agent gateway config file
cat > cfg/${AGW_NAME}.yaml << EOF
name: ${AGW_NAME}
protocols:
  - MCP
googleManaged:
  governedAccessPath: AGENT_TO_ANYWHERE
agentConnectivityTemplate: projects/${PROJ_NO}/locations/${REGION}/agentConnectivityTemplates/act-${AGW_NAME}
registries:
  - "//agentregistry.googleapis.com/projects/${PROJ_ID}/locations/${REGION}"
EOF
# import agent gateway config file (create gateway)
gcloud network-services agent-gateways import ${AGW_NAME} \
  --source="cfg/${AGW_NAME}.yaml" \
  --location=${REGION}

Verify Agent Gateway deployment

Confirm the Agent Registry and connectivity template configuration:

# verify agent gateway deployment details
gcloud network-services agent-gateways describe ${AGW_NAME} \
  --location=${REGION} \
  --format="yaml(name,agentConnectivityTemplate,registries)"

Example output:

agentConnectivityTemplate: projects/[PROJ_NO]/locations/us-central1/agentConnectivityTemplates/act-foo-us-central1
name: projects/[PROJ_ID]/locations/us-central1/agentGateways/agw-foo-us-central1-ata
registries:
- //agentregistry.googleapis.com/projects/[PROJ_ID]/locations/us-central1

Inspect the PSC network attachment to confirm the gateway connection:

# show psc network attachment details
gcloud compute network-attachments describe psc-na-${REGION}-agw \
  --region=${REGION} \
  --format="yaml(connectionEndpoints)"

Example output:

connectionEndpoints:
- ipAddress: 10.10.10.2
  projectIdOrNum: '[TENANT_PROJ_NO]'
  status: ACCEPTED
  subnetwork: https://www.googleapis.com/compute/v1/projects/[PROJ_ID]/regions/us-central1/subnetworks/subnet-us-central1-agw

Delegate authorization

Agent Gateway secures and governs outbound tool traffic using Authorization Policies (networksecurity.authzPolicies) integrated with Identity-Aware Proxy (IAP) Unified Access Policies (UAP).

While Agent Gateway supports basic inline ALLOW and DENY rules, enterprise environments require centralized, identity-centric governance. With IAM Unified Access Policies (or Access Policies), you manage egress access rules using standard IAM v3 access policies.

figure3

Fig 3. Authorization architecture

The authorization flow connects three components:

  1. Gateway authorization policy (authzPolicy):
    • A regional resource targeting Agent Gateway.
    • Configured with policyProfile: REQUEST_AUTHZ and action: CUSTOM to route all outbound authorization checks to the IAP authorization extension.
  2. IAP service extension (authzExtension):
    • A regional resource that delegates request authorization to Identity-Aware Proxy (iap.googleapis.com).
    • Evaluates policies in ENFORCE mode using policy version V2.
  3. IAM Unified Access Policy and Binding (accessPolicy & policyBinding):
    • Global IAM v3 resources containing fine-grained access rules.
    • Authenticates the SPIFFE principal identity of the calling agent, verifies the universal iap.googleapis.com/resources.egressViaIAP permission, and evaluates Common Expression Language (CEL) conditions against destination attributes.

Deploy authorization extension

# create authz extension config file
cat > "cfg/${AGW_NAME}-svc-ext-authz-iap.yaml" << EOF
name: ${AGW_NAME}-svc-ext-authz-iap
service: iap.googleapis.com
failOpen: false
timeout: 1s
metadata:
  iapPolicyVersion: "V2"
EOF
# import authz extension config file (create authz extension)
gcloud service-extensions authz-extensions import ${AGW_NAME}-svc-ext-authz-iap \
  --source="cfg/${AGW_NAME}-svc-ext-authz-iap.yaml" \
  --location=${REGION}

Verify authorization extension

Check the authorization extension is active:

# list authz extensions
gcloud service-extensions authz-extensions list \
  --location=${REGION} \
  --format="table(
    name.basename():label=NAME,
    createTime.date(tz=LOCAL):label=CREATED,
    service:label=SERVICE,
    metadata:label=METADATA,
    timeout:label=TIMEOUT
  )"

Deploy authorization policy

Create a network-security authorization policy configuration that targets Agent Gateway and delegates request verification to the authorization extension for IAP:

# create authz policy config file
cat > cfg/${AGW_NAME}-authz-policy-iap.yaml << EOF
name: ${AGW_NAME}-authz-policy-iap
target:
  resources:
    - "projects/${PROJ_ID}/locations/${REGION}/agentGateways/${AGW_NAME}"
policyProfile: REQUEST_AUTHZ
action: CUSTOM
customProvider:
  authzExtension:
    resources:
      - "projects/${PROJ_ID}/locations/${REGION}/authzExtensions/${AGW_NAME}-svc-ext-authz-iap"
EOF
# import authz policy config file (create authz policy)
gcloud network-security authz-policies import ${AGW_NAME}-authz-policy-iap \
  --source=cfg/${AGW_NAME}-authz-policy-iap.yaml \
  --location=${REGION}

Verify authorization policy

Check the authorization policy is active:

# list authz policies
gcloud network-security authz-policies list \
  --location=${REGION} \
  --format="table(
    name.basename():label=NAME,
    action:label=ACTION,
    policyProfile:label=POLICY_PROFILE,
    customProvider.authzExtension.resources[0].basename():label=CUSTOM_PROVIDER_RESOURCE
  )"

Create IAM access policies

Agent Gateway now delegates authorization checks to IAP and resolves destination metadata from Agent Registry. Next, define an IAM Unified Access Policy rule to govern outbound tool execution.

IAP evaluates CEL attribute expressions against the following Agent Registry destination attributes:

  • Registered status (destination.is_registered):
    • Boolean (true/false) indicating if the destination is cataloged in Agent Registry.
  • MCP server name (destination.agent_registry.mcp_server.name):
    • Canonical MCP server resource name registered in Agent Registry.
  • MCP method (destination.agent_registry.mcp_server.method):
    • The MCP method being invoked (eg, tools/call, tools/list, initialize).
  • Tool name (destination.agent_registry.mcp_server.tool.name):
    • The specific tool name invoked (eg, get_sprocket_by_id or get_sprockets), enabling fine-grained, tool-level authorization on registered MCP servers.

Define IAM access policy rule

The IAM policy rule manifest specifies:

  • Principals: The SPIFFE principal identity representing the Gemini Enterprise core assistant agent.
  • Permissions: The universal iap.googleapis.com/resources.egressViaIAP permission required for all IAP-governed egress traffic.
  • Conditions: A CEL expression (destination.is_registered == true) ensuring the agent can only invoke endpoints cataloged in Agent Registry.

Create the policy rule manifest file:

# create access policy rule file
cat > cfg/uap-policy-${SLUG}-rules.json << EOF
[
  {
    "description": "allow ge assistant to any registered service",
    "effect": "ALLOW",
    "principals": [
      "principal://${TRUST_DOMAIN}/resources/discoveryengine/projects/${PROJ_NO}/locations/global/engines/${GE_APP_INIT}/assistants/default_assistant/agents/default/core_assistant"
    ],
    "operation": {
      "permissions": [
        "iap.googleapis.com/resources.egressViaIAP"
      ]
    },
    "conditions": {
      "iap.googleapis.com": {
        "expression": \
        "destination.is_registered == true"
      }
    }
  }
]
EOF

Deploy IAM access policy

Create the global IAM access policy using the rules defined in the manifest file:

# create iam access policy
gcloud iam access-policies create uap-policy-${SLUG} \
  --details-rules=cfg/uap-policy-${SLUG}-rules.json \
  --project=${PROJ_ID} \
  --location=global

Verify IAM access policy

Check the IAM access policy was created successfully and inspect the rule details:

# show iam access policy details
gcloud iam access-policies describe uap-policy-${SLUG} \
  --project=${PROJ_ID} \
  --location=global

Example output:

details:
  rules:
  - conditions:
      iap.googleapis.com:
        expression: destination.is_registered == true
    description: allow ge assistant to any registered service
    effect: ALLOW
    operation:
      permissions:
      - iap.googleapis.com/resources.egressViaIAP
    principals:
    - principal://agents.global.org-[ORG_ID].system.id.goog/resources/discoveryengine/projects/[PROJ_NO]/locations/global/engines/app-foo-global_[EPOCH]/assistants/default_assistant/agents/default/core_assistant
name: projects/[PROJ_ID]/locations/global/accessPolicies/uap-policy-foo

Bind IAM access policy to project

To activate enforcement across all Agent Gateways in your project, create a policy binding that attaches the IAM access policy to the project resource:

# bind iam access policy to project resource
gcloud iam policy-bindings create uap-bind-${SLUG} \
  --policy="projects/${PROJ_ID}/locations/global/accessPolicies/uap-policy-${SLUG}" \
  --target-resource="//cloudresourcemanager.googleapis.com/projects/${PROJ_ID}" \
  --project=${PROJ_ID} \
  --location=global

Verify IAM access policy binding

Check the active policy binding points to the correct policy and target:

# show policy binding details
gcloud iam policy-bindings describe uap-bind-${SLUG} \
  --project=${PROJ_ID} \
  --location=global

This concludes the Agent Gateway portion... next on to the MCP server section.

6. MCP server

In this section, you deploy an external FastMCP OAuth 2.0 Resource Server (mcp-sprockets) exposing get_sprockets and get_sprocket_by_id tools, verify that it cryptographically validates Google OAuth 2.0 Bearer tokens against https://oauth2.googleapis.com/tokeninfo. Next you will lock down the external firewall rule strictly to the Cloud NAT static IP.

Deploy external MCP server

A startup script installs Caddy and FastMCP, launches the mcp-sprockets server on 127.0.0.1:8080, and configures Caddy to serve HTTPS on ${MCP_DOMAIN}.

Every time Gemini Enterprise calls an MCP tool (eg, get_sprockets and get_sprocket_by_id), the server will:

  1. Read the caller source IP from the X-Forwarded-For header (egress_source_ip).
  2. Extract the Authorization: Bearer [access_token] header and verify the token against the Google OAuth 2.0 verification endpoint (https://oauth2.googleapis.com/tokeninfo?access_token=[token]).
  3. Return "authenticated_user" ([user_email]) alongside "egress_source_ip", proving that the external MCP server verified both the Cloud NAT static IP and the end user Google OAuth 2.0 identity.
# create startup script for the sprockets mcp server vm
cat > cfg/server-startup.sh << 'EOF'
#!/bin/bash
set -euo pipefail

MCP_DOMAIN=$(curl -s -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/instance/attributes/mcp-domain)

export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y caddy python3-venv curl

mkdir -p /opt/mcp-sprockets
python3 -m venv /opt/mcp-sprockets/venv
/opt/mcp-sprockets/venv/bin/pip install --upgrade pip
/opt/mcp-sprockets/venv/bin/pip install "fastmcp>=2.14.0" "uvicorn>=0.34.0"

cat <<'PYEOF' > /opt/mcp-sprockets/main.py
import json
import urllib.parse
import urllib.request
from typing import Any, Dict
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_http_headers
from mcp.types import ToolAnnotations

mcp = FastMCP("mcp-sprockets")

SPROCKETS = {
    "SPK-101": {"id": "SPK-101", "model": "ANSI-40-12T", "teeth": 12, "pitch_in": 0.5, "material": "1045 Carbon Steel", "stock": 140, "unit_price": 18.50},
    "SPK-102": {"id": "SPK-102", "model": "ANSI-40-24T", "teeth": 24, "pitch_in": 0.5, "material": "1045 Carbon Steel", "stock": 85, "unit_price": 29.00},
    "SPK-103": {"id": "SPK-103", "model": "ANSI-60-16T", "teeth": 16, "pitch_in": 0.75, "material": "304 Stainless Steel", "stock": 60, "unit_price": 42.00},
    "SPK-104": {"id": "SPK-104", "model": "ANSI-60-32T", "teeth": 32, "pitch_in": 0.75, "material": "304 Stainless Steel", "stock": 35, "unit_price": 68.00},
    "SPK-105": {"id": "SPK-105", "model": "ANSI-80-20T", "teeth": 20, "pitch_in": 1.0, "material": "4140 Alloy Steel", "stock": 50, "unit_price": 95.00}
}

def verify_google_oauth_token(headers: Dict[str, str]) -> str:
    token = headers.get("authorization", "")[7:].strip() if headers.get("authorization", "").lower().startswith("bearer ") else ""
    if not token:
        return "unauthenticated (missing Bearer token)"
    url = "https://oauth2.googleapis.com/tokeninfo?" + urllib.parse.urlencode({"access_token": token})
    with urllib.request.urlopen(url, timeout=5) as resp:
        info = json.loads(resp.read().decode("utf-8"))
        return info.get("email", info.get("sub", "verified-oauth-user"))

@mcp.tool(
    description="list all sprockets in the inventory catalog",
    annotations=ToolAnnotations(
        readOnlyHint=True,
        destructiveHint=False,
        idempotentHint=True,
        openWorldHint=False
    )
)
def get_sprockets() -> Dict[str, Any]:
    headers = get_http_headers(include_all=True)
    caller_ip = headers.get("x-forwarded-for", "unknown").split(",")[0].strip()
    try:
        user_email = verify_google_oauth_token(headers)
    except Exception as exc:
        return {"egress_source_ip": caller_ip, "authenticated_user": None, "error": f"OAuth token verification failed: {exc}"}
    return {
        "egress_source_ip": caller_ip,
        "authenticated_user": user_email,
        "sprockets": list(SPROCKETS.values())
    }

@mcp.tool(
    description="fetch details of a specific sprocket by its ID (eg, SPK-101)",
    annotations=ToolAnnotations(
        readOnlyHint=True,
        destructiveHint=False,
        idempotentHint=True,
        openWorldHint=False
    )
)
def get_sprocket_by_id(sprocket_id: str) -> Dict[str, Any]:
    headers = get_http_headers(include_all=True)
    caller_ip = headers.get("x-forwarded-for", "unknown").split(",")[0].strip()
    try:
        user_email = verify_google_oauth_token(headers)
    except Exception as exc:
        return {"egress_source_ip": caller_ip, "authenticated_user": None, "error": f"OAuth token verification failed: {exc}"}
    sprocket = SPROCKETS.get(sprocket_id.upper())
    if sprocket:
        return {
            "egress_source_ip": caller_ip,
            "authenticated_user": user_email,
            "sprocket": sprocket
        }
    return {
        "egress_source_ip": caller_ip,
        "authenticated_user": user_email,
        "error": f"Sprocket '{sprocket_id}' not found."
    }

app = mcp.http_app(stateless_http=True)
PYEOF

cat <<'SVCEOF' > /etc/systemd/system/mcp-sprockets.service
[Unit]
Description=Sprockets FastMCP Server
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/mcp-sprockets
ExecStart=/opt/mcp-sprockets/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8080 --forwarded-allow-ips="*"
Restart=always

[Install]
WantedBy=multi-user.target
SVCEOF

systemctl daemon-reload
systemctl enable --now mcp-sprockets.service
systemctl restart mcp-sprockets.service

cat <<CADDYEOF > "/etc/caddy/Caddyfile"
${MCP_DOMAIN} {
    reverse_proxy 127.0.0.1:8080
}
CADDYEOF

systemctl restart caddy
EOF
# create vm instance
gcloud compute instances create vm-${MCP_NAME} \
  --zone=${ZONE} \
  --machine-type=e2-micro \
  --image-family=debian-12 \
  --image-project=debian-cloud \
  --shielded-secure-boot \
  --network-interface="subnet=subnet-${REGION}-mcp,address=${MCP_EXT_IP}" \
  --metadata="mcp-domain=${MCP_DOMAIN}" \
  --metadata-from-file="startup-script=cfg/server-startup.sh"

Verify MCP server readiness and OAuth 2.0 tokeninfo validation

Wait a few minutes for the startup script to install FastMCP and Caddy and to provision the Let's Encrypt TLS certificate.

# poll until caddy serves valid tls and fastmcp responds to tools/list
for i in $(seq 1 30); do
  RESP=$(curl -s -X POST "${MCP_URL}" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}' || true)
  if echo "${RESP}" | grep -q "get_sprockets"; then
    echo "mcp server and let's encrypt tls ready"
    break
  fi
  echo "...waiting for mcp server and let's encrypt tls (attempt ${i}/30)..."
  sleep 5
done

Validate sslip.io DNS resolution, the Let's Encrypt TLS certificate served by Caddy, and the FastMCP tools/list response:

# query mcp server for tools list
curl -v -X POST "${MCP_URL}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}' \
  2>&1 | grep -E "Connected to|subject:|issuer:|SSL certificate|HTTP/|event:|data:"

Example output:

* Connected to mcp.203.0.113.20.sslip.io (203.0.113.20) port 443
*  subject: CN=mcp.203.0.113.20.sslip.io
*  issuer: C=US; O=Let's Encrypt; CN=E5
*  SSL certificate verify ok.
> POST /mcp HTTP/2
< HTTP/2 200
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"get_sprockets","description":"list all sprockets in the inventory catalog","inputSchema":{"properties":{},"type":"object"},"annotations":{"destructiveHint":false,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":true}},{"name":"get_sprocket_by_id","description":"fetch details of a specific sprocket by its ID (eg, SPK-101)","inputSchema":{"properties":{"sprocket_id":{"type":"string"}},"required":["sprocket_id"],"type":"object"},"annotations":{"destructiveHint":false,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":true}}]}}

Before locking down the external firewall, run a test tools/call passing a Google OAuth 2.0 Bearer token (gcloud auth print-access-token) to verify that mcp-sprockets validates the token against https://oauth2.googleapis.com/tokeninfo and returns your "authenticated_user" email alongside the caller source IP address:

# query mcp server (test tools/call with google oauth 2.0 bearer token)
curl -s -X POST "${MCP_URL}" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "get_sprocket_by_id",
      "arguments": {"sprocket_id": "SPK-105"}
    }
  }' | sed -n 's/^data: //p' | jq '.result.structuredContent'

Example output (showing your direct Cloud Shell IP prior to firewall lockdown and your verified Google OAuth email):

{
  "egress_source_ip": "198.51.100.154",
  "authenticated_user": "user@example.com",
  "sprocket": {
    "id": "SPK-105",
    "model": "ANSI-80-20T",
    "teeth": 20,
    "pitch_in": 1.0,
    "material": "4140 Alloy Steel",
    "stock": 50,
    "unit_price": 95.0
  }
}

Lock down firewall to Cloud NAT static IP

Update the external VPC network firewall policy so TCP port 443 accepts traffic only from the Cloud NAT static external IP (${NAT_EXT_IP}/32):

# create fw policy rule allowing https only from cloud nat static ip
gcloud compute network-firewall-policies rules create 2003 \
  --description="allow in any https from nat ip" \
  --firewall-policy=fw-policy-${SLUG}-external \
  --global-firewall-policy \
  --action=allow \
  --direction=INGRESS \
  --layer4-configs=tcp:443 \
  --src-ip-ranges="${NAT_EXT_IP}/32" \
  --dest-ip-ranges=240.240.240.0/24 \
  --enable-logging
# delete temporary open fw policy rules
gcloud compute network-firewall-policies rules delete 2001 \
  --firewall-policy=fw-policy-${SLUG}-external \
  --global-firewall-policy

gcloud compute network-firewall-policies rules delete 2002 \
  --firewall-policy=fw-policy-${SLUG}-external \
  --global-firewall-policy

Check the external firewall policy rules are in place:

# list all rules in the global network firewall policy
gcloud compute network-firewall-policies describe fw-policy-${SLUG}-external \
  --global \
  --flatten="rules[]" \
  --format="table(
    rules.priority:label=RULE,
    rules.action:label=ACTION,
    rules.direction:label=DIRECTION,
    firstof(
      rules.match.srcIpRanges,
      rules.match.srcAddressGroups,
      rules.match.srcSecureTags.name
    ).map().sub('projects/[^/]+/(locations/global/|global/)', '').list():label=SRC,
    firstof(
      rules.targetForwardingRules,
      rules.match.destIpRanges,
      rules.targetType
    ).map().sub('projects/[^/]+/(locations/global/|global/)', '').list():label=DST
  )"

Check that direct connections outside the Cloud NAT static IP are now blocked:

# query mcp server
curl -i --connect-timeout 5 "${MCP_URL}" \
  || echo "Expected: Direct access blocked (not originating from ${NAT_EXT_IP})"

Example output:

curl: (28) Connection timed out after 5001 milliseconds
Expected: Direct access blocked (not originating from 203.0.113.10)

This concludes the MCP server portion... next on to the Agent Registry section.

7. Agent Registry

The Agent Registry is a centralized inventory of all agents, MCP servers, and endpoints (APIs) in the AI ecosystem. It is also a catalog that can be used to list, search, and discover other registered tools and services for use by AI applications. Agent Gateway uses the registry data model as the governance framework to enforce egress access control. IAM role grants for principal calling agents are checked against the policy bound to the registry resource.

In this section, you will register the MCP server and its tool schema (toolspec.json) in Agent Registry. To support Gemini Enterprise interacting with Google APIs and Services through Agent Gateway, you will also create a single registry endpoint service (oauth2-googleapis) that specifies the Google API interfaces used by Gemini Enterprise for exchanging the auth code for tokens.

Register MCP server

To let Gemini Enterprise discover the tools available on the MCP server, provide a tool specifications file (cfg/toolspec.json) when registering the service in Agent Registry.

Create MCP tool specification

# create tool spec file for the sprockets mcp server
cat <<'EOF' > cfg/toolspec.json
{
  "tools": [
    {
      "name": "get_sprockets",
      "description": "List all sprockets in the inventory catalog.",
      "inputSchema": {
        "type": "object",
        "properties": {}
      },
      "isReadOnly": true,
      "isDestructive": false,
      "isIdempotent": true,
      "isOpenWorld": false
    },
    {
      "name": "get_sprocket_by_id",
      "description": "Fetch details of a specific sprocket by its ID (e.g., SPK-101).",
      "inputSchema": {
        "type": "object",
        "properties": {
          "sprocket_id": {
            "type": "string",
            "description": "The sprocket identifier (e.g., SPK-101, SPK-105)."
          }
        },
        "required": [
          "sprocket_id"
        ]
      },
      "isReadOnly": true,
      "isDestructive": false,
      "isIdempotent": true,
      "isOpenWorld": false
    }
  ]
}
EOF

Create MCP server service

# create registry service (mcp server)
gcloud agent-registry services create ${MCP_NAME} \
  --project=${PROJ_ID} \
  --location=${REGION} \
  --display-name="${MCP_DOMAIN}" \
  --description="MANDATORY SPROCKETS INVENTORY & CATALOG AGENT: You MUST \
ALWAYS invoke this tool (get_sprockets or get_sprocket_by_id) for ANY question \
about sprockets, inventory catalog listings, sprocket IDs (SPK-101 through \
SPK-105), ANSI models (ANSI-40, ANSI-60, ANSI-80), tooth counts, pitch, \
material, stock levels, unit prices, authenticated_user, or egress_source_ip \
verification. NEVER guess or fabricate inventory data yourself and NEVER \
transfer sprocket queries to file_and_coding_agent or web search. Always \
delegate every sprocket or inventory lookup to this tool." \
  --mcp-server-spec-type=tool-spec \
  --mcp-server-spec-content=cfg/toolspec.json \
  --interfaces=protocolBinding=JSONRPC,url="${MCP_URL}"

Verify MCP server in Agent Registry

Verify that the service is listed as a registered MCP server in the region along with its endpoint URL and available tools:

# list registry regional mcp servers
gcloud agent-registry mcp-servers list \
  --location=${REGION} \
  --project=${PROJ_ID} \
  --format="table(
    name.basename():label=REGISTRY_ID,
    displayName:label=DISPLAY_NAME,
    interfaces[0].url:label=ENDPOINT_URL,
    tools[].name.list():label=TOOLS
  )"

Example output:

REGISTRY_ID                                          DISPLAY_NAME               ENDPOINT_URL                          TOOLS
agentregistry-00000000-0000-0000-5c39-1f6e542bb2b8   mcp.203.0.113.20.sslip.io  https://mcp.203.0.113.20.sslip.io/mcp  get_sprockets,get_sprocket_by_id

View the service configuration spec to see that it registers the exact tool definitions, input schemas, and behavior annotations for each tool:

# describe mcp server tool specs
gcloud agent-registry services describe ${MCP_NAME} \
  --location=${REGION} \
  --project=${PROJ_ID} \
  --format="yaml(mcpServerSpec.content.tools)"

Register OAuth endpoint

To let the Gemini Enterprise DataConnector backend (acting as the OAuth client in the authorization flow) send POST requests to the Google OAuth 2.0 authorization server (https://oauth2.googleapis.com/token) through Agent Gateway, the token endpoint must be registered in Agent Registry so that IAP evaluates destination.is_registered == true.

Create endpoint service

# create registry service (endpoint)
gcloud agent-registry services create oauth2-googleapis \
  --project=${PROJ_ID} \
  --location=${REGION} \
  --display-name="oauth2.googleapis.com" \
  --description="google oauth2 endpoint" \
  --endpoint-spec-type=no-spec \
  --interfaces=protocolBinding=JSONRPC,url=https://oauth2.googleapis.com

Verify endpoint registration

# list registry regional endpoints
gcloud agent-registry endpoints list \
  --project=${PROJ_ID} \
  --location=${REGION} \
  --flatten="interfaces[]" \
  --format="table(
    displayName,
    name.basename():label=ENDPOINT_ID,
    interfaces.url:label=URL
  )"

Expected output:

DISPLAY_NAME           ENDPOINT_ID                                         URL
oauth2.googleapis.com  agentregistry-00000000-0000-0000-0012-3456789abcde  https://oauth2.googleapis.com

IAM access policy

Because the IAM access policy defines an allow policy for any registered service requested by the Gemini Enterprise app core_assistant agent identity, no additional IAM policy bindings are required for OAuth token exchange.

  - conditions:
      iap.googleapis.com:
        expression: destination.is_registered == true
    description: allow ge assistant to any registered service
    effect: ALLOW

This concludes the Agent Registry portion... next on to the Gemini Enterprise section.

8. Gemini Enterprise

In this section, you configure the identity provider, trial license, and Google Identity OAuth 2.0 client credentials, create a Gemini Enterprise app (${GE_APP_ID}), bind outbound egress to your Agent Gateway (${AGW_URI}), and connect the registered sprockets MCP server as an OAuth 2.0 custom MCP data store.

Create Gemini Enterprise app

The following method uses the discoveryengine.googleapis.com API to create the Gemini Enterprise app resources and configuration. To configure using the Google Cloud Console UI, see Create an app for instructions.

# create engine (ge app)
curl -s -X POST "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines?engineId=${GE_APP_INIT}" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "displayName": "${GE_APP_DISPLAY_NAME}",
  "dataStoreIds": [],
  "solutionType": "SOLUTION_TYPE_SEARCH",
  "industryVertical": "GENERIC",
  "appType": "APP_TYPE_INTRANET",
  "searchEngineConfig": {
    "searchTier": "SEARCH_TIER_ENTERPRISE",
    "searchAddOns": [
      "SEARCH_ADD_ON_LLM"
    ]
  },
  "commonConfig": {
    "companyName": "${GE_APP_ORG_NAME}"
  },
  "observabilityConfig": {
    "observabilityEnabled": true,
    "sensitiveLoggingEnabled": true
  }
}
EOF

Verify app creation

# fetch engine (ge app) id
export GE_APP_ID=$(curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" -H "X-Goog-User-Project: ${PROJ_ID}" \
  | jq -r --arg name "${GE_APP_DISPLAY_NAME}" '.engines[] | select(.displayName==$name) | .name | split("/") | last')

echo "engine (ge app) id: ${GE_APP_ID}"

View the engine details to see the created configuration:

# get engine (ge app) details
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}"

Enable identity provider

Enable Google Identity as the identity provider for end user authentication on your Gemini Enterprise app.

The following method uses the discoveryengine.googleapis.com API to configure the Gemini Enterprise app identity provider. To configure using the Google Cloud Console UI, see Configure identity provider for instructions.

# set identity provider
curl -s -X PATCH "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/aclConfig" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "idpConfig": {
    "idpType": "GSUITE"
  }
}
EOF

Verify identity provider

# show identity provider
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/aclConfig" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}"

The output "idpType": "GSUITE" corresponds to the Google Identity provider.

(Optional) Enable Gemini Enterprise trial license

If you are using a project that has Gemini Enterprise licenses assigned, you can skip this step. If you are using a new project without a license, continue and follow these steps.

Create a license configuration resource to entitle Gemini Enterprise user seats for 30 days. This will set the default license to the new trial, so any user logging in is automatically granted a seat:

# configure free trial subscription
curl -s -X POST "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/licenseConfigs?licenseConfigId=free_trial_gemini" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "subscriptionTier": "SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT",
  "freeTrial": true
}
EOF

Verify license was applied

# show license config
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/licenseConfigs/free_trial_gemini" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}"

Check for "subscriptionTerm": "SUBSCRIPTION_TERM_ONE_MONTH" and "freeTrial": true.

# verify auto-registration enabled on default user store
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/userStores/default_user_store" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}"

Check for ../free_trial_gemini" and "enableLicenseAutoRegister": true.

To allow Gemini Enterprise (the OAuth 2.0 Client) to obtain a user-delegated Google OAuth 2.0 Bearer token and present it to the mcp-sprocketsResource Server on your behalf, configure the Google Auth Platform OAuth consent screen and create a Web application OAuth 2.0 Client ID in your project.

Print the direct Google Cloud Console URL for the Google Auth Platform Overview in your project:

# print google auth platform consent screen url
echo "https://console.cloud.google.com/auth/overview?project=${PROJ_ID}"
  1. Open the URL in your browser and click Get started (if not already configured for your project).
  2. Under App Information, set App name to mcp-sprockets-oauth and select your email address (${USER_IDENTITY}) as the User support email, then click Next.
  3. Under Audience, select Internal (recommended if your account belongs to a Google Workspace organization) or External (if using a personal account; if you select External, add ${USER_IDENTITY} under **Audience Test users**), then click Next.
  4. Enter your email address under Contact Information, accept the policy agreement, and click Create.

2. Create the Web application OAuth 2.0 Client ID

Print the direct Google Cloud Console URL for the Clients page:

# print google auth platform clients url
echo "https://console.cloud.google.com/auth/clients?project=${PROJ_ID}"
  1. Open the URL and click + Create client.
  2. Select Web application as the Application type and set Name to ge-mcp-sprockets-client.
  3. Select the checkbox for This client will be used by an AI-powered agent to designate the OAuth client for AI agent workflows acting on behalf of users.
  4. Under Authorized redirect URIs, click + Add URI and add the Gemini Enterprise OAuth callback URI:
    • https://vertexaisearch.cloud.google.com/oauth-redirect
  5. Click Create, copy the generated Client ID and Client secret, and export them in your shell session:

Set the OAuth 2.0 client ID and client secret generated from the previous step:

# set oauth 2.0 client id (paste in id)
export OAUTH_CLIENT_ID="SET_YOUR_OAUTH_CLIENT_ID_HERE"
# set oauth 2.0 client secret (paste in secret)
export OAUTH_CLIENT_SECRET="SET_YOUR_OAUTH_CLIENT_SECRET_HERE"

Check the client ID is set:

# print oauth 2.0 client id
echo "oauth client id: ${OAUTH_CLIENT_ID}"

Bind to Agent Gateway

Routing outbound traffic from Gemini Enterprise through Agent Gateway establishes a centralized enforcement and static IP routing boundary for all AI agent tool invocations:

  • Centralized policy enforcement: Agent Gateway acts as an inline proxy that evaluates outbound tool requests against authorization policies before traffic leaves the agent environment.
  • Deterministic VPC & Cloud NAT egress: Binding Gemini Enterprise to an Agent Gateway configured with vpcEgress: ALL_TRAFFIC ensures tool calls route through the PSC network attachment and exit using your reserved Cloud NAT static IP.
  • Unified auditability: Provides centralized request logging and audit trails across all connected MCP servers.

Patch the engine agentGatewaySetting to enable:

# bind engine (ge app) to agent gateway
curl -s -X PATCH "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}?updateMask=agentGatewaySetting.defaultEgressAgentGateway.name" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "agentGatewaySetting": {
    "defaultEgressAgentGateway": {
      "name": "${AGW_URI}"
    }
  }
}
EOF

Verify Agent Gateway binding

Retrieve the app configuration to confirm the agentGatewaySetting binding:

# verify engine (ge app) agent gateway configuration
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  | jq '{name: .name, displayName: .displayName, agentGatewaySetting: .agentGatewaySetting}'

Example output:

{
  "name": "projects/[PROJ_NO]/locations/global/collections/default_collection/engines/app-foo-global_[EPOCH]",
  "displayName": "foo global app",
  "agentGatewaySetting": {
    "defaultEgressAgentGateway": {
      "name": "projects/[PROJ_ID]/locations/us-central1/agentGateways/agw-foo-us-central1-ata"
    }
  }
}

Create custom MCP server data store

In this section you will connect the mcp-sprockets server to Gemini Enterprise by creating an OAuth 2.0 custom MCP data store configured with Google Identity (https://accounts.google.com/o/oauth2/v2/auth and https://oauth2.googleapis.com/token).

Using the Discovery Engine API, this is a two-step process:

  1. Create (:setUpDataConnector): Creates a dedicated Collection resource (${MCP_NAME}-collection-%timestamp), attaches the DataConnector (custom_mcp) with auth_type: "OAUTH", and provisions its backing DataStore (..._mcp_data).
  2. Activate (PATCH .../dataConnector?updateMask=actionConfig): Activates the connector action runtime (actionState: "ACTIVE") using the Agent Registry tool spec, Google Identity OAuth 2.0 parameters, and binds the DataStore (dataStoreIds) to your Gemini Enterprise Engine.
# fetch mcp server agent registry resource name
export MCP_REGISTRY_URI=$(gcloud agent-registry mcp-servers list \
  --location=${REGION} \
  --project=${PROJ_ID} \
  --filter="displayName:${MCP_DOMAIN}" \
  --format="value(name)")

echo "mcp registry name: ${MCP_REGISTRY_URI}"
echo "mcp url: ${MCP_URL}"

Create data connector

# create custom mcp data connector with google identity oauth 2.0 and link to engine
curl -s -X POST "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/${GE_LOCATION}:setUpDataConnector" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "collectionId": "${MCP_NAME}-collection-$(date +%s)",
  "collectionDisplayName": "${MCP_NAME}-collection",
  "dataConnector": {
    "dataSource": "custom_mcp",
    "dataSourceVersion": 1,
    "params": {
      "oauth_access_token": "unused"
    },
    "refreshInterval": "86400s",
    "entities": [
      {
        "entityName": "mcp_data"
      }
    ],
    "connectorModes": [
      "FEDERATED"
    ],
    "actionConfig": {
      "isActionConfigured": true,
      "createBapConnection": true,
      "actionParams": {
        "auth_type": "OAUTH",
        "instance_uri": "${MCP_URL}",
        "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
        "auth_uri_params": "&access_type=offline&prompt=consent",
        "token_uri": "https://oauth2.googleapis.com/token",
        "client_id": "${OAUTH_CLIENT_ID}",
        "client_secret": "${OAUTH_CLIENT_SECRET}",
        "scopes": "openid email profile",
        "client_secret_basic_override": true,
        "mcp_server_source": "REGISTRY_MCP",
        "registry_mcp_server_name": "${MCP_REGISTRY_URI}",
        "mcp_agent_instructions": "MANDATORY SPROCKETS INVENTORY & CATALOG AGENT: Always invoke get_sprockets or get_sprocket_by_id for any question about sprockets, inventory catalog listings, sprocket IDs (SPK-101 through SPK-105), ANSI models (ANSI-40, ANSI-60, ANSI-80), tooth counts, pitch, material, stock levels, unit prices, authenticated_user, or egress_source_ip verification.",
        "use_agent_gateway_egress": true,
        "agent_gateway_engine": "projects/${PROJ_ID}/locations/global/collections/default_collection/engines/${GE_APP_ID}"
      }
    }
  }
}
EOF

Verify data connector creation

# fetch collection id
export GE_COLLECTION_ID=$(curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  | jq -r --arg dname "${MCP_NAME}-collection" '.collections[] | select(.displayName == $dname) | .name | split("/") | last' | head -n 1)

echo "ge collection id: ${GE_COLLECTION_ID}"

Check the "registry_mcp_server_name" field populates with the Agent Registry UUID for the MCP server and "auth_type" is OAUTH:

# show data connector details
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/${GE_COLLECTION_ID}/dataConnector" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  | jq '{name, state, actionState, connectorModes, bapConfig, auth_type: .actionConfig.actionParams.auth_type, registry_mcp_server_name: .actionConfig.actionParams.registry_mcp_server_name}'

View the MCP server registry entry in the Google Cloud Console UI:

echo "mcp server registry page url: https://console.cloud.google.com/agent-platform/agent-registry/mcp-servers/${REGION}/${MCP_REGISTRY_URI##*/}/overview?project=${PROJ_ID}"

Activate data connector

# activate and bind data connector with google identity oauth 2.0
curl -s -X PATCH "https://discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/global/collections/${GE_COLLECTION_ID}/dataConnector?updateMask=actionConfig" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "name": "projects/${PROJ_ID}/locations/global/collections/${GE_COLLECTION_ID}/dataConnector",
  "actionConfig": {
    "isActionConfigured": true,
    "createBapConnection": true,
    "actionParams": {
      "auth_type": "OAUTH",
      "instance_uri": "${MCP_URL}",
      "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
      "auth_uri_params": "&access_type=offline&prompt=consent",
      "token_uri": "https://oauth2.googleapis.com/token",
      "client_id": "${OAUTH_CLIENT_ID}",
      "client_secret": "${OAUTH_CLIENT_SECRET}",
      "scopes": "openid email profile",
      "client_secret_basic_override": true,
      "mcp_server_source": "REGISTRY_MCP",
      "registry_mcp_server_name": "${MCP_REGISTRY_URI}",
      "mcp_agent_instructions": "MANDATORY SPROCKETS INVENTORY & CATALOG AGENT: Always invoke get_sprockets or get_sprocket_by_id for any question about sprockets, inventory catalog listings, sprocket IDs (SPK-101 through SPK-105), ANSI models (ANSI-40, ANSI-60, ANSI-80), tooth counts, pitch, material, stock levels, unit prices, authenticated_user, or egress_source_ip verification.",
      "use_agent_gateway_egress": true,
      "agent_gateway_engine": "projects/${PROJ_ID}/locations/global/collections/default_collection/engines/${GE_APP_ID}"
    }
  }
}
EOF

Verify custom MCP server linkages

# show engine (ge app) details
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  | jq '{name: .name, dataStoreIds: .dataStoreIds, agentGatewaySetting: .agentGatewaySetting}'

Check for the linked data store "dataStoreIds": ["mcp-sprockets-collection-[EPOCH]_mcp_data"].

# show collection details
curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  | jq --arg app "${GE_APP_ID}" '.collections[] | select(.dataConnector.actionConfig.actionParams.agent_gateway_engine // "" | endswith($app)) | .dataConnector | {name: .name, state: .state, actionState: .actionState, connectorModes: .connectorModes, auth_type: .actionConfig.actionParams.auth_type}'

Check for "state": "ACTIVE" and "auth_type": "OAUTH".

Tool actions

When you inspect the mcp-sprockets-collection data store in the Gemini Enterprise dashboard, you will notice that the Actions tab is not used and the ↻ Reload custom actions button is disabled. This is expected behavior.

View the data store details page in the Google Cloud Console UI:

echo "data store details page url: https://console.cloud.google.com/gemini-enterprise/locations/${GE_LOCATION}/collections/${GE_COLLECTION_ID}/connector/details?project=${PROJ_ID}"

Depending on how you connect a custom MCP server to Gemini Enterprise, tool discovery and governance are handled in one of two ways:

  • Direct Custom MCP (BYO_MCP workflow): When you configure a custom MCP server directly inside Gemini Enterprise without Agent Registry, the data store itself manages the tool catalog (connectorModes: ["FEDERATED", "ACTIONS"]). You must open the Actions tab, click ↻ Reload custom actions to fetch the tools/list schema, and manually toggle individual tools (add and subtract) on or off in the UI.
  • Agent Registry Import (REGISTRY_MCP workflow used in this codelab): When you import an MCP server from Agent Registry, Agent Registry serves as the authoritative source of truth for the MCP endpoint, its interface metadata, and its tool catalog (connectorModes: ["FEDERATED"]). Gemini Enterprise automatically enables the registered MCP tools at runtime through the engine Agent Gateway setting without requiring you to manually reload or toggle actions in the data store UI.

This concludes the Gemini Enterprise portion... next on to the Validate section.

9. Validate

In this section, you complete the one-time Google OAuth 2.0 user consent flow, invoke MCP tools on the external mcp-sprockets server from Gemini Enterprise, and verify that the external MCP server validated both your reserved Cloud NAT static IP (egress_source_ip) and your Google OAuth 2.0 identity (authenticated_user).

User access

Construct the URL for the Gemini Enterprise web app:

# fetch app user url
export GE_WIDGET_ID=$(curl -s "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}/widgetConfigs/default_search_widget_config" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  -H "Content-Type: application/json" \
  | jq -r '.configId')

export GE_APP_USER_URL="https://vertexaisearch.cloud.google.com/home/cid/${GE_WIDGET_ID}"

echo "app user url: ${GE_APP_USER_URL}"

Before querying the agent, you must complete a one-time OAuth 2.0 consent flow that grants Gemini Enterprise permission to obtain an access token for your Google identity (${USER_IDENTITY}). This enables Gemini Enterprise to authenticate outbound tool calls through Agent Gateway so the external mcp-sprockets server can verify your identity on every request.

  1. Open ${GE_APP_USER_URL} in your browser and click Get started.
  2. In the chat box, click the Connectors icon (the puzzle piece icon) and click Authorize next to mcp-sprockets-collection.
  3. A new window appears prompting you to Choose an account to continue to mcp-sprockets-oauth (the OAuth consent screen app name you configured). There select your user account (${USER_IDENTITY}).
  4. On the Google OAuth consent screen, review the requested permissions that allow Google (the Identity Provider) to share your user identity (openid, email, profile) with mcp-sprockets-oauth, then click Continue.
  5. Click the Connectors icon again and verify that the toggle for mcp-sprockets-collection is enabled, confirming that authorization succeeded.

Test agent queries

Assistant chat UI tests

Submit the following test query in chat:

look up sprocket SPK-105 and confirm the model, material, unit_price, authenticated_user, and egress_source_ip

Try a second query in chat:

list all sprockets in the inventory catalog and report the authenticated_user and egress_source_ip

Verify that the assistant returns the correct answers and displays an interactive action citation badge.

For example, "Sprocket Lookup SPK-105 (5s)MCP tool iconAgent Gatewaycircle check icon" beneath each response, confirms the tool was executed.

See that the assistant returns the sprocket specifications along with "authenticated_user" matching ${USER_IDENTITY} and "egress_source_ip" matching your reserved ${NAT_EXT_IP}.

Assistant streamAssist API tests

Once your user account has authorized the OAuth 2.0 connector, you can also invoke the default_assistant:streamAssist API directly from the CLI and verify that the external mcp-sprockets server returns both your Cloud NAT static IP (${NAT_EXT_IP}) and your verified Google OAuth 2.0 identity (${USER_IDENTITY}):

Submit the following test query:

# query engine (ge app) streamAssist
curl -s -X POST "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}/assistants/default_assistant:streamAssist" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r '.[].answer.replies[]?.groundedContent?.content?.text // empty'
{
  "query": {
    "text": "look up sprocket SPK-103 and confirm the model, material, unit_price, authenticated_user, and egress_source_ip"
  },
  "toolsSpec": {
    "vertexAiSearchSpec": {
      "dataStoreSpecs": [
        {
          "dataStore": "projects/${PROJ_NO}/locations/${GE_LOCATION}/collections/default_collection/dataStores/${GE_COLLECTION_ID}_mcp_data"
        }
      ]
    }
  }
}
EOF

Example output:

Here are the details for sprocket SPK-103 from the inventory catalog:
- Model: ANSI-60-16T
- Material: 304 Stainless Steel
- Unit Price: $42.00
- Authenticated User (Google OAuth 2.0): user@example.com
- Egress Source IP (Cloud NAT): 203.0.113.10

Try a another query:

# query engine (ge app) streamAssist
curl -s -X POST "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}/assistants/default_assistant:streamAssist" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r '.[].answer.replies[]?.groundedContent?.content?.text // empty'
{
  "query": {
    "text": "find a sprocket model i can get for under \$20 and report the details, authenticated_user, and egress_source_ip"
  },
  "toolsSpec": {
    "vertexAiSearchSpec": {
      "dataStoreSpecs": [
        {
          "dataStore": "projects/${PROJ_NO}/locations/${GE_LOCATION}/collections/default_collection/dataStores/${GE_COLLECTION_ID}_mcp_data"
        }
      ]
    }
  }
}
EOF

Inspect logs in Cloud Logging

Verify that Gemini Enterprise routed the tool calls through Agent Gateway, the PSC network attachment, and Cloud NAT by inspecting the logs in Cloud Logging.

Verify Agent Gateway logs

Confirm that Agent Gateway intercepted the request, resolved the target in Agent Registry, evaluated the authorization policy, and forwarded the tool call:

# show agent gateway logs
gcloud logging read \
  "resource.type=networkservices.googleapis.com/Gateway
   AND resource.labels.location=${REGION}" \
  --project=${PROJ_ID} \
  --limit=8 \
  --format="table(
    timestamp.date('', tz=LOCAL):label=TIMESTAMP,
    httpRequest.status:label=STATUS,
    httpRequest.serverIp:label=SERVER_IP,
    jsonPayload.agentGatewayInfo.mcpInfo.method:label=MCP_METHOD,
    jsonPayload.agentGatewayInfo.mcpInfo.parameter:label=TOOL,
    jsonPayload.authzPolicyInfo.result:label=AUTHZ,
    jsonPayload.agentGatewayInfo.agentRegistryResource.basename():label=REGISTRY_MCP
  )"

Example output:

TIMESTAMP                STATUS  SERVER_IP         MCP_METHOD                 TOOL                AUTHZ    REGISTRY_MCP
YYYY-MM-DDTHH:MM:SS.ZZZ  200     203.0.113.20:443  tools/call                 get_sprocket_by_id  ALLOWED  agentregistry-00000000-0000-0000-5c39-1f6e542bb2b8
YYYY-MM-DDTHH:MM:SS.ZZZ  200     203.0.113.20:443  tools/list                                     ALLOWED  agentregistry-00000000-0000-0000-5c39-1f6e542bb2b8
YYYY-MM-DDTHH:MM:SS.ZZZ  202     203.0.113.20:443  notifications/initialized                      ALLOWED  agentregistry-00000000-0000-0000-5c39-1f6e542bb2b8
YYYY-MM-DDTHH:MM:SS.ZZZ  200     203.0.113.20:443  initialize                                     ALLOWED  agentregistry-00000000-0000-0000-5c39-1f6e542bb2b8

Verify that the output contains:

  • STATUS: 200 (successful execution) and 202 (notifications/initialized handshake).
  • SERVER_IP: External MCP server IP and port (${MCP_EXT_IP}:443).
  • MCP_METHOD & TOOL: The MCP protocol sequence (initialize, notifications/initialized, tools/list, and tools/call with get_sprocket_by_id).
  • AUTHZ: ALLOWED (IAP authorization permitted egress).
  • REGISTRY_MCP: Resolved Agent Registry resource ID (agentregistry-...).

Verify Cloud NAT logs

Confirm that Cloud NAT translated the outbound connection from the Agent Gateway PSC network attachment IP (10.10.10.2) to your reserved static external IPv4 address (${NAT_EXT_IP}) when connecting to ${MCP_EXT_IP}:443:

# show cloud nat logs
gcloud logging read \
  "resource.type=nat_gateway
   AND resource.labels.gateway_name=nat-${SLUG}-${REGION}
   AND jsonPayload.connection.dest_ip=${MCP_EXT_IP}" \
  --project=${PROJ_ID} \
  --limit=8 \
  --format="table(
    timestamp.date('', tz=LOCAL):label=TIMESTAMP,
    jsonPayload.allocation_status:label=ALLOCATION_STATUS,
    jsonPayload.connection.src_ip:label=SRC_IP,
    jsonPayload.connection.nat_ip:label=NAT_IP,
    jsonPayload.connection.dest_ip:label=DEST_IP,
    jsonPayload.connection.dest_port:label=PORT
  )"

Example output:

TIMESTAMP                ALLOCATION_STATUS  SRC_IP      NAT_IP        DEST_IP       PORT
YYYY-MM-DDTHH:MM:SS.ZZZ  OK                 10.10.10.2  203.0.113.10  203.0.113.20  443

Verify firewall logs

Confirm that the internal firewall policy (fw-policy-${SLUG}-internal) permitted egress traffic from Agent Gateway to the internet destination:

# show firewall policy logs (internal)
gcloud logging read \
  "logName:compute.googleapis.com%2Ffirewall
   AND jsonPayload.rule_details.reference:fw-policy-${SLUG}-internal" \
  --project=${PROJ_ID} \
  --limit=8 \
  --format="table(
    timestamp.date('', tz=LOCAL):label=TIMESTAMP,
    jsonPayload.connection.src_ip:label=SRC_IP,
    jsonPayload.connection.dest_ip:label=DEST_IP,
    jsonPayload.connection.dest_port:label=PORT,
    jsonPayload.vpc.vpc_name:label=VPC_NETWORK,
    jsonPayload.rule_details.reference.basename():label=POLICY,
    jsonPayload.rule_details.priority:label=RULE,
    jsonPayload.rule_details.direction:label=DIRECTION,
    jsonPayload.disposition:label=DISPOSITION
  )"

Confirm that the external firewall policy (fw-policy-${SLUG}-external) permitted ingress traffic to the MCP server from the internal network public NAT IP address:

# show firewall policy logs (external)
gcloud logging read \
  "logName:compute.googleapis.com%2Ffirewall
   AND jsonPayload.rule_details.reference:fw-policy-${SLUG}-external" \
  --project=${PROJ_ID} \
  --limit=8 \
  --format="table(
    timestamp.date('', tz=LOCAL):label=TIMESTAMP,
    jsonPayload.connection.src_ip:label=SRC_IP,
    jsonPayload.connection.dest_ip:label=DEST_IP,
    jsonPayload.connection.dest_port:label=PORT,
    jsonPayload.vpc.vpc_name:label=VPC_NETWORK,
    jsonPayload.rule_details.reference.basename():label=POLICY,
    jsonPayload.rule_details.priority:label=RULE,
    jsonPayload.rule_details.direction:label=DIRECTION,
    jsonPayload.disposition:label=DISPOSITION
  )"

This concludes the validate portion... next on to the Cleanup section.

10. Cleanup

Follow these steps to delete the resources and configurations created in this lab.

Remove Gemini Enterprise components

# delete gemini enterprise engine (app)
curl -s -X DELETE "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/default_collection/engines/${GE_APP_ID}" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}"

# delete custom mcp collection, data connector, and backing data store
curl -s -X DELETE "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1alpha/projects/${PROJ_ID}/locations/${GE_LOCATION}/collections/${GE_COLLECTION_ID}" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}"

# reset identity provider configuration
curl -s -X PATCH "https://${GE_LOCATION}-discoveryengine.googleapis.com/v1/projects/${PROJ_ID}/locations/${GE_LOCATION}/aclConfig" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: ${PROJ_ID}" \
  -H "Content-Type: application/json" \
  -d '{"idpConfig":{"idpType":"IDP_TYPE_UNSPECIFIED"}}'

To remove the OAuth 2.0 client credentials (ge-mcp-sprockets-client), open the Google Auth Platform Clients page and click the Delete (trash can) icon next to ge-mcp-sprockets-client:

# print google auth platform clients url
echo "https://console.cloud.google.com/auth/clients?project=${PROJ_ID}"

Remove MCP server and Agent Registry components

# delete external mcp server vm
gcloud -q compute instances delete vm-${MCP_NAME} --zone=${ZONE}

# delete agent registry mcp service
gcloud -q agent-registry services delete ${MCP_NAME} --location=${REGION}

# delete agent registry oauth2 endpoint
gcloud -q agent-registry services delete oauth2-googleapis --location=${REGION}

Remove Agent Gateway components

# delete gateway authz policy and iap service extension
gcloud -q network-security authz-policies delete ${AGW_NAME}-authz-policy-iap \
  --location=${REGION}

gcloud -q service-extensions authz-extensions delete ${AGW_NAME}-svc-ext-authz-iap \
  --location=${REGION}

# delete agent gateway and connectivity template
gcloud -q network-services agent-gateways delete ${AGW_NAME} \
  --location=${REGION}

gcloud -q network-services agent-connectivity-templates delete act-${AGW_NAME} \
  --location=${REGION}
# delete iam unified access policy binding and access policy
gcloud -q iam policy-bindings delete uap-bind-${SLUG} \
  --location=global \
  --project=${PROJ_ID}

gcloud -q iam access-policies delete uap-policy-${SLUG} \
  --location=global \
  --project=${PROJ_ID}

Remove firewall components

# delete network firewall policy associations
gcloud -q compute network-firewall-policies associations delete \
  --name=fw-policy-bind-${SLUG}-internal \
  --firewall-policy=fw-policy-${SLUG}-internal \
  --global-firewall-policy

gcloud -q compute network-firewall-policies associations delete \
  --name=fw-policy-bind-${SLUG}-external \
  --firewall-policy=fw-policy-${SLUG}-external \
  --global-firewall-policy

# delete custom firewall policy rules
gcloud -q compute network-firewall-policies rules delete 1001 \
  --firewall-policy=fw-policy-${SLUG}-internal \
  --global-firewall-policy

gcloud -q compute network-firewall-policies rules delete 2003 \
  --firewall-policy=fw-policy-${SLUG}-external \
  --global-firewall-policy

# delete network firewall policies
gcloud -q compute network-firewall-policies delete fw-policy-${SLUG}-internal \
  --global

gcloud -q compute network-firewall-policies delete fw-policy-${SLUG}-external \
  --global

Remove network components

# delete cloud nat gateway and cloud router
gcloud -q compute routers nats delete nat-${SLUG}-${REGION} \
  --router=cr-nat-${SLUG}-${REGION} \
  --region=${REGION}

gcloud -q compute routers delete cr-nat-${SLUG}-${REGION} \
  --region=${REGION}

# delete psc network attachment
gcloud -q compute network-attachments delete psc-na-${REGION}-agw \
  --region=${REGION}

# delete subnets
gcloud -q compute networks subnets delete subnet-${REGION}-agw \
  --region=${REGION}

gcloud -q compute networks subnets delete subnet-${REGION}-mcp \
  --region=${REGION}

# delete reserved static external ip addresses
gcloud -q compute addresses delete ip-${SLUG}-nat ip-${SLUG}-mcp \
  --region=${REGION}

# delete internal and external vpc networks
gcloud -q compute networks delete \
  vnet-${SLUG}-internal \
  vnet-${SLUG}-external

Remove organization policy overrides and local files

# delete project-level organization policy overrides
for policy in \
  discoveryengine.managed.disableCustomMcpServerConnector \
  discoveryengine.managed.allowedEgressFqdns \
  discoveryengine.managed.allowedDataSources \
  iam.managed.disableAccessPolicyBinding \
  compute.vmExternalIpAccess; do
  gcloud -q org-policies delete ${policy} --project=${PROJ_ID} 2>/dev/null || true
done

# remove local project files
rm -rf cfg

This concludes the cleanup work... next on to the Conclusion!

11. Conclusion

Congratulations! You built an end-to-end architecture enabling a Gemini Enterprise app to securely discover and invoke tools on an external IP-allowlisted and OAuth 2.0-protected custom MCP server:

  • Static IP VPC egress (ALL_TRAFFIC & Cloud NAT): Routed all outbound Agent Gateway traffic through a PSC network attachment (10.10.10.2) and translated internet-bound requests to a reserved static external IPv4 address (${NAT_EXT_IP}) using Cloud NAT.
  • OAuth 2.0 MCP Resource Server & Agent Registry: Deployed an external streamable HTTP FastMCP server (mcp-sprockets) protected by a strict source IP firewall allowlist (${NAT_EXT_IP}/32) and Google OAuth 2.0 tokeninfo verification, and registered its endpoint and tool schema (get_sprockets and get_sprocket_by_id) in Agent Registry.
  • Gemini Enterprise OAuth 2.0 integration & governance: Provisioned a Gemini Enterprise app, bound outbound tool traffic to Agent Gateway, attached the registered MCP server as an OAuth 2.0 REGISTRY_MCP data connector, and verified both egress_source_ip and authenticated_user alongside Cloud Logging telemetry.

cosmopup

Cosmopup thinks Codelabs are killer diller!

What is next?

Feel free to offer comments, questions, or corrections by using this feedback form.

Thank you!