go-sdk/mcp/event.go
Jonathan Amsterdam a519c182e8
mcp: export Event (#145)
Export the Event type in preparation for providing user-definable
storage for resumable streams.

Also, move event code to a separate file.

This PR is just renaming and code motion.
2025-07-21 10:54:59 -04:00

138 lines
3.6 KiB
Go

// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// This file is for SSE events.
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events.
package mcp
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"iter"
"net/http"
"strings"
)
// An Event is a server-sent event.
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields.
type Event struct {
Name string // the "event" field
ID string // the "id" field
Data []byte // the "data" field
}
// Empty reports whether the Event is empty.
func (e Event) Empty() bool {
return e.Name == "" && e.ID == "" && len(e.Data) == 0
}
// writeEvent writes the event to w, and flushes.
func writeEvent(w io.Writer, evt Event) (int, error) {
var b bytes.Buffer
if evt.Name != "" {
fmt.Fprintf(&b, "event: %s\n", evt.Name)
}
if evt.ID != "" {
fmt.Fprintf(&b, "id: %s\n", evt.ID)
}
fmt.Fprintf(&b, "data: %s\n\n", string(evt.Data))
n, err := w.Write(b.Bytes())
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
return n, err
}
// scanEvents iterates SSE events in the given scanner. The iterated error is
// terminal: if encountered, the stream is corrupt or broken and should no
// longer be used.
//
// TODO(rfindley): consider a different API here that makes failure modes more
// apparent.
func scanEvents(r io.Reader) iter.Seq2[Event, error] {
scanner := bufio.NewScanner(r)
const maxTokenSize = 1 * 1024 * 1024 // 1 MiB max line size
scanner.Buffer(nil, maxTokenSize)
// TODO: investigate proper behavior when events are out of order, or have
// non-standard names.
var (
eventKey = []byte("event")
idKey = []byte("id")
dataKey = []byte("data")
)
return func(yield func(Event, error) bool) {
// iterate event from the wire.
// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#examples
//
// - `key: value` line records.
// - Consecutive `data: ...` fields are joined with newlines.
// - Unrecognized fields are ignored. Since we only care about 'event', 'id', and
// 'data', these are the only three we consider.
// - Lines starting with ":" are ignored.
// - Records are terminated with two consecutive newlines.
var (
evt Event
dataBuf *bytes.Buffer // if non-nil, preceding field was also data
)
flushData := func() {
if dataBuf != nil {
evt.Data = dataBuf.Bytes()
dataBuf = nil
}
}
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 {
flushData()
// \n\n is the record delimiter
if !evt.Empty() && !yield(evt, nil) {
return
}
evt = Event{}
continue
}
before, after, found := bytes.Cut(line, []byte{':'})
if !found {
yield(Event{}, fmt.Errorf("malformed line in SSE stream: %q", string(line)))
return
}
if !bytes.Equal(before, dataKey) {
flushData()
}
switch {
case bytes.Equal(before, eventKey):
evt.Name = strings.TrimSpace(string(after))
case bytes.Equal(before, idKey):
evt.ID = strings.TrimSpace(string(after))
case bytes.Equal(before, dataKey):
data := bytes.TrimSpace(after)
if dataBuf != nil {
dataBuf.WriteByte('\n')
dataBuf.Write(data)
} else {
dataBuf = new(bytes.Buffer)
dataBuf.Write(data)
}
}
}
if err := scanner.Err(); err != nil {
if errors.Is(err, bufio.ErrTooLong) {
err = fmt.Errorf("event exceeded max line length of %d", maxTokenSize)
}
if !yield(Event{}, err) {
return
}
}
flushData()
if !evt.Empty() {
yield(evt, nil)
}
}
}