Skip to main content
Glama
arkaigrowth

Gmail MCP Server

by arkaigrowth
README.md
# Gmail MCP Server

Multi-account Gmail integration for AI assistants via the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP).

> **Give your AI assistant secure access to all your Gmail accounts** -- search, read, triage, draft, label, archive, trash-stage, and send emails with per-account routing, macOS Keychain token storage, and two-step confirmation flows for sensitive actions.

---

## The Problem

Built-in Gmail integrations for AI assistants have significant limitations:

- **Single account only** -- no way to switch between personal, work, and business inboxes
- **Insecure token storage** -- most open-source servers store OAuth tokens as plaintext JSON files
- **Overly broad scopes** -- many request full `mail.google.com` access (read, write, delete, send)
- **No send protection** -- an AI can compose _and send_ an email in a single step with no human checkpoint

## What This Server Does Differently

| Concern | This Server | Typical MCP Gmail Server |
|---------|------------|--------------------------|
| **Accounts** | Unlimited, with aliases (`personal`, `work`) | Single account |
| **Token storage** | macOS Keychain (encrypted at rest) | Plaintext JSON files |
| **OAuth scopes** | 4 scoped Gmail scopes plus scoped Docs/Drive (9 total, no `mail.google.com`) | `mail.google.com` (full access) |
| **Sending** | Two-step: stage draft, then confirm with a 60-second code | Direct send, no confirmation |
| **Trash/bulk changes** | Two-step staged confirmation | Often direct mutation |
| **Credentials** | Bring your own Google Cloud OAuth client | Often bundled (security risk) |
| **Shell injection** | `execFileSync` with argument arrays | `execSync` with string interpolation |

## Features

- **35 tools** for serious Gmail and Workspace workflows: search/read, batch read, thread reads, draft create/update/list, forwarding, labels, filters, archive, staged trash, staged bulk labeling, profile, attachments, two-step send, Google Docs/Drive reads, and scoped Drive writes
- **Multi-account routing** -- every tool accepts an optional `account` parameter (email or alias)
- **Active account** -- set a default account that persists across restarts
- **Rich draft bodies** -- create/update drafts can include raw `html_body` with a plain-text fallback for Gmail-safe formatted replies
- **Safe modify** -- label/archive/trash operations require explicit message IDs or staged query preview/confirmation
- **Attachment handling** -- downloads to a sandboxed quarantine directory with SHA-256 integrity hashes and read-only permissions
- **Minimal dependencies** -- only the official Google APIs, MCP SDK, and Zod

## Quick Start

### Prerequisites

- **Node.js** 18+ (tested on 22)
- **macOS** (uses Keychain for token storage)
- A [Google Cloud project](#google-cloud-setup) with OAuth credentials

### Install & Build

```bash
git clone https://github.com/arkaigrowth/gmail-mcp-server.git
cd gmail-mcp-server
npm install
npm run build
```

### Smoke Test

```bash
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/index.js
```

You should see a JSON response listing all 35 tools.

### Add to Your AI Assistant

**Claude Code** (`~/.claude/settings.json` under `mcpServers`):

```json
{
  "gmail": {
    "command": "node",
    "args": ["/absolute/path/to/gmail-mcp-server/dist/index.js"]
  }
}
```

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "gmail": {
      "command": "node",
      "args": ["/absolute/path/to/gmail-mcp-server/dist/index.js"]
    }
  }
}
```

### Authenticate Your First Account

Once the MCP server is connected, tell your AI assistant:

> "Authenticate my Gmail account user@gmail.com with alias personal"

This will:
1. Open your browser to Google's consent screen
2. Store the OAuth refresh token in your macOS Keychain
3. Register the account for future use

You can then immediately start using it:

> "Search my personal inbox for emails from GitHub in the last week"

---

## Google Cloud Setup

You need a Google Cloud OAuth client (type: "Desktop app") with the Gmail API enabled. Each Google account needs its own OAuth client from a project where that account has access.

### Step-by-step

1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project (or use an existing one)
3. Enable the **Gmail API**: [APIs & Services > Library](https://console.cloud.google.com/apis/library) > search "Gmail API" > Enable
4. Configure the **OAuth consent screen**: [APIs & Services > OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent)
   - User type: **External** (or Internal if using Google Workspace)
   - Add the Gmail scopes: `gmail.readonly`, `gmail.compose`, `gmail.modify`, `gmail.settings.basic`
   - Add the Workspace scopes used by the Docs/Drive tools: `documents.readonly`, `documents`, `drive.metadata.readonly`, `drive.readonly`, `drive.file`
   - Add your email as a test user (while in "Testing" status)
5. Create **OAuth credentials**: [APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials) > Create Credentials > OAuth client ID
   - Application type: **Desktop app**
   - Download the JSON file
6. Save the JSON file as `~/.gmail-mcp/client_secret.json`

> **Tip**: If you have multiple Google accounts, you can specify a per-account path via the `client_secret_path` parameter when authenticating.

### Credential File Lookup Order

1. `client_secret_path` parameter passed to `gmail_authenticate`
2. Path stored in the account's config (`accounts.json`)
3. `GMAIL_MCP_CLIENT_SECRET_PATH` environment variable
4. `~/.gmail-mcp/client_secret.json` (default)

---

## Tool Reference

### Account Management

| Tool | Description |
|------|-------------|
| `gmail_list_accounts` | List all configured accounts and their auth status |
| `gmail_switch_account` | Set the active default account (persisted across restarts) |
| `gmail_authenticate` | OAuth flow -- opens browser, stores token in Keychain |

### Email Operations

| Tool | Description |
|------|-------------|
| `gmail_search_messages` | Legacy search using Gmail query syntax (`from:alice is:unread`) |
| `gmail_search_emails` | Paginated rich search with messages, `nextPageToken`, and estimate |
| `gmail_read_message` | Read a specific message with full body content |
| `gmail_batch_read_messages` | Read explicit shortlisted message IDs, with partial failures |
| `gmail_read_thread` | Read a conversation thread, optionally capped with `max_messages` |
| `gmail_list_labels` | List all labels (system + custom) with message counts |
| `gmail_get_profile` | Get mailbox metadata (message/thread counts) |

### Labels, Archive, Trash

| Tool | Description |
|------|-------------|
| `gmail_create_label` | Create a label, or return an existing label by name |
| `gmail_apply_labels_to_messages` | Add/remove labels on explicit message IDs only |
| `gmail_archive_messages` | Archive explicit message IDs by removing `INBOX` |
| `gmail_stage_trash_messages` | Stage explicit messages for Trash and return a confirmation code |
| `gmail_confirm_trash_messages` | Confirm staged Trash operation; account-bound, short expiry |
| `gmail_prepare_bulk_label_query` | Preview and stage capped query-based bulk labeling |
| `gmail_confirm_bulk_label_operation` | Confirm a staged bulk label operation |

### Gmail filters

| Tool | Description |
|------|-------------|
| `gmail_create_filter` | Create a filter from criteria and label actions, with optional existing label-name resolution |
| `gmail_list_filters` | List filter IDs, criteria, and actions |
| `gmail_delete_filter` | Delete exactly one filter by ID |

Filter label names must already exist. The server returns available label names when a requested name cannot be resolved, and does not auto-create labels.

### Compose & Send

| Tool | Description |
|------|-------------|
| `gmail_create_draft` | Create a draft email (does not send) |
| `gmail_list_drafts` | List draft metadata |
| `gmail_update_draft` | Update an existing draft; rejects drafts with attachments in v1 |
| `gmail_create_forward_draft` | Create draft forwards; quotes original content inline |
| `gmail_send_draft` | Stage a draft for sending -- returns a confirmation code |
| `gmail_confirm_send` | Confirm and send with the code (60-second expiry) |

### Attachments

| Tool | Description |
|------|-------------|
| `gmail_download_attachment` | Download to quarantine directory with SHA-256 hash |

> All email tools accept an optional `account` parameter (email or alias). If omitted, the active default account is used. If no default is set and only one account exists, it's auto-selected.

Draft bodies are plain text by default. For rich Gmail rendering, pass `html_body` with `body` as the plain-text fallback; the server emits `multipart/alternative` with `text/plain` first and `text/html` second. You can also set `content_type: "text/html"` to treat `body` as raw HTML when no separate fallback is available. Outbound attachments are not supported in draft create/update yet.

---

## Security

### Token Storage

OAuth tokens (refresh tokens, client credentials) are stored **exclusively** in the macOS Keychain using the `security` CLI. No plaintext token files are ever written to disk.

```
Keychain Service: gmail-mcp-server
Keychain Account: <your-email-address>
```

The `~/.gmail-mcp/accounts.json` config file stores only metadata (email, alias, description, credential file path) -- never secrets.

### OAuth Scopes

The consent flow requests one combined set of scopes (`REQUIRED_AUTH_SCOPES` in `src/scopes.ts`). It covers Gmail plus the Google Docs and Drive tools this server also exposes (reading shared docs and specs, and writing scoped Docs/Drive files), so the Workspace scopes are requested in the same flow.

**Gmail scopes**

| Scope | Grants |
|-------|--------|
| `gmail.readonly` | Read messages, threads, labels, profile |
| `gmail.compose` | Create and send drafts |
| `gmail.modify` | Label, archive, trash, and other safe mailbox state changes |
| `gmail.settings.basic` | Create, list, and delete Gmail filters |

**Google Docs / Drive scopes**

| Scope | Grants |
|-------|--------|
| `documents.readonly` | Read the text of Google Docs the account can open |
| `documents` | Create and edit Google Docs the account can access |
| `drive.metadata.readonly` | Read Drive file metadata (names, types), no file content |
| `drive.readonly` | Read and download the content of any Drive file the account can access, including Office files shared by teammates |
| `drive.file` | Create and manage the Drive files this server creates or opens |

The Gmail scopes are intentionally narrower than `mail.google.com` full access: the server does not request `gmail.send`; direct `messages.send` is blocked and all sends must go through draft staging plus confirmation. The Workspace scopes add Docs/Drive read and scoped-write access on top of that.

The scopes are defined as grouped constants in `src/scopes.ts` (`REQUIRED_GMAIL_SCOPES`, `REQUIRED_WORKSPACE_READ_SCOPES`, `REQUIRED_WORKSPACE_WRITE_SCOPES`). If you only need the Gmail tools, remove the Workspace groups from `REQUIRED_AUTH_SCOPES` and rebuild; the consent flow then requests only the Gmail scopes and the Docs/Drive tools stop working.

Tokens store the granted scope list in Keychain metadata. Tokens granted before a scope was added produce a clear reauth-required error for the tools that need it; rerun `gmail_authenticate` for that account.

> **Re-authentication required:** Adding `gmail.settings.basic` means each account must rerun `gmail_authenticate` and complete the consent flow before filter tools work. Existing tokens keep working for old tools, but filter calls fail with insufficient scopes until re-authentication.

### Two-Step Send Confirmation

Sending email requires two separate tool calls:

1. **`gmail_send_draft`** -- stages the draft and returns a 16-character cryptographic confirmation code (valid for 60 seconds)
2. **`gmail_confirm_send`** -- the AI must present the code back, and the account must match

Additionally, `messages.send` is explicitly blocked on the Gmail client -- all sends must go through the draft confirmation flow.

### Two-Step Trash And Bulk Label Confirmation

Trash and query-based bulk label operations use short-lived, account-bound confirmation codes. Explicit message ID label/archive operations are direct because their targets are already selected; query-based bulk labeling always previews before mutation.

### OAuth Callback Security

- Callback server binds to `127.0.0.1` only (never `0.0.0.0`)
- Random `state` parameter validated to prevent CSRF
- Callback server auto-terminates after 5 minutes by default (override via `GMAIL_MCP_AUTH_TIMEOUT_MS`)
- Dynamic port allocation (no fixed ports)

### Attachment Quarantine

Downloaded attachments are:
- Saved to `~/.gmail-mcp/quarantine/<messageId>/` with `0o700` directory permissions
- Written with `0o400` (read-only) file permissions
- Filename sanitized to prevent path traversal
- Accompanied by a SHA-256 hash for integrity verification

### Shell Injection Prevention

All calls to the macOS `security` CLI use `execFileSync` with argument arrays, not `execSync` with string interpolation. This prevents shell injection via crafted email addresses or token data.

---

## Multi-Account Examples

### Setup two accounts

```
"Authenticate user@gmail.com with alias personal"
"Authenticate work@company.com with alias work, client_secret_path ~/.gmail-mcp/work_client_secret.json"
```

### Switch between them

```
"Switch to my work account"
"Search work for emails from the finance team this month"
"Switch to personal"
"Read my latest emails"
```

### Or specify per-call

```
"Search my personal inbox for order confirmations"
"Search my work inbox for meeting invites tomorrow"
```

---

## File Structure

```
~/.gmail-mcp/
  accounts.json          # Account metadata (no secrets)
  defaults.json          # Active account preference
  client_secret.json     # Default OAuth client credentials (from Google)
  quarantine/            # Downloaded attachments (sandboxed)
    <messageId>/
      attachment.pdf     # Read-only (0o400)
```

```
macOS Keychain
  Service: gmail-mcp-server
  Account: user@gmail.com    # Token JSON, including granted OAuth scopes
  Account: work@company.com  # Token JSON, including granted OAuth scopes
```

---

## Development

```bash
# Build
npm run build

# Run in development (with tsx)
npm run dev

# Run tests
npm test

# Smoke test (JSON-RPC)
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/index.js
```

### Project Structure

```
src/
  index.ts          # Entry point: MCP server + stdio transport
  tools.ts          # 35 tool registrations with Zod schemas
  gmail-client.ts   # Gmail API wrapper (search, read, draft, labels, filters, archive, trash, send, attachments)
  accounts.ts       # Account config management (load, resolve, persist)
  keychain.ts       # macOS Keychain integration via security CLI
  oauth.ts          # OAuth browser flow with localhost callback
  scopes.ts         # Gmail OAuth scope constants and metadata checks
  types.ts          # Shared TypeScript interfaces
skills/
  gmail-multi/      # Repo-local workflow guidance for agents using this server
```

---

## Platform Support

Currently **macOS only** due to the Keychain dependency. Contributions for Linux (`secret-tool` / `libsecret`) and Windows (`wincred`) credential storage are welcome.

---

## License

MIT

TDQS

B3.3/5.0

Scored across 35 tools

Disambiguation4/5

Most tools have clearly distinct purposes, but there is some overlap between gmail_search_messages and gmail_search_emails, and between archive/trash workflows. Descriptions help differentiate, but ambiguity remains for agents.

Naming Consistency5/5

All tools follow a consistent verb_noun snake_case pattern with domain prefixes (gmail_, drive_, docs_). Naming is predictable and readable across the entire set.

Tool Count3/5

35 tools is high, but it combines Gmail and Google Drive/Docs functionality. The count could be reduced by merging similar operations (e.g., staging/confirmation pairs), but it remains within acceptable bounds for the combined scope.

Completeness3/5

Common Gmail operations are covered (search, read, send, labels, filters, archive, trash), but missing permanent deletion and direct marking of read/unread. Drive/Docs coverage is basic (search, metadata, read, create, upload) but lacks editing and broader file operations.

Maintenance

ActivityStale
ResponsivenessNo issues