multi-google-mcp
by GoodHabitx
README.md
# multi-google-mcp
A self-hosted, **multi-account** Google [MCP](https://modelcontextprotocol.io/) server. One local server exposes **Gmail** (read + draft) and **Google Calendar** (read + create/update/confirm-gated delete) across as many Google accounts as you authorize — each addressed per call by an `account` argument.
Built to get around the single-account limit of hosted Gmail/Calendar connectors: authorize `you@gmail.com`, `you2@gmail.com`, … and talk to each by name from one server.
## What it does — and deliberately doesn't
**Gmail — read + draft only:**
`gmail_list_accounts` · `gmail_search` · `gmail_get_thread` · `gmail_list_drafts` · `gmail_create_draft` · `gmail_list_labels` · `gmail_create_label` · `gmail_modify_labels`
> **No send tool exists.** `gmail_create_draft` stages a draft; sending stays a human action in Gmail.
**Google Calendar — events (read + write + confirm-gated delete) + calendar management (create/rename/hide/delete calendars):**
`calendar_list_calendars` · `calendar_list_events` · `calendar_get_event` · `calendar_create_event` · `calendar_update_event` · `calendar_delete_event` · `calendar_create_calendar` · `calendar_set_calendar_visibility` · `calendar_unsubscribe_calendar` · `calendar_delete_calendar` · `calendar_update_calendar`
> Create/update/delete **default to `sendUpdates="none"`** — attendees are never emailed unless you explicitly pass `send_updates` (`"all"` / `"externalOnly"`).
>
> **Two deliberate exceptions, both confirm-gated:** `calendar_delete_event` permanently deletes **one event** — requires `confirm=true`, fetches the event first so a missing event reports cleanly, and (like create/update) never emails attendees unless you opt in via `send_updates`. `calendar_delete_calendar` permanently deletes a whole *calendar* (not an event) — owned-only and gated on `confirm=true` **plus** `confirm_summary` matching the calendar's real name exactly — see [Calendar management](#calendar-management) below.
**Scopes requested:** `gmail.readonly`, `gmail.compose`, `gmail.modify`, `gmail.labels`, `calendar.events`, `calendar.readonly`, `calendar.calendarlist`, `calendar.calendars`. `gmail.send` and the full-access `mail.google.com` are intentionally **not** requested, and neither is the single full `calendar` scope — the two granular calendar-management scopes cover everything this server needs without also granting ACL/free-busy/settings access.
## Calendar management
Every calendar has a TYPE relative to an account, and the tool you use depends on it:
| Type | `accessRole` | To rename/update | To remove |
|---|---|---|---|
| **Owned** (you created it, or it's `primary`) | `owner` | `calendar_update_calendar` | `calendar_delete_calendar` — **permanent** |
| **Subscribed** (someone shared it with you) | anything else | *(not yours to rename)* | `calendar_unsubscribe_calendar` — reversible |
- `calendar_list_calendars` now reports `owned`, `access_role`, `primary`, `selected`, `hidden` per calendar, and accepts `show_hidden` / `show_deleted` to surface calendars normally left out of the list.
- `calendar_create_calendar` — create a new calendar you own.
- `calendar_set_calendar_visibility` — show/hide or select/deselect a calendar in your list without removing it.
- `calendar_unsubscribe_calendar` — leave a calendar you're subscribed to (refuses on your primary calendar or one you own).
- `calendar_update_calendar` — rename or edit description/location/time_zone on a calendar you own (refuses if you don't own it).
- `calendar_delete_calendar` — **permanently** delete a calendar you own. Refuses on the primary calendar, refuses if you don't own it, refuses without `confirm=true`, and refuses unless `confirm_summary` exactly matches the calendar's real name (a mismatch echoes the real name back so you can retry deliberately).
### Re-authorization required
The two calendar-management scopes (`calendar.calendarlist`, `calendar.calendars`) are new in v2.1.0. An account authorized before this version keeps every other tool working; the 5 management tools above return
```
account <email> needs re-authorization for calendar management: run authorize.py <email>
```
instead of a raw API error until you re-run `authorize.py <email>` for that account. Gmail and calendar event tools are unaffected.
## Requirements
- Python 3.9+
- A Google Cloud project you control (see setup). A self-hosted app talking to your own Gmail/Calendar needs its own OAuth client — there's no way around this.
## Setup
### 1. Google Cloud (one-time)
1. Create or pick a project in the [Google Cloud Console](https://console.cloud.google.com/).
2. **Enable the Gmail API and the Google Calendar API.**
3. Configure the **OAuth consent screen** (User type: *External*). Add each Google account you'll authorize as a **Test user** — or **Publish** the app to production so refresh tokens don't expire after 7 days. Publishing an External app additionally requires homepage and privacy-policy URLs on the Branding page; since your app's only user is you, just put **this repo's URL in all three link fields** (any page you stand behind works — domain ownership is only checked at verification, which a personal app never undergoes). No page at all? Stay in Testing (no URLs needed) and re-run `authorize.py` weekly.
4. Create an OAuth client of type **Desktop app** and **download its JSON** from the
creation dialog (the interactive setup asks for the file's path), or save it as:
```
~/.config/multi-google-mcp/client_secret.json
```
(Windows: `%USERPROFILE%\.config\multi-google-mcp\client_secret.json`.)
### 2. Install + configure (interactive)
```
git clone https://github.com/GoodHabitx/multi-google-mcp
cd multi-google-mcp
# Windows (PowerShell)
./setup.ps1
# macOS / Linux
./setup.sh
```
Creates `.venv/`, installs the Google client libraries, then launches the
**interactive setup** (`setup.py`): it prints each Google Console link (clickable
via OSC 8 where the terminal supports it, and copied to the clipboard — reliable
even over remote sessions), places the `client_secret.json` you download, runs
the browser consent for each account, and registers the MCP server. Re-run
`setup.py` directly (with the venv python) any time to add an account or redo a
step — it's idempotent. Steps 3–4 below are what it automates; Step 1 is the
Console work it guides you through.
### 3. Authorize each account (opens a browser)
```
# macOS / Linux
.venv/bin/python authorize.py you@gmail.com
# Windows
.venv\Scripts\python.exe authorize.py you@gmail.com
```
Repeat per account. Tokens are saved (mode 600) under `~/.config/multi-google-mcp/tokens/<email>.json` — **outside this repo.**
### 4. Register with your MCP client (user scope, per-machine)
```
claude mcp add google -s user -- /abs/path/.venv/bin/python /abs/path/server.py
```
Then **restart** the session so it loads the server. Tools appear as `mcp__google__gmail_search`, `mcp__google__calendar_list_events`, etc. (The `google` name is the server key — pick whatever you like; it becomes the tool prefix.)
## Re-authorization
Added a scope after authorizing an account (e.g. Calendar)? Just re-run `authorize.py <email>` for it — the new consent mints a token carrying the current scopes. Gmail keeps working in the meantime; calendar calls return a clean 403 until the account is re-authorized.
## Security
- **No secret ever lives in the repo.** The OAuth client + per-account tokens live under `~/.config/multi-google-mcp/` (override with `MULTI_GOOGLE_MCP_CONFIG_DIR`); `.gitignore` blocks them regardless.
- No `gmail.send`, no send tool.
- Calendar writes never notify attendees unless `send_updates` is explicitly set — including `calendar_delete_event`, which defaults to `sendUpdates="none"`.
- Two confirm-gated destructive calls in this server: `calendar_delete_event` (permanently deletes **one event** — requires `confirm=true`) and `calendar_delete_calendar` (permanently deletes a whole *calendar*, not an event — owned-only, gated on `confirm=true` + an exact-name `confirm_summary` match). See [Calendar management](#calendar-management).
## Why there's no "one-click" login
A self-hosted app that reads *your own* Gmail needs its own OAuth client and a consent click. Gmail is a Google "restricted scope" that only a **verified** app may use seamlessly, and verification is a heavy security assessment that's impractical for a personal tool — so the Google Cloud steps above are the minimum. A maintainer *could* bundle a shared Desktop-app client so users only "Sign in with Google" (they'd hit an "unverified app" screen); that's intentionally not enabled here.
## Tests
```
.venv/bin/python tests/test_threading.py
.venv/bin/python tests/test_calendar_mgmt.py
```
`test_threading.py` is hermetic — no network, no tokens, no real drafts (the Gmail service is faked in-process).
`test_calendar_mgmt.py` has two parts: a hermetic schema self-check (every `TOOL_HANDLERS` entry has a matching `TOOL_DEFS` schema and vice versa — always runs), plus a **live** create → rename → hide/show → delete round-trip against a real, throwaway calendar (`ZZ-mgmt-selftest-<timestamp>`) — plus a create-event → delete-event leg on it — on a real authorized account. The live leg only runs when `MULTI_GOOGLE_MCP_LIVE_TEST_ACCOUNT` is set to an already-authorized account's email; it's skipped (not failed) otherwise, so CI/local runs without credentials stay green.
## License
MIT — see [`LICENSE`](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues