Skip to content

MemoryEventStore: panic "no progress during purge" when first chunk per stream is empty (SSE priming Append(nil)) #860

@shivas

Description

@shivas

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:

  1. Use NewMemoryEventStore with a small MaxBytes (e.g. SetMaxBytes(50) after creation).
  2. Open a session and stream ID.
  3. Append(nil) (same as the SDK’s priming append).
  4. Append a non-empty payload large enough that total stored bytes exceed MaxBytes (e.g. 100 bytes).
  5. Append again with more payload so purge() 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-sdk v1.4.1
  • Cause: mcp/event.go purge() only sets changed when r > 0 after removeFirst(); empty chunks do not increment nBytes but still occupy the front of dataList.
  • Related code: mcp/streamable.go — SSE priming path stores the priming event via Append(..., nil) when effectiveVersion >= protocolVersion20251125.
  • Possible fix: Treat any removeFirst() from a dataList with dl.size > 0 as 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 custom EventStore implementation until fixed.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions