Skip to main content
Glama
gmail-mcp

gmail-mcp

Gmail MCP

CI Python 3.11+ License: MIT 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 / .csv so 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:

  1. Get a credentials.json from Google Cloud Console — see docs/google-cloud-setup.md.

  2. Place it at the OS config directory: Linux uses ~/.config/gmail-mcp/; macOS uses ~/Library/Application Support/gmail-mcp/; Windows uses %APPDATA%\gmail-mcp\.

  3. 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-mcp alone waits for MCP requests and does not start sign-in.

  4. 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 command with the Python executable from the installed environment. On Windows, use an escaped path such as C:\\path\\to\\venv\\Scripts\\python.exe. Other MCP clients can launch the same command and arguments, or gmail-mcp.

  5. 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

GMAIL_MCP_CONFIG_DIR

OS-specific (see below)

Overrides where credentials.json, token.json, and gmail-mcp.log live.

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

list_emails, get_thread_messages, read_email, send_email, reply_email, archive_email, trash_email, mark_read, add_label, remove_label, create_draft, list_drafts, update_draft, delete_draft, list_attachments, download_attachment

Batch

read_emails, mark_emails_read, archive_emails, trash_emails, add_labels, download_attachments

File export

read_emails_full, search_and_save, append_emails

Full parameter reference and examples: docs/TOOLS.md.

Documentation

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 tools
add_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

    A3.6/5.0

    Scored across 25 tools

    Disambiguation3/5

    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.

    Naming Consistency4/5

    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.

    Tool Count3/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      F
      maintenance
      MCP server that connects Claude with Gmail to read, send, delete, and manage messages and labels via the Google Gmail API.
      3
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      A local MCP server that provides tool-level access to Gmail, allowing Claude to search, read, send, and manage emails using the Gmail API.
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      A custom MCP server that gives Claude full Gmail control — send directly, read, reply, search, label, and delete emails.
      128 npm
      MIT