experimental-ext-tool-annot.../specification/draft/trust-annotations.mdx
Sam Morrow 716eb8d89f
docs: trim spec frontmatter to known fields (title only)
Official extension specs (ext-auth) use only `title:` in frontmatter
plus an <Info> protocol-revision marker. Remove the invented
extension_identifier / status / maintainers / related / profile_of
fields and restate the identifier and profile relationship in the body
prose where they belong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-11 00:14:19 +02:00

176 lines
7.6 KiB
Text

---
title: Trust Annotations
---
<Info>**Protocol Revision**: draft</Info>
**Extension identifier:** `io.modelcontextprotocol/trust-annotations`
> ⚠️ **Experimental draft skeleton.** This document captures the agreed shape
> and the open questions. Normative text is intentionally thin pending
> reference-implementation validation. Substantive discussion happens on PRs
> against this file and on [SEP-1913](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1913).
## Abstract
This extension defines a small, stable, scheme-agnostic vocabulary for
classifying **data in transit** through MCP tool results, plus an optional
`evidenceRef` pointer that lets a deployment attach richer, out-of-band
evidence without growing the on-wire schema. It is the primary
data-classification extension in the Tool Annotations IG's trust work; other
extensions (information-flow control, action metadata) compose with it rather
than duplicating it.
The design follows the [@localden](https://github.com/localden) review steer on
SEP-1913 — take a *narrow first cut* of the taxonomy and avoid hard-to-remove
schema — while preserving the layered "small annotation on the wire, rich
evidence out-of-band" consensus that emerged in the SEP-1913 thread.
## Motivation
Data crosses tool boundaries today with no standardized markers for whether it
is sensitive or whether it originated from an untrusted source. Clients and
hosts are left to infer this from tool names or model behavior. Two coarse,
broadly-applicable signals cover the majority of client-actionable cases:
- **`sensitive`** — the content should be treated as confidential (PII,
credentials, proprietary data). Drives consent prompts and egress policy.
- **`untrusted`** — the content originated from an open-world / attacker-
influenceable source (web pages, third-party email, user-generated content).
Drives prompt-injection defenses.
Anything richer than these two booleans is deliberately **not** on the wire; it
hangs off `evidenceRef` (see below).
## Specification
### Dependencies
This extension depends only on the base MCP `_meta` mechanism. It does not
require Tool Resolution ([SEP-1862](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1862)),
though it composes with it.
### Annotation shape
Trust annotations are carried under the extension-namespaced `_meta` key on a
`CallToolResult` (and MAY appear on an individual `ContentBlock` — see
[Attachment point](#attachment-point)).
```jsonc
{
"_meta": {
"io.modelcontextprotocol/trust-annotations": {
"sensitive": true, // optional boolean
"untrusted": true, // optional boolean
"evidenceRef": { // optional pointer, see below
"type": "data-class.v1",
"digest": "sha256:…",
"canonicalization": "cbor/rfc8949",
"schema": "https://…/data-class.v1.json",
"ref": "audit://…" // optional locator
}
}
}
}
```
Both booleans are optional and default to `false`/absent. Absence MUST be
treated as "no claim made," never as "asserted false."
### The `evidenceRef` slot
`evidenceRef` is the extension point that keeps the wire schema small while
letting deployments attach arbitrarily rich evidence. Its shape (adapted from
the SEP-1913 discussion):
| Field | Required | Meaning |
| :--- | :--- | :--- |
| `type` | yes | **Open string** naming the class of the referenced record. NOT an enum. Examples: `"data-class.v1"`, `"ifc.fides.v1"`, `"sequence"`, `"policy-decision"`. |
| `digest` | yes | Hash of the referenced record, so a client holding the data can re-derive it. |
| `canonicalization` | yes | How the digest was computed (e.g. `"cbor/rfc8949"`), so the client can re-hash independently. |
| `schema` | recommended | Identifier/version of the record `ref` resolves to. |
| `ref` | optional | Locator into the deployment's audit/evidence stream. |
> **Normative intent:** `type` MUST remain an open string. Narrowing it to a
> closed enum would foreclose the capability-token, cosigning, and
> sequence-shape models raised in SEP-1913 review. A non-binding **registry**
> of well-known `type` values is maintained in this repo; unknown `type` values
> MUST be safely ignorable by a client that does not understand them (the
> `digest`/`canonicalization` pair is still a usable, bounded signal).
This single slot subsumes the previously separate `attestationChainRef` /
`policyDecisionRef` ideas — both become `type` values.
### Coarse vs. rich classification (DataClass)
SEP-1913 carried a four-level data classification
(`public` / `personal` / `confidential` / `highly_confidential`) plus a
regulatory scope (e.g. `confidential:hipaa`). **This extension deliberately
keeps only the coarse `sensitive` boolean on the wire.** Richer classification
is recovered as an `evidenceRef` profile:
```jsonc
"evidenceRef": {
"type": "data-class.v1",
// record resolves to e.g. { "class": "highly_confidential", "regulatory": ["hipaa"] }
"digest": "sha256:…",
"canonicalization": "cbor/rfc8949"
}
```
This is an explicit scope decision: the binary lives on the wire for universal
client actionability; the taxonomy lives behind a profile so it can evolve
without a breaking schema change.
### Attachment point
Annotations attach at the **`CallToolResult` level by default.** A server MAY
additionally annotate an individual `ContentBlock` when it has reason to
localize the signal (e.g. one search result among many is untrusted). When both
are present, the content-block annotation refines the result-level one for that
block; it MUST NOT *weaken* a result-level claim (union semantics — once
`true`, stays `true`).
### Lifecycle and `list_changed`
Trust annotations defined here are **response-level**: they describe a specific
tool result and are not part of the tool *definition*. They therefore do **not**
participate in `tools/list_changed`. (If a future revision attaches trust
vocabulary to tool definitions, that surface would follow `list_changed`; this
draft does not.)
### Propagation
This extension does **not** specify session-level escalation/propagation rules.
Those remain an [open question](../../docs/open-questions.md) on the SEP-1913
umbrella. A host MAY implement propagation locally; this extension only
standardizes the per-result annotation.
## Reference implementation
[`kapil8811/mcp-trust-annotations`](https://github.com/kapil8811/mcp-trust-annotations)
— a Python SDK with a `@trust_annotated` decorator, `to_wire`/`from_wire`
round-tripping, a policy engine (audit/warn/enforce), a healthcare-scenario
demo, and an LLM-based usability study (138 tests). The SDK predates this
narrowed shape and is being aligned to the two-boolean + `evidenceRef` model.
## Trust model
Enforcement does not rest on developer honesty. See
[docs/trust-model.md](../../docs/trust-model.md): registries and marketplaces
verifying annotations are the enforcement layer; the annotation is a *claim*,
and `evidenceRef` is how that claim is made checkable.
## Open questions
- Is `sensitive` the right single coarse signal, or do we need `sensitive` +
the `data-class.v1` profile from day one?
- Exact required-vs-recommended split on `evidenceRef` fields.
- Whether content-block-level annotation needs a worked multi-result example
before the draft is implementable.
## Changelog
| Date | Change |
| ---------- | ------------------------------------------------------------- |
| 2026-06-10 | Initial draft skeleton. Narrowed to `sensitive` + `untrusted` + `evidenceRef`; DataClass demoted to a profile; `requires_review` moved to `action-metadata`. |