Skip to content

Add optional security-scan receipt _meta extension (v1) — resolves #1273#1404

Open
eeee2345 wants to merge 3 commits into
modelcontextprotocol:mainfrom
eeee2345:feat/security-scan-meta-extension
Open

Add optional security-scan receipt _meta extension (v1) — resolves #1273#1404
eeee2345 wants to merge 3 commits into
modelcontextprotocol:mainfrom
eeee2345:feat/security-scan-meta-extension

Conversation

@eeee2345

Copy link
Copy Markdown

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:

  • scanned_artifact_digest is required, so a verdict binds to exact bytes and a client can reject a receipt that does not join to the current package or artifact.
  • scan_scope is a required, non-empty array describing what was actually evaluated, so dependency or package coverage is machine-distinguishable from handler-side validation.
  • verdict includes inconclusive as a first-class value, and an inconclusive verdict requires a machine-readable inconclusive_reason (artifact_digest_mismatch, unsupported_package_type, scope_excludes_handler_validation, evidence_unavailable, stale_scan). So a scan that leaves handler-side validation unassessed stays representable rather than collapsing into clean.

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

  • Schema is defined in docs/reference/api/openapi.yaml and regenerated with make generate-schema, per docs/reference/server-json/CONTRIBUTING.md. server.schema.json is in sync (make check-schema passes).
  • The receipt schema uses additionalProperties: true so it can grow without breaking existing validators.
  • The change is additive and optional; existing server.json documents remain valid.
  • official-registry-requirements.md notes that the official registry preserves only the publisher-provided key today, so a publisher-asserted receipt is carried nested under publisher-provided, with an example. registry-attested and third-party-attested are reserved for when the registry supports them. This keeps the doc honest about current behavior versus the format-level shape.

Files

  • docs/reference/api/openapi.yaml: SecurityScanReceipt component plus the new _meta key
  • docs/reference/server-json/draft/server.schema.json: regenerated
  • docs/reference/server-json/generic-server-json.md: format documentation and example
  • docs/reference/server-json/official-registry-requirements.md: current preservation behavior and publishing example
  • docs/reference/server-json/CHANGELOG.md: Draft (Unreleased) entry

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.

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>
@JinNing6

Copy link
Copy Markdown

Thanks for turning the discussion into a concrete PR.

This looks like the right v1 boundary to me: optional, scanner-neutral, _meta-scoped, and evidence-bound rather than a server-level safety badge. I especially like that scanned_artifact_digest, non-empty scan_scope, attestation, and first-class inconclusive make the client behavior checkable instead of relying on scanner-specific prose.

Keeping capability_posture out of this PR also seems right. The receipt should answer “what evidence was checked?”, while posture can remain a future sibling field for “what authority does this server expose?”

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.

@HarperZ9

Copy link
Copy Markdown

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: clean stays evidence-scoped, because it has to bind to scanned_artifact_digest and a non-empty scan_scope, while inconclusive remains first-class with a machine-readable reason. The official-registry note is also useful because it keeps current preservation behavior separate from the generic format-level extension.

One downstream client test I would add when this lands: a receipt with verdict: clean and matching artifact digest can render only with the displayed scan_scope; a digest mismatch or stale receipt must degrade to inconclusive rather than a clean badge.

@eeee2345

Copy link
Copy Markdown
Author

@HarperZ9 thanks — and for closing #1405 to keep review in one place. Agreed on the invariant: the digest binding plus a non-empty scan_scope is what keeps clean from drifting into a server-level claim, and splitting the official-registry preservation note from the format-level extension was the cleaner cut.

Good call on the downstream client test — a receipt with verdict: clean and a matching scanned_artifact_digest accepted, versus a mismatched digest or empty scope rejected. I'll add that so the binding is covered by a test, not just the schema shape. And happy to tighten scan_scope to an enum if the maintainers would rather a smaller v1 surface.

eeee2345 added 2 commits June 30, 2026 02:33
…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>
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.

3 participants