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:
-
Tool orchestration overhead: How do models discover and reliably call email actions without maintaining custom function schemas?
-
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, andmodify_labels) dynamically viatools/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
McpToolsetclass 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
- Indirect prompt injection: Untrusted inbound emails could hijack the model into sending unauthorized outbound emails.
- Irreversibility and brand liability: Sent emails cannot be recalled; hallucinations create instant damage.
- 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:
- In the Google Cloud Console Credentials Page, configure the OAuth consent screen (Internal or External).
- Create an OAuth 2.0 Client ID (select Desktop App for local testing or Web Application for production).
- 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
- Navigate to IAM & Admin > Service Accounts and create a Service Account (e.g.,
sa-workspace-agent). - Copy the 21-digit Numeric Client ID (OAuth2 Unique ID) from the service account details.
- 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
- Sign in to admin.google.com as a Super Admin.
- Go to Security > Access and data control > API controls > Manage Domain Wide Delegation.
- Click Add new, paste the Numeric Client ID, and add the comma-delimited OAuth scopes.
- 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:
- Never hardcode JSON keys: In Cloud Run, authenticate your service container using Workload Identity attached to the runtime service account.
- Always supply
X-Goog-User-Project: This header ensures API quotas and metrics route accurately to your GCP project. - Configure timeout and error resiliency: Remote Streamable HTTP MCP connections should specify
timeout=60andADK_ENABLE_MCP_GRACEFUL_ERROR_HANDLING="1"to avoid dropping conversational context during network spikes. - Follow the principle of least privilege: Scope permissions to the minimal set required (e.g.,
gmail.readonlyfor analysis bots,gmail.modifyfor 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:
- Dynamic tool discovery: Gemini inspects the live Gmail MCP schema and executes list_threads(q=‘Forge Project’).
- Context extraction & synthesis: The agent fetches message contents via get_thread, extracts timeline updates, and identifies pending deliverables.
- 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?
- Apply to join the Google Workspace Developer Preview Program to access managed MCP endpoints.
- Work through the hands-on Google Workspace MCP & ADK Codelab.




