Gmail MCP Server
Provides tools for reading and searching emails, managing labels, threads, drafts, blocked senders, and sending messages to allowlisted recipients across two Gmail accounts.
Provides Google Workspace integration for Drive, Sheets, Docs, and Calendar across two accounts, enabling file search and reading, spreadsheet and document editing, and calendar event management.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Gmail MCP Server
Built on Tyler Szakacs's open-source Gmail multi-inbox MCP server (tszaks/ghub, MIT; see LICENSE), extended here with remote OAuth access, the Google Workspace endpoint, the send allowlist and per-client accountability. Hostnames, addresses and the owner's accounts are placeholders.
A self-hosted MCP server, reachable from anywhere over one hostname carrying two authenticated Streamable HTTP endpoints: Mail (two Gmail inboxes) and Google Workspace (Drive, Sheets, Docs, Calendar over the same two accounts). Any AI client that speaks MCP over HTTP with OAuth can connect to either — Claude Code, claude.ai and the Claude mobile apps, Codex, Cursor.
Endpoints (placeholder host): https://mcp.example.com/mcp (Mail) and https://mcp.example.com/workspace/mcp (Google Workspace) — both public; see Connecting a New Client
Auth: OAuth 2.1, issued by this server's own authorization server after a Google login restricted to the owner's two addresses. Each client registers separately per endpoint and holds its own revocable token, scoped read and/or write — there is no shared secret to copy, and a token issued for one endpoint is refused at the other.
What This Does
Mail (/mcp) gives AI agents read access to two Gmail accounts — personal and Texas A&M — plus inbox organization, draft composition, and sending restricted to a hard allowlist.
Google Workspace (/workspace/mcp) gives AI agents read and write access to Drive, Sheets, Docs, and Calendar across the same two accounts: finding and reading files, editing spreadsheets and documents, and managing calendar events. See Available Tools for the full list.
Accounts (both endpoints):
personal→ownerpersonal@gmail.comtamu→owner_school@tamu.edu
Related MCP server: Google Workspace MCP Server
The Send Boundary
Sending is enabled, but the server will only deliver to addresses on an explicit allowlist (seeded with the owner's own two addresses). This is the security property the whole design turns on, so it is worth being precise about how it works.
Where the check happens. src/allowlist.ts exposes assertSendAllowed(). It is called from exactly one place: GmailAccountClient.deliver() in src/gmail-client.ts, which is the only method in the codebase containing a users.messages.send or users.drafts.send call. The gate is that method's first statement. A recipient that is not allowlisted is refused before any network request is made.
What the check reads. Not the caller's arguments — the final MIME bytes destined for the Gmail API raw field. A critic pass demonstrated a live parser-differential attack where to: "alex@gmail.com\r\nBcc: evil@example.com" was rewritten by nodemailer's address parser into RFC 5322 group syntax, leaving the attacker as the only real recipient while an argument-checking allowlist would have read "alex@gmail.com" and waved it through. Checking the bytes that actually leave removes the representation gap. src/compose.ts also rejects that input, but the boundary does not depend on two parsers agreeing forever.
send_draft re-reads the draft. Drafts can be created in one session and sent in another, or edited in Gmail in between. sendDraft() fetches the draft with format='raw' and gates on those bytes. Nothing recorded at creation time is trusted.
Config location — outside the repo, read-only to the agent:
/root/.gmail-multi-mcp/send-allowlist.json # { "allowed": [ ... ] }, mode 0400
/root/.gmail-multi-mcp/send-audit.log # one JSON line per decision, allow and denyThat is the same directory as accounts.json, and the filename is fixed rather than configurable. No tool exposed by this server can read, edit, reload, or report-then-modify the allowlist. Widening it is an SSH-and-edit-the-file task, deliberately.
Failure modes all deny: missing file, unreadable file, malformed JSON, wrong shape, empty list, and a message whose recipients cannot be parsed all raise rather than sending. Addresses are normalized before comparison (Gmail dot/plus-tag equivalence for gmail.com; plus-tags only for Workspace domains such as tamu.edu, where dots are significant), so a cosmetic variant cannot be used to walk around the list — and a plus-tagged variant of an approved address is correctly allowed, since it still reaches the owner's mailbox.
Connecting a New Client
Both endpoints are public and there is no token to paste. A client registers itself per endpoint, you complete a Google login (only the owner's two addresses are accepted) and a consent screen naming the endpoint being authorized ("Mail" or "Google Workspace"), and it stores its own token. A client that wants both connects twice — one registration per endpoint, each scoped read and/or write.
URLs:
https://mcp.example.com/mcp(Mail),https://mcp.example.com/workspace/mcp(Google Workspace)Transport: Streamable HTTP
Auth: OAuth 2.1 — discovered automatically from the 401's
WWW-Authenticateheader
Claude Code:
claude mcp add --transport http gmail-remote https://mcp.example.com/mcp
claude mcp add --transport http google-workspace https://mcp.example.com/workspace/mcpthen run /mcp and authenticate in the browser, once per endpoint.
Client family | How to connect | Notes |
Claude Code |
| — |
claude.ai / Claude mobile apps | Add each URL as a custom connector in the client's own UI, same browser flow | One registration per endpoint covers both web and phone |
Codex | Add each URL as a custom connector in Codex's own UI, same browser flow | Adding a connector in the ChatGPT desktop app configures Codex, not ChatGPT chat — switch the app to Codex mode |
Cursor | Add each URL as a custom connector in Cursor's own UI, same browser flow | — |
ChatGPT chat | Cannot connect — nothing server-side fixes this | Custom MCP in regular ChatGPT chat needs OpenAI's Developer Mode, not offered on Plus |
Adding or renaming a tool requires the client to reconnect — the tool list is cached at connect time.
After connecting, confirm the new client appears by name in bash scripts/clients.sh list. To cut one off: bash scripts/clients.sh revoke <client_id>, effective immediately with no restart.
No other configuration is needed — the server handles multi-account routing internally. Tools that take an account parameter accept "personal" or "tamu".
The legacy Tailscale-only
/sseendpoint and its shared bearer token were deleted on 2026-08-30. Nothing answers on/sse. Tailscale is now purely the SSH/admin path. See ADR 007 (amended), ADR 012, ADR 013.
Available Tools
23 tools on the Mail endpoint (/mcp), and 35 tools (13 read + 22 write, Drive/Sheets/Docs/Calendar over both accounts) on the Google Workspace endpoint (/workspace/mcp). endpoints.json is the ground truth for which tools each endpoint exposes and in what order. Mail tools:
Tool | What it does |
| Read emails from inbox |
| Search by query string |
| Fetch full thread |
| List Gmail labels |
| Mark emails read |
| Apply labels |
| Remove labels |
| Apply labels to every message in a thread |
| Remove labels from every message in a thread |
| Archive emails |
| Move to trash |
| Create a new label |
| Delete a label |
| Block a sender |
| List blocked senders |
| Unblock a sender |
| Mute a thread |
| Create a draft (does not send) |
| Compose and send — allowlisted recipients only |
| Send an existing draft — re-checked against the allowlist |
| Delete drafts |
| List drafts |
| Search drafts |
Composition notes
Body is plain text, never HTML.
create_draftandsend_messagetake onebodystring in a small markdown-ish dialect (blank line = paragraph,-/1.= lists,[text](url)links,**bold**). Both thetext/plainandtext/htmlMIME parts are derived from that one string, so they cannot disagree, and</>/&render literally. This is what fixed the original draft-vs-sent line-break bug:multipart/alternativewith both parts present andquoted-printableforced on each.Signatures are not inserted. Gmail signature reuse was disabled 2026-08-17: the API only exposes a signature set as the account default, and setting a default makes Gmail auto-insert it into every compose in the web UI.
get_signatureis listed on no endpoint inendpoints.json, andinclude_signatureis out of every tool schema; the code path is intact and dormant. Write a plain sign-off in the body instead. See ADR 005.
Google Workspace tools (/workspace/mcp)
Drive, Sheets, Docs and Calendar over both accounts. Writes report what Google confirms, not what was asked; a lapsed Google authorization for one account is named at the point of use and the other account keeps working (ADR 011).
Drive (8)
Tool | What it does |
| Find files by name or content, both accounts unless one is given, newest first |
| List a folder's direct contents, newest first |
| Metadata for one file — name, type, size, parents, owners, link |
| Read a file's text — Doc as Markdown, PDF/Office extracted, Sheet points to the Sheets tools |
| Create a folder, optionally inside a parent |
| Rename and/or move a file |
| Move a file to trash (recoverable) |
| Upload a file from the server's own outbox; called with no |
Sheets (16)
Tool | What it does |
| List a spreadsheet's tabs and sizes |
| Read a tab as a raw grid, addressed by row number and column letter |
| Read a tab as header-keyed rows, each with its sheet row number |
| Find the rows where one column matches a value |
| Read the dropdown/validation rule on a cell or range |
| Overwrite exactly one range of cells |
| Add rows after the last row of data, aligned to the table's first header column |
| Change named columns of one existing row by header name |
| Add a new tab, optionally with a header row |
| Rename a tab |
| Permanently delete a tab and its contents |
| Insert blank rows or columns before a given position |
| Permanently delete rows or columns |
| Apply bold, italic, color, alignment, number format, or wrap to a range |
| Create a chart from a data range |
| Create a new spreadsheet, optionally into a Drive folder |
Docs (5)
Tool | What it does |
| Read a Doc as Markdown, every tab, each labelled with its |
| Create a new, empty Doc |
| Add a new paragraph at the end of one tab |
| Find and replace every occurrence of a phrase throughout one tab |
| Insert or replace text at a heading or a unique passage, never a character position |
Calendar (6)
Tool | What it does |
| List the calendars each account can see, with ids and time zones |
| List/search events in a time window; sweeps every calendar shown in the sidebar by default, naming each event's calendar |
| Full details of one event |
| Create an event — attendees limited to the owner's own addresses |
| Change fields of an existing event — same attendee restriction |
| Delete an event (recoverable) |
Owner-Only Server Scripts
Not MCP tools. Run these over SSH on the VPS from /root/gmail-multi-inbox-mcp/.
Script | Purpose |
| Print the OAuth consent URL for one account. Re-run as |
| Dump the raw delivered MIME of a message. Also |
| Can each account reach Drive, Sheets, Docs and Calendar? Real calls, per account, fails loudly. Run it from the laptop as |
| The live Workspace harness against real Google, both accounts: reads checked against independent read-backs; writes as copy → mutate → read back → trash. |
Infrastructure
VPS: DigitalOcean, NYC1 region, Ubuntu 24.04, ~$6/month
IP: 100.64.0.10 (tailnet — the only address that answers). The public IP 203.0.113.10 is recorded here for provider-console purposes only; the VPS has zero publicly-reachable inbound ports
Code on server: /root/gmail-multi-inbox-mcp/
Process manager: pm2 (gmail-mcp process, starts on boot)
Runtime: Node 24
SSH Access
Agents — use the wrapper, not raw ssh:
bash scripts/vps.sh 'pm2 list'
bash scripts/vps.sh 'grep -c label_messages dist/index.js'It supplies the key, starts in the deployed repo directory, and is allow-listed
in .claude/settings.json so it runs without a prompt.
The key: ~/.ssh/mcp_deploy_key (passwordless ED25519, comment
macbook-deploy-20260817); its public half is in /root/.ssh/authorized_keys
on the server. Raw invocation, if you need it:
ssh -i ~/.ssh/mcp_deploy_key root@100.64.0.10ssh root@203.0.113.10 does not work, and the reason changed. Since
2026-08-30 port 22 is closed to the public internet, so that command now
times out — a real firewall block. Before then it failed with
Permission denied (publickey), which was not a security block: the deploy
key is not one of the default filenames (id_rsa, id_ed25519, …) that ssh
checks, and no default-named key exists on this MacBook, so a bare invocation
simply offered nothing. Both failure modes point at the same fix — use
scripts/vps.sh, or pass -i and the tailnet address. A machine must be
joined to the tailnet to reach the VPS at all.
If Tailscale ever dies on the VPS, the only way in is DigitalOcean's web
console with the root password (owner's password manager). That is not a
network exposure: sshd has passwordauthentication no.
Gmail OAuth
GCP Project: email-503620
OAuth app type: Desktop app, published to production (not "Testing", so refresh tokens do not expire after 7 days)
Scopes granted: mail scopes since the start; Drive, Sheets, Docs and Calendar scopes were added for both accounts in plan 002 Phase 0 (2026-09-19) to bring up the Google Workspace endpoint.
Scope | Needed for |
| messages, threads, drafts, labels, modify — and send |
|
|
| Drive search/read/create/move/trash (restricted scope, same "unverified app" interstitial as |
| the Sheets tools on the Google Workspace endpoint |
| the Docs tools on the Google Workspace endpoint |
| the Calendar tools on the Google Workspace endpoint |
| identify which account actually consented |
There is no separate "send scope" to withhold: mail.google.com is a single full-access scope that includes sending. The restriction on who can be sent to is enforced by this server's allowlist, not by OAuth — see The Send Boundary. The first six rows are GOOGLE_ACCOUNT_SCOPES (src/gmail-client.ts); tools/reauth.mjs requests that full list plus the two identity scopes.
Credential files on server:
/root/credentials.json # OAuth client credentials
/root/.gmail-multi-mcp/accounts/personal/credentials.json
/root/.gmail-multi-mcp/accounts/personal/token.json # Access token (personal)
/root/.gmail-multi-mcp/accounts/tamu/credentials.json
/root/.gmail-multi-mcp/accounts/tamu/token.json # Access token (tamu)If a token expires or becomes invalid:
ssh -i ~/.ssh/mcp_deploy_key root@100.64.0.10
cd /root/gmail-multi-inbox-mcp
node tools/reauth.mjs personal # prints a consent URL
# open it, consent; the browser lands on http://localhost/?code=... and fails to
# load (nothing is listening) — that is expected. Copy the `code` value.
node tools/reauth.mjs personal "<code>" # exchanges it and writes token.json
pm2 restart gmail-mcpNo MCP tool can re-authorize an account; this is an owner task on the server.
Who Is Connected, and Cutting One Off
There is no shared token to find. Each client holds its own OAuth token, and the tools for inspecting that print no secrets:
bash scripts/clients.sh list # every registered client, by name
bash scripts/clients.sh revoke <client_id> # kill one client's tokens (--yes to confirm)
bash scripts/clients.sh migrate-scopes # rewrite stored mail:read/mail:write tokens to read/write (--yes to confirm)Revocation takes effect immediately, with no restart, and affects only that client. See ADR 015.
Configuration Files
File | Location | Purpose |
| Server only (not in git) |
|
| This repo | pm2 process config (reads |
| This repo | Which tools each endpoint exposes, in wire order (read at boot; a malformed file refuses to start) |
| Server: | Gmail account definitions (not in git) |
| Server: | Who this server may send to. Mode 0400, outside the repo, no tool can write it |
| Server: | One JSON line per send decision, allowed and denied |
| Server: | Registered OAuth clients and issued tokens. Read via |
| Server: | One JSONL line per authenticated request on any endpoint ( |
endpoints.jsonandsend-allowlist.jsonare not the same kind of control. The first is a convenience switch that lives in git and anything with repo access can flip. The second is the security boundary, and it deliberately lives somewhere the agent, the repo, and git history cannot reach.
Project Documentation
Working docs live under docs/, each folder indexed by its own README.md:
Folder | Holds |
| What the tool does. |
| How it was built, in phases. |
| Why each significant decision was made, and what else was considered. |
| Open problems, and how past ones were resolved. |
CLAUDE.md at the root is the briefing AI agents load when working in this repo. docs/machine-setup.md, docs/human-verification.md, docs/research-report.md and docs/mcp-server-lessons.md are standalone references.
Making Changes
Edit code in the local clone.
npm test— builds, then runs the suite againstdist/.bash scripts/deploy.sh— builds locally, shipsdist/over tar, verifies the module graph, restarts pm2.Commit and push to GitHub.
bash scripts/deploy.sh --rollbackrestores the previous build.
Do not build on the server.
tscOOMs on the 1GB droplet (googleapis ships very large type definitions), and agit pulldeploy produces a server that dies at boot withERR_MODULE_NOT_FOUND—dist/is gitignored but was half-committed long ago, so any new module is silently untracked.deploy.shsidesteps both by building where the resources are and shipping the artifact. Seescripts/deploy.sh's header for the full history.
Adding or renaming a tool also requires the MCP client to reconnect (/mcp, or restart Claude Code). Deploying alone is not enough — the client caches the tool list, and a stale one silently hides new tools and shows old schemas.
Toggling tools on/off without code changes: edit endpoints.json and deploy (the file is read at boot, so the restart deploy.sh performs is what applies it). A name no module defines refuses to boot rather than being silently ignored. The mail endpoint's list is frozen by a golden test — see test/golden/.
pm2 Quick Reference
pm2 status # Check if gmail-mcp is running
pm2 logs gmail-mcp # Tail logs
pm2 restart gmail-mcp # Restart after config/code changes
pm2 start ecosystem.config.cjs # Start fresh from config file
pm2 save # Persist process list across rebootsThis 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.
Your agent needs a mailbox of its own — to receive, thread, draft and send, with attachments, without borrowing your personal inbox or your company's SMTP. **What you can ask for** • "Create an inbox for this agent and tell me its address." • "Read the new messages in this thread and draft a reply." • "Send this message with the attachment and wait for the response." • "Search this inbox for everything from that domain." • "Show delivery metrics and the events on this inbox." **How to use it** Point any MCP client at https://mcp.aisa.one/mail/mcp and sign in with OAuth — there is no key to create or paste. 49 tools: create and delete inboxes, list and read messages, raw message bodies, attachments, threads, drafts and draft attachments, send and reply, message search, inbox events, metrics, and list entries — reads and writes. **Why this rather than the source** A real inbox an agent owns, rather than an SMTP credential it borrows from a human. **It is also a door to the rest** The same login reaches 26 sources and 580+ operations. Find the contact elsewhere in the catalogue, then write to them from here — without adding a second server. **What it costs** Finding and inspecting an operation is free. Running one is billed per call at API prices, with no seat and no monthly minimum, and every call takes max_price_usd so an agent cannot overspend by accident. **Where else it reaches** https://mcp.aisa.one/sales/mcp finds the person to write to.
Give AI agents their own email inboxes: list, read and send mail via scoped, revocable OAuth grants.
Permissioned access to Gmail, Drive and Calendar via the user's own Google account
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP server for Google Workspace APIs - Docs, Sheets, Drive, Gmail, and Calendar. Enables reading, creating, and editing Google Docs and Sheets, managing comments, reading emails, and viewing calendar events.3453 npm17MIT
- AlicenseBqualityDmaintenanceProvides natural language control over Google Workspace services (Calendar, Drive, Gmail, Docs, Sheets, Slides, Forms, Tasks, Chat) via MCP, with OAuth 2.1 multi-user authentication.100MIT
- AlicenseAqualityBmaintenanceEnables natural language interaction with multiple Google accounts (Gmail, Drive, Calendar) from MCP-compatible clients like Claude.6340 npm2MIT
- FlicenseBqualityCmaintenanceMCP server providing full Google Workspace access (Gmail, Drive, Calendar, Docs, Sheets, Slides, Forms, Tasks, Contacts) using your own OAuth credentials, enabling natural language management of Google services.100-