gmail-mcp
Provides tools to read, search, send, label, and manage Gmail emails through OAuth2, including batch operations and file-export for large result sets.
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., "@gmail-mcp List my 5 most recent unread emails"
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.
Gmail MCP
An MCP server that lets Claude (Desktop, Code, or any MCP client) read, search, send, label, and manage your Gmail through OAuth2. 25 tools, batch operations, file-export for large result sets, retry-with-backoff, and OS-keyring token storage.
Highlights
25 tools across single, batch, and file-export families — see
docs/TOOLS.md.Batch operations with automatic chunks of 100 message reads/downloads or 1000 label updates per Gmail request, preserving order and partial failures.
File-export tools (
read_emails_full,search_and_save,append_emails) save results to.md/.json/.jsonl/.csvso MCP output limits aren't hit when working with hundreds of emails.Resilient: bounded retry on safe operations, respecting
Retry-After. Sending mail and creating drafts are attempted once to avoid duplicates.Secure token storage via OS keyring (optional) or filesystem (default).
Tested: offline regression tests, mocked Gmail responses, and an actual MCP startup/tool-listing test. CI covers Windows, macOS, and Linux.
Related MCP server: Gmail MCP Server
Quick start
pip install -e . # install in editable mode
# (or once published: pipx install gmail-mcp)Then:
Get a
credentials.jsonfrom Google Cloud Console — seedocs/google-cloud-setup.md.Place it at the OS config directory: Linux uses
~/.config/gmail-mcp/; macOS uses~/Library/Application Support/gmail-mcp/; Windows uses%APPDATA%\gmail-mcp\.Authenticate in a terminal with a browser, using the Python environment where you installed this package:
python -c "from gmail_mcp.auth import get_gmail_service; get_gmail_service()"This opens Google's sign-in page and caches the token. Server startup is lazy: launching
gmail-mcpalone waits for MCP requests and does not start sign-in.Add this server to your MCP client's configuration. For Claude Desktop, merge the following into
claude_desktop_config.json:{ "mcpServers": { "gmail-mcp": { "command": "/absolute/path/to/your/python", "args": ["-m", "gmail_mcp"] } } }Replace
commandwith the Python executable from the installed environment. On Windows, use an escaped path such asC:\\path\\to\\venv\\Scripts\\python.exe. Other MCP clients can launch the same command and arguments, orgmail-mcp.Restart Claude Desktop. Try: "List my 5 most recent unread emails."
Configuration
Set environment variables (or edit src/gmail_mcp/config.py):
Variable | Default | Effect |
| OS-specific (see below) | Overrides where |
OS-specific config dir defaults:
Linux:
~/.config/gmail-mcp/macOS:
~/Library/Application Support/gmail-mcp/Windows:
%APPDATA%\gmail-mcp\
To switch to OS keyring storage, set Config.token_storage = "keyring" in
config.py and delete the cached token.json so the OAuth flow re-runs.
Tools at a glance
Family | Tools |
Single |
|
Batch |
|
File export |
|
Full parameter reference and examples: docs/TOOLS.md.
Documentation
docs/google-cloud-setup.md— step-by-step OAuth client setup.docs/ARCHITECTURE.md— module map, request flow, design decisions.docs/TROUBLESHOOTING.md— common errors and fixes.docs/TOOLS.md— every tool, every parameter, every response.docs/AUDIT.md— reliability audit, compatibility changes, and validation limits.
Contributing
See CONTRIBUTING.md for dev setup, tests, and the PR
checklist. Bug reports and feature requests welcome via GitHub Issues.
License
MIT.
Available Tools
25 toolsadd_labelA
Add a label (system or custom) to an email.
Required:
message_id (str) — Gmail message ID.
label (str) — label name. System labels: STARRED, IMPORTANT,
SENT, DRAFT, TRASH, SPAM. Custom labels: any name you've
created in Gmail settings.
Returns on success:
{"message_id": "...", "labels": ["INBOX", "IMPORTANT", "..."]}}
Workflow note: Label names are case-sensitive in Gmail.
"STARRED" works, "starred" may not. Custom label names
must exactly match how they appear in Gmail.
Inputs: label (required), message_id (required).
add_labelsA
CHOICE HINT: When the user asks to "add a label" to multiple emails,
ALWAYS prefer this tool over making N calls to add_label(). It uses
Gmail's batchModify API in chunks of at most 1000 message IDs.
Add a label to multiple emails in a single MCP request.
Uses messages().batchModify() with up to 1000 IDs per Gmail API call.
Required:
message_ids (list[str]) — Gmail message IDs.
label (str) — label name. System labels: STARRED, IMPORTANT, etc.
Custom labels must exactly match how they appear in Gmail.
Returns on success:
{"ok": true, "data": {
"requested": N, "succeeded": N, "failed": 0,
"results": [{"id": "...", "ok": true}, ...]
}}
Workflow note: Use this instead of N add_label() calls.
Label names are case-sensitive.
Inputs: label (required), message_ids (required).
append_emailsA
Append more emails to an existing .md, .json, or .jsonl file.
Use this for pagination — fetch 50 emails, save them, then call this
tool to append the next 50 to the same file. Works with .md, .json,
and .jsonl files created by read_emails_full or search_and_save.
Required:
message_ids (list[str]) — Gmail message IDs to fetch and append.
save_path (str) — path to the existing file. File must exist.
Supported extensions: .md, .json, .jsonl
Returns on success:
{"ok": true, "data": {
"appended": true,
"path": "C:\Users\...\my-emails.md",
"count": 50,
"total_size_bytes": 320000
}}
Inputs: save_path (required), message_ids (required).
archive_emailA
Archive an email — removes it from INBOX without deleting it.
Archived emails are still searchable and accessible via search.
Required:
message_id (str) — Gmail message ID from list_emails.
Returns on success:
{"message_id": "...", "labels": ["IMPORTANT", "SENT"]}}
(INBOX is removed from labels)
Workflow note: This is non-destructive. The email stays in Gmail
with all other labels intact. Use list_emails(query="is:inbox")
to confirm it's gone from the inbox view.
Inputs: message_id (required).
archive_emailsA
CHOICE HINT: When the user asks to "archive" or "remove from inbox" multiple
emails, ALWAYS prefer this tool over making N calls to archive_email(). It uses
Gmail's batchModify API in chunks of at most 1000 message IDs.
Archive multiple emails in a single MCP request.
Uses messages().batchModify() with up to 1000 IDs per Gmail API call.
Required:
message_ids (list[str]) — Gmail message IDs.
Returns on success:
{"ok": true, "data": {
"requested": N, "succeeded": N, "failed": 0,
"results": [{"id": "...", "ok": true}, ...]
}}
Workflow note: Non-destructive. Use this instead of N archive_email() calls.
Inputs: message_ids (required).
create_draftA
Create a draft email saved in Gmail's Drafts folder — not sent immediately.
Review it in Gmail before sending manually.
Required:
to (str) — recipient email address.
subject (str) — email subject line.
body (str) — email body text.
Optional:
cc (str, default="") — CC recipients.
bcc (str, default="") — BCC recipients.
attachments (list[str] | None, default=None) — file paths to attach.
Returns on success:
{"draft_id": "...", "message_id": "..."}
Workflow note: Drafts are saved to Gmail, not stored locally.
Use list_drafts() to see all drafts. Use update_draft(draft_id, ...)
to modify a draft. Use delete_draft(draft_id) to remove.
The draft stays in Gmail until manually sent or deleted.
Inputs: to (required), body (required), subject (required), cc, +2 more.
delete_draftA
Delete a draft from Gmail's Drafts folder.
Required:
draft_id (str) — draft ID from list_drafts.
Returns on success:
{"draft_id": "..."}
Workflow note: This permanently removes the draft. No undo.
Call list_drafts() first to confirm the draft_id before deleting.
Inputs: draft_id (required).
download_attachmentA
Download a file attachment from an email and save it to a local path.
Required:
message_id (str) — Gmail message ID from list_emails.
attachment_id (str) — attachment ID from list_attachments.
save_path (str) — local file path to save to. Must be inside
your home directory or the gmail-mcp config directory.
Parent directories are created automatically.
If the file already exists, a counter suffix is added
(e.g. "report (1).pdf") to avoid overwriting.
Returns on success:
{"filename": "report.pdf", "path": "C:/Users/.../report.pdf",
"size": 204800}
Workflow note: Paths are validated — path traversal attempts
(e.g. "../../etc/passwd") are rejected. Use absolute paths
or paths relative to your home directory. Supported file
types: any MIME type. Max attachment size: 25MB.
Inputs: save_path (required), message_id (required), attachment_id (required).
download_attachmentsC
Download attachments in batches of 100, preserving input order and unique files.
Inputs: items (required).
get_thread_messagesA
List all messages in a thread by thread ID.
Returns message IDs, subjects, from addresses, dates, and snippets.
Use this to find the Gmail message ID needed by reply_email.
Required:
thread_id (str) — the Gmail thread ID. Get this from list_emails
or read_email (look for "thread_id" in responses).
Returns on success:
{"thread_id": "...", "total": N, "messages": [
{"id": "...", "subject": "...", "from": "...", "date": "...", "snippet": "..."}
]}
Workflow note: reply_email requires a Gmail message ID in the in_reply_to
parameter — NOT an email Message-ID header. Use get_thread_messages
to find the right message_id first. Example flow:
1. list_emails(query="subject:my thread") → get thread_id
2. get_thread_messages(thread_id) → get message_id
3. reply_email(thread_id=..., in_reply_to=message_id, ...)
Inputs: thread_id (required).
list_attachmentsA
List all file attachments in an email with their IDs, filenames,
MIME types, and sizes.
Required:
message_id (str) — Gmail message ID from list_emails.
Returns on success:
{"attachments": [
{"id": "...", "filename": "report.pdf",
"mime_type": "application/pdf", "size": 204800}
]}
Workflow note: The attachment "id" field is a Gmail attachment ID —
pass it to download_attachment along with the message_id and
a save_path to download the file. Returns empty list if the
email has no attachments.
Inputs: message_id (required).
list_draftsA
List the user's draft emails from Gmail's Drafts folder.
Optional:
max_results (int, default=5) — max drafts to return.
Returns on success:
{"total": N, "drafts": [
{"id": "...", "subject": "...", "from": "...", "to": "...",
"date": "...", "snippet": "..."}
]}
Workflow note: Each draft has a "id" (draft_id) used by update_draft
and delete_draft. The "subject" field shows the draft's subject line.
Use update_draft(draft_id, body="new content") to edit a draft.
Inputs: max_results.
list_emailsA
Search inbox for emails matching a query string.
Returns a list of emails with their IDs, subjects, from addresses, dates,
snippets, and labels. IDs can be passed to read_email or reply_email.
Required: none — both params have defaults.
Optional:
query (str, default="") — Gmail search syntax.
Examples: "from:alice@example.com", "subject:invoice is:unread",
"after:2026/01/01 before:2026/01/31", "has:attachment".
Leave empty to get most recent inbox emails.
max_results (int, default=5) — how many emails to return. Max 500.
Returns on success:
{"total": N, "emails": [{"id": "...", "subject": "...", "from": "...",
"date": "...", "snippet": "...", "labels": [...]}]}
Workflow note: The "id" field in each email is a Gmail message ID —
pass it to read_email, reply_email, or get_thread_messages.
Leave query empty to get recent inbox. Use Gmail search syntax
to filter: from:, subject:, after:, before:, is:unread, has:attachment.
Inputs: query, max_results.
mark_emails_readA
CHOICE HINT: When the user asks to "mark as read" or "mark read" multiple
emails, ALWAYS prefer this tool over making N calls to mark_read(). It uses
Gmail's batchModify API in chunks of at most 1000 message IDs.
Mark multiple emails as read in a single MCP request.
Uses messages().batchModify() with up to 1000 IDs per Gmail API call.
Required:
message_ids (list[str]) — Gmail message IDs.
Returns on success:
{"ok": true, "data": {
"requested": N, "succeeded": N, "failed": 0,
"results": [{"id": "...", "ok": true}, ...]
}}
Workflow note: Use this instead of N calls to mark_read() to bundle
all operations into a single MCP request.
Inputs: message_ids (required).
mark_readA
Mark an email as read — removes the UNREAD label.
Required:
message_id (str) — Gmail message ID from list_emails.
Returns on success:
{"message_id": "...", "labels": ["INBOX", "IMPORTANT"]}}
(UNREAD is removed from labels)
Workflow note: Use this after reading an important email to
clear the unread indicator.
Inputs: message_id (required).
read_emailA
Fetch the full content of an email by its Gmail message ID.
Returns subject, from, to, date, body (plain text), html_body, and labels.
Required:
message_id (str) — the Gmail message ID from list_emails or
get_thread_messages. NOT an email Message-ID header.
Returns on success:
{"id": "...", "subject": "...", "from": "...", "to": "...",
"date": "...", "body": "plain text...", "html_body": "HTML...",
"labels": ["INBOX", "IMPORTANT"]}
Workflow note: body may be None if the email is HTML-only.
html_body will be present for HTML emails. Labels include system
labels like INBOX, SENT, TRASH, UNREAD and custom labels.
Use list_attachments(message_id) to find downloadable files.
Inputs: message_id (required).
read_emailsA
CHOICE HINT: When the user asks to "read", "show", or "fetch" content from
multiple emails (e.g. "read these 5 emails", "show me the latest 3 emails"),
ALWAYS prefer this tool over making N calls to read_email(). It bundles all
Gmail API calls into bounded HTTP batches.
Fetch full content of multiple emails in a single MCP request.
Uses googleapiclient.http.BatchHttpRequest — calls are bundled
into batches of up to 100 per HTTP round-trip.
Handles up to 100 messages per call automatically.
Optional:
save_to_file (bool, default=False) — force save all results to a
.md file regardless of size. Useful for large fetches.
max_chars (int, default=30000) — if estimated output exceeds this,
auto-saves to .md and returns a compact summary with the file path.
Returns on success (small):
{"ok": true, "data": {
"requested": N, "succeeded": N, "failed": 0,
"results": [{"id": "...", "ok": true, "data": {...}}, ...]
}}
Returns on success (large / save_to_file):
{"ok": true, "data": {
"saved": true,
"path": "C:\Users\...\gmail-mcp\emails\2026-05-11-piraeus-49.md",
"count": 49,
"size_bytes": 184523,
"summary_table": "## Summary
| # | Date | From | Subject | ...|", "requested": 49, "succeeded": 49, "failed": 0, "results": [{"id": "...", "ok": true, "data": {"date": "...", "from": "...", "subject": "...", "snippet": "...", "labels": [...]}} ...] }}
Workflow note: Use this instead of N calls to read_email() to bundle
all fetches into a single MCP request. Each result has the same
shape as a read_email() response.
Inputs: message_ids (required), max_chars, save_to_file.
read_emails_fullA
Read multiple emails and always save the full results to a file.
Unlike read_emails() which returns data inline, this tool always writes
to disk and returns only a compact confirmation — making it safe to use
even for hundreds of emails without hitting MCP output limits.
Required:
message_ids (list[str]) — Gmail message IDs to fetch.
save_path (str) — local path to save the file. Must be inside your
home directory (path traversal is rejected).
Optional:
format (str, default="md") — output format. One of:
"md" — Markdown with summary table + full body per email
"json" — pretty-printed JSON
"jsonl" — one JSON object per line
"csv" — complete fields, text/HTML bodies, identifiers and errors
Returns on success:
{"ok": true, "data": {
"saved": true,
"path": "C:\Users\...\my-emails.md",
"count": 49,
"format": "md",
"total_size_bytes": 184523
}}
Workflow note: Use this instead of read_emails() when you know the
result will be large. Always saves — never returns data inline.
Inputs: save_path (required), message_ids (required), format.
remove_labelA
Remove a label from an email.
Required:
message_id (str) — Gmail message ID.
label (str) — label name to remove.
Returns on success:
{"message_id": "...", "labels": ["INBOX", "IMPORTANT"]}}
(the label is removed from labels array)
Workflow note: Removes the label from the message's label set.
No error if the label was not present.
Inputs: label (required), message_id (required).
reply_emailA
Convenience wrapper to reply to an existing thread.
Automatically prepends "Re: " to the subject if missing.
Can CC all original recipients with reply_all=True.
Required:
to (str) — recipient address.
body (str) — reply text.
thread_id (str) — Gmail thread ID to reply within.
in_reply_to (str) — Gmail message ID being replied to.
GET THIS FROM get_thread_messages(thread_id) — do not guess.
Optional:
subject (str, default="") — if empty, "Re: " is prepended to the
original message's subject.
cc (str, default="") — additional CC recipients.
bcc (str, default="") — BCC recipients.
reply_all (bool, default=False) — if True, auto-CCs all other
recipients from the original message (To and CC headers).
Returns on success:
{"message_id": "...", "thread_id": "...", "to": "...", "subject": "..."}
Workflow note: CRITICAL — in_reply_to must be a Gmail message ID
(e.g. "19e09c7f2e8d487d"), NOT an email Message-ID header
(e.g. "<CAC4dBmc...@mail.gmail.com>"). To find the right ID:
1. Call get_thread_messages(thread_id)
2. Pick the message ID from the "messages" array
3. Pass that to reply_email's in_reply_to parameter
Inputs: to (required), body (required), thread_id (required), in_reply_to (required), +4 more.
search_and_saveA
One-shot search + save: search Gmail and write results directly to a file.
Combines list_emails + read_emails_full into a single tool call.
No data is returned inline — just a compact summary + file path.
Required:
query (str) — Gmail search syntax
(e.g. "from:PiraeusBank after:2026/03/01 has:attachment").
Optional:
max_results (int, default=100) — max emails to fetch.
save_path (str) — local file path. If empty, auto-generates one in
the configured gmail-mcp directory under emails/. Overwrite is rejected — use a fresh
path or let the tool auto-generate.
format (str, default="md") — "md", "json", "jsonl", or "csv".
Returns on success:
{"ok": true, "data": {
"saved": true,
"path": "C:\Users\...\emails\2026-05-11-piraeus-47.md",
"count": 47,
"format": "md",
"total_size_bytes": 184523,
"query": "from:PiraeusBank after:2026/03/01",
"search_summary": "47 emails matched your search query"
}}
Workflow note: Use this when you want to search Gmail and immediately
persist results to a file without worrying about MCP output limits.
Inputs: query (required), format, save_path, max_results.
send_emailA
Send a new email. Can thread into an existing conversation by including
thread_id and in_reply_to/references headers. Supports CC, BCC, and
file attachments.
Required:
to (str) — recipient email address.
subject (str) — email subject line.
body (str) — email body text.
Optional:
cc (str, default="") — CC recipients, comma-separated.
bcc (str, default="") — BCC recipients, comma-separated.
thread_id (str, default="") — Gmail thread ID to reply within.
If provided with in_reply_to and references, the email threads
into the existing conversation.
in_reply_to (str, default="") — the Message-ID header of the
message being replied to. Required for threading.
references (str, default="") — References header for threading.
Usually same as in_reply_to.
attachments (list[str] | None, default=None) — absolute file paths
to files to attach. Paths must be absolute or relative to cwd.
Returns on success:
{"message_id": "...", "thread_id": "...", "to": "...", "subject": "..."}
Workflow note: For threading, you need both thread_id and in_reply_to.
If you only have a thread_id but not in_reply_to, use
get_thread_messages(thread_id) first to find a message_id.
Attachments: paths must exist and be under 25MB.
The body parameter is the email text — not a file path.
Inputs: to (required), body (required), subject (required), cc, +5 more.
trash_emailA
Move an email to trash. Emails in trash are auto-deleted after 30 days.
Required:
message_id (str) — Gmail message ID from list_emails.
Returns on success:
{"message_id": "...", "labels": [..., "TRASH"]}}
Workflow note: To permanently delete, trash the email first —
Gmail auto-purges trash after 30 days.
Inputs: message_id (required).
trash_emailsA
CHOICE HINT: When the user asks to "trash" or "delete" multiple emails,
ALWAYS prefer this tool over making N calls to trash_email(). It uses
Gmail's batchModify API in chunks of at most 1000 message IDs.
Move multiple emails to trash in a single MCP request.
Uses messages().batchModify() with up to 1000 IDs per Gmail API call.
Required:
message_ids (list[str]) — Gmail message IDs.
Returns on success:
{"ok": true, "data": {
"requested": N, "succeeded": N, "failed": 0,
"results": [{"id": "...", "ok": true}, ...]
}}
Workflow note: Use this instead of N trash_email() calls.
Inputs: message_ids (required).
update_draftA
Update a draft body, preserving omitted headers, threading and attachments.
body is required. Empty to/subject/cc/bcc retain existing values.
attachments=None preserves existing files; [] explicitly removes them.
Inputs: draft_id (required), cc, to, bcc, +3 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 25 tools
The singular/batch pairs (read_email vs read_emails, archive_email vs archive_emails, etc.) are mostly distinguishable through descriptions and choice hints, but the read/search/save cluster is crowded: list_emails, read_email, read_emails, read_emails_full, search_and_save, and append_emails overlap conceptually. An agent can usually choose correctly, but several tools require careful reading to avoid misselection.
Most tool names follow a predictable snake_case verb_noun pattern, with clear singular/plural batch variants. Minor deviations exist, such as mark_emails_read (awkward ordering) and read_emails_full, but the convention is still largely consistent.
25 tools is at the heavy end for a Gmail server. Many batch variants earn their place, but some duplication (multiple read/save tools plus singular/batch pairs) suggests the surface could be consolidated.
The server covers core email workflows: search/list, read, send, reply, draft CRUD, archiving, trashing, labels, mark-read, attachment listing/downloading, and batch operations. Minor gaps include no batch remove_label, no create/list custom labels, and no mark-unread or permanent-delete operation, but these are workable around.
Maintenance
Related MCP Connectors
A MCP server for Gmail that lets you search, read, and draft emails and replies.
Email infrastructure for AI agents — send, receive, search, and reply to email over MCP.
Connect any IMAP/SMTP mailbox to Claude & ChatGPT: search, read, sort, draft. Sends only with OK.
Your IMAP mailbox as an MCP server: read, search and (if you allow it) organize mail. Open source.
Related MCP Servers
- AlicenseBqualityFmaintenanceMCP server that connects Claude with Gmail to read, send, delete, and manage messages and labels via the Google Gmail API.3MIT
- AlicenseNot gradedqualityDmaintenanceA local MCP server that provides tool-level access to Gmail, allowing Claude to search, read, send, and manage emails using the Gmail API.1MIT
- AlicenseAqualityDmaintenanceA Gmail MCP server that enables Claude to send, search, and read emails using either Gmail API or SMTP/IMAP backend.3108 PyPIMIT
- AlicenseNot gradedqualityDmaintenanceA custom MCP server that gives Claude full Gmail control — send directly, read, reply, search, label, and delete emails.128 npmMIT