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) #
-
Start the stack:
mise run dev -
Register the server with Claude Code:
mise run tofu-external-mcp-connectThis reads the Clerk client id and secret from
.env, registerstofu-external-mcpin your private~/.claude.json(never the repo), and checks the server is up. Source:tools/local/scripts/connect-tofu-external-mcp-local.sh. -
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 #
- Run
/mcp, choosetofu-external-mcp, then Authenticate. A browser opens the Clerk login. - Pick your org on the consent screen. The token is scoped to that org, so tools only see that org’s data.
- Click Allow. The login completes in Claude Code.
- Check:
claude mcp listshowstofu-external-mcp - ✓ Connected. Ask Claude to calllist_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.
-
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, thentaskkill /PID <PID> /F. -
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):
- In Clerk, register
http://localhost:<port>/callbackandhttp://127.0.0.1:<port>/callbackon the OAuth app. - 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). - 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.