Skip to content

Tool Pinning hashes only the description; the artifact layer moves on 74.6% of releases #482

Open

Description

@stillmarcus24

Tool Pinning hashes the description string and nothing else. hash_entity() in src/mcp_scan/models.py returns md5(entity.description), so the input schema, the command, the args, and the resolved package version all sit outside the pin. That last one is the layer we measure, and it moves constantly.

Across 7,949 multi-version servers and 59,821 release transitions in the public registry, the invocation surface a client renders at approval time — package id, transport, runtime arguments, env var names, remote host — held byte-identical while the resolved package version changed underneath. 74.6% of releases. 61.8% of servers do it on every release they ship, and the top decile of publishers accounts for 59.8%, so it is the ecosystem rather than a few noisy teams. A description hash is stable across every one of them.

Pinning the artifact instead is not the fix. At a 74.6% alarm rate, Axelsson's base-rate argument puts that trigger at 7.46% of all MCP releases needing to be attacks before it reaches 10% precision.

So we built a narrower trigger and then measured it against ourselves. Alert on capability expansion — a newly reachable host, a newly read secret, a new exec or filesystem-write site — rather than on change. Raw rate came back 22.7%. We ran a blind validation set of 90, adjudicated every signal-bearing case against source, and measured precision at 57.1%. Our own headline was inflated 1.75x. Corrected the rate is 13.0%, which is 5.7x fewer prompts and drops the required attack rate to 1.30%.

Two asks. Is the md5 choice deliberate given the adversary controls the description, and who owns pinning now that Storage.py has no counterpart under agent_scan? The contingency tables and the blind answer key are ready to hand over so your team re-derives that precision figure rather than taking it from us.

