Gmail Streamable Server
FreeNot checkedStreamable HTTP MCP server for Gmail that enables AI agents to search threads, read messages, manage drafts, and organize the inbox via OAuth authentication.
About
Streamable HTTP MCP server for Gmail that enables AI agents to search threads, read messages, manage drafts, and organize the inbox via OAuth authentication.
README
Fetch-native MCP server for Gmail — search threads, read messages, manage drafts, and organize your inbox on Bun or Cloudflare Workers.
Author: overment
[!IMPORTANT] This branch targets the candidate
2026-07-28protocol using exact@modelcontextprotocol/[email protected]and@modelcontextprotocol/[email protected]. These packages are prereleases. Do not claim final2026-07-28conformance until the dated specification and stable packages are published and the final-delta validation passes.
[!WARNING] You connect this server to your MCP client at your own responsibility. Language models can make mistakes or perform unintended actions. Review tool outputs, verify writes in Gmail, and prefer small, incremental changes. Remote deployments still need TLS, rate limiting, audit logging, secret management, and compliance with Google OAuth policies.
The same tool and OAuth product behavior is available in two runtimes:
- Bun with the MCP resource server on
PORTand OAuth proxy onPORT + 1 - Cloudflare Workers with MCP, discovery, and OAuth proxy routes on one origin
Motivation
Gmail's API is powerful but not LLM-friendly out of the box. This server focuses on:
- Let LLMs understand inbox state in a single action (
inbox_overview) instead of multiple queries - Provide enriched search results with subject, sender, date — not just thread IDs
- Support batch operations (
modify_threadhandles up to 100 threads at once) - Map API responses into human-readable feedback useful for both LLM and user
- Safer write flow: drafts first, send explicitly
In short, it's not a direct mirror of Gmail's API — it's tailored so AI agents know exactly how to use it effectively.
Features
- ✅ Overview — Get inbox stats + highlights (unread, starred, recent threads)
- ✅ Search — Find threads with Gmail query syntax, enriched results
- ✅ Read — Get full threads and messages with body content
- ✅ Labels — Discover label IDs for filtering and organizing
- ✅ Modify — Batch archive, star, mark read/unread (up to 100 threads)
- ✅ Drafts — Create, update, and send drafts with reply threading
- ✅ OAuth 2.1 — CIMD + PKCE proxy with opaque MCP resource-token mapping
- ✅ Credential separation — MCP bearer tokens are never sent to Gmail; provider refresh tokens stay in storage
- ✅ Dual Runtime — Bun and Cloudflare Workers
- ✅ MCP v2 candidate — Fetch-native
2026-07-28with SDK stateless legacy fallback
Design Principles
- LLM-friendly: Tools are simplified, not 1:1 Gmail API mirrors
- Discovery-first:
inbox_overviewandlist_labelshelp avoid guessing - Batch-first:
modify_threadaccepts arrays to minimize tool calls - Safer writes: Drafts first, send explicitly
- Clear feedback: Summaries with structured content and next steps
Installation
Prerequisites: Bun 1.2+, a Google account, and a Gmail-enabled Google Cloud project. Cloudflare deployment also requires a Cloudflare account and Wrangler 4.
Ways to Run (Pick One)
- Local + OAuth (recommended)
- Cloudflare Worker (wrangler dev) — Local Worker testing
- Cloudflare Worker (deploy) — Remote production
1. Local + OAuth (Recommended)
- Go to Google Cloud Console
- Create a project and enable the Gmail API
- Create OAuth 2.0 Client ID (Web application)
- Set redirect URIs:
http://127.0.0.1:3001/oauth/callback alice://oauth/callback - Copy Client ID and Secret
cd gmail-mcp
bun install
cp env.example .env
Edit .env:
PORT=3000
AUTH_ENABLED=true
AUTH_STRATEGY=oauth
PROVIDER_CLIENT_ID=your-client-id.apps.googleusercontent.com
PROVIDER_CLIENT_SECRET=your-client-secret
PROVIDER_ACCOUNTS_URL=https://accounts.google.com
OAUTH_AUTHORIZATION_URL=https://accounts.google.com/o/oauth2/v2/auth
OAUTH_TOKEN_URL=https://oauth2.googleapis.com/token
OAUTH_REVOCATION_URL=https://oauth2.googleapis.com/revoke
OAUTH_SCOPES=https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.compose https://www.googleapis.com/auth/gmail.modify
OAUTH_REDIRECT_URI=alice://oauth/callback
OAUTH_REDIRECT_ALLOWLIST=alice://oauth/callback,http://127.0.0.1:3001/oauth/callback
OAUTH_EXTRA_AUTH_PARAMS=access_type=offline&prompt=consent
Run:
bun dev
# MCP: http://127.0.0.1:3000/mcp
# OAuth: http://127.0.0.1:3001
Tip: The Authorization Server runs on PORT + 1.
2. Cloudflare Worker (Local Dev)
Create an ignored .dev.vars for local Worker secrets:
PROVIDER_CLIENT_ID=your-client-id.apps.googleusercontent.com
PROVIDER_CLIENT_SECRET=your-client-secret
RS_TOKENS_ENC_KEY=your-base64url-32-byte-key
Then run:
bun run dev:worker
Endpoint: http://127.0.0.1:8787/mcp
3. Cloudflare Worker (Deploy)
- Create KV namespace:
bun x wrangler kv namespace create TOKENS
Replace the placeholder URLs, host allowlist, and KV namespace ID in
wrangler.jsonc.Set secrets:
bun x wrangler secret put PROVIDER_CLIENT_ID
bun x wrangler secret put PROVIDER_CLIENT_SECRET
# Generate encryption key (32-byte base64url):
openssl rand -base64 32 | tr -d '=' | tr '+/' '-_'
bun x wrangler secret put RS_TOKENS_ENC_KEY
Note:
RS_TOKENS_ENC_KEYencrypts OAuth tokens stored in KV (AES-256-GCM).
Update the redirect URI and allowlist in
wrangler.jsonc.Add Workers URL to your Google OAuth app's redirect URIs
Deploy:
bun x wrangler deploy
Endpoint: https://<worker-name>.<account>.workers.dev/mcp
Client Configuration
MCP Inspector (quick test):
bunx @modelcontextprotocol/inspector
# Connect to: http://localhost:3000/mcp
Claude Desktop / Cursor:
{
"mcpServers": {
"gmail": {
"command": "bunx",
"args": ["mcp-remote", "http://127.0.0.1:3000/mcp", "--transport", "http-only"],
"env": { "NO_PROXY": "127.0.0.1,localhost" }
}
}
}
For Cloudflare, replace URL with https://<worker-name>.<account>.workers.dev/mcp.
Token and request flow
OAuth authorization state is product state, not an MCP transport session. It remains in TokenStore implementations while every MCP request receives a fresh SDK server.
sequenceDiagram
participant C as MCP client
participant AS as Gmail MCP OAuth proxy
participant G as Google OAuth
participant S as TokenStore (file/KV + memory)
participant R as /mcp resource server
participant API as Gmail API
C->>AS: GET /authorize (CIMD client_id, PKCE challenge)
AS->>S: Save short-lived authorization transaction
AS->>G: Redirect to Google authorization
G->>AS: GET /oauth/callback (provider code)
AS->>G: Exchange code for Google access + refresh tokens
AS->>S: Keep provider tokens in authorization transaction
AS->>C: Redirect with one-time proxy authorization code
C->>AS: POST /token (code + PKCE verifier)
AS->>S: Store opaque MCP access/refresh -> provider-token record
AS-->>C: Opaque MCP access + refresh tokens
C->>R: MCP request with opaque MCP bearer
R->>S: Validate record; refresh provider access if near expiry
S-->>R: Provider access token only
R->>API: Gmail request with provider access token
API-->>R: Gmail response
R-->>C: MCP tool result
Trust boundary rules:
- The inbound MCP bearer is retained only as
AuthInfo.tokenby the SDK auth boundary; tools cannot read or forward it. OAuth mode always requires a valid opaque RS record; the older permissiveAUTH_REQUIRE_RSandAUTH_ALLOW_DIRECT_BEARERtoggles were removed. - The current Gmail access token is exposed to tools only as
AuthInfo.extra.gmailAccessTokenthrough the project-specific context adapter. - Google refresh tokens never enter
AuthInfo, tool context, MCP content, or structured output. /authorize,/oauth/callback,/token,/revoke,/register, and discovery routes are dispatched before and outside MCP handling.- Modern requests and legacy fallback are stateless and never create
Mcp-Session-Id.
Tools
get_profile
Get the connected Gmail account email. Call to confirm which account is active.
// Input
{}
// Output
{ email: "[email protected]" }
inbox_overview
Get inbox stats + highlights for a time range. Call this first for a quick summary.
// Input
{
days?: number; // 1-365, default: 7
}
// Output
{
period: "last 7 days",
counts: { total, unread, inbox, sent, starred, important? },
highlights?: {
recentUnread: Array<{ id, subject?, from? }>,
starred: Array<{ id, subject?, from? }>
},
meta?: { nextSteps? }
}
list_labels
Discover label IDs and names. Use before filtering by labelIds.
// Input
{}
// Output
{
items: Array<{ id, name, type?, messagesTotal?, threadsTotal? }>,
meta?: { nextSteps?, relatedTools? }
}
search_threads
Search threads with Gmail query syntax. Returns enriched results.
// Input
{
query?: string; // Gmail search: "from:alice newer_than:7d"
labelIds?: string[];
includeSpamTrash?: boolean;
limit?: number; // 1-50, default: 25
cursor?: string;
}
// Output
{
items: Array<{
id, subject?, from?, date?, snippet?,
messageCount?, isUnread?, webUrl?
}>,
pagination?: { hasMore, nextCursor?, itemsReturned, limit },
meta?: { nextSteps?, hints?, relatedTools? }
}
get_thread
Get a full thread with all messages.
// Input
{
threadId: string;
format?: "minimal" | "metadata" | "full" | "raw";
metadataHeaders?: string[];
maxBodyChars?: number;
}
// Output
{
thread: { id, historyId?, messageCount, messages: [...], webUrl? },
meta?: { nextSteps?, relatedTools? }
}
get_message
Fetch a single message with full content.
// Input
{
messageId: string;
format?: "minimal" | "metadata" | "full" | "raw";
metadataHeaders?: string[];
maxBodyChars?: number;
}
// Output
{
message: { id, threadId?, snippet?, headers?, body?, webUrl? },
meta?: { nextSteps?, relatedTools? }
}
modify_thread
Batch add/remove labels on threads (up to 100). Supports convenience actions.
// Input
{
threadIds: string[]; // 1-100 thread IDs
addLabelIds?: string[];
removeLabelIds?: string[];
actions?: {
archive?: boolean; // Remove INBOX
unarchive?: boolean; // Add INBOX
markRead?: boolean; // Remove UNREAD
markUnread?: boolean; // Add UNREAD
star?: boolean; // Add STARRED
unstar?: boolean; // Remove STARRED
trash?: boolean;
untrash?: boolean;
};
}
// Output
{
results: Array<{ threadId, success, error? }>,
summary: { total, succeeded, failed },
applied: { addLabelIds?, removeLabelIds? },
meta?: { nextSteps?, relatedTools? }
}
create_draft
Create a draft from structured fields or raw MIME.
// Input
{
to?: string | string[]; // Required unless raw provided
cc?: string | string[];
bcc?: string | string[];
subject?: string;
text?: string;
html?: string;
threadId?: string; // For replies
inReplyTo?: string; // Message-ID for threading
raw?: string; // base64url RFC 2822
}
// Output
{
draft: { id, messageId?, threadId?, snippet? },
meta?: { nextSteps?, relatedTools? }
}
update_draft
Replace a draft's content (Gmail drafts are immutable internally).
// Input
{
draftId: string;
to?: string | string[];
cc?: string | string[];
bcc?: string | string[];
subject?: string;
text?: string;
html?: string;
threadId?: string;
raw?: string;
}
send_draft
Send a draft. Optionally update it before sending.
// Input
{
draftId: string;
to?: string | string[]; // Override before send
cc?: string | string[];
bcc?: string | string[];
subject?: string;
text?: string;
html?: string;
threadId?: string;
raw?: string;
}
// Output
{
sent: { id, threadId?, labelIds?, snippet?, webUrl? },
meta?: { nextSteps?, relatedTools? }
}
Examples
1. Get inbox summary
{ "name": "inbox_overview", "arguments": { "days": 7 } }
Response:
Inbox (last 7 days): 42 unread, 156 inbox, 12 sent, 3 starred
Recent unread:
Alice: Meeting tomorrow at 3pm
GitHub: PR merged in project-x
Starred:
Boss: Q4 Planning document
2. Search for unread emails from a sender
{
"name": "search_threads",
"arguments": {
"query": "from:[email protected] is:unread newer_than:7d",
"limit": 10
}
}
3. Read a thread
{
"name": "get_thread",
"arguments": {
"threadId": "19be18067165251d",
"format": "full"
}
}
4. Archive multiple threads
{
"name": "modify_thread",
"arguments": {
"threadIds": ["19be18067165251d", "19be17f8a2c3b4d5"],
"actions": { "archive": true, "markRead": true }
}
}
Response:
Modified 2/2 threads. -INBOX -UNREAD
5. Reply to a thread (draft first)
{
"name": "create_draft",
"arguments": {
"threadId": "19be18067165251d",
"to": "[email protected]",
"text": "Thanks, I'll be there!"
}
}
{
"name": "send_draft",
"arguments": { "draftId": "r8651610029774" }
}
HTTP Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/mcp |
POST | MCP JSON-RPC 2.0 |
/mcp |
GET / DELETE | 405 (MCP protocol sessions are not used) |
/health |
GET | Health check |
/.well-known/oauth-authorization-server |
GET | OAuth AS metadata |
/.well-known/oauth-protected-resource/mcp |
GET | RFC 9728 OAuth protected-resource metadata |
/.well-known/oauth-protected-resource |
GET | Backward-compatible OAuth RS metadata alias |
/.well-known/oauth-authorization-server |
GET | OAuth authorization-server metadata |
OAuth proxy (PORT + 1 on Bun; same origin on Workers):
GET /authorize— Start OAuth flowGET /oauth/callback— Provider callbackPOST /token— Token exchangePOST /revoke— Revoke tokens
Development
bun dev # Bun MCP + OAuth proxy
bun run typecheck # Bun and Worker TypeScript checks
bun run test # Protocol, OAuth, provider, and storage tests
bun run lint # Biome check
bun run format:check # Formatting check
bun run build # Bun bundle
bun run build:worker # Wrangler dry-run bundle
bun run types:worker:check # Generated binding type check
bun start # Run Bun production entry
For an actual local workerd protocol check, start wrangler.test.jsonc and run the official-client probe in another terminal:
bunx wrangler dev --config wrangler.test.jsonc --env-file wrangler.types.env
bun run test:workerd-client
Architecture
src/
├── shared/
│ ├── tools/
│ │ └── gmail/ # Gmail tools shared by Bun and Workers
│ │ ├── get-profile.ts
│ │ ├── inbox-overview.ts
│ │ ├── list-labels.ts
│ │ ├── search-threads.ts
│ │ ├── get-thread.ts
│ │ ├── get-message.ts
│ │ ├── modify-thread.ts
│ │ ├── create-draft.ts
│ │ ├── update-draft.ts
│ │ └── send-draft.ts
│ ├── oauth/ # OAuth flow (PKCE, discovery)
│ └── storage/ # Token storage (file, KV, memory)
├── core/ # Fresh-server MCP v2 factory/runtime
├── http/ # Fetch-native security, auth gate, and routing
├── services/
│ └── gmail.ts # Unchanged Gmail API client behavior
├── schemas/ # Complete Zod 4 input/output schemas
├── index.ts # Bun dual-port entry
└── worker.ts # Workers entry
Candidate status, storage compatibility, and rollback
- Candidate baseline: protocol
2026-07-28, server/client2.0.0-beta.5, Zod 4. This is not the final-release gate. - Pre-migration repository baseline:
48d4ca49f3dba5621bb739608bcb2e483f1c14d6. FileTokenStoreremains version 1 and reads the existing plaintext or whole-file AES-256-GCM representation.- Worker KV keys and values are unchanged:
rs:access:*,rs:refresh:*,txn:*,code:*, andsession:*; optional AES-GCM wrapping is unchanged. - No migration rewrites, deletes, or invalidates provider refresh tokens or OAuth records; existing FileTokenStore expiry and provider-refresh behavior is preserved.
- Roll back application code by redeploying the recorded baseline (or reverting the migration changes) without clearing
.data, KV, MCP resource tokens, or Google refresh tokens. Both versions can read the same stored records.
Troubleshooting
| Issue | Solution |
|---|---|
| "Unauthorized" | Complete OAuth flow again; refresh token may be revoked. |
| "Invalid Credentials" | Ensure OAUTH_SCOPES match your Google app and user consent. |
| "Insufficient Permission" | Add gmail.modify scope for modify_thread. |
| "Rate Limit Exceeded" | Slow down requests; use smaller limits. |
| "Thread not found" | Thread IDs expire; search again to get fresh IDs. |
| Draft update fails | Drafts are immutable; updates replace the underlying message. |
| OAuth does not start (Worker) | curl -i -X POST https://<worker>/mcp should return 401 with WWW-Authenticate. |
| Empty search results | Check query syntax; use list_labels to verify label IDs. |
| KV namespace error | Run wrangler kv namespace create TOKENS and update wrangler.jsonc. |
License
MIT
Installing Gmail Streamable Server
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/iceener/gmail-streamable-mcp-serverFAQ
Is Gmail Streamable Server MCP free?
Yes, Gmail Streamable Server MCP is free — one-click install via Unyly at no cost.
Does Gmail Streamable Server need an API key?
No, Gmail Streamable Server runs without API keys or environment variables.
Is Gmail Streamable Server hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Gmail Streamable Server in Claude Desktop, Claude Code or Cursor?
Open Gmail Streamable Server on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Gmail
Read, send and search emails from Claude
by GoogleSlack
Send, search and summarize Slack messages
by SlackRunbear
No-code MCP client for team chat platforms, such as Slack, Microsoft Teams, and Discord.
Discord Server
A community discord server dedicated to MCP by [Frank Fiegel](https://github.com/punkpeye)
Klavis AI
Open Source MCP Infra. Hosted MCP servers and MCP clients on Slack and Discord.
Work90210/APIFold
Turn any REST API into a hosted MCP server. 18 free public servers (GitHub, Stripe, Slack, OpenAI, Notion, and more) — no setup required, bring your own API key
by Work90210arikusi/deepseek-mcp-server
MCP server for DeepSeek AI with chat, reasoning, multi-turn sessions, function calling, thinking mode, and cost tracking.
by arikusihashgraph-online/hashnet-mcp-js
MCP server for the Registry Broker. Discover, register, and chat with AI agents on the Hashgraph network.
by hashgraph-onlineprofullstack/mcp-server
A comprehensive MCP server aggregating 20+ tools including SEO optimization, document conversion, domain lookup, email validation, QR generation, weather data,
by profullstackWayStation-ai/mcp
Seamlessly and securely connect Claude Desktop and other MCP hosts to your favorite apps (Notion, Slack, Monday, Airtable, etc.). Takes less than 90 secs.
by waystation-aiCompare Gmail Streamable Server with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All communication MCPs
