MCP server for Gmail — send, read, and manage emails from Claude Code (or any MCP client).
- Python 3.12+
- uv package manager
- Google Cloud project with Gmail API enabled
- OAuth 2.0 client credentials (Desktop app type)
- Go to Google Cloud Console
- Click Select a Project (top bar) → New Project
- Name it (e.g.,
gmail-mcp) and click Create
- In your project, go to APIs & Services → Library
- Search for Gmail API
- Click Enable
- Go to APIs & Services → OAuth consent screen
- Select External (or Internal if using Google Workspace)
- Fill in required fields:
- App name:
gmail-tools-mcp(or anything you like) - User support email: your email
- Developer contact: your email
- App name:
- Click Save and Continue
- On the Scopes page, click Add or Remove Scopes
- Find and add:
https://www.googleapis.com/auth/gmail.modify - Click Save and Continue
- On the Test users page, click Add Users
- Add your Gmail address (the account you want to manage)
- Click Save and Continue → Back to Dashboard
- Go to APIs & Services → Credentials
- Click Create Credentials → OAuth client ID
- Application type: Desktop app
- Name:
gmail-tools-mcp(or anything) - Click Create
- Click Download JSON — save this file, you'll need it next
git clone <repo-url> gmail-tools-mcp
cd gmail-tools-mcp
uv syncCreate the credentials directory and copy your downloaded JSON file into it:
mkdir -p .credentials
cp ~/Downloads/client_secret_*.json .credentials/client_creds.jsonYour .credentials/ directory should now look like:
.credentials/
└── client_creds.json ← the file you just copied
The client_creds.json file contains your OAuth client ID and secret. It looks like this (do NOT share it):
{
"installed": {
"client_id": "123456789-abcdef.apps.googleusercontent.com",
"project_id": "your-project-id",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"client_secret": "GOCSPX-...",
"redirect_uris": ["http://localhost"]
}
}Run the server for the first time to authenticate:
uv run gmail-tools \
--creds-file-path .credentials/client_creds.json \
--token-path .credentials/gmail_tokens.jsonWhat happens:
- A browser window opens showing Google's OAuth consent screen
- Sign in with the Gmail account you added as a test user (Step 3)
- Click Allow to grant Gmail access
- The browser shows "The authentication flow has completed"
- Back in the terminal, the server starts and your tokens are encrypted and saved
After authentication, your .credentials/ directory will contain:
.credentials/
├── client_creds.json ← your OAuth client credentials (from Google)
├── gmail_tokens.json ← encrypted OAuth tokens (auto-generated)
└── gmail_tokens.json.key ← encryption key for the tokens (auto-generated)
On subsequent runs, the server loads the encrypted tokens automatically — no browser needed unless tokens expire and can't be refreshed.
Add the server to your Claude Code MCP configuration. Edit ~/.claude.json and add under mcpServers:
{
"mcpServers": {
"gmail-tools": {
"command": "uv",
"args": [
"--directory", "/absolute/path/to/gmail-tools-mcp",
"run", "gmail-tools",
"--creds-file-path", "/absolute/path/to/gmail-tools-mcp/.credentials/client_creds.json",
"--token-path", "/absolute/path/to/gmail-tools-mcp/.credentials/gmail_tokens.json"
]
}
}
}Replace /absolute/path/to/gmail-tools-mcp with the actual path where you cloned the repo.
For Claude Desktop, add the same config to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows).
The server provides 6 tools:
| Tool | Description | Required Inputs |
|---|---|---|
send-email |
Send an email (confirms with user first) | recipient, subject, message |
get-unread-emails |
List unread emails in primary inbox | none |
read-email |
Read full content of an email by ID | email_id |
trash-email |
Move email to trash (confirms with user first) | email_id |
mark-as-read |
Mark an email as read | email_id |
open-email |
Open an email in the default browser | email_id |
The server also provides 3 prompts for structured workflows:
| Prompt | Description | Arguments |
|---|---|---|
manage-email |
Email administrator assistant with all tools | none |
draft-email |
Compose an email draft for review | content, recipient, recipient_email |
edit-draft |
Revise an existing email draft | changes, current_draft |
gmail-tools --creds-file-path <path> --token-path <path>
| Argument | Required | Description |
|---|---|---|
--creds-file-path |
Yes | Path to OAuth 2.0 client credentials JSON (downloaded from Google Cloud Console) |
--token-path |
Yes | Path to store/load encrypted OAuth tokens (created automatically on first run) |
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ First Run │ │ Cached Run │ │ Expired │
│ │ │ │ │ │
│ No tokens on │ │ Tokens exist │ │ Tokens │
│ disk │ │ and are valid │ │ expired │
│ │ │ │ │ │
│ → Opens browser │ │ → Loads from │ │ → Auto │
│ → OAuth consent │ │ encrypted │ │ refresh │
│ → Encrypts & │ │ vault │ │ → Re-seal │
│ saves tokens │ │ → Ready! │ │ → Ready! │
└─────────────────┘ └──────────────────┘ └─────────────┘
Token storage:
- Tokens are encrypted with Fernet (AES-128-CBC + HMAC-SHA256) before writing to disk
- The encryption key is stored in a separate
.keyfile - Both files are created with 0600 permissions (owner read/write only)
- The
.credentials/directory is excluded from git via.gitignore
If authentication fails:
- Delete
.credentials/gmail_tokens.jsonand.credentials/gmail_tokens.json.key - Re-run the server — it will trigger a fresh OAuth consent flow
| Feature | Implementation |
|---|---|
| Token encryption | Fernet (AES-128-CBC + HMAC-SHA256) |
| File permissions | 0600 on vault + key files |
| Email validation | RFC 5322 regex + CR/LF injection check + 254-char limit |
| Message ID validation | Alphanumeric only, 64-char limit |
| Subject sanitization | CR/LF replaced with spaces, 500-char limit |
| Body validation | 100,000-char limit, non-blank check |
| Rate limiting | 30 calls/min (general), 10 calls/min (send/trash) |
| Error handling | No credentials or stack traces in error messages |
| Schema hardening | additionalProperties: false on all tool schemas |
uv sync --group devuv run pytestUnit tests use mocks — no Gmail credentials needed. They run by default and cover all code paths.
uv run pytest --cov=src/gmail_tools --cov-report=term-missingCurrent coverage: 100% (322/322 statements)
Integration tests send real emails through the Gmail API. They require valid credentials at .credentials/.
uv run pytest tests/test_integration_live.py -m integration -vIntegration tests are skipped by default in normal pytest runs. They:
- Send a test email to the authenticated account (self)
- Send via the dispatch layer (same path MCP clients use)
- Fetch unread emails from the real inbox
- Full roundtrip: send → read back → verify content → trash cleanup
tests/
├── test_guards.py # Input validation (address, subject, body, msg_id)
├── test_vault.py # Credential encryption (seal/unseal, permissions)
├── test_throttle.py # Rate limiter (ceiling, window pruning)
├── test_dispatch.py # Tool dispatch + prompt rendering
├── test_client.py # GmailClient methods (auth, compose, fetch, etc.)
├── test_server_integration.py # MCP server handlers, error paths
├── test_exploratory.py # Boundary values, unicode, edge cases
├── test_bug_probes.py # Targeted security probes (6 categories)
└── test_integration_live.py # Live Gmail API tests (requires credentials)
| Problem | Solution |
|---|---|
FileNotFoundError: Client secrets not found |
Check --creds-file-path points to your downloaded Google JSON file |
RuntimeError: Gmail authentication failed |
Delete token files and re-authenticate (see Authentication Flow above) |
| Browser doesn't open for OAuth | Ensure you're running on a machine with a browser; the server uses localhost redirect |
Throttled error |
Rate limit hit — wait 60 seconds. Limits: 10/min for send/trash, 30/min for reads |
Malformed email address |
Address must match RFC 5322 format, max 254 characters, no control characters |
OAUTHLIB_INSECURE_TRANSPORT warning |
Only appears in dev; production uses HTTPS for token exchange |
| Tokens won't decrypt | The .key file may be corrupted — delete both token files and re-authenticate |
gmail-tools-mcp/
├── src/gmail_tools/
│ ├── __init__.py # CLI entry point (argparse)
│ └── server.py # MCP server (tools, prompts, client, vault, guards)
├── tests/ # 167 unit tests + 4 integration tests
├── .credentials/ # OAuth credentials (gitignored)
├── pyproject.toml # Project config, dependencies, test settings
└── README.md
MIT