Metadata-Version: 2.4
Name: multi-gw-mcp
Version: 1.0.2
Summary: Multi-account Google Workspace MCP server — Gmail, Drive, Sheets, Calendar
Project-URL: Homepage, https://github.com/3j-technologies/multi-gw-mcp
Project-URL: Repository, https://github.com/3j-technologies/multi-gw-mcp
Project-URL: Issues, https://github.com/3j-technologies/multi-gw-mcp/issues
Author: 3J Technologies
License-Expression: MIT
License-File: LICENSE
Keywords: fastmcp,gmail,google-calendar,google-drive,google-sheets,google-workspace,mcp,model-context-protocol
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: fastmcp<4,>=3.1
Requires-Dist: google-api-python-client>=2.160.0
Requires-Dist: google-auth-oauthlib>=1.2.1
Requires-Dist: google-auth>=2.38.0
Requires-Dist: starlette>=0.30.0
Requires-Dist: uvicorn>=0.30.0
Description-Content-Type: text/markdown

# Google Workspace MCP Server

A [MCP](https://modelcontextprotocol.io) server built with [FastMCP](https://gofastmcp.com) for Google Workspace with **multi-account support**. Works with Claude Desktop, Claude Code, or any MCP client to interact with Gmail, Google Drive, Sheets, and Calendar across multiple Google accounts.

## Features

- **Multi-account profiles** — Named profiles (e.g. `business`, `personal`) to switch between accounts
- **Two transports** — local **STDIO** (per-profile desktop OAuth) **and** remote **HTTP + per-user OAuth**, selected via `GW_MCP_TRANSPORT` (see [Transports](#transports))
- **Zero-friction setup** — OAuth flows triggered from within Claude, no CLI needed
- **Gmail** — Search, read, send, threads, attachments, **labels (create/list/update/delete)**, a full set of **organizing tools** (archive/unarchive, mark read/unread, star/unstar, trash/untrash, per-message & per-thread label modify, batch modify, **`gmail_apply_to_search`**), and **filters (rules)**: list/create/delete the auto-label/auto-archive rules — **25 tools**
- **Google Drive** — Upload, download, list, search, folders, move, trash (7 tools)
- **Google Sheets** — Read, write, append, create, metadata (5 tools)
- **Google Calendar** — Events, create/update/delete, free/busy, calendars (6 tools)
- **51 tools total** including auth/setup + profile tools
- **Clean architecture** — Services layer (Google API) separated from tools layer (MCP)

## Prerequisites

A **Google Cloud Project** with OAuth 2.0 credentials:
1. Go to [Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials)
2. Create an **OAuth 2.0 Client ID** (type: **Desktop app**)
3. Enable these APIs in [API Library](https://console.cloud.google.com/apis/library):
   Gmail API, Google Drive API, Google Sheets API, Google Calendar API

That's it. No redirect URIs to configure — the server handles it.

## Quick Start (Claude Desktop)

Add to your Claude Desktop config:

**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "google-workspace": {
      "command": "uvx",
      "args": ["--from", "/path/to/google-workspace-mcp-py", "google-workspace-mcp"]
    }
  }
}
```

Restart Claude Desktop. That's it — no `pip install`, no venv. `uvx` handles everything.

> **Requires [uv](https://docs.astral.sh/uv/getting-started/installation/)** — install with `curl -LsSf https://astral.sh/uv/install.sh | sh` (Mac/Linux) or `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"` (Windows)

### Install from PyPI

Each release is published to PyPI as [`multi-gw-mcp`](https://pypi.org/project/multi-gw-mcp/), so you can also install/run it by name instead of a local path:

```bash
pip install multi-gw-mcp          # or: uvx --from multi-gw-mcp google-workspace-mcp
```

## Quick Start (Claude Code)

```bash
claude mcp add google-workspace -- uvx --from /path/to/google-workspace-mcp-py google-workspace-mcp
```

## Setup Flow (happens inside Claude)

Once the server is running, Claude handles everything:

1. **Claude calls `check_setup_status`** → sees "not configured"
2. **You provide** your Google Cloud Client ID and Secret
3. **Claude calls `setup_google_workspace`** → config saved automatically
4. **Claude calls `authenticate_account("business")`** → your browser opens to Google sign-in → you click Allow → token saved
5. **Claude calls `authenticate_account("personal")`** → same for second account
6. **Done.** All tools work. Tokens auto-refresh.

No terminal commands. No config files to hand-edit. No copy-pasting tokens.

## Transports

The server runs in one of two modes, chosen by the `GW_MCP_TRANSPORT` env var
(`main()` in `server.py`):

| `GW_MCP_TRANSPORT` | Mode | Auth |
|--------------------|------|------|
| `stdio` (default) | Local STDIO | Per-**profile** desktop OAuth; tokens on disk under `GW_MCP_CONFIG_DIR`. Use the in-chat setup flow above. |
| `http` (or `sse`) | Remote HTTP server | Per-**user** OAuth via FastMCP's built-in `GoogleProvider`. Each connecting user signs in with their own Google account through the MCP protocol; additional accounts are added at runtime with `add_gw_account`. Requires `GW_MCP_PUBLIC_URL`, `GW_MCP_WEB_CLIENT_ID`, `GW_MCP_WEB_CLIENT_SECRET`. |

In HTTP mode the server binds `GW_MCP_HOST`/`GW_MCP_PORT` and exposes a `/health`
check plus an OAuth callback for adding profiles. Both transports share the same
51 tools and the same OAuth scopes.

## If Published to PyPI

Once published, the config simplifies to:

```json
{
  "mcpServers": {
    "google-workspace": {
      "command": "uvx",
      "args": ["google-workspace-mcp"]
    }
  }
}
```

## Usage

Every tool accepts an optional `profile` parameter. If omitted, the first profile is used.

```
# Search business Gmail
gmail_search(query="from:aws subject:invoice after:2025/04/01", profile="business")

# Search personal Gmail
gmail_search(query="from:billing@example.com invoice", profile="personal")

# Organize: archive + mark read + label everything from a newsletter in one call
# (label args accept NAMES or IDs — names are resolved automatically)
gmail_apply_to_search(
    query="from:newsletter@example.com older_than:30d",
    add_label_ids=["Archive/Newsletters"],
    archive=True, mark_read=True, max_results=200, profile="business"
)

# Star / archive a single message, or archive a whole conversation
gmail_star(message_id="...")
gmail_archive(message_id="...")
gmail_modify_thread(thread_id="...", remove_labels=["INBOX"], add_labels=["STARRED"])

# Trash is recoverable (not a permanent delete)
gmail_trash(message_id="...")
gmail_untrash(message_id="...")

# Filters (rules): auto-label + skip-inbox every future newsletter
# (label NAMES are resolved to IDs; the label must already exist)
gmail_create_filter(
    from_address="newsletter@example.com",
    add_labels=["Newsletters"], archive=True, mark_read=True, profile="business"
)
gmail_list_filters(profile="business")
gmail_delete_filter(filter_id="ANe1...", profile="business")

# Upload to Drive
drive_upload(local_path="/tmp/report.xlsx", folder_id="abc123", profile="business")

# Read a Google Sheet
sheets_read(spreadsheet_id="abc123", range="Sheet1!A1:D10")

# Check calendar
calendar_list_events(time_min="2025-04-01T00:00:00Z", time_max="2025-04-02T00:00:00Z")
```

## All Tools (51)

| Category | Tool | Description |
|----------|------|-------------|
| **Setup** | `check_setup_status` | Check config & auth status (STDIO) |
| **Setup** | `setup_google_workspace` | Save OAuth credentials + profiles (STDIO) |
| **Setup** | `authenticate_account` | Open browser OAuth for a profile (STDIO) |
| **Setup** | `add_google_profile` | Add another account to config (STDIO) |
| **Profiles** | `list_google_profiles` | List accounts & auth status (STDIO) |
| **Profiles** | `whoami` | Show the authenticated user (HTTP) |
| **Profiles** | `add_gw_account` | Add another Google account at runtime (HTTP) |
| **Profiles** | `list_gw_accounts` | List connected accounts (HTTP) |
| **Gmail** | `gmail_search` | Search with Gmail query syntax |
| **Gmail** | `gmail_read` | Read a message by ID |
| **Gmail** | `gmail_read_thread` | Read entire thread |
| **Gmail** | `gmail_send` | Send email or reply |
| **Gmail** | `gmail_list_labels` | List system + user labels |
| **Gmail** | `gmail_create_label` | Create a user label |
| **Gmail** | `gmail_update_label` | Rename a user label |
| **Gmail** | `gmail_delete_label` | Delete a user label |
| **Gmail** | `gmail_modify_message` | Add/remove labels on a message |
| **Gmail** | `gmail_batch_modify` | Add/remove labels on many messages |
| **Gmail** | `gmail_modify_thread` | Add/remove labels on a whole thread |
| **Gmail** | `gmail_archive` / `gmail_unarchive` | Remove/add `INBOX` |
| **Gmail** | `gmail_mark_read` / `gmail_mark_unread` | Remove/add `UNREAD` |
| **Gmail** | `gmail_star` / `gmail_unstar` | Add/remove `STARRED` |
| **Gmail** | `gmail_trash` / `gmail_untrash` | Move to / restore from Trash (recoverable; not permanent delete) |
| **Gmail** | `gmail_apply_to_search` | Run a search and bulk archive/label/mark-read all matches |
| **Gmail** | `gmail_list_filters` | List all filters (auto-label/auto-archive rules) |
| **Gmail** | `gmail_create_filter` | Create a filter — match by from/to/subject/query/etc., act via labels + flags (archive/mark_read/star/forward) |
| **Gmail** | `gmail_delete_filter` | Delete a filter by ID |
| **Gmail** | `gmail_get_attachment_data` | Get attachment as base64 (or save server-side) |
| **Gmail** | `gmail_save_attachment_to_server` | Save attachment to the server filesystem |
| **Drive** | `drive_list` | List/search files |
| **Drive** | `drive_upload` | Upload local file |
| **Drive** | `drive_upload_base64` | Upload from base64 data |
| **Drive** | `drive_download` | Download file |
| **Drive** | `drive_create_folder` | Create folder |
| **Drive** | `drive_move` | Move between folders |
| **Drive** | `drive_trash` | Trash a file |
| **Sheets** | `sheets_read` | Read data from range |
| **Sheets** | `sheets_write` | Write data to range |
| **Sheets** | `sheets_append` | Append rows |
| **Sheets** | `sheets_create` | Create spreadsheet |
| **Sheets** | `sheets_info` | Get metadata |
| **Calendar** | `calendar_list_events` | List events |
| **Calendar** | `calendar_create_event` | Create event |
| **Calendar** | `calendar_update_event` | Update event |
| **Calendar** | `calendar_delete_event` | Delete event |
| **Calendar** | `calendar_free_busy` | Check availability |
| **Calendar** | `calendar_list_calendars` | List calendars |

## Architecture

```
src/google_workspace_mcp/
├── server.py          # FastMCP composition + middleware
├── auth/
│   ├── oauth.py       # OAuth2 + profiles + auto-setup + browser auth flow
│   └── setup.py       # Optional CLI fallback (gw-mcp-auth)
├── services/          # Google API wrappers (reusable, testable)
│   ├── gmail.py
│   ├── drive.py
│   ├── sheets.py
│   └── calendar.py
├── tools/             # FastMCP @mcp.tool decorated functions
│   ├── profiles.py    # Setup + auth + profile tools
│   ├── gmail.py
│   ├── drive.py
│   ├── sheets.py
│   └── calendar.py
└── middleware/
    └── error_handler.py   # Google API errors → friendly messages
```

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `GW_MCP_TRANSPORT` | `stdio` | `stdio` (local) or `http`/`sse` (remote server) |
| `GW_MCP_HOST` | `127.0.0.1` | Bind host (HTTP mode) |
| `GW_MCP_PORT` | `8000` | Bind port (HTTP mode) |
| `GW_MCP_PUBLIC_URL` | _(unset)_ | Public base URL — set to enable HTTP per-user OAuth |
| `GW_MCP_WEB_CLIENT_ID` / `GW_MCP_WEB_CLIENT_SECRET` | _(unset)_ | Web OAuth client for HTTP mode |
| `GW_MCP_CONFIG_DIR` | `~/.config/google-workspace-mcp` | Config & token directory |
| `GW_MCP_OAUTH_PORT` | `3456` | Local port for OAuth callback (STDIO mode) |
| `GW_MCP_ALLOWED_CLIENT_REDIRECTS` | _(built-in default)_ | Comma-separated allow-list of **MCP-client** redirect URI patterns. Defaults already cover Claude Code/Desktop loopback and Claude Web. Override only to extend. |

### OAuth scopes

Both transports request the same scopes:

- `gmail.readonly`, `gmail.send`, **`gmail.modify`**, **`gmail.settings.basic`**
- `drive.file` (HTTP) / `drive` (STDIO), `spreadsheets`, `calendar`

`gmail.modify` covers label changes and `messages.trash`/`untrash`.
**`gmail.settings.basic`** is required for the filter (rule) tools
(`gmail_list_filters` / `gmail_create_filter` / `gmail_delete_filter`).
Permanent deletion is intentionally **not** supported (it would require the
broader full-mailbox scope).

> ⚠️ **Re-consent required.** Adding `gmail.settings.basic` changes the
> requested scope set, so **every existing user must re-authenticate** on their
> next use — existing tokens lack the new scope and the filter tools will return
> an insufficient-scope error until the account is re-authed. This is a one-time
> consent screen; no data is lost.

## Remote / HTTP mode OAuth (Claude Code, Desktop & Web)

When `GW_MCP_PUBLIC_URL` is set the server becomes its own OAuth authorization
server and proxies upstream to Google. There are **two distinct redirect
layers** — do not conflate them:

1. **MCP client → this server** (handled in code). Claude clients register via
   Dynamic Client Registration with their own redirect URI:
   - **Claude Code (CLI)** and **Claude Desktop** use an RFC 8252 loopback on a
     dynamic port: `http://localhost:<port>/...` / `http://127.0.0.1:<port>/...`.
   - **Claude Web (claude.ai)** uses the fixed callback
     `https://claude.ai/api/mcp/auth_callback`.

   All three are accepted by the built-in `GW_MCP_ALLOWED_CLIENT_REDIRECTS`
   allow-list — no configuration needed.

2. **This server → Google** (one human step). The server always sends Google a
   single fixed redirect: **`<GW_MCP_PUBLIC_URL>/auth/callback`**. This exact
   URI **must be registered** in the Google Cloud Console:

   > APIs & Services → Credentials → your **Web** OAuth 2.0 Client
   > (`GW_MCP_WEB_CLIENT_ID`) → **Authorized redirect URIs** → add
   > `https://<your-public-host>/auth/callback`
   > (e.g. `https://mcp.example.com/auth/callback`).

   If this is missing, **every** client fails with Google
   `redirect_uri_mismatch` — it is not a per-client problem.

## Optional CLI

If you prefer terminal setup over in-chat setup:

```bash
uvx --from /path/to/project gw-mcp-auth --init       # Create config interactively
uvx --from /path/to/project gw-mcp-auth --profile biz # Auth one profile
uvx --from /path/to/project gw-mcp-auth --status      # Check status
```

## Built With

- [FastMCP](https://gofastmcp.com) — Python MCP framework
- [Google API Python Client](https://github.com/googleapis/google-api-python-client)
- [uv](https://docs.astral.sh/uv/) — Fast Python package management

## Contributing

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the dev
setup and workflow. For security issues, please follow [SECURITY.md](SECURITY.md)
rather than opening a public issue.

## License

[MIT](LICENSE) © 3J Technologies
