Skip to main content
Glama
bajor

gmail-mcp-send-to-list-only

by bajor

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

IMPORTANT

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 visible To, one Subject, and an audited body;

  • HTML is accepted only through the body_html field 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.send call; and

  • the 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 Cc, Bcc, headers, raw addresses, or arbitrary MIME through MCP

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:

  1. Create or select one Google Cloud project.

  2. Enable only the Gmail API needed by this project.

  3. 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.

  4. Under Data Access, add exactly the sensitive gmail.send scope: https://www.googleapis.com/auth/gmail.send.

  5. Create an OAuth client with application type Desktop app.

  6. Download the client JSON outside the repository, for example to ~/.config/gmail-mcp-send-to-list-only/client_secret.json.

  7. 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.json

Never 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=10

Replace 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/opencode

Then 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/opencode for generated one-off artifacts;

  • /home/USERNAME/reports for reviewed reports that are safe to mail; and

  • /home/USERNAME/outbox for 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

NOTE

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 auth

Sign 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 doctor

Expected 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-local

This 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 mcp

Do 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_ids must be a non-empty, duplicate-free subset of configured IDs.

  • subject must not contain carriage return, line feed, or NUL characters.

  • body_text is required and encoded as UTF-8 plain text.

  • body_html is optional and encoded as UTF-8 HTML in a multipart/alternative body.

  • attachments is optional and each path must resolve under a configured attachment root.

  • filename is optional and must be one safe file name when provided.

  • content_type is optional and defaults to a guessed MIME type or application/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:

  1. Stop or restart the MCP client so the old process cannot send.

  2. Edit GMAIL_ALLOWED_RECIPIENTS in the environment that starts the MCP client.

  3. Run doctor and confirm the recipient count.

  4. Restart the MCP client.

  5. Call gmail_list_allowed_recipients and 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_FILE to an existing Desktop client JSON file outside the repository.

  • Token missing or expired: run auth. If the stored scopes differ, run logout-local and then auth; the application refuses broader or different saved scopes.

  • Token permission failure: run chmod 600 on the token. The parent configuration directory should use mode 0700.

  • 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_ROOTS was 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_ROOTS contains 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 the mcp command 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 test

make 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.

Install Server
F
license - not found
A
quality
A
maintenance

Maintenance

–Maintainers
–Response time
–Release cycle
1Releases (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.

Related MCP Servers

  • A
    license
    -
    quality
    D
    maintenance
    MCP server that sends outbound emails with automatic single-thread mapping in Gmail, enabling reply-style conversations within a single thread.
    65
    1
    MIT
  • F
    license
    -
    quality
    C
    maintenance
    Multi-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.
  • A
    license
    -
    quality
    D
    maintenance
    A standardized MCP server for managing Gmail, including sending, drafting, labels, threads, and settings via OAuth2 authentication.
    MIT

View all related MCP servers

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

View all MCP Connectors

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/bajor/gmail-mcp-send-to-list-only'

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