How to Build a LangGraph Gmail Agent with Arcade (MCP)

How to Build a LangGraph Gmail Agent with Arcade (MCP)

Arcade.dev Team's avatar
Arcade.dev Team
OCTOBER 16, 2025
7 MIN READ
TUTORIALS
Rays decoration image
Ghost Icon

Building production-ready AI agents that can interact with Gmail requires solving authentication, tool calling, and orchestration challenges simultaneously. This guide demonstrates how to build a LangGraph Gmail agent using Arcade's Model Context Protocol (MCP) integration, enabling secure multi-user email automation with enterprise-grade authentication.

What You'll Build

This tutorial walks through creating a LangGraph-based Gmail agent that can:

  • Authenticate multiple users via OAuth 2.0
  • Send, draft, and search emails on behalf of authorized users
  • Maintain secure credential isolation between users
  • Scale to handle thousands of concurrent operations

Prerequisites and Setup

Required Components

Before starting, ensure you have:

  • An active Arcade.dev account with API key
  • Python 3.8+ installed
  • LangGraph installed (pip install langgraph)
  • A Google Cloud Console project with OAuth 2.0 credentials configured

Installation

# Install Arcade Python client and dependencies
pip install arcadepy arcade_tdk langgraph langchain

# Set up environment variables
export ARCADE_API_KEY="your_arcade_api_key"
export GOOGLE_CLIENT_ID="your_google_client_id"
export GOOGLE_CLIENT_SECRET="your_google_client_secret"

Arcade MCP Architecture for Secure Gmail Automation

Arcade serves as both an MCP server and a bridge to other MCP servers using HTTP transport. This architecture provides:

  • Authentication Layer: OAuth 2.0 flows managed server-side
  • Tool Calling Interface: Unified API for Gmail operations
  • Security Boundaries: LLMs never see authentication tokens
  • Multi-User Support: Per-user credential isolation and management

The platform transforms single-user MCP servers into production-ready systems through pre-built connectors and managed authorization layers. Learn more about Arcade's architecture.

Building the LangGraph Agent Structure

Core Agent Implementation

Create a LangGraph agent that integrates with Arcade's Gmail toolkit:

from langgraph.graph import StateGraph, END
from arcadepy import Arcade
from typing import TypedDict, Annotated, Sequence
import operator

class AgentState(TypedDict):
    messages: Annotated[Sequence[str], operator.add]
    user_id: str
    authenticated: bool
    pending_action: dict

class ArcadeGmailAgent:
    def __init__(self):
        self.arcade = Arcade(api_key=os.environ.get("ARCADE_API_KEY"))
        self.graph = self._build_graph()

    def _build_graph(self):
        workflow = StateGraph(AgentState)

        # Add nodes
        workflow.add_node("check_auth", self.check_authentication)
        workflow.add_node("handle_auth", self.handle_authorization)
        workflow.add_node("execute_gmail", self.execute_gmail_action)
        workflow.add_node("process_response", self.process_response)

        # Define edges
        workflow.add_edge("check_auth", "handle_auth")
        workflow.add_conditional_edges(
            "handle_auth",
            self.route_after_auth,
            {
                "authenticated": "execute_gmail",
                "needs_auth": "check_auth"
            }
        )
        workflow.add_edge("execute_gmail", "process_response")
        workflow.add_edge("process_response", END)

        workflow.set_entry_point("check_auth")
        return workflow.compile()

Authentication Node Implementation

Handle OAuth flows within the LangGraph workflow:

async def check_authentication(self, state: AgentState):
    """Check if user has valid Gmail authorization"""
    user_id = state["user_id"]

    # Check Gmail.SendEmail authorization status
    auth_response = await self.arcade.tools.authorize(
        tool_name="Gmail.SendEmail",
        user_id=user_id
    )

    if auth_response.status != "completed":
        state["authenticated"] = False
        state["pending_action"] = {
            "type": "authorization_required",
            "url": auth_response.url,
            "message": "Complete Gmail authorization to proceed"
        }
    else:
        state["authenticated"] = True

    return state

async def handle_authorization(self, state: AgentState):
    """Handle authorization completion"""
    if not state["authenticated"]:
        # Wait for user to complete OAuth
        return state

    # Load Gmail toolkit for authenticated user
    gmail_tools = await self.arcade.tools.list(
        toolkit="gmail",
        user_id=state["user_id"]
    )
    state["available_tools"] = gmail_tools.items
    return state

Integrating Gmail Tools with LangGraph

Tool Execution Pattern

Connect LangGraph decisions to Arcade's Gmail toolkit:

