gmail-mcp-send-to-list-only
Allows sending Gmail messages only to a fixed startup allowlist, with optional HTML content and attachments from configured local directories.
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., "@gmail-mcp-send-to-list-onlysend 'Meeting reminder' to recipient_1"
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.
gmail-mcp-send-to-list-only
A local Python Model Context Protocol (MCP) server that sends Gmail messages only to a fixed startup allowlist. Messages support plain text, optional HTML, and optional attachments from configured local directories.
The MCP caller selects generated recipient IDs such as recipient_1; it never supplies email
addresses. The server rejects an empty selection, duplicate IDs, unknown IDs, extra headers,
and any serialized message whose From or To addresses differ from the startup policy.
Linux VPS / headless deployment
Running this on a Linux VPS or other headless server over SSH?
Follow the Linux VPS deployment and OAuth guide before running
auth. It covers SSH-tunneled OAuth, persistent token.json storage, automatic token refresh,
persistent environment configuration, and VPS-specific troubleshooting.
The default Desktop OAuth flow starts a localhost callback and attempts to open a browser. On a headless VPS, use the dedicated guide so the callback is carried through an SSH tunnel and the resulting token is stored directly on the server.
Related MCP server: Gmail MCP Server (Multi-User)
Security guarantee and boundary
For an unchanged process started from trusted code and trusted configuration, every Gmail API send request produced by this server has exactly these properties:
every direct recipient is an address in
GMAIL_ALLOWED_RECIPIENTS;the caller cannot provide an address,
Cc,Bcc, sender, header, or raw MIME value;the MIME message has one
From, one visibleTo, oneSubject, and an audited body;HTML is accepted only through the
body_htmlfield and always has a plain-text fallback;attachments are accepted only as files under
GMAIL_ALLOWED_ATTACHMENT_ROOTS;the final serialized MIME is parsed and audited before it can reach the Gmail client;
the only Gmail OAuth scope is
https://www.googleapis.com/auth/gmail.send;the only Gmail API operation is one non-retrying
users.messages.sendcall; andthe MCP server uses local standard input/output (STDIO), not a network listener.
This is an application-level recipient lock, not a recipient-scoped Google permission. Google
does not provide an OAuth scope that restricts sending to particular addresses. The
gmail.send token can send to arbitrary addresses if it is stolen or used by different code.
Inside the guarantee | Outside the guarantee |
Untrusted MCP tool arguments and model-generated content | An attacker who can modify the code, Python environment, process environment, or process memory |
Header injection attempts through subject or body inputs | Direct use or theft of the OAuth token or OAuth client secret |
Unknown, duplicate, or empty recipient-ID selections | Forwarding, aliases, or group expansion performed after delivery to an allowed address |
Attempts to add | Compromise of Gmail, Google Cloud, the host, or an allowed recipient mailbox |
Attachment paths outside configured allowed roots | Disclosure caused by intentionally broad attachment roots |
If the ultimate human recipients must also be constrained, allow only direct mailboxes that you control. Do not allow mailing lists, groups, aliases, or addresses with forwarding rules. Enforce the same restriction independently in Google Workspace routing or another outbound mail gateway when a host or token compromise is in scope.
All recipients selected for one message appear together in To and can see one another. Send
separate messages when recipients must not be disclosed to each other.
Deliberately unsupported
The server has no raw-address input, Cc, Bcc, custom headers, raw MIME, drafts, replies,
mailbox reads, generic Gmail API calls, HTTP transport, GUI, database, telemetry, preview
workflow, or automatic send retry.
Sending is immediate, external, and non-idempotent. A timeout or transport error can leave delivery status unknown. Do not resend automatically after an ambiguous failure; inspect the Gmail Sent folder manually first.
Requirements
Python 3.11 or newer;
one Google account with Gmail enabled;
one Google Cloud project with the Gmail API enabled;
a Desktop OAuth client for that project; and
local filesystem permissions that protect the OAuth files and MCP client configuration.
The project runs on Linux and macOS. The commands below use a POSIX shell.
1. Install
Clone the repository and create an isolated virtual environment:
git clone https://github.com/bajor/gmail-mcp-send-to-list-only.git
cd gmail-mcp-send-to-list-only
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -e .Do not install into the system Python or use --break-system-packages.
2. Create the Google OAuth client
Follow Google's current Gmail Python quickstart for the Google Cloud console steps, with these project-specific constraints:
Create or select one Google Cloud project.
Enable only the Gmail API needed by this project.
Configure the Google Auth platform. For a personal Gmail account, choose an External audience and add the one Gmail account under test users while the app is in Testing. For a Google Workspace account, the administrator can choose Internal when appropriate.
Under Data Access, add exactly the sensitive
gmail.sendscope:https://www.googleapis.com/auth/gmail.send.Create an OAuth client with application type Desktop app.
Download the client JSON outside the repository, for example to
~/.config/gmail-mcp-send-to-list-only/client_secret.json.Restrict the local files:
mkdir -p ~/.config/gmail-mcp-send-to-list-only
chmod 700 ~/.config/gmail-mcp-send-to-list-only
chmod 600 ~/.config/gmail-mcp-send-to-list-only/client_secret.jsonNever commit the downloaded client JSON. The repository ignores common client-secret and token filenames, but filesystem permissions and operator discipline remain required.
3. Configure the immutable startup policy
The server reads configuration from the process environment inherited from the MCP client. It does
not load a repository .env file, and normal setup should not duplicate these values inside each
MCP client's server definition. Export the variables once in the shell, service manager, or wrapper
that starts Claude Code, Codex, OpenCode, or another local STDIO MCP launcher.
Set these variables before starting the MCP client:
export GMAIL_CLIENT_SECRET_FILE=/home/USERNAME/.config/gmail-mcp-send-to-list-only/client_secret.json
export GMAIL_TOKEN_FILE=/home/USERNAME/.config/gmail-mcp-send-to-list-only/token.json
export GMAIL_SENDER_EMAIL=sender@example.com
export GMAIL_ALLOWED_RECIPIENTS=alice@example.com,bob@example.com
export GMAIL_ALLOWED_ATTACHMENT_ROOTS=/home/USERNAME/opencode-tmp,/home/USERNAME/reports
export GMAIL_MAX_ATTACHMENT_BYTES=25000000
export GMAIL_MAX_TOTAL_ATTACHMENT_BYTES=25000000
export GMAIL_MAX_ATTACHMENTS=10Replace every example value locally. GMAIL_SENDER_EMAIL must be the Gmail address used during
authorization. GMAIL_ALLOWED_RECIPIENTS must be a non-empty comma-separated list of the only
permitted direct email addresses.
Recipient IDs are generated at startup as recipient_1, recipient_2, and so on in the same
order as the comma-separated addresses. Reordering the list changes which address each generated
ID resolves to. Addresses must be unique ASCII addr-spec values without display names. Spaces
around commas are ignored.
GMAIL_ALLOWED_ATTACHMENT_ROOTS is optional. If it is unset or empty, attachment sending is
disabled. When set, each comma-separated entry must be an existing directory. Attachment paths are
resolved before sending and must stay inside one of these directories. GMAIL_MAX_ATTACHMENT_BYTES
defaults to 25000000 bytes per file. GMAIL_MAX_TOTAL_ATTACHMENT_BYTES defaults to 25000000
bytes across all attachments in one message. GMAIL_MAX_ATTACHMENTS defaults to 10 files per
message.
The process loads and validates the complete policy once at startup. Changing the parent shell or client configuration has no effect on a running MCP server. Restart the MCP client after any intentional allowlist change. An invalid or empty policy prevents the server from starting.
Enable attachments
Attachments are disabled unless GMAIL_ALLOWED_ATTACHMENT_ROOTS is set. This is intentional: the
model can only attach files from directories that the operator explicitly allows.
Use a narrow scratch directory when possible:
mkdir -p /tmp/opencode
export GMAIL_ALLOWED_ATTACHMENT_ROOTS=/tmp/opencodeThen write generated reports, PDFs, or exports under /tmp/opencode before calling
gmail_send_email with an attachment path. Avoid broad roots such as $HOME or a full notes vault
unless you explicitly want the MCP caller to be able to attach any file under that tree.
Common safe patterns:
/tmp/opencodefor generated one-off artifacts;/home/USERNAME/reportsfor reviewed reports that are safe to mail; and/home/USERNAME/outboxfor a dedicated handoff folder.
Every root must already exist before the MCP process starts. If a caller sends an attachment while no roots are configured, the server rejects the request before Gmail is called with:
Attachments require configured allowed attachment roots.Fix that by setting GMAIL_ALLOWED_ATTACHMENT_ROOTS in the environment inherited by the MCP server,
or in the client-specific environment block when your MCP client supports one, then restart the MCP
client.
4. Authorize the one Gmail account
On a Linux VPS or other headless host, use theLinux VPS deployment and OAuth guide. It shows how to complete the localhost OAuth callback through an SSH tunnel and keep the resulting token on the VPS.
Run the local Desktop OAuth flow:
.venv/bin/gmail-mcp-send-to-list-only authSign in only to the account named by GMAIL_SENDER_EMAIL. The command requests exactly
gmail.send, opens a local callback on an ephemeral port, and atomically stores the token with
mode 0600 in a directory with mode 0700.
Run the diagnostic command:
.venv/bin/gmail-mcp-send-to-list-only doctorExpected results are PASS for Python, OAuth scope, recipient policy, client secret, token
permissions, and token scope. WARN means a setup artifact is not present yet. FAIL produces
a non-zero exit status and must be fixed before use.
To remove only the configured local token:
.venv/bin/gmail-mcp-send-to-list-only logout-localThis does not revoke access at Google. Revoke the app separately in the Google account security settings when the token may have been exposed.
5. Connect an MCP client
Use a local STDIO MCP configuration that starts this repository's Python entry point. Keep the
GMAIL_* values in the parent process environment, not repeated as per-client config values. If a
client requires an explicit forwarding allowlist, list only the variable names. Keep approval
prompts enabled for gmail_send_email; sending is external and non-idempotent.
You can also start the server manually from a shell that already exports the variables:
.venv/bin/gmail-mcp-send-to-list-only mcpDo not type into that process: standard output carries the MCP protocol.
Claude Code
Claude Code supports local STDIO servers in user, project, or local scope. Replace both absolute
repository paths below, and start Claude Code from an environment where the required GMAIL_*
variables are already set:
claude mcp add-json --scope user gmail-send-to-list-only '{
"type": "stdio",
"command": "/ABSOLUTE/PATH/gmail-mcp-send-to-list-only/.venv/bin/python",
"args": ["-m", "gmail_mcp_send_to_list_only", "mcp"],
"cwd": "/ABSOLUTE/PATH/gmail-mcp-send-to-list-only"
}'Restart Claude Code after editing MCP configuration or changing the exported variables.
Codex
Codex supports local STDIO servers in ~/.codex/config.toml or a trusted project's
.codex/config.toml. Replace both absolute repository paths below. The env_vars line forwards
values that were exported before Codex started; it does not store the values in config.toml.
[mcp_servers.gmail-send-to-list-only]
command = "/ABSOLUTE/PATH/gmail-mcp-send-to-list-only/.venv/bin/python"
args = ["-m", "gmail_mcp_send_to_list_only", "mcp"]
cwd = "/ABSOLUTE/PATH/gmail-mcp-send-to-list-only"
env_vars = ["GMAIL_CLIENT_SECRET_FILE", "GMAIL_TOKEN_FILE", "GMAIL_SENDER_EMAIL", "GMAIL_ALLOWED_RECIPIENTS", "GMAIL_ALLOWED_ATTACHMENT_ROOTS", "GMAIL_MAX_ATTACHMENT_BYTES", "GMAIL_MAX_TOTAL_ATTACHMENT_BYTES", "GMAIL_MAX_ATTACHMENTS"]
enabled = true
required = true
startup_timeout_sec = 20
tool_timeout_sec = 120
enabled_tools = ["gmail_list_allowed_recipients", "gmail_send_email"]
default_tools_approval_mode = "writes"writes asks for approval before the send tool because the server marks it as non-read-only;
the list tool remains read-only. Keep approval prompts enabled. Restart Codex after editing its
configuration or changing the exported variables. See the current
Codex MCP configuration reference for
client-specific setup details.
OpenCode
OpenCode supports local MCP servers in opencode.json, opencode.jsonc, or the global
~/.config/opencode/opencode.jsonc. Replace both absolute repository paths below, and start
OpenCode from an environment where the required GMAIL_* variables are already set:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"gmail-send-to-list-only": {
"type": "local",
"command": [
"/ABSOLUTE/PATH/gmail-mcp-send-to-list-only/.venv/bin/python",
"-m",
"gmail_mcp_send_to_list_only",
"mcp"
],
"cwd": "/ABSOLUTE/PATH/gmail-mcp-send-to-list-only",
"enabled": true,
"timeout": 120000
}
}
}Restart OpenCode after editing its configuration or changing the exported variables.
If OpenCode is not started from a wrapper that exports GMAIL_ALLOWED_ATTACHMENT_ROOTS, set the
attachment root in the MCP server definition. Keep the root narrow:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"gmail-send-to-list-only": {
"type": "local",
"command": [
"/ABSOLUTE/PATH/gmail-mcp-send-to-list-only/.venv/bin/python",
"-m",
"gmail_mcp_send_to_list_only",
"mcp"
],
"cwd": "/ABSOLUTE/PATH/gmail-mcp-send-to-list-only",
"environment": {
"GMAIL_ALLOWED_ATTACHMENT_ROOTS": "/tmp/opencode"
},
"enabled": true,
"timeout": 120000
}
}
}This only forwards the attachment-root setting. The sender, token path, client-secret path, and
recipient allowlist still need to be present in OpenCode's parent environment unless you choose to
store those values in the same environment block. Restart OpenCode after changing the block; a
running MCP process keeps the old policy.
MCP tools
gmail_list_allowed_recipients
Input: none.
Output: the startup policy's recipient IDs and addresses. Listing is read-only and idempotent.
gmail_send_email
Input:
{
"recipient_ids": ["recipient_1"],
"subject": "Example subject",
"body_text": "Example plain-text body.",
"body_html": "<p><strong>Example HTML body.</strong></p>",
"attachments": [
{
"path": "/home/USERNAME/opencode-tmp/example.pdf",
"filename": "example.pdf",
"content_type": "application/pdf"
}
]
}recipient_idsmust be a non-empty, duplicate-free subset of configured IDs.subjectmust not contain carriage return, line feed, or NUL characters.body_textis required and encoded as UTF-8 plain text.body_htmlis optional and encoded as UTF-8 HTML in amultipart/alternativebody.attachmentsis optional and eachpathmust resolve under a configured attachment root.filenameis optional and must be one safe file name when provided.content_typeis optional and defaults to a guessed MIME type orapplication/octet-stream.there is no address field and no optional delivery-header field.
Success returns the Gmail message ID, Gmail thread ID, selected recipient IDs, and resolved recipient addresses. Failure is returned before Gmail is called when policy validation fails. Gmail and transport failures use sanitized errors and do not expose OAuth secrets.
Change or revoke access
To change the recipient set:
Stop or restart the MCP client so the old process cannot send.
Edit
GMAIL_ALLOWED_RECIPIENTSin the environment that starts the MCP client.Run
doctorand confirm the recipient count.Restart the MCP client.
Call
gmail_list_allowed_recipientsand verify every ID and address before sending.
To rotate credentials, run logout-local, replace the Desktop client JSON when necessary, and
run auth again. If a token or host is suspected compromised, revoke the Google account grant
before creating a replacement token.
Troubleshooting
Server fails to start: run
doctor; fix the reported sender or allowlist error. The allowlist must be one non-empty comma-separated list with unique addresses.Client secret warning: set
GMAIL_CLIENT_SECRET_FILEto an existing Desktop client JSON file outside the repository.Token missing or expired: run
auth. If the stored scopes differ, runlogout-localand thenauth; the application refuses broader or different saved scopes.Token permission failure: run
chmod 600on the token. The parent configuration directory should use mode0700.Unknown recipient ID: call
gmail_list_allowed_recipients; callers must use generated IDs exactly as listed and cannot submit an address instead.Attachments require configured allowed attachment roots.GMAIL_ALLOWED_ATTACHMENT_ROOTSwas unset or empty when the MCP server started. Set it to one or more existing directories, for example/tmp/opencode, and restart the MCP client.Attachment rejected: check that
GMAIL_ALLOWED_ATTACHMENT_ROOTScontains the parent directory, the attachment path is a file, and the file is within the configured count, per-file size, and total size limits.Send error says status may be unknown: inspect Gmail's Sent folder before deciding whether to send a new message. The application deliberately performs no automatic retry.
MCP client cannot initialize the server: use absolute paths, start the MCP client from an environment with all required
GMAIL_*variables set, and run themcpcommand manually to expose startup errors.
Development and verification
Install development dependencies and run the complete local gate:
.venv/bin/python -m pip install -e ".[dev]"
make testmake test runs Ruff, strict mypy, and pytest. The tests include:
allowlist parsing and immutable policy validation;
exact OAuth scope and secure token-file behavior;
recipient resolution, MIME injection cases, and an independent final MIME audit;
a fake Gmail client that verifies exactly one non-retrying send call;
MCP schema and tool behavior tests; and
an offline end-to-end MCP client/server test proving that an off-list ID fails before the fake sender is invoked.
The test suite never uses real credentials and never sends real email. A live Gmail smoke test is intentionally excluded because it creates an external, non-idempotent side effect.
Provenance
The project structure, local OAuth workflow, diagnostics, and CI approach were adapted from
bajor/gmail-mcp-read-only. The recipient-lock,
MIME audit, send-only client, and send-specific tests are implemented for this repository.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceMCP server that sends outbound emails with automatic single-thread mapping in Gmail, enabling reply-style conversations within a single thread.651MIT
- Flicense-qualityCmaintenanceMulti-user Gmail MCP server that enables sending/receiving emails, managing accounts, and searching mail via SMTP/IMAP with per-user isolation and encrypted app passwords.
- Alicense-qualityDmaintenanceA standardized MCP server for managing Gmail, including sending, drafting, labels, threads, and settings via OAuth2 authentication.MIT
- AlicenseAqualityDmaintenanceMCP server that gives AI agents the ability to send, read, and search email via Gmail.492MIT
Related MCP Connectors
Read, search, send, organize, draft and schedule email across your inboxes from any MCP client.
Hosted email MCP for AI agents with inboxes, send/receive, memory, recovery, and credits.
MCP server teaching AI agents to implement TideCloak: auth, E2EE, IGA, security analysis
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/bajor/gmail-mcp-send-to-list-only'
If you have feedback or need assistance with the MCP directory API, please join our Discord server