---
title: "Authenticate an MCP Server with Google Cloud IAM"
description: "Use Google OIDC ID tokens so agents on Cloud Run can call your MCP server with no shared secret. Verified code for minting, sending, and validating the token."
canonicalUrl: "https://zuplo.com/learn/mcp/authentication/google-iam"
pageType: "mcp-auth-guide"
method: "cloud-iam"
platform: "gcp"
lastVerified: "2026-07-24"
specVersion: "2026-07-28"
---

# Authenticate an MCP Server with Google Cloud IAM

> The agent gets a short-lived identity token from Google and the MCP server verifies it against Google's public keys, so there is no key to store or rotate.

**Google Cloud IAM · Google Cloud**

## The exchange

The agent asks Google for an ID token whose audience is the MCP server URL, then presents it as a bearer token. The server verifies the signature against Google's JWKS, checks the audience and issuer, and reads the calling service account from the email claim. Google is never contacted by the server.

**Participants:** Your agent (runtime service account), Google (metadata server), MCP server (verifies via JWKS)

1. **Your agent → Google** — `GET …/identity?audience=…` — `Metadata-Flavor: Google`
2. **Google → Your agent** — `id_token` — `aud = https://mcp.example.com/mcp` _(response; carries the credential)_
3. **Your agent → MCP server** — `POST /mcp` — `Authorization: Bearer <id-token>` _(carries the credential)_
4. **MCP server** (internal step) — Verify the signature against Google's JWKS
5. **MCP server** (internal step) — Check aud and iss, then read the caller — `email = agent@project.iam.gserviceaccount.com`
6. **MCP server → Your agent** — `200 OK` — `tool result` _(response)_

Notes:

- **Steps 1–2** — Minted from the runtime service account's ambient credentials, so nothing appears in configuration and there is no secret to rotate.
- **Steps 4–5** — Verification is offline against cached JWKS. Google is never called by the server, and the audience check is what stops a token minted for another service from being replayed here.

## How it works

The agent requests a Google OIDC ID token whose audience is the MCP server's URL, then sends it as `Authorization: Bearer <id-token>`. The server verifies the signature against Google's JWKS, checks the `aud` and `iss` claims, and reads the calling service account's email from the token. Tokens are minted from the runtime service account's ambient credentials, so no secret appears in configuration.

_MCP Specification 2026-07-28._

### At a glance

- **Best for** — Agents that hold Google credentials, calling a server that can verify a Google token
- **MCP spec** — Platform identity rather than MCP authorization; the spec does not cover it
- **Works with** — Your own workloads only; no MCP client can mint these tokens
- **Effort** — A day, mostly IAM bindings

## Connect your agent

Each sample connects to the server, sends the credential, and exposes the tools to the model.

### TypeScript

#### Claude Agent SDK

```typescript title="agent.ts"
import { GoogleAuth } from "google-auth-library";
import { query } from "@anthropic-ai/claude-agent-sdk";

const MCP_URL = "https://mcp-server-abc123-uc.a.run.app";
const idClient = await new GoogleAuth().getIdTokenClient(MCP_URL);

// Headers are read when the run starts, so mint them per run rather
// than hoisting the bearer string into a module constant.
for await (const message of query({
  prompt: "List your tools.",
  options: {
    mcpServers: {
      platform: {
        type: "http",
        url: `${MCP_URL}/mcp`,
        headers: Object.fromEntries(
          new Headers(await idClient.getRequestHeaders()),
        ),
      },
    },
  },
})) {
  console.log(message);
}
```

#### MCP TypeScript SDK

```typescript title="agent.ts"
import { GoogleAuth } from "google-auth-library";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const MCP_URL = "https://mcp-server-abc123-uc.a.run.app";
const auth = new GoogleAuth();

// Cache the client, not the token string: it re-mints automatically
// as the ~1h token nears expiry. Caching the bearer string is why
// warm instances start failing after an hour.
const idClient = await auth.getIdTokenClient(MCP_URL);
const headers = await idClient.getRequestHeaders();

const transport = new StreamableHTTPClientTransport(
  new URL(`${MCP_URL}/mcp`),
  { requestInit: { headers: Object.fromEntries(new Headers(headers)) } },
);
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
```

