tofu Internal MCP

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) #

  1. Run mise dev (or mise mcp-dev) inside your Coder workspace. tofu-internal-mcp comes up as the tofu-internal-mcp docker compose service on port 3002.
  2. The project-scoped .mcp.json already points Claude Code at http://localhost:3002 with the static TOFU_MCP_API_TOKEN header.
  3. claude mcp list should show tofu-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.

  1. The client hits / → 401 + WWW-Authenticate: Bearer ... resource_metadata=…
  2. It fetches /.well-known/oauth-protected-resource, whose authorization_servers is the Clerk issuer (TOFU_MCP_CLERK_ISSUER_URL), then Clerk’s own metadata
  3. 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”.
  4. The Clerk-issued JWT is sent on every MCP request; the server verifies signature, iss, typ and 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 equal TOFU_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 with client_id and 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) #

  1. Dedicated read-only Postgres user (mcp_readonly) in prod
  2. Connection-level default_transaction_read_only=true
  3. Per-query BEGIN TRANSACTION READ ONLY in execute_sql
  4. Regex blocks COMMIT/ROLLBACK/BEGIN/SAVEPOINT in execute_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.