Activity

  1. stillmarcus24 commented on Sep 24, 2026

    @stillmarcus24
    Author

    The contingency tables and answer key are now published: github.com/stillmarcus24/assurance-run, capability-validation. node rederive.cjs from a clone, Node 18+, standard library, no network, no install. It recomputes the precision from the bytes rather than restating it.

    Scope of the release, stated up front. The assignment is fixed by seed 20260920 and timestamped ahead of the labels; the pack evidences that ordering, not the adjudicator's isolation. The per-case observation vectors are withheld. Package and version pairs are included, so all 90 adjudications re-run against the npm tarballs directly, which checks the labels against artifacts rather than against our records.

    One figure is restated. The 57.1% in the original issue carries a denominator rule that was not stated with it: two of the 30 flagged cases adjudicated unclear and were excluded. Counted as false positives, precision is 53.3% and the corrected alert rate is 12.1%. Both conventions print. The distinction is material for the same reason the issue is: a rule that is invisible cannot be audited.

    The md5 question is withdrawn as moot. src/mcp_scan is no longer on main and Tool Pinning was removed rather than reworked.

    The question that replaces it is narrower and current. signed_binary.py classifies npx, node, uvx and docker as code launchers, on the stated grounds that a signed launcher implies nothing about the package it executes. Launcher-based stdio servers therefore fall outside signed-binary identity, and Tool Pinning no longer sits behind them. That is the population the measurement covers. Across 7,949 multi-version servers and 59,821 transitions in the registry version-history sweep, the approval surface held byte-identical while the resolved version moved on 44,639 of them, 74.6%, partition exact.

    So the open question is what carries artifact identity for a launcher-based server between releases. If the answer is that nothing does yet, then the calibration baseline for whatever fills that gap is only observable while it is happening. StillOS sweeps the registry every six hours and holds 24 snapshots and 167,221 definition mutations since 2026-09-19, a series distinct from the figure above and not reconstructable after the fact.

    There is a commercial shape to that, and it does not belong in an issue tracker. Name a channel and we will take it there.

  2. Paraphern commented on Oct 12, 2026

    @Paraphern

    The 74.6% figure and the base-rate analysis are sharp - and they point at the right problem: pinning the wrong artifact produces an alarm rate nobody survives. One layer above the package-resolution surface this measures, though, sits the tool-contract layer, and it changes the math.

    The package-resolution layer (what npx installs) moves on 74.6% of releases. But the tool-contract layer (what the server actually serves via tools/list) is an independent signal: two deployments on the same version can see different tool surfaces (per-client user-agent divergence, feature flags, remote config), and two different versions can serve identical tool contracts (notation-only releases - schema dialect swaps, key re-ordering). In our weekly drift watch across 14 public MCP servers, the most common release type is exactly the latter: one recent vendor major fired on all 20 pinned tools, and 16 of the 20 were pure serialization churn. Under a binary artifact-pin, that's 16 false alarms; under a direction-graded tool-contract pin, it's 5 real findings with path-qualified leaves.

    The capability-expansion trigger above is the right narrowing for the package layer. For the tool-contract layer, the analogous taxonomy is: instruction text rewritten = escalate; input constraint tightened = breaks callers; loosened = old calls stay valid; serialization churn = not a finding. This absorbs the version-churn false positives the base-rate analysis identifies while catching the class the description-only pin misses: schema and annotation changes that arrive with no description change at all.

    On the md5 question: yes, the adversary controls the description - but more fundamentally, the description is one field of the contract. Pinning the complete definition (name, description, inputSchema, annotations - canonicalized, sha-256 per field and per contract) makes the adversary's job harder than any single-field hash, because changing any dimension trips the pin. We ship that layer open-source (rugsnare, Apache-2.0), with per-version drift records for 14 public MCP servers: https://github.com/Paraphern/rugsnare/tree/main/docs/servers

  3. stillmarcus24 commented on Oct 13, 2026

    @stillmarcus24
    Author

    The layer distinction is correct and I'll take it: 74.6% measures package resolution, which is a different question from what the server serves. Pinning the complete definition — name, description, inputSchema, annotations, canonicalized per field — is the right contract, and your direction taxonomy is the right grading.

    Both pins are still static. They answer "does this deployment match the approved one," not "how often does an approved surface move." That second number decides whether a pin is an alarm policy or a filing cabinet, and until now it has been theorised rather than measured.

    We have run tools/list against live remote endpoints daily since 2026-09-20. Through 2026-10-12:

    • 555.30 server-years of exposure, counted only where the endpoint enumerated at both ends of an interval. WAF, auth-gated, dead and timeout epochs contribute zero — otherwise the rate is understated.
    • 5,799 silent changes across 1,920 distinct servers. Silent = surface moved, server_version did not.
    • 10.44 events per server-year; 3.46 distinct servers affected per server-year.
    • Direction, mapped onto your taxonomy: 1,735 capability additions, 323 removals, 1,308 param-only.
    • Our prior was a Jeffreys upper bound on zero observed events, 3.45e-3. Measured incidence is 3,026x that bound.

    Coverage is 608 watched targets of 19,968 remote-only servers (3.0%); the rate is from the watched sample, not claimed for the population. It is an incidence of change, never of compromise — severity and hostile fraction are counterparty inputs.

    I checked the vendors in docs/servers against our corpus before writing this. Twelve of them publish a canonical public MCP endpoint; all twelve are under our clock, and eleven return WAF or AUTH to an anonymous prober — only context7 enumerates. So we are not duplicating you: rugsnare sees credentialed enterprise surfaces we structurally cannot, and we hold the rate on the anonymous tail you do not watch. Neither half is derivable from the other.

    Proposal: we publish our dated column keyed to your server slugs, you publish the per-release contract hash, and the combined table is the first drift rate with both halves measured. Our side is ready to post now — confirm and it goes up against your filenames.

  4. Paraphern commented on Oct 13, 2026

    @Paraphern

    Confirmed — and credit where due: you checked our docs/servers corpus against your endpoints before proposing, which is exactly the right way to establish that the two halves are non-derivable. The complementarity read matches what we see from our side.

    Three fixes agreed:

    1. Slugs: use our docs/servers filenames as the stable keys (mongodb, supabase, context7, ...) — we commit to keeping them stable and rename-free.
    2. Our half: we will publish machine-readable per-release pins (full tool contracts + per-tool hashes) under audits/drift-watch/pins/ in the rugsnare repo — starting with the current 24-server corpus baselines, and every subsequent drift-watch run appends the new release's pin alongside its dated report. Your dated column keys against those.
    3. Labelling: your column marks incidence-of-change, never compromise — agreed, and we mirror the same language on our side.

    One editorial suggestion for the combined table: our findings are direction-graded (BREAKING / LOOSENED / EXPANDED / NOTATION). Your split already maps (1,735 additions / 323 removals / 1,308 param-only) — carrying the class column through the table makes it answer "how often" and "how severe" in one view instead of two. Ready when you are.

  5. Paraphern commented on Oct 13, 2026

    @Paraphern

    Catalog is up, as promised: audits/drift-watch/pins/ — 45 snapshots at commit 3a976ab, keyed <slug>@<version>.json against the stable docs/servers/ slugs: 21 npm servers at both ends of their graded window, superpowers at both tags, vercel-skills baseline. One planned absence: bybit — the server exits at boot (remote manifest kill-switch), so there is nothing to pin; noted in the catalog README.

    Two notes for the combined table. First, the full corpus is now graded on a single rugsnare version (1.3.0) — exactly one re-grade moved a count: browserstack 22 → 24 (the recursive decomposer reaches deeper nested changes); every other row held, including both zero-drift controls. Second, netlify's row carries two numbers on purpose: 58 summed across the 24 adjacent version boundaries of its window, 9 for the direct 1.14.0→1.18.0 pair — pick whichever decomposition your column keys to and we will match it.

    Format notes: the snapshots are the exact .rugsnare/pins.json bytes per PIN-SPEC — hash semantics, canonicalization rules, drift classes — and §7 documents the opaque-record composition for receipt systems. Your column can key on the slugs today; the drift watch appends new-release pins on its 3-day cadence.

  6. stillmarcus24 commented on Oct 13, 2026

    @stillmarcus24
    Author

    Catalog verified — 45 snapshots at 3a976ab, bybit's absence documented.

    Before either of us publishes: keying on slugs does not work. Substring-matching your slugs against my dated corpus (7,962 surface events over 2,390 hosted endpoints) returns 69 hits of which 68 are hosting substrings — supabase matches 18 unrelated projects on supabase.co, vercel 39 apps on vercel.app. Exactly one is genuine: mcp.context7.com.

    Two specifics worth your time regardless of what we do jointly:

    • Your hostinger pins (2.4.0–2.11.0) are version-space-disjoint from the io.github.hostinger/hostinger-api-mcp I observe (1.61.0–1.63.4). Disjoint version spaces under one slug usually means two artifacts.
    • Your figma pins (0.7.0 → 0.13.2) track io.github.GLips/Figma-Context-MCP, not com.figma.mcp/mcp. A row labelled "figma" today describes two different servers.

    A correct join needs (registry name, version) enumerated, not inferred. That is real work and I am not able to put it in front of this one — if the combined table matters to you, say what it is for and who is paying for it, and I will scope it.

  7. Paraphern commented on Oct 13, 2026

    @Paraphern

    Verified on our side, and you are right on all three counts — slug substring matching was my naive proposal, and 68-of-69 false positives kills it. To answer your question directly: the table's purpose is the first measured drift rate across both halves — npm-credentialed and hosted-anonymous — as a published drift-watch artifact; there is no funder, it is self-funded research.

    Given that, the right split is that we carry the keying: we will enumerate (registry name, version) for our entries and publish a mapping table in the catalog (slug → registry id where one exists), so your side joins on exact keys it already holds. The two findings get fixed in the mapping, not by renaming slugs (we promised those stable): figma → io.github.GLips/Figma-Context-MCP (verified against the npm repository field — community server, explicitly not com.figma.mcp/mcp), hostinger → npm artifact (line at 2.13.0), explicitly disjoint from the registry-hosted line (0.1.x → 1.x). If that makes the join zero-work on your side, the mapping lands with the next drift-watch run; if it still competes with your priorities, the catalog stands alone and the offer stays open.

  8. Paraphern commented on Oct 18, 2026

    @Paraphern

    The registry mapping is complete: registry-mapping.json - 11 of 22 corpus servers mapped to MCP Registry IDs, 11 null (not in the registry, no official entry, or the registry entry is a different artifact). All 22 searched. Sentry is confirmed same-artifact (the registry lists our npm package @sentry/mcp-server); apify is null because the registry entry (com.apify/apify-mcp-server, versions 0.10-0.15) is a different package from our corpus (@apify/actors-mcp-server, versions 0.16-0.18).

    The figma and hostinger identity notes are in their pages. Pin snapshots are keyed by slug and version - the 11 mapped servers carry registry IDs; the join is your dated column against those IDs, our per-release hashes from the pin snapshots.

    One caveat on overlap: our corpus is npm-local, yours is hosted endpoints. Of the 11 mapped, only context7 and sentry have confirmed hosted endpoints that match servers in our corpus. The figma entry in our mapping is the community server (GLips, npm-local); the hosted figma endpoint (com.figma.mcp/mcp) is the official server, which is not in our corpus. The overlap is likely narrower than the mapping suggests - worth checking against your endpoint list before scoping the join.

  9. stillmarcus24 commented on Oct 21, 2026

    @stillmarcus24
    Author

    We ran the endpoint-list check you suggested before scoping anything, and it confirms what you flagged — harder than the mapping implies. Seven of the 22 slugs appear in our hosted corpus at an official vendor apex, but only context7 enumerates for an anonymous prober. sentry, figma, hostinger and notion answer with a WAF; apify and azure-devops want auth. The live, both-sides-visible overlap is a single server — the same n=1 we reported on the 13th, and completing the mapping didn't move it.

    We don't read that as sinking the table — it's the result. Your eleven are npm-local artifacts; the hosted endpoints wearing those names are mostly walled to anyone without credentials, which is the half rugsnare sees and our clock structurally can't. context7 is the one row that proves the columns join; the other ten are the evidence that neither side reconstructs the other — a sharper claim than an intersection.

    If that framing holds for you, we'll publish our dated column keyed to your registry IDs: context7 as the worked join, the walled tail labelled plainly as yours-to-see, ours-to-miss.

  10. Paraphern commented on Oct 22, 2026

    @Paraphern

    That framing holds — and your own phrasing is the right headline for the table: neither side reconstructs the other is a sharper claim than an intersection, and the other ten rows prove it better than a wider overlap would.

    To make the join concrete, our side already exists in the shape you proposed:

    • registry-mapping.json — all 22 slugs searched, 11 mapped to Registry IDs, 11 null with the reason on each row (not-in-registry / no official entry / different artifact, as with apify). Join key is the Registry ID, as you proposed.
    • Pin snapshots keyed by slug + version, so your dated column can carry our per-release hashes on every mapped row, not just context7. Your column is clock-indexed; ours is version-indexed — the join needs both axes, and neither of us has the other's.
    • Per-server drift summaries in the same keying, ready to merge. One row worth annotating beyond context7: sentry is a both-miss — WAF-walled for your prober, and in our could-not-scan list (startup exceeds the scan timeout), so neither method produces data for it. A third label for rows both methods are structurally blind on is worth having: "both miss here" is exactly the profile a joint artifact should name.

    One data point for the worked join: the context7 window we scanned (4.0.4 → 4.1.3) is zero-findings. The single both-visible server is currently clean — which makes the table honest rather than dramatic: the methods disagree on coverage, not on verdicts.

    Publish your dated column whenever ready — we'll mirror it with the mapped eleven and the null-eleven reasons as a standalone table either of us can link.

  11. stillmarcus24 commented on Oct 23, 2026

    @stillmarcus24
    Author

    @Paraphern — our column is published and keyed to your Registry IDs: PR #3 · RECONCILIATION-RUGSNARE.md · artifact rugsnare-2026-10-22.json.

    Zero-dependency reconciler so no figure below has to be taken on trust: node tools/reconcile_rugsnare.mjs selftest — 24 known-answer checks.

    The result is a negative one, and it should be published as one: zero silent changes. We have no silent drift to contribute on your corpus.

    count
    your corpus 22 slugs
    mapped to a registry id 11
    present in our census 11
    declaring any remote endpoint 5
    LIVE to an anonymous prober 1
    surface events on it 2
    silent 0

    Both context7 events are the same 4.2.0 → 4.3.0 transition, both silent: false. Your per-release pin already catches them, so the combined table has one row and nothing on it is news to your column.

    Per mapped slug, so the walled tail is explicit rather than collapsed:

    slug our state endpoint
    context7 LIVE · 2 events, 0 silent mcp.context7.com/mcp
    hostinger WAF mcp.hostinger.com
    sentry WAF mcp.sentry.dev/mcp
    currents TOOLS_DENIED gateway.pipeworx.io/currents/mcp
    browserstack NOT_UNDER_CLOCK mcp.browserstack.com/mcp
    chrome-devtools mongodb figma sap-fiori ui5 firebase NO_DECLARED_ENDPOINT —

    Your sentry both-miss annotation holds and the artifact carries it — WAF-walled to our prober, on your could-not-scan list. Zero exposure is not zero drift, and collapsing those understates the rate in both columns.

    Two classes in our column are deliberately not verdicts, and the selftest asserts the definitions so they cannot be read as ones:

    • NOT_UNDER_CLOCK — declared in the registry, not in our probe set at that time. Absence of a verdict, not a verdict of absence.
    • NO_DECLARED_ENDPOINT — the registry record declares no remote endpoint at all, so there is nothing for an anonymous prober to reach. A property of the record, not a finding about the vendor. Six of your eleven mapped slugs.

    That last number is the real reason the overlap is one row. It isn't that either scanner is weak — it's that six of eleven official entries publish no hosted surface, so an anonymous-vantage drift rate over this corpus is a slice of unknown size. Your framing was right and the measurement makes it sharper: neither side reconstructs the other, and the eleven non-joining rows carry that better than a wider intersection would.

    One trap worth passing on, since your join is slug-keyed and ours is URL-keyed: we match endpoint URLs exactly, never by substring. A substring search for sentry in our event series returns regsentry.com and sdsentry.0xpi.com — neither is Sentry. That would have produced two fabricated Sentry rows that looked exactly like corroboration. There's a selftest case asserting no lookalike URL survives into the artifact.

    Our column is clock-indexed and dated — measured_at_utc: 2026-10-22T19:40:08.749Z, census snapshot census-2026-10-22T19-10-03-335Z.json, is_current_state: false. Yours is version-indexed. The join needs both axes, as you said, and neither of us can produce the other's.

    Ready to merge your pin snapshots in on the same keying whenever you want the combined table — and if your corpus grows a hosted tier, the join is already written.

  12. stillmarcus24 commented on Oct 23, 2026

    @stillmarcus24
    Author

    Correction to my previous comment, which was wrong.

    I reported "zero silent changes" and "we have no silent drift to contribute on your corpus." The facts in it were right and the conclusion was not. I joined your corpus on our hosted axis — endpoint URL → wall-clock observation — found 1 of 22 endpoints reachable to an anonymous prober, and stopped there. Your corpus is npm packages, pinned per release. We have a package-keyed series with 154,142 rows that I never joined.

    Rebuilt on both axes: PR #3 · RECONCILIATION-RUGSNARE.md · artifact now schema: stillos.reconciliation.v2 and names what it supersedes and why.

    On your axis: 9 of 22 packages covered, 65 version-pair rows, 17 capability changes, 8 of them introducing a new network host.

    Two of those windows are gaps you named in this thread:

    • @supabase/mcp-server-supabase — your 10-17 run records "scan failed: 0.8.3 entry path differs from 0.14.0; manual fix queued." We hold 0.12.0 → 0.13.0, which introduces 127.0.0.1, api.supabase.green, supabase.green with exec_delta=8, net_delta=15. Plus 0.13.0 → 0.14.0. That is the window your scanner couldn't reach.
    • @sentry/mcp-server — listed under "no new releases." We hold 0.39.0 → 0.40.0, exec_delta=2.

    @browserstack/mcp-server is the densest server in the set — 9 capability changes across 11 changed windows (the other 2 are HASH_ONLY), 6 of them adding hosts. The ones worth your attention are the beta→GA crossings:

    window new hosts
    1.5.0-beta.22 → 1.5.0 ai-platform-service.bsstag.com, auth-preprod.bsstag.com, auth.browserstack.com, …
    1.5.1 → 2.0.0 staging.example.com
    2.3.0-beta.2 → 2.3.0-beta.3 www.example.com

    Staging and preprod infrastructure appearing in a GA release is the kind of thing a per-release pin catches and a wall-clock prober never will. Also chrome-devtools-mcp 1.11.0 → 1.12.0 adds internal.corp alongside agenticresourcediscovery.org and raw.githubusercontent.com, with exec_delta=7, net_delta=44.

    What I am not claiming, both classified in the artifact rather than folded into the 17:

    • 1 BUNDLE_NOISE — @ui5/mcp-server 0.3.0 → 0.3.1 reports net_delta -18391, exec -486, fs -198 and drops 289 "hosts" including -asdf.com, ..., .com, 0.0.0.127. A patch release does not remove eighteen thousand network references; a dependency was un-vendored. At that magnitude our instrument cannot separate un-vendoring from a real capability change, so it is not counted as one. Worth flagging for your own diffs — my first detector used a junk-ratio threshold and scored this as clean, because most of those 289 strings are plausible hostnames. The threshold has to be on magnitude.
    • 4 HASH_ONLY — content hash moved, no tracked category did. Reported as themselves.

    The hosted-axis numbers from my previous comment all still hold and are kept in the artifact, labelled as not matching your vantage: 5 of 22 declare any remote endpoint, 2 surface events, 0 silent, and sentry is still a both-miss on that axis. Six mapped slugs declare no remote endpoint at all. None of that was the problem — reading it as the answer to your question was.

    Since the cause was reusing the axis from a different reconciliation instead of asking which axis you measure, it is now a build gate rather than a note to self: tools/axis_coverage.mjs requires every artifact to declare each axis with its series files, join key, index, and whether it matches the counterparty's vantage; it fails the build when none matches, and fails it when the write-up asserts a negative result the matching axis contradicts. Run against the artifact behind my previous comment it exits non-zero. It caught the stale write-up on its first run.

    node tools/reconcile_rugsnare.mjs selftest — 29 known-answer checks, zero deps. Exact matching throughout: a substring pass had put @ryancardin/noaa-tides-currents-mcp-server under currents and @bitbybit-dev/mcp, a CAD library, under bybit. Same trap I flagged to you on the URL side, and I walked into it on the package side an hour later.

    Ready to merge your pin snapshots against these windows whenever you want the combined table.

  13. Paraphern commented on Oct 24, 2026

    @Paraphern

    The correction is the process working — the axis gate exists because of v1, and the rebuilt column is the one worth mirroring. Here is our side of the mirror: your column against our keys and our published pin corpus, all 22 rows. Our numbers are from the published drift-watch runs (2026-10-12 baseline, 2026-10-17, weekly 2026-10-19; a 2026-10-24 re-run extends several windows — notably azure→119 @2.11.0, hostinger→40 @3.8.1, browserstack→30 @2.2.7 — and lands with the next weekly report).

    Mapped 11

    slug registry id (our key) our findings (window) your package axis your hosted state
    chrome-devtools io.github.ChromeDevTools/chrome-devtools-mcp 32 (1.8.0→1.11.0) 4 rows: 2 capability (incl. 1.11.0→1.12.0 +internal.corp, net_delta 44) + 1 hash-only NO_DECLARED_ENDPOINT
    browserstack io.github.browserstack/mcp-server 24 (1.4.1→2.2.3) 20 rows: 9 capability (staging/preprod hosts in GA) + 2 hash-only NOT_UNDER_CLOCK
    context7 io.github.upstash/context7 1 (4.0.4→4.4.4; audit window 4.0.4→4.1.3 was zero) 8 rows: 1 capability (4.1.3→4.2.0, +MCP_AUTH_ENFORCEMENT, +MCP_OAUTH_TOKEN_VALIDATION) LIVE · 2 events, 0 silent
    currents io.github.pipeworx-io/currents 51 (2.3.3→2.7.0) not in series TOOLS_DENIED
    hostinger io.github.hostinger/hostinger-api-mcp 39 (2.4.0→3.3.1) not in series WAF
    sentry io.github.getsentry/sentry-mcp 0 (0.42.0 clean; audit: could-not-scan — startup timeout) 3 rows: 1 capability (0.39.0→0.40.0, exec_delta 2) WAF
    sap-fiori io.github.SAP/fiori-mcp-server 4 (1.13.0→1.15.13) 17 rows: 0 capability + 1 hash-only NO_DECLARED_ENDPOINT
    ui5 io.github.UI5/mcp-server 0 (control) 4 rows: 2 capability + 1 bundle-noise (0.3.0→0.3.1, un-vendored dep) NO_DECLARED_ENDPOINT
    mongodb io.github.mongodb-js/mongodb-mcp-server 20 (2.1.2→3.1.0) 6 rows: 0 capability NO_DECLARED_ENDPOINT
    figma io.github.GLips/Figma-Context-MCP 2 (0.7.0→0.13.2, notation-only across 17 releases) 1 row: 0 changes NO_DECLARED_ENDPOINT
    firebase io.github.firebase/firebase-mcp 0 (control) not in series NO_DECLARED_ENDPOINT

    Null 11 (unjoinable by registry id — reason per row)

    slug null reason (our key file) our findings (window) your package axis
    azure-devops not found in registry search 110 (2.5.0→2.10.0; 75 tools GONE, 29 NEW) not in series
    supabase no official entry could-not-scan (0.8.3 entry path ≠ 0.14.0) 2 rows: 2 capability — 0.12.0→0.13.0 +127.0.0.1, api.supabase.green, supabase.green; 0.13.0→0.14.0
    hubspot no official entry 6 (0.2.0→0.4.0; npm ships releases while the GitHub repo is dead) not in series
    notion no official @notionhq entry 1 (2.4.1→2.5.2) not in series
    heroku no official entry 1 (1.2.5→1.2.12; the control lost its zero) not in series
    apify different artifact (actors-mcp ≠ apify-mcp) 1 (0.16.0→0.18.4) not in series
    launchdarkly uncertain provenance entry 0 (0.7.0 clean) not in series
    netlify hosted namespace only 9 (1.14.0→1.19.0) not in series
    bybit not in registry boot-gated (remote kill-switch at boot; never scanned, history-only) not in series
    shopify no official entry 6 (1.14.4→1.17.0) not in series
    transcend not in registry 2 (2.1.2→2.1.13; BREAKING on both assessment tools) not in series

    Four rows do the real work in this join: context7 4.1.3→4.2.0 (your series extends past our clean audit window and catches two auth env vars); supabase (the exact window our scanner couldn't reach is the window your series holds, three new hosts incl. a loopback — ours-to-miss, theirs-to-see); sentry (hosted both-miss stands; package axes complement: our weekly 0.42.0 clean vs your 0.39.0→0.40.0 exec_delta 2); azure-devops (our largest row has no registry id at all — registry absence correlates with nothing).

    Two process notes: your v2 artifact rows carry registry_id: null — the join key lives on our side (registry-mapping.json); we'll send a PR inlining the ids if wanted. And per your standing offer: pin snapshots keyed slug+version merge against your windows — the combined table lands in audits/drift-watch/ on our next push, same keying, linkable from either side.

  14. stillmarcus24 commented on Oct 28, 2026

    @stillmarcus24
    Author

    @Paraphern — yes to both, and the join is already keyed for it.

    1. Registry ids. Please send the PR inlining them. Our rows carry registry_id: null because the mapping is yours and we were not going to guess it; having it inline on both sides makes the artifact independently joinable without either repo being the authority on the other's key.

    2. Pin snapshots. Merge them against our windows, slug+version, into audits/drift-watch/. We will link the combined table from RECONCILIATION-RUGSNARE.md rather than reproduce it, so there is one table and one owner of it.

    On the four rows that do the work: azure-devops is the one we would most like your keying on. 110 findings with 75 tools gone and 29 new, and no registry id at all, is the strongest single piece of evidence in this thread that registry presence correlates with nothing — and it is entirely on your side. We have no series for it.


    New measurement on the axis this issue is actually about.

    The title of this issue is that pinning hashes only the description. We have now measured the layer underneath that, and it is the one neither of our pins can see.

    Our hosted series emits a PARAMS_CHANGED event when a tool keeps its name and its declared server version, and its input schema parameter set changes. Window 2026-09-21 → 2026-10-28:

    PARAMS_CHANGED events 3,185 over 1,159 servers
    of those, server version unmoved on both sides 2,350 (73.8%)
    …carrying a real version string on both sides, not null 2,324 (73.0% floor)
    parameters gained / lost +10,011 / −1,285 (7.8:1)
    durable param slots / flapping 9,618 / 380 (3.8%)
    top-10 servers' share of events 7.1% (top-20: 11.5%)

    Why this is distinct from both of our existing columns: a per-release artifact pin detects that bytes moved, and a tool-list diff detects that names moved. Arguments changing under a held tool name and a held version produce no event in either. There is no package, no artifact, no version delta, and no registry stores historical input schemas — so it is not back-fillable by anyone who was not recording it.

    The class we think is worth your attention is not the one we went looking for:

    • Credential arrival — 227 distinct (server, tool, param), 174 of them silent (76.7%). A tool that previously took no secret now accepts one. Most frequent: mpp_credential (39), api_key (31), apiKey (21), guest_pass (15), _session_id (13), account_token (9).
    • Caller-controlled destination — only 12 events, 8 silent. Real but small; we would not headline it. Live re-verified today: marx.finance register_agent gained webhookUrl + webhookHeaders; licium.ai bounty_post gained funding_webhook_url; quintadb (.com/.ru/.com.ua) update_action_rule gained run_webhook.
    • Safety-gate removal — 25 events, only 6 silent. The largest block is honest: mcp.rationalbloks.com dropped confirm_destructive from four deploy_* tools and dry_run from three *_schema tools with a version bump, which is your pin working.

    What cuts against it, stated up front:

    • 19.2% (611 events) are unattributable. We only began persisting parameter names per tool on 2026-10-03. Those events record that a parameter set moved and not which parameter. They are reported separately and are not counted as clean.
    • One false-positive class we walked into. justicelibre.org's delete_account_item read as a silently removed confirmation gate. A live re-probe shows the tool now takes ["what","id","confirm"] — it renamed confirmation → confirm. A parameter lost and a near-identical one gained in the same delta is a rename, not a removal. Population-wide that is 18 of 1,285 losses (1.4%) — small, but it landed on one of our four headline rows, because the shortlist is short. It is now a guard in the engine rather than a note.
    • Sense disambiguation is load-bearing here, same trap we traded notes on for slugs and URLs. A substring matcher returned 34 destination rows; 22 were flight searches (origin, destination_iata, origin_lat). author tokenizes to auth. A bare key is a sort or cache key far more often than a credential. Every one of those is an asserted self-test case, not a convention.

    Engine and self-test, zero dependencies, so nothing above has to be taken on trust:

    node core/mcp_param_drift.cjs            # the table
    node core/mcp_param_drift.cjs --classes  # every attributed row, with its URL and tool
    node core/mcp_param_drift.cjs --test     # 40 known-answer checks
    

    If this axis is useful to you, we will publish the series the same way as the last column — dated, keyed to URL, with the unattributable window labelled — and it joins your corpus wherever a slug declares a hosted endpoint. On your 22 rows that is a small intersection, for the same reason as last time: six of eleven mapped slugs declare no remote surface at all. The value is likely the reverse direction, your pin corpus against our argument-level events on servers that have both.

  15. Paraphern commented on Oct 28, 2026

    @Paraphern

    Yes to the axis - and your headline class is the right one to headline. Credential arrival is the shape our schema-diff grades when a version moves (a new param accepting a secret is a graded finding on our side); what your series sees that nothing of ours can is the 73.8% where the version never moved - args changed under a held name and a held version, no package, no artifact delta, not back-fillable by anyone who wasn't recording. That's a genuinely new column, not an overlap.

    Two of your honest cuts map to scars we share: the rename guard (a param lost + near-identical one gained is a rename, not a removal - our grader learned the same lesson on spec dialects, where a keyword swap graded as loosened until we taught it notation) and sense disambiguation being load-bearing (your destination -> 22 flight searches is our sentry -> regsentry.com trade from the first reconciliation).

    On the exchange: publish the series with your stated discipline (dated, URL-keyed, unattributable window labelled) and we'll consume it against the pin corpus on the servers that have both surfaces - agreed that the reverse direction is where the value is, for the six-of-eleven reason you named. Registry-ids PR is up: stillmarcus24/mcp-census-server#4 (11 rows inlined from our registry-mapping.json, null reasons untouched, provenance block added; based on the #3 head since the artifact lives there until merge). The merged table is published as the canonical single-owner view: audits/drift-watch/census-merged.md - link away.

  16. stillmarcus24 commented on Oct 31, 2026

    @stillmarcus24
    Author

    Still OS Digital Holdings LLC publishes the MCP Parameter Drift Series v1, window 2026-09-21 to 2026-10-31: 3,433 parameter-set changes across 1,218 live MCP servers, recorded from our own observations because no registry, archive or mirror stores historical input schemas. It is not back-fillable by anyone who was not recording, and it ships with its control.

    @Paraphern the three disciplines you asked for are in it, and the control cuts our own headline, so take the number below rather than the one we would have posted last week.

    audits/param-drift/2026-10-31/, keyed by server URL, one row per event. 2,524 of 3,433 (73.5%) carry a declared version unmoved on both sides. 2,498 of those carry a real version string on both sides, a 72.8% floor; 26 are vacuous. 611 events (17.8%) are labeled UNATTRIBUTABLE in their own file. They predate the per-tool parameter field our clock added 2026-10-03 and are not folded into the clean cut.

    The control is the part worth reading. Silent share by event kind on the same population: DESCRIPTION_CHANGED 93.4% of 29,175, SURFACE_CHANGED 78.7% of 6,428, PARAMS_CHANGED 73.5% of 3,433. Parameters are the least silent of the three, so the raw share is a statement about version hygiene across these servers and not about parameters. Anyone quoting our 73.5% as a parameter finding, us included, is borrowing the population baseline.

    What survives is narrower and harder to wave off. Restricted to the 1,106 servers that moved a real version at least once in the window, servers that demonstrably can signal: 717 of 1,626 param changes, 44.1%, still silent. Leave-one-out, recomputed rather than reported as a share: dropping the busiest server gives 74.0%, dropping the top ten gives 74.2%, against top-10 and top-20 shares of 7.2% and 11.6%. It is the population.

    Then we asked the servers instead of our log. 14 silent credential and destination rows re-probed live today: 13 confirmed the parameter is still on that tool, 1 unreachable, 0 absent. Reported because it can fail. The rename defect we fixed earlier in this thread was caught by a re-probe and not by our code, so a green run here is the only reason the class rows should be trusted at all.

    For consumption, CREDENTIAL_ARRIVED is the column: 276 events, 186 (67.4%) silent, 276 distinct (server, tool, param). Your grader already grades credential arrival when a version moves; these are the ones where it never did. DESTINATION_ARRIVED is 27 events, 15 silent, and SAFETY_REMOVED 26 events, 7 silent. Real, small, and we are not headlining either.

    Data in that directory is CC BY 4.0; the code stays MIT. Cite it as: MCP Parameter Drift Series v1, Still OS Digital Holdings LLC, window 2026-09-21/2026-10-31.

    Disclosure: assisted by AI. The runs above were executed on our own corpus before this was written and I am responsible for what is claimed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions