SEP Extension proposal: verifiable tool results(Verifiable MCP) — ZK proofs / TEE attestations attached to `tools/call` results (working reference implementation, pre-SEP feedback)

Author: AkiraTamaiCreated Sep 10, 2026Updated Sep 18, 2026
Labelstracking

Where was this agreed?

https://discord.com/channels/1358869848138059966/1379811011669921883/1547475087748374548

Description

GitHub Discussions is closed to non-collaborators, so I'm posting this here as a pre-SEP proposal rather than a bug or tracking item. I'll also share it in the Security IG channel on Discord and link both from the eventual Extensions Track SEP PR.

Summary

I'd like to gauge interest in an optional MCP extension that lets a server attach machine-verifiable evidence (a zero-knowledge proof, a TEE attestation, or a signed commitment) to a tools/call result, so a client can check that the returned value was computed by the expected program on the expected inputs without trusting the server operator.

There is a working, runnable reference implementation with a draft spec, a self-contained demo, and CI-tested provers for several proof systems:

If there is enough interest, I plan to follow the Creating Extensions process and submit an Extensions Track SEP. Before that I'd like feedback on (a) whether the problem is worth solving at the protocol-extension level, (b) whether the design fits MCP's design principles, and (c) whether this should be incubated under the Security IG.


Overview: the client verifies the proof carried in the result's _meta locally; the verification key comes from a pinned registry, never from the server Image


Detailed view: evidence envelope, proof formats, key registry, input provenance, and the three result paths Image


The gap

MCP 2026-07-28 has strong answers for who is calling (authorization) and, with Server Cards / signed manifests (discussion #2913), which server/tool is being called. What is still missing is whether the returned value is correct:

  • A client receives {"content":[{"type":"text","text":"72"}]} from a riskScore tool. Nothing tells it whether 72 came from the advertised model, from a stale cache, or was made up.
  • For tools that run someone else's model, price feeds, compliance checks, or anything where the server has an incentive to lie, transport auth and logging (discussion #3112, discussion #3215) establish accountability after the fact, but not correctness at the time the agent acts.
  • Some of these tools also need the inputs hidden from the operator (e.g. a credit check), which today means simply trusting the server.

This is exactly what verifiable computation is for, and the pieces (Groth16/UltraHonk circuits, RISC Zero zkVM, ezkl ZKML, AWS Nitro attestation, TLSNotary) exist today. What doesn't exist is a common place and shape for that evidence in MCP, so every server that wants it invents its own _meta fields and every client has to special-case them.

Proposed shape (summary of the draft spec)

Everything is negotiated via the standard extensions capability map and lives in _meta; unmodified clients/servers ignore it entirely.

  • Capability: extensions["io.github.ripple-node-lab/verifiable-tools"] advertises proofFormats, blindExecution, resultTtlMs. (Vendor prefix because io.modelcontextprotocol/ is reserved; would move on acceptance.)
  • Result metadata: result._meta[<id>] carries { proofFormat, proof, publicInputs, inputCommitment, outputCommitment, nonce?, circuitHash, inputAttestations? }. inputCommitment = sha256(salt || JCS(arguments)), outputCommitment = sha256(JCS(content)); publicInputs[0..2] are always inputCommitment, outputCommitment, nonce so the binding to this request/response is format-independent.
  • Proof formats are pluggable and identified by string; the spec defines the binding rules and a per-format profile appendix. Implemented: snarkjs-v2 (Groth16), noir-v1 (UltraHonk), risc0-v1 (zkVM receipt), ezkl-v1 (ZKML), tee-nitro-v1 (Nitro attestation, COSE/CBOR), plus demo signature/commitment formats.
  • Long-running proofs reuse the existing Tasks extension (tasks/get returns the same CallToolResult with the proof) instead of inventing a new async mechanism.
  • Deferred proofs: a result may carry a resultId; verifiable-tools/prove later returns the proof for it, so latency-sensitive callers can act first and verify later.
  • Blind execution (optional): verifiable-tools/call accepts encryptedArguments (HPKE, RFC 9180) with the salted commitment, so the operator's MCP process never sees the plaintext; only the proving environment does.
  • Input provenance (optional): inputAttestations (oracle signatures, or TLSNotary presentations of an upstream HTTPS response) are bound into publicInputs, so the proof covers "the price I used really came from that feed".
  • Verification key pinning: clients fetch verification keys / attestation roots from an origin-allowlisted registry keyed by circuitHash; the server is never the trust root for its own proof.

How it maps to the design principles

  • Composability over specificity — no new primitives; it is _meta + capability negotiation + Tasks. Only two extension methods (verifiable-tools/prove, verifiable-tools/call), both optional.
  • Interoperability over optimization — degrades to plain tools/call when either side lacks the capability; a server that cannot prove a particular call still returns a normal result unless the client set requireProof.
  • Demonstration over deliberationnpm run demo runs 10 scenarios end-to-end (sync/async/blind/deferred/TEE/four ZK systems/provenance); Docker sidecars for RISC Zero, ezkl and TLSNotary run in CI. Measured prove/verify times are in docs/BENCHMARKS.md.
  • Standardization over innovation — the proof systems are existing, deployed ones; the extension only standardizes where the evidence goes and how it is bound to the request/response.
  • Convergence over choice — the format string is the one deliberately open point, so the same envelope can carry future systems. Feedback welcome on whether that is the right cut.

There is also an adapter that runs the whole thing over the published @modelcontextprotocol/[email protected] (capabilities.extensions + custom methods), which I understand is a prerequisite for an Extensions Track SEP.

Questions for the community

  1. Is "verifiable results" something the Security IG sees as in scope, and would it be worth incubating as experimental-ext-verifiable-tools?
  2. Is _meta + Tasks the right layering, or should proof retrieval be a resource / a separate result content type?
  3. Should the extension standardize any concrete proof formats, or only the envelope + binding rules and leave formats to profiles?
  4. Overlap/complementarity with discussion #2913 (signed manifests → which code), discussion #3215 (execution receipts → that it ran), discussion #3112 (audit ledger → after the fact): I see this as the "correct output" leg of the same problem; does that framing hold?

Limitations (so nobody has to discover them)

Demo-grade trust setup (insecure Powers of Tau for the Groth16 circuit, mock Nitro CA/PCRs), TLSNotary verification is delegated to a sidecar rather than in-process, ezkl inputs are restricted to [0, 2^24], and there is no principal/session binding on resultId yet. All documented in docs/SECURITY.md.

AI disclosure

Per the AI contribution policy: the reference implementation, draft spec text and this issue were produced with substantial AI assistance (Devin, by Cognition), directed and reviewed by me. I understand the design and implementation and will answer questions myself; if any of my follow-up replies are AI-drafted I will say so in the reply.

Source: modelcontextprotocol/modelcontextprotocol