mcp-gmail
Allows reading, sending, deleting, and managing Gmail messages and labels via the Gmail API.
Click on "Install 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., "@mcp-gmailshow me unread emails from last week"
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.
mcp-gmail
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 trash —
messages.trash/threads.trashonly. Permanent deletion (messages.delete/threads.delete) is deliberately not exposed.Drafts-only outbound — compose plain text + HTML (with
multipart/alternativefallback), 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/listreports 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
Install dependencies:
bun install.Set up Google Cloud credentials — see Google Cloud Console Setup.
Configure environment — copy
.env.exampleto.env.developmentand add your Google OAuth credentials.Build:
bun run build.Configure Claude Desktop with
dist/mcp-server/index.jsand yourMCP_GMAIL_CLIENT_ID/MCP_GMAIL_CLIENT_SECRET(see Configuration).Start the auth server:
bun run server:auth:dev(separate process; handles OAuth onlocalhost:3334).Authenticate — call the
gmail_auth_starttool in your MCP client, follow the URL, sign in. Tokens land at~/.mcp-gmail-tokens.json(mode0600). (gmail_auth_startis annotatedWRITE_REMOTEbecause it persists tokens, so it registers atMCP_GMAIL_ACCESS_LEVEL=writeor higher; the defaultread-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.comfrom 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
newsletterfrom before 2026 to myreading-list/archivelabel and dropnewsletter."
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 installGoogle Cloud Console Setup
1. Create a project
Open the Google Cloud Console.
Project dropdown → New Project.
Name it (e.g.
mcp-gmail) → Create.
2. Enable the Gmail API
APIs & Services → Library.
Search for Gmail API → Enable.
3. Configure the OAuth consent screen
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
APIs & Services → OAuth consent screen → Get Started.
App Information: app name, your support email → Next.
Audience: External → Next.
Contact Information: your email → Next.
Agree to the user-data policy → Continue → Create.
3b. Publish the app
OAuth consent screen → Audience.
Publishing status → Publish App → Confirm. (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.
OAuth consent screen → Data Access → Add or remove scopes.
Tick
https://www.googleapis.com/auth/gmail.modify→ Update → Save.
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
APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID.
Application type: Web application.
Name: anything (e.g.
mcp-gmail).Authorized redirect URIs → add
http://localhost:3334/auth/callback(must matchMCP_GMAIL_REDIRECT_URI).Create, then copy the Client ID and Client Secret.
Configuration
Environment Variables
Name | Required | Default | Purpose |
| yes | — | OAuth 2.0 Client ID ( |
| yes | — | OAuth 2.0 Client Secret. |
| no |
| Must match the URI registered in Google Cloud. |
| no |
| Space-separated OAuth scopes. |
| no |
| Port the auth server listens on. Must match the redirect URI port. |
| no |
| Token file location. Override to keep multiple accounts side-by-side. |
| no |
| Maximum tool access level to register. One of: |
| no |
| Directory where attachment downloads are written. |
| no |
| Cap on inline-returned attachment bytes; larger attachments must be saved via the download tool. |
| no |
| Audit-log scope. One of |
| no |
| Path to the JSONL audit log. |
| no |
| Size-based rotation threshold in bytes. Set to |
| no |
| Number of rotated audit-log files to retain. |
| no | — |
|
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 :3334Authentication
OAuth runs out-of-band via the standalone auth server:
Start
bun run server:auth:dev(listens onhttp://localhost:3334).In your MCP client, call the
gmail_auth_starttool — it returns a sign-in URL.Open the URL, sign in with the Google account you want to access, grant the requested scope.
Tokens (including refresh token) are persisted to
~/.mcp-gmail-tokens.json(override withMCP_GMAIL_TOKEN_PATH).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 |
|
| Server version, scopes, token store path. |
|
| Returns the URL to start Google OAuth consent.auth-server |
|
| Whether a token is persisted + scope/expiry metadata.no-token |
label
Tool | Level | Purpose |
|
| List all system + user labels with |
|
| Create a user label. |
|
| Rename a user label.system-labels |
|
| Delete a user label.system-labels label-delete-effect |
message
Tool | Level | Purpose |
|
| Gmail-query search at message granularity.paginated |
|
| Full message: headers, body, labels, attachments.html-stripmsg-format |
|
| Write the raw RFC 2822 message to |
|
| Add label ids to a message. |
|
| Remove label ids from a message. |
|
| Remove the |
|
| Add the |
|
| Remove the |
|
| Move to Trash via |
|
| Add/remove labels on up to 1000 messages in one call.batch-modify |
attachment
Tool | Level | Purpose |
|
| Download an attachment, to disk via |
|
| Get filename, MIME type, size without downloading bytes.attach-metadata |
thread
Tool | Level | Purpose |
|
| Gmail-query search at thread granularity.paginated thread-shape |
|
| Full thread: every message with headers, body, label ids, attachments. |
|
| Add label ids to every message in a thread. |
|
| Remove label ids from every message in a thread. |
|
| Remove the |
|
| Add the |
|
| Remove the |
|
| Move every message in the thread to Trash via |
draft
Tool | Level | Purpose |
|
| Create a Gmail draft (saved, never sent).draft-shape |
|
| Replace an existing draft's contents (same fields as |
|
| List drafts with headers + snippet; optional |
|
| Get a draft's full headers, body, label ids, and attachment refs. |
|
| 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*.exampletemplates.OAuth tokens live at
MCP_GMAIL_TOKEN_PATH(default~/.mcp-gmail-tokens.json), mode0600.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_statustool exposes presence flags and metadata only.The auth server binds to
localhost:3334only 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 3334Gmail 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 againClaude 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.jsThen 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 DesktopDevelopment
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 *.mdExtending 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:
Pick a resource module (or create a new one) and name the tool
gmail_<resource>_<action>(snake_case; plural resource for collection ops). Setannotationsto one of the presets insrc/utils/annotations.ts(READ_ONLY_REMOTE,WRITE_REMOTE,WRITE_IDEMPOTENT_REMOTE,DESTRUCTIVE_REMOTE) — the access-level gate insrc/utils/access-level.tsmaps the annotation toread/write/destructiveand decides whether to register the tool under the currentMCP_GMAIL_ACCESS_LEVELvalue, and the audit log uses the derived level as thelevelfield.Validate inputs with a Zod schema; mark optional fields explicitly.
Set MCP annotations honestly via the constants in
src/utils/annotations.ts(READ_ONLY_REMOTE,WRITE_IDEMPOTENT_REMOTE,DESTRUCTIVE_REMOTE,WRITE_REMOTE).Return successes via
jsonResult(...)and failures viaerrorResult('verbing', err)so the client getsisError: truewith a recognisable message.Update
EXPECTED_TOOLSinscripts/smoke.tsand the matching list insrc/tool-registration.test.tsso bothbun run test:smokeand the unit suite stay in sync.
The auth server must be running on
↩:3334.Never returns access or refresh token values.
↩System labels (INBOX, SENT, etc.) cannot be renamed or deleted; Gmail rejects the request.
↩Gmail removes the label from every message that had it; the messages themselves are untouched.
↩Returns
↩{<items>, nextPageToken?}. PassnextPageTokenback aspageTokento fetch the next page; it's omitted on the last page.If the message has no
↩text/plainpart, the HTML body is stripped and returned instead.
↩formatdefaults to'full'. Pass'metadata'to skip the body (headers + labels only, withbodyandattachmentsempty) — cheaper when the caller doesn't need content.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 — withformat=rawGmail does not break out headers (usegmail_message_get).With
↩outputPath, writes the decoded bytes and returns{messageId, path, sizeBytes}. Without it, returns{filename, mimeType, data}(base64url) — suitable for small attachments only.Backed by
↩messages.get(format=full)— fetches the message part tree without downloading the attachment bytes. Returns{messageId, attachmentId, filename, mimeType, sizeBytes}.Each thread carries
↩id,snippet,messageCount, latest-message headers, and the union of label ids across all messages.Plain-text body via
↩bodyText, optional rich body viabodyHtml(both →multipart/alternativeso plain-text clients still render). Attachments accept either a bare path or{path, filename?, mimeType?}to override either field. WithreplyToMessageIdwe wireIn-Reply-To, extendReferences, prependRe:to Subject, and tie the draft to the right thread. WithreplyAll: true(requiresreplyToMessageId),to(= original From + To) andcc(= original Cc) auto-populate, with the authenticated account removed; caller-suppliedto/ccwin.Sugar over
↩messages.modify/threads.modifyso callers don't have to know the magic system-label id.Recoverable for ~30 days from Gmail's Trash UI. Permanent deletion (
↩messages.delete/threads.delete) is intentionally not exposed.Backed by Gmail
↩messages.batchModify. At least one ofaddLabelIdsorremoveLabelIdsis required. Returns{count, messageIds, addLabelIds, removeLabelIds}echoing the operation; Gmail returns 204 No Content on success.
Maintenance
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