# When must an MCP HTTP-native instance invalidate a resumption token after `Last-Event-ID` replay? **Date of Record:** 2026 November 6 **Scope:** Applies to MCP servers implementing the HTTP-native streamable transport where SSE event IDs are client-consumable resumption cursors and session state is keyed by a session token. Does not apply to plain (non-resumable) SSE, to stdio transport, or to servers that treat event IDs as opaque and non-replayable. ## Core Distinction The non-obvious boundary is between *event ID monotonicity* and *session token validity*. A resumption token (`Last-Event-ID`) and a session token (`Mcp-Session-Id`) are independent axes. The failure mode is treating a successful replay as proof that the session is still live, or treating a live session as proof that the requested event ID is still replayable. These are separate invariants: - **Replay invariant:** the requested `Last-Event-ID` must be ≥ the session's retained replay floor and ≤ the session's high-water mark. - **Liveness invariant:** the session token must not have been expired, evicted, or superseded. A server that honors `Last-Event-ID` from a stale or unknown session token re-delivers events into a context that no longer has the state those events mutate. That is the failure. ## What Was Observed During a reconnect storm (client process restarted, then reconnected with a persisted session token and a stale `Last-Event-ID` from before a server-side rolling restart), the server: 1. Found the session token in its in-memory session map (it had not yet been evicted). 2. Found the requested event ID still within the retained ring buffer. 3. Replayed events 1041–1099, then resumed live streaming. But the session's tool-call state had been reset by the restart — the replay re-issued `notifications/tools/list_changed` and partial progress notifications for calls the client had already completed. The client applied them twice, producing duplicate side effects. The replay was *technically valid* (IDs were in range) but *semantically invalid* (the session's state generation had changed). The tell: the replay floor and the session generation counter were tracked separately, and the reconnect path checked only the former. ## Condition of Validity `Last-Event-ID` replay is valid **only if** all three hold: - The session token resolves to a session whose `generation` matches the generation at which the requested event ID was emitted. - The requested ID is within `[replay_floor, high_water_mark]` for that generation. - The session has not been marked `evicted` or `superseded`. If the session was recreated (new generation) under the same token, or if the token was reused after eviction, the server MUST reject the resumption with `409 Conflict` and require the client to re-initialize — it MUST NOT replay across generations, even when the ID is numerically in range. ## Decision Boundary On a `GET` with `Last-Event-ID`: - **Accept replay** iff session is live, generation matches, and `replay_floor ≤ Last-Event-ID ≤ high_water_mark`. - **Reject with 409** iff session is live but generation differs, or ID is below `replay_floor` (events already compacted) — force re-init. - **Reject with 404** iff the session token is unknown or evicted — client must `initialize` again. - **Never** silently clamp `Last-Event-ID` to `high_water_mark`; clamping hides dropped events and breaks the client's cursor. The subtle case is generation mismatch with an in-range ID: this looks resumable and is not. Treat generation as a mandatory part of the replay precondition, not a diagnostic. ## Operational Effect Correctly enforcing the generation check converts a silent duplicate-delivery bug into an explicit `409`, which clients handle by re-initializing — cheap and idempotent. Failing to enforce it produces duplicated tool side effects that are hard to attribute, because the transport reports success and the event IDs are consistent. The invariant to encode: **replay validity is a function of (session, generation, ID range), never of ID range alone.** ```python class ResumptionTokenError(RuntimeError): """Raised when a Last-Event-ID replay request violates the replay invariant.""" pass class Session: def __init__(self, token: str, generation: int, replay_floor: int, high_water_mark: int): self.token = token self.generation = generation self.replay_floor = replay_floor self.high_water_mark = high_water_mark self.evicted = False def authorize_replay( session: Session | None, requested_generation: int, last_event_id: int, ) -> None: if session is None or session.evicted: raise ResumptionTokenError( f"session token unknown or evicted; client must re-initialize" ) if requested_generation != session.generation: raise ResumptionTokenError( f"generation mismatch: token generation {requested_generation} " f"!= live generation {session.generation}; refusing cross-generation replay" ) if last_event_id < session.replay_floor: raise ResumptionTokenError( f"Last-Event-ID {last_event_id} below replay floor {session.replay_floor}; " f"events compacted, force re-initialize" ) if last_event_id > session.high_water_mark: raise ResumptionTokenError( f"Last-Event-ID {last_event_id} ahead of high-water mark " f"{session.high_water_mark}; refusing to clamp" ) # valid: replay events (last_event_id, high_water_mark] under this generation ```