Skip to main content
Glama
dhrubajyoti-giri

Google Workspace MCP Server

Google Workspace MCP

A standalone MCP server exposing Google Workspace tools (Gmail, Drive, Docs, Sheets, Calendar, Slides, Tasks) via MCP OAuth 2.0 — no static bearer tokens, no per-user config.

How It Works

MCP Client (QwenPaw / Claude Desktop / Claude Code)
  │
  │ POST /mcp/ (no token)
  ├── 401 with WWW-Authenticate: Bearer resource_metadata="https://.../oauth-protected-resource/mcp"
  │
  │ GET /.well-known/oauth-protected-resource/mcp   → discovers auth server URL
  │ GET /.well-known/oauth-authorization-server       → discovers endpoints (/authorize, /token, /register, /revoke)
  │ POST /register                                  → gets client_id
  │ GET /authorize                                  → browser: Google login + scope selection
  │ POST /token                                     → receives MCP JWT (access_token + refresh_token)
  │
  │ POST /mcp/  (Authorization: Bearer <JWT>)
  │
  │ JWT "sub" claim = Google email → GoogleClient looks up credentials in access_tokens.json → calls Google API
  • Token format: JWT signed with MCP_JWT_SECRET. sub = Google email.

  • Token store: access_tokens.json (per-user Google refresh tokens + granted scopes)

  • OAuth flow: Two-step — identify (openid/email) → scope selection UI → authorize

  • Token refresh: MCP access tokens expire in 8h; clients auto-refresh using the 30d refresh token

  • Scope changes: Re-authorize via the client's "re-connect" button (existing scopes pre-selected)

Related MCP server: Google Workspace MCP Server

Prerequisites

  • Docker + Docker Compose

  • A publicly reachable HTTPS URL (EXTERNAL_URL) behind a TLS proxy (Caddy, nginx, Cloudflare Tunnel)

  • Google Cloud OAuth 2.0 credentials (client_secret.json) — instructions

Quick Start

1. Configure

cp .env.example .env

# Edit .env:
#   EXTERNAL_URL=https://mcp.yourdomain.com      # your public HTTPS domain
#   MCP_JWT_SECRET=$(openssl rand -hex 32)
#   SECRETS_VOLUME_PATH=/absolute/path/to/secrets  # host dir for OAuth files

2. Add Google credentials

cp your-downloaded-client_secret.json /absolute/path/to/secrets/client_secret.json

3. Start

docker compose up -d --build

4. Verify

curl http://localhost:8000/healthz
# Expected: {"status":"ok",...}  (may show "degraded" until first user authorizes)

5. Add to your MCP client (no bearer token needed)

The MCP client auto-discovers OAuth on first connect — no token in the config.

QwenPaw — place this JSON as a new driver config:

{
  "key": "google-workspace-mcp",
  "name": "Google Workspace MCP",
  "description": "Gmail, Drive, Docs, Sheets, Calendar, Slides",
  "enabled": true,
  "transport": "streamable_http",
  "url": "http://127.0.0.1:8000/mcp/"
}

QwenPaw fields oauth_status, access_summary, tools, and headers are auto-populated after the first connection. Set only key, name, transport, and url.

Claude Desktop — add to claude_desktop_config.json:

{
  "mcpServers": {
    "google-workspace-mcp": {
      "url": "http://127.0.0.1:8000/mcp/"
    }
  }
}

Claude Code:

claude mcp add google-workspace-mcp --transport http --url http://127.0.0.1:8000/mcp/

6. Authorize

On first MCP tool call, your client will:

  1. Auto-discover OAuth endpoints (via the 401 → WWW-Authenticate/.well-known/...)

  2. Register a dynamic client

  3. Open a browser to Google login + scope selection

  4. Store the MCP token internally

After authorization, oauth_status in the client shows "connected" and tools are available.

Getting Google OAuth Credentials

  1. Go to Google Cloud Console

  2. Create a new project (or use existing)

  3. Enable APIs: Gmail API, Drive API, Docs API, Sheets API, Calendar API, Slides API

  4. Go to APIs & Services → Credentials → Create Credentials → OAuth client ID

  5. Application type: Web application

  6. Name: google-workspace-mcp

  7. Authorized redirect URIs: Add https://mcp.yourdomain.com/oauth/callback

  8. Download the JSON → save as secrets/client_secret.json

  9. Set EXTERNAL_URL in .env to match your domain (for OAuth callback + discovery)

Scope Selection

The /authorize flow shows a scope selector with all Google API scopes. Existing grants are shown pre-selected on re-authorization.

Service

Read scopes

Write scopes

Gmail

gmail.readonly

gmail.modify, gmail.send, gmail.compose, gmail.metadata, gmail.settings.basic, gmail.labels, gmail.insert

Drive

drive.readonly

drive, drive.file

Docs

documents.readonly

documents

Sheets

spreadsheets.readonly

spreadsheets

Calendar

calendar.readonly

calendar

Slides

presentations.readonly

presentations

Other

contacts.readonly, tasks.readonly

contacts, tasks, chat.bot, chat.messages, chat.spaces

Plus always included: openid, userinfo.email, userinfo.profile

Environment Variables

Variable

Default

Description

EXTERNAL_URL

https://mcp.example.com

Public HTTPS URL (OAuth callbacks + metadata discovery)

MCP_JWT_SECRET

change-me-in-production

Secret for signing MCP JWT access/refresh tokens

MCP_PORT

8000

Container listen port

GOOGLE_CLIENT_SECRET_FILE

/secrets/client_secret.json

Path to Google OAuth credentials (mounted)

GOOGLE_REGISTRY_FILE

/secrets/access_tokens.json

Per-user token registry (auto-populated)

GOOGLE_SCOPES

(see .env)

Default Google API scopes if MCP client doesn't request any

MCP_ACCESS_TOKEN_TTL

28800

MCP access token lifetime in seconds (8h) — client auto-refreshes

MCP_REFRESH_TOKEN_TTL

2592000

MCP refresh token lifetime in seconds (30d)

MCP_AUTH_CODE_TTL

600

MCP authorization code lifetime in seconds (10 min)

SCOPE_SELECTOR_MODE

all

all (default — show every available Google scope) · requested (only MCP-client-requested scopes)

SCOPE_SELECTOR_MODE

all

all (default — show every available scope in selector) · requested (show only MCP-client-requested scopes)

SECRETS_VOLUME_PATH

/path/to/secrets

Host directory for client_secret.json + access_tokens.json

TZ

Asia/Kolkata

Timezone

LOG_LEVEL

INFO

Log verbosity

Token Lifetimes

Token

Lifetime

Auto-managed

MCP access token (JWT)

8 hours (MCP_ACCESS_TOKEN_TTL)

Client refreshes via refresh token

MCP refresh token (JWT)

30 days (MCP_REFRESH_TOKEN_TTL)

Rotated on each refresh

MCP auth code

10 minutes (MCP_AUTH_CODE_TTL)

Exchanged once for tokens

Google refresh token

Unlimited (until revoked)

Auto-refreshed by server when access expires

All MCP token lifetimes are configurable. Google's token refresh is transparent — GoogleClient.get_credentials() refreshes silently when the access token expires.

Docker Compose

# docker-compose.yml uses env_file: .env only (no environment: block).
# Secrets volume mounted at /secrets in container.
services:
  google-workspace-mcp:
    build: .
    env_file: .env
    volumes:
      - ${SECRETS_VOLUME_PATH}:/secrets:rw
mcp.yourdomain.com {
    # Block /mcp from public internet (clients connect via localhost)
    route /mcp/* {
        respond 403
    }
    # Proxy OAuth + health endpoints
    reverse_proxy google-workspace-mcp:8000
}

Why block /mcp/* with 403? The MCP bearer-token (JWT) is enforced application-side, but the 403 is defense-in-depth — a network-level block so the MCP interface is never reachable from the public internet. If you'd rather expose /mcp/* publicly and rely solely on JWT auth, switch to Caddyfile.example-full-proxy instead.

Token Persistence

Once a Google account is authorized (entry in access_tokens.json), the user does not need to re-authorize until that entry is removed or Google revokes the refresh token. Google access tokens expire silently; the bridge auto-refreshes them using the stored refresh token — no user interaction required. Remove an account by deleting its entry from access_tokens.json.

Troubleshooting

Problem

Solution

401 on /mcp/ after auth

MCP client should auto-refresh. If stuck, restart client or re-authorize

"No Google credentials found for user@..."

User hasn't completed OAuth. Trigger OAuth via your MCP client (it discovers endpoints automatically)

403 insufficient_scope

Re-authorize with additional scopes via your MCP client (disconnect + reconnect)

404 on /.well-known/oauth-authorization-server

Check EXTERNAL_URL matches your Caddy domain

Client can't discover OAuth

Verify EXTERNAL_URL is publicly reachable over HTTPS

client_secret.json not found

Mount it at /secrets/client_secret.json (or your configured path)

500 on /oauth/callback after scope change

Google OAuth scope-mismatch Warning caught — token is recovered automatically; restart the flow (codes are single-use)

Project Structure

google-workspace-mcp/
├── app/
│   ├── config.py            # Settings (env-driven, no hardcoded values)
│   ├── main.py              # FastAPI + MCP OAuth middleware + routes
│   ├── mcp_server.py        # FastMCP server with all tools
│   ├── oauth.py             # Two-step Google OAuth web UI (/oauth/*)
│   ├── oauth_provider.py    # OAuthAuthorizationServerProvider implementation
│   ├── registry.py          # JSON-backed per-user token registry
│   ├── google_client.py     # Per-user Google credentials (via JWT subject)
│   ├── generate_config.py   # Optional config generator (manual use only)
│   └── tools/               # Tool implementations (gmail, drive, docs, ...)
├── Caddyfile                # Proxy: blocks /mcp, proxies OAuth + healthz
├── Caddyfile.example-full-proxy
├── docker-compose.yml       # env_file only (no environment: block)
├── Dockerfile
├── .env / .env.example
└── README.md

Works With

Any MCP client supporting OAuth 2.0 Dynamic Client Registration + PKCE:

  • QwenPaw

  • Claude Desktop

  • Claude Code

  • Any compliant implementation

Related MCP Connectors

Related MCP Servers