Advanced Deployment

The self-hosting reference for production Workspace MCP deployments: putting the server behind a reverse proxy, understanding origin validation, choosing a credential store backend, and every environment variable the server reads.

Synced with v2.2.1 ·

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_URL when all OAuth endpoints should use the external URL — recommended for reverse proxy setups.
  • Use GOOGLE_OAUTH_REDIRECT_URI when 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_URIS and OAUTH_ALLOWED_ORIGINS accept 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"                # optional

The 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

VariablePurpose
GOOGLE_OAUTH_CLIENT_ID requiredOAuth 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

VariablePurpose
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

VariablePurpose
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

VariablePurpose
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_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

VariablePurpose
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 gcsGCS 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

VariablePurpose
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

VariablePurpose
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

VariablePurpose
GOOGLE_PSE_API_KEY API key for Programmable Search Engine
GOOGLE_PSE_ENGINE_ID Search Engine ID for PSE