multi-gmail-mcp
Provides tools for interacting with multiple Gmail / Google Workspace accounts through the Gmail API. Enables searching and reading emails and threads, managing labels, creating drafts, saving attachments, exporting messages as .eml, sanitized HTML, or PDF, and performing triage actions like archiving and marking read across configured accounts.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@multi-gmail-mcpin my work account, archive all unread newsletters from this week"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
multi-gmail-mcp
A self-hosted MCP server that gives Claude read, triage, draft, export, and label management access to several Gmail / Google Workspace accounts at once, in one connector.
Claude's official Gmail connector supports one Google account per Claude account. This server takes as many as you configure. It runs on your machine over stdio, your mail travels only between your machine and Google's API, and no third-party service sits in the middle.
you, on any device
|
Claude (desktop app / Claude Code / any stdio MCP client)
| MCP · stdio · local-only
v
+--------------------------------------------------------+
| multi-gmail-mcp (Python) |
| search · read · label · archive · draft |
| save attachments · export .eml / .html / PDF |
| create / rename / delete / undo labels (journaled) |
| NO send by default · NO mail deletion, ever |
| per-account encrypted tokens · metadata-only audit |
+--------------------------------------------------------+
| one OAuth token per account (Fernet-encrypted,
| key in the macOS Keychain)
v
account-1 account-2 account-3 ...
\ | /
Gmail API (scope: gmail.modify, which cannot permanently delete)Tools
Every tool takes an explicit account (an alias from your config, or the full address).
There is no default account.
Tool | What it does |
| Configured accounts and whether each is authorized (read-only) |
| Gmail search syntax, |
| One message, plus its attachment list with the indexes |
| A full conversation |
| Label names and ids |
| Add or remove labels on one message, a batch of 2 to 100 messages ( |
| A draft (optionally a threaded reply) that you review and send yourself in Gmail |
| One message's attachments, byte-for-byte, into a folder you configured |
| One message as |
| A new user label (nested with |
| Rename a user label or change its visibility or color; journaled, undoable |
| Delete a user label, never mail; journaled, undoable |
| Revert a journaled |
| Recent journaled label operations and their op ids (read-only) |
The five label-administration tools are registered only while "label_admin": true (the
default). A send_email tool exists but is not registered unless you arm it (below).
What it deliberately can't do
Send mail, by default. Arming send is two-factor:
"enable_send": truein the config file ANDGMAIL_MCP_ALLOW_SEND=1in the server's environment. Google has no OAuth scope that allows drafts without send, so drafts-only is enforced by the server, and a config edit alone (accidental or injected) can never enable sending.Delete mail. There is no mail-deletion tool, the
gmail.modifyscope cannot permanently delete, and addingTRASHorSPAMis refused on every path (single, batch, and thread). Trash auto-purges after about 30 days, so adding it would be deletion on a delay. Removing them (untrash, not-spam) is allowed.Sign itself in. Authorization is
authenticate.py, a CLI a human runs.Touch an account you didn't configure, or write files anywhere except the export folders you named.
Exports
save_attachmentssaves attachments exactly as Gmail stores them:"all"or a list of indexes fromread_email. Inline parts (such as images shown in the body) are saved only withinclude_inline: true.export_emailwrites any of three formats (default:pdfandeml), and each format reports its own result:eml: the exact RAW message bytes Gmail returns;html: a sanitized, policy-wrapped view. Scripts, remote content and active markup are removed, and embeddedcid:images are inlined;pdf: that same view, rendered offline by headless Chrome over a private DevTools pipe. Remote images are never fetched. PDF is off until you enable it (below).
Where files go.
destinationnames a root fromexport_rootsin your config, never a path, andsubfolderis an optional relative folder inside it. The layout is<root>/<alias>-<12 hex of the account address's SHA-256>/<subfolder>/<message id>/, withattachments/,views/, and a.gmail-mcp/manifest.json.How they're written. Files are never overwritten (a numbered name is used instead), are created
0600, carry the macOS quarantine attribute, and are published only after a durable record, so a crash never leaves a half-written file under a final name.Limits. 50 MiB per file, and 50 files or 200 MiB per call. At least 1 GiB of disk is kept free. A message too long for the page limit is refused (
too-many-pages), never truncated.Roots must already exist, be owned by you, and not be group- or world-writable. The server never creates them.
setup.shcreates the configured ones for you.
Label management
update_labelanddelete_labelare journaled before they act, in~/.gmail-mcp-private/label-journal/, and return an operation id forundo_label_change. A changed field is reverted only if it still holds the value the change set; a third state is reported as a conflict, never overwritten.delete_labelneedsconfirm_nameequal to the label's current name. It first counts the Gmail filters that reference the label and refuses unlessconfirm_filter_referencesequals that count. It then snapshots the label's settings and every member message id. A label with more members thanlabel_delete_max_messages(default 10,000) is refused.Undoing a deletion recreates the label with a new id and reapplies the recorded memberships. Filters that pointed at the old id are not repaired.
Refused, for now: renaming or deleting a label that has nested labels (
unsupported-cascade), and case-only name collisions.Not journaled:
modify_labelschanges on a batch or a thread.
Related MCP server: mailpouch
Security model (the short version)
Official libraries only. Anthropic's
mcpSDK, Google's API and auth clients,cryptography, andkeyring. No community MCP code to vet or be poisoned through.Per-account encrypted tokens. Fernet (authenticated encryption), with the key in the macOS Keychain and an atomic
0600key-file fallback. Decryption tries every reachable key, so a locked-Keychain session can't strand your tokens.Strict account isolation. Every call names an account, and the server re-verifies the token's real signed-in address against your config before first use. Export folders and journal entries bind to that verified address, never to an alias.
Prompt-injection fencing. Each result starts with a trusted line that holds only aliases, validated ids, fixed codes and counts. Everything from email, or echoed from arguments, is inside an UNTRUSTED envelope with a per-call random nonce. This is defense in depth, not a guarantee; drafts-only and no-delete are the backstop.
Contained rendering. Chrome runs headless with scripts off, every request intercepted, a dead proxy, a restrictive CSP and resolver rules, in its own process group. Its home, temp and crash folders are inside a private render folder. An independent supervisor enforces the time, memory and scratch-disk budgets and kills the whole group.
Metadata-only audit log. One JSON line per call (tool, alias, ids, counts) in
~/.gmail-mcp-private/audit.log(0600, rotating). Never content, queries, subjects or recipients. If an audit write fails, the result says so.Local only. stdio transport; the server never opens a network port.
Pinned supply chain.
requirements.lockis hash-pinned (pip --require-hashes).
Full guarantees, enforcement layers and residual risks: SECURITY.md.
Requirements
macOS. Reading, search and labels may work on Linux through
keyring's Secret Service backend or the key-file fallback, but that is untested. Exports need macOS, because every exported file must carry the macOS quarantine attribute. FileVault is strongly recommended.Python 3.10 or newer for the server. Python 3.14.6 for the export view, which is the sanitized
.html, the PDF, and inline-image detection:The view relies on eleven CPython 3.14.6 internals, which are fingerprinted.
On any other version they report
stdlib-changed:htmlandpdfexports are unavailable, and inline images are saved as ordinary attachments. Reading, search, labels, attachments and.emlkeep working.Pin the interpreter so an update can't switch the view off, for example Homebrew's
python@3.14withbrew pin python@3.14.
Google Chrome, for PDF only, at
/Applications/Google Chrome.appor whereverpdf.chrome_pathpoints.A Google Cloud project with a Desktop-app OAuth client. It's free, and you create it once (below).
Claude Code or the Claude desktop app as the client, or any stdio MCP client.
Runtime state lives in one private folder, ~/.gmail-mcp-private/ (0700):
accounts.jsonandoauth_client.json;tokens/, holding the encrypted tokens;audit.log;label-journal/;the
locks/,render-gates/andrender-tmp/folders.
GMAIL_MCP_RUNTIME_DIR (an absolute path) moves the whole folder, and GMAIL_MCP_CONFIG
points at a different accounts.json.
Setup: the AI-assisted way (recommended)
This repo ships a CLAUDE.md that briefs Claude Code on the whole setup:
git clone https://github.com/dkaleganov/multi-gmail-mcp
cd multi-gmail-mcp
claudeThen say:
Set up this Gmail MCP server for my accounts.
Claude Code walks you through everything: the Google Cloud project, the OAuth client,
your accounts.json, the export folder, the venv and locked dependencies, each browser
sign-in, registration, and verification. You do the parts only a human should:
creating credentials in the Google console, and clicking Allow once per account. Claude
never sees a secret. Tokens are written encrypted by the sign-in CLI, and the OAuth client
JSON is a file you download yourself.
Setup: the manual way
Google Cloud (once).
At console.cloud.google.com, create a project and enable the Gmail API.
Configure the OAuth consent screen: External, then fill in the app name and your email. Then Publish to production, or refresh tokens expire every 7 days.
Under Credentials, choose Create credentials → OAuth client ID → Application type: Desktop app, then Download JSON.
Place the client file:
mkdir -p ~/.gmail-mcp-private && chmod 700 ~/.gmail-mcp-private mv ~/Downloads/client_secret_*.json ~/.gmail-mcp-private/oauth_client.json chmod 600 ~/.gmail-mcp-private/oauth_client.jsonConfigure your accounts: copy
accounts.example.jsonto~/.gmail-mcp-private/accounts.jsonand put in your own aliases and addresses.Run the setup script:
./setup.shIt does the rest, in order:
picks Python (3.14 if present) and builds a venv;
installs the hash-pinned dependencies;
creates any missing export folder;
opens a browser sign-in for each configured account. Pick the matching inbox. On the "Google hasn't verified this app" screen (it's your app), click Advanced → Continue;
registers the server with Claude Code as
multi-gmail-mcp;verifies the setup and shows the PDF readiness state.
Test: in a new Claude session, ask for
list_accounts.
Google Workspace accounts
If an account is on Google Workspace, the admin may need to allow the app: Admin console →
Security → API controls → App access control, then mark your OAuth client trusted. The
trade-off is that this trusts the client org-wide, so guard your oauth_client.json.
Configuration
accounts.json (see accounts.example.json):
Key | Default | Meaning |
| One entry per account: | |
|
| Half of the two-factor send arming; also needs |
|
| Named export folders (up to 16; names |
|
| Absolute path to Chrome; |
|
| 1 to 1000; a longer message is refused, never truncated |
|
| 10 to 45, for the whole render |
|
| 64 to 65536; must be set, from measurements, before PDF can turn on |
|
| 16 to 65536; same |
|
| 10 to 5000; same |
|
| Registers the five label-administration tools |
|
| 1 to 100000; |
An invalid value never crashes the server. It turns off only the feature it governs and
prints a fixed warning to stderr. For example, an invalid label_admin registers no
admin tools, and an invalid label_delete_max_messages refuses every deletion.
Enabling PDF (optional)
PDF output stays off (pdf-unavailable) until both of these are true.
The renderer release gates pass on this machine's exact Chrome build. The suite starts real Chrome with synthetic fixtures. Its only network target is a local listener on
127.0.0.1/::1. Each protective layer is tested against a positive control: scripts, network, local files, page limit, wide content, supervisor, process containment and resources. PDFs are checked with macOS PDFKit. It takes a few minutes:.venv/bin/python tools/render_gates.py --out .test-tmp/gates-$(date +%Y%m%d) --write-record--write-recordwrites the gate record into~/.gmail-mcp-private/render-gates/, and only if every gate passed.Memory budgets are measured and set. The same run writes
gates.json, and itsresults.resources.measurementssection shows each fixture's peak memory footprint and scratch use on your machine. Then:set
pdf.memory_budget_mbabove the largest peak you want to allow, with headroom;set
pdf.scratch_budget_mbwell above the observed scratch use;set
pdf.monitor_interval_msto a short sampling interval, such as 50.
These are monitored limits, not hard caps: memory can overshoot between two samples before the group is killed.
Then check the state and start a new Claude session:
.venv/bin/python tools/pdf_readiness.pyPDF turns itself off after every Chrome update. The gate record is bound to the
Chrome binary and version, the render recipe, and the renderer's own code. After any
change, readiness reports gates-not-passed until you re-run step 1. The other formats
keep working meanwhile.
Using it
Some examples to ask Claude (home and team are aliases from your config):
"Search team for unread invoices from the last week, and show me the next page too."
"Read that thread and draft a reply saying we'll confirm by Friday."
"Archive everything from that newsletter in home."
"Save the PDF attachment from that message, and export the message itself as
.eml.""Rename the label
Receipts/2025toReceipts/Archive, then undo it."
Drafts land in the account's Drafts folder; you review and send them from Gmail yourself.
Tests
.venv/bin/python -m unittest discover -s tests -t .The suite runs offline against a fake Gmail and a fake Chrome. It uses a throwaway
runtime folder under .test-tmp/ (gitignored) with a Keychain tripwire, so it never
touches your real folder, tokens or Keychain. Run it with Python 3.14.6, the version the
export view is fingerprinted against.
Known limits
A long export or label operation blocks that session's server. That can be up to a 45 s render, plus lock waits of up to 60 s (render, label admin) or 120 s (one message).
Parsing a crafted message is bounded, not cheap. On the export path it can cost up to about 20 s and over 2 GB of memory before the view is refused. Message parsing does not yet run in a supervised child process.
PDF budgets are monitored, not hard caps (see above).
A saved
.emlkeeps the message's original active HTML. Opening it in Mail or previewing it with Quick Look is outside the renderer's controls. Spotlight and Quick Look may also parse files in~/Downloadswithout a click; an export root whose path ends in.noindexavoids indexing.Export roots are shared by all accounts. A synced or shared root gives a prompt-injected session a way to move mail content out of the machine.
Chrome's browser process runs unsandboxed as you. Keep Chrome updated and re-run the gates.
The export view's inline-image prediction is conservative. For very large or unusual messages, some embedded images are saved as attachments even though the view might show them.
Undo can't verify memberships individually. Batch and thread label changes aren't journaled, and
undo_label_changecounts accepted, missing, unresolved and conflicting ids rather than reading each membership back.Pagination tokens are valid only in the server process that issued them. A new session restarts its search.
More machines
Each install is independent, with its own encrypted tokens: set it up on another machine and the two never conflict. Re-run the PDF gates and measurements on each machine.
Kill switch
Disable:
claude mcp remove multi-gmail-mcpRevoke one account: delete
~/.gmail-mcp-private/tokens/<alias>.encand/or revoke the app at https://myaccount.google.com/permissionsRemove everything: delete
~/.gmail-mcp-private/(exported files stay in the folders you configured)
Files to audit
File | What it is |
| The MCP server and the exact tools Claude can call |
| Gmail API wrapper, MIME parsing, label resolution, header-injection guards |
| OAuth loopback flow, encrypted atomic token storage, account validation; the sign-in CLI a human runs |
| Metadata-only audit logging ( |
| Runtime folder, settings validation, the trusted-line and fence contract |
| Attachment enumeration and inline prediction; file-name rules |
| Write controls (pinned roots, no-clobber, quarantine), the export identity and recovery protocol, the export tools |
| The sanitized view |
| Chrome over a pipe, with the supervisor; process inspection |
| The label recovery journal, label administration, host-wide locks |
| The renderer release gates; the readiness report |
| The offline suite |
| One-command install; the brief that lets Claude Code run your setup |
License and author
MIT, see LICENSE. Built by Dima Kaleganov with Claude Code.
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted email MCP for your own Gmail, Outlook.com, Microsoft 365, iCloud or IMAP inbox: read, search, draft, reply in thread, forward and file mail. It moves or flags up to 500 messages in one call, and a send leaves exactly one copy in Sent. A calendar is a separate connection, and connecting one adds diary and scheduling tools.
Email safety MCP server. Detects phishing, prompt injection, CEO fraud for AI agents.
Email infrastructure for AI agents — send, receive, search, and reply to email over MCP.
MCP server connecting AI agents to 100+ apps (Gmail, Slack, Notion, GitHub) via one-click OAuth.
Related MCP Servers
- FlicenseAqualityCmaintenanceLocal-first MCP server for agents that need to work across multiple Gmail and Microsoft 365 accounts without cloud token storage.6-
- AlicenseAqualityAmaintenanceAn MCP server that gives AI agents permission-gated, audit-logged access to private email providers (Proton Mail via Bridge and plain IMAP), running locally with OAuth-based authentication and human-controlled escalation for destructive operations.6646 npm11MIT
- AlicenseNot gradedqualityCmaintenanceA local-first MCP server for managing multiple Gmail accounts with separate OAuth grants, supporting search, retrieval, labels, drafts, and attachments while deferring direct send and destructive actions for safety.MIT
- AlicenseAqualityBmaintenanceMCP server that gives an AI assistant simultaneous access to multiple Gmail accounts with per-account read-only, draft, or send tiers, two-phase write confirmation, and safeguards for handling untrusted email content.1015 npmMIT