Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

StatusMeaning
CONFORMSmnemo satisfies the requirement today, either directly or because it never adopted the thing being removed.
GAPmnemo could close this without waiting for anyone. It has not.
UPSTREAM-BLOCKEDClosing 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-05
  • 2025-03-26
  • 2025-06-18
  • 2025-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 changeWhat the spec requiresmnemo todayrmcp 3.1.3 todayStatus
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 checkpointbranch/replay, and lease_token carries recallforget_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 setWhat 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 changeWhat the spec requiresmnemo todayrmcp 3.1.3 todayStatus
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 orderServers 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 cacheScope is private and 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, and resources/list returns one agent’s memory records. A public scope would permit a shared intermediary to serve one caller’s catalog, or one agent’s memories, to a different caller. CacheScope::default() is Public, 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_cacheable in crates/mnemo-mcp/tests/mcp_2026_07_28_conformance.rs asserts this on both surfaces, and was verified by mutation.

These fields belong to 2026-07-28 while mnemo negotiates 2025-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 understand cacheScope is told the truth now rather than after the eventual revision bump.

Why the error-code row stays open on purpose. mnemo emits -32002 because that is what 2025-11-25 - the revision it actually negotiates - specifies. Changing it to -32602 now would make mnemo non-conformant with the version it speaks in order to match a version it does not. The row closes when mnemo adopts 2026-07-28, not before.

Results

Spec changeWhat the spec requiresmnemo todayrmcp 3.1.3 todayStatus
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.

RequirementWhat the spec requiresWhat mnemo does todayStatus
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 responsesAuthorization 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 featuremnemoStatus
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:

HandleMinted byConsumed byPassed as
checkpoint_idmnemo.checkpointmnemo.branch, mnemo.replayan ordinary tool argument
lease_tokenmnemo.recall (ADR 0001)mnemo.forget_subjectan 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. The CallerContext is resolved per request by identity.rs from that request’s own _meta (ADR 0002), and call_tool inserts it immediately before dispatch. Two calls on one connection carrying two different capabilities produce two different callers. _meta remains a per-request carrier under 2026-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 gets AnonymousOnAuthenticatedTransport, 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 rowStatusWhat closes it
Stateless lifecycleUPSTREAM-BLOCKEDrmcp moving ProtocolVersion::LATEST to V_2026_07_28
SEP-2243 Mcp-Method / Mcp-NameUPSTREAM-BLOCKEDthe same move; rmcp gates the headers on that revision
SSE resumability removalUPSTREAM-BLOCKEDthe same move; the behaviour is rmcp’s transport, not mnemo’s
Resource-not-found -32002GAP, deliberateadopting 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.