Skip to content

refactor(availability): one shared per-entry probe for server + doctor (#435, part 1) - #437

Merged
debugmcpdev merged 2 commits into
mainfrom
feature/435-shared-availability-probe
Aug 23, 2026
Merged

refactor(availability): one shared per-entry probe for server + doctor (#435, part 1)#437
debugmcpdev merged 2 commits into
mainfrom
feature/435-shared-availability-probe

Conversation

@debugmcpdev

Copy link
Copy Markdown
Collaborator

Part 1 of #435 (from the #423 doctor review): list_supported_languages and mcp-debugger doctor hand-mirrored the same per-entry availability loop — disabled gate, installed gate, getFactory().catch fail-open, metadata-over-entry attach preference — with only computeModeAvailability actually shared. The "doctor can never disagree with the server" guarantee rested on keeping two loops manually in sync.

What changed

probeLanguageEntry (src/utils/language-availability.ts) is now that loop, and both consumers call it:

  • The server injects its 30s TTL validation cache through the new runValidate wrapper seam — same caching behavior as before, one line where the loop used to be.
  • Doctor injects its per-probe timeout through the same seam and records the raw validate outcome (validation / probeError / timedOut) via closure; the wrapper rethrows so computeModeAvailability fails open exactly as it does for the server, and doctor keeps reporting the divergence honestly (probe.failed / probe.timedOut).

Parity fence: tests/core/unit/server/server-doctor-parity.test.ts runs the real list_supported_languages handler and the real diagnose() against one registry fixture covering every availability shape (valid toolchain, invalid, throwing validate, metadata-attach overriding the registry entry, not-installed, DEBUG_MCP_DISABLE_LANGUAGES) and asserts the per-language modes are identical. If either path grows semantics the other doesn't, this test fails.

Plus 10 unit tests for probeLanguageEntry itself (disabled/not-installed gates skip factory load, fail-open on load error, metadata preference, runValidate seam, unprobeable factories).

Behavioral deltas (server side, both strict improvements doctor already had)

  • A throwing factory.getMetadata() no longer crashes the whole list_supported_languages response — it falls back to the registry entry's attach declaration.
  • getFactory load errors are recorded on the probe result instead of being silently discarded (the server still fails open; doctor uses the recorded error for its broken verdict).

Part 2 of #435 (adapter-owned doctor presentation via typed AdapterMetadata fields or an optional factory method) stays open — it's a wider @debugmcp/shared interface change across all nine adapter packages.

Verification

  • Typecheck clean; lint clean; 2953 unit tests green (server + session + unit trees), including all pre-existing doctor and list_supported_languages tests unchanged.
  • Live doctor python mock on the rebuilt dist: identical output, exit 0.

🤖 Generated with Claude Code

#435, part 1)

list_supported_languages and doctor previously hand-mirrored the same
per-entry loop (disabled gate, installed gate, getFactory fail-open,
metadata-over-entry attach preference) with only computeModeAvailability
shared — so the "doctor can never disagree with the server" guarantee
rested on a mirroring convention.

probeLanguageEntry (language-availability.ts) is now that loop, consumed
by both: the server injects its TTL validation cache via the runValidate
wrapper; doctor injects its per-probe timeout and records the raw
outcome through the same seam. A parity test runs both paths against
one registry fixture covering every availability shape (valid,
invalid, throwing validate, metadata-attach override, not-installed,
disabled) and asserts identical modes — the fence that keeps them from
drifting apart again.

Behavioral deltas, both strict improvements the doctor path already
had: the server's loop now fault-isolates a throwing
factory.getMetadata (previously it would crash the whole
list_supported_languages response) and no longer swallows getFactory
load errors invisibly (recorded on the probe result; server behavior
unchanged — it still fails open).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.38710% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/cli/commands/doctor/diagnose.ts 96.29% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Review batch on PR #437 (13 of 15 findings addressed; the remaining two
— routing checkLaunchToolchain through the shared probe, and a typed
getFactory surface instead of the duck-typed cast — noted on #435):

- Timing regression: hoisting the probe clock made a slow factory
  dynamic import eat the validate/extras budget, manufacturing a
  spurious probe.timedOut that escalated to the handler's forced exit
  on healthy languages. The budget clock now starts when validate runs;
  factory-import time is excluded (regression-tested with a 900ms
  import + 500ms extras under a 1s budget).
- A wedged getFactory (hung dynamic import) previously hung doctor
  forever — the whole probe now runs under a 2x-timeout envelope and
  reports broken + probe.timedOut, wiring it into the force-exit
  containment.
- probeLanguageEntry now carries the validate outcome itself
  (validation / validationError / probeable) instead of doctor smuggling
  it out via mutated closure variables; dead result fields (installed,
  attach) dropped.
- Unknown attach strings from third-party factory metadata are
  normalized (metadata → entry → 'none') so computeModeAvailability's
  non-exhaustive switch can no longer yield modes.attach undefined and
  crash a consumer.
- getMetadata throws and validate-less factories now leave a warn-level
  breadcrumb (main's server previously surfaced the former as a hard
  error; the graceful fallback kept, the silence not).
- Doctor's broken diagnoses are now accurate per shape: load failure
  (with the underlying error when the registry propagates one — the
  concrete AdapterRegistry swallows loader errors today, noted on
  #435), registry-returned-nothing, and loaded-but-no-validate
  (version skew), each with honest durationMs instead of 0.
- list_supported_languages probes all languages in parallel like the
  doctor path (cold-cache cost = max, not sum).
- Parity test reuses the shared createMockEnvironment /
  createMockFileSystem helpers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants