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
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues