Local development

Local development #

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

Run tofu-external-mcp and bonsapi in your local dev stack and connect Claude Code to them through a real Clerk login (ENVIRONMENT_NAME=dev_local). For what differs between local, dev and prod see Environments.

You need the claude CLI and the dev stack. In a Coder workspace with VS Code, also read Ports.

Set up (once) #

  1. Start the stack:

    mise run dev
    
  2. Register the server with Claude Code:

    mise run tofu-external-mcp-connect
    

    This reads the Clerk client id and secret from .env, registers tofu-external-mcp in your private ~/.claude.json (never the repo), and checks the server is up. Source: tools/local/scripts/connect-tofu-external-mcp-local.sh.

  3. Close your Claude Code session and resume it (for example claude --resume) so it picks up the new config.

    Running the task again replaces your entry and signs you out of this server; log in again with /mcp. The client secret is stored in plaintext in ~/.claude.json, so treat that file as a credential.

Log in #

  1. Run /mcp, choose tofu-external-mcp, then Authenticate. A browser opens the Clerk login.
  2. Pick your org on the consent screen. The token is scoped to that org, so tools only see that org’s data.
  3. Click Allow. The login completes in Claude Code.
  4. Check: claude mcp list shows tofu-external-mcp - ✓ Connected. Ask Claude to call list_entities.

Ports #

Two ports are involved. Both are fixed for everyone.

Port What Runs in Must reach your browser as
3003 tofu-external-mcp server (login start page, OAuth endpoints) the dev stack localhost:3003
28765 Claude Code’s login callback (pinned in .mcp.json, the connect script and Clerk) Claude Code, only while a login is pending localhost:28765

On a laptop running the stack directly, nothing else is needed. In a Coder workspace with VS Code, your browser runs on your computer, so forward both ports with the same number. The repo’s .vscode/settings.json (remote.portsAttributes) labels both and sets requireLocalPort, so VS Code shows an error instead of silently picking another local port. In the Ports panel both rows must read localhost:3003 and localhost:28765. The callback port is defined once, in .mcp.json (oauth.callbackPort); the connect script reads it from there, and a test checks that the VS Code setting matches. The requireLocalPort setting applies to everyone who opens this repo, so a developer whose local port 3003 is used by something else sees a forwarding error until they free it.

If a port is taken, or you need to change it #

The port must be free on your computer, not in the workspace.

  1. Find and stop what holds it, on your computer:

    # macOS / Linux (use 3003 for the server)
    lsof -nP -iTCP:28765 -sTCP:LISTEN
    kill <PID>
    

    On Windows: netstat -ano | findstr :28765, then taskkill /PID <PID> /F.

  2. In VS Code’s Ports panel, remove the row, forward the port again, and check that Local Address reads the same number.

To use a different callback port instead (a team change, because Clerk’s redirect list is shared):

  1. In Clerk, register http://localhost:<port>/callback and http://127.0.0.1:<port>/callback on the OAuth app.
  2. Change the port in .mcp.json (oauth.callbackPort), tools/local/scripts/connect-tofu-external-mcp-local.sh (CALLBACK_PORT) and .vscode/settings.json (remote.portsAttributes).
  3. Run mise run tofu-external-mcp-connect, restart your Claude Code session, and forward the new port.

Troubleshooting #

Symptom Cause Fix
Browser stays blank on localhost:3003/authorize Port 3003 isn’t forwarded to your computer, or something else on your machine holds it Ports panel must show localhost:3003; see “If a port is taken”
Clerk: “redirect_uri … does not match any … pre-registered redirect urls” The callback port isn’t 28765 (for example a --callback-port override) Re-run mise run tofu-external-mcp-connect, or register your port in Clerk
Consent page: Allow does nothing, page keeps loading Port 28765 is held locally or not forwarded, so the redirect hangs Free the port, forward 28765, retry
Browser: “Authentication failed — Invalid state parameter” A stale login URL from an earlier attempt Start a new login from /mcp
“Authentication successful” but no tools; error says “No client secret was sent” (server log oauth_token_exchange_failed ... invalid_client) No client secret in your Claude Code config mise run tofu-external-mcp-connect, restart the session
/authorize answers 400 “Loopback redirect_uri is not allowed in this environment” The server isn’t running with ENVIRONMENT_NAME=dev_local (for example it was started on the host without your .env) Run it through mise run dev, or set ENVIRONMENT_NAME=dev_local
Tool says “Not authenticated with bonsapi” Local bonsapi has no OAuth client id (the connect task warns about this) docker compose up -d bonsapi, then retry
Tools work but list_entities is empty The token’s org isn’t the org with your data Authenticate again and pick the right org
Login fails with “port already in use” Another process, or a second login, holds 28765 Free the port, retry
Nothing above fits Server misconfigured Check docker logs tofu-external-mcp, and that TOFU_EXTERNAL_MCP_URL in your local env is http://localhost:3003 (a deployed hostname there breaks the OAuth handshake)

Develop on the server #

cd apps/tofu-external-mcp
bun test          # or: pnpm tofu-external-mcp test
pnpm tofu-external-mcp check   # format + lint + typecheck

Code is bun-native TS — no build step. bun --watch reloads on file change inside the container. CI-facing mise tasks (tofu-external-mcp-check/tofu-external-mcp-test, mirroring tofu-internal-mcp’s mcp-check/mcp-test) are wired into ts-check/ts-test (libs/typescript/.tasks.toml), which .github/workflows/ci.yml’s TypeScript jobs (ts-check, ts-test-rest) run whenever detect-changes.yml’s typescript filter (includes apps/tofu-external-mcp/**) matches — see ENG-6797.