mcp: Implement subscriptions/listen rpc (SEP-2575) (#1007)

Fixes #966
This commit is contained in:
Guglielmo Colombo 2026-06-22 18:57:36 +02:00 • committed by GitHub
commit cd7d80c591
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -208,12 +208,30 @@ type ConnectionConfig struct {
Bind func(*Connection) Handler // required
OnDone func() // optional
OnInternalError func(error) // optional
// PropagateCancellation controls whether cancellation of the context
// passed to [NewConnection] is observable by request handlers.
//
// By default (false), the connection wraps that context (see [notDone])
// so handlers' Done channels do not fire when the connection's root
// context is cancelled. Cancellation of an in-flight handler is then
// expected to flow only through the jsonrpc2 layer's explicit channels
// (the [Preempter] reacting to the peer's cancel notification, or a
// transport read/write failure cancelling every in-flight request).
//
// Set this true when cancellation of the connection's context is itself
// a meaningful signal that handlers should react to (for example, when
// the connection is tied to a carrier that owns it and whose end means
// the request is cancelled).
PropagateCancellation bool // optional
}
// NewConnection creates a new [Connection] object and starts processing
// incoming messages.
func NewConnection(ctx context.Context, cfg ConnectionConfig) *Connection {
ctx = notDone{ctx}
if !cfg.PropagateCancellation {
ctx = notDone{ctx}
}
c := &Connection{
state: inFlightState{closer: cfg.Closer},
@ -530,6 +548,14 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter
ac.retire(&Response{ID: id, Error: err})
}
s.outgoingCalls = nil
// Cancel any incoming requests still in flight: with the reader gone we
// cannot receive cancellation notifications, and likely cannot write a
// response either, so parked handlers have nothing useful left to do.
// Mirrors the equivalent cleanup on write failure.
for _, r := range s.incomingByID {
r.cancel()
}
})
}
@ -724,9 +750,12 @@ func (c *Connection) write(ctx context.Context, msg Message) error {
var err error
// Fail writes immediately if the connection is shutting down.
//
// TODO(rfindley): should we allow cancellation notifications through? It
// could be the case that writes can still succeed.
// Allow outgoing "notifications" forwarded by the Notify method.
// This will allow to send the cancelled notification when the client is shutting down.
c.updateInFlight(func(s *inFlightState) {
if req, ok := msg.(*Request); ok && !req.IsCall() && s.outgoingNotifications > 0 {
return
}
err = s.shuttingDown(ErrServerClosing)
})
if err == nil {
@ -771,6 +800,15 @@ func (c *Connection) internalErrorf(format string, args ...any) error {
}
// notDone is a context.Context wrapper that returns a nil Done channel.
//
// Request handlers' contexts are derived from the connection's root context,
// which by default is wrapped in notDone so a transport-level cancellation
// does not implicitly cancel every in-flight handler. Cancellation of an
// in-flight handler is instead expected to flow only through the jsonrpc2
// layer's explicit channels: the [Preempter] calling [Connection.Cancel] in
// response to the peer's cancel notification, or the transport itself
// failing (the read loop exits on EOF or a write fails) — both of which
// cancel every in-flight incoming request in turn.
type notDone struct{ ctx context.Context }
func (ic notDone) Value(key any) any {