Conversation
Adds an optional io.modelcontextprotocol.registry/security-scan extension under the existing _meta reverse-DNS namespace, holding an array of evidence-scoped, scanner-neutral security scan receipts. Resolves the converged v1 proposal in modelcontextprotocol#1273. Each receipt binds a verdict (clean | warnings | findings | inconclusive) to a specific scanner, rule_set_ref, policy_profile, and scanned_artifact_digest, with an explicit machine-readable scan_scope of what was actually evaluated. The schema enforces the client invariant named in-thread: scanned_artifact_digest is required so a clean verdict binds to exact bytes, scan_scope is a required non-empty array, and inconclusive verdicts require a machine-readable inconclusive_reason (artifact_digest_mismatch, unsupported_package_type, scope_excludes_handler_validation, evidence_unavailable, stale_scan) so a dependency or package scan that leaves handler-side validation unassessed stays representable rather than collapsing into clean. attestation (publisher-asserted | registry-attested | third-party-attested) keeps self-asserted receipts distinct from attested ones. Signatures and capability posture are intentionally out of scope for this version. scanner and rule_set_ref stay open strings so any community scanner can populate them; the registry does not endorse any particular one. Schema is defined in openapi.yaml and regenerated via make generate-schema. Documented in generic-server-json.md and official-registry-requirements.md, with a CHANGELOG entry. Additive and optional; existing server.json documents remain valid. Signed-off-by: Adam Lin <adam@agentthreatrule.org>
|
Thanks for turning the discussion into a concrete PR. This looks like the right v1 boundary to me: optional, scanner-neutral, Keeping From my side, I would support this as a small additive schema/docs change. Happy to review wording or field-shape adjustments if maintainers prefer a narrower first cut. |
|
This is the right PR to converge on from #1273. I opened #1405 before noticing this one, but closed mine to avoid splitting review. The important invariant is preserved here: One downstream client test I would add when this lands: a receipt with |
|
@HarperZ9 thanks — and for closing #1405 to keep review in one place. Agreed on the invariant: the digest binding plus a non-empty Good call on the downstream client test — a receipt with |
…iant Adds a downstream-client test for the io.modelcontextprotocol.registry/security-scan extension (modelcontextprotocol#1404) and a named render-invariant note in the format spec. The test compiles the canonical draft server.schema.json (the same file tools/validate-examples compiles) and validates _meta security-scan receipts the way a client would read them: - clean receipt with a well-formed scanned_artifact_digest and a non-empty scan_scope is accepted - mismatched/malformed scanned_artifact_digest (not algorithm:hex) is rejected, and a missing digest is rejected - empty scan_scope is rejected (minItems 1) - verdict inconclusive without inconclusive_reason is rejected, and is accepted once the machine-readable reason is present generic-server-json.md gains a Render invariant subsection making the client rule explicit: do not surface clean unless the receipt binds to the current artifact digest and the displayed claim names the covered scan_scope; an unbound digest or empty scope must be treated as inconclusive rather than clean. Signed-off-by: Adam Lin <adam@agentthreatrule.org>
golangci-lint flagged cloneReceipt as unused (the table cases build maps inline, no clone needed). Removing it; the schema test is unchanged. Signed-off-by: Adam Lin <adam@agentthreatrule.org>
|
@HarperZ9 following up when you have a moment — the security-scan receipt _meta extension is ready and I believe we're aligned on the digest-binding + non-empty scan_scope invariant. Happy to adjust anything before review. |
|
@eeee2345 looks good from my end. :) |
|
Hi maintainers — I’m building an independent downstream consumer for the experimental security-scan receipt shape in this PR: mcp-evidence-gate. I pinned a compatibility profile to the current PR head SHA (
The Action is currently referenced at immutable release SHA Could you confirm whether these are still the intended consumer-facing invariants for v1?
I’m happy to adjust the consumer or add focused fixtures based on maintainer feedback. |
|
Downstream follow-up with implemented edge cases: I added consumer-side checks in mcp-evidence-gate#1 and executable cases in dogfood#1:
Should these remain downstream consumer-policy concerns, or should any become documented receipt invariants? The verifier also treats |
|
One observation from an angle this thread has not covered, offered as data rather than as a design objection — the digest binding looks right to me and I am not asking for it to change. The digest binds bytes. Some risk moves without the bytes moving.
A receipt bound to digest How often, measured rather than asserted. We run a daily external diff of the whole registry population (25,030 entries, series from 2026-07-30). Across the events of that class so far: So roughly 45% of ownership and source changes arrive with no artifact change on the same day — a receipt with Two things this is not. It is not an argument for widening v1 — the scope discipline in this PR is the reason it is reviewable. And it is not a case for a What I think it does argue. If Happy to contribute the ownership-change series as public evidence if it is useful — it is free, needs no key, and the format is whatever is convenient. Not asking for anything in return, and not proposing our scanner as a value for |
Content/digest-scoped receipts assert nothing about who controls the name the artifact is published under; a clean receipt bound to a digest stays joinable through ownership transfer, source-repo removal/relocation, unpublish, or name republication. Custody is a distinct axis, not scanner-assessable, so it is a documented scope boundary (clients MUST NOT infer custody from a content scan) rather than a new scan_scope value or inconclusive_reason. Motivated by the registry-population ownership-change discussion on the PR; no schema change. Signed-off-by: Adam Lin <adam@agentthreatrule.org>
|
@Nikolife2016 — this is the kind of contribution the thread needed, and framing it as data rather than a design objection is what makes it land. The measurement carries it: across your daily registry diff, ~45% of the ownership/source events you tracked arrived with no artifact change the same day, so a I want to agree explicitly with the two things you ruled out, because they're the same discipline that keeps this PR reviewable: not widening v1, and no Where this lands is Yes to the offer — please share the ownership-change series; I'd like to cite it as the measured motivation for that boundary, since measured beats asserted. On whether this is a reserved scope value in v1 or prose I'll defer to whoever owns the boundary here; my lean is the docs note now, with an optional scope vocabulary reserved for a later cut so the v1 schema doesn't grow. (And +1 to @yandexuanxuan's consumer-side invariants upthread.) |
|
Series published, and one correction to the number before you cite it. The artifact: The number moved, and you should cite the current one. I quoted 44.9% upthread. Today the same computation reads: Nothing was restated — the series simply grew by four days. If a stable figure is wanted for a docs note, "roughly half" survives both readings and will survive the next one; a precise percentage in prose will be stale within the month, which is the same reason I would keep the number out of the schema. Four limits, and they are inside the file rather than in this comment, because the file is what gets cited:
Each day's dataset manifest is hashed and timestamped into Bitcoin via OpenTimestamps, so the series can be shown to have existed on a given date without trusting us. Current anchored digest is in the file's On your placement call: the docs note now, with a reserved scope vocabulary deferred, reads right to me — and I say that as someone with an obvious interest in custody becoming a first-class field, which is a reason to discount my agreement rather than to weight it. If the wording of the boundary section would be better with the measurement stated differently, say so and I will produce whatever cut is most useful — including one that makes our own coverage limits explicit, since a boundary note that overstates its evidence is worse than none. |
|
Correction to the figure I posted upthread (44.9% → 46.3%). It pools four event types, and two of them cannot be "accompanied by a version" by construction, so the pooled share overstates silent handovers. Recomputed today from the same file, split by type: The honest statements are narrower and, I think, more useful for the scope-boundary section:
|
|
The custody series is now frozen and citable: https://doi.org/10.5281/zenodo.22251006 (CC BY 4.0). It carries the event list as JSON and CSV, a stdlib recount script, the corrected per-type figures (maintainer handovers silent in 32 of 42; repository moves in 1 of 77), the signed daily manifest and the OpenTimestamps proofs for the deposit day, with the coverage limits stated in the README (deep npm subset for maintainer events, whole population for registry-field events). If the scope-boundary section ever wants a citation, that DOI is stable; the live file at |
|
One more correction to my own numbers, and then I will stop moving them. Upthread I called the same-day shares lower bounds. That was wrong: both the custody change and a release are inferred by diffing consecutive daily snapshots, so their true times are interval-censored, and a change and a release that fall on different sides of a snapshot boundary were counted as unaccompanied even if they were hours apart. External review (I. Santos-Grueiro) caught it. Same 204 events, now with a second flag that tolerates a release in an adjacent observation (±1 day): So the honest statement is "at daily observation resolution", with the ±1 column showing how much the boundary matters — and a maintainer-set change is not an established transfer of control; the layers (package authority, registry-name authority, repository provenance) are now reported separately. Dataset v2 with both flags per event and the recount script: https://doi.org/10.5281/zenodo.22268322 (concept DOI for all versions 10.5281/zenodo.22251005). The live file at |
|
Independent data point for this design, plus one concrete signal I would suggest the receipt carry. I run a small census of the registry (read-only: registry metadata + published npm documents + the referenced hook scripts fetched from the published package). Census, September 2026 (6,000 registry entries, reproduced weekly):
Report: https://github.com/ciceroyang/mcp-supply-audit/blob/main/docs/census-2026-09.md · rolling artifact: https://github.com/ciceroyang/mcp-supply-audit/releases/tag/mcp-census · scanner (MIT, zero-dep): same repo. Suggested signal: This is a property of the published artifact, not of handler-side validation, so it reads naturally as a
Two properties of that pattern are worth a scope flag rather than a bare verdict, because they change what the verdict means: (a) the expected checksum is fetched from the same origin as the artifact, so verification covers transport corruption but not a compromised release origin; (b) the base URL, repository and version are overridable through environment variables ( Why the receipt shape matters, from experience. My detector's critical tier changed three times in one day: v0 flagged any If the field lands I am happy to emit receipts in this shape from the census; the scanner already binds findings to the declared version and the fetched script bytes. |
|
Following the census comment above with one concrete failure mode for the receipt to cover, because it is the case A scanner that crashes can still produce a clean-looking report. In AgentAuditKit 0.6.5 a scanner exception becomes an The design implication I would draw: a receipt that says |
|
Follow-up with executable downstream evidence for the incomplete-scanner failure mode raised by @ciceroyang above. We implemented and promoted a fail-closed scanner-execution-completeness path across an independent Producer -> Core -> Dogfood chain, without changing the v1 Promoted immutable identities:
What the experiment now proves:
The boundary is intentional: this does not add So the downstream result is simply: absence of findings is not accepted as evidence that required scanner work completed. This is project-owned executable evidence, not a claim of Registry adoption or maintainer approval. |
|
Thanks — the executable chain is what makes this checkable rather than asserted. Two things I would take from it:
For the v1 extension I would keep the schema unchanged and ask only that a producer publishing a receipt either includes such a completion record or leaves |
|
One correction to my comment of 15 September: the scanner is AGPL-3.0, not MIT. The repository is the extract that became agentgate's collection package, and it ships under one licence. Sorry for the noise. |
Every index entry now carries a scanExecution block: which scanners were required, which completed, and whether their output was present, readable and self-consistent. A required component that failed, was skipped or produced unreadable output makes the record incomplete, and build-index enforces that rather than leaving it to convention: the verdict is incomplete however clean the findings look. The mapping is the one I said I would do in modelcontextprotocol/registry#1404. Field names inside scanner_execution - required, completed, failed, status, output_present, output_parseable, semantic_consistency - follow that thread's shape on purpose, so the two records can be diffed field by field instead of argued about in prose. Everything outside the block stays in the local style. Also in this change: a validateScanExecution reference implementation with no dependencies, a JSON Schema for other consumers, and a test that asserts the two agree on the required fields so the schema cannot drift from the code that writes the records. The spec is docs/spec/scan-execution-v1.md. 12 tests in packages/collect/test/execution.test.mjs, including that nothing required is not a pass, that an unknown status is treated as failed, and that the index refuses to call an unfinished scan clean. Full suite 536 pass / 0 fail.
|
Implemented on my side, mostly so the comparison stops being rhetorical: each evidence block in my census output is now one component in exactly this shape — The field names match this thread's on purpose, so a producer and a consumer can diff two records field by field rather than in prose. Spec and the mapping table from our block statuses: https://github.com/ciceroyang/agentgate/blob/main/docs/spec/scan-execution-v1.md |
|
Coming at this from an implementation that already consumes this shape, so a concrete gap rather than a naming opinion. We record a sibling block per index entry —
Why those earn their place: the failure we built around is a scanner that crashes or emits an unparseable report while the wrapper still produces a low-severity finding and exit code 0 — the receipt then reads like a clean scan. Two smaller things we learned that are cheap to state normatively:
Spec we implemented against (frozen; the field names were aligned to this PR's shape so the two records can be compared field by field): https://github.com/ciceroyang/agentgate/blob/main/docs/spec/scan-execution-v1.md It runs over ~2,000 registry records a day; the coverage distribution is public at https://xn--5kvo87g.com/evidence.html Happy to open a follow-up PR adding an optional execution/coverage object to the receipt if that is useful to the thread. If you would rather keep v1 minimal, the consumption rule in (1) is a one-paragraph addition to the docs. |
|
Governance evidence refresh (2026-09-21):
All four |
|
@ciceroyang thanks — the independent implementation is useful evidence, especially because it exercises the same failure mode from a separate producer/consumer path rather than only mirroring our own tests. From the downstream-consumer side, the two interoperability rules you identified match what we have learned:
Our current For interoperability, a small docs-level consumption rule covering unknown execution states would already be valuable. An optional execution/coverage object could be explored separately if the PR owner or maintainers want that additional surface. The independent implementation is also useful corroboration for the narrower claim we can actually make today: execution completeness is implementable across separate producer/consumer systems without turning Thanks for publishing the mapping and implementation. |
Resolves the converged v1 proposal in #1273. Thanks to @JinNing6 and @HarperZ9 for the design discussion in that thread; the shape here follows what the three of us converged on, scoped to the small v1 cut @HarperZ9 outlined.
What this adds
An optional io.modelcontextprotocol.registry/security-scan extension under the existing _meta reverse-DNS namespace, holding an array of evidence-scoped, scanner-neutral security scan receipts. It sits next to io.modelcontextprotocol.registry/publisher-provided and follows the same convention, rather than adding a bare top-level field.
Each receipt binds a verdict to the exact evidence that produced it: scanner, scanner_version, rule_set_ref, policy_profile, scanned_artifact_ref, scanned_artifact_digest, scan_scope, verdict, scanned_at, freshness_expires_at, evidence_ref, evidence_digest, and attestation. The point is that clean only ever means clean under this scanner version, rule set, policy profile, and artifact digest, for the listed scope, not a server-level safety property.
The client invariant @HarperZ9 named is enforced in the schema, not just prose:
attestation (publisher-asserted, registry-attested, third-party-attested) keeps a self-asserted receipt distinct from an attested one. Signatures and capability posture are intentionally out of scope for this version; the docs note capability posture as a possible future sibling under the same namespace, and attestation plus evidence_digest carry verifiability for now without pulling in key distribution.
scanner and rule_set_ref are open strings so any community scanner can populate them; the registry does not endorse any particular one.
Scope and conventions
Files
Validation
make generate-schema, make check-schema, and make validate (schema validity, sync, and all doc examples against both the JSON schema and the Go validator) pass locally. go build ./..., go vet, and go test ./internal/validators/... pass. The change touches only schema and docs, no Go code, API handlers, or database, so existing server behavior is unaffected.
Happy to adjust naming or field shapes to match registry conventions, and to split anything out if a smaller first cut is preferred. Reviews welcome async whenever it suits.