Skip to main content
Glama
Gchahm

Gmail MCP Connector

by Gchahm
README.md
# Gmail MCP Connector

A full Gmail [MCP](https://modelcontextprotocol.io) connector running as a Cloudflare Worker. It reads, searches, labels, drafts, sends, and manages Gmail — including minting **short-lived download URLs for attachments** so the raw bytes can be fetched out-of-band without ever entering the model's context, a key capability this connector adds over generic Gmail integrations.

Authorization follows the **MCP authorization spec (OAuth 2.1)**: each user who connects runs the OAuth handshake themselves, and the **client holds the token** — this connector is multi-user and does not use a shared account secret.

## How auth works

This Worker plays two OAuth roles at once (via [`@cloudflare/workers-oauth-provider`](https://github.com/cloudflare/workers-oauth-provider)):

- To the **MCP client**, it is the **authorization server + resource server**. It issues its **own** token to the client and validates that token on every `/api/mcp` request.
- To **Google**, it is an **OAuth client**. It bridges the user to Google's consent screen and obtains a **separate** Google token.

Per the spec, the client's token is **never** passed through to Google. Each user's Google tokens are stored **encrypted in KV** (in the grant `props`) and used to call Gmail on their behalf. The client only ever holds the Worker's token.

```
MCP client ──token(A)──▶ Worker (RS/AS) ──token(B)──▶ Google/Gmail
           ◀─issues A──               ◀──issues B──
                           token A never forwarded to Google
```

## Endpoints

| Endpoint | Purpose |
| --- | --- |
| `POST /api/mcp` | MCP JSON-RPC endpoint (requires a valid access token) |
| `GET /attachment?ticket=<id>` | Redeem a one-time attachment download ticket; streams the raw bytes (no auth header — the ticket is the credential) |
| `GET /authorize` | Authorization UI — bridges the user to Google consent |
| `GET /oauth/callback/google` | Google redirect target; completes the grant |
| `POST /token`, `POST /register` | Token + dynamic client registration (library-served) |
| `GET /.well-known/oauth-protected-resource` | RFC 9728 metadata (library-served) |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 metadata (library-served) |

## Setup

1. **Create a KV namespace** for token storage and put its id in `wrangler.toml`:
   ```sh
   wrangler kv namespace create OAUTH_KV
   # paste the returned id into the [[kv_namespaces]] block in wrangler.toml
   ```

2. **Create a Google OAuth client** (type: *Web application*) in the Google Cloud console, and enable the Gmail API. Add this authorized redirect URI:
   ```
   {your-worker-origin}/oauth/callback/google
   ```
   (e.g. `https://gmail-mcp.your-subdomain.workers.dev/oauth/callback/google`, or your custom domain.)

3. **Set the client secrets:**
   ```sh
   wrangler secret put GOOGLE_CLIENT_ID
   wrangler secret put GOOGLE_CLIENT_SECRET
   ```
   > No `GOOGLE_REFRESH_TOKEN` — tokens are now obtained per-user at connect time.

4. *(Optional)* Set `BASE_URL` in `wrangler.toml` if you serve behind a custom domain. If left empty, the Worker derives redirect URIs from the incoming request origin.

5. **Deploy:**
   ```sh
   wrangler deploy
   ```

6. **Connect a client.** Add `{your-worker-origin}/api/mcp` as a custom MCP server. The client performs dynamic client registration + the OAuth flow automatically; you'll be sent to Google to sign in and consent on first connect.

## Tools

`search_threads`, `get_thread`, `get_message`, `get_attachment`, `send_email`, `reply_to_thread`, `create_draft`, `list_drafts`, `trash_message`, `list_labels`, `create_label`, `label_message`, `unlabel_message`, `label_thread`, `unlabel_thread`.

### Attachments

`get_attachment` does **not** return bytes — it returns a short-lived download URL:

```json
{ "download_url": "https://<worker>/attachment?ticket=<id>", "method": "GET", "expires_in_seconds": 600 }
```

Do a plain HTTP `GET` on `download_url` to stream the raw bytes. No auth header is needed — the opaque, high-entropy ticket in the URL is the credential. The ticket is **one-time use** and expires after 10 minutes.

This keeps the attachment bytes out of the tool result (and therefore out of the model's context): the consuming agent fetches them out-of-band into its own runtime, so large attachments that would blow the MCP tool-result size cap are no longer a problem. Under the hood, minting the URL writes a ticket into `OAUTH_KV` carrying that grant's Google access token; the `GET /attachment` endpoint redeems it server-side, fetches the bytes from Gmail, and deletes the ticket. The Google access token never appears in a URL, a tool result, or logs.

If the ticket's copied access token has expired by download time (rare within the 10-minute window), the endpoint returns `401` — just call `get_attachment` again for a fresh URL.

Optionally pass `file_name` and `mime_type` (from `get_message`'s attachment metadata) so the download response sets a correct `Content-Type` and `Content-Disposition`.

## Development

```sh
pnpm install
./node_modules/.bin/tsc --noEmit   # typecheck
wrangler dev                       # local dev server
```