gmail-mcp
gmail-mcp
An MCP server that gives an AI assistant several Gmail accounts at once — read and draft only. It cannot send, it cannot delete, and it cannot reach anything but the Gmail API.
Every tool takes a required account parameter; list_accounts shows what is
configured. Adding another account is an OAuth flow at runtime, not a redeploy.
What it cannot do, and where the guarantee actually lives
How it is prevented | |
Send | No send tool. No code path reaches |
Delete | Not offered at all. Gmail's |
Change filters or forwarding |
|
Touch Drive, Docs, Sheets, Calendar | Those scopes are not requested. |
Contact anything but Google | Three constant hosts, one outbound function, |
One thing you must know, because it is a real limit and not a detail.
Google has no draft-only scope. gmail.compose is the narrowest scope that
can create a draft, and at the API level it also permits sending. So the grant
this server holds could send; the reason it cannot is that this server offers
no send tool and no code path that reaches the send endpoint. That is the same
shape of guarantee the Proton server gives, and it is worth stating plainly
rather than implying that Google enforces it.
The second consequence of staying narrow: this server cannot mark anything as
read. Changing labels needs gmail.modify, which also grants send. That was
the wrong trade, so reading leaves the mailbox untouched.
Requirements
Node.js 20 or newer
Your own Google OAuth client (below)
What you have to do in the Google Cloud Console
This part cannot be automated — it is your account and your consent screen.
Create or pick a project. console.cloud.google.com → project selector → New project. Any name.
Enable the Gmail API. APIs & Services → Library → search "Gmail API" → Enable. Enable nothing else; this server uses nothing else.
Configure the consent screen. APIs & Services → OAuth consent screen.
User type: External (unless every mailbox is in a Workspace you own, then Internal).
Fill in app name and your own address; nothing else is required.
Scopes: add exactly these two, and no others:
https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.composeTest users: add every Gmail address you intend to connect. While the app is in Testing, only listed addresses can authorise it, and refresh tokens expire after 7 days. For a permanent setup either publish the app (Google will ask for verification because Gmail scopes are sensitive) or keep it in Testing and re-authorise weekly. Decide this before you rely on it — a token that quietly dies after a week is the kind of failure that goes unnoticed.
Create the client. APIs & Services → Credentials → Create credentials → OAuth client ID → Desktop app. Download the JSON.
Install it:
mkdir -p ~/.config/gmail-mcp install -m 0600 ~/Downloads/client_secret_*.json ~/.config/gmail-mcp/oauth-client.json chmod 700 ~/.config/gmail-mcpThe server refuses to start from a file that group or others can read.
If you use a Web application client instead of Desktop, add your redirect
URI there and set GMAIL_MCP_REDIRECT_URI to the same value.
Adding an account
Ask the assistant, or call the tools directly:
begin_account_authwithaccount: "privat"— a short id you choose. It returns a Google consent URL.Open the URL as the owner of that mailbox and approve.
Your browser will then show an error — "This site can't be reached", "connection refused", or a blank page. That is expected and is not a failure. Nothing listens on the redirect address; the part that matters is in the address bar:
http://localhost/?state=privat&code=4/0AX4X...&scope=... ^^^^^^^^^^^^^^ thisCopy the value of
code=, up to the next&.finish_account_authwith the sameaccountand thatcode. The code is single-use and expires within minutes, so do this straight away; if it fails, start again at step 1.
The redirect address is taken from your client file, because it has to match
what Google registered for that client to the character — a mismatch shows up
as redirect_uri_mismatch on the consent screen. GMAIL_MCP_REDIRECT_URI only
applies if the file names none.
The refresh token is written to ~/.config/gmail-mcp/accounts/<id>/token.json
with mode 0600 in a 0700 directory, atomically. The account's address is
read back from Gmail and stored in meta.json; list_accounts shows it.
Repeat for each mailbox. Ids must match [a-z0-9][a-z0-9._-]{0,63}.
To revoke: delete the account directory, and remove the app at myaccount.google.com/permissions for that address. Deleting the file alone leaves the grant standing on Google's side.
The recipient rule
A reply draft is only created if the message was addressed to that account
(To or Cc). If it went somewhere else, the call is refused and both
addresses are named. It fails closed: if the original cannot be read, no draft
is created.
Only To and Cc are examined. Delivered-To and X-Original-To carry the
delivering mailbox rather than the address the mail was sent to, and checking
those would let through exactly the cases this rule is about.
Running it
Locally (stdio)
claude mcp add gmail --scope user -- /path/to/gmail-mcp/src/stdio.mjsClaude Code reads ~/.claude.json, not claude_desktop_config.json.
Over HTTP
mkdir -p ~/.config/gmail-mcp
install -m 0600 /dev/null ~/.config/gmail-mcp/http-token
openssl rand -hex 32 > ~/.config/gmail-mcp/http-tokenThen run src/http.mjs, or install systemd/gmail-mcp.service. It binds to
127.0.0.1 only; exposing it is your job and your risk — put it behind a
tunnel or an authenticated proxy you control.
The endpoint takes a static bearer token and has no OAuth: it answers 404
on /.well-known/* and sends no WWW-Authenticate, because a client that sees
either will try dynamic client registration and fail confusingly. An unknown
session id answers 404 rather than 400, so a client re-initializes instead
of retrying a session that a restart threw away.
Configuration
Variable | Default |
|
|
|
|
| the client file's own value, else |
|
|
|
|
|
|
Tools
list_accounts, begin_account_auth, finish_account_auth, labels_list,
message_search, message_read, thread_read, draft_list, draft_read,
draft_create, draft_reply, attachment_download.
One-time codes
Reading mail means confirmation codes reach the model. src/otp-filter.mjs
masks them first — copied verbatim from the protonmail-mcp repo, where it also
carries a differential test against the Python original it was ported from.
Gmail's opaque ids are exempt by field name and shape, because destroying
them makes every follow-up call impossible.
Layer 2 blanks the whole string when text merely announces a code, so a reply preview quoting such a message can come back empty. The draft is still created correctly.
Tests
npm test41 cases, no network and no Google account required: global fetch is replaced
via node --import, so the production build has no switch for redirecting its
own outbound door. Four mutations are checked by hand and each is caught by
exactly one case — recipient rule off, send path allowed, header check removed,
tokens written world-readable.
Why this is not tszaks/gmail-multi-inbox-mcp
That project was the starting point and was reviewed first. It requests
https://mail.google.com/ (full Gmail including permanent delete),
gmail.settings.basic (which can create forwarding addresses), full Drive,
Sheets, Docs and Calendar; it ships send_email, send_draft, trash_emails,
share_drive_file and 55 more tools; its HTTP mode binds 0.0.0.0 with no
authentication at all; it writes tokens and the client secret with a plain
writeFile and contains no chmod anywhere; and unsubscribe_from_email
POSTs to a URL taken from the List-Unsubscribe header — an address chosen by
whoever sent the mail, http:// included, with no allowlist and no guard
against private ranges.
None of that is a criticism of its goals; it is simply a different tool. This one is written to a narrower brief.
License
MIT