Skip to main content
Glama
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.                                                                     |