# π§ gmail-mcp
> Read, search, send, draft, label, filter, and thread Gmail from any MCP-enabled AI assistant. Wraps the [Gmail API](https://developers.google.com/gmail/api) with scope-gated tools and in-process safeguards.
[](https://github.com/klodr/gmail-mcp/actions/workflows/ci.yml)
[](https://github.com/klodr/gmail-mcp/actions/workflows/codeql.yml)
[](https://vitest.dev)
[](https://codecov.io/gh/klodr/gmail-mcp)
[](https://scorecard.dev/viewer/?uri=github.com/klodr/gmail-mcp)
[](https://www.bestpractices.dev/projects/12613)
[](https://socket.dev/npm/package/@klodr/gmail-mcp)
[](https://coderabbit.ai)
[](https://www.npmjs.com/package/@klodr/gmail-mcp)
[](https://www.npmjs.com/package/@klodr/gmail-mcp)
[](https://nodejs.org)
[](https://modelcontextprotocol.io)
[](https://modelcontextprotocol.io)
[](https://github.com/klodr/gmail-mcp/pulls)
[](https://github.com/sponsors/klodr)
[](https://www.patreon.com/klodr)
[](https://ko-fi.com/klodr)
> [!NOTE]
> Hardened + enhanced fork of [GongRzhe/Gmail-MCP-Server](https://github.com/GongRzhe/Gmail-MCP-Server) (archived 2026-03-03), via [ArtyMcLabin/Gmail-MCP-Server](https://github.com/ArtyMcLabin/Gmail-MCP-Server). Since the divergence point: **180+ commits** and an extensive rewrite β security hardening, Gmail-surface improvements (reply-all, send-as alias, thread-level tools, download-to-disk, recipient pairing, batch ops with retryβ¦), supply-chain hygiene, and CI gating. Every PR goes through CodeRabbit + dual-model Qodo Merge before merge. See [SECURITY.md](.github/SECURITY.md) for the controls and threat model, and the [comparison table](#-why-this-mcp) below for the parent-forks delta.
A Model Context Protocol (MCP) server that lets AI assistants (Claude Desktop, Claude Code, Cursor, Continue, OpenClawβ¦) read and manage a Gmail account through scope-gated tools. Exposes the Gmail v1 API surface you actually need (messages, threads, labels, filters, attachments, drafts, reply-all) behind a single `npx` install.
> [!IMPORTANT]
> **Positioning.** Upstream calls this fork the *maximalist* one. The label is accurate from the outside, but it misframes the intent. `klodr/gmail-mcp` is built around four properties β and the code, tests and CI/CD all exist to pay rent on these and only these:
>
> - **Secure** β input sanitization, recipient pairing, rate limiting, sender resolution, and audit logging are enforced *in the server* (not in the operator's attention). The Gmail surface is mediated, not exposed raw.
> - **Autonomous** β designed to stay safe when no human is watching every action. Different threat model from upstream's "I use it daily in my own workflow"; the in-process middleware is what bridges that gap.
> - **Tested** β 945 vitest cases across 43 files, **99.37%** statement coverage (91% branch), plus a fast-check property-based fuzz suite and a dedicated hardening test file.
> - **Resilient under acceleration** β 17 CI/CD workflows (CodeQL, OSV, Gitleaks, leak-detect, Scorecard, Socket, SLSA, lockfile lint, signed releasesβ¦) track the security ecosystem at the pace it actually changes β days, sometimes hours. Recent concrete instance: **qs CVE-2026-8723**, surfaced and shipped within hours of the public advisory across the klodr/* MCP family.
>
> Full rationale and the per-tool design trade-offs live in [`docs/DESIGN_DECISIONS.md`](docs/DESIGN_DECISIONS.md).
## β¨ Why this MCP?
Comparison of the three maintained forks of the original Gmail MCP server, focusing on what an agent platform actually needs β prompt-injection safety, supply-chain integrity, and operational hygiene:
| Capability | [GongRzhe/Gmail-MCP-Server](https://github.com/GongRzhe/Gmail-MCP-Server) (original, unmaintained) | [ArtyMcLabin/Gmail-MCP-Server](https://github.com/ArtyMcLabin/Gmail-MCP-Server) (intermediate fork) | **klodr/gmail-mcp** (this repo) |
|---|:---:|:---:|:---:|
| **Core Gmail surface** | | | |
| Send / draft / read / search messages | β
| β
| β
|
| Label CRUD | β
| β
| β
|
| Filter CRUD | β οΈ `list_filters` broken | β
fixed | β
|
| Batch modify / delete | β
| β
| β
|
| Reply threading (`In-Reply-To` / `References`) | β orphaned replies | β
| β
|
| Reply-all tool | β | β
| β
|
| Send-as alias (`from` parameter) | β | β
| β
|
| Thread-level tools (`get_thread`, `list_inbox_threads`, `get_inbox_with_threads`) | β | β
| β
|
| Download email to disk (`json`/`eml`/`txt`/`html`) | β | β
| β
|
| Download attachment | β
| β
| β
|
| Download all attachments of a message at once, optionally as one ZIP (original filenames kept) | β | β | β
|
| `read_email` shows the Cc / Bcc lines (to decide between reply and reply-all) | β | β
| β
|
| **OAuth / authorization** | | | |
| `--scopes` flag for least-privilege auth | β | β
| β
|
| Tool list filtered by granted scopes | β | β
| β
|
| OAuth credentials file mode `0o600` | β | β
| β
|
| OAuth loopback callback checked with `state` and PKCE (S256), so an intercepted authorization code cannot be redeemed | β | β | β
|
| A `gcp-oauth.keys.json` left in the project folder is imported only by `auth`, never on a normal start | β copied on every start | β copied on every start | β
|
| **Security β input handling** | | | |
| CRLF header injection sanitization (`\r\n\0`) | β | β οΈ partial | β
|
| Path traversal in `download_attachment` | β | β
fixed | β
|
| Path **jails** (attachment source + download destination) blocking prompt-injected exfiltration | β | β | β
|
| Symlink-safe writes (`O_NOFOLLOW` + post-`mkdir` realpath re-verification, TOCTOU defense) | β | β | β
|
| Zod bounds on `maxResults` / `batchSize` / `messageIds` length | β | β | β
|
| Cryptographic MIME boundary (`crypto.randomBytes`, not `Math.random`) | β | β | β
|
| **Security β outbound mail and prompt injection** | | | |
| Optional recipient allowlist (`GMAIL_MCP_RECIPIENT_PAIRING`): sends, replies, forwards and drafts go only to paired addresses | β | β | β
|
| With the allowlist on, the model cannot extend it through gmail-mcp: the human confirms in the MCP client, or runs `npx @klodr/gmail-mcp pair add` | β | β | β
|
| `send_draft` refuses a draft whose To / Cc / Bcc does not parse as addresses | β (no `send_draft`) | β | β
|
| Write caps per tool family over 24 h and 30 days; batch tools count every message, not every call | β | β | β
|
| Filters that forward mail are refused (a forwarding rule silently redirects all future matching mail) | β `create_filter` accepts `action.forward` | β `create_filter` accepts `action.forward` | β
refused, with the reason |
| Downloaded files marked as coming from the internet (macOS quarantine, Windows Mark of the Web), as Mail and Outlook do | β | β | β
|
| Exported HTML: scripts, frames and forms blocked; remote images, stylesheets, fonts and media (tracking pixels) blocked unless asked for | β (no HTML export) | β no CSP | β
|
| **MCP protocol & tool surface** | | | |
| MCP SDK version | v0.4.x | v1.27.x | v1.32.x |
| Tool annotations (`readOnlyHint` / `destructiveHint` / `idempotentHint`) | β | β
| β
|
| `llms-install.md` (LLM-readable install guide) | β | β | β
|
| **Publishing / discoverability** | | | |
| Published on npm | β stale β no future releases (repo archived) | β (consumed as a GitHub install from the intermediate fork) | β
dedicated scoped package, signed releases |
| Active maintenance (last 30 d) | β (archived 2026-03-03) | β οΈ sporadic | β
daily review cycle (CodeRabbit + human) |
| **Supply-chain integrity** | | | |
| Node.js floor | β `>=14` ([EOL April 2023](https://nodejs.org/en/about/previous-releases)) | β `>=14` ([EOL April 2023](https://nodejs.org/en/about/previous-releases)) | β
`>=24.18.1` (tested on Node 24 and 26) |
| CI: CodeQL Advanced (`javascript-typescript` + `actions`) | β | β | β
|
| CI: OpenSSF Scorecard (weekly scan + badge) | β | β | β
|
| CI: Socket Security supply-chain alerts | β | β | β
|
| CI: CodeRabbit assertive reviews on every PR | β | β | β
|
| Release: signed builds (Sigstore + SLSA in-toto attestation + npm provenance) | β | β | β
|
| Release: single-file ESM bundle | β | β | β
|
| **Testing** | | | |
| Unit/property tests | β (0 tests) | β οΈ (97 tests) | β
(**945 tests**, 43 files) |
| Statement coverage across `src/**` | 0% | 16.14% | **99.37%** (91% branch) |
| Fast-check property-based fuzz suite | β | β | β
|
| Hardening-specific test file (jails, CRLF, O_EXCL) | β | β | β
|
| **CI/CD hardening** | | | |
| Shell-injection-safe GitHub Actions workflows | β | β
| β
|
| Workflows use least-privilege `permissions:` scopes | β | β
| β
|
| All GitHub Actions pinned by full commit SHA | β | β | β
|
| **Operational** | | | |
| `CHANGELOG.md` (Keep-a-Changelog) | β | β | β
|
| `SECURITY.md` (vulnerability reporting) | β | β | β
|
`klodr/gmail-mcp` is the only one of the three with **(a)** source-path jails that make prompt-injection attachment exfiltration inert, **(b)** outbound guard rails a hostile email cannot talk the model past β an optional recipient allowlist the model cannot extend through gmail-mcp, no forwarding filters, write caps that count every message β **(c)** a modern supply chain (Scorecard, Socket, Sigstore), and **(d)** an in-repo review policy (`.coderabbit.yaml`) that every PR must pass before merge.
Fork columns checked against the code of [GongRzhe `a890d19`](https://github.com/GongRzhe/Gmail-MCP-Server/commit/a890d19) (2025-08-06, archived) and [ArtyMcLabin `83e8daf`](https://github.com/ArtyMcLabin/Gmail-MCP-Server/commit/83e8daf) (`main`, 2026-08-12).
## π¦ Installation
```bash
npm install -g @klodr/gmail-mcp
```
Or directly via `npx`:
```bash
npx -y @klodr/gmail-mcp
```
Requires **Node.js 24.18.1+** (tested on Node 24 and 26).
## βοΈ Configuration
### 1οΈβ£ Google Cloud OAuth credentials
1. Open the [Google Cloud Console](https://console.cloud.google.com/).
2. Create a project and enable the **Gmail API**.
3. Under **APIs & Services β Credentials**, create an **OAuth 2.0 Client ID** (Desktop or Web). For Web, add `http://localhost:3000/oauth2callback` to the authorized redirect URIs.
4. Download the JSON, rename it to `gcp-oauth.keys.json`, place it at `~/.gmail-mcp/gcp-oauth.keys.json` (or override with `GMAIL_OAUTH_PATH=/abs/path/gcp-oauth.keys.json`).
### 2οΈβ£ Authenticate (once)
```bash
npx -y @klodr/gmail-mcp auth --scopes=gmail.readonly
```
Always pass `--scopes` with the minimum you actually need β the MCP filters the tool list at startup based on the granted scopes, so a read-only token doesn't expose write tools to the LLM. A browser opens for Google's consent flow; tokens are written to `~/.gmail-mcp/credentials.json` (mode `0o600`).
### 3οΈβ£ Register the server with your MCP client
```json
{
"mcpServers": {
"gmail": {
"command": "npx",
"args": ["-y", "@klodr/gmail-mcp"]
}
}
}
```
Client-specific config file:
- **Claude Code**: `~/.claude.json`
- **Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) / `%APPDATA%\Claude\claude_desktop_config.json` (Windows)
- **Cursor**: `~/.cursor/mcp.json`
- **OpenClaw**: `~/.openclaw/openclaw.json`
See [llms-install.md](./llms-install.md) for an LLM-readable install guide.
## π OAuth scopes
| Scope shorthand | Full Gmail scope | What it grants |
|---|---|---|
| `gmail.readonly` | `β¦/auth/gmail.readonly` | Read messages, threads, labels (filter tools require `gmail.settings.basic`) |
| `gmail.modify` | `β¦/auth/gmail.modify` | Readonly + apply/remove labels, delete messages |
| `gmail.compose` | `β¦/auth/gmail.compose` | Create drafts |
| `gmail.send` | `β¦/auth/gmail.send` | Send messages |
| `gmail.labels` | `β¦/auth/gmail.labels` | Manage labels only |
| `gmail.settings.basic` | `β¦/auth/gmail.settings.basic` | Manage filters |
Recipes:
```bash
# Read-only browsing
npx @klodr/gmail-mcp auth --scopes=gmail.readonly
# Read + send (mailing-list bot)
npx @klodr/gmail-mcp auth --scopes=gmail.readonly,gmail.send
# Everything (default; explicit)
npx @klodr/gmail-mcp auth --scopes=gmail.modify,gmail.settings.basic
# Default + permanent delete (delete_email / batch_delete_emails)
# gmail.modify authorizes trash; mail.google.com is the only scope
# that authorizes purging from Trash. Both are listed because the
# tool gate does exact scope-name matching β a token holding only
# mail.google.com would not enable the gmail.modify-gated tools,
# even though Google's scope hierarchy would technically accept the
# same calls.
npx @klodr/gmail-mcp auth --scopes=gmail.modify,mail.google.com,gmail.settings.basic
```
## π‘οΈ Safeguards
### In-process middleware (autonomy enforcement)
The runtime constraints that an autonomous LLM client cannot violate are enforced *in the server*, not in the operator's attention span. Seven in-process modules sit on the tool entry path; the ones that are opt-in only activate when the matching env var is set.
**Always-on** (active for every call out of the box):
- **`sanitize.ts`** β strips/escapes input the LLM provides (CRLF injection, control characters, NUL bytes) before it reaches Gmail headers, filenames, or filesystem paths.
- **`sender-resolver.ts`** β resolves the `from` parameter against the account's authorized send-as aliases only. Spoofing the sender field is rejected before the Gmail API send call.
- **`rate-limit.ts`** β bounds write volume per tool family (`send`, `delete`, `modify`, `drafts`, `labels`, `filters`, `pairing`) over rolling daily/monthly windows. The batch tools (`batch_modify_emails`, `batch_delete_emails`, `move_to_spam_batch`) count each message they touch, so one call on 1 000 messages uses 1 000 units, not 1. The state is persisted to disk so it survives restarts. Disable with `GMAIL_MCP_RATE_LIMIT_DISABLE=true` for test runs only.
- **`quarantine.ts`** β marks every file written by `download_attachment`, `download_all_attachments` and `download_email` as downloaded from the internet, as Mail and Outlook do: the `com.apple.quarantine` attribute on macOS (Gatekeeper checks an executable or script when it is opened), the Mark of the Web on Windows; nothing on Linux. Never blocking: if the mark cannot be set, the file is still saved. Off with `GMAIL_MCP_QUARANTINE=false`.
- **Forwarding filters refused** (`filter-manager.ts`) β `create_filter` and the filter templates never install a rule that forwards mail: such a rule silently redirects every future matching message, and Gmail forwards only to a verified forwarding address, whose registration needs the `gmail.settings.sharing` scope β not in the default scopes and used by no tool here; the refusal holds even if `--scopes` grants it. Create those in the Gmail web interface.
- **Exported HTML** (`email-export.ts`) β the file `download_email` writes in `html` format carries a Content-Security-Policy blocking scripts, plugins, frames, `