Skip to content

Proposal: Deterministic Per-Primitive Digests#45

Open
SamMorrowDrums wants to merge 6 commits into
modelcontextprotocol:mainfrom
SamMorrowDrums:deterministic-primitive-surface-digest
Open

Proposal: Deterministic Per-Primitive Digests#45
SamMorrowDrums wants to merge 6 commits into
modelcontextprotocol:mainfrom
SamMorrowDrums:deterministic-primitive-surface-digest

Conversation

@SamMorrowDrums

Copy link
Copy Markdown

Summary

Early-stage proposal (SEP format) to sound out with the Transports WG before pursuing a core SEP.

Adds a deterministic, opaque surface digest carried on the _meta of every server result — a content hash of the caller-visible primitive surface (tools, prompts, resources, resource templates). It gives stateless clients a deterministic, pull-based way to detect that a server's surface changed on any subsequent request, without an SSE stream, subscriptions/listen, or session IDs.

Builds on the stateless/sessionless direction of SEP-2575 / SEP-2567, complements the TTL caching model of SEP-2549 (validator + budget), and follows the _meta conventions of SEP-414.

Why this fits the Transports WG

This is squarely a transport-layer change-detection concern: how does a client learn the server's surface changed (deploy, permission change, schema drift) when the transport is stateless and may be load-balanced across instances?

Motivated by concrete operational pain running a large remote MCP server:

  • didn't want to stand up an SSE GET stream solely for deploy updates;
  • tools/list_changed isn't universally honored (and stateless clients won't open subscriptions/listen either);
  • revocable session IDs were meant to force renegotiation but bricked the server with real clients in testing;
  • pushing a deploy-driven list_changed to the right connection in a scaled fleet needs cross-instance fan-out and has no single 'change instant' during a rolling deploy.

A per-response digest needs zero cross-instance coordination: each instance stamps the surface it serves; the client compares.

Key points

  • Response-side detection: io.modelcontextprotocol/surfaceDigest on every result; optional per-kind surfaceComponents.
  • Reflection / conditional requests: clients MAY echo expectedSurfaceDigest on a request; servers MAY reject a stale tools/call before executing with a new SurfaceChangedError (-32005, HTTP-412-style). Handles output/input-schema drift and lets the harness re-plan/re-prompt.
  • Deterministic, not TTL-based: deploys, permission changes, and schema changes all deterministically change the digest.

Status

  • Status: Draft, seeking a sponsor.
  • File: proposals/XXXX-deterministic-primitive-surface-digest.md.

Feedback welcome on capability shape, whether to mandate RFC 8785 canonicalization, reflection scope, and deploy-time retry guidance (see Open Questions).

SamMorrowDrums and others added 2 commits June 25, 2026 14:28
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adopt per-primitive ETags (io.modelcontextprotocol/digest) in each
primitive's _meta with optimistic If-Match-style reflection
(expectedDigest) and DigestChangedError; drop the aggregate surface
digest. Lead with production deployment as the fundamental case, make
capability advertisement optional, inherit cacheScope, and add optional
HTTP header mirroring for rolling upgrades.
@SamMorrowDrums SamMorrowDrums changed the title Proposal: Deterministic Primitive Surface Digest Proposal: Deterministic Per-Primitive Digests Jun 30, 2026
@SamMorrowDrums

SamMorrowDrums commented Jun 30, 2026

Copy link
Copy Markdown
Author

Reworked this proposal in response to the Caching & Optimization track discussion.

Moved to a digest per primitive (an opaque ETag on each tool/prompt/resource/template, in its _meta), and dropped the aggregate surface digest entirely. Rationale: per-primitive maps directly onto standard ETag-per-entity semantics, keeps the validator scoped to the primitive actually being used, and an unrelated change never trips a call to an unaffected tool. Whole-surface discovery is delegated to ttlMs (re-list when stale) and, optionally, list_changed.

Optimistic conditional requests. A client MAY echo io.modelcontextprotocol/expectedDigest on tools/call / prompts/get / resources/read. The server MAY reject with DigestChangedError (-32005, an If-Match/412 analogue) carrying the current digest, or serve and return the current digest. Client implementors MAY fail open, fail closed, or prompt (e.g. ultra-sensitive clients hide changed tools).

Fits the caching picture. The digest is the call-time validator; ttlMs stays the list freshness budget; a digest's shareability inherits the primitive's cacheScope (public → CDN/proxy-shareable; private → per-auth-context). No structural schema change — every primitive and single-primitive result already has _meta.

Capabilities as an optional optimization (per @mark): advertisement is a hint, not a precondition. A client uses the digest on sight and MAY send expectedDigest without pre-checking; an unsupporting server ignores it.

Production deployment is the headline case. Reframed the motivation around the thing we've never had a working spec answer for — a server redeploys and its primitives (incl. outputSchema) change — and why GET streams, list_changed, and session revocation each failed in practice at scale.

Rolling upgrades / HTTP. Added optional HTTP header mirroring for single-primitive ops so intermediaries can revalidate within a still-fresh TTL window (open question: reuse ETag/If-Match vs an Mcp-* header). The push-side single-subscription-RPC direction (SEP-1442) is acknowledged as complementary and out of scope.

