fix(claude): stop stale .mcp.json residue outranking a project switch, and gate stale builds #106
Reference in New Issue
Block a user
Delete Branch "worktree-fix-statusline-project"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The bug
Switching projects (
mcpctl config claude --project sre) left the Claude Code status line reporting the old project forever.config claudewrites user scope (~/.claude.json) and never rewrites a checkout's.mcp.json. Butstatuslinepreferred that file unconditionally — so a legacy project-named entry left by an older mcpctl (homeautomation→mcpctl mcp -p homeautomation, from before the constant server name) outranked every switch.Reproduced live: with user scope on
sre,mcpctl statusline --directory ~/developer/michalzxc/claude/debugprintedmcpctl:homeautomation— a server Claude Code also had indisabledMcpServersfor that directory, so the line named a project that was not mounted at all.The fix
Rank the sources by how deliberate each is:
mcpctlpin in that directory's.mcp.json— a decision, and the scope Claude Code itself prefers for a shared server name;config claude --projectwrites, so a switch can win;.mcpctl-projectmarker.A server Claude Code has switched off for that directory (
disabledMcpServers/disabledMcpjsonServers) is skipped at every step. A.mcp.jsonserver in neither list is still awaiting approval and does count — blanking the line on a fresh checkout is the more confusing failure.config claudenow also warns when the working directory's.mcp.jsoncontradicts the switch, naming the file. The two scopes are merged rather than chosen between, so nothing else would tell you.Why the build guard is in here
Diagnosing this turned up a second bug with the same shape. Everyone branches off main and builds from their own branch; a branch behind main still builds and installs perfectly, it just ships an artifact missing whatever landed meanwhile — and
rpm -U --forceoverwrites the good one with it.That is not hypothetical: mid-investigation, a build from a stale checkout replaced
/usr/bin/mcpctlwith one that had nostatuslinecommand at all, months after the status line landed on main. Nothing errored; the feature simply vanished from the installed CLI.scripts/check-main-sync.shfetches main, compares, and fails before any work happens, listing the commits you are missing. It gatesbuild-rpm.sh,build-deb.sh,build-mcpd.sh(each also runs standalone) anddeploy-k8s.sh, where a stale branch would pin its sha in Pulumi and make it the cluster's source of truth.--dry-runis exempt: it ships nothing, and blocking a read-only inspection is what teaches people to exportMCPCTL_ALLOW_BEHIND_MAIN=1permanently, disabling the gate for real deploys too. Offline it degrades to the last fetchedorigin/main, then a localmain, saying which it used.Verification
statusline.test.ts— the 3 regression tests fail against the unfixed code (verified by stashing the fix) and pass with it.mcpctl:sre; theconfig claudewarning fires end-to-end against an isolatedCLAUDE_CONFIG_DIR.build-rpm.sh,build-mcpd.shanddeploy-k8s.sheach exit 1 before any build, push, pg_dump or pulumi. The ref was built withgit commit-tree, so neither HEAD nor the working tree moved.Note:
release.shreported 2 pre-existing smoke failures inhealth-readiness.smoke.test.ts(docsand thesmoke-aws-docsfixture have nohealthCheck.tool;docmost/get_workspace401;my-home-assistant/ha_config_list_areasunknown tool). All environmental and untouched by this branch, which changes no mcpd code.Same hazard as the package build, with the cluster on the receiving end: a branch behind main builds images missing whatever landed there, and deploy-k8s.sh pins that sha in Pulumi — making the stale build the cluster's source of truth. build-mcpd.sh gets its own call because it is run standalone as well as from deploy-k8s.sh, so neither can rely on the other having checked. `--dry-run` is exempt. It builds and cuts over nothing, and blocking a read-only inspection is exactly what teaches people to export MCPCTL_ALLOW_BEHIND_MAIN=1 permanently — which would disable the gate for the real deploys too. The failure text is now artifact-agnostic ("produce an artifact" / "shipping it"), since one helper now speaks for packages, images and deploys. Verified against a synthetic ref one commit ahead: build-mcpd.sh exits 1 before any docker work, and deploy-k8s.sh exits 1 before the test gate, the pg_dump, the image build and pulumi. Neither the working tree nor HEAD was moved to test this — the ref was built with git commit-tree and deleted afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019wUmrfkVQR6CKcYKxENq7k