55package io .modelcontextprotocol .server ;
66
77import java .time .Duration ;
8+ import java .util .Collections ;
89import java .util .HashMap ;
910import java .util .List ;
1011import java .util .Map ;
1112import java .util .Optional ;
13+ import java .util .Set ;
1214import java .util .UUID ;
1315import java .util .concurrent .ConcurrentHashMap ;
1416import java .util .concurrent .CopyOnWriteArrayList ;
2527import io .modelcontextprotocol .spec .McpSchema .CompleteResult .CompleteCompletion ;
2628import io .modelcontextprotocol .spec .McpSchema .ErrorCodes ;
2729import io .modelcontextprotocol .spec .McpSchema .LoggingLevel ;
28- import io .modelcontextprotocol .spec .McpSchema .LoggingMessageNotification ;
2930import io .modelcontextprotocol .spec .McpSchema .PromptReference ;
3031import io .modelcontextprotocol .spec .McpSchema .ResourceReference ;
3132import io .modelcontextprotocol .spec .McpSchema .SetLevelRequest ;
@@ -111,12 +112,10 @@ public class McpAsyncServer {
111112
112113 private final ConcurrentHashMap <String , McpServerFeatures .AsyncPromptSpecification > prompts = new ConcurrentHashMap <>();
113114
114- // FIXME: this field is deprecated and should be remvoed together with the
115- // broadcasting loggingNotification.
116- private LoggingLevel minLoggingLevel = LoggingLevel .DEBUG ;
117-
118115 private final ConcurrentHashMap <McpSchema .CompleteReference , McpServerFeatures .AsyncCompletionSpecification > completions = new ConcurrentHashMap <>();
119116
117+ private final ConcurrentHashMap <String , Set <String >> resourceSubscriptions = new ConcurrentHashMap <>();
118+
120119 private List <String > protocolVersions ;
121120
122121 private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory ();
@@ -149,8 +148,11 @@ public class McpAsyncServer {
149148
150149 this .protocolVersions = mcpTransportProvider .protocolVersions ();
151150
152- mcpTransportProvider .setSessionFactory (transport -> new McpServerSession (UUID .randomUUID ().toString (),
153- requestTimeout , transport , this ::asyncInitializeRequestHandler , requestHandlers , notificationHandlers ));
151+ mcpTransportProvider .setSessionFactory (transport -> {
152+ String sessionId = UUID .randomUUID ().toString ();
153+ return new McpServerSession (sessionId , requestTimeout , transport , this ::asyncInitializeRequestHandler ,
154+ requestHandlers , notificationHandlers , () -> this .cleanupForSession (sessionId ));
155+ });
154156 }
155157
156158 McpAsyncServer (McpStreamableServerTransportProvider mcpTransportProvider , McpJsonMapper jsonMapper ,
@@ -174,8 +176,9 @@ public class McpAsyncServer {
174176
175177 this .protocolVersions = mcpTransportProvider .protocolVersions ();
176178
177- mcpTransportProvider .setSessionFactory (new DefaultMcpStreamableServerSessionFactory (requestTimeout ,
178- this ::asyncInitializeRequestHandler , requestHandlers , notificationHandlers ));
179+ mcpTransportProvider .setSessionFactory (
180+ new DefaultMcpStreamableServerSessionFactory (requestTimeout , this ::asyncInitializeRequestHandler ,
181+ requestHandlers , notificationHandlers , sessionId -> this .cleanupForSession (sessionId )));
179182 }
180183
181184 private Map <String , McpNotificationHandler > prepareNotificationHandlers (McpServerFeatures .Async features ) {
@@ -215,6 +218,10 @@ private Map<String, McpRequestHandler<?>> prepareRequestHandlers() {
215218 requestHandlers .put (McpSchema .METHOD_RESOURCES_LIST , resourcesListRequestHandler ());
216219 requestHandlers .put (McpSchema .METHOD_RESOURCES_READ , resourcesReadRequestHandler ());
217220 requestHandlers .put (McpSchema .METHOD_RESOURCES_TEMPLATES_LIST , resourceTemplateListRequestHandler ());
221+ if (Boolean .TRUE .equals (this .serverCapabilities .resources ().subscribe ())) {
222+ requestHandlers .put (McpSchema .METHOD_RESOURCES_SUBSCRIBE , resourcesSubscribeRequestHandler ());
223+ requestHandlers .put (McpSchema .METHOD_RESOURCES_UNSUBSCRIBE , resourcesUnsubscribeRequestHandler ());
224+ }
218225 }
219226
220227 // Add prompts API handlers if provider exists
@@ -685,12 +692,73 @@ public Mono<Void> notifyResourcesListChanged() {
685692 }
686693
687694 /**
688- * Notifies clients that the resources have updated.
689- * @return A Mono that completes when all clients have been notified
695+ * Notifies only the sessions that have subscribed to the updated resource URI.
696+ * @param resourcesUpdatedNotification the notification containing the updated
697+ * resource URI
698+ * @return A Mono that completes when all subscribed sessions have been notified
690699 */
691700 public Mono <Void > notifyResourcesUpdated (McpSchema .ResourcesUpdatedNotification resourcesUpdatedNotification ) {
692- return this .mcpTransportProvider .notifyClients (McpSchema .METHOD_NOTIFICATION_RESOURCES_UPDATED ,
693- resourcesUpdatedNotification );
701+ return Mono .defer (() -> {
702+ String uri = resourcesUpdatedNotification .uri ();
703+ Set <String > subscribedSessions = this .resourceSubscriptions .get (uri );
704+ if (subscribedSessions == null || subscribedSessions .isEmpty ()) {
705+ logger .debug ("No sessions subscribed to resource URI: {}" , uri );
706+ return Mono .empty ();
707+ }
708+ return Flux .fromIterable (subscribedSessions )
709+ .flatMap (sessionId -> this .mcpTransportProvider
710+ .notifyClient (sessionId , McpSchema .METHOD_NOTIFICATION_RESOURCES_UPDATED ,
711+ resourcesUpdatedNotification )
712+ .doOnError (e -> logger .error ("Failed to notify session {} of resource update for {}" , sessionId ,
713+ uri , e ))
714+ .onErrorComplete ())
715+ .then ();
716+ });
717+ }
718+
719+ private Mono <Void > cleanupForSession (String sessionId ) {
720+ return Mono .fromRunnable (() -> {
721+ removeSessionSubscriptions (sessionId );
722+ });
723+ }
724+
725+ private void removeSessionSubscriptions (String sessionId ) {
726+ this .resourceSubscriptions .forEach ((uri , sessions ) -> sessions .remove (sessionId ));
727+ this .resourceSubscriptions .entrySet ().removeIf (entry -> entry .getValue ().isEmpty ());
728+ }
729+
730+ private McpRequestHandler <Object > resourcesSubscribeRequestHandler () {
731+ return (exchange , params ) -> Mono .defer (() -> {
732+ McpSchema .SubscribeRequest subscribeRequest = jsonMapper .convertValue (params ,
733+ new TypeRef <McpSchema .SubscribeRequest >() {
734+ });
735+ String uri = subscribeRequest .uri ();
736+ String sessionId = exchange .sessionId ();
737+ this .resourceSubscriptions .computeIfAbsent (uri , k -> Collections .newSetFromMap (new ConcurrentHashMap <>()))
738+ .add (sessionId );
739+ logger .debug ("Session {} subscribed to resource URI: {}" , sessionId , uri );
740+
741+ return Mono .just (Map .of ());
742+ });
743+ }
744+
745+ private McpRequestHandler <Object > resourcesUnsubscribeRequestHandler () {
746+ return (exchange , params ) -> Mono .defer (() -> {
747+ McpSchema .UnsubscribeRequest unsubscribeRequest = jsonMapper .convertValue (params ,
748+ new TypeRef <McpSchema .UnsubscribeRequest >() {
749+ });
750+ String uri = unsubscribeRequest .uri ();
751+ String sessionId = exchange .sessionId ();
752+ Set <String > sessions = this .resourceSubscriptions .get (uri );
753+ if (sessions != null ) {
754+ sessions .remove (sessionId );
755+ if (sessions .isEmpty ()) {
756+ this .resourceSubscriptions .remove (uri , sessions );
757+ }
758+ }
759+ logger .debug ("Session {} unsubscribed from resource URI: {}" , sessionId , uri );
760+ return Mono .just (Map .of ());
761+ });
694762 }
695763
696764 private McpRequestHandler <McpSchema .ListResourcesResult > resourcesListRequestHandler () {
@@ -878,10 +946,6 @@ private McpRequestHandler<Object> setLoggerRequestHandler() {
878946
879947 exchange .setMinLoggingLevel (newMinLoggingLevel .level ());
880948
881- // FIXME: this field is deprecated and should be removed together
882- // with the broadcasting loggingNotification.
883- this .minLoggingLevel = newMinLoggingLevel .level ();
884-
885949 return Mono .just (Map .of ());
886950 });
887951 };
0 commit comments