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.
138 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|