Skip to main content
Glama
pyrodie18

Gmail MCP

by pyrodie18
README.md
# Gmail MCP

Private multi-account Gmail MCP server for Codex.

## Development

Open this repository through Remote SSH and choose **Dev Containers: Reopen in
Container**. The container exposes only this repository, runs as the unprivileged
`vscode` user, and uses a repository-specific Codex home on the VM.
Its sole Docker security override relaxes seccomp because the real Codex
workspace sandbox was verified to require nested user namespaces on this VM.
No capabilities, privileged mode, AppArmor override, or Docker socket are used.

The central development image provides Python, uv, Codex, and generic build
tools. Project dependencies are installed from the checked-in `uv.lock` into
this repository's ignored `.venv`. The devcontainer creates that environment
automatically and keeps it isolated from every other repository and worktree.

The project commands are:

```bash
uv lock --check
uv sync --frozen --group dev
uv run --frozen pytest
uv run --frozen flake8 src tests
uv build
```

The first time this repository is opened, authenticate its isolated Codex state
with the primary development ChatGPT account by running `codex login` in the
container. Do not put API keys in the devcontainer configuration.

## Google Cloud configuration

This server accesses each Gmail account with a user-authorized OAuth 2.0
**Web application** client. It does not use an API key or a service account.
Complete these steps in the [Google Cloud console](https://console.cloud.google.com/)
before starting the server.

1. **Create and select a project.** Use a dedicated project for this server;
   keeping development and production projects separate makes it less likely
   that test credentials or consent-screen settings leak into production.

2. **Enable the Gmail API.** Open **APIs & Services > Library**, search for
   **Gmail API**, select it, and click **Enable**. The OAuth client cannot
   request Gmail scopes until this API is enabled.

3. **Configure the OAuth consent screen.** Open **Google Auth platform** and
   complete the setup screens (the first visit may show **Get started**):

   - On **Branding**, provide an accurate application name, a user-support
     email address, and developer contact email. Do not use Google trademarks
     or a Google product logo in the app name or logo.
   - On **Audience**, choose **Internal** only when every account is in the
     same Google Workspace organization. Otherwise choose **External**. For a
     private personal or development deployment, leave an External app in
     **Testing** and add every Gmail account to **Test users**.
   - On **Data Access**, use **Add or remove scopes** and add the scopes this
     service requests:

     ```text
     https://www.googleapis.com/auth/gmail.modify
     https://www.googleapis.com/auth/gmail.compose
     https://www.googleapis.com/auth/gmail.send
     ```

     The service requests these by default; retain the same list if you set
     `GOOGLE_OAUTH_SCOPES` yourself. `gmail.modify` permits reading and
     changing mailbox state, `gmail.compose` manages drafts and sending, and
     `gmail.send` permits sending. Do not add broader scopes such as
     `https://mail.google.com/` unless the code is deliberately changed to
     need them.

4. **Create the OAuth client.** Open **Google Auth platform > Clients**, click
   **Create client**, choose **Web application**, and give it a descriptive
   name such as `gmail-mcp-local`.

   Under **Authorized redirect URIs**, add exactly:

   ```text
   http://localhost:8080/admin/oauth/callback
   ```

   No Authorized JavaScript origin is needed for this server-side flow. The
   value in Google Cloud must exactly equal `GOOGLE_REDIRECT_URI`, including
   its scheme, host, port, path, case, and trailing slash. A
   `redirect_uri_mismatch` error means those values differ.

   `http` is permitted for `localhost` during local development. For any
   non-localhost hostname, use a domain you control and an HTTPS callback
   endpoint (normally behind a TLS reverse proxy); do not register a raw LAN
   IP address as the callback. Add that same HTTPS URL both here and in
   `GOOGLE_REDIRECT_URI`.

5. **Copy the client ID and client secret.** After creating the client, copy
   its values from the client details page. Treat the secret as a password:
   keep it out of Git, the repository, shell history, and screenshots. You
   will place the values in the VM-only secrets file in the next section.

### Testing versus production

The Gmail scopes above are sensitive or restricted. For an **External** app in
Testing, Google allows only the Gmail addresses listed as test users and their
authorizations (including offline refresh tokens) expire after seven days;
re-run the account authorization flow when that happens. This is the simplest
path for a private deployment.

To authorize arbitrary external users without the testing limit, publish the
app and complete Google's OAuth verification. Restricted scopes can require
scope justification, a demo video, and—when restricted data is stored on or
transmitted through servers—a security assessment. An Internal Workspace app
does not require public OAuth verification, but its Workspace administrator
may still need to allow the app's Gmail access. Review Google's
[Gmail scope classifications](https://developers.google.com/workspace/gmail/api/auth/scopes),
[app audience guidance](https://support.google.com/cloud/answer/15549945),
and [verification requirements](https://support.google.com/cloud/answer/13464321)
before publishing.

## LAN deployment and account authorization

The server may run on a LAN VM while the browser and Codex client run on a
different computer. Do not expose the unencrypted Docker port to an untrusted
network: both the admin token and MCP bearer token are credentials.

Choose one of these deployment models:

- **SSH tunnel (recommended for a private deployment):** no DNS, certificate,
  or application changes. The service stays on the LAN VM, while the Windows
  workstation reaches it securely as `localhost` through SSH. This is the
  simplest way to use the normal Google OAuth flow.
- **LAN HTTPS endpoint:** use this when multiple clients must reach the service
  directly. Use a domain that you control, LAN DNS, a TLS reverse proxy, and a
  publicly trusted certificate obtained with DNS-01. This is also required if
  Google OAuth must redirect directly to the LAN endpoint rather than through
  an SSH tunnel.

### Option 1: Windows SSH tunnel (recommended)

This option does **not** move the service to the Windows PC. It securely
forwards the Windows PC's `localhost:8080` to the VM's `localhost:8080`. Google
therefore sees the allowed `http://localhost` callback, and the VM receives the
callback over the tunnel.

1. On the VM, keep the standard localhost callback in both Google Cloud and
   `~/.config/dev-secrets/gmail_mcp.env`:

   ```dotenv
   GOOGLE_REDIRECT_URI=http://localhost:8080/admin/oauth/callback
   ```

   Start the service on the VM as described in [Quick Start](#quick-start).
   The current Compose configuration publishes port 8080 on the VM, which also
   makes it reachable at the VM's loopback address for SSH forwarding.

2. On the Windows workstation, open PowerShell or Windows Terminal. Verify the
   built-in OpenSSH client is available:

   ```powershell
   ssh -V
   ```

   If the command is unavailable, install **OpenSSH Client** from **Settings >
   System > Optional features**. Do not install or configure an SSH server on
   Windows for this workflow; Windows is the SSH client and the LAN VM is the
   SSH server. See [Microsoft's OpenSSH overview](https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh-overview)
   if the optional feature is managed by your organization.

3. Create the tunnel. Replace the placeholder with the SSH user and LAN DNS
   name or private IP address of the VM:

   ```powershell
   ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:8080:127.0.0.1:8080 <vm-user>@<vm-host-or-ip>
   ```

   Keep this terminal open. `-N` opens no remote shell, and binding the local
   side to `127.0.0.1` prevents other LAN devices from using the tunnel.

   If TCP port 8080 is already in use on Windows, use a different local port,
   such as `18080`, in the command **and** change the registered callback and
   `GOOGLE_REDIRECT_URI` to `http://localhost:18080/admin/oauth/callback`.

4. In a second PowerShell window, confirm that the tunnel reaches the service:

   ```powershell
   curl.exe http://127.0.0.1:8080/health
   ```

5. Complete account authorization in a browser on the Windows workstation:

   ```text
   http://localhost:8080/admin/oauth/start?alias=personal&token=<ADMIN_TOKEN>
   ```

   Google redirects the browser to `localhost`; SSH delivers that request to
   the VM, which exchanges the authorization code and stores the refresh token.
   The tunnel need only be present whenever an account is added or reauthorized.

6. To keep normal MCP traffic encrypted without setting up LAN TLS, leave the
   tunnel running and add the MCP server from Windows PowerShell:

   ```powershell
   $env:MCP_BEARER_TOKEN = "<same MCP_BEARER_TOKEN from the VM secrets file>"
   codex mcp add gmail --url http://localhost:8080/mcp --bearer-token-env-var MCP_BEARER_TOKEN
   ```

   `$env:` sets the token for the current PowerShell session only. Do not use
   `setx` for this secret. Stop the tunnel with `Ctrl+C` when it is no longer
   needed.

### Option 2: Direct LAN HTTPS endpoint

Use this model only when clients must reach the server without an SSH tunnel.
Google requires a non-localhost OAuth callback to use HTTPS and a domain name;
do not register a raw LAN IP address or an internal-only suffix such as
`.local`.

1. Choose a domain that you control, for example `gmail-mcp.example.com`.
   Configure LAN (split-horizon) DNS so that name resolves to the VM's private
   IP address for every client that will use it.

2. Put a TLS reverse proxy such as Caddy on the VM. Obtain a publicly trusted
   certificate using the DNS-01 challenge through your DNS provider's API. The
   challenge proves control of the DNS name with a TXT record, so the VM does
   not need to accept connections from the public internet. Caddy needs the DNS
   provider module for the provider you use; follow that provider module's
   installation guidance. See Caddy's [automatic HTTPS](https://caddyserver.com/docs/automatic-https)
   and [`tls` directive](https://caddyserver.com/docs/caddyfile/directives/tls)
   documentation.

   A representative Caddyfile (shown with Cloudflare's DNS module) is:

   ```caddyfile
   gmail-mcp.example.com {
       tls {
           dns cloudflare {env.CLOUDFLARE_API_TOKEN}
       }
       reverse_proxy 127.0.0.1:8080
   }
   ```

   Keep Caddy's DNS API token outside this repository. If Caddy runs on the VM
   host, bind the Compose application port to loopback so the unencrypted
   backend cannot bypass Caddy:

   ```yaml
   ports:
     - "127.0.0.1:8080:8080"
   ```

   Alternatively, enforce the same restriction with the VM firewall: permit
   LAN TCP/443 to Caddy and deny LAN TCP/8080. Caddy terminates TLS; the
   loopback connection from Caddy to this application is plain HTTP.

3. Register this exact redirect URI in Google Cloud and set the same value in
   the VM secrets file:

   ```dotenv
   GOOGLE_REDIRECT_URI=https://gmail-mcp.example.com/admin/oauth/callback
   MCP_ALLOWED_HOSTS=gmail-mcp.example.com,gmail-mcp.example.com:443
   ```

   Restart the application after changing its environment, and reload Caddy
   after changing its configuration.

4. Authorize an account and connect Codex using the HTTPS hostname:

   ```text
   https://gmail-mcp.example.com/admin/oauth/start?alias=personal&token=<ADMIN_TOKEN>
   ```

   ```powershell
   $env:MCP_BEARER_TOKEN = "<same MCP_BEARER_TOKEN from the VM secrets file>"
   codex mcp add gmail --url https://gmail-mcp.example.com/mcp --bearer-token-env-var MCP_BEARER_TOKEN
   ```

   `MCP_ALLOWED_ORIGINS` remains empty for a native Codex client. Set it only
   when intentionally allowing a browser-based MCP client, and restrict it to
   the exact browser origin or origins.

### Authorization alternatives

The standard OAuth flow above is the only supported path in this repository for
personal Gmail accounts and the server's full feature set (mailbox access,
labels, drafts, and sending). A Gmail API key cannot access private mailbox
data, and a standalone service account does not have a personal Gmail mailbox.

- A **Google Workspace service account with domain-wide delegation** can avoid
  per-user consent screens, but only a Workspace super administrator can grant
  it and the application must be changed to impersonate each mailbox. It is
  not configured by this project and gives the service broad administrative
  authority.
- **Google Workspace SMTP relay** can send mail based on the server's allowed
  IP address, but it cannot read mail or manage Gmail labels and drafts. It
  would require a send-only redesign.
- **IMAP/SMTP app passwords** are static credentials with account and policy
  restrictions. They do not work with the current Gmail API implementation and
  would also require a redesign. Do not put an account password in this
  service.

## Quick Start

For a Windows workstation connecting to a LAN VM, follow either deployment
option above when completing steps 5 and 6. The `localhost` URLs below are the
default for a browser and Codex running on the same machine as the service.

1. Complete the [Google Cloud configuration](#google-cloud-configuration) and
   record the OAuth client ID and secret.
2. Generate the remaining server secrets:

```bash
uv run --frozen python - <<'PY'
from cryptography.fernet import Fernet
import secrets
print("GMAIL_MCP_ENCRYPTION_KEY=" + Fernet.generate_key().decode())
print("ADMIN_TOKEN=" + secrets.token_urlsafe(32))
print("MCP_BEARER_TOKEN=" + secrets.token_urlsafe(32))
PY
```

3. On the VM (not inside the dev container), create
   `~/.config/dev-secrets/gmail_mcp.env` from `.env.example`. Set
   `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and `GOOGLE_REDIRECT_URI` from
   the Google Cloud steps, then add the generated values. Set mode `0600`.

4. Start the server with command-scoped secret injection:

```bash
./scripts/dev-compose up --build
```

5. Add accounts through the admin OAuth flow. Use a browser that can reach the
   callback URL registered in Google Cloud, then sign in as a listed test user
   (or an allowed Internal/verified user):

```text
http://localhost:8080/admin/oauth/start?alias=personal&token=<ADMIN_TOKEN>
```

6. Connect Codex:

```bash
export MCP_BEARER_TOKEN="<same token>"
codex mcp add gmail --url http://localhost:8080/mcp --bearer-token-env-var MCP_BEARER_TOKEN
```

## Required Environment

- `GOOGLE_CLIENT_ID`
- `GOOGLE_CLIENT_SECRET`
- `GOOGLE_REDIRECT_URI`
- `GMAIL_MCP_ENCRYPTION_KEY`
- `ADMIN_TOKEN`
- `MCP_BEARER_TOKEN`

Optional:

- `DATABASE_URL`, default `sqlite:////data/gmail_mcp.sqlite3`
- `MCP_ALLOWED_HOSTS`, comma-separated, default `localhost,localhost:*,127.0.0.1,127.0.0.1:*`
- `MCP_ALLOWED_ORIGINS`, comma-separated, default empty
- `LOG_LEVEL`, default `INFO`

## Safety Model

Draft creation happens immediately. Sending mail, sending drafts, label changes, archive, mark read/unread, star/unstar, and trash create pending actions first. A caller must execute `gmail_pending_action_confirm` with the returned `pending_action_id` before Gmail state changes.

The committed `.env.example` is a variable-name contract only. Real values stay
outside Git and are passed to Docker Compose only for the command being run.