fix(mcp): reload the served snapshot when project files change - #438
Merged
Conversation
An MCP server parsed its project once at startup and served that snapshot for the lifetime of the process. Harnesses keep the server up for days while builds regenerate annotations.yml and JUnit XML underneath it, so it answered from a tree that no longer existed — and the stale answer was well-formed, indistinguishable from a real one (#437). Record which local files each parse read: the four data YAMLs and reqstool_config.yml, stamped whether or not they exist, plus every test_results glob pattern with the concrete files it matched. Absent files are tracked deliberately — an annotations.yml the build has yet to generate is the common trigger. ProjectSession.ensure_fresh() re-stats that fingerprint and rebuilds only on a mismatch; the LSP keeps rebuilding from its client's file-change notifications. The MCP tools now resolve the repository per call rather than closing over it at startup, without which reloading would be invisible to them. A reload that fails raises instead of falling back to the superseded snapshot, and the failed build re-stamps its inputs so a tree that does not parse is parsed once, not once per request. Adds a refresh tool for unconditional reloads and a snapshot field on get_status reporting built_at, tracked_files and warnings — where a test_results pattern matching no files is reported as such rather than counted as zero tests. Only local sources are watched; remote ones are version-pinned downloads. Requirements: MCP_0006, MCP_0007, MCP_0008 Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & Why
reqstool mcpparsed its project once at startup and served that snapshot for the lifetime of the process. AI harnesses keep the server spawned for days while builds regenerateannotations.ymland JUnit XML underneath it, so it answered from a tree that no longer existed — and the stale answer was well-formed, indistinguishable from a real one.Two things caused it, and both are fixed here:
ProjectSession.rebuild()already existed (the LSP uses it on file-change notifications), but calling it would have been invisible to the MCP tools, which held the old repo. Tools now resolve the repository per call via a_repo()helper.source_pathscovered only the four data YAMLs, and the JUnit XML files matched by eachtest_resultsglob were discarded after parsing — so "the pattern matched no files" was indistinguishable from "this project has no tests".Changes:
SnapshotFingerprint(common/snapshot_fingerprint.py) — stamps(exists, mtime_ns, size)for the four data YAMLs plusreqstool_config.yml, and records eachtest_resultspattern with the concrete files it matched. Files absent at parse time are stamped deliberately: anannotations.ymlthe build has yet to generate is the common staleness trigger. Paths are recorded under the real project directory, not the temp symlink tree, which is removed once parsing finishes.ProjectSession.ensure_fresh()— re-stats the fingerprint and rebuilds only on a mismatch. The LSP is untouched: it keeps rebuilding from its client'sdidChangeWatchedFilesnotifications. MCP has no such channel, which is why it checks per request.SnapshotReloadErrorrather than answering from the superseded snapshot. The failed build re-stamps the inputs it knew about, so a broken tree is parsed once, not once per request; fixing the file restores service on the next call.refreshtool — reloads unconditionally, for use right after a build.snapshotfield onget_status—built_at,reload,tracked_files,warnings. Atest_resultspattern matching no files is reported there instead of being silently counted as zero tests.Only local sources are fingerprinted; remote ones (git/maven/npm/pypi) are version-pinned downloads materialized into a temp directory, so there is nothing stable to stat.
refreshcovers them.Per-request re-parsing (the issue's first suggestion) was rejected in favour of stat-then-rebuild: a full rebuild walks the whole import chain including network fetches for remote imports, whereas the freshness check costs a few dozen
stat()s and onerglobper configured pattern. No debounce interval — a debounce window is exactly the "answered from just before the build" case this is meant to eliminate. Rationale inopenspec/changes/mcp-snapshot-freshness/design.md.Deliberately not included: the
verifytool from point 3 of the issue. It belongs with thestatus/exportredesign in #311 and must derive from the shared verdict computation (MCP_0005) rather than becoming a second opinion on completeness.New requirements
MCP_0006(freshness),MCP_0007(reload failure reporting),MCP_0008(snapshot provenance and missing-artifact reporting) with matching SVCs; OpenSpec changemcp-snapshot-freshness.Closes: #437
Author Checklist
/quality:full-pr-reviewand addressed findingsdocs/modules/ROOT/pages/mcp.adocupdated (newrefreshtool,snapshotfield, "Snapshot Freshness" section)openspec validate --all --strictpassesTest Plan
Before Deployment
Author
hatch run dev:pytest --cov=reqstool— 987 passed, 2 skipped (need GITHUB_TOKEN/GITLAB_TOKEN)hatch run dev:flake8andhatch run dev:black --checkcleanmainbyte-identical forstatusandreport --format asciidocontest_standard/ms-001,test_basic/ms-101, and../reqstool-demo— the CLI is unaffectedreqstool status local -p docs/reqstool→ 75/75 complete · PASS (new SVCs covered)refreshreloads, a project that no longer parses errors instead of answering staleReviewer
session.repoat startupensure_fresh()calls;rebuild()still driven by watched-file events)rglobper configuredtest_resultspattern runs per request