-
Notifications
You must be signed in to change notification settings - Fork 389
Description
Describe the bug
MemoryEventStore.purge() can panic with panic: no progress during purge when the store is over MaxBytes and a purge pass only removes leading empty chunks ([]byte / nil with length 0). Purge treats progress only when removeFirst() returns r > 0, so if every stream’s first chunk is empty (but later chunks hold real payload), one pass removes only those empty chunks with r == 0, changed stays false, and the code panics.
That situation is not hypothetical: for SSE with an EventStore and protocol ≥ 2025-11-25, streamableServerConn calls Append(..., nil) for the priming event so indexes match the wire, which inserts a zero-length first chunk per stream. Under memory pressure, purge can hit this path.
To Reproduce
Steps to reproduce the behavior:
- Use
NewMemoryEventStorewith a smallMaxBytes(e.g.SetMaxBytes(50)after creation). Opena session and stream ID.Append(nil)(same as the SDK’s priming append).Appenda non-empty payload large enough that total stored bytes exceedMaxBytes(e.g. 100 bytes).Appendagain with more payload sopurge()runs while the list still looks like[nil, …payload…]with the empty chunk first.
The last Append can trigger the panic during purge() inside Append.
Expected behavior
Purge should evict data until nBytes <= MaxBytes (or until no evictable data remains) without panicking. Removing a leading empty chunk should count as progress so the next iteration can evict non-empty bytes.
Logs
Example panic / goroutine header:
panic: no progress during purge
goroutine ... [running]:
github.com/modelcontextprotocol/go-sdk/mcp.(*MemoryEventStore).purge(...)
github.com/modelcontextprotocol/go-sdk/mcp.(*MemoryEventStore).Append(...)
github.com/modelcontextprotocol/go-sdk/mcp.(*streamableServerConn).Write(...)
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2.(*Connection).write(...)
Additional context
- Version: e.g.
github.com/modelcontextprotocol/go-sdkv1.4.1 - Cause:
mcp/event.gopurge()only setschangedwhenr > 0afterremoveFirst(); empty chunks do not incrementnBytesbut still occupy the front ofdataList. - Related code:
mcp/streamable.go— SSE priming path stores the priming event viaAppend(..., nil)wheneffectiveVersion >= protocolVersion20251125. - Possible fix: Treat any
removeFirst()from adataListwithdl.size > 0as progress, or strip leading empty chunks in a loop until a non-empty eviction or empty list. - Workaround for callers: raise
MaxBytes, or use a customEventStoreimplementation until fixed.