MCP 2026-07-28 conformance
State of mnemo against the 2026-07-28 MCP specification revision, row by
row. Anchored at commit aec648f
against rmcp 3.1.3 (what the workspace rmcp = "3.0" requirement resolves to).
This page exists because mnemo’s posture is to follow rmcp’s implementation rather than race the spec, and that posture is only defensible if there is a document saying which parts are and are not done. Without one, “we follow rmcp” is indistinguishable from “we have not looked”.
What the statuses mean
| Status | Meaning |
|---|---|
| CONFORMS | mnemo satisfies the requirement today, either directly or because it never adopted the thing being removed. |
| GAP | mnemo could close this without waiting for anyone. It has not. |
| UPSTREAM-BLOCKED | Closing it requires rmcp to move first. The rmcp version that would unblock it is named in the row. mnemo does not fork rmcp. |
The headline
mnemo negotiates 2025-11-25, not 2026-07-28.
rmcp 3.1.3 knows the newer revision - it carries ProtocolVersion::V_2026_07_28,
the CacheScope type, the SEP-2243 header constants and the MRTR result types -
but its ProtocolVersion::LATEST is still V_2025_11_25, so that is what an
initialize handshake settles on. Most of what follows falls out of that one
fact.
The revisions mnemo advertises, which
crates/mnemo-mcp/tests/mcp_2026_07_28_conformance.rs asserts against the
running server:
2024-11-052025-03-262025-06-182025-11-25
2026-07-28 is deliberately not in that list, and goes back in when mnemo
implements it, not when rmcp does.
Lifecycle and transport
| Spec change | What the spec requires | mnemo today | rmcp 3.1.3 today | Status |
|---|---|---|---|---|
| Sessions removed (SEP-2567) | No protocol sessions, no Mcp-Session-Id. List endpoints must not vary per connection. Cross-call state uses “explicit, server-minted handles passed as ordinary tool arguments”. | Never used sessions. Cross-call state is already handle-threaded: checkpoint_id carries checkpoint → branch/replay, and lease_token carries recall → forget_subject. tools/list varies by the capability presented on that request (ADR 0002), which is a per-request property, not a per-connection one. Pinned by explicit_handle_roundtrip.rs. | Still implements Mcp-Session-Id for revisions at or below 2025-11-25. | CONFORMS |
| Stateless lifecycle (SEP-2575) | Remove initialize / notifications/initialized. Every request carries io.modelcontextprotocol/protocolVersion and client capabilities in _meta. | Uses the initialize handshake; get_info() answers it. The per-request _meta plumbing already exists - mnemo reads its own capability key out of _meta on every call (ADR 0002) - so this is a lifecycle change, not new plumbing. A caller should assume it must perform the initialize handshake and must not send per-request io.modelcontextprotocol/protocolVersion; it will be ignored. | Gated behind negotiating 2026-07-28; LATEST is 2025-11-25, so the gate never opens by default. | UPSTREAM-BLOCKED - unblocks when rmcp moves ProtocolVersion::LATEST to V_2026_07_28. |
server/discover (SEP-2575) | Servers MUST advertise supported protocol versions, capabilities and identity. | Inherits rmcp’s default discover, derived from supported_protocol_versions() and get_info(). | Provides the RPC and the default implementation. | CONFORMS |
| Advertised version set | What server/discover reports must be what the server serves. | Narrowed to the four revisions above. Previously took rmcp’s default (ProtocolVersion::KNOWN_VERSIONS), which advertised 2026-07-28 - a revision mnemo does not serve. See “The defect this page found”. | Default is every revision the SDK knows, 2026-07-28 included. | CONFORMS (fixed here) |
subscriptions/listen (SEP-2575) | Replaces the HTTP GET endpoint and resources/subscribe / resources/unsubscribe. | Implements no subscriptions at all, so there is nothing to migrate. | Implements subscriptions/listen. | CONFORMS |
ping, logging/setLevel, notifications/roots/list_changed removed (SEP-2575) | These methods go away. | Overrides none of them; rmcp’s set_level default already answers method-not-found. | Retains them for older revisions. | CONFORMS |
| SSE resumability removed (SEP-2575) | No Last-Event-ID, no SSE event IDs. A broken stream loses the in-flight request. | stdio by default. Under the optional http-transport feature the stream is rmcp’s Streamable HTTP, so resumability is rmcp’s behaviour, not mnemo’s. A caller should assume Last-Event-Id resumption still works at the negotiated revision, and should not rely on it, since it disappears when mnemo adopts 2026-07-28. | Still implements Last-Event-Id for older revisions. | UPSTREAM-BLOCKED |
Headers and caching
| Spec change | What the spec requires | mnemo today | rmcp 3.1.3 today | Status |
|---|---|---|---|---|
Mcp-Method / Mcp-Name (SEP-2243) | Required on Streamable HTTP POST requests so gateways can route without reading the body. | Not applicable on stdio, which is the default transport. Under http-transport the headers are rmcp’s to emit and validate. A caller should assume these headers are neither required nor validated, so a gateway cannot route mnemo traffic on them today and must read the body. | Has HEADER_MCP_METHOD and HEADER_MCP_NAME, explicitly gated on ProtocolVersion::STANDARD_HEADERS, which is V_2026_07_28. | UPSTREAM-BLOCKED - the code path exists but cannot activate while the negotiated revision is 2025-11-25. |
ttlMs on list results (SEP-2549) | tools/list, prompts/list, resources/list, resources/read and resources/templates/list carry a freshness hint in milliseconds. | Implemented. tools/list advertises 60000 (the catalog is compiled in and role-filtered, so it is stable for a fixed capability, but an operator manifest change must not be cached for an hour). resources/list advertises 0, immediately stale, because any mnemo.remember invalidates the listing. mnemo serves no prompts/* or resources/templates/*. | Fields present on the list-result types. | CONFORMS |
cacheScope on list results (SEP-2549) | "public" or "private", controlling whether shared intermediaries may cache. | Implemented as private on both listings, and that is a correctness requirement rather than a tuning choice. See the note below. | CacheScope enum present; note its Default is Public. | CONFORMS |
Deterministic tools/list order | Servers SHOULD return tools in a deterministic order so clients can cache. | Filters the router’s list by caller role; does not reorder it. | ToolRouter::list_all() sorts by name before returning. | CONFORMS (via rmcp) |
| Resource-not-found error code | -32002 becomes -32602 (Invalid Params). | Calls McpError::resource_not_found, which emits -32002. A caller should assume resource-not-found arrives as -32002, not -32602. | That constructor still emits ErrorCode::RESOURCE_NOT_FOUND, which is -32002. | GAP, deliberately unclosed - see below. |
Why
cacheScopeisprivateand must stay that way. Both of mnemo’s listings are scoped to the caller’s per-request identity (ADR 0002): two callers presenting different capabilities see different tool catalogs, andresources/listreturns one agent’s memory records. Apublicscope would permit a shared intermediary to serve one caller’s catalog, or one agent’s memories, to a different caller.CacheScope::default()isPublic, so leaving the field unset is not the safe option it appears to be once anything downstream begins reading it.per_caller_listings_are_never_publicly_cacheableincrates/mnemo-mcp/tests/mcp_2026_07_28_conformance.rsasserts this on both surfaces, and was verified by mutation.These fields belong to
2026-07-28while mnemo negotiates2025-11-25, so no client is currently promised them. They are emitted regardless: a client that does not know the field ignores it, while an intermediary that does understandcacheScopeis told the truth now rather than after the eventual revision bump.
Why the error-code row stays open on purpose. mnemo emits
-32002because that is what2025-11-25- the revision it actually negotiates - specifies. Changing it to-32602now would make mnemo non-conformant with the version it speaks in order to match a version it does not. The row closes when mnemo adopts2026-07-28, not before.
Results
| Spec change | What the spec requires | mnemo today | rmcp 3.1.3 today | Status |
|---|---|---|---|---|
resultType on all results (SEP-2322) | Required field, "complete" or "input_required". Clients MUST read an absent field as "complete". | Returns CallToolResponse::Complete. Observed on the wire at the negotiated revision: result_type: None, which is correct - rmcp clears the field for peers that negotiated an older version. | Models ResultType and clears it per negotiated version. | CONFORMS (via rmcp) |
| Multi Round-Trip Requests (SEP-2322) | Server-initiated requests are replaced by InputRequiredResult plus a client retry. | Every mnemo tool is synchronous and always resolves to Complete, so there is nothing to implement. | Implements the MRTR types and the client-side retry loop. | CONFORMS |
Authorization (RFC 9728, RFC 8707)
This section exists because its absence was the most likely thing to be mistaken for conformance. The rows above are about the wire protocol; nothing in them says whether mnemo does OAuth. It does not.
mnemo implements no OAuth authorization. There is no
/.well-known/oauth-protected-resource document, no WWW-Authenticate
challenge, no authorization-server discovery and no bearer-token audience
validation. Authentication is an HMAC capability presented per request
(ADR 0002), which is a different mechanism, not a partial implementation of this
one.
That is a conformant position rather than a gap, and the spec says so in the paragraph that governs the whole authorization document:
Authorization is OPTIONAL for MCP implementations. When supported: Implementations using an HTTP-based transport SHOULD conform to this specification. Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.
mnemo’s default transport is stdio and it takes its capability key from the
environment (MNEMO_CAPABILITY_KEY), which is the behaviour that paragraph
prescribes.
| Requirement | What the spec requires | What mnemo does today | Status |
|---|---|---|---|
| RFC 9728 Protected Resource Metadata | “MCP servers MUST implement OAuth 2.0 Protected Resource Metadata.” The MUST is real, and conditional: it binds servers that support authorization, which is itself OPTIONAL, and stdio servers are told SHOULD NOT. | Not implemented. No /.well-known/oauth-protected-resource is served on any transport. A caller should assume discovery will 404 and must not infer from that that the server is unprotected; authenticate with an HMAC capability instead. | Not implemented, and conformant on stdio |
RFC 8707 Resource Indicators, resource parameter | “MCP clients MUST implement Resource Indicators … MUST be included in both authorization requests and token requests.” | Does not apply. This MUST binds clients, and mnemo ships an MCP server; the repo publishes no MCP client. | Not applicable |
| RFC 8707 audience validation | “MCP servers MUST validate that access tokens were issued specifically for them as the intended audience.” | Not implemented as written, because mnemo accepts no OAuth access tokens to validate. The equivalent property is enforced on its own mechanism: a capability verifies against the issuer key and its principal, and an unverifiable capability is rejected rather than downgraded. A caller should assume an OAuth bearer token will not be accepted. | Not implemented (no OAuth tokens accepted) |
RFC 9207 iss in authorization responses | Authorization servers SHOULD include iss; clients MUST validate a present iss. | Does not apply. mnemo is neither an authorization server nor a client. | Not applicable |
Why this is worth stating rather than leaving blank
Serving RFC 9728 discovery is close to unheard of in practice: the scanning work in this project’s sibling reports 0 of 2,303 public MCP server configurations serving it. That figure is reported rather than reproduced here, and it is context, not an excuse. The useful thing is not that mnemo is normal, it is that a reader can find out in one place instead of inferring from silence.
An operator putting mnemo behind an OAuth-protected gateway should terminate authorization at that gateway and pass a capability inward. mnemo will not participate in the OAuth flow, will not advertise an authorization server, and will not accept the gateway’s access token in place of a capability.
Deprecations
None of these require work: mnemo never adopted any of them.
| Deprecated feature | mnemo | Status |
|---|---|---|
| Roots (SEP-2577) | Never implemented. The spec’s suggested migration, passing paths as tool parameters, is what mnemo already does. | CONFORMS |
| Sampling (SEP-2577) | Never implemented. mnemo does not call an LLM; it stores and retrieves. | CONFORMS |
| Logging (SEP-2577) | Never implemented. mnemo logs through tracing to stderr, which is the spec’s suggested migration. | CONFORMS |
| HTTP+SSE transport (SEP-2596) | Never implemented. The optional http-transport feature is Streamable HTTP (ADR 0002). | CONFORMS |
The explicit-handle pattern, and the audit behind it
SEP-2567 states the replacement for sessions directly:
Servers that need cross-call state use explicit, server-minted handles passed as ordinary tool arguments.
mnemo already worked this way. Under 2025-11-25 that was one valid option
among several; under 2026-07-28 it is the sanctioned one. Being on the right
side of a change by accident is not the same as being on it on purpose, so the
property is now tested rather than assumed
(crates/mnemo-mcp/tests/explicit_handle_roundtrip.rs).
Two handles carry every piece of cross-call state mnemo has:
| Handle | Minted by | Consumed by | Passed as |
|---|---|---|---|
checkpoint_id | mnemo.checkpoint | mnemo.branch, mnemo.replay | an ordinary tool argument |
lease_token | mnemo.recall (ADR 0001) | mnemo.forget_subject | an ordinary tool argument |
The LeaseStore is server-side state, but it is not a session: it is the
bookkeeping behind a server-minted handle that the caller threads back through
a tool argument, which is precisely the shape the SEP prescribes. A lease is
bound to the principal that minted it and expires, so it is narrower than a
session, not a rebranding of one.
Audit: what depends on identity that is not an argument
Every tool in crates/mnemo-mcp/src/tools/ was checked for behaviour that
depends on connection-scoped identity rather than on a value in the request.
- 19 of 23 tools take
Parameters<T>and nothing else. Their signatures admit no context at all, so connection-scoped state is not merely unused, it is unrepresentable. - 4 tools take
Extension<CallerContext>:mnemo.recall,mnemo.forget_subject,mnemo.delegate,mnemo.trajectory_audit. This is not connection state. TheCallerContextis resolved per request byidentity.rsfrom that request’s own_meta(ADR 0002), andcall_toolinserts it immediately before dispatch. Two calls on one connection carrying two different capabilities produce two different callers._metaremains a per-request carrier under2026-07-28, which puts protocol version and client capabilities there too, so this shape survives the revision unchanged.
The one deliberate deviation
When a request presents no capability at all, resolve_caller falls back to
a boot-derived identity: the agent_id fixed when the server process started.
That value is process-scoped, so this is a genuine departure from
“everything arrives in the request”, and it is kept on purpose.
The reason is that on stdio one process is one peer, and the operator who started the server genuinely is the caller. Requiring a capability there would break every existing stdio deployment on upgrade to buy no security, because there is no second caller to distinguish from.
The deviation is bounded rather than open-ended:
- It applies only where no capability was presented. A capability that is present but unverifiable is an error, never a downgrade to the boot identity, since silently downgrading would hand a forged token the operator’s authority.
- On the network-facing
http-transport, the fallback does not apply at all: an unauthenticated request getsAnonymousOnAuthenticatedTransport, because there the fallback would let anyone who can reach the port act as the operator.
See crates/mnemo-mcp/src/identity.rs for the full resolution table.
The defect this page found
Writing the table surfaced one live defect rather than only recording known ones.
mnemo did not override supported_protocol_versions(), so it took rmcp’s
default of ProtocolVersion::KNOWN_VERSIONS. rmcp derives server/discover
from that list. The result, confirmed against a running server:
advertised protocol_version = 2025-11-25
supported_protocol_versions = [2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, 2026-07-28]
mnemo was telling any discovering client that it speaks 2026-07-28 while
answering initialize with 2025-11-25 and implementing none of the newer
revision’s server-side requirements. A client entitled to believe that
advertisement would have sent 2026-07-28 requests to a server that still
expects the handshake that revision removes, and would have received list
results with neither ttlMs nor cacheScope.
That is a machine-readable claim that was not true - the same
claimed-but-not-wired shape this repo has repaired before (role_filter #124,
tool-catalog attestation v0.5.20, LeaseStore under ADR 0001).
The fix is to narrow the list to what mnemo serves. This is rmcp’s own supported
mechanism rather than a workaround: its negotiate_protocol_version documents
that “a server that narrows that list is never made to answer initialize with
a version it cannot serve”, and a client asking for an unlisted revision
negotiates down to the server fallback instead of failing. No client breaks.
What is still open, and what would close it
Four rows are not CONFORMS. None of the four is closable by mnemo alone today, which is the reason each carries a caller-assumption sentence in the table above rather than a promise.
| Open row | Status | What closes it |
|---|---|---|
| Stateless lifecycle | UPSTREAM-BLOCKED | rmcp moving ProtocolVersion::LATEST to V_2026_07_28 |
SEP-2243 Mcp-Method / Mcp-Name | UPSTREAM-BLOCKED | the same move; rmcp gates the headers on that revision |
| SSE resumability removal | UPSTREAM-BLOCKED | the same move; the behaviour is rmcp’s transport, not mnemo’s |
Resource-not-found -32002 | GAP, deliberate | adopting 2026-07-28, not an independent change |
All four collapse into one event: rmcp flips LATEST, mnemo adds V_2026_07_28
to supported_protocol_versions(), and this page is rewritten rather than
amended. The error-code row is listed as a GAP rather than UPSTREAM-BLOCKED
because mnemo could emit -32602 today; it does not, because -32002 is what
the revision it actually speaks specifies, and changing it early would trade real
conformance for imaginary conformance.
Separately, the authorization rows are not on this list. They are not open work: mnemo implements no OAuth and, on stdio, the spec says it should not. See the section above for what a caller should assume instead.
The two rows that were mnemo’s to close, ttlMs and cacheScope, are closed
as of 0.5.26.