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.
If you cannot reliably rewrite headers at the proxy, set WORKSPACE_MCP_ALLOW_NULL_ORIGIN_CONSENT=true instead. The
server then accepts only POST /consent requests whose Origin is exactly null; other paths, other
methods, and real cross-site origins are still validated normally. The
consent handler keeps its double-submit CSRF check — the form token
must match the SameSite=Lax MCP_CONSENT_STATE cookie — so
origin validation is defense in depth here rather than the only guard.
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, 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_MCP_MAX_FILE_BYTES | Caps how many bytes a download may buffer into process memory (Drive files, Gmail and Chat attachments, Docs exports). Unset or 0 means uncapped, matching earlier releases; set a positive integer such as 5242880 for 5 MiB. An oversized download returns a Drive link and alternative tools instead of loading the file. Downloads streamed straight to disk are not capped; invalid or negative values fail startup. |
WORKSPACE_MCP_STATELESS_INLINE_MAX_BYTES | Stateless mode: caps the file get_drive_file_download_url returns inline as an embedded resource. Default 10 MiB, 0 disables inline returns, never above WORKSPACE_MCP_MAX_FILE_BYTES. Invalid or negative values fail startup |
WORKSPACE_MCP_MAX_OFFICE_XML_BYTES | Caps how far one .docx, .xlsx or .pptx may expand during text extraction, separately for the XML read and the text produced. Default 26214400 (25 MiB); 0 removes the limit; invalid or negative values fail startup. A file over the limit is reported as too large to extract rather than loaded. |
WORKSPACE_MCP_SESSION_IDLE_TIMEOUT | Seconds a Streamable HTTP session may sit idle before it is closed and its memory freed, default 3660 (61 minutes). A returning client gets 404 Session not found and starts a new session. 0 defers to FastMCP's FASTMCP_HTTP_SESSION_IDLE_TIMEOUT (no expiry when that is unset); invalid or negative values fail startup. Ignored in stateless mode |
WORKSPACE_MCP_GOOGLE_API_WORKERS | Thread pool size for blocking Google API requests, shared by every user of the process. Unset keeps Python's default of min(32, CPU count + 4); raise it to the concurrent tool calls a busy multi-user deployment expects, since the threads mostly wait on network I/O. Invalid or non-positive values fail startup |
WORKSPACE_MCP_GOOGLE_API_TIMEOUT_SECONDS | Socket timeout in seconds for each Google API request, default 60, so a stalled connection cannot hang a worker thread. Raise it for single requests that legitimately run longer, such as exporting a very large Doc; lower it to fail fast. Drive and Docs downloads capped by WORKSPACE_MCP_MAX_FILE_BYTES and Chat attachment downloads use a fixed timeout instead (120s read, 30s connect), and external token validation keeps 30s. Invalid or non-positive values fail startup |
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_FILES_GCS_BUCKET | HTTP transports: stage downloaded attachments in this GCS bucket and return V4 signed URLs (valid one hour) instead of instance-local /attachments/{id} links, so scaled-out and scale-to-zero deployments work and stateless mode returns download URLs. Needs the gcs extra; the service account needs object read and write on the bucket, plus roles/iam.serviceAccountTokenCreator on itself when there is no exported private key. Objects are not deleted by the server, so add a bucket lifecycle rule. Ignored under stdio |
WORKSPACE_MCP_FILES_GCS_PREFIX | Object prefix for attachments staged in WORKSPACE_MCP_FILES_GCS_BUCKET, default attachments |
WORKSPACE_MCP_URL | Remote MCP endpoint URL for CLI |
ALLOWED_FILE_DIRS | Colon-separated allowlist for local file reads |
WORKSPACE_MCP_DISABLE_LOCAL_FILES | true for hosted servers that cannot see the caller's disk: tools stop advertising file_path and refuse local paths with guidance to pass a URL or inline content, and Drive upload tools offer return_upload_url instead. Implied by stateless mode; off by default |
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_DISABLED_TOOLS | Comma-separated tool names to block individually, e.g. send_gmail_message,manage_event. Composes with every other filtering option — a blocked tool stays blocked regardless of tier or permissions. CLI equivalent: --disabled-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. Join levels with + (gmail:readonly+send) to grant only those levels' scopes instead of everything below them; 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_TOKEN_VALIDATION_WORKERS | External OAuth only: size of the token validation pool, default 4. A full pool rejects requests with 401, so raise it to the concurrent tool calls a multi-user gateway expects. Invalid or non-positive values fail startup |
WORKSPACE_MCP_TOKEN_VALIDATION_CACHE_TTL | External OAuth only: seconds to cache a validated token's identity (by hash), skipping the userinfo round-trip. Off by default, capped at 300; a revoked token passes until the TTL expires. Failures are never cached; invalid or negative values fail startup |
WORKSPACE_MCP_STATELESS_MODE | true for stateless container-friendly operation |
WORKSPACE_MCP_LOG_LEVEL | CRITICAL, ERROR, WARNING, INFO (default), or DEBUG. DEBUG may include user text; invalid values fall back to INFO |
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_ALLOWED_HOSTS | DNS-rebinding protection: comma-separated hostnames without ports, e.g. workspace-mcp,10.0.0.5. When set, requests whose Host is not loopback, listed, or the host of WORKSPACE_EXTERNAL_URL or an allowed origin get 403, so include every name clients and health probes use. Unset, any Host is accepted |
WORKSPACE_MCP_ALLOW_NULL_ORIGIN_CONSENT | true to allow Origin: null only on POST /consent — an escape hatch for OAuth redirect chains you cannot fix at the proxy (default false) |
WORKSPACE_MCP_OAUTH_PROXY_REQUIRE_CONSENT | always (default) shows the OAuth proxy consent screen on every authorization; remember shows it once per browser for each MCP client, still prompting on cross-site navigations. Any other value fails startup |
WORKSPACE_MCP_OAUTH_PROXY_STORAGE_BACKEND | memory, disk, or valkey — OAuth proxy state storage |
WORKSPACE_MCP_READINESS_TIMEOUT_SECONDS | Seconds the /health/ready probe waits on OAuth proxy storage before returning 503, default 1; invalid values log a warning and use the default. /health stays liveness-only, so point readiness at /health/ready to pull an unreachable Valkey instance out of rotation instead of restarting it |
WORKSPACE_MCP_OAUTH_PROXY_REFRESH_TOKEN_EXPIRY_SECONDS | Client-facing refresh-token lifetime, which also bounds how long per-login OAuth proxy records are kept. 1 to 31536000 (the one-year default); invalid values are ignored with a warning |
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) |
Trusted-Gateway Identity
| Variable | Purpose |
|---|---|
TRUST_GATEWAY_IDENTITY | true to enable proxy-verified per-user identity. Mutually exclusive with MCP_ENABLE_OAUTH21=true. |
GATEWAY_IDENTITY_JWKS_URL required* | Proxy JWKS endpoint for assertion verification. Must use HTTPS except for loopback dev endpoints. |
GATEWAY_IDENTITY_HEADER | Header carrying the JWT - default x-pomerium-jwt-assertion |
GATEWAY_IDENTITY_ALGORITHMS | Comma-separated asymmetric signing algorithm(s) - default ES256 |
GATEWAY_IDENTITY_ISSUER | Optional issuer (iss) claim validation |
GATEWAY_IDENTITY_AUDIENCE required* | Assertion audience (aud) identifying this deployment. * Required when TRUST_GATEWAY_IDENTITY=true. |
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 |