#### Vercel AI SDK

```typescript title="agent.ts"
import { GoogleAuth } from "google-auth-library";
import { createMCPClient } from "@ai-sdk/mcp";

const MCP_URL = "https://mcp-server-abc123-uc.a.run.app";
const idClient = await new GoogleAuth().getIdTokenClient(MCP_URL);

// Ask for headers on every connect so an expiring token is re-minted.
const mcp = await createMCPClient({
  transport: {
    type: "http",
    url: `${MCP_URL}/mcp`,
    headers: Object.fromEntries(
      new Headers(await idClient.getRequestHeaders()),
    ),
  },
});
const tools = await mcp.tools();
```

#### Mastra

```typescript title="agent.ts"
import { GoogleAuth } from "google-auth-library";
import { MCPClient } from "@mastra/mcp";

const MCP_URL = "https://mcp-server-abc123-uc.a.run.app";
const idClient = await new GoogleAuth().getIdTokenClient(MCP_URL);

const mcp = new MCPClient({
  servers: {
    platform: {
      url: new URL(`${MCP_URL}/mcp`),
      requestInit: {
        headers: Object.fromEntries(
          new Headers(await idClient.getRequestHeaders()),
        ),
      },
    },
  },
});
const tools = await mcp.listTools();
```

### Python

#### Claude Agent SDK

```python title="agent.py"
import asyncio
import google.auth.transport.requests
import google.oauth2.id_token
from claude_agent_sdk import query, ClaudeAgentOptions

MCP_URL = "https://mcp-server-abc123-uc.a.run.app"

async def main():
    # Audience must be the server's base URL, not the /mcp path.
    request = google.auth.transport.requests.Request()
    token = google.oauth2.id_token.fetch_id_token(request, MCP_URL)

    options = ClaudeAgentOptions(mcp_servers={
        "platform": {
            "type": "http",
            "url": f"{MCP_URL}/mcp",
            "headers": {"Authorization": f"Bearer {token}"},
        }
    })
    async for message in query(prompt="List your tools.", options=options):
        print(message)

asyncio.run(main())
```

#### OpenAI Agents SDK

```python title="agent.py"
import asyncio
import google.auth.transport.requests
import google.oauth2.id_token
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp

MCP_URL = "https://mcp-server-abc123-uc.a.run.app"

async def main():
    # Audience must be the server's base URL, not the /mcp path.
    request = google.auth.transport.requests.Request()
    token = google.oauth2.id_token.fetch_id_token(request, MCP_URL)

    async with MCPServerStreamableHttp(
        name="Platform MCP",
        params={
            "url": f"{MCP_URL}/mcp",
            "headers": {"Authorization": f"Bearer {token}"},
        },
    ) as server:
        agent = Agent(name="Assistant", mcp_servers=[server])
        print((await Runner.run(agent, "List your tools.")).final_output)

asyncio.run(main())
```

#### FastMCP

```python title="agent.py"
import google.auth.transport.requests
import google.oauth2.id_token
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport

MCP_URL = "https://mcp-server-abc123-uc.a.run.app"
request = google.auth.transport.requests.Request()
token = google.oauth2.id_token.fetch_id_token(request, MCP_URL)

client = Client(StreamableHttpTransport(
    url=f"{MCP_URL}/mcp",
    headers={"Authorization": f"Bearer {token}"},
))

async def main():
    async with client:
        print(await client.list_tools())
```

## When to use something else

**Use this when:**

- The agent runs with Google credentials — usually on Google Cloud — so it can mint tokens with no stored secret. The server only has to verify them.
- You want no stored secret: tokens are short-lived and minted from ambient credentials.
- Tool calls need to be attributed to a specific workload rather than to a key.

