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

How to Query Postgres from GPT-5 via Arcade (MCP)

Large language models need structured data access to provide accurate, data-driven insights. This guide demonstrates how to connect GPT-5 to PostgreSQL databases through Arcade's Model Context Protocol implementation, enabling secure database queries without exposing credentials directly to language models. Prerequisites Before implementing database connectivity, ensure you have: * Python 3.8 or higher installed * PostgreSQL database with connection credentials * Arcade API key (free t

Rays decoration image
THOUGHT LEADERSHIP

How to Connect GPT-5 to Slack with Arcade (MCP)

Building AI agents that interact with Slack requires secure OAuth authentication, proper token management, and reliable tool execution. This guide shows you how to connect GPT-5 to Slack using Arcade's Model Context Protocol (MCP) implementation, enabling your agents to send messages, read conversations, and manage channels with production-grade security. Prerequisites Before starting, ensure you have: * Arcade.dev account with API key * Python 3.10+ or Node.js 18+ installed * OpenAI A

Rays decoration image
THOUGHT LEADERSHIP

How to Build a GPT-5 Gmail Agent with Arcade (MCP)

Building AI agents that can access and act on Gmail data represents a significant challenge in production environments. This guide demonstrates how to build a fully functional Gmail agent using OpenAI's latest models through Arcade's Model Context Protocol implementation, enabling secure OAuth-based authentication and real-world email operations. Prerequisites Before starting, ensure you have: * Active Arcade.dev account with API key * Python 3.10 or higher installed * OpenAI API key w

Blog CTA Icon

Get early access to Arcade, and start building now.