@@ -10,14 +10,22 @@ package mcp
1010import (
1111 "bufio"
1212 "bytes"
13+ "context"
1314 "errors"
1415 "fmt"
1516 "io"
1617 "iter"
18+ "maps"
1719 "net/http"
20+ "slices"
1821 "strings"
22+ "sync"
1923)
2024
25+ // If true, MemoryEventStore will do frequent validation to check invariants, slowing it down.
26+ // Remove when we're confident in the code.
27+ const validateMemoryEventStore = true
28+
2129// An Event is a server-sent event.
2230// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields.
2331type Event struct {
@@ -136,3 +144,285 @@ func scanEvents(r io.Reader) iter.Seq2[Event, error] {
136144 }
137145 }
138146}
147+
148+ // An EventStore tracks data for SSE streams.
149+ // A single EventStore suffices for all sessions, since session IDs are
150+ // globally unique. So one EventStore can be created per process, for
151+ // all Servers in the process.
152+ // Such a store is able to bound resource usage for the entire process.
153+ //
154+ // All of an EventStore's methods must be safe for use by multiple goroutines.
155+ type EventStore interface {
156+ // AppendEvent appends data for an outgoing event to given stream, which is part of the
157+ // given session. It returns the index of the event in the stream, suitable for constructing
158+ // an event ID to send to the client.
159+ AppendEvent (_ context.Context , sessionID string , _ StreamID , data []byte ) (int , error )
160+
161+ // After returns an iterator over the data for the given session and stream, beginning
162+ // just after the given index.
163+ // Once the iterator yields a non-nil error, it will stop.
164+ // After's iterator must return an error immediately if any data after index was
165+ // dropped; it must not return partial results.
166+ After (_ context.Context , sessionID string , _ StreamID , index int ) iter.Seq2 [[]byte , error ]
167+
168+ // StreamClosed informs the store that the given stream is finished.
169+ // A store cannot rely on this method being called for cleanup. It should institute
170+ // additional mechanisms, such as timeouts, to reclaim storage.
171+ StreamClosed (_ context.Context , sessionID string , streamID StreamID ) error
172+
173+ // SessionClosed informs the store that the given session is finished, along
174+ // with all of its streams.
175+ // A store cannot rely on this method being called for cleanup. It should institute
176+ // additional mechanisms, such as timeouts, to reclaim storage.
177+ SessionClosed (_ context.Context , sessionID string ) error
178+ }
179+
180+ // A dataList is a list of []byte.
181+ // The zero dataList is ready to use.
182+ type dataList struct {
183+ size int // total size of data bytes
184+ first int // the stream index of the first element in data
185+ data [][]byte
186+ }
187+
188+ func (dl * dataList ) appendData (d []byte ) {
189+ // If we allowed empty data, we would consume memory without incrementing the size.
190+ // We could of course account for that, but we keep it simple and assume there is no
191+ // empty data.
192+ if len (d ) == 0 {
193+ panic ("empty data item" )
194+ }
195+ dl .data = append (dl .data , d )
196+ dl .size += len (d )
197+ }
198+
199+ // removeFirst removes the first data item in dl, returning the size of the item.
200+ // It panics if dl is empty.
201+ func (dl * dataList ) removeFirst () int {
202+ if len (dl .data ) == 0 {
203+ panic ("empty dataList" )
204+ }
205+ r := len (dl .data [0 ])
206+ dl .size -= r
207+ dl .data [0 ] = nil // help GC
208+ dl .data = dl .data [1 :]
209+ dl .first ++
210+ return r
211+ }
212+
213+ // lastIndex returns the index of the last data item in dl.
214+ // It panics if there are none.
215+ func (dl * dataList ) lastIndex () int {
216+ if len (dl .data ) == 0 {
217+ panic ("empty dataList" )
218+ }
219+ return dl .first + len (dl .data ) - 1
220+ }
221+
222+ // A MemoryEventStore is an [EventStore] backed by memory.
223+ type MemoryEventStore struct {
224+ mu sync.Mutex
225+ maxBytes int // max total size of all data
226+ nBytes int // current total size of all data
227+ store map [string ]map [StreamID ]* dataList // session ID -> stream ID -> *dataList
228+ }
229+
230+ // MemoryEventStoreOptions are options for a [MemoryEventStore].
231+ type MemoryEventStoreOptions struct {}
232+
233+ // MaxBytes returns the maximum number of bytes that the store will retain before
234+ // purging data.
235+ func (s * MemoryEventStore ) MaxBytes () int {
236+ s .mu .Lock ()
237+ defer s .mu .Unlock ()
238+ return s .maxBytes
239+ }
240+
241+ // SetMaxBytes sets the maximum number of bytes the store will retain before purging
242+ // data. The argument must not be negative. If it is zero, a suitable default will be used.
243+ // SetMaxBytes can be called at any time. The size of the store will be adjusted
244+ // immediately.
245+ func (s * MemoryEventStore ) SetMaxBytes (n int ) {
246+ s .mu .Lock ()
247+ defer s .mu .Unlock ()
248+ switch {
249+ case n < 0 :
250+ panic ("negative argument" )
251+ case n == 0 :
252+ s .maxBytes = defaultMaxBytes
253+ default :
254+ s .maxBytes = n
255+ }
256+ s .purge ()
257+ }
258+
259+ const defaultMaxBytes = 10 << 20 // 10 MiB
260+
261+ // NewMemoryEventStore creates a [MemoryEventStore] with the default value
262+ // for MaxBytes.
263+ func NewMemoryEventStore (opts * MemoryEventStoreOptions ) * MemoryEventStore {
264+ return & MemoryEventStore {
265+ maxBytes : defaultMaxBytes ,
266+ store : make (map [string ]map [StreamID ]* dataList ),
267+ }
268+ }
269+
270+ // AppendEvent implements [EventStore.AppendEvent] by recording data
271+ // in memory.
272+ func (s * MemoryEventStore ) AppendEvent (_ context.Context , sessionID string , streamID StreamID , data []byte ) (int , error ) {
273+ s .mu .Lock ()
274+ defer s .mu .Unlock ()
275+
276+ streamMap , ok := s .store [sessionID ]
277+ if ! ok {
278+ streamMap = make (map [StreamID ]* dataList )
279+ s .store [sessionID ] = streamMap
280+ }
281+ dl , ok := streamMap [streamID ]
282+ if ! ok {
283+ dl = & dataList {}
284+ streamMap [streamID ] = dl
285+ }
286+ // Purge before adding, so at least the current data item will be present.
287+ // (That could result in nBytes > maxBytes, but we'll live with that.)
288+ s .purge ()
289+ dl .appendData (data )
290+ s .nBytes += len (data )
291+ return dl .lastIndex (), nil
292+ }
293+
294+ // After implements [EventStore.After].
295+ func (s * MemoryEventStore ) After (_ context.Context , sessionID string , streamID StreamID , index int ) iter.Seq2 [[]byte , error ] {
296+ // Return the data items to yield.
297+ // We must copy, because dataList.removeFirst nils out slice elements.
298+ copyData := func () ([][]byte , error ) {
299+ s .mu .Lock ()
300+ defer s .mu .Unlock ()
301+ streamMap , ok := s .store [sessionID ]
302+ if ! ok {
303+ return nil , fmt .Errorf ("MemoryEventStore.After: unknown session ID %q" , sessionID )
304+ }
305+ dl , ok := streamMap [streamID ]
306+ if ! ok {
307+ return nil , fmt .Errorf ("MemoryEventStore.After: unknown stream ID %v in session %q" , streamID , sessionID )
308+ }
309+ if dl .first > index {
310+ return nil , fmt .Errorf ("MemoryEventStore.After: data purged at index %d, stream ID %v, session %q" , index , streamID , sessionID )
311+ }
312+ return slices .Clone (dl .data [index - dl .first :]), nil
313+ }
314+
315+ return func (yield func ([]byte , error ) bool ) {
316+ ds , err := copyData ()
317+ if err != nil {
318+ yield (nil , err )
319+ return
320+ }
321+ for _ , d := range ds {
322+ if ! yield (d , nil ) {
323+ return
324+ }
325+ }
326+ }
327+ }
328+
329+ // StreamClosed implements [EventStore.StreamClosed].
330+ func (s * MemoryEventStore ) StreamClosed (_ context.Context , sessionID string , streamID StreamID ) error {
331+ if sessionID == "" {
332+ panic ("empty sessionID" )
333+ }
334+
335+ s .mu .Lock ()
336+ defer s .mu .Unlock ()
337+
338+ sm := s .store [sessionID ]
339+ dl := sm [streamID ]
340+ s .nBytes -= dl .size
341+ delete (sm , streamID )
342+ if len (sm ) == 0 {
343+ delete (s .store , sessionID )
344+ }
345+ s .validate ()
346+ return nil
347+ }
348+
349+ // SessionClosed implements [EventStore.SessionClosed].
350+ func (s * MemoryEventStore ) SessionClosed (_ context.Context , sessionID string ) error {
351+ s .mu .Lock ()
352+ defer s .mu .Unlock ()
353+ for _ , dl := range s .store [sessionID ] {
354+ s .nBytes -= dl .size
355+ }
356+ delete (s .store , sessionID )
357+ s .validate ()
358+ return nil
359+ }
360+
361+ // purge removes data until no more than s.maxBytes bytes are in use.
362+ // It must be called with s.mu held.
363+ func (s * MemoryEventStore ) purge () {
364+ // Remove the first element of every dataList until below the max.
365+ for s .nBytes > s .maxBytes {
366+ changed := false
367+ for _ , sm := range s .store {
368+ for _ , dl := range sm {
369+ if dl .size > 0 {
370+ r := dl .removeFirst ()
371+ if r > 0 {
372+ changed = true
373+ s .nBytes -= r
374+ }
375+ }
376+ }
377+ }
378+ if ! changed {
379+ panic ("no progress during purge" )
380+ }
381+ }
382+ s .validate ()
383+ }
384+
385+ // validate checks that the store's data structures are valid.
386+ // It must be called with s.mu held.
387+ func (s * MemoryEventStore ) validate () {
388+ if ! validateMemoryEventStore {
389+ return
390+ }
391+ // Check that we're accounting for the size correctly.
392+ n := 0
393+ for _ , sm := range s .store {
394+ for _ , dl := range sm {
395+ for _ , d := range dl .data {
396+ n += len (d )
397+ }
398+ }
399+ }
400+ if n != s .nBytes {
401+ panic ("sizes don't add up" )
402+ }
403+ }
404+
405+ // debugString returns a string containing the state of s.
406+ // Used in tests.
407+ func (s * MemoryEventStore ) debugString () string {
408+ s .mu .Lock ()
409+ defer s .mu .Unlock ()
410+ var b strings.Builder
411+ for i , sess := range slices .Sorted (maps .Keys (s .store )) {
412+ if i > 0 {
413+ fmt .Fprintf (& b , "; " )
414+ }
415+ sm := s .store [sess ]
416+ for i , sid := range slices .Sorted (maps .Keys (sm )) {
417+ if i > 0 {
418+ fmt .Fprintf (& b , "; " )
419+ }
420+ dl := sm [sid ]
421+ fmt .Fprintf (& b , "%s %d first=%d" , sess , sid , dl .first )
422+ for _ , d := range dl .data {
423+ fmt .Fprintf (& b , " %s" , d )
424+ }
425+ }
426+ }
427+ return b .String ()
428+ }
0 commit comments