Gmail MCP Connector
A production-ready Model Context Protocol server that exposes Gmailread/search/send/draft capabilities as tools, built with FastMCP and theGmail REST API.
Package layout
server.py # entry point: registers tools, runs stdio or HTTP transport
gmail_connector/
__init__.py
auth.py # OAuth2 credential lifecycle only
errors.py # shared error handling (run_safely decorator) only
tools.py # the MCP tool functions only
compose.py # template-based subject/body drafting for compose_email
notifications.py # background polling for notifications_alter
requirements.txt
Dockerfile # containerizes the connector (HTTP transport)
docker-compose.yml # connector + MCP Inspector + custom Tools-only web UI
inspector/Dockerfile # containerizes the official MCP Inspector
web-ui/ # custom Tools-only browser UI (FastAPI backend + static page)
app.py # proxies MCP tool calls from the browser to gmail-connector
static/index.html # self-contained frontend: pick a tool, fill fields, run it
Dockerfile
Tools exposed
| Tool | Purpose |
|---|---|
list_recent_emails(max_results=10, label="INBOX", from_email=None) |
List recent emails in a label, or from one specific sender when from_email is given, with sender/subject/date/snippet. |
search_emails(query="", max_results=10, from_email=None) |
Search using Gmail query syntax (from:, subject:, after:, is:unread, ...); from_email is combined with query if both are given. |
reply_to_email(from_email, body) |
Immediately sends a threaded reply to the most recent email from from_email (same subject/thread, proper In-Reply-To/References headers) — no message ID lookup needed. |
modify_email(from_email, add_labels=None, remove_labels=None) |
Adds/removes Gmail labels on the most recent email from from_email — no message ID needed. Requires the gmail.modify scope — see the note below. |
trash_email(from_email) |
Moves the most recent email from from_email to Trash — recoverable for 30 days via restore_email. Requires the gmail.modify scope. |
restore_email(from_email) |
Restores the most recent trashed email from from_email back to the Inbox. Requires the gmail.modify scope. |
forward_email(from_email, to_email, body=None) |
Immediately sends the most recent email from from_email, forwarded to to_email, with the original sender/date/subject/body embedded and your optional body note prepended. |
send_email(to, subject, body, cc=None) |
Immediately sends a real, new email with a caller-supplied subject/body. |
create_draft(to, subject, body) |
Creates a draft without sending. |
compose_email(to, topic, cc=None) |
Generates a subject and body from a short topic using a fixed template (no external API), then immediately sends. |
notifications_alter() |
Read-only. Starts a background poller (on first call) that watches for any new email in Inbox or Spam — brand-new conversations and replies alike — and returns/clears whatever's queued since your last call. Never sends, replies, modifies, archives, labels, or deletes anything. |
Both listing tools accept max_results up to 200 and paginate internally past Gmail's 100-per-page cap, sothe caller never has to think about nextPageToken.
Every tool above also accepts an account parameter (default "default") to select which authenticatedGmail account to operate on — see Multiple Gmail accounts below.
compose_email is for when the caller only wants to give a recipient and a one-line description ofwhat the email should say (e.g. "let the team know the deploy is delayed"), rather than writing an exactsubject and body. The subject is derived from the topic text itself, and the body wraps it in a plain"Hi, ... Best regards," template — there's no AI rewriting and no API key required, so the output mirrorstopic fairly literally rather than reading as naturally composed prose.
modify_emailneeds a broader OAuth scope. It usesgmail.modify, in addition to thegmail.readonly/gmail.send/gmail.composescopes the other tools use. If you authenticated before thistool existed, your cachedtoken.jsononly covers the old scopes andmodify_emailwill fail with apermission error until you re-run the consent flow (delete the account's token file, or run the bootstrapsnippet from First-run authentication again) so it's re-issued with the newscope included.
notifications_alter works by polling, not true push notifications — a public HTTPS webhook plusa Google Cloud Pub/Sub topic would be needed for real push, which isn't practical for a local/privatedeployment. Instead, the first call for a given account starts a background thread (inside the same serverprocess) that checks for new inbox mail roughly every 2 minutes. Every message is tracked by ID so it's onlyever reported once, even across overlapping poll windows or repeated calls. Matches queue in memory; callingthe tool again drains and returns whatever's accumulated since your last call. Because state is in-memoryonly, it resets if the container/process restarts, and polling only starts once something has actually calledthis tool at least once (it doesn't run automatically at server startup).
1. Google Cloud project & OAuth consent screen setup
- Go to the Google Cloud Console and create a new project (or select anexisting one).
- In the left nav, go to APIs & Services → OAuth consent screen.
- User type: External (unless you have a Google Workspace org and want Internal).
- Fill in app name, support email, and developer contact email.
- Scopes: you can add them here or let the OAuth flow request them at runtime; either way the connectorrequests exactly:
https://www.googleapis.com/auth/gmail.readonlyhttps://www.googleapis.com/auth/gmail.sendhttps://www.googleapis.com/auth/gmail.compose
- Test users: while the app is in "Testing" publishing status, add your own Gmail address (and anyoneelse's who needs access) as a Test user, or the consent screen will reject the login.
2. Enable the Gmail API
In the same project, go to APIs & Services → Library, search for Gmail API, and click Enable.
3. Create a Desktop OAuth client
- Go to APIs & Services → Credentials → Create Credentials → OAuth client ID.
- Application type: Desktop app.
- Give it a name (e.g. "Gmail MCP Connector") and click Create.
- Download the resulting JSON file. This is your OAuth client credentials file (contains a
client_idandclient_secret— not a token, and not a service account key). - Save it somewhere on disk, e.g.
~/.gmail-mcp/credentials.json(the default location — seeEnvironment variables to use a different path).
4. Install dependencies
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt
5. First-run authentication
The connector authenticates lazily, on the first tool call that needs Gmail access — there's no separate"login" command to run. On that first call:
auth.pylooks for a cached token atGMAIL_TOKEN_FILE(default~/.gmail-mcp/token.json). None existsyet, so it falls back to the interactive flow.- It reads your Desktop OAuth client from
GMAIL_OAUTH_CREDENTIALS(default~/.gmail-mcp/credentials.json) and opens your default browser to Google's consent screen. - Sign in and grant the requested Gmail scopes.
- The resulting credentials (including a refresh token) are written to the token file automatically.
On every subsequent run, the cached token is loaded and silently refreshed with the refresh token — nobrowser popup — unless the refresh token itself has been revoked or expired, in which case the interactiveflow runs again automatically.
To test locally over stdio before wiring it into any client:
python server.py
This starts the MCP server on stdio. You can point an MCP Inspector or compatible local client atpython server.py to exercise the tools directly, which will also trigger the first-run browser consent.
6. Multiple Gmail accounts
Every tool accepts an optional account parameter (default "default"), so this connector can operate onmore than one Gmail account — all sharing the same OAuth client (credentials.json), but each with its owntoken file.
To add another account, e.g. "work", run the same bootstrap snippet as first-run auth, naming the account:
python -c "from gmail_connector.auth import get_credentials; get_credentials('work')"
This opens the browser consent flow again — sign into the other Google account this time. The resultingtoken is saved separately at <config dir>/tokens/work.json; the "default" account keeps using<config dir>/token.json, completely unaffected. From then on, pass account: "work" to any tool to operateon that mailbox instead, e.g. list_recent_emails(account="work").
No extra Docker or environment configuration is needed for this — tokens/ lives inside the same configdirectory (gmail-mcp-data/ when using Docker) that's already persisted.
7. Running as HTTP (for deployment)
python server.py --http --port 8000
# optionally: --host 0.0.0.0 (default) or a specific bind address
This runs the server as a long-lived HTTP process serving the MCP streamable HTTP transport athttp://<host>:<port>/mcp. Deploy it behind your usual process manager / container orchestrator / reverseproxy like any other Python web service. Make sure GMAIL_MCP_CONFIG_DIR (or the individualGMAIL_OAUTH_CREDENTIALS / GMAIL_TOKEN_FILE paths) point at a persistent volume, since the token file mustsurvive restarts to avoid re-triggering interactive auth.
The very first authentication still requires an interactive browser consent flow (
run_local_server),which needs a local browser and a reachable loopback port. Run first-run auth once locally (or over an SSHtunnel to the deployment host) to producetoken.json, then ship that token file to the deployedenvironment — the deployed process will only need silent refreshes after that.
8. Registering the endpoint in your MCP platform
Once the server is reachable (locally over stdio, or remotely over HTTP), register it with your MCP-compatibleclient or platform:
- Local/stdio clients (e.g. Claude Desktop, an MCP Inspector): add an entry pointing at the command
python /path/to/server.py, run from the project's virtual environment. - HTTP-based platforms: open your platform's MCP integrations / connectors panel and add a new remote MCPserver using the URL
http://<host>:<port>/mcp(or your HTTPS reverse-proxy URL in production). Consultyour specific platform's docs for the exact field names, since this varies by product.
9. Docker setup with a browser UI (MCP Inspector)
docker-compose.yml runs two containers:
gmail-connector— this project, in--httpmode on port8000.inspector— the official@modelcontextprotocol/inspector,a web UI for calling MCP tools by hand, on port6274.
Step A — bootstrap token.json locally first
The interactive OAuth consent flow opens a real browser and listens on a loopback port on your machine — itcannot run headlessly inside a container. So do first-run auth once, on your host, before touchingDocker, and let Docker mount the resulting token:
mkdir -p gmail-mcp-data
cp /path/to/credentials.json gmail-mcp-data/credentials.json
# triggers the same get_credentials() lazy-auth logic auth.py uses, without starting a server
GMAIL_MCP_CONFIG_DIR=./gmail-mcp-data python -c "from gmail_connector.auth import get_credentials; get_credentials()"
A browser window opens, you sign in and grant access, and gmail-mcp-data/token.json is written. Thisgmail-mcp-data/ folder is exactly what docker-compose.yml mounts into the gmail-connector container at/data, so the containerized server only ever needs silent token refreshes from then on.
Step B — build and start
docker compose up --build
Step C — open the Inspector UI
Visit http://localhost:6274 in your browser. The Inspector container prints a one-time session URL(with an auth token query parameter) to its logs on startup — if the plain localhost:6274 page asks for aproxy session token, run docker compose logs inspector and open the full URL shown there instead.
Step D — connect the Inspector to the connector
In the Inspector's connection form:
- Transport type:
Streamable HTTP - URL:
http://gmail-connector:8000/mcp
Use the service name gmail-connector, not localhost — the Inspector's proxy process runs inside theinspector container, and on the Compose network the connector is only reachable at its service name.
Once connected, the Inspector's Tools tab lists every tool from the table above, with auto-generatedforms built from their docstrings/type hints, so you can call each one directly from the browser to verifyeverything end-to-end.
To stop everything: docker compose down (your token/credentials survive in ./gmail-mcp-data/ on the host).
10. Custom Tools-only web UI (alternative to the Inspector)
The MCP Inspector always shows Resources/Prompts/Ping/Sampling/Roots tabs alongside Tools, with no way tohide them. If you just want a page that lists the Gmail tools and lets you call them — nothing else —docker-compose.yml also runs a web-ui service at http://localhost:3000.
It's a small FastAPI backend (web-ui/app.py) that speaks MCP protocol to gmail-connector over streamableHTTP using the official mcp Python client, plus a single self-contained static page(web-ui/static/index.html) with no framework: a dropdown to pick a tool, a form built from that tool'sinput schema, and a "Run Tool" button. No session token to copy, no other tabs — just the 7 Gmail tools.
It comes up automatically with docker compose up -d alongside the connector and Inspector. Openhttp://localhost:3000, pick a tool from the dropdown, fill in the fields, and click Run Tool.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
GMAIL_MCP_CONFIG_DIR |
~/.gmail-mcp |
Base directory for credentials/token files when the two below aren't set explicitly. |
GMAIL_OAUTH_CREDENTIALS |
<config dir>/credentials.json |
Path to the Desktop OAuth client JSON downloaded from Google Cloud Console. |
GMAIL_TOKEN_FILE |
<config dir>/token.json |
Path where the cached/refreshed user token is read from and written to. |
Security notes
send_email,reply_to_email, andcompose_emailperform a real, irreversible action. Oncecalled, the email leaves your account and cannot be recalled by this connector. Treat thegmail.sendscopeas sensitive:only grant this connector to models/agents and users you trust to decide when to send mail on your behalf,and consider steering the calling model towardcreate_draftby default when a request is ambiguous.compose_emailalso sends without a preview step, though its output is a deterministic template(not AI-generated), so the wording is predictable fromtopicrather than a black box.token.jsoncontains a live refresh token that grants ongoing read/send/compose access to the account —protect it like a credential (file permissions, secret storage, not committed to version control).credentials.json(the OAuth client secret) should also be kept out of version control; it identifies yourOAuth client but does not by itself grant access to any mailbox.- The Google OAuth consent screen will show these scopes as sensitive (
gmail.send) andrestricted-adjacent (gmail.readonly,gmail.compose); moving the app to "In production" for usebeyond your own test users requires Google's OAuth verification review for sensitive scopes. - All errors are logged server-side with full detail via
logging, but only short, non-sensitive messages areever returned to the calling model — avoid logging raw email bodies or credentials at levels that ship toexternal log aggregators you don't control.