1818import io .modelcontextprotocol .spec .McpServerTransportProvider ;
1919import io .modelcontextprotocol .spec .McpServerSession ;
2020import io .modelcontextprotocol .util .Assert ;
21+ import io .modelcontextprotocol .util .KeepAliveScheduler ;
22+
2123import org .slf4j .Logger ;
2224import org .slf4j .LoggerFactory ;
2325import reactor .core .publisher .Flux ;
@@ -107,6 +109,8 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
107109 */
108110 private volatile boolean isClosing = false ;
109111
112+ private KeepAliveScheduler keepAliveScheduler ;
113+
110114 /**
111115 * Constructs a new WebMvcSseServerTransportProvider instance with the default SSE
112116 * endpoint.
@@ -116,7 +120,10 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
116120 * messages via HTTP POST. This endpoint will be communicated to clients through the
117121 * SSE connection's initial endpoint event.
118122 * @throws IllegalArgumentException if either objectMapper or messageEndpoint is null
123+ * @deprecated Use the builder {@link #builder()} instead for better configuration
124+ * options.
119125 */
126+ @ Deprecated
120127 public WebMvcSseServerTransportProvider (ObjectMapper objectMapper , String messageEndpoint ) {
121128 this (objectMapper , messageEndpoint , DEFAULT_SSE_ENDPOINT );
122129 }
@@ -130,7 +137,10 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messag
130137 * SSE connection's initial endpoint event.
131138 * @param sseEndpoint The endpoint URI where clients establish their SSE connections.
132139 * @throws IllegalArgumentException if any parameter is null
140+ * @deprecated Use the builder {@link #builder()} instead for better configuration
141+ * options.
133142 */
143+ @ Deprecated
134144 public WebMvcSseServerTransportProvider (ObjectMapper objectMapper , String messageEndpoint , String sseEndpoint ) {
135145 this (objectMapper , "" , messageEndpoint , sseEndpoint );
136146 }
@@ -146,9 +156,33 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messag
146156 * SSE connection's initial endpoint event.
147157 * @param sseEndpoint The endpoint URI where clients establish their SSE connections.
148158 * @throws IllegalArgumentException if any parameter is null
159+ * @deprecated Use the builder {@link #builder()} instead for better configuration
160+ * options.
149161 */
162+ @ Deprecated
150163 public WebMvcSseServerTransportProvider (ObjectMapper objectMapper , String baseUrl , String messageEndpoint ,
151164 String sseEndpoint ) {
165+ this (objectMapper , baseUrl , messageEndpoint , sseEndpoint , null );
166+ }
167+
168+ /**
169+ * Constructs a new WebMvcSseServerTransportProvider instance.
170+ * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
171+ * of messages.
172+ * @param baseUrl The base URL for the message endpoint, used to construct the full
173+ * endpoint URL for clients.
174+ * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
175+ * messages via HTTP POST. This endpoint will be communicated to clients through the
176+ * SSE connection's initial endpoint event.
177+ * @param sseEndpoint The endpoint URI where clients establish their SSE connections.
178+ * * @param keepAliveInterval The interval for sending keep-alive messages to
179+ * @throws IllegalArgumentException if any parameter is null
180+ * @deprecated Use the builder {@link #builder()} instead for better configuration
181+ * options.
182+ */
183+ @ Deprecated
184+ public WebMvcSseServerTransportProvider (ObjectMapper objectMapper , String baseUrl , String messageEndpoint ,
185+ String sseEndpoint , Duration keepAliveInterval ) {
152186 Assert .notNull (objectMapper , "ObjectMapper must not be null" );
153187 Assert .notNull (baseUrl , "Message base URL must not be null" );
154188 Assert .notNull (messageEndpoint , "Message endpoint must not be null" );
@@ -162,6 +196,17 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUr
162196 .GET (this .sseEndpoint , this ::handleSseConnection )
163197 .POST (this .messageEndpoint , this ::handleMessage )
164198 .build ();
199+
200+ if (keepAliveInterval != null ) {
201+
202+ this .keepAliveScheduler = KeepAliveScheduler
203+ .builder (() -> (isClosing ) ? Flux .empty () : Flux .fromIterable (sessions .values ()))
204+ .initialDelay (keepAliveInterval )
205+ .interval (keepAliveInterval )
206+ .build ();
207+
208+ this .keepAliveScheduler .start ();
209+ }
165210 }
166211
167212 @ Override
@@ -209,10 +254,13 @@ public Mono<Void> closeGracefully() {
209254 return Flux .fromIterable (sessions .values ()).doFirst (() -> {
210255 this .isClosing = true ;
211256 logger .debug ("Initiating graceful shutdown with {} active sessions" , sessions .size ());
212- })
213- .flatMap (McpServerSession ::closeGracefully )
214- .then ()
215- .doOnSuccess (v -> logger .debug ("Graceful shutdown completed" ));
257+ }).flatMap (McpServerSession ::closeGracefully ).then ().doOnSuccess (v -> {
258+ logger .debug ("Graceful shutdown completed" );
259+ sessions .clear ();
260+ if (this .keepAliveScheduler != null ) {
261+ this .keepAliveScheduler .shutdown ();
262+ }
263+ });
216264 }
217265
218266 /**
@@ -435,4 +483,106 @@ public void close() {
435483
436484 }
437485
486+ /**
487+ * Creates a new Builder instance for configuring and creating instances of
488+ * WebMvcSseServerTransportProvider.
489+ * @return A new Builder instance
490+ */
491+ public static Builder builder () {
492+ return new Builder ();
493+ }
494+
495+ /**
496+ * Builder for creating instances of WebMvcSseServerTransportProvider.
497+ * <p>
498+ * This builder provides a fluent API for configuring and creating instances of
499+ * WebMvcSseServerTransportProvider with custom settings.
500+ */
501+ public static class Builder {
502+
503+ private ObjectMapper objectMapper = new ObjectMapper ();
504+
505+ private String baseUrl = "" ;
506+
507+ private String messageEndpoint ;
508+
509+ private String sseEndpoint = DEFAULT_SSE_ENDPOINT ;
510+
511+ private Duration keepAliveInterval ;
512+
513+ /**
514+ * Sets the JSON object mapper to use for message serialization/deserialization.
515+ * @param objectMapper The object mapper to use
516+ * @return This builder instance for method chaining
517+ */
518+ public Builder objectMapper (ObjectMapper objectMapper ) {
519+ Assert .notNull (objectMapper , "ObjectMapper must not be null" );
520+ this .objectMapper = objectMapper ;
521+ return this ;
522+ }
523+
524+ /**
525+ * Sets the base URL for the server transport.
526+ * @param baseUrl The base URL to use
527+ * @return This builder instance for method chaining
528+ */
529+ public Builder baseUrl (String baseUrl ) {
530+ Assert .notNull (baseUrl , "Base URL must not be null" );
531+ this .baseUrl = baseUrl ;
532+ return this ;
533+ }
534+
535+ /**
536+ * Sets the endpoint path where clients will send their messages.
537+ * @param messageEndpoint The message endpoint path
538+ * @return This builder instance for method chaining
539+ */
540+ public Builder messageEndpoint (String messageEndpoint ) {
541+ Assert .hasText (messageEndpoint , "Message endpoint must not be empty" );
542+ this .messageEndpoint = messageEndpoint ;
543+ return this ;
544+ }
545+
546+ /**
547+ * Sets the endpoint path where clients will establish SSE connections.
548+ * <p>
549+ * If not specified, the default value of {@link #DEFAULT_SSE_ENDPOINT} will be
550+ * used.
551+ * @param sseEndpoint The SSE endpoint path
552+ * @return This builder instance for method chaining
553+ */
554+ public Builder sseEndpoint (String sseEndpoint ) {
555+ Assert .hasText (sseEndpoint , "SSE endpoint must not be empty" );
556+ this .sseEndpoint = sseEndpoint ;
557+ return this ;
558+ }
559+
560+ /**
561+ * Sets the interval for keep-alive pings.
562+ * <p>
563+ * If not specified, keep-alive pings will be disabled.
564+ * @param keepAliveInterval The interval duration for keep-alive pings
565+ * @return This builder instance for method chaining
566+ */
567+ public Builder keepAliveInterval (Duration keepAliveInterval ) {
568+ this .keepAliveInterval = keepAliveInterval ;
569+ return this ;
570+ }
571+
572+ /**
573+ * Builds a new instance of WebMvcSseServerTransportProvider with the configured
574+ * settings.
575+ * @return A new WebMvcSseServerTransportProvider instance
576+ * @throws IllegalStateException if objectMapper or messageEndpoint is not set
577+ */
578+ public WebMvcSseServerTransportProvider build () {
579+ if (messageEndpoint == null ) {
580+ throw new IllegalStateException ("MessageEndpoint must be set" );
581+ }
582+ return new WebMvcSseServerTransportProvider (objectMapper , baseUrl , messageEndpoint , sseEndpoint ,
583+ keepAliveInterval );
584+ }
585+
586+ }
587+
438588}
0 commit comments