55package io .modelcontextprotocol .client .transport ;
66
77import java .io .IOException ;
8+ import java .net .URI ;
89import java .util .List ;
10+ import java .util .concurrent .atomic .AtomicReference ;
911import java .util .function .BiConsumer ;
1012import java .util .function .Function ;
1113
@@ -128,7 +130,17 @@ public class WebFluxSseClientTransport implements McpClientTransport {
128130 * The SSE endpoint URI provided by the server. Used for sending outbound messages via
129131 * HTTP POST requests.
130132 */
131- private String sseEndpoint ;
133+ private final String sseEndpoint ;
134+
135+ /**
136+ * Used to capture the full SSE URI from the web client when connecting.
137+ */
138+ private final AtomicReference <URI > sseUri = new AtomicReference <>();
139+
140+ /**
141+ * Validator for the message endpoint.
142+ */
143+ private final SseMessageEndpointValidator messageEndpointValidator ;
132144
133145 /**
134146 * Constructs a new SseClientTransport with the specified WebClient builder and
@@ -152,13 +164,30 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapp
152164 * @throws IllegalArgumentException if either parameter is null
153165 */
154166 public WebFluxSseClientTransport (WebClient .Builder webClientBuilder , McpJsonMapper jsonMapper , String sseEndpoint ) {
167+ this (webClientBuilder , jsonMapper , sseEndpoint , new DefaultSseMessageEndpointValidator ());
168+ }
169+
170+ /**
171+ * Constructs a new SseClientTransport with the specified WebClient builder and
172+ * ObjectMapper. Initializes both inbound and outbound message processing pipelines.
173+ * @param webClientBuilder the WebClient.Builder to use for creating the WebClient
174+ * instance
175+ * @param jsonMapper the ObjectMapper to use for JSON processing
176+ * @param sseEndpoint the SSE endpoint URI to use for establishing the connection
177+ * @param messageEndpointValidator validator for the message endpoint
178+ * @throws IllegalArgumentException if either parameter is null
179+ */
180+ public WebFluxSseClientTransport (WebClient .Builder webClientBuilder , McpJsonMapper jsonMapper , String sseEndpoint ,
181+ SseMessageEndpointValidator messageEndpointValidator ) {
155182 Assert .notNull (jsonMapper , "jsonMapper must not be null" );
156183 Assert .notNull (webClientBuilder , "WebClient.Builder must not be null" );
184+ Assert .notNull (messageEndpointValidator , "messageEndpointValidator must not be null" );
157185 Assert .hasText (sseEndpoint , "SSE endpoint must not be null or empty" );
158186
159187 this .jsonMapper = jsonMapper ;
160188 this .webClient = webClientBuilder .build ();
161189 this .sseEndpoint = sseEndpoint ;
190+ this .messageEndpointValidator = messageEndpointValidator ;
162191 }
163192
164193 @ Override
@@ -195,6 +224,14 @@ public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> h
195224 this .inboundSubscription = events .concatMap (event -> Mono .just (event ).<JSONRPCMessage >handle ((e , s ) -> {
196225 if (ENDPOINT_EVENT_TYPE .equals (event .event ())) {
197226 String messageEndpointUri = event .data ();
227+ try {
228+ this .messageEndpointValidator .validate (this .sseUri .get (), messageEndpointUri );
229+ }
230+ catch (InvalidSseMessageEndpointException ex ) {
231+ messageEndpointSink .tryEmitError (ex );
232+ s .error (ex );
233+ return ;
234+ }
198235 if (messageEndpointSink .tryEmitValue (messageEndpointUri ).isSuccess ()) {
199236 s .complete ();
200237 }
@@ -276,16 +313,17 @@ public Mono<Void> sendMessage(JSONRPCMessage message) {
276313 * Includes automatic retry logic for handling transient connection failures.
277314 */
278315 // visible for tests
279- protected Flux <ServerSentEvent <String >> eventStream () {// @formatter:off
280- return this .webClient
281- .get ()
316+ protected Flux <ServerSentEvent <String >> eventStream () {
317+ return this .webClient .get ()
282318 .uri (this .sseEndpoint )
283319 .accept (MediaType .TEXT_EVENT_STREAM )
284320 .header (HttpHeaders .PROTOCOL_VERSION , MCP_PROTOCOL_VERSION )
285- .retrieve ()
286- .bodyToFlux (SSE_TYPE )
321+ .exchangeToFlux (exchange -> {
322+ this .sseUri .set (exchange .request ().getURI ());
323+ return exchange .bodyToFlux (SSE_TYPE );
324+ })
287325 .retryWhen (Retry .from (retrySignal -> retrySignal .handle (inboundRetryHandler )));
288- } // @formatter:on
326+ }
289327
290328 /**
291329 * Retry handler for the inbound SSE stream. Implements the retry logic for handling
@@ -368,6 +406,8 @@ public static class Builder {
368406
369407 private McpJsonMapper jsonMapper ;
370408
409+ private SseMessageEndpointValidator messageEndpointValidator = new DefaultSseMessageEndpointValidator ();
410+
371411 /**
372412 * Creates a new builder with the specified WebClient.Builder.
373413 * @param webClientBuilder the WebClient.Builder to use
@@ -399,13 +439,26 @@ public Builder jsonMapper(McpJsonMapper jsonMapper) {
399439 return this ;
400440 }
401441
442+ /**
443+ * Sets the validator that ensure the message endpoint returned over the SSE
444+ * connection is valid.
445+ * @param messageEndpointValidator the validator
446+ * @return this builder
447+ */
448+ public Builder messageEndpointValidator (SseMessageEndpointValidator messageEndpointValidator ) {
449+ Assert .notNull (messageEndpointValidator , "messageEndpointValidator must not be null" );
450+ this .messageEndpointValidator = messageEndpointValidator ;
451+ return this ;
452+ }
453+
402454 /**
403455 * Builds a new {@link WebFluxSseClientTransport} instance.
404456 * @return a new transport instance
405457 */
406458 public WebFluxSseClientTransport build () {
407459 return new WebFluxSseClientTransport (webClientBuilder ,
408- jsonMapper == null ? McpJsonDefaults .getMapper () : jsonMapper , sseEndpoint );
460+ jsonMapper == null ? McpJsonDefaults .getMapper () : jsonMapper , sseEndpoint ,
461+ messageEndpointValidator );
409462 }
410463
411464 }
0 commit comments