async def execute_gmail_action(self, state: AgentState):
    """Execute Gmail operations through Arcade MCP"""
    action = state["pending_action"]
    user_id = state["user_id"]

    # Map action types to Arcade Gmail tools
    tool_mapping = {
        "send_email": "Gmail.SendEmail",
        "create_draft": "Gmail.WriteDraftEmail",
        "search_emails": "Gmail.SearchThreads",
        "list_emails": "Gmail.ListEmails"
    }

    tool_name = tool_mapping.get(action["type"])

    try:
        response = await self.arcade.tools.execute(
            tool_name=tool_name,
            input=action["parameters"],
            user_id=user_id
        )
        state["messages"].append(f"Successfully executed: {response.output}")
    except Exception as e:
        if getattr(e, "type", "") == "authorization_required":
            state["authenticated"] = False
            state["pending_action"]["auth_url"] = getattr(e, "url", "")
        else:
            state["messages"].append(f"Error: {str(e)}")

    return state

Building Multi-Step Gmail Workflows

Create multi-step Gmail operations using LangGraph's graph structure:

class EmailCampaignAgent:
    def __init__(self):
        self.arcade = Arcade(api_key=os.environ.get("ARCADE_API_KEY"))
        self.workflow = self._build_campaign_graph()

    def _build_campaign_graph(self):
        workflow = StateGraph(CampaignState)

        # Campaign workflow nodes
        workflow.add_node("search_contacts", self.search_email_contacts)
        workflow.add_node("draft_emails", self.create_personalized_drafts)
        workflow.add_node("review_drafts", self.review_and_approve)
        workflow.add_node("send_approved", self.send_approved_emails)

        # Conditional routing based on approval
        workflow.add_conditional_edges(
            "review_drafts",
            lambda x: "send" if x["approved"] else "revise",
            {
                "send": "send_approved",
                "revise": "draft_emails"
            }
        )

        return workflow.compile()

    async def search_email_contacts(self, state):
        """Search Gmail for relevant contacts"""
        response = await self.arcade.tools.execute(
            tool_name="Gmail.SearchThreads",
            input={"query": state["search_query"]},
            user_id=state["user_id"]
        )
        state["contacts"] = self._extract_contacts(response.output)
        return state

Configuring MCP Server Connection

Desktop Client Configuration

For development with VS Code or similar IDEs:

{
  "mcp": {
    "servers": {
      "arcade-gmail": {
        "url": "https://api.arcade.dev/v1/mcps/arcade-anon/mcp"
      }
    }
  }
}

Production Server Configuration

Deploy with self-hosted Arcade engine:

auth:
  providers:
    - id: gmail-provider
      description: "Gmail OAuth for LangGraph agents"
      enabled: true
      type: oauth2
      provider_id: google
      client_id: ${env:GOOGLE_CLIENT_ID}
      client_secret: ${env:GOOGLE_CLIENT_SECRET}

api:
  development: false
  host: 0.0.0.0
  port: 9099

tools:
  directors:
    - id: default
      enabled: true
      max_tools: 64

workers:
  - id: "langgraph-gmail-worker"
    enabled: true
    http:
      uri: "http://localhost:8002"
      secret: ${env:WORKER_SECRET}
      timeout: 30s

Handling Multi-User Authentication

User Session Management

Implement per-user session tracking within LangGraph:

class MultiUserGmailOrchestrator:
    def __init__(self):
        self.arcade = Arcade(api_key=os.environ.get("ARCADE_API_KEY"))
        self.user_graphs = {}
        self.user_sessions = {}

    async def get_user_graph(self, user_id: str):
        """Get or create user-specific LangGraph instance"""
        if user_id not in self.user_graphs:
            # Create new graph for user
            agent = ArcadeGmailAgent()
            self.user_graphs[user_id] = agent.graph

            # Initialize user session
            self.user_sessions[user_id] = {
                "authenticated": False,
                "tools_loaded": False,
                "last_activity": datetime.now()
            }

        return self.user_graphs[user_id]

    async def process_user_request(self, user_id: str, request: dict):
        """Route request through user-specific graph"""
        graph = await self.get_user_graph(user_id)

        initial_state = {
            "messages": [],
            "user_id": user_id,
            "authenticated": self.user_sessions[user_id]["authenticated"],
            "pending_action": request
        }

        result = await graph.ainvoke(initial_state)

        # Update session state
        self.user_sessions[user_id]["authenticated"] = result["authenticated"]
        self.user_sessions[user_id]["last_activity"] = datetime.now()

        return result

OAuth Callback Handling

Process OAuth completions for multiple users:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post("/api/oauth/callback")
async def oauth_callback(request: Request):
    body = await request.json()
    user_id = body.get("userId")

    # Wait for OAuth completion
    arcade = Arcade(api_key=os.environ.get("ARCADE_API_KEY"))
    auth_result = await arcade.auth.wait_for_completion(
        authorization_id=body.get("authorizationId")
    )

    if auth_result.status == "completed":
        # Update user graph state
        orchestrator = get_orchestrator_instance()
        orchestrator.user_sessions[user_id]["authenticated"] = True

        # Load Gmail tools
        gmail_tools = await arcade.tools.list(
            toolkit="gmail",
            user_id=user_id
        )

        return JSONResponse({
            "success": True,
            "tools_available": len(gmail_tools.items)
        })

    return JSONResponse({"success": False})