Open questions updated accordingly (header shape, resource content vs definition ETags, canonicalization normativity, reflection scope, deploy-time retry storms).

@schlpbch schlpbch left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify what deterministic means relative to this SEP.

My feeling is that "unique" is the better term. And that you are bascially proposing an encoding. What properties does this encoding have? Should it be e.g. decodable?

@@ -0,0 +1,650 @@
# SEP-XXXX: Deterministic Per-Primitive Digests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the definition of "deterministic" in the content of this SEP?

Deterministic is quite a difficult term.

@schlpbch schlpbch Jul 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A common definition of deterministic is given here:

https://en.wikipedia.org/wiki/Deterministic_finite_automaton

But I can't make the link to your PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the kind of determinism I mean: https://en.wikipedia.org/wiki/Deterministic_algorithm

In computer science, a deterministic algorithm is an algorithm that, given a particular input, will always produce the same output, with the underlying machine always passing through the same sequence of states.

primitive surface should be able to reflect that reality deterministically. Yet
the existing mechanisms do not deliver it for real, scaled deployments:

- **TTL alone is non-deterministic with respect to deploys.** A client that

@schlpbch schlpbch Jul 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does that mean? IMHO, Time to live is not related to determinism.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doy you mean a well-ordered lists of identifiers (ordered by time)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's misused in this application, should rephrase.

## Abstract

This SEP gives every caller-visible primitive — each tool, prompt, resource, and
resource template — a deterministic, opaque **digest** of its own definition,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't "unique" the better term?

…t proposal

Resolve the seven open questions per WG review (header story: ETag for lists,
Mcp-* header for calls; MAY-equivalent canonicalization; complementary resource
content revalidation; protocol-version changes kept observable; reflection on all
single-primitive methods; soft process-and-flag posture for rolling deploys with
draining guidance; single primitiveDigests capability). Rewrite Open Questions
into resolved vs. still-open.
@SamMorrowDrums

Copy link
Copy Markdown
Author

Folded in the resolutions to all seven open questions from WG review (thanks @-Shaun and @-Mark). Summary of what changed:

  • Header story (Q1). Split cleanly: */list responses use a standard ETag/If-None-Match (the list body is a cacheable entity); single-primitive calls do not reuse ETag/If-Match (a response ETag would validate the call output, not the primitive contract), so the digest stays in _meta and MAY be mirrored into a dedicated Mcp-* header over HTTP.
  • Canonicalization (Q2). One easy standard recipe (RFC 8785 JCS + SHA-256), but servers MAY use any equivalent ETag-style mechanism that meets the determinism requirements — interop depends only on opaque equality.
  • Resource content vs definition (Q3). Added a complementary note: resource content revalidation uses ordinary ETag on resources/read under a distinct validator from the primitive digest; if a server provides it, clients SHOULD use it.
  • Protocol-version (Q4). A version change (e.g. from a deploy) is exactly what we want to notice, so it's an observable change, not normalized out.
  • Reflection scope (Q5). Applies to all single-primitive methods and all primitive kinds, not just tools/call.
  • Deploy-time storms (Q6). Added a soft "process and flag" posture (serve the call, reflect the new digest in result _meta), plus guidance on connection draining — and the key point that the subscription channel drains last, which is precisely why deploy-change propagation doesn't belong on it.
  • Capability (Q7). Single primitiveDigests hint; request-side reflection is not separately discoverable.

The Open Questions section now separates the resolved items from the few genuine residuals (exact Mcp-* header spellings, whether soft-vs-strict should be client-signaled, and where the resource content-validator key/header lives).

Firm up the three residual open questions and replace Open Questions with a
settled Design Decisions recap: normative Mcp-Digest / Mcp-Expected-Digest
header names; reserved io.modelcontextprotocol/contentDigest key for resource
content revalidation; strict-vs-soft reflection as a server/operator policy
rather than a client-negotiated knob.
@SamMorrowDrums

Copy link
Copy Markdown
Author

Tightened this up to be opinionated — the Open Questions section is gone, replaced by a settled Design Decisions recap. The three items I'd previously left dangling are now firm:

  • Header names are normative: Mcp-Digest (response) and Mcp-Expected-Digest (request); standard ETag/If-Match MUST NOT be reused for single-primitive digests.
  • Resource content revalidation has a reserved key: io.modelcontextprotocol/contentDigest, distinct from the definition digest.
  • Strict vs. soft reflection is a server/operator policy, not a client-negotiated knob — the client sends expectedDigest identically either way and handles both outcomes.

No residual open questions; happy to argue any of these if reviewers disagree.

TTL keeps a client from staying stale (proactive, time-bounded); the per-primitive
digest corrects a client that is stale right now (reactive, deterministic at call
time). Make this complementarity explicit in the Abstract and TTL interaction.
Hand edits from the author: tighten the abstract and motivation, reframe TTL
as the pessimistic check and the digest as the optimistic check, add the
deployment/connection-draining argument up front, broaden the other-drivers
section (permissions, feature flags), and trim the aggregate/push discussion.
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.

2 participants