An MCP (Model Context Protocol) server that enables Claude to manage Gmail inboxes through natural conversation. Ask Claude to triage your inbox, search emails, send messages, and more.
- 16 Gmail Tools - Full inbox management via conversational AI
- Read-Only Mode - Run with
READ_ONLY=truefor safe cross-project use (9 tools, single OAuth scope) - Human-in-the-Loop Security - All write operations require explicit approval
- Encrypted Token Storage - AES-256-GCM encryption for OAuth tokens at rest
- Rate Limiting - Token bucket protection (100 req/min default)
- Multiple Transports - stdio (local), HTTP/SSE (remote deployments)
- Python 3.11+
- Poetry
- Google Cloud project with Gmail API enabled
git clone https://github.com/your-org/gmail-mcp-server.git
cd gmail-mcp-server
poetry install- Go to Google Cloud Console
- Create OAuth 2.0 Client ID with type "Desktop app" (required for local server flow)
- Enable Gmail API in your project
- Note your Client ID and Client Secret
export GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
export GOOGLE_CLIENT_SECRET="your-client-secret"
export TOKEN_ENCRYPTION_KEY=$(openssl rand -hex 32)Create or update .mcp.json in your project root:
{
"mcpServers": {
"gmail": {
"command": "poetry",
"args": ["run", "python", "-m", "gmail_mcp"],
"cwd": "/path/to/gmail-mcp-server",
"env": {
"GOOGLE_CLIENT_ID": "${GOOGLE_CLIENT_ID}",
"GOOGLE_CLIENT_SECRET": "${GOOGLE_CLIENT_SECRET}",
"TOKEN_ENCRYPTION_KEY": "${TOKEN_ENCRYPTION_KEY}"
}
}
}
}Restart Claude Code, then run /mcp to verify the Gmail server is connected.
| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID |
OAuth 2.0 Client ID from Google Cloud Console |
GOOGLE_CLIENT_SECRET |
OAuth 2.0 Client Secret |
TOKEN_ENCRYPTION_KEY |
64-character hex string (256-bit AES key) |
| Variable | Default | Description |
|---|---|---|
READ_ONLY |
false |
Read-only mode — only gmail.readonly scope, no write tools |
TRANSPORT |
stdio |
Transport mode: stdio, http, sse, streamable-http |
PORT |
3000 |
HTTP server port (for http/sse transport) |
OAUTH_PORT |
3000 |
OAuth callback port (fallback: 3001, 3002) |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
RATE_LIMIT_MAX |
100 |
Max requests per minute per user |
HITL_TIMEOUT_MS |
300000 |
Approval expiration (5 min default) |
TOKEN_STORAGE_PATH |
~/.gmail-mcp/tokens/ |
Custom token storage location |
| Mode | Use Case |
|---|---|
stdio |
Local development with Claude Code/Desktop |
http / sse |
Remote deployments (Replit, Docker) |
streamable-http |
Stateless deployments |
Set READ_ONLY=true for safe, read-only Gmail access — ideal for a global MCP config shared across all projects.
What changes:
| Full Mode (default) | Read-Only Mode | |
|---|---|---|
| OAuth scopes | 4 scopes (readonly, modify, compose, labels) | 1 scope (gmail.readonly) |
| Tools registered | 16 (3 auth + 7 read + 6 write) | 9 (3 auth + 6 read) |
| Write tools visible | Yes (with HITL approval) | No — Claude can't see them |
| Consent screen | 4 checkboxes | 1 checkbox |
Global read-only config (~/.claude/.mcp.json):
{
"mcpServers": {
"gmail": {
"command": "gmail-mcp",
"args": [],
"env": {
"GOOGLE_CLIENT_ID": "${GOOGLE_CLIENT_ID}",
"GOOGLE_CLIENT_SECRET": "${GOOGLE_CLIENT_SECRET}",
"TOKEN_ENCRYPTION_KEY": "${TOKEN_ENCRYPTION_KEY}",
"READ_ONLY": "true"
}
}
}
}To override in a specific project (e.g., for full access), add a project-level .mcp.json without READ_ONLY or set it to "false".
Note: Switching between modes requires re-authentication (gmail_login) since Google doesn't upgrade scopes on existing tokens.
| Tool | Purpose |
|---|---|
gmail_login |
Sign in via Google OAuth (opens browser) |
gmail_logout |
Sign out and clear stored tokens |
gmail_get_auth_status |
Check if authenticated |
Example prompts:
- "Sign me into Gmail"
- "Am I logged into Gmail?"
- "Sign out of Gmail"
These tools read data without modifying your inbox. No approval required.
| Tool | Description | Read-Only |
|---|---|---|
gmail_triage_inbox |
Categorize emails by urgency (urgent, social, newsletter, other) | Yes |
gmail_search |
Search using Gmail query syntax | Yes |
gmail_summarize_thread |
Get full thread content for summarization | Yes |
gmail_draft_reply |
Get context for composing a reply | Yes |
gmail_chat_inbox |
Natural language inbox queries | Yes |
gmail_download_email |
Download email as .eml, HTML, and attachments | Yes |
gmail_apply_labels |
Add or remove labels from messages | No* |
*gmail_apply_labels requires gmail.modify scope and is excluded in read-only mode.
Example prompts:
"Triage my inbox"
"Show me unread emails from today"
"Search for emails from boss@company.com"
"Find emails with attachments from last week"
"Summarize this email thread"
"Help me reply to this email"
"Add the 'Important' label to these messages"
Gmail Query Syntax (for gmail_search):
from:sender@example.com- From specific senderto:recipient@example.com- To specific recipientsubject:keyword- Subject contains keywordafter:2024/01/01- After datehas:attachment- Has attachmentsis:unread- Unread messageslabel:important- Specific label
All write operations use Human-in-the-Loop approval:
- Tool returns a preview +
approval_id - Claude shows you the preview and asks for confirmation
- You approve, tool executes with the
approval_id
| Tool | Description |
|---|---|
gmail_send_email |
Compose and send emails |
gmail_archive_email |
Remove from inbox (keeps in All Mail) |
gmail_delete_email |
Move to trash (deleted after 30 days) |
gmail_unsubscribe |
Extract unsubscribe link from newsletters |
gmail_create_label |
Create a new label |
gmail_organize_labels |
Rename, delete, or update label visibility |
Example prompts:
"Send an email to john@example.com about the meeting tomorrow"
"Archive all newsletters from this week"
"Delete these promotional emails"
"Help me unsubscribe from this newsletter"
"Create a label called 'Projects'"
"Rename the 'Old' label to 'Archive'"
Approval Flow Example:
You: "Send an email to john@example.com about the meeting"
Claude: "I'll compose this email for you:
To: john@example.com
Subject: Meeting Tomorrow
Body: Hi John, I wanted to confirm our meeting tomorrow at 2pm...
Would you like me to send this?"
You: "Yes, send it"
Claude: "Email sent successfully!"
┌──────────┐ ┌─────────┐ ┌─────────────────┐ ┌───────────┐
│ User │ <──> │ Claude │ <──> │ Gmail MCP Server│ <──> │ Gmail API │
└──────────┘ └─────────┘ └─────────────────┘ └───────────┘
│ │
│ ┌─────┴─────┐
MCP Protocol │ Encrypted │
(JSON-RPC) │ Tokens │
└───────────┘
| Layer | Protection |
|---|---|
| HITL Approval | All write operations require explicit user confirmation |
| Token Encryption | AES-256-GCM with unique IV per token |
| Rate Limiting | Token bucket algorithm prevents API quota exhaustion |
| Audit Logging | JSON-formatted logs to stderr for all operations |
| Input Validation | Pydantic schemas validate all tool parameters |
# Step 1: No approval_id → Return preview
{
"status": "pending_approval",
"approval_id": "uuid-here",
"expires_at": "2024-01-01T12:05:00Z",
"preview": { "to": "...", "subject": "...", "body": "..." },
"message": "ACTION NOT TAKEN. Please review and confirm."
}
# Step 2: With approval_id → Execute
{
"status": "success",
"data": { "message_id": "..." }
}Approvals expire after 5 minutes (configurable via HITL_TIMEOUT_MS).
gmail_mcp/
├── __main__.py # Entry point, transport selection
├── server.py # FastMCP server, tool registration
├── tools/
│ ├── auth/ # gmail_login, gmail_logout, gmail_get_auth_status
│ ├── read/ # 6 read-only tools
│ └── write/ # 6 HITL-protected write tools
├── auth/
│ ├── oauth.py # Google OAuth flow (local server)
│ ├── tokens.py # AES-256-GCM encryption
│ └── storage.py # Encrypted file storage
├── gmail/
│ ├── client.py # Authenticated Gmail API client
│ ├── messages.py # Message operations
│ ├── threads.py # Thread operations
│ └── labels.py # Label management
├── hitl/
│ ├── manager.py # Approval lifecycle
│ └── models.py # Pydantic models
├── middleware/
│ ├── rate_limiter.py # Token bucket rate limiting
│ ├── audit_logger.py # JSON logging to stderr
│ └── validator.py # Input validation
└── schemas/
└── tools.py # Tool parameter models
OSError: [Errno 48] Address already in use
The OAuth callback server tries ports 3000, 3001, 3002 in sequence. To use a different port:
export OAUTH_PORT=4000If running in a headless environment (SSH, Docker):
- Copy the URL from the logs and open manually
- Or pre-authenticate locally and copy the token file to the server
Your OAuth client type is incorrect. Google blocks loopback redirects for non-desktop apps.
Fix: In Google Cloud Console:
- Delete the existing OAuth client
- Create new OAuth 2.0 Client ID with type "Desktop app"
- Update your environment variables with new credentials
- Ensure
~/.gmail-mcp/tokens/exists and is writable - Verify
TOKEN_ENCRYPTION_KEYis exactly 64 hex characters - If key changed, delete old token files and re-authenticate
CSRF protection triggered. Don't reuse browser tabs from old auth attempts. Restart the login flow.
Check logs with LOG_LEVEL=DEBUG:
LOG_LEVEL=DEBUG python -m gmail_mcppoetry install
poetry shellpytest --covruff check . && ruff format . && mypy .# stdio transport (default)
python -m gmail_mcp
# HTTP transport
TRANSPORT=http python -m gmail_mcp-
Single-User Mode - Currently operates with
user_id="default". For multi-account, run separate server instances with differentTOKEN_STORAGE_PATH. -
No Device Flow - Gmail scopes are "restricted" and cannot use device flow. Local server flow requires a browser.
-
Headless Deployments - Require workarounds:
- Pre-authenticate locally, copy token file to server
- Use Google Workspace service account with domain-wide delegation
MIT