Skip to main content
Glama
chandhinii1510-coder

Gmail SMTP MCP Server

README.md
# Gmail SMTP MCP Server (Python)

Sends email as a teammate's Gmail account via SMTP + an **App Password**,
instead of Google OAuth. No Google Cloud project, no client secret, no
`redirect_uri` allowlist, no consent screen. This is a Python port of the
Node.js version — same architecture, same endpoints.

## How auth works

claude.ai's connector setup still expects a normal OAuth 2.1 + PKCE dance —
that part of the MCP spec doesn't change. What changes is *who* the
"identity provider" is:

1. User adds your connector in claude.ai → claude.ai opens `/authorize`.
2. Instead of "Sign in with Google", they see a form asking for their Gmail
   address + an [App Password](https://myaccount.google.com/apppasswords)
   (requires 2-Step Verification on their Google account).
3. The server test-sends an email to confirm the credentials work, encrypts
   and stores the app password, then redirects back to claude.ai with an
   auth code — same as a normal OAuth flow.
4. claude.ai exchanges that code at `/token` for a bearer token.
5. Every `send_email_tool` call carries that bearer token; the server looks
   up which Gmail account it maps to (via the token's `subject`) and sends
   via SMTP using only Python's standard library (`smtplib`).

Nothing here talks to Google's OAuth API. There's no client secret to leak
or rotate.

## Setup

```bash
pip install -r requirements.txt

# Generate a 32-byte encryption key for storing app passwords at rest
export MASTER_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
export BASE_URL=http://localhost:3000

uvicorn server:app --port 3000
```

Add both `MASTER_KEY` and `BASE_URL` to a `.env` file if you'd rather not
export them each time — just make sure something loads it before `uvicorn`
starts (e.g. `python-dotenv`, or your platform's env var settings).

## Deploying to Render

1. Push this repo, create a new Web Service pointing at it.
   - Build command: `pip install -r requirements.txt`
   - Start command: `uvicorn server:app --host 0.0.0.0 --port $PORT`
2. Set env vars: `MASTER_KEY` (generate as above), `BASE_URL` (your Render
   URL, e.g. `https://your-app.onrender.com`).
3. **Important:** `data.json` (where users/tokens live) is written to local
   disk. Render's free web services have ephemeral disks — a redeploy wipes
   it and every teammate has to reconnect. For anything beyond testing,
   either add a Render persistent disk mount, or swap `store.py` for
   Postgres/SQLite (the function signatures in `store.py` are the only
   thing you'd need to keep the same).
4. In claude.ai, add a custom connector pointing at
   `https://your-app.onrender.com/mcp`.

## Files

- `server.py` — FastAPI app: OAuth-shaped `/authorize` + `/token` endpoints,
  the MCP server (built with `mcp`'s `FastMCP`) mounted at `/mcp` with
  bearer-token auth enforced automatically by the SDK.
- `store.py` — encrypted (AES-256-GCM) storage for app passwords, auth
  codes, and access tokens.
- `mailer.py` — sends via `smtp.gmail.com:465` using Python's built-in
  `smtplib`, no extra package needed.

## A gotcha worth knowing about if you modify this

FastMCP's `streamable_http_app()` normally manages its own startup/shutdown
(a "session manager") when run standalone. When you mount it inside another
FastAPI app with `app.mount("/mcp", ...)`, FastAPI does **not** automatically
run the sub-app's lifespan — so streaming requests fail with
`RuntimeError: Task group is not initialized`. The fix already in
`server.py` is wiring `mcp.session_manager.run()` into the outer FastAPI
app's own `lifespan`. If you restructure this file, keep that wiring intact.

## Security notes

- App passwords are encrypted at rest with `MASTER_KEY`, but they're still
  bearer credentials with full mail-send access — treat `data.json` (or
  your DB) like you would a password table.
- Access tokens are long-lived (90 days) bearer tokens with no rotation.
  Fine for an internal team tool; add refresh tokens or shorter expiry if
  this ever leaves that trust boundary.
- Anyone who can reach `/authorize` can register any Gmail address they
  control an app password for — there's no allowlist. Add one (e.g. only
  `@caratlane.com` addresses) if you want to restrict this before sharing
  the connector link.