Skip to main content
Glama
knowledgeislands

mcp-gmail

mcp-gmail

CI npm version License: MIT

An MCP (Model Context Protocol) server that connects Claude with Gmail through the Google Gmail API.

Features

  • Search and triage — Gmail-query syntax at message + thread granularity, batch-relabel up to 1000 messages in a single API call.

  • Label management — list/create/rename/delete user labels; toggle read/unread/archive/trash via sugar tools so callers don't have to know system-label ids.

  • Recoverable trashmessages.trash / threads.trash only. Permanent deletion (messages.delete / threads.delete) is deliberately not exposed.

  • Drafts-only outbound — compose plain text + HTML (with multipart/alternative fallback), attachments with filename/MIME-type overrides, reply + reply-all (self-dedupe via cached profile). Never sends mail — the user reviews drafts in Gmail and clicks Send.

  • Strict input schemas — every tool registers a Zod schema; tools/list reports proper JSON Schema and honest MCP annotations (readOnlyHint, destructiveHint, idempotentHint).

Quality: 333 tests at 100% coverage; CI also boots the built server over stdio MCP and asserts the wire-level tool surface on every commit (bun run test:smoke).

Related MCP server: gmail-mcp

Quick Start

  1. Install dependencies: bun install.

  2. Set up Google Cloud credentials — see Google Cloud Console Setup.

  3. Configure environment — copy .env.example to .env.development and add your Google OAuth credentials.

  4. Build: bun run build.

  5. Configure Claude Desktop with dist/mcp-server/index.js and your MCP_GMAIL_CLIENT_ID/MCP_GMAIL_CLIENT_SECRET (see Configuration).

  6. Start the auth server: bun run server:auth:dev (separate process; handles OAuth on localhost:3334).

  7. Authenticate — call the gmail_auth_start tool in your MCP client, follow the URL, sign in. Tokens land at ~/.mcp-gmail-tokens.json (mode 0600). (gmail_auth_start is annotated WRITE_REMOTE because it persists tokens, so it registers at MCP_GMAIL_ACCESS_LEVEL=write or higher; the default read-only configuration hides it along with every other mutating tool.)

Example Conversations

Concrete asks you might make of Claude with this server connected.

Triage by sender:

"Find all unread emails from notifications@github.com from the last 30 days and archive them."

Claude uses gmail_messages_search with from:notifications@github.com is:unread newer_than:30d to collect ids, then gmail_messages_batch_modify (single round-trip, up to 1000 ids per call) to drop the INBOX label from the batch.

Draft a contextual reply:

"Find the meeting invite from Alice yesterday and draft a reply confirming I'll be there at 2pm."

Claude uses gmail_messages_search to locate the thread, gmail_message_get to read the headers, then gmail_draft_create with replyToMessageId set — the draft inherits In-Reply-To, the References chain, the threadId, and a Re: subject. Pass replyAll: true and Claude auto-populates To (= original From + To) and Cc (= original Cc); your authenticated address is dropped from both so you don't email yourself.

Find what needs attention:

"Show me unread threads with attachments from this week."

Claude calls gmail_threads_search with is:unread has:attachment newer_than:7d and returns subject, from, snippet, label ids, and attachment counts per thread — fast structured output, not free-form text.

Bulk relabel:

"Move every message labelled newsletter from before 2026 to my reading-list/archive label and drop newsletter."

Claude resolves both label ids via gmail_labels_list, searches with label:newsletter before:2026/01/01, and applies the swap in a single gmail_messages_batch_modify({addLabelIds, removeLabelIds}) call.

Installation

Prerequisites

  • Bun 1.3+ for the dev loop

  • Node.js 22.0.0 or higher to run the compiled dist/

  • A Google account for Cloud Console access

bun install

Google Cloud Console Setup

1. Create a project

  1. Open the Google Cloud Console.

  2. Project dropdown → New Project.

  3. Name it (e.g. mcp-gmail) → Create.

2. Enable the Gmail API

  1. APIs & Services → Library.

  2. Search for Gmail APIEnable.

For brand-new projects, Google gates this behind a one-time wizard. If you see "Google Auth Platform not configured yet" with a Get Started button, follow 3a. Otherwise jump to 3b.