Error Handling and Recovery

Implementing Robust Error Management

class ResilientGmailAgent:
    def __init__(self):
        self.arcade = Arcade(api_key=os.environ.get("ARCADE_API_KEY"))
        self.retry_config = {
            "max_attempts": 3,
            "backoff_factor": 2
        }

    async def handle_gmail_error(self, error, state):
        """Intelligent error recovery for Gmail operations"""
        error_type = getattr(error, 'type', str(type(error).__name__))

        recovery_strategies = {
            "authorization_required": self.trigger_reauth,
            "rate_limit_exceeded": self.apply_backoff,
            "insufficient_scope": self.request_additional_scopes,
            "token_expired": self.refresh_token
        }

        handler = recovery_strategies.get(error_type, self.default_error_handler)
        return await handler(error, state)

    async def trigger_reauth(self, error, state):
        """Initiate re-authentication flow"""
        state["authenticated"] = False
        state["pending_action"] = {
            "type": "reauth_required",
            "auth_url": getattr(error, "url", ""),
            "message": "Gmail authorization expired"
        }
        return state

    async def apply_backoff(self, error, state):
        """Implement exponential backoff for rate limits"""
        attempt = state.get("retry_attempt", 0) + 1
        wait_time = self.retry_config["backoff_factor"] ** attempt

        await asyncio.sleep(wait_time)
        state["retry_attempt"] = attempt

        if attempt < self.retry_config["max_attempts"]:
            state["should_retry"] = True
        else:
            state["messages"].append("Rate limit exceeded, please try later")

        return state

Performance Optimization

Caching Strategy for LangGraph Nodes

from collections import OrderedDict
import time

class CachedGmailAgent:
    def __init__(self):
        self.arcade = Arcade(api_key=os.environ.get("ARCADE_API_KEY"))
        self.tool_cache = OrderedDict()
        self.cache_ttl = 3600000  # 1 hour in milliseconds

    async def get_gmail_tools(self, user_id: str):
        """Retrieve Gmail tools with caching"""
        cache_key = f"gmail_tools_{user_id}"

        # Check cache
        if cache_key in self.tool_cache:
            entry = self.tool_cache[cache_key]
            if time.time() * 1000 - entry["timestamp"] < self.cache_ttl:
                self.tool_cache.move_to_end(cache_key)
                return entry["tools"]
            else:
                del self.tool_cache[cache_key]

        # Fetch fresh tools
        tools = await self.arcade.tools.list(
            toolkit="gmail",
            user_id=user_id
        )

        # Cache results
        self.tool_cache[cache_key] = {
            "tools": tools.items,
            "timestamp": time.time() * 1000
        }

        # LRU eviction
        if len(self.tool_cache) > 100:
            self.tool_cache.popitem(last=False)

        return tools.items

Batch Processing for Multiple Operations

async def batch_gmail_operations(self, operations: list, user_id: str):
    """Process multiple Gmail operations efficiently"""
    results = []

    # Group operations by type for optimization
    grouped_ops = {}
    for op in operations:
        op_type = op["type"]
        if op_type not in grouped_ops:
            grouped_ops[op_type] = []
        grouped_ops[op_type].append(op)

    # Execute batched operations
    for op_type, ops in grouped_ops.items():
        if op_type == "send_email":
            # Send multiple emails in sequence
            for op in ops:
                result = await self.arcade.tools.execute(
                    tool_name="Gmail.SendEmail",
                    input=op["parameters"],
                    user_id=user_id
                )
                results.append(result)

        elif op_type == "search":
            # Combine search queries when possible
            combined_query = " OR ".join([op["query"] for op in ops])
            result = await self.arcade.tools.execute(
                tool_name="Gmail.SearchThreads",
                input={"query": combined_query},
                user_id=user_id
            )
            results.append(result)

    return results

Production Deployment

Kubernetes Configuration

Deploy your LangGraph Gmail agent at scale:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: langgraph-gmail-agent
spec:
  replicas: 3
  selector:
    matchLabels:
      app: langgraph-gmail
  template:
    metadata:
      labels:
        app: langgraph-gmail
    spec:
      containers:
        - name: arcade-engine
          image: ghcr.io/arcadeai/engine:latest
          env:
            - name: ARCADE_API_KEY
              valueFrom:
                secretKeyRef:
                  name: arcade-secrets
                  key: api-key
            - name: GOOGLE_CLIENT_ID
              valueFrom:
                secretKeyRef:
                  name: oauth-secrets
                  key: google-client-id
            - name: GOOGLE_CLIENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: oauth-secrets
                  key: google-client-secret
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "1Gi"
              cpu: "1000m"

