safe-gmail-mcp
v0.2.1
Published
Security-focused local MCP server for reading Gmail headers and explicitly confirming Gmail sends.
Downloads
507
Maintainers
Readme
Safe Gmail MCP
Safe Gmail MCP is a local Model Context Protocol server for safely reading Gmail headers and sending Gmail messages through an explicit two-step confirmation flow.
It requests:
https://www.googleapis.com/auth/gmail.modifyThat scope is required to read messages and apply a processed label. It does
not request delete, settings, forwarding, attachment download, or
mail.google.com scopes.
Project Note
Safe Gmail MCP was initially built as an internal tool for BLISS-AI. Much of the implementation was AI-assisted, and not every line has been thoroughly vetted. The tool was created with good intentions and a security-first design, but you should still review it before relying on it for sensitive workflows.
Please use it, let us know if you face any issues, and send improvements if you find something that should be tightened. We plan to keep maintaining it.
Try Bliss AI: https://www.MeditatewithBliss.com
Security Model
- OAuth uses a localhost-only browser flow with PKCE and CSRF
state. - The redirect URI is loopback only:
http://127.0.0.1:<port>/oauth/callback. - Tokens stay on the user's machine under
~/.safe-gmail-mcp/. - Token files are written with restrictive permissions,
0600where the OS supports it. - Access tokens, refresh tokens, client secrets, email bodies, and attachment contents are never logged.
- Unread checks return headers and snippets only. Reading a body requires a
separate
read_email_bodytool call. - After a body is read, the server applies the Gmail label
Safe Gmail MCP/Processedand leaves Gmail's unread state unchanged. - Sending is disabled unless
SAFE_GMAIL_MCP_ENABLE_SEND=true. - Bulk sending is disabled unless both
SAFE_GMAIL_MCP_ENABLE_SEND=trueandSAFE_GMAIL_MCP_ENABLE_BULK_SEND=trueare set. - Email cannot be sent from a single tool call. The client must call
prepare_send_email, inspect the returned digest and preview, then callconfirm_send_emailwith the exact digest. - Bulk email follows the same prepare/confirm pattern with a separate batch digest and a 25-message maximum batch size.
- Pending sends expire after 10 minutes by default.
- v1 does not support arbitrary local file attachments.
Install from npm
Safe Gmail MCP is published on npm:
https://www.npmjs.com/package/safe-gmail-mcpRequirements:
- Node.js 20 or newer
- npm
npm install -g safe-gmail-mcpThen connect Gmail:
safegmail connectBy default, this fetches Safe Gmail's default Google OAuth app metadata from a BLISS-controlled HTTPS endpoint and opens Google login directly. The local browser UI also has a Use my own Google OAuth app option for users who prefer their own Google Cloud project.
You can also run it without a global install:
npx -y safe-gmail-mcp serveFor a quick CLI check:
npx -y safe-gmail-mcp --helpGoogle OAuth Setup
Public/native OAuth clients cannot keep a client secret as a real package secret. This package does not commit or publish the default Google OAuth client ID or client secret. Instead, it fetches default OAuth app metadata at runtime from:
https://admin.meditatewithbliss.com/api/safe-gmail-mcp/oauth-clientThat endpoint is a plain unauthenticated GET API that returns only OAuth app metadata, not user tokens. Safe Gmail MCP still uses the OAuth Authorization Code flow with PKCE, and Gmail tokens stay on the user's machine.
Expected endpoint shape:
{
"clientId": "YOUR_DEFAULT_CLIENT_ID.apps.googleusercontent.com",
"clientSecret": "YOUR_DEFAULT_CLIENT_SECRET",
"scopes": ["https://www.googleapis.com/auth/gmail.modify"]
}The simplest path is:
safegmail connectClick Connect Gmail and complete Google login. Gmail tokens stay only on your machine.
If you do not want to trust the fetched default OAuth app, click Use my own
Google OAuth app in the local UI. Paste your Google Desktop OAuth client ID
and client secret there; they will be saved only to
~/.safe-gmail-mcp/config.json with restrictive permissions.
When BYO credentials are saved, safegmail connect shows the saved client ID,
whether a secret is saved, and actions to change it, delete it, or return to the
fetched default Safe Gmail OAuth app.
You can also provide it with an environment variable:
export SAFE_GMAIL_MCP_GOOGLE_CLIENT_ID="YOUR_CLIENT_ID.apps.googleusercontent.com"
export SAFE_GMAIL_MCP_GOOGLE_CLIENT_SECRET="YOUR_CLIENT_SECRET"or directly in ~/.safe-gmail-mcp/config.json:
{
"googleClientId": "YOUR_CLIENT_ID.apps.googleusercontent.com",
"googleClientSecret": "YOUR_CLIENT_SECRET"
}Broad public distribution with the Gmail modify scope may require Google OAuth app verification. This project does not include a hosted token broker; tokens stay local.
Auth Flow
safegmail connectThe long-form command is also supported:
safe-gmail-mcp authThe CLI starts a web server bound only to 127.0.0.1 on a random available port
and opens:
http://127.0.0.1:<port>The page shows a Continue with Google button and a short safety note. If default
OAuth metadata can be fetched, Continue with Google opens Google login directly.
If it cannot be fetched, the page falls back to BYO OAuth app fields. The page also offers
Use my own Google OAuth app for local-only BYO credentials. After Google
redirects back to the loopback callback, the CLI verifies state, exchanges
the code using PKCE, writes tokens under ~/.safe-gmail-mcp/, and exits.
Check status:
safe-gmail-mcp auth statusDisconnect Gmail tokens only:
safegmail disconnectThis deletes ~/.safe-gmail-mcp/tokens.json. It does not delete saved BYO
OAuth app credentials, recipient policy, pending sends, or the audit log.
Delete all local Safe Gmail MCP state before uninstalling:
safegmail disconnect --allThis deletes ~/.safe-gmail-mcp/, including Gmail tokens, saved BYO OAuth app
credentials, pending sends, config, and audit log.
The long-form token-only logout remains available:
safe-gmail-mcp auth logoutMCP Client Config
Claude Desktop example:
{
"mcpServers": {
"safe-gmail": {
"command": "npx",
"args": ["-y", "safe-gmail-mcp", "serve"],
"env": {
"SAFE_GMAIL_MCP_ENABLE_SEND": "false",
"SAFE_GMAIL_MCP_ENABLE_BULK_SEND": "false"
}
}
}
}Set SAFE_GMAIL_MCP_ENABLE_SEND=true only after you are comfortable with the
confirmation flow and local recipient policy.
Set SAFE_GMAIL_MCP_ENABLE_BULK_SEND=true only if you also want confirmed bulk
batches to send.
Tools
prepare_send_email
Inputs:
{
"to": ["[email protected]"],
"cc": ["[email protected]"],
"bcc": ["[email protected]"],
"subject": "Subject",
"body": "Plain text body",
"htmlBody": "<p>Optional HTML alternative</p>"
}This validates recipients, applies allowlist/blocklist rules, writes a local pending send record, and returns:
- pending ID
- SHA-256 digest of the canonical payload
- preview with recipients, subject, body length, HTML presence, and expiry
It does not send email.
confirm_send_email
Inputs:
{
"pendingId": "PENDING_ID_FROM_PREPARE",
"digest": "DIGEST_FROM_PREPARE"
}This sends only when:
SAFE_GMAIL_MCP_ENABLE_SEND=true- Gmail is authenticated
- the pending record exists
- the pending record has not expired
- the digest exactly matches the canonical payload
Returns the Gmail message ID on success.
list_pending_sends
Lists pending IDs, recipients, subject, created time, expiry time, and digest. It does not show the full body by default.
discard_pending_send
Deletes a pending send without sending it.
prepare_bulk_send
Inputs:
{
"messages": [
{
"to": ["[email protected]"],
"subject": "Subject",
"body": "Plain text body",
"htmlBody": "<p>Optional HTML alternative</p>"
}
]
}This validates and stages up to 25 email messages as one batch. It applies the same recipient allowlist/blocklist rules to every message, stores a local pending bulk record, and returns:
- pending bulk ID
- SHA-256 digest of the canonical batch payload
- preview with message count, recipients, subjects, and expiry
It does not send email.
confirm_bulk_send
Inputs:
{
"pendingBulkId": "PENDING_BULK_ID_FROM_PREPARE",
"digest": "DIGEST_FROM_PREPARE"
}This sends the prepared batch sequentially only when:
SAFE_GMAIL_MCP_ENABLE_SEND=trueSAFE_GMAIL_MCP_ENABLE_BULK_SEND=true- Gmail is authenticated
- the pending bulk record exists
- the pending bulk record has not expired
- the digest exactly matches the canonical batch payload
Returns Gmail message IDs and sent count on success.
list_pending_bulk_sends
Lists pending bulk IDs, message counts, recipients, subjects, created time, expiry time, and digest. It does not show full bodies by default.
discard_pending_bulk_send
Deletes a pending bulk send without sending it.
list_unread_email_headers
Inputs:
{
"maxResults": 10
}Lists unread Gmail messages that have not been labeled
Safe Gmail MCP/Processed. It returns message ID, thread ID, sender,
recipients, subject, date, snippet, and label IDs. It does not return the email
body.
read_email_body
Inputs:
{
"messageId": "GMAIL_MESSAGE_ID"
}Reads one email body by message ID, returns headers plus text and HTML body
content, then applies the Safe Gmail MCP/Processed Gmail label. It does not
remove Gmail's UNREAD label.
Config Reference
Config file:
~/.safe-gmail-mcp/config.jsonExample:
{
"googleClientId": "YOUR_CLIENT_ID.apps.googleusercontent.com",
"googleClientSecret": "YOUR_CLIENT_SECRET",
"allowedRecipients": ["example.com", "[email protected]"],
"blockedRecipients": ["[email protected]", "blocked-domain.example"],
"pendingTtlMinutes": 10,
"fromEmail": "[email protected]"
}Allowlist behavior:
- exact email entries match one address
- domain entries match any address at that exact domain
- if an allowlist exists, all other recipients are blocked
Blocklist behavior:
- exact email entries block one address
- domain entries block any address at that exact domain
- blocklist takes precedence over allowlist
fromEmail is optional and only controls the RFC 822 From header. Gmail still
sends as the authenticated account or configured Gmail alias.
Environment Variables
| Variable | Purpose |
| --- | --- |
| SAFE_GMAIL_MCP_GOOGLE_CLIENT_ID | Overrides the config or fetched default OAuth client ID. |
| SAFE_GMAIL_MCP_GOOGLE_CLIENT_SECRET | Overrides the config or fetched default Google Desktop app client secret. |
| SAFE_GMAIL_MCP_DEFAULT_OAUTH_URL | Overrides the default OAuth metadata endpoint. |
| SAFE_GMAIL_MCP_ENABLE_SEND=true | Enables confirm_send_email to actually call Gmail. |
| SAFE_GMAIL_MCP_ENABLE_BULK_SEND=true | Enables confirm_bulk_send; also requires SAFE_GMAIL_MCP_ENABLE_SEND=true. |
Local Files
Safe Gmail MCP stores local state outside the repository:
~/.safe-gmail-mcp/
config.json
tokens.json
pending/
pending-bulk/
audit.logThe audit log records timestamp, action, recipients, subject, digest, result, and a short error summary. It does not record full bodies or tokens.
Limitations
- Gmail access uses
gmail.modifyso the server can read messages and apply its processed label. - Header listing is separate from body reading; list tools do not return full email bodies.
- No delete, filters, forwarding rules, or settings tools.
- The only label mutation is applying
Safe Gmail MCP/Processedafterread_email_body. - No arbitrary local file attachments in v1.
- Bulk sends are capped at 25 messages per prepared batch and are sent sequentially.
- No hosted token broker.
- First-time default auth depends on the default OAuth metadata endpoint being reachable. BYO OAuth credentials work without that endpoint.
- Inbox placement is not guaranteed by Gmail API usage.
Threat Model
Primary risks:
- a malicious MCP client trying to send email without user review
- token theft from local disk
- prompt injection asking the model to exfiltrate email content or send to an attacker-controlled address
- accidental sends to the wrong recipient
Mitigations:
- two-step prepare/confirm with digest binding
- sending disabled by default
- bulk sending has an additional opt-in and batch size cap
- recipient allowlist/blocklist
- short pending-send expiry
- local-only OAuth callback
- PKCE and CSRF state
- restrictive local token file permissions
- no email body in pending list or audit log
- separate header-list and body-read tools
- processed label filtering for emails the MCP has already handled
Residual risks:
- any local process running as the same OS user may be able to access that user's files
- an MCP client with send enabled can still ask for confirmation, so users must inspect the preview and digest
- an MCP client with Gmail access can read email bodies when it calls
read_email_body; use a trusted MCP client and review tool calls - the default OAuth app metadata endpoint is public and requires trust in the BLISS-controlled endpoint unless users bring their own OAuth client ID
Development
npm ci
npm run typecheck
npm test
npm run build
npm pack --dry-runTests use a mock Gmail client and must not call Google APIs.
Release Notes
Use npm provenance where feasible:
npm publish --provenanceBefore publishing, run:
npm ci
npm test
npm run build
npm pack --dry-run
npm run smoke:pack