Skip to content

Do not advertise capture tools a runtime cannot serve - #641

Merged
MongLong0214 merged 4 commits into
mainfrom
fix-f002-capture-preflight
Aug 14, 2026
Merged

Do not advertise capture tools a runtime cannot serve#641
MongLong0214 merged 4 commits into
mainfrom
fix-f002-capture-preflight

Conversation

@MongLong0214

Copy link
Copy Markdown
Owner

Summary

Closes F-002 (P0). Addresses the asset-readiness half of #633 and #635.

The MCP server advertised capture tools in tools/list while the assets those tools need were not
on disk. A user saw the capability, called it, and the call failed. Startup now checks the package
manifest, SPEC, and the schema, and a server that cannot serve capture does not offer capture.

Measured evidence this closes

Four commitlore runtimes were serving MCP on one machine at the same moment:

3 × …/.claude/plugins/cache/commitlore/commitlore/0.6.0/dist/commitlore.mjs mcp
3 × …/.local/share/commitlore/v0.8.2/dist/commitlore.mjs mcp
3 × …/.local/share/commitlore/v0.8.1/dist/commitlore.mjs mcp
1 × …/.local/share/commitlore/dev-3b70a1bebfb3/dist/commitlore.mjs mcp

Two of those roots no longer exist:

plugins/cache/commitlore/0.6.0      bundle=NO  spec=NO
share/commitlore/dev-3b70a1bebfb3   bundle=NO  spec=NO

Those processes keep answering after their installation was deleted underneath them. Reproduce with
ps -axo args= | grep -i "commitlore.*mcp".

RED, verified independently against unmodified main

The three regressions were run on f474cf43 with only the new test file added:

× does not advertise capture tools when SPEC assets are absent
  → expected [ 'commitlore_query', …(6) ] to not include 'commitlore_prepare_capture'
× turns a stale prepare_capture call into a runtime-specific repair
  → expected 'commitlore: this installation is miss…' to contain 'capture is unavailable'
× identifies itself as degraded read-only during initialize

A correction to the finding's wording

The review describes the failure as a raw ENOENT. On current main it is no longer literally
that: the base leaks the missing asset's absolute path with a generic reinstall line. The
defect is the same — the message names a path instead of the runtime and what to do about it — but
the finding's wording is one release out of date and this PR does not repeat it.

What is out of scope, deliberately

Runtime identity convergence is F-001's, and a parallel change owns it. Rather than add a second
identity mechanism, this uses the current entrypoint path and package root directly. When F-001
lands, these messages should read from the shared identity instead.

Test plan

  • 106 cases across mcp-preflight, mcp and mcp-capture
  • The same MCP suites in a linux/amd64 container
  • npx tsc --noEmit
  • Negative controls: removing tool filtering fails the listing case; removing the stale-call
    guard fails the actionable-message case; removing the startup warning fails the degraded
    diagnostic case — each restored and green
  • Canonical build and npm run artifact:verify

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 4 commits in origin/main..b51c6b407c10b99feb9a0de6947dfba51ddb6c49
Active constraints: not read — commitlore: git log --follow accepts exactly one pathspec, so renames are not followed for 10 paths; query one path at a time to follow its rename chain (10 changed paths)

Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR.

@MongLong0214

Copy link
Copy Markdown
Owner Author

Holding this — the fix does not cover the case that produced the evidence

An adversarial review raised it and I reproduced it live.

createServer decides readiness once:

const captureAssets = preflightCaptureAssets();   // src/mcp/server.ts:508
const captureReady = captureAssets.ready;

const advertisedTools = captureReady ? TOOLS : TOOLS.filter();   // computed once
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [...advertisedTools] }));

if (!captureReady && [PREPARE, VERIFY, STAGE].includes(request.params.name)) throw 

tools/list and the call gate both read that snapshot. Nothing re-checks.

Measured

Server started with assets present, spec/ deleted mid-session, tools/list called again:

SPEC deleted
capture tool counts per tools/list: [3, 3]

Three capture tools advertised before the deletion, three after.

Why this matters more than a general edge case

This is exactly the state the finding was written from. Measured on one machine:

3 × …/.claude/plugins/cache/commitlore/commitlore/0.6.0/dist/commitlore.mjs mcp   bundle=NO spec=NO
1 × …/.local/share/commitlore/dev-3b70a1bebfb3/dist/commitlore.mjs mcp            bundle=NO spec=NO

Those processes started while their installation existed and kept serving after it was deleted underneath them. A startup snapshot cannot see that, so the runtimes that produced #633 and #635 would still advertise capture and still fail on the call.

Also raised, still to check

  • test/mcp-preflight.test.ts:179 asserts the message does not contain ENOENT, a token the product never emits — that assertion cannot fail and therefore pins nothing.
  • The degraded state is reported on serverInfo.description and stderr, while this repository's own notes say hosts inject initialize.instructions. Reporting it where clients do not read is close to not reporting it.