Monitoring and Observability

Track agent performance and authentication metrics:

class AgentMonitor:
    def __init__(self):
        self.metrics = {
            "graph_executions": 0,
            "auth_successes": 0,
            "gmail_operations": 0,
            "error_recoveries": 0
        }

    async def track_graph_execution(self, user_id: str, execution_time: float):
        """Monitor LangGraph execution metrics"""
        self.metrics["graph_executions"] += 1

        # Log to monitoring system
        await self.send_metrics({
            "timestamp": datetime.now().isoformat(),
            "user_id": self.hash_user_id(user_id),
            "execution_time_ms": execution_time * 1000,
            "service": "langgraph-gmail",
            "protocol": "mcp"
        })

    def generate_health_report(self):
        """Generate system health metrics"""
        return {
            "status": "healthy",
            "graph_execution_rate": self.calculate_execution_rate(),
            "auth_success_rate": self.calculate_auth_rate(),
            "active_graphs": len(self.get_active_graphs()),
            "gmail_quota_usage": self.check_gmail_quota()
        }

Testing Your LangGraph Gmail Agent

Unit Testing Authentication Flows

import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.mark.asyncio
async def test_gmail_authentication():
    """Test Gmail authentication through LangGraph"""
    agent = ArcadeGmailAgent()

    # Mock Arcade client
    agent.arcade = AsyncMock()
    agent.arcade.tools.authorize.return_value = MagicMock(
        status="completed"
    )

    state = {
        "user_id": "test_user",
        "messages": [],
        "authenticated": False,
        "pending_action": {"type": "send_email"}
    }

    result = await agent.check_authentication(state)

    assert result["authenticated"] == True
    agent.arcade.tools.authorize.assert_called_once_with(
        tool_name="Gmail.SendEmail",
        user_id="test_user"
    )

@pytest.mark.asyncio
async def test_gmail_operation_execution():
    """Test Gmail operation execution"""
    agent = ArcadeGmailAgent()

    # Mock successful execution
    agent.arcade = AsyncMock()
    agent.arcade.tools.execute.return_value = MagicMock(
        output={"message_id": "123", "status": "sent"}
    )

    state = {
        "user_id": "test_user",
        "messages": [],
        "authenticated": True,
        "pending_action": {
            "type": "send_email",
            "parameters": {
                "to": "test@example.com",
                "subject": "Test",
                "body": "Test message"
            }
        }
    }

    result = await agent.execute_gmail_action(state)

    assert "Successfully executed" in result["messages"][0]
    agent.arcade.tools.execute.assert_called_once()

Next Steps

With your LangGraph Gmail agent built on Arcade's MCP platform, you can:

The combination of LangGraph's orchestration capabilities with Arcade's secure authentication layer enables building production-ready Gmail agents that handle multi-user scenarios while maintaining enterprise-grade security boundaries.

SHARE THIS POST

RECENT ARTICLES

Rays decoration image
THOUGHT LEADERSHIP

Enterprise MCP Guide For Retail Banking & Payments: Use Cases, Best Practices, and Trends

The global payments industry processes $2.0 quadrillion in value flows annually, generating $2.5 trillion in revenue. Yet despite decades of digital transformation investment, critical banking operations,anti-money laundering investigation, KYC onboarding, payment reconciliation,remain largely manual. Model Context Protocol (MCP) represents the infrastructure breakthrough that enables financial institutions to move beyond chatbot pilots to production-grade AI agents that take multi-user authoriz

Rays decoration image
THOUGHT LEADERSHIP

Enterprise MCP Guide For Capital Markets & Trading: Use Cases, Best Practices, and Trends

Capital markets technology leaders face a critical infrastructure challenge: scattered AI pilots, disconnected integrations, and fragmented, domain-specific systems that turn engineers into human APIs manually stitching together trading platforms, market data feeds, and risk management tools. The Model Context Protocol (MCP) represents a fundamental shift from this costly one-off integration approach to a universal standardization layer that acts as the backbone for AI-native financial enterpris

Rays decoration image
THOUGHT LEADERSHIP

Enterprise MCP Guide For InsurTech: Use Cases, Best Practices, and Trends

The insurance industry faces a pivotal transformation moment. Model Context Protocol (MCP) has moved from experimental technology to production infrastructure, with 16,000+ active servers deployed across enterprises and millions of weekly SDK downloads. For InsurTech leaders, the question is no longer whether to adopt MCP, but how to implement it securely and effectively. Arcade's platform provides the MCP runtime for secure, multi-user authorization so AI agents can act on behalf of users acros

Blog CTA Icon

Get early access to Arcade, and start building now.