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.