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.

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.

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"                # 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_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

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

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

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)

🔧 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