@@ -300,6 +300,27 @@ func (c *Client) Connect(ctx context.Context, t Transport, opts *ClientSessionOp
300300 if hc , ok := cs .mcpConn .(clientConnection ); ok {
301301 hc .sessionUpdated (cs .state )
302302 }
303+ subscribeParams := & SubscriptionsListenParams {}
304+ if c .opts .ToolListChangedHandler != nil {
305+ subscribeParams .Notifications .ToolsListChanged = true
306+ }
307+ if c .opts .PromptListChangedHandler != nil {
308+ subscribeParams .Notifications .PromptsListChanged = true
309+ }
310+ if c .opts .ResourceListChangedHandler != nil {
311+ subscribeParams .Notifications .ResourcesListChanged = true
312+ }
313+ if subscribeParams .Notifications .ToolsListChanged ||
314+ subscribeParams .Notifications .PromptsListChanged ||
315+ subscribeParams .Notifications .ResourcesListChanged {
316+ // ClientSession.Close cancels the listenCtx context to send notifications/cancelled.
317+ listenCtx , cancelListen := context .WithCancel (context .Background ())
318+ cs .listenCancel = cancelListen
319+ if err := cs .subscriptionsListen (listenCtx , subscribeParams ); err != nil {
320+ cancelListen ()
321+ return nil , fmt .Errorf ("opening subscriptions/listen: %w" , err )
322+ }
323+ }
303324 return cs , nil
304325 }
305326
@@ -414,6 +435,7 @@ type ClientSession struct {
414435 conn * jsonrpc2.Connection
415436 client * Client
416437 keepaliveCancel context.CancelFunc
438+ listenCancel context.CancelFunc
417439 mcpConn Connection
418440
419441 // No mutex is (currently) required to guard the session state, because it is
@@ -430,6 +452,15 @@ type ClientSession struct {
430452 // Pending URL elicitations waiting for completion notifications.
431453 pendingElicitationsMu sync.Mutex
432454 pendingElicitations map [string ]chan struct {}
455+
456+ // resourceSubsMu guards resourceSubs.
457+ resourceSubsMu sync.Mutex
458+ // resourceSubs maps a subscribed resource URI to the cancel func of the
459+ // goroutine running its dedicated subscriptions/listen stream. Populated
460+ // only under SEP-2575; the legacy protocol routes Subscribe and
461+ // Unsubscribe straight to the resources/subscribe and resources/unsubscribe
462+ // RPCs and leaves this map untouched.
463+ resourceSubs map [string ]context.CancelFunc
433464}
434465
435466type clientSessionState struct {
@@ -496,6 +527,10 @@ func (cs *ClientSession) Close() error {
496527 if cs .keepaliveCancel != nil {
497528 cs .keepaliveCancel ()
498529 }
530+ if cs .listenCancel != nil {
531+ cs .listenCancel ()
532+ }
533+ cs .cancelAllResourceSubscriptions ()
499534 err := cs .conn .Close ()
500535
501536 if cs .onClose != nil && cs .calledOnClose .CompareAndSwap (false , true ) {
@@ -1083,6 +1118,7 @@ var clientMethodInfos = map[string]methodInfo{
10831118 notificationLoggingMessage : newClientMethodInfo (clientMethod ((* Client ).callLoggingHandler ), notification ),
10841119 notificationProgress : newClientMethodInfo (clientSessionMethod ((* ClientSession ).callProgressNotificationHandler ), notification ),
10851120 notificationElicitationComplete : newClientMethodInfo (clientMethod ((* Client ).callElicitationCompleteHandler ), notification | missingParamsOK ),
1121+ notificationSubscriptionsAck : newClientMethodInfo (clientMethod ((* Client ).callSubscriptionsAckHandler ), notification | missingParamsOK ),
10861122}
10871123
10881124func (cs * ClientSession ) sendingMethodInfos () map [string ]methodInfo {
@@ -1279,17 +1315,91 @@ func (cs *ClientSession) Complete(ctx context.Context, params *CompleteParams) (
12791315// Subscribe sends a "resources/subscribe" request to the server, asking for
12801316// notifications when the specified resource changes.
12811317func (cs * ClientSession ) Subscribe (ctx context.Context , params * SubscribeParams ) error {
1282- _ , err := handleSend [* emptyResult ](ctx , methodSubscribe , newClientRequest (cs , orZero [Params ](params )))
1283- return err
1318+ if ! cs .usesNewProtocol () {
1319+ _ , err := handleSend [* emptyResult ](ctx , methodSubscribe , newClientRequest (cs , orZero [Params ](params )))
1320+ return err
1321+ }
1322+ if params == nil || params .URI == "" {
1323+ return fmt .Errorf ("Subscribe: missing URI" )
1324+ }
1325+ uri := params .URI
1326+
1327+ var listenCtx context.Context
1328+ cs .resourceSubsMu .Lock ()
1329+ if _ , exists := cs .resourceSubs [uri ]; ! exists {
1330+ var cancel context.CancelFunc
1331+ listenCtx , cancel = context .WithCancel (context .Background ())
1332+ if cs .resourceSubs == nil {
1333+ cs .resourceSubs = make (map [string ]context.CancelFunc )
1334+ }
1335+ cs .resourceSubs [uri ] = cancel
1336+ }
1337+ cs .resourceSubsMu .Unlock ()
1338+ if listenCtx == nil {
1339+ // Already subscribed to this URI
1340+ return nil
1341+ }
1342+
1343+ return cs .subscriptionsListen (listenCtx , & SubscriptionsListenParams {
1344+ Notifications : NotificationSubscriptions {
1345+ ResourceSubscriptions : []string {uri },
1346+ },
1347+ })
12841348}
12851349
1286- // Unsubscribe sends a "resources/unsubscribe" request to the server, cancelling
1287- // a previous subscription.
1350+ // Unsubscribe cancels a previous [ClientSession.Subscribe] for params.URI.
1351+ //
1352+ // Under the legacy protocol it sends a "resources/unsubscribe" request.
1353+ //
1354+ // Under SEP-2575 it cancels the background "subscriptions/listen" stream
1355+ // opened by Subscribe for the URI. Unsubscribe is idempotent: calling it for
1356+ // a URI that is not currently subscribed is a no-op.
12881357func (cs * ClientSession ) Unsubscribe (ctx context.Context , params * UnsubscribeParams ) error {
1289- _ , err := handleSend [* emptyResult ](ctx , methodUnsubscribe , newClientRequest (cs , orZero [Params ](params )))
1358+ if ! cs .usesNewProtocol () {
1359+ _ , err := handleSend [* emptyResult ](ctx , methodUnsubscribe , newClientRequest (cs , orZero [Params ](params )))
1360+ return err
1361+ }
1362+ if params == nil || params .URI == "" {
1363+ return fmt .Errorf ("Unsubscribe: missing URI" )
1364+ }
1365+ cs .resourceSubsMu .Lock ()
1366+ cancel , ok := cs .resourceSubs [params .URI ]
1367+ delete (cs .resourceSubs , params .URI )
1368+ cs .resourceSubsMu .Unlock ()
1369+ if ok {
1370+ cancel ()
1371+ }
1372+ return nil
1373+ }
1374+
1375+ // cancelAllResourceSubscriptions cancels every active SEP-2575 resource
1376+ // subscription opened via Subscribe. The listen goroutines exit
1377+ // asynchronously as their contexts unwind. Called from Close.
1378+ func (cs * ClientSession ) cancelAllResourceSubscriptions () {
1379+ cs .resourceSubsMu .Lock ()
1380+ subs := cs .resourceSubs
1381+ cs .resourceSubs = nil
1382+ cs .resourceSubsMu .Unlock ()
1383+ for _ , cancel := range subs {
1384+ cancel ()
1385+ }
1386+ }
1387+
1388+ // SubscriptionsListen opens a SEP-2575 "subscriptions/listen" stream.
1389+ //
1390+ // The server's first message on the stream is "notifications/subscriptions/acknowledged";
1391+ // subsequent opted-in notifications (e.g. tools/list_changed) are delivered through the
1392+ // usual handlers registered in [ClientOptions].
1393+ func (cs * ClientSession ) subscriptionsListen (ctx context.Context , params * SubscriptionsListenParams ) error {
1394+ params = injectRequestMeta (cs , params )
1395+ _ , err := handleSend [* emptyResult ](ctx , methodSubscriptionsListen , newClientRequest (cs , orZero [Params ](params )))
12901396 return err
12911397}
12921398
1399+ func (c * Client ) callSubscriptionsAckHandler (context.Context , * ClientRequest [* SubscriptionsAcknowledgedParams ]) (Result , error ) {
1400+ return nil , nil
1401+ }
1402+
12931403func (c * Client ) callToolChangedHandler (ctx context.Context , req * ToolListChangedRequest ) (Result , error ) {
12941404 if cs , ok := req .GetSession ().(* ClientSession ); ok {
12951405 cs .toolsCache .invalidate ()
0 commit comments