Plain version first: a version number is a promise that nothing a client depends on changed without you saying so. A lock file that checks the promise only protects you from today on. Every version you already published is still on npm, still installable, and nobody has checked it.
I maintain seven MCP servers. After adding a surface lock to all of them (the tools, instructions and lists a client sees, plus which methods answer without a token, hashed next to the version), I wrote a replay: for every version ever published on npm, install it in a temporary directory with --ignore-scripts, start it over stdio, capture its surface with the same normalisation as the lock, and compare each version with the one before.
142 versions. The live servers all matched the versions they report. Three things did not:
- Two old releases of my Senate server, 1.1.0 and 1.1.2, crash on startup:
Dynamic require of "events" is not supported. The esbuild bundle was ESM with a CommonJSrequireleft inside. Nobody installs a two-versions-old release on purpose, so nobody noticed, and npm would have offered both forever. I deprecated them. - A minor release of my medical terminology server removed six tools from the default surface (they moved behind a licensing flag). My own CHANGELOG called it "technically breaking". The client calling
snomed_searchyesterday just gets "unknown tool". - The replay itself was wrong once: it flagged a
0.5.0 → 0.6.0break as outside a major, but in0.xsemver allows breaking in a minor. I fixed the rule. A tool that reports drift has to earn its alarms.
The step I'd most like others to copy: prove your probe can say "no" before trusting its "yes". One of my test doubles answered result to every method, and the lock faithfully recorded a prompts/list that doesn't exist.
Reproduce this
Prerequisites: an MCP server published on npm with at least a few versions, runnable over stdio; Node.js 22+. Optional: a live remote endpoint with a status route that reports the deployed version. Your agent will need from you: the npm package name; the live endpoint URL if there is one. Steps:
- Install the replay tool:
npm i -D @sbissoli/mcp-surface(https://www.npmjs.com/package/@sbissoli/mcp-surface, source https://github.com/SidneyBissoli/mcp-br-commons/tree/main/packages/mcp-surface; CLI verbs are in Portuguese:travar= lock,verificar= verify). - Run
npx mcp-surface replay --url https://<host>/mcpfrom the server's repo (omit--urlif there is no live endpoint). It installs every published version with--ignore-scripts, starts each over stdio and captures its surface. - Read the output in
baselines/(a Markdown table and a JSON file, written in Portuguese like the CLI): versions that fail to start; removals that came without a major bump (in0.x, without a minor bump); whether the live endpoint matches the surface of the version it reports. - For each version that doesn't start,
npm deprecate <pkg>@<version> "<reason>". For each removal outside a major, decide on purpose: note the reason in the CHANGELOG, or restore the tool. - Before writing any lock from a test double, run two sanity probes: one method that must answer, and a made-up method (for example
mcp-surface/metodo-que-nao-existe) that must NOT. If the made-up one gets aresult, the double answers everything; fix it first. - Commit
baselines/so the next replay is compared with this one.
Verify: the replay lists every published version with whether it started and its surface hash; npm view <pkg>@<version> deprecated prints your reason for each version you deprecated; the made-up method probe returns an error, not a result.