Building enterprise AI agents with ADK and the Gmail MCP Server

Authors: @Gary_Wang @Constance_Li

Early Access Notice: In order to get early access to certain features and managed endpoints of the Gmail MCP server (`https://gmailmcp.googleapis.com/mcp/v1\`), apply to join the Google Workspace Developer Preview Program.

Introduction

Have you ever tried building an autonomous AI agent to triage customer inboxes, summarize urgent email threads, or draft responses, only to spend days writing brittle REST API wrappers and wrestling with token refresh edge cases?

Connecting Large Language Models (LLMs) to enterprise communication hubs like Google Workspace and Gmail is one of the highest-value opportunities for generative AI automation. However, enterprise developers often face two fundamental roadblocks:

  1. Tool orchestration overhead: How do models discover and reliably call email actions without maintaining custom function schemas?

  2. Enterprise identity and delegation: How to securely authenticate interactive user-facing assistants versus headless 24/7 background worker bots across one or many mailboxes?

In this post, we explore how to build production-grade Gmail agents using the Google Agent Development Kit (ADK) and the managed Model Context Protocol (MCP) endpoint. We’ll walk through two distinct authentication architectures—OAuth 2.0 User Consent and Service Account Domain-Wide Delegation (DWD)—and share a blueprint for deploying on Google Cloud Run.


The architectural shift: Why MCP and Google ADK?

Historically, integrating Gemini with Google Workspace APIs meant manually crafting OpenAPI specs or Python function-calling dictionaries for every Gmail endpoint, along with custom error handling, schema serialization, and pagination logic.

The Model Context Protocol (MCP) standardizes how AI applications connect to external tools and data sources. Google Workspace now provides managed, remote Streamable HTTP MCP endpoints such as https://gmailmcp.googleapis.com/mcp/v1.

Key benefits of this architecture

  • Standardized JSON-RPC 2.0 discovery: The agent queries available tools (such as list_threads, get_message, send_message, and modify_labels) dynamically via tools/list.
  • Zero custom tool wrappers: No need to hand-code parameter parsing or function schemas for the LLM.
  • Native ADK toolset integration: Google ADK’s McpToolset class connects Gemini models to remote Streamable HTTP MCP endpoints with just a few lines of configuration.

Choosing between interactive user flow and service account Domain-wide Delegation (DWD)

The most critical architectural decision when designing an enterprise email agent is identity and credential delegation. Here we list two options that can be used for this purpose:

Authentication comparison matrix

[!NOTE]
Enterprise compliance note: Because the Service Account DWD pattern and internal OAuth client flows are scoped strictly to your organization’s Google Workspace domain, they are exempt from Google’s public OAuth app verification and external CASA Tier 2/3 assessments. The organization’s Workspace Super Admin maintains full sovereign control over which scopes are authorized via the Google Workspace Admin console.

Feature / Dimension Option 1: Interactive User Flow Option 2: Service Account DWD
Primary Identity End user’s Google Account Service Account impersonating a target Workspace user
Personal @gmail.com Support Yes No (Google Workspace domains only)
Workspace @domain.com Support Yes Yes
User Sign-in Prompt Required? Yes (One-time interactive browser consent) No (100% Headless & automated)
Workspace Admin Approval Needed? No (Unless organization restricts 3rd party apps) Yes (Super Admin authorization in admin.google.com)
Server Token Storage Must store per-user Refresh Tokens (Secret Manager / DB) No user refresh tokens needed (Dynamic ADC token minting)
Security Scope User grants access strictly to their own inbox Admin delegates authority across specified domain scopes
Best Production Use Case Multi-tenant SaaS apps, interactive user-facing assistants Headless triage bots, 24/7 enterprise background workflows

Step-by-step implementation

Let’s walk through configuring both methods, solving the token refresh challenge, and implementing the agents in Python.

1. Required OAuth scopes

When connecting generative AI models to enterprise communication hubs, the fundamental security principle is least privilege. We intentionally configure our scopes to use gmail.compose rather than gmail.send for security concerns.

SCOPES = [
    "https://www.googleapis.com/auth/cloud-platform",
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.modify",
    "https://www.googleapis.com/auth/gmail.compose", # Draft-only (Recommended)# 
    "https://www.googleapis.com/auth/gmail.labels",
]

Why you should avoid granting direct gmail.send to LLMs

  1. Indirect prompt injection: Untrusted inbound emails could hijack the model into sending unauthorized outbound emails.
  2. Irreversibility and brand liability: Sent emails cannot be recalled; hallucinations create instant damage.
  3. Restricted OAuth compliance: Minimizes enterprise blast radius.

The recommended pattern is “agent drafts, human approves”, which implements a human-in-the-loop workflow using create_draft and review labels.


2. Setting up option 1: Interactive user flow

Option 1 is ideal when building interactive assistants where end users grant access to their individual accounts. Refer to Google Workspace MCP & ADK Codelab for a step-by-step guide on how to set up Option 1.

GCP Console setup:

  1. In the Google Cloud Console Credentials Page, configure the OAuth consent screen (Internal or External).
  2. Create an OAuth 2.0 Client ID (select Desktop App for local testing or Web Application for production).
  3. Save the Client ID and Client Secret in .env.
GOOGLE_GENAI_USE_VERTEXAI=1
GOOGLE_CLOUD_PROJECT=your_project_id
GOOGLE_CLOUD_LOCATION=us-central1
OAUTH_CLIENT_ID=your_client_id.apps.googleusercontent.com
OAUTH_CLIENT_SECRET=your_client_secret

Running interactive authorization:

Run the following auth.py to trigger the local consent flow and store the authorized credentials in token.json and Application Default Credentials (ADC):

# auth.py
import json, os
from google_auth_oauthlib.flow import InstalledAppFlow

def run_oauth_flow():
    client_config = {
        "installed": {
            "client_id": os.environ["OAUTH_CLIENT_ID"],
            "client_secret": os.environ["OAUTH_CLIENT_SECRET"],
            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
            "token_uri": "https://oauth2.googleapis.com/token",
        }
    }
    flow = InstalledAppFlow.from_client_config(client_config, scopes=SCOPES)
    creds = flow.run_local_server(port=0)

    cred_dict = json.loads(creds.to_json())
    cred_dict["type"] = "authorized_user"
    cred_dict["quota_project_id"] = os.environ.get("GOOGLE_CLOUD_PROJECT")

    with open("token.json", "w") as f:
        json.dump(cred_dict, f, indent=2)
    return creds

3. Setting up option 2: Service account domain-wide delegation (DWD)

Option 2 allows headless background workers to interact with inboxes on behalf of domain users without any interactive login prompts.

Step 1: Create a Service Account in GCP Console

  1. Navigate to IAM & Admin > Service Accounts and create a Service Account (e.g., sa-workspace-agent).
  2. Copy the 21-digit Numeric Client ID (OAuth2 Unique ID) from the service account details.
  3. Grant your caller principal or runtime identity the Service Account Token Creator (roles/iam.serviceAccountTokenCreator) role.

Step 2: Authorize scopes in Google Workspace Admin Console

  1. Sign in to admin.google.com as a Super Admin.
  2. Go to Security > Access and data control > API controls > Manage Domain Wide Delegation.
  3. Click Add new, paste the Numeric Client ID, and add the comma-delimited OAuth scopes.
  4. Click Authorize.

Step 3: Configure environment (.env)

GOOGLE_GENAI_USE_VERTEXAI=1
GOOGLE_CLOUD_PROJECT=your_project_id
GOOGLE_CLOUD_LOCATION=us-central1
SERVICE_ACCOUNT_EMAIL=sa-workspace-agent@your_project.iam.gserviceaccount.com
TARGET_USER_EMAIL=target_employee@yourcompany.com

4. Resilient in-memory token caching (TokenRelay)

When an LLM agent executes complex, multi-step tool calls in rapid succession, refreshing tokens synchronously within tool execution hooks can trigger redundant requests and infinite retry loops. Refer to Google Workspace MCP & ADK Codelab for the detailed source code.

The TokenRelay cache solves this by managing proactive refreshes with thread-safe in-memory caching:

import datetime
from google.auth.transport.requests import Request

class TokenRelay:
    """Manages in-memory caching of Google OAuth access tokens for Gmail MCP.
    
    Proactively refreshes access tokens 5 minutes prior to expiration.
    """

    def __init__(self, credentials):
        self._creds = credentials
        self._cached_token: str | None = None
        self._expiry: datetime.datetime | None = None

    def get_token(self) -> str:
        """Returns a valid access token, proactively refreshing when needed."""
        now = datetime.datetime.now()
        if (
            self._cached_token is None
            or self._expiry is None
            or now >= (self._expiry - datetime.timedelta(minutes=5))
        ):
            try:
                self._creds.refresh(Request())
                self._cached_token = self._creds.token
                self._expiry = now + datetime.timedelta(minutes=50)
            except Exception as e:
                print(f"[TokenRelay] Token refresh failed: {e}")
        return self._cached_token or ""

5. Assembling the agents with Google ADK

Now, let’s assemble both agents using Google ADK’s LlmAgent and McpToolset.

Option 1: Interactive user agent

Refer to Google Workspace MCP & ADK Codelab for a step-by-step guide and source code on how to implement Option 1.

import os
from google.oauth2.credentials import Credentials
from google.adk.agents.llm_agent import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

os.environ["ADK_ENABLE_MCP_GRACEFUL_ERROR_HANDLING"] = "1"

# 1. Load user credentials and wrap with TokenRelay
creds = Credentials.from_authorized_user_file("token.json", scopes=SCOPES)
token_relay = TokenRelay(creds)

# 2. Define dynamic header provider for quota & auth
def auth_header_provider(tool_context=None) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {token_relay.get_token()}",
        "X-Goog-User-Project": os.environ.get("GOOGLE_CLOUD_PROJECT", "your-project-id"),
    }

# 3. Connect to managed Gmail MCP endpoint
gmail_mcp = McpToolset(
    connection_params=StreamableHTTPConnectionParams(
        url="https://gmailmcp.googleapis.com/mcp/v1",
        timeout=60,
    ),
    header_provider=auth_header_provider,
)

# 4. Initialize the ADK Agent
root_agent = LlmAgent(
    model="gemini-3.7-flash",
    name="gmail_oauth_agent",
    instruction="You are an AI email assistant. Help the user search, summarize, and draft emails using Gmail MCP tools.",
    tools=[gmail_mcp],
)

Option 2: Enterprise headless DWD agent

import os
import google.auth
from google.auth import impersonated_credentials
from google.adk.agents.llm_agent import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

os.environ["ADK_ENABLE_MCP_GRACEFUL_ERROR_HANDLING"] = "1"

# 1. Obtain base Application Default Credentials (e.g., Cloud Run Workload Identity)
base_credentials, _ = google.auth.default(scopes=SCOPES)

# 2. Impersonate Service Account and delegate domain authority
creds = impersonated_credentials.Credentials(
    source_credentials=base_credentials,
    target_principal=os.environ["SERVICE_ACCOUNT_EMAIL"],
    target_scopes=SCOPES,
    subject=os.environ["TARGET_USER_EMAIL"],  # Target employee mailbox
)

token_relay = TokenRelay(creds)

def auth_header_provider(tool_context=None) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {token_relay.get_token()}",
        "X-Goog-User-Project": os.environ.get("GOOGLE_CLOUD_PROJECT", "your-project-id"),
    }

gmail_mcp = McpToolset(
    connection_params=StreamableHTTPConnectionParams(
        url="https://gmailmcp.googleapis.com/mcp/v1",
        timeout=60,
    ),
    header_provider=auth_header_provider,
)

root_agent = LlmAgent(
    model="gemini-3.7-flash",
    name="gmail_dwd_agent",
    instruction="You are an autonomous enterprise operations assistant performing automated inbox triage on behalf of the designated mailbox.",
    tools=[gmail_mcp],
)

Production deployment blueprint on Google Cloud Run

To scale your agent in production, deploy the application container to Google Cloud Run utilizing Workload Identity. Pay attention to the following best practices to ensure security and reliability:

  1. Never hardcode JSON keys: In Cloud Run, authenticate your service container using Workload Identity attached to the runtime service account.
  2. Always supply X-Goog-User-Project: This header ensures API quotas and metrics route accurately to your GCP project.
  3. Configure timeout and error resiliency: Remote Streamable HTTP MCP connections should specify timeout=60 and ADK_ENABLE_MCP_GRACEFUL_ERROR_HANDLING="1" to avoid dropping conversational context during network spikes.
  4. Follow the principle of least privilege: Scope permissions to the minimal set required (e.g., gmail.readonly for analysis bots, gmail.modify for categorization and triage).

Testing and local verification with ADK web server

You can test the agents locally using the built-in ADK developer UI. To understand how this architecture operates in production, let’s ground it in an enterprise scenario: Automating cross-functional project updates and draft generation.

Real-world application scenario: autonomous inbox triage for project updates

An engineering operations team receives dozens of daily emails regarding an initiative called “Forge Project”. Manual triage requires someone to read each thread, identify blocking items, draft updates, and update labels.

In our test inbox, we have active emails regarding the Forge Project in the inbox:

Execution walkthrough with ADK

We ask our headless DWD agent:

“Search for recent emails about the Forge Project, summarize the current blockers, and draft a response to the latest update acknowledging the timeline.”

The execution unfolds across three coordinated phases:

  1. Dynamic tool discovery: Gemini inspects the live Gmail MCP schema and executes list_threads(q=‘Forge Project’).
  2. Context extraction & synthesis: The agent fetches message contents via get_thread, extracts timeline updates, and identifies pending deliverables.
  3. Safe drafting (HITL): Instead of executing an irreversible send, the agent calls create_draft to prepare a response and applies the FORGE_TRIAGED label.

The screenshot above illustrates how the agent’s MCP tool output accurately matches the live Gmail inbox state. The agent was able to successfully retrieve and summarize emails related to the ‘Forge Project’.


Conclusion and next steps

Integrating Google Agent Development Kit (ADK) with the Google Workspace Gmail MCP Server unlocks a robust, standardized foundation for enterprise AI automation. By pairing MCP’s standard tool discovery with the appropriate authentication model:

  • Interactive User Flow for multi-tenant and interactive assistants
  • Service Account Domain-Wide Delegation for 24/7 autonomous background triage

You will be able to achieve both the security compliance required by enterprise IT and the seamless developer experience of modern agent frameworks.

Ready to build?

2 Likes