tofu Internal MCP #
Hono + Bun MCP server that gives AI agents read-only PostgreSQL access to the BonsAI database. Lives at apps/tofu-internal-mcp.
Trust model #
Engineering-only, flat org-wide access. Any logged-in Clerk user can read any org’s data through this server — there is no per-org scoping. That’s an acceptable trade-off for an internal engineering tool used by trusted staff, but it means this server must never be exposed to customers or non-engineering users. The name says it plainly: this is the internal MCP server. A separate, per-user-scoped server (tofu-external-mcp) that forwards each caller’s own Clerk token — so BonsAPI enforces that user’s org/entity permissions — is being built for anything customer-facing.
What you can do #
| Tool | Purpose |
|---|---|
list_schemas |
List schemas (public only) |
list_tables |
List tables in a schema |
describe_table |
Columns + constraints for a table |
get_relationships |
Outgoing + incoming foreign keys |
list_enums |
Postgres enum types + values |
execute_sql |
Run a SELECT — every query wrapped in BEGIN TRANSACTION READ ONLY |
Use it from Claude Code (local dev) #
- Run
mise dev(ormise mcp-dev) inside your Coder workspace. tofu-internal-mcp comes up as thetofu-internal-mcpdocker compose service on port3002. - The project-scoped
.mcp.jsonalready points Claude Code athttp://localhost:3002with the staticTOFU_MCP_API_TOKENheader. claude mcp listshould showtofu-internal-mcp - ✓ Connected.
If it’s not connected, check docker logs tofu-internal-mcp and that TOFU_MCP_API_TOKEN is in your shell env (Doppler-injected).
Use it from claude.ai (web connector) #
Public URL: https://mcp.gotofu.com (prod EKS) — this hostname is deferred to the later hostname migration (see Deploy below) and has not moved yet. Auth flow:
This server is a pure OAuth resource server. Clerk is the only authorization server and the only client registry: there is no /register, /authorize or /token here, and no Dynamic Client Registration (DCR). Anyone who can reach the URL cannot self-register a client.
- The client hits
/→ 401 +WWW-Authenticate: Bearer ... resource_metadata=… - It fetches
/.well-known/oauth-protected-resource, whoseauthorization_serversis the Clerk issuer (TOFU_MCP_CLERK_ISSUER_URL), then Clerk’s own metadata - It signs in at Clerk with a pre-registered confidential client (client_id + secret). Only the Claude org’s connector config holds the secret: Organization settings → Connectors → custom connector → “Use your own OAuth client”.
- The Clerk-issued JWT is sent on every MCP request; the server verifies signature,
iss,typand expiry against Clerk’s JWKS.
Clerk OAuth app setup #
The Clerk “tofu MCP” OAuth app must stay confidential (“Public” off, consent screen on) and have both https://claude.ai/api/mcp/auth_callback and https://claude.com/api/mcp/auth_callback as redirect URIs. Clerk DCR must be off: curl -s https://<clerk-domain>/.well-known/oauth-authorization-server | jq .registration_endpoint → null. Other MCP clients (Claude Code, Cursor) need their own Clerk OAuth app.
Dev workspaces are gated behind Cloudflare Access — claude.ai’s egress IPs (160.79.104.0/21) need an Allow policy on the mcp--* subdomain to bypass.
Auth model #
Two paths, same endpoint:
- Static Bearer token (
TOFU_MCP_API_TOKEN) — used by Claude Code locally and CI. Set in Doppler. - Clerk JWT — claude.ai web connector. tofu-internal-mcp validates signature, issuer, type and expiry. The token’s
client_id(the client the MCP connector signs in with) must equalTOFU_MCP_CLERK_CLIENT_ID, which always comes from Doppler; if it is unset every Clerk token is rejected. The client secret is never held by this server. Accepted and rejected tokens are logged withclient_idand user id only.
src/middleware/auth.ts is the single decision point for authenticated MCP requests.
Develop on it #
mise mcp-dev # docker compose up tofu-internal-mcp + tail logs
mise mcp-test # bun test
mise mcp-check # format + lint + typecheck
Code is bun-native TS — no build step. bun --watch reloads on file change inside the container.
Important env vars #
TOFU_MCP_* env var names are unchanged by the rename — they stay as-is until the later hostname migration.
| Var | Where | What |
|---|---|---|
TOFU_MCP_DATABASE_URL |
Doppler | Read-only Postgres URL. In dev_local docker uses database:5432; prod uses YugabyteDB Cloud with mcp_readonly user. |
TOFU_MCP_API_TOKEN |
Doppler | Static Bearer for Claude Code |
TOFU_MCP_URL |
.env (rewritten by override-coder-env.sh in Coder, defaults to https://mcp.gotofu.com) |
The server’s public URL — used in WWW-Authenticate and the OAuth discovery resource field |
TOFU_MCP_CLERK_JWKS_URL |
Doppler | Clerk JWKS for JWT validation |
TOFU_MCP_CLERK_ISSUER_URL |
Doppler | Clerk issuer for JWT validation; also advertised as authorization_servers |
TOFU_MCP_CLERK_CLIENT_ID |
Doppler | The one allowed OAuth app client ID, checked against the token’s client_id claim. Required; no secret is configured here. |
Safety (defense in depth) #
- Dedicated read-only Postgres user (
mcp_readonly) in prod - Connection-level
default_transaction_read_only=true - Per-query
BEGIN TRANSACTION READ ONLYinexecute_sql - Regex blocks
COMMIT/ROLLBACK/BEGIN/SAVEPOINTinexecute_sql
Deploy #
EKS via Kustomize: deployment/resources/tofu-mcp/ + deployment/overlays/prod/tofu-mcp/. Same CI pipeline as every other service — image is built, tag is replaced in kustomization, kubectl apply -k rolls it out. Hosted at mcp.gotofu.com behind ALB + WAF (Anthropic egress 160.79.104.0/21 must be allowlisted).
Note: the Kustomize resource paths, hostnames, and Doppler secrets above still use the pre-rename tofu-mcp name. That’s intentional — this repo-level rename does not touch deploy-coupled config; it’s deferred to the later hostname migration that consolidates on mcp.gotofu.co.jp and repoints these resources.