3a. First-time setup

  1. APIs & Services → OAuth consent screenGet Started.

  2. App Information: app name, your support email → Next.

  3. Audience: ExternalNext.

  4. Contact Information: your email → Next.

  5. Agree to the user-data policy → ContinueCreate.

3b. Publish the app

  1. OAuth consent screen → Audience.

  2. Publishing statusPublish AppConfirm. (Avoids the 7-day refresh-token expiry of "Testing" mode. The app stays unverified — fine for personal use; you'll see a one-time "advanced → continue" warning during sign-in.)

3c. Configure data access (scopes)

This step is mandatory. If a scope isn't pre-declared here, Google silently drops it from consent, and Gmail API calls return 403 even after a "successful" sign-in.

  1. OAuth consent screen → Data AccessAdd or remove scopes.

  2. Tick https://www.googleapis.com/auth/gmail.modifyUpdateSave.

After changing scopes here, delete the token file (default ~/.mcp-gmail-tokens.json) and re-run the gmail_auth_start tool so the consent screen prompts again with the new scope set.

4. Create OAuth credentials

  1. APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID.

  2. Application type: Web application.

  3. Name: anything (e.g. mcp-gmail).

  4. Authorized redirect URIs → add http://localhost:3334/auth/callback (must match MCP_GMAIL_REDIRECT_URI).

  5. Create, then copy the Client ID and Client Secret.

Configuration

Environment Variables

Name

Required

Default

Purpose

MCP_GMAIL_CLIENT_ID

yes

OAuth 2.0 Client ID (xxxx.apps.googleusercontent.com).

MCP_GMAIL_CLIENT_SECRET

yes

OAuth 2.0 Client Secret.

MCP_GMAIL_REDIRECT_URI

no

http://localhost:3334/auth/callback

Must match the URI registered in Google Cloud.

MCP_GMAIL_SCOPES

no

https://www.googleapis.com/auth/gmail.modify

Space-separated OAuth scopes.

MCP_GMAIL_AUTH_PORT

no

3334

Port the auth server listens on. Must match the redirect URI port.

MCP_GMAIL_TOKEN_PATH

no

~/.mcp-gmail-tokens.json

Token file location. Override to keep multiple accounts side-by-side.

MCP_GMAIL_ACCESS_LEVEL

no

read

Maximum tool access level to register. One of: read (default — read-only tools only, least privilege), write (adds non-destructive mutations like gmail_draft_create, gmail_label_create, gmail_auth_start), destructive (adds delete tools). Levels nest. Each tool's level is derived from its MCP annotations (readOnlyHint: trueread; destructiveHint: truedestructive; explicit readOnlyHint: false AND destructiveHint: falsewrite; missing annotations → destructive fail-safe); a tool registers when its derived level ≤ the configured level. Unknown values abort startup.

MCP_GMAIL_DOWNLOAD_PATH

no

~/Downloads

Directory where attachment downloads are written.

MCP_GMAIL_INLINE_ATTACHMENT_MAX_BYTES

no

262144 (256 KiB)

Cap on inline-returned attachment bytes; larger attachments must be saved via the download tool.

MCP_GMAIL_AUDIT_LOG

no

writes

Audit-log scope. One of off, writes (record only non-read tool calls), all (record every invocation).

MCP_GMAIL_AUDIT_LOG_PATH

no

~/.local/state/mcp-gmail/audit.jsonl

Path to the JSONL audit log.

MCP_GMAIL_AUDIT_LOG_MAX_BYTES

no

10485760 (10 MiB)

Size-based rotation threshold in bytes. Set to 0 to disable rotation.

MCP_GMAIL_AUDIT_LOG_KEEP

no

5

Number of rotated audit-log files to retain.

NODE_ENV

no

server:*:dev/server:mcp:inspect scripts set development so .env.development loads.

Claude Desktop Configuration

Run bun run build first so dist/mcp-server/index.js exists, then add to your Claude Desktop config:

{
  "mcpServers": {
    "mcp-gmail": {
      "command": "node",
      "args": ["/path/to/mcp-gmail/dist/mcp-server/index.js"],
      "env": {
        "MCP_GMAIL_CLIENT_ID": "your-client-id",
        "MCP_GMAIL_CLIENT_SECRET": "your-client-secret"
      }
    }
  }
}

A starter is in claude-config-sample.json.

Running From Source (Dev)

cp .env.example .env.development
# edit .env.development with your Google OAuth credentials, then:
bun run server:mcp:dev    # MCP server
bun run server:auth:dev   # OAuth server on :3334

Authentication

OAuth runs out-of-band via the standalone auth server:

  1. Start bun run server:auth:dev (listens on http://localhost:3334).

  2. In your MCP client, call the gmail_auth_start tool — it returns a sign-in URL.

  3. Open the URL, sign in with the Google account you want to access, grant the requested scope.

  4. Tokens (including refresh token) are persisted to ~/.mcp-gmail-tokens.json (override with MCP_GMAIL_TOKEN_PATH).

  5. The MCP server reads that file and refreshes tokens transparently when they expire.

To force re-authentication (or if the refresh token is revoked), delete the token file and call gmail_auth_start again.

Scope troubleshooting. If Gmail API calls return 403 after a successful sign-in, inspect the scope field in the token file — Google only grants scopes that are pre-declared on the OAuth consent screen → Data Access tab (see step 3c). If gmail.modify is missing from scope, add it to Data Access, delete the token file, and re-authenticate.

Available Tools

32 tools across six areas, all prefixed gmail_. Each tool's access level (read, write, or destructive) is derived from its MCP annotations (readOnlyHint / destructiveHint), not its name, so the access-level gate (MCP_GMAIL_ACCESS_LEVEL) decides at boot which to register. Default MCP_GMAIL_ACCESS_LEVEL=read exposes only the 12 read tools; write adds non-destructive mutations (draft/label-create/relabel/trash/auth); destructive enables all 32. Default OAuth scope: https://www.googleapis.com/auth/gmail.modify.

auth

Tool

Level

Purpose

gmail_about

read

Server version, scopes, token store path.

gmail_auth_start

write

Returns the URL to start Google OAuth consent.auth-server

gmail_auth_status

read

Whether a token is persisted + scope/expiry metadata.no-token

label

Tool

Level

Purpose

gmail_labels_list

read

List all system + user labels with id and name.

gmail_label_create

write

Create a user label.

gmail_label_update

write

Rename a user label.system-labels

gmail_label_delete

destructive

Delete a user label.system-labels label-delete-effect

message

Tool

Level

Purpose

gmail_messages_search

read

Gmail-query search at message granularity.paginated

gmail_message_get

read

Full message: headers, body, labels, attachments.html-stripmsg-format

gmail_message_raw

read

Write the raw RFC 2822 message to outputPath (e.g. .eml).raw-no-body

gmail_message_label

write

Add label ids to a message.

gmail_message_unlabel

write

Remove label ids from a message.

gmail_message_mark_read

write

Remove the UNREAD label.sugar

gmail_message_mark_unread

write

Add the UNREAD label.sugar

gmail_message_archive

write

Remove the INBOX label.sugar

gmail_message_trash

write

Move to Trash via messages.trash.trash

gmail_messages_batch_modify

write

Add/remove labels on up to 1000 messages in one call.batch-modify

attachment

Tool

Level

Purpose

gmail_attachment_get

read

Download an attachment, to disk via outputPath or inline.attach-inline

gmail_attachment_metadata

read

Get filename, MIME type, size without downloading bytes.attach-metadata

thread

Tool

Level

Purpose

gmail_threads_search

read

Gmail-query search at thread granularity.paginated thread-shape

gmail_thread_get

read

Full thread: every message with headers, body, label ids, attachments.

gmail_thread_label

write

Add label ids to every message in a thread.

gmail_thread_unlabel

write

Remove label ids from every message in a thread.

gmail_thread_mark_read

write

Remove the UNREAD label from every message in the thread.sugar

gmail_thread_mark_unread

write

Add the UNREAD label to every message in the thread.sugar

gmail_thread_archive

write

Remove the INBOX label from every message in the thread.sugar

gmail_thread_trash

write

Move every message in the thread to Trash via threads.trash.trash

draft

Tool

Level

Purpose

gmail_draft_create

write

Create a Gmail draft (saved, never sent).draft-shape

gmail_draft_update

write

Replace an existing draft's contents (same fields as gmail_draft_create).

gmail_drafts_list

read

List drafts with headers + snippet; optional query filter.paginated

gmail_draft_get

read

Get a draft's full headers, body, label ids, and attachment refs.

gmail_draft_delete

destructive

Permanently delete a draft (does not go to Trash).

This server deliberately exposes draft creation but no sending tool. The user reviews drafts in Gmail and clicks Send — Claude never directly delivers mail. The OAuth scope technically permits sending; the MCP surface does not.

Security Model

  • Secrets (MCP_GMAIL_CLIENT_SECRET) come from env vars only; never committed. .env* files are gitignored except .env*.example templates.

  • OAuth tokens live at MCP_GMAIL_TOKEN_PATH (default ~/.mcp-gmail-tokens.json), mode 0600.

  • Token writes are atomic — temp file + rename(). A crash mid-write cannot corrupt the token file.

  • Token values are never logged or returned by any MCP tool. The gmail_auth_status tool exposes presence flags and metadata only.

  • The auth server binds to localhost:3334 only and accepts a single OAuth callback at a time; CSRF state entries expire after 10 minutes.

  • If the token file is lost, revoked, or you want to switch Google accounts, delete the file and re-authenticate.

Troubleshooting

Port 3334 already in use. Another auth-server process is bound to the port. Free it:

bunx kill-port 3334

Gmail API returns 403 after a successful sign-in. The OAuth consent screen didn't pre-declare the scope, so Google silently dropped it. Inspect ~/.mcp-gmail-tokens.json and check the scope field; if gmail.modify is missing, add it via OAuth consent screen → Data Access (step 3c), delete the token file, and re-run the gmail_auth_start tool.

Token revoked or refresh fails. Delete the token file and re-authenticate:

rm ~/.mcp-gmail-tokens.json
# then call the `gmail_auth_start` tool again

Claude Desktop shows no tools / "Cannot find module". The built server isn't where the config points. Rebuild and verify:

bun run build
ls dist/mcp-server/index.js

Then restart Claude Desktop. The args path in the Claude config must point at the compiled dist/mcp-server/index.js, not the TS source.

bun run test:smoke fails with "tool surface mismatch". You've added or removed a tool but the smoke test's expected list is out of sync. Update both scripts/smoke.ts (EXPECTED_TOOLS) and the matching list in src/tool-registration.test.ts.

Refresh token expires every 7 days. Your OAuth consent screen is in Testing mode. Switch to Published under OAuth consent screen → Audience (step 3b) — the app stays unverified for personal use; you'll see a one-time "advanced → continue" warning during sign-in.

Directory Structure

├── claude-config-sample.json   # Example Claude Desktop config
├── .github/workflows/ci.yml    # Lint, typecheck, test:coverage, smoke
├── package.json
├── tsconfig.json               # Base TS config
├── tsconfig.build.json         # Build config (emits to dist/)
├── .env.example                # Template for GMAIL_* env vars
├── scripts/
│   └── smoke.ts                # Wire-level tool-surface smoke test (bun run test:smoke)
├── src/
│   ├── config/index.ts         # loadConfig(env?) → Config (no env read at import)
│   ├── auth-server/index.ts    # Standalone OAuth server (port 3334)
│   ├── mcp-server/index.ts     # MCP server entry — loadConfig() + registers every tool
│   ├── tools/                  # Thin tool defs grouped by resource; call into main/
│   │   ├── auth/               # about, authenticate, check-auth-status
│   │   ├── labels/             # label_list/create/update/delete
│   │   ├── messages/           # message_* (search, get, label, sugar wrappers, batch_modify)
│   │   ├── attachments/        # attachment_get + attachment_get_metadata
│   │   ├── threads/            # thread_* (search, get, label, sugar wrappers)
│   │   └── drafts/             # draft_create/update/list/get/delete
│   ├── main/                   # Real implementation (config injected as first arg)
│   │   ├── auth/               # OAuth2Client + token refresh + atomic token persistence
│   │   ├── gmail-client/       # Gmail payload parsing (headers, body, attachments)
│   │   ├── auth-info/          # about / authenticate / auth-status handlers
│   │   └── {labels,messages,threads,drafts,attachments}/  # one function per tool
│   └── utils/                  # MIME builder, paths, result envelopes, access-level, audit-log, annotations
└── dist/                       # Build output (gitignored, created by `bun run build`)
    └── mcp-server/index.js     # Compiled entry point used by Claude Desktop

Development

bun run server:mcp:dev      # bun --watch, MCP server
bun run server:auth:dev     # bun --watch, OAuth server
bun run server:mcp:start    # build then run from dist/ under node
bun run server:auth:start   # build then run auth server from dist/ under node
bun run server:mcp:inspect  # MCP Inspector against TS source
bun run test                # vitest (use `bun run test`, not `bun test`)
bun run test:coverage       # vitest + 100% threshold enforced
bun run test:smoke          # build + boot server over stdio MCP, assert wire-level tool surface
bun run lint:types          # tsc --noEmit
bun run lint:check          # Biome
bun run lint:fix            # Biome auto-fix (--unsafe)
bun run lint:md             # prettier + markdownlint for *.md

Extending the Server

Add a new tool by registering it in the appropriate module under src/tools/<resource>/ and re-exporting from src/tools/index.ts. Follow the existing pattern:

  1. Pick a resource module (or create a new one) and name the tool gmail_<resource>_<action> (snake_case; plural resource for collection ops). Set annotations to one of the presets in src/utils/annotations.ts (READ_ONLY_REMOTE, WRITE_REMOTE, WRITE_IDEMPOTENT_REMOTE, DESTRUCTIVE_REMOTE) — the access-level gate in src/utils/access-level.ts maps the annotation to read / write / destructive and decides whether to register the tool under the current MCP_GMAIL_ACCESS_LEVEL value, and the audit log uses the derived level as the level field.

  2. Validate inputs with a Zod schema; mark optional fields explicitly.

  3. Set MCP annotations honestly via the constants in src/utils/annotations.ts (READ_ONLY_REMOTE, WRITE_IDEMPOTENT_REMOTE, DESTRUCTIVE_REMOTE, WRITE_REMOTE).

  4. Return successes via jsonResult(...) and failures via errorResult('verbing', err) so the client gets isError: true with a recognisable message.

  5. Update EXPECTED_TOOLS in scripts/smoke.ts and the matching list in src/tool-registration.test.ts so both bun run test:smoke and the unit suite stay in sync.

  1. The auth server must be running on :3334.

  2. Never returns access or refresh token values.

  3. System labels (INBOX, SENT, etc.) cannot be renamed or deleted; Gmail rejects the request.

  4. Gmail removes the label from every message that had it; the messages themselves are untouched.

  5. Returns {<items>, nextPageToken?}. Pass nextPageToken back as pageToken to fetch the next page; it's omitted on the last page.

  6. If the message has no text/plain part, the HTML body is stripped and returned instead.

  7. format defaults to 'full'. Pass 'metadata' to skip the body (headers + labels only, with body and attachments empty) — cheaper when the caller doesn't need content.

  8. Returns {messageId, path, sizeBytes}. The body never travels through the response, so this is safe for messages with large attachments. Subject/date aren't returned — with format=raw Gmail does not break out headers (use gmail_message_get).

  9. With outputPath, writes the decoded bytes and returns {messageId, path, sizeBytes}. Without it, returns {filename, mimeType, data} (base64url) — suitable for small attachments only.

  10. Backed by messages.get(format=full) — fetches the message part tree without downloading the attachment bytes. Returns {messageId, attachmentId, filename, mimeType, sizeBytes}.

  11. Each thread carries id, snippet, messageCount, latest-message headers, and the union of label ids across all messages.

  12. Plain-text body via bodyText, optional rich body via bodyHtml (both → multipart/alternative so plain-text clients still render). Attachments accept either a bare path or {path, filename?, mimeType?} to override either field. With replyToMessageId we wire In-Reply-To, extend References, prepend Re: to Subject, and tie the draft to the right thread. With replyAll: true (requires replyToMessageId), to (= original From + To) and cc (= original Cc) auto-populate, with the authenticated account removed; caller-supplied to / cc win.

  13. Sugar over messages.modify / threads.modify so callers don't have to know the magic system-label id.

  14. Recoverable for ~30 days from Gmail's Trash UI. Permanent deletion (messages.delete / threads.delete) is intentionally not exposed.

  15. Backed by Gmail messages.batchModify. At least one of addLabelIds or removeLabelIds is required. Returns {count, messageIds, addLabelIds, removeLabelIds} echoing the operation; Gmail returns 204 No Content on success.

Install Server
A
license - permissive license
B
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/knowledgeislands/mcp-gmail'

If you have feedback or need assistance with the MCP directory API, please join our Discord server