Skip to main content
Glama
Seventhdd

gmail-multi-mcp-server

by Seventhdd
README.md
# gmail-multi-mcp-server

An MCP server that connects Claude to **several Gmail accounts at once**, exposing the same tool
surface as the built-in Gmail connector. Every tool takes an extra `account` argument naming which
mailbox to act on — that is the only structural difference from the single-mailbox connector.

```
"Check my work inbox for anything from the lender, and draft the reply from my personal account."
```

---

## Why this exists

The built-in Gmail connector authorizes one mailbox. If you run separate accounts — work, personal, a
company alias — you have to keep switching. This server holds credentials for as many Gmail accounts
as you authorize and routes each call to the one you name.

## Requirements

- Node.js 20 or newer
- A Google Cloud project with the Gmail API enabled and an OAuth client of type **Desktop app**

## 1. Get a Google OAuth client

1. Open the [Google Cloud Console](https://console.cloud.google.com/) and select or create a project.
2. **APIs & Services → Library →** enable **Gmail API**.
3. **APIs & Services → OAuth consent screen →** configure it. While the app is in *Testing*, add every
   Gmail address you plan to connect under **Test users**.
4. **APIs & Services → Credentials → Create credentials → OAuth client ID →** application type
   **Desktop app**. Download the JSON.

Desktop-app clients allow loopback redirects on any port, which is what the `add-account` flow uses.

## 2. Install and build

```bash
git clone <this repo>
cd Gmail-MCP
npm install
npm run build
```

## 3. Point the server at your OAuth client

Pick one:

```bash
# Option A — save the downloaded JSON where the server looks by default
mkdir -p ~/.gmail-mcp && cp ~/Downloads/client_secret_*.json ~/.gmail-mcp/credentials.json

# Option B — point at it explicitly
export GMAIL_MCP_CREDENTIALS=/path/to/client_secret.json

# Option C — pass the values directly
export GMAIL_MCP_CLIENT_ID=...apps.googleusercontent.com
export GMAIL_MCP_CLIENT_SECRET=...
```

## 4. Authorize each mailbox

Run once per Gmail account:

```bash
node dist/index.js add-account --alias work
node dist/index.js add-account --alias personal
node dist/index.js list-accounts
```

Each run prints a Google consent URL, waits on a local loopback port for the redirect, and stores the
resulting refresh token. On a headless machine, add `--manual` and paste the redirected URL back in.

Accounts added while the server is running are picked up automatically — no restart needed.

## 5. Connect it to Claude

**Claude Code**

```bash
claude mcp add gmail-multi -- node /absolute/path/to/Gmail-MCP/dist/index.js
```

**Claude Desktop** — add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "gmail-multi": {
      "command": "node",
      "args": ["/absolute/path/to/Gmail-MCP/dist/index.js"],
      "env": {
        "GMAIL_MCP_CREDENTIALS": "/absolute/path/to/client_secret.json"
      }
    }
  }
}
```

---

## Selecting an account

Every tool except `list_accounts` accepts `account`. It matches, in order:

1. the full email address — `mikael@example.com`
2. the alias set at `add-account` time — `work`
3. the local part before `@` — `mikael`
4. any unambiguous prefix

`account` is optional when only one mailbox is configured, or when `GMAIL_MCP_DEFAULT_ACCOUNT` is set.
Otherwise it is required, and the error lists what is available.

**IDs are per-mailbox.** A message, thread, draft, or label ID from one account is meaningless in
another. A "not found" error usually means the wrong `account` was passed.

## Tools

35 tools — the connector's surface plus two for multi-account.

| Area | Tools |
| --- | --- |
| Accounts | `list_accounts`, `get_profile` |
| Threads | `search_threads`, `get_thread`, `label_thread`, `unlabel_thread`, `trash_thread`, `untrash_thread`, `mark_thread_spam`, `unmark_thread_spam`, `apply_sensitive_thread_label`, `batch_apply_sensitive_thread_labels` |
| Messages | `get_message`, `get_message_attachment`, `label_message`, `unlabel_message`, `update_message_labels`, `trash_message`, `untrash_message`, `mark_message_spam`, `unmark_message_spam`, `apply_sensitive_message_label`, `batch_apply_sensitive_message_labels` |
| Sending | `send_message`, `reply`, `forward` |
| Drafts | `create_draft`, `update_draft`, `get_draft`, `list_drafts`, `delete_draft` |
| Labels | `list_labels`, `create_label`, `update_label`, `delete_label` |

Tool names deliberately match the Gmail connector's, so prompts and habits carry over. MCP clients
namespace tools by server, so both can be enabled at the same time without collision.

### Differences from the built-in connector

Everything below is additive — nothing the connector does behaves differently here.

- **`account`** on every tool, plus `list_accounts` and `get_profile`.
- **`response_format`** (`markdown` | `json`) on every tool. Markdown is the default and stays compact;
  the full structured payload is returned as `structuredContent` either way.
- **`delete_draft`** and **`get_message_attachment`**, which the connector references but does not expose.
- **`reply`** takes `quoteOriginal` (default `false`) to append the original as a quoted block.
- **`forward`** takes `includeAttachments` (default `true`); set it to `false` to skip large files.
- **`get_message_attachment`** takes `savePath` to write a file to disk instead of inlining base64.
- Responses are capped at 25,000 characters, shrinking paginated lists first and saying exactly what
  was dropped and how to get the rest.

### Behavior worth knowing

- `search_threads` returns a **preview** of each thread's oldest messages (~5), like the connector.
  Call `get_thread` before answering anything about recent or unread mail.
- Drafts are excluded from `search_threads` unless the query mentions drafts. Use `list_drafts`.
- Labels are addressed by **ID**, not display name. Call `list_labels` first.
- `DRAFT` and `SENT` are read-only; adding `TRASH`/`SPAM` is routed to the `apply_sensitive_*` tools so
  destructive actions are always explicitly named.
- `update_draft` merges fields, but **attachments are replaced, not merged** — the response reports how
  many were dropped.
- `reply` sets `In-Reply-To` and `References` so clients thread correctly, and never addresses the
  reply back to the authorized mailbox itself.

## Security

- Refresh tokens live in `~/.gmail-mcp/accounts.json`, written atomically with `0600` inside a `0700`
  directory. Anyone with read access to that file has full access to the mailboxes.
- The OAuth flow validates the `state` parameter and listens only on `127.0.0.1`.
- `remove-account` deletes the local token. To fully revoke, also remove the app at
  [myaccount.google.com/permissions](https://myaccount.google.com/permissions).
- The granted scopes allow reading, sending, and deleting mail. `send_message`, `reply`, and `forward`
  deliver immediately — the tool descriptions instruct the model to prefer `create_draft` when the user
  has not clearly asked for mail to go out.

## Development

```bash
npm run build         # compile TypeScript to dist/
npm test              # build, then unit tests + stdio smoke test
npm run test:unit     # MIME, reply addressing, parsing, truncation — no network
npm run test:smoke    # boots the server, checks all 35 tools register
npm run inspect       # open the MCP Inspector against the server
```

```
src/
  index.ts            CLI entry point and server bootstrap
  constants.ts        scopes, limits, paths
  types.ts            normalized message/thread/draft shapes
  auth/               token store, OAuth flow, account registry
  schemas/common.ts   shared Zod fields (account, formats, pagination, colors)
  services/           Gmail parsing, MIME building, formatting, errors
  tools/              tool registration by domain
```

## Troubleshooting

| Symptom | Fix |
| --- | --- |
| "No Gmail accounts are authorized yet" | Run `node dist/index.js add-account` |
| "Google did not return a refresh token" | Revoke the app at myaccount.google.com/permissions, then re-run `add-account` |
| "Account 'x' is ambiguous" | Use the full email address |
| 403 on every call | The Gmail API is not enabled on the Cloud project, or the address is not a listed test user |
| Not-found errors on valid IDs | The ID belongs to another mailbox — check `account` |

## License

MIT