gmail-mcp
by maxhuk
README.md
# gmail-mcp
A local MCP server that gives Claude Code access to **several Gmail accounts at once**, and replies to customers **from the alias they wrote to** (for example `support@example.com`).
- [1. What it does](#1-what-it-does)
- [2. Setup](#2-setup)
- [3. How the reply address is chosen](#3-how-the-reply-address-is-chosen)
- [4. Attachments](#4-attachments)
- [5. Security model](#5-security-model)
- [6. Development](#6-development)
- [7. Troubleshooting](#7-troubleshooting)
---
## 1. What it does
| Tool | Purpose |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `list_accounts` | Signed-in accounts and the "Send mail as" addresses of each. |
| `search_threads` | Search one account with Gmail syntax (`to:support@example.com is:unread`). |
| `get_thread` | Read a conversation as plain text, including the list of attachments. |
| `save_attachment` | Download an attachment to the inbox folder and return its local path. |
| `create_draft` | Write a draft, optionally with files from the outbox. As a reply, it threads correctly and picks the right alias. **Nothing is sent.** |
| `update_draft` | Revise a draft in place (same draft id). Pass only what changes; existing attachments are kept. |
| `list_drafts` | Find drafts, e.g. ones created in an earlier session. |
| `delete_draft` | Discard a draft. Permanent: drafts skip the Trash. |
| `send_draft` | Send a draft you reviewed. |
Every tool takes an `account`. You can pass the account address or one of its aliases: `support@example.com` resolves to `owner@example.com`. A new (non-reply) message is then sent from that alias.
---
## 2. Setup
You do steps 2.1–2.3 once. You repeat step 2.5 for each Google account.
### 2.1 Gmail: allow sending from each alias
For every alias you want to reply from (`support@`, `sales@`, …):
1. Open Gmail as the account that owns the aliases.
2. Go to **Settings → See all settings → Accounts → Send mail as → Add another email address**.
3. Enter the alias. Leave **Treat as an alias** ticked.
Because these are aliases of your own Workspace user, Gmail adds them without SMTP setup.
> **New app later?** Add the alias in the Workspace Admin console, _and_ add it here. Receiving needs the first; replying needs the second.
### 2.2 Google Cloud: create an OAuth client
1. Open [console.cloud.google.com](https://console.cloud.google.com) and create a project, e.g. `gmail-mcp`.
2. **APIs & Services → Library** → enable the **Gmail API**.
3. **Google Auth Platform → Branding**: set an app name and your support email.
4. **Audience**:
- User type: **External** (needed so personal `@gmail.com` accounts can sign in).
- Publishing status: click **Publish app** → **In production**.
5. **Data Access** → add these scopes:
- `https://www.googleapis.com/auth/gmail.readonly`
- `https://www.googleapis.com/auth/gmail.compose`
6. **Clients → Create client** → type **Desktop app** → download the JSON.
7. Save it as `~/.config/gmail-mcp/client_secret.json`:
```bash
mkdir -p ~/.config/gmail-mcp && chmod 700 ~/.config/gmail-mcp
```
```bash
mv ~/Downloads/client_secret_*.json ~/.config/gmail-mcp/client_secret.json
```
> **Why "In production"?** In "Testing" mode Google expires refresh tokens after 7 days, so you'd have to sign in again every week. In production the app stays unverified, which is fine for personal use. You'll see a warning when signing in (step 2.5).
### 2.3 Install and build
```bash
npm install
```
```bash
npm run build
```
### 2.4 Register with Claude Code
```bash
claude mcp add gmail --scope user -- node <path-to-repo>/dist/index.js
```
Recommended permissions in `~/.claude/settings.json`. Reading and drafting run freely; sending and deleting always ask:
```json
{
"permissions": {
"allow": [
"mcp__gmail__list_accounts",
"mcp__gmail__search_threads",
"mcp__gmail__get_thread",
"mcp__gmail__save_attachment",
"mcp__gmail__list_drafts",
"mcp__gmail__create_draft",
"mcp__gmail__update_draft"
],
"ask": ["mcp__gmail__send_draft", "mcp__gmail__delete_draft"]
}
}
```
### 2.5 Sign in each account
Run once per account. The address is optional; it only pre-selects the account in the browser.
```bash
npm run login -- owner@example.com
```
```bash
npm run login -- you@gmail.com
```
On the **"Google hasn't verified this app"** screen, click **Advanced → Go to gmail-mcp**. This screen appears because you are the app's developer and its only users.
New accounts show up right away; you don't need to restart.
---
## 3. How the reply address is chosen
When `create_draft` gets a `replyToMessageId`, it decides the **From** address in this order:
1. **`from` argument**, if given. It must be a "Send mail as" address.
2. **Your own message?** If you're following up on a message you sent, it reuses that sender.
3. **The alias the customer wrote to.** It takes the first "Send mail as" address found in the original message's `To`, then `Cc`, then `Delivered-To` headers.
4. **The account default** as a fallback.
The result always includes `from` and `fromReason`, so the choice is never silent.
The reply also:
- goes to the customer's `Reply-To` if set, otherwise to the sender;
- keeps the original subject (with `Re:`) and sets `In-Reply-To` / `References`, so Gmail threads it;
- quotes the original message underneath (turn off with `quoteOriginal: false`).
`update_draft` keeps all of this. It keeps the sender, recipients and threading headers unless you override them. A new `body` replaces the old text, and reply drafts get the original quoted again underneath.
---
## 4. Attachments
Two local folders, created on first start with owner-only access:
| Folder | Default | Used for |
| ------ | --------------------- | ------------------------------------------------------------------- |
| Inbox | `~/gmail-mcp/inbox/` | `save_attachment` writes files to `<inbox>/<account>/<messageId>/`. |
| Outbox | `~/gmail-mcp/outbox/` | The **only** folder files can be attached from. |
### 4.1 Reading an attachment
1. `get_thread` lists each attachment with its name, type, size and `partId`.
2. `save_attachment` with the `messageId` and `partId` downloads it and returns the path.
3. Claude opens the file from that path (images, PDFs and text files work directly).
Files are never overwritten: saving the same attachment again returns the existing file.
### 4.2 Attaching a file
1. Put the file in the outbox, e.g. `~/gmail-mcp/outbox/guide.pdf`.
2. Ask Claude to attach it. `create_draft` takes `attachments: ["guide.pdf"]`; `update_draft` takes `addAttachments` and `removeAttachments` (by filename).
The combined size limit is Gmail's 25 MB.
### 4.3 Changing the folders
Create `~/.config/gmail-mcp/config.json` and set either key:
```json
{
"inboxDir": "~/Documents/Mail/inbox",
"outboxDir": "~/Documents/Mail/outbox"
}
```
Paths must be absolute or start with `~/`. Restart Claude Code sessions to apply.
---
## 5. Security model
| Concern | How it's handled |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| What the server can do | Scopes are `gmail.readonly` + `gmail.compose`: read, manage drafts, send. It can't delete mail or change labels. |
| Accidental sending | Replies are drafts. `send_draft` is separate and should be set to "ask" in Claude Code. |
| Discarding drafts | `delete_draft` only touches drafts, but it's permanent. Set it to "ask" as well. |
| Token storage | `~/.config/gmail-mcp/tokens/<email>.json`, mode `0600`, outside the repo. Change the folder with `GMAIL_MCP_HOME`. |
| Leaking local files | Attachments come only from the outbox. Paths outside it, including via `..` or symlinks, are refused. |
| Untrusted downloads | Attachments are only saved, never opened or run. File names are cleaned so they can't point outside the inbox folder. |
| Header injection | Header values with line breaks are rejected. Recipients are validated as email addresses. |
| Revoking access | Remove the app at [myaccount.google.com/permissions](https://myaccount.google.com/permissions) and delete the token file. |
---
## 6. Development
```bash
npm run check
```
This runs typecheck, lint and tests together.
| Path | Responsibility |
| ---------------------- | ---------------------------------------------------------------- |
| `src/index.ts` | Entry point: wires config, auth and the MCP server over stdio. |
| `src/server.ts` | Tool definitions (input schemas, descriptions) → `GmailService`. |
| `src/gmail/service.ts` | Gmail API calls for every tool. |
| `src/gmail/reply.ts` | Pure reply planning: recipients, subject, threading, quote. |
| `src/gmail/alias.ts` | Pure alias selection. |
| `src/gmail/mime.ts` | Builds the RFC 2822 message, multipart when files are attached. |
| `src/gmail/parse.ts` | Reads headers and body text out of Gmail payloads. |
| `src/attachments/` | Filename cleanup, outbox path check, inbox saving. |
| `src/settings.ts` | Optional `config.json`: inbox and outbox folders. |
| `src/auth/` | OAuth client, token storage, refresh persistence. |
| `src/cli/login.ts` | Browser sign-in (loopback redirect + PKCE). |
The pure modules have no I/O and are covered by unit tests. `GmailService` is tested against a fake Gmail API in `test/service.test.ts`.
---
## 7. Troubleshooting
| Symptom | Fix |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Access blocked: … admin policy` when signing in a Workspace account | Admin console → **Security → Access and data control → API controls → Manage third-party app access** → add your OAuth client ID as **Trusted**. |
| `… is outside the outbox folder` | Copy the file into the outbox first (see section 4.2). |
| `… is not a "Send mail as" address` | Add the alias in Gmail (step 2.1). The server picks up new aliases within 10 minutes. |
| `saved sign-in … revoked or expired` | Run `npm run login` for that account again. If this happens weekly, the app is still in "Testing" (step 2.2.4). |
| Tools missing in Claude Code | Run `npm run build`, and check the path registered in step 2.4 still exists. |
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues