fix(health): a passing tools/list is live, not healthy
#104
Reference in New Issue
Block a user
Delete Branch "worktree-fix-healthcheck-unifi"
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
mcpctl get instanceshowed all eight servers healthy while the UniFi one had never once reached its controller.The default probe is
tools/list. MCP servers answer that from a static in-process table — no credentials, no upstream, ~3ms. It cannot fail for any reason a user cares about, so it reportedhealthyfor every process that managed to start.The fix
healthytools/callonhealthCheck.toollivetools/listonlyliveis the default for a server with nohealthCheck.tool. It isn't a warning — it's an admission that nothing is watching that server's upstream. Probe events now name which probe ran and which tool.Also:
healthCheck.tooloptional, so timings are tunable without inventing a readiness probecreate server --health-check-tool/-args/-interval/-timeout/-failure-threshold(everything applyable is a create flag); merges over a--from-templatehealthCheckdescribe instanceexplains aliveverdictcreate.tsheld a raw NUL byte in a string literal — grep treated the whole file as binary and silently skipped it. Escaped as\\0.Verified on the live fleet
Readiness probes configured on all eight servers; two that had read healthy for months turned red immediately:
UniFi's probe latency went 3ms (
tools/list, lying) → 1847ms (first reallist_sites: login, TLS, controller round trip).Companion infra change: kubernetes-deployment
fix/mcpctl-server-egress-targets(pod egress was capped at 80/443; the UniFi controller is on :8443).2464 unit tests pass; typecheck clean; no new lint findings (874 pre-existing, unchanged).
🤖 Generated with Claude Code
https://claude.ai/code/session_0114dg56YmVacyqhp5fitcTb
live, nothealthy`mcpctl get instances` showed all eight servers healthy while the UniFi one had never once reached its controller. The default probe is `tools/list`, which MCP servers answer from a static in-process table — no credentials, no upstream, ~3ms. It cannot fail for any reason the user cares about, so it was reporting `healthy` for every process that managed to start. Split the two passes: healthy — readiness: `tools/call` on `healthCheck.tool`. The upstream answered, so the server can actually do its job. live — liveness: `tools/list` only. Process up, upstream unverified. `live` is now the default for any server without a `healthCheck.tool`. It is not a warning; it is an admission that nothing is watching that server. Probe events name which probe ran and which tool ("Readiness check (list_sites) passed"), so the events log distinguishes the two after the fact. Also: - `healthCheck.tool` is optional now, so the timings can be tuned without inventing a readiness probe. - `create server --health-check-tool/-args/-interval/-timeout/ -failure-threshold`, per the rule that everything applyable is a create flag. Merges over a `--from-template` healthCheck rather than replacing it. - `describe instance` explains a `live` verdict instead of leaving it cryptic. - create.ts held a raw NUL byte in a string literal, which made grep treat the whole file as binary and silently skip it. Escaped as `\0`. Verified against the live fleet: with readiness probes configured, my-grafana went unhealthy (Grafana API 403) and my-node-red degraded (connect timeout to a Tailscale address) — both had read healthy for months. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114dg56YmVacyqhp5fitcTb