MCP Resource Subscription Consistency: List Cursors, Update Notifications, and Version Reconciliation
`resources/updated` only signals that a URI may have changed; it carries no content version. This article uses discovery generations, subscription leases, and read-digest reconciliation to handle lost notifications, out-of-order updates, and resource-set changes during pagination.
Key Points
- Topic Focus: AI Agent、MCP、RAG
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
A Client scans three pages of files with resources/list, then subscribes to one URI. During pagination, a resource is deleted and recreated, and an update notification is lost while the Client is disconnected. The Client still adds the old content to the prompt because notifications/resources/updated carries no version number, and the Client did not save the digest from its previous read. The arrival of a notification does not mean the content has been synchronized, and completing a list scan does not necessarily represent a snapshot from the same instant.
RAG Incremental Indexing Consistency explains the update strategy after data enters the index, while RAG Document Freshness SLOs track source health. This article focuses on the MCP Resource protocol itself: how URI discovery, subscriptions, notifications, and reads can produce a provably new local version.
Protocol Events Are Not Resource Versions
A Server may provide resources only after declaring the resources capability. subscribe and listChanged are two independent optional sub-capabilities. resources/list and resources/templates/list use opaque cursors, and the Server determines the page size. A Client must not parse, modify, or persist a cursor across sessions. The specification recommends stable cursors but does not guarantee that multiple pages form a transactional snapshot. Even if listChanged is declared, the Server only SHOULD send notifications, and those notifications contain no sequence number or watermark. Notifications can therefore only trigger invalidation; the absence of a notification is never proof of completeness.
The control plane stores resourceDiscoveryScan.v1: the session generation, first-request time, digest of each page's cursor, URI/descriptor set digest, observed notifications, and completion time. If the Server provides a negotiated snapshot ID extension, every page must be bound to the same ID before SNAPSHOT_PROVEN can be published. When standard MCP provides no such proof, complete at least two consecutive A/B full scans. Restart reconciliation if either scan receives a list-changed notification, the two scan digests differ, or a cursor becomes invalid. Even if both scans match, the system may only publish SNAPSHOT_UNPROVEN with measuredAt, an upper staleness bound, and the next review time. It must not claim that this is an authoritative snapshot to authorization-revocation or cleanup tasks that require complete enumeration.
The resourceSubscription.v1 subscription ledger binds the subject, Server, normalized URI, session, capability receipt, subscription generation, start/end times, last notification sequence, and last accepted read digest. The official schema defines resources/subscribe and resources/unsubscribe; a successful unsubscribe only stops subsequent update notifications and does not delete cached content. Without the subscribe capability, use periodic reads with jitter and publish the upper staleness bound rather than pretending the data is real-time. Agent Tool Capability Catalog Generation can use the same generation mechanism to isolate separate sessions.
DISCOVERED -> SUBSCRIBING -> ACTIVE
ACTIVE -> UPDATE_SEEN -> READ_PENDING -> VERSION_ACCEPTED -> ACTIVE
ACTIVE -> UNSUBSCRIBING -> CLOSED | UNSUBSCRIBE_UNKNOWN
Any read or write with an unknown identity -> RECONCILIATION_REQUIRED
An updated notification carries only the URI. On receipt, coalesce concurrent pulses for the same URI and call resources/read. Before each dispatch, use CAS to allocate a monotonically increasing readOrdinal for (session, subscriptionGeneration, URI), then save the request ID and trigger reason. The Client computes resourceVersion.v1 for the response, including the URI, MIME type, content digest, read ordinal, authorization subject, Server implementation version, and local generation. lastModified is only a hint and cannot replace the digest.
The sole acceptance point, acceptResourceVersion, requires the subscription to remain ACTIVE, match the generation and URI, reference an issued request and ordinal, and satisfy readOrdinal > lastAcceptedOrdinal. It then atomically advances the watermark and writes the version receipt and invalidation outbox. If B=6 returns first, the late A=5 can only leave a stale receipt; if A returns first, 5 and 6 can be accepted in sequence. Generation-Fenced Singleflight for Cache Revalidation can reuse the same fence.
beginUnsubscribe uses CAS to change ACTIVE to UNSUBSCRIBING while freezing closeOrdinal=lastIssuedOrdinal and the close epoch; no further reads are allocated afterward. Regardless of whether a read was issued before close, its late content or 404 only writes a STALE_AFTER_CLOSE audit record and does not update the version, tombstone, or outbox. A lost unsubscribe ACK moves the subscription to UNSUBSCRIBE_UNKNOWN; the local side remains silent and reconciles within the same session or eventually closes the session, but it must not reopen the subscription. Only when a read that remains ACTIVE returns not found may the ordinal fence mark it as TOMBSTONE_PENDING and use list reconciliation to confirm deletion. Before content enters the model, it must also pass through Agent Tool Output Guardrails.
Fault injection covers changes across multiple pages with lost notifications, looping cursors, forged snapshot IDs, out-of-order update notifications, lost unsubscribe ACKs, races between close and read acceptance, late content/404 after CLOSED, notifications from old sessions, read A being slower than B, URI encoding ambiguities, and cache hits after permission revocation. Assertions require that an old generation never overwrite a new version, a single scan never produce a complete catalog, accepted versions/tombstones/invalidations from non-ACTIVE subscriptions remain at zero, and every derived index remain traceable after permission revocation.
Release gates require both acceptance of unreconciled catalogs and consumption of SNAPSHOT_UNPROVEN by completeness tasks to remain at zero. Across 100,000 notification/read/close reorderings, version regressions, late tombstones, and outbox writes after CLOSED must all remain at zero. Read amplification from duplicate notifications must stay below 1.2×; 99.9% of active resources must converge within 60 seconds, and permission revocation p99 must be under 30 seconds. Roll out first with shadow double scans, ordinals, and close fences, then enable subscription refresh for 5%. A rollback must preserve watermarks and close receipts so that old reads do not lose their fences.
Related
Related Content
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
Why Does MCP Matter? Understanding the AI Tool Protocol
Technical trends, engineering practices, and an independent developer perspective on MCP and AI Agents.
Article
Core Architecture of AI Agents: Planning, Memory, Tools, and Execution
Technical trends, engineering practices, and independent developer perspectives around AI Agents and large language models.
Article
Complete Technology Stack Analysis for LLM Application Development
Covers tech trends, engineering practices, and indie developer perspectives around large models, RAG, and AI.