Environments

Environments #

Classification: Restricted (describes source code, infrastructure, and access control).

dev_local Deployed dev Prod
ENVIRONMENT_NAME dev_local dev_aws production
URL http://localhost:3003 https://dev-mcp.internal.gotofu.com https://mcp.gotofu.com
Runs as docker compose service tofu-external-mcp (mise tofu-external-mcp-dev) dev-mng overlay, 1 replica (the older dev overlay is scaled to 0) prod overlay, 2 replicas behind an ALB, Cloudflare Access in front
Reachable from your machine / workspace internal hostname, not public public, behind Cloudflare Access (Anthropic egress IPs)
Typical client Claude Code CLI in a Claude Code session internal use claude.ai connector
bonsapi local bonsapi container, local DB in-cluster bonsapi in-cluster bonsapi
OAuth issuer advertised Clerk’s (TOFU_EXTERNAL_MCP_CLERK_ISSUER_URL) this server’s own URL this server’s own URL
scopes_supported profile email offline_access user:org:read profile email offline_access profile email offline_access
/authorize adds user:org:read yes no no
Loopback redirect_uri (localhost, 127.0.0.0/8, ::1) accepted rejected (400) rejected (400)

All three verify the same thing on every request: a Clerk OAuth token (typ: at+jwt) signed by the Clerk instance, with a client_id that matches TOFU_EXTERNAL_MCP_CLERK_CLIENT_ID. They differ only in where the server runs and what the OAuth discovery document advertises. See Auth.

What differs in code by environment #

Where dev_local Anywhere else
routes/oauth-discovery.ts issuer = TOFU_EXTERNAL_MCP_CLERK_ISSUER_URL (startup error if unset); scopes include user:org:read issuer = TOFU_EXTERNAL_MCP_URL; three base scopes
routes/oauth-proxy.ts /authorize adds user:org:read when the client omitted it; accepts loopback redirect_uri forwards the scope untouched; loopback redirect_uri → 400
routes/oauth-proxy.ts /token, /register unchanged unchanged
middleware/auth.ts unchanged unchanged

The gate is isLocalEnvironment() in src/oauth-scopes.ts (ENVIRONMENT_NAME === 'dev_local', strict: an unset value counts as not local).

Environment variables #

All under the TOFU_EXTERNAL_MCP_* prefix (Doppler) — never reuse the internal server’s TOFU_MCP_* vars.

Var What
TOFU_EXTERNAL_MCP_URL This server’s public URL — feeds OAuth discovery (resource, authorization_servers) and must exactly match wherever the connecting client actually reaches it (http://localhost:3003 locally; the deployed dev/prod hostname once live)
ENVIRONMENT_NAME dev_local locally, dev_aws on deployed dev, production on prod. Gates the dev_local-only OAuth behaviour (see What differs in code by environment)
TOFU_EXTERNAL_MCP_CLERK_ISSUER_URL / _JWKS_URL Clerk issuer + JWKS for verifying forwarded tokens. Required to start in every environment; also the advertised issuer in dev_local
TOFU_EXTERNAL_MCP_CLERK_CLIENT_ID / _CLIENT_SECRET This server’s dedicated Clerk OAuth app — pinned client_id for verification. The secret is what an OAuth client (for example Claude Code in dev_local) sends to /token; the server never injects it. bonsapi reads the client id as CLERK_OAUTH_CLIENT_ID (deployment/resources/bonsapi/deployment.yaml, and the local docker-compose.yml)
TOFU_EXTERNAL_MCP_AGENT_SIGNING_SECRET Shared with webapp — verifies the x-tofu-caller-agent claim a first-party session-token caller signs (src/api/agent-tool-access.ts), gating tools registered only for a specific chat-orchestrator agent (e.g. sales-only coupon tools). Optional: unset means every agent-restricted tool stays hidden from everyone (fails closed), not a startup failure.
TOFU_EXTERNAL_MCP_BONSAPI_URL / BONSAPI_INTERNAL_HOST Where to reach bonsapi (src/bonsapi/custom-fetch.ts)

Deploy #

Dev: auto-deploys on every merge to main that touches apps/tofu-external-mcp/**, via build-tofu-external-mcp/deploy-tofu-external-mcp in .github/workflows/dev-deploy.yaml (path-filtered through detect-changes.yml’s tofu_external_mcp output).

Prod (ENG-7562): live, via build-tofu-external-mcp/deploy-tofu-external-mcp jobs in the shared .github/workflows/deploy.yaml — the same tag-triggered prod-deploy.yaml pipeline every other prod service runs through (needs: [database-migration, sync-secrets, build-tofu-external-mcp], ./.github/actions/eks-deploy). Kustomize: deployment/resources/tofu-external-mcp/ + deployment/overlays/prod/tofu-external-mcp/, hosted at mcp.gotofu.com — a real cert/security-group-locked ALB. The internal server (tofu-mcp) moved off that hostname to internal-mcp.gotofu.com to free it up for this app (see tofu Internal MCP). TOFU_EXTERNAL_MCP_URL in prod Doppler must be https://mcp.gotofu.com exactly — it feeds the OAuth WWW-Authenticate header/discovery, which is stricter than the module’s own external-mcp.gotofu.com fallback default (that fallback only ever applies to local dev, where the env var is unset). Anthropic’s egress IPs (160.79.104.0/21) are allowlisted via a Cloudflare Access policy in front of that hostname, configured outside this repo (not expressible in the Kustomize ingress manifest — same as the internal server’s equivalent setup). There is no per-org access restriction beyond the identity checks described in Auth.