SEP-3140: Signed Capability Declarations & Trustworthy Trust Labels - #3140
Conversation
|
Note: AI Policy disclosure |
|
Hi @pcarleton and @localden , requesting you to review the SEP and sponsor the changes. |
|
I built a private, vendor-neutral fixture against the independently verified proposal blob for PR 3140 at head The normative checks exercise declaration negotiation and unsigned policy; RFC 8785 plus SHA-256/base64url content hashing; string per-name versions; complete signed manifests; detached JWS/JWKS verification; canonical server binding; time/nonce checks; RFC 9728 publisher discovery with deterministic HTTPS/private/link-local/loopback/DNS-pinning SSRF checks; restrictive unknown trust values; and material-change re-verification and re-gating. The 35/35 focused checks pass on Homebrew Node 24. Three consistency checks that recompute Concrete ambiguities remain: monotonic ordering for string versions; delete/re-add semantics; exact detached-JWS protected headers and signing input; canonical URI equivalence; title/icon changes that alter the complete hash but remain non-material; redirect/proxy/multi-address DNS pinning and publisher-key rotation; clock skew and nonce replay; and missed, duplicated, reordered, or racing Optional secure-by-default authentication at lines 228–238 was not implemented and is documented as an exclusion. The local dependency-free JCS implementation is a fixture limitation relative to the proposal’s reference-implementation recommendation; it is tested against a published RFC 8785 vector but is not a maintained third-party JCS library. If this is useful, I’m happy to share minimized test vectors from the fixture, compare behavior with other implementations, and help turn the remaining ambiguities into independently reproducible candidate conformance cases. AI assistance disclosure: OpenAI Codex assisted with the fixture, tests, analysis, and drafting this comment. I reviewed the fixture, test evidence, analysis, and draft; I understand the claims above and am responsible for all conclusions and any publication. |
|
Hi @localden @pcarleton , I have implemented a PoC for the SEP proposal. Steps to run the PoC:
|
|
Strong direction. Binding signed manifests to One gap I do not see addressed yet: signatures and content hashes attest to what was declared at signing time. They do not, by themselves, constrain what the implementation does afterward. In static audits of MCP servers, most findings are manifest/capability mismatches — undeclared network, write, subprocess, or env reach — where the declared surface stays plausible while code drifts. That failure mode leaves signatures intact; it is not a “bad signature” event. If For context: I recently started weekly static rescans of public MCP server implementations against their declared tool surfaces. I have two observation points so far — 2026-08-02 and 2026-08-03 — so I do not yet have time-series drift measurements to cite. The rescans are specifically to accumulate that series; I will report back if the data bears on whether signed declarations need a non-signature drift channel. Question: does SEP-3140 intend to cover “declaration unchanged, behavior changed” paths, or is that explicitly out of scope for this SEP? |
|
@Tetsurohhori yes, this SEP covers "declaration unchanged, behaviour changed". I've added a working demo showing that a tool doesn't just claim what it can do, it actually gets held to it. The signed capability list is enforceable, so any surprise network, file, or secret access can be detected, blocked and logged. |
|
@omkarparth — thank you for answering the scope question directly, and for shipping something runnable against it. I have data that bears on the enforcement side, from the opposite direction. Over the past week I hand-verified 12 static findings of exactly the class this SEP targets — declared MCP tool surface versus observed code — by fetching each file at its pinned commit and reading the call site. One of the twelve survived. The other eleven were the tool doing the thing it exists to do:
That last one is the case I would most want your demo to have an answer for. The code needed name resolution in order to implement a network restriction. An enforcement layer that blocks "surprise network access" has to permit the call that builds the block list, and the only thing distinguishing it from an exfiltration path is intent. The one that survived is different in kind, and it is public if you want a fixture: So my read is that the difficulty is not enforcement, it is declaration granularity. A capability list precise enough that the filesystem server's ordinary work is not a violation is close to I also have one external label on it. On None of this argues against signed capability declarations. It argues that the signature and the enforcement are the tractable halves, and that whoever writes the declaration is carrying the part that decides whether the mechanism is useful or just noisy. AI-assistance disclosure: the static tooling, the analysis, and this comment were assisted by AI (Claude, Cursor). I read each of the twelve call sites at the pinned commit myself and remain responsible for what is claimed here. |
This SEP proposes an additive mechanism for MCP servers to make their declared capabilities — the
tools/list,prompts/list, andresources/listoutputs a client feeds to a model — authenticatable, integrity-protected, versioned, and labeled with a trustworthy, standardized risk vocabulary.It introduces (1) a content hash + version on each declaration, (2) a signed capability manifest (JWS) bound to a discoverable publisher identity, (3) a standardized, signature-covered
trustlabel block (effect / egress / data-sensitivity / reversibility), and (4) change semantics fornotifications/*/list_changedthat let a client detect and re-gate material changes ("rug pulls").A secondary, optional section proposes a secure-by-default posture for authentication.
The goal is to convert the spec's current unactionable guidance — "clients MUST consider tool annotations untrusted unless they come from a trusted server" — into something any MCP client can actually verify and enforce. The gap is inherent to the protocol and affects every client — interactive / human-in-the-loop and autonomous alike; autonomy makes it acute, but a human approver cannot verify authenticity or detect a silent change either.
Motivation and Context
The gap
MCP tool declarations are model-facing instructions in all but name: a model reads a tool's
description,inputSchema, andannotationsto decide when and how to invoke it. Yet the protocol delivers these with no authenticity, no integrity, and no trustworthy risk labeling. The specification acknowledges the danger but delegates it without providing a mechanism:There is no protocol mechanism to establish that a server is "trusted," to verify that a declaration is authentic, or to detect that it changed. The
MUSTis therefore unsatisfiable: the implementer has been assigned a trust decision the protocol renders unverifiable.Two concrete attack classes follow directly:
description; the model treats them as instructions (indirect prompt injection).notifications/tools/list_changedand silently swaps in malicious definitions. There is no version, hash, or re-consent contract to catch this.Why this belongs in the protocol, not the implementer
The natural objection is "let the client/host handle it." That fails for this specific class:
annotationscannot back a security decision. They are attacker-controlled and explicitly untrusted, so a host cannot use them to gate anything (reliance on them is CWE-807).In short: provenance and trustworthy labeling of what a server declares is the one security property that neither the host nor OAuth can synthesize on their own — it requires a protocol primitive.
Who is affected: every client — a human in the loop does not close the gap
This is a protocol-level gap that affects every MCP client, not only autonomous ones:
description/annotations; they have no way to verify that the declaration is authentic, unmodified, or unchanged since they approved it. A poisoned description reads as legitimate, and a silentlist_changedrug pull happens after approval. Human review therefore does not mitigate these attacks — it is the same unverified text either way.Authenticated declarations + machine-enforceable labels help any host: they surface verifiable provenance and risk to a human reviewer, and enable policy (risk-graduated approval, egress control, information-flow constraints) where there is no human. Both modes benefit from the same primitive.
Goals and Non-Goals
Goals
Non-Goals
How Has This Been Tested?
Breaking Changes
No. The proposed changes are fully backward compatible and the MCP server integrations secure moving forward.
Types of changes
Checklist
Additional context
Note:Extends / complements (does not replace): the Server Card WG, Tool Annotations IG, Tool Scopes WG, the Registry,
ext-auth, and the Security Best Practices.Relationship to existing work
This SEP is deliberately additive and complementary to work already underway in several MCP groups; it extends and composes with them rather than replacing any.
trustblock is a signed, standardized subset of tool annotations. This SEP builds on the annotations model by adding cryptographic integrity and a closed, host-enforceable vocabulary; it does not replace free-formannotations(which remain untrusted-by-default).trustlabels describe the risk / sensitivity a host gates on. The two are orthogonal and composable; this SEP aligns its label vocabulary with tool-scopes work rather than duplicating it.ext-auth— identity / authorization extensions align with the secondary secure-by-default posture and the publisher-identity model.Alignment with MCP design principles
Key design decisions
A signed manifest (JWS) rather than per-field signatures. One signature transitively authenticates every declaration through its contentHash, keeping messages small and verification a single operation.
Reuse of RFC 9728 Protected Resource Metadata for key discovery. No new discovery surface is introduced; the publisher JWKS and identity sit beside the existing authorization_servers and inherit the SSRF protections MCP already requires.
Binding to the RFC 8707 canonical server URI. Prevents replay of an otherwise-valid manifest against a different server (audience confusion).
Closed, enumerated trust labels (unknown ⇒ most-restrictive). A host can make a deterministic policy decision instead of parsing free text; this is what makes the labels usable for gating — whether surfacing risk to a human reviewer or enforcing policy without one.
Hashes in the manifest rather than full declaration bodies. Compactness; full-body signing is offered as a stricter option (see Open questions).