What is not in question

The startup gating itself is right and the RED was real: on unmodified main the three regressions fail, including expected [...] to not include 'commitlore_prepare_capture'. This is a gap in coverage, not a wrong direction.

@MongLong0214

Copy link
Copy Markdown
Owner Author

The gap is closed — measured the same way it was found

The blind review's finding was that readiness was a startup snapshot, so a server kept advertising capture after its assets were deleted underneath it. Reproduced then, reproduced now against the fix, same script:

before after
capture tools per tools/list, assets present → deleted [3, 3] [3, 0]

Three capture tools advertised before the deletion and three after; now three and none.

That is the state the finding came from. On one machine, four runtimes were answering as MCP servers and two of them — 0.6.0 from the plugin cache and dev-3b70a1bebfb3 — were serving from roots with bundle=NO spec=NO. They started while installed and kept answering after deletion. A startup snapshot cannot see that; a request-time check can.

Completion condition, stronger than the review document's

The document asks for a startup preflight. That alone would have left the measurement above at [3, 3]. The condition this PR meets, approved by the owner:

  1. startup preflight, and
  2. re-check at capture call time, and
  3. tools/list reflects current state rather than a snapshot

Also addressed from the same review

  • The assertion at test/mcp-preflight.test.ts:179 checked that the message does not contain ENOENT, a token the product never emits — it could not fail. It now asserts what the message must contain.
  • Degraded state stays on initialize.instructions, which is the field hosts inject. It cannot change after initialization, so the dynamic half is carried by tools/list and by the capture-call error.

Verification

  • 109 cases across mcp-preflight, mcp and mcp-capture
  • npx tsc --noEmit
  • canonical build, manifest refreshed, npm run artifact:verifyfe3613c6
  • the live before/after above, run by hand rather than through the suite

Runtime identity remains F-001's (#644); this PR names the runtime from its own entrypoint and package root and should read from the shared identity once that lands.

Advertising a capture tool promises that the running installation has the assets needed to execute it, not merely the handler code.

Limit: MCP capture advertisement requires package manifest, SPEC, and schema to be available in the active runtime
Ruled-out: code-only tool advertisement | a stale runtime can expose capture after its SPEC is gone
Blast: module
Undo: easy
Certainty: firm
Record-Id: r-preflight002
Provenance: authored
Verified: npx tsc --noEmit; npx vitest run test/mcp-preflight.test.ts; npx vitest run test/mcp.test.ts test/mcp-capture.test.ts test/doctor.test.ts; docker linux/amd64 MCP suites (106 tests); docker linux/amd64 npm ci && npm run build && npm run artifact:manifest; npm run artifact:verify; find dist -type f | wc -l = 274
CommitLore-Version: 2.0.0
A readiness answer has a shelf life: the runtimes behind this finding outlived their installations. Recheck the required assets at the MCP request boundary so capture is withdrawn and calls receive a repair before they start work.

Limit: capture readiness can change after MCP server startup
Ruled-out: a daemon, watcher process, hosted service, or new database | a filesystem check at the boundary is enough
Blast: module
Undo: easy
Certainty: firm
Record-Id: r-recheck002
Provenance: authored
Verified: npx tsc --noEmit; npx vitest run test/mcp-preflight.test.ts test/mcp.test.ts test/mcp-capture.test.ts; Node 24 linux MCP suites with linked-worktree Git metadata mounted; docker linux/amd64 npm ci && npm run build && npm run artifact:manifest; npm ci; npm run artifact:verify; find dist -type f | wc -l = 274; live stdio spec removal [3,0]; negative control [3,3]
CommitLore-Version: 2.0.0
Provenance: authored
Record-Id: r-canonf002b
Verified: docker linux/amd64 build and artifact:manifest; artifact:verify reports fe3613c6; 274 files in dist
Rebasing onto main put this branch's own deferral to the test. It said F-001
owns runtime convergence and that reading the package version here was only
what an operator could act on until that work landed. It has landed, so the
version comes from runtimeIdentity() and the local reader is gone.

The message stays this branch's: a failure reports the fallback without the
former absolute path of a deleted installation, which the identity error text
would have carried. Convergence is F-001's; not leaking a dead path is F-002's.

Blast: module
Undo: easy
Certainty: firm
Ruled-out: keep the local packageVersion reader | it is a second answer to a question F-001 now owns, which is the divergence this pair of findings exists to remove
Provenance: authored
Record-Id: r-f002onf001
Verified: tsc --noEmit; 115 cases across mcp-preflight, mcp, mcp-capture and runtime-identity; live tools/list before and after deleting spec/ reports [3, 0]; docker linux/amd64 build and artifact:manifest; artifact:verify 33a26f61; 280 files in dist
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.

1 participant