Reverse Proxy Setup
Behind a reverse proxy (nginx, Apache, Cloudflare, etc.) the server
constructs OAuth URLs using its internal address — e.g. http://localhost:8000 — while external clients need the
public URL. Two overrides solve this:
# Configures all OAuth endpoints to use your external URL (recommended)
export WORKSPACE_EXTERNAL_URL="https://your-domain.com"
# Or override only the OAuth callback URL
export GOOGLE_OAUTH_REDIRECT_URI="https://your-domain.com/oauth2callback"- Use
WORKSPACE_EXTERNAL_URLwhen all OAuth endpoints should use the external URL — recommended for reverse proxy setups. - Use
GOOGLE_OAUTH_REDIRECT_URIwhen you only need to override the callback URL. It must exactly match what is configured in Google Cloud Console. - Your proxy must forward OAuth-related requests —
/oauth2callback,/oauth2/*, and/.well-known/*— to the MCP server. OAUTH_CUSTOM_REDIRECT_URISandOAUTH_ALLOWED_ORIGINSaccept comma-separated additional redirect URIs and CORS origins when you need them.
Referrer-Policy pitfall
Do not set Referrer-Policy: no-referrer on your proxy. It makes browsers send Origin: null on the
same-origin consent POST, which origin validation rejects with {"error": "Origin not allowed"} (logged as Rejected HTTP request from Origin: null) even when WORKSPACE_EXTERNAL_URL is correct. Use strict-origin-when-cross-origin (the browser default) or same-origin instead.
Origin: null on the consent POST
Some clients send Origin: null on the consent POST even
with correct proxy headers — Chrome serializes the form origin as
opaque after the cross-origin OAuth redirect chain (seen with the
Claude Code CLI flow). If you hit this, strip only a
literal null Origin for the /consent endpoint:
location ^~ /consent {
set $consent_origin $http_origin;
if ($http_origin = "null") { set $consent_origin ""; }
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Origin $consent_origin;
proxy_pass http://127.0.0.1:<port>;
}This is safe because the consent endpoint is CSRF-protected by its
unguessable txn_id, and nginx forwards the empty-valued
Origin header as an empty value that ASGI decodes to empty bytes; the
middleware validates only when the raw origin is truthy, so this
request skips validation while real origins still pass through and get
validated.
Origin Validation
Streamable HTTP requests that carry an Origin header are
checked against loopback origins, WORKSPACE_EXTERNAL_URL,
and OAUTH_ALLOWED_ORIGINS to reduce DNS-rebinding risk.
Non-browser MCP clients that omit the header are unaffected, and
connections to a localhost/127.0.0.1 URL are
allowed without extra configuration.
VS Code webview origins
VS Code webview clients send a vscode-webview://<extension-id> origin, which is
rejected by default. Add the specific origin to OAUTH_ALLOWED_ORIGINS to permit it:
# Permit a specific VS Code extension's webview origin
export OAUTH_ALLOWED_ORIGINS="vscode-webview://your.extension-id"Origins with the vscode-webview:// scheme are scoped
per-extension using the authority component — allowlisting one
extension's URI permits only that extension; others are rejected.
Credential Store Backends
The server manages Google OAuth credentials through an abstract credential store with pluggable backends, selected by environment variable:
local_directory(default) — plaintext JSON files protected by filesystem permissions (0o600 / 0o700). Suitable for local development and single-user deployments.gcs— stores each user's credentials as an object in a Google Cloud Storage bucket, with atomic read-modify-write via generation preconditions, first-class Cloud IAM and Audit Logs integration, and transparent bucket-level CMEK encryption at rest. Designed for multi-user OAuth 2.1 mode where users are looked up individually by email.
# Install the optional dependency if you plan to use the GCS backend:
# uv sync --extra gcs
# or: pip install "workspace-mcp[gcs]"
# Select backend (default: local_directory). Supported: local_directory, gcs
export WORKSPACE_MCP_CREDENTIAL_STORE_BACKEND="gcs"
# --- local_directory options ---
export WORKSPACE_MCP_CREDENTIALS_DIR="/path/to/credentials"
# Backward-compatible alias:
export GOOGLE_MCP_CREDENTIALS_DIR="/path/to/credentials"
# Default directory locations (if no directory env var is set):
# - ~/.google_workspace_mcp/credentials (if home directory accessible)
# - ./.credentials (fallback)
# --- gcs options ---
export WORKSPACE_MCP_GCS_BUCKET="my-workspace-mcp-tokens" # required
export WORKSPACE_MCP_GCS_PREFIX="credentials/" # optional
export WORKSPACE_MCP_GCS_REQUIRE_CMEK="true" # optionalThe GCS backend authenticates via Application Default Credentials — on
Cloud Run the runtime service account needs roles/storage.objectUser (or equivalent) on the bucket.
It intentionally does not support user enumeration; use your identity
provider for that.
CMEK enforcement
By default GCS encrypts objects with Google-managed keys. For
customer-managed encryption, set a default KMS key on the bucket (e.g.
via Terraform's google_storage_bucket.encryption.default_kms_key_name) —
all credentials written to the bucket inherit the key transparently.
To guard against deploying to a bucket without CMEK, set WORKSPACE_MCP_GCS_REQUIRE_CMEK=true: the store verifies
the bucket has a default KMS key at startup and refuses to initialize
otherwise. This check reads bucket metadata, so the service account
also needs storage.buckets.get (grant roles/storage.bucketViewer in addition to the
object-level role).
Python API
For programmatic access — custom tooling, migrations, or embedding the server — the store exposes a small API:
from auth.credential_store import get_credential_store, LocalDirectoryCredentialStore
# Get the global credential store instance
store = get_credential_store()
# Store credentials for a user
store.store_credential("[email protected]", credentials)
# Retrieve credentials
creds = store.get_credential("[email protected]")
# List all users with stored credentials (local_directory backend only;
# GCSCredentialStore intentionally does not support enumeration — use the
# upstream identity provider to enumerate users instead).
if isinstance(store, LocalDirectoryCredentialStore):
users = store.list_users()Environment Variable Reference
Every environment variable the server reads, grouped by concern. Variables marked required* are required for development only — Claude Desktop stores credentials securely in the OS keychain.
🔐 Authentication
| Variable | Purpose |
|---|---|
GOOGLE_OAUTH_CLIENT_ID required | OAuth client ID from Google Cloud |
GOOGLE_OAUTH_CLIENT_SECRET | OAuth client secret for confidential clients; optional for public OAuth 2.1 PKCE clients |
OAUTHLIB_INSECURE_TRANSPORT required* | Set to 1 for development — allows http:// redirect |
USER_GOOGLE_EMAIL | Default email for single-user auth |
GOOGLE_CLIENT_SECRET_PATH | Custom path to client_secret.json |
GOOGLE_MCP_CREDENTIALS_DIR | Credential directory — default ~/.google_workspace_mcp/credentials |
🖥️ Server
| Variable | Purpose |
|---|---|
WORKSPACE_MCP_BASE_URI | Base server URI (no port) — default http://localhost |
WORKSPACE_MCP_PORT | Listening port — default 8000. Also controls the stdio-mode OAuth callback port. The PORT env var takes precedence if set. |
WORKSPACE_MCP_HOST | Bind host — default 0.0.0.0 for OAuth 2.1 HTTP, 127.0.0.1 for legacy streamable HTTP |
WORKSPACE_MCP_TRANSPORT | stdio or streamable-http; used when --transport is not passed |
WORKSPACE_MCP_HTTP_PORT | Advanced legacy-stdio sidecar /mcp port for local workspace-cli access. Disabled when empty. Binds to 127.0.0.1 only. |
WORKSPACE_EXTERNAL_URL | External URL for reverse proxy setups |
WORKSPACE_MCP_BRAND_NAME | OAuth 2.1 consent-page server name — default FastMCP's name |
WORKSPACE_MCP_BRAND_ICON_URL | OAuth 2.1 consent-page logo (hosted URL or data: URI), shown at 64px wide |
WORKSPACE_MCP_BRAND_WEBSITE_URL | OAuth 2.1 consent-page website link |
WORKSPACE_ATTACHMENT_DIR | Downloaded attachments dir and default trusted local attachment directory — default ~/.workspace-mcp/attachments/ |
WORKSPACE_MCP_URL | Remote MCP endpoint URL for CLI |
ALLOWED_FILE_DIRS | Colon-separated allowlist for local file reads |
🧰 Tool Selection
| Variable | Purpose |
|---|---|
WORKSPACE_MCP_TOOLS | Comma-separated services, e.g. gmail,drive,calendar; empty means all services |
WORKSPACE_MCP_TOOL_TIER | core, extended, or complete; empty means all tools |
WORKSPACE_MCP_READ_ONLY | true, 1, or yes to request read-only scopes and filter write tools |
WORKSPACE_MCP_PERMISSIONS | Space-separated service:level entries, e.g. gmail:send drive:readonly; mutually exclusive with tools and read-only |
🔑 OAuth 2.1 & Multi-User
| Variable | Purpose |
|---|---|
MCP_ENABLE_OAUTH21 | true to enable OAuth 2.1 multi-user support. Required for remote or shared HTTP endpoints; optional for local-only legacy HTTP, which binds to 127.0.0.1 by default. |
EXTERNAL_OAUTH21_PROVIDER | true for external OAuth flow with bearer tokens |
WORKSPACE_MCP_STATELESS_MODE | true for stateless container-friendly operation |
WORKSPACE_MCP_LOG_DIR | Directory for mcp_server_debug.log — defaults to ~/.google_workspace_mcp/logs |
GOOGLE_OAUTH_REDIRECT_URI | Override OAuth callback URL — default auto-constructed |
OAUTH_CUSTOM_REDIRECT_URIS | Comma-separated additional redirect URIs |
OAUTH_ALLOWED_ORIGINS | Comma-separated additional CORS origins |
WORKSPACE_MCP_OAUTH_PROXY_STORAGE_BACKEND | memory, disk, or valkey — OAuth proxy state storage |
FASTMCP_SERVER_AUTH_GOOGLE_JWT_SIGNING_KEY | Custom encryption key for OAuth proxy storage; required for public OAuth 2.1 clients when GOOGLE_OAUTH_CLIENT_SECRET is omitted |
WORKSPACE_MCP_ALLOWED_CLIENT_REDIRECT_URIS | Comma-separated allowlist of redirect URIs that dynamically-registered OAuth clients may use. Default unset (any URI permitted, per DCR). Supports glob patterns (*, *.example.com) |
🗄️ Credential Store
| Variable | Purpose |
|---|---|
WORKSPACE_MCP_CREDENTIAL_STORE_BACKEND | local_directory (default) or gcs — see the credential store section above |
WORKSPACE_MCP_CREDENTIALS_DIR | Directory for the local_directory backend |
GOOGLE_MCP_CREDENTIALS_DIR | Backward-compatible alias for WORKSPACE_MCP_CREDENTIALS_DIR |
WORKSPACE_MCP_GCS_BUCKET required for gcs | GCS bucket name for the gcs backend |
WORKSPACE_MCP_GCS_PREFIX | Optional object-name prefix for the gcs backend |
WORKSPACE_MCP_GCS_REQUIRE_CMEK | true to require a bucket default KMS key at startup (fails fast if unset) |
🔧 Service Account
| Variable | Purpose |
|---|---|
GOOGLE_SERVICE_ACCOUNT_KEY_FILE | Path to service account JSON key file (domain-wide delegation) |
GOOGLE_SERVICE_ACCOUNT_KEY_JSON | Inline service account JSON key (alternative to file) |
DWD_ALLOWED_DOMAINS | Comma-separated domain allowlist for per-request impersonation (optional) |
🔍 Custom Search
| Variable | Purpose |
|---|---|
GOOGLE_PSE_API_KEY | API key for Programmable Search Engine |
GOOGLE_PSE_ENGINE_ID | Search Engine ID for PSE |