**Use something else when:**

- A third party has to call the server, since only a caller holding Google credentials can mint these tokens.
- An interactive MCP client needs to connect. Claude, ChatGPT and Cursor cannot produce a Google ID token and need OAuth or a signing proxy.
- The call is made on behalf of a named person and the API behind the server enforces per-user permissions.

> This is platform identity rather than MCP authorization, so the specification has nothing to say about it. The trade-off is reach: it is the strongest option for your own workloads and unavailable to everything else, which is why servers that need both usually place a gateway in front to translate.

## What lands on you next

Cloud IAM is strong between your own services and unusable for anything outside them. The questions that follow are all about what happens when something outside them needs in.

- **A partner's agent needs two of these tools. They have no Google account.** — `mcp-oauth-inbound` — The inbound method is a policy, not server code, so the route can authenticate against the identity provider you already run instead of Google IAM. Exactly one such policy per project.
- **This service account's token works. Can it be replayed at our other MCP route?** — `RFC 8707 resource indicators` — No. A gateway-issued token is bound to one route's canonical resource URI, so a token minted for one MCP route is rejected at another.
- **Which service account called which tool, and how often?** — `capability_invocation` — Each tool call resolves to a caller and a capability with an outcome and a latency, rather than to a shared egress IP in an access log.
- **Can I forward the caller's ID token to the API behind the server?** — `mcp-token-exchange-inbound` — The spec forbids passthrough with a MUST NOT. Inbound auth headers are stripped and an independent upstream credential is minted per user or per gateway.

All of these attach to one MCP route's `policies.inbound` — the same policy engine on the way in and on the way out. See [Zuplo MCP Gateway](/mcp-gateway).

## FAQ

**Why an ID token rather than an access token?**

An ID token carries an audience claim that can be bound to one specific service, which is what prevents a token minted for another service from working. Access tokens address Google APIs, not your own endpoints.

**Should the audience be the server URL or the /mcp path?**

The service's base URL. Mint and verify the same value. Including the /mcp path in the audience is a common cause of audience-mismatch failures.

**Does the token need refreshing manually?**

Not if the ID token client is cached rather than the token string. The Google auth library re-mints as the roughly one-hour token approaches expiry. Caching the bearer string is what causes long-lived instances to start failing after an hour.

**Can Claude or ChatGPT connect to a server protected this way?**

No. No MCP client can mint a Google ID token, which is why Google's own guidance is to choose a different method when the client cannot supply the credential. Interactive clients need the spec OAuth flow or a local proxy that signs on their behalf.

**Is Cloud Run IAM sufficient by itself?**

It closes the ingress, which is most of the value, but it tells the application nothing. Verifying the token in the handler is what supplies the caller identity for authorization decisions and audit logs.

**Does this pattern work on Lambda or Kubernetes?**

The shape does, with different mechanics: AWS uses SigV4 request signing, and Kubernetes can validate projected service-account tokens against the cluster's OIDC issuer. In all three cases no MCP client can produce the credential, so the pattern stays internal.

## Keep reading

- **API key** — [API Key Authentication for MCP Servers](/learn/mcp/authentication/api-key) — Service-to-service calls where you control both the agent and the MCP server
- **AWS IAM (SigV4)** — [Authenticate an MCP Server with AWS IAM (SigV4)](/learn/mcp/authentication/aws-iam-sigv4) — Agents you write that run in AWS, calling a server behind Lambda, API Gateway, or AgentCore
- **Microsoft Entra ID** — [Authenticate an MCP Server with Microsoft Entra ID (Azure AD) Managed Identity](/learn/mcp/authentication/azure-managed-identity) — Agents and MCP servers that both run in Azure, in one Entra tenant
- **MCP Authentication** — [every authentication method](/learn/mcp/authentication)
