feat: Make SSE endpoint configurable in server transports

This commit updates the WebFluxSseServerTransport, WebMvcSseServerTransport, and HttpServletSseServerTransport classes to accept the SSE endpoint path as a constructor argument.

This change provides more flexibility in configuring the SSE endpoint, allowing it to be customized instead of being hardcoded to "/sse".
It also aligns the Java SDK's behavior with the Python SDK's SseServerTransport, which already supports this customization.

A default SSE endpoint of "/sse" is maintained for backward compatibility.

Also, addressed minor code quality issues identified by SonarLint and reordered modifiers for static final fields to conform with the Java Language Specification.

Signed-off-by: Alexandros Pappas <alexandros.pappas@yiluhub.com>
This commit is contained in:
Alexandros Pappas 2025-01-16 15:28:35 +01:00 • committed by Christian Tzolov
commit c2ece20cc2
2 changed files with 111 additions and 58 deletions

View file

@ -72,31 +72,31 @@ import org.springframework.web.reactive.function.client.WebClient;
*/
public class WebFluxSseClientTransport implements ClientMcpTransport {
private final static Logger logger = LoggerFactory.getLogger(WebFluxSseClientTransport.class);
private static final Logger logger = LoggerFactory.getLogger(WebFluxSseClientTransport.class);
/**
* Event type for JSON-RPC messages received through the SSE connection. The server
* sends messages with this event type to transmit JSON-RPC protocol data.
*/
private final static String MESSAGE_EVENT_TYPE = "message";
private static final String MESSAGE_EVENT_TYPE = "message";
/**
* Event type for receiving the message endpoint URI from the server. The server MUST
* send this event when a client connects, providing the URI where the client should
* send its messages via HTTP POST.
*/
private final static String ENDPOINT_EVENT_TYPE = "endpoint";
private static final String ENDPOINT_EVENT_TYPE = "endpoint";
/**
* Default SSE endpoint path as specified by the MCP transport specification. This
* endpoint is used to establish the SSE connection with the server.
*/
private final static String SSE_ENDPOINT = "/sse";
private static final String SSE_ENDPOINT = "/sse";
/**
* Type reference for parsing SSE events containing string data.
*/
private final static ParameterizedTypeReference<ServerSentEvent<String>> SSE_TYPE = new ParameterizedTypeReference<>() {
private static final ParameterizedTypeReference<ServerSentEvent<String>> SSE_TYPE = new ParameterizedTypeReference<>() {
};
/**

View file

@ -57,32 +57,35 @@ import org.springframework.web.reactive.function.server.ServerResponse;
* {@link Sinks} for thread-safe message broadcasting.
*
* @author Christian Tzolov
* @author Alexandros Pappas
* @see ServerMcpTransport
* @see ServerSentEvent
*/
public class WebFluxSseServerTransport implements ServerMcpTransport {
private final static Logger logger = LoggerFactory.getLogger(WebFluxSseServerTransport.class);
private static final Logger logger = LoggerFactory.getLogger(WebFluxSseServerTransport.class);
/**
* Event type for JSON-RPC messages sent through the SSE connection.
*/
public final static String MESSAGE_EVENT_TYPE = "message";
public static final String MESSAGE_EVENT_TYPE = "message";
/**
* Event type for sending the message endpoint URI to clients.
*/
public final static String ENDPOINT_EVENT_TYPE = "endpoint";
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
/**
* Default SSE endpoint path as specified by the MCP transport specification.
*/
public final static String SSE_ENDPOINT = "/sse";
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
private final ObjectMapper objectMapper;
private final String messageEndpoint;
private final String sseEndpoint;
private final RouterFunction<?> routerFunction;
/**
@ -106,18 +109,34 @@ public class WebFluxSseServerTransport implements ServerMcpTransport {
* setup. Must not be null.
* @throws IllegalArgumentException if either parameter is null
*/
public WebFluxSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
public WebFluxSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
this.objectMapper = objectMapper;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route()
.GET(SSE_ENDPOINT, this::handleSseConnection)
.POST(messageEndpoint, this::handleMessage)
.GET(this.sseEndpoint, this::handleSseConnection)
.POST(this.messageEndpoint, this::handleMessage)
.build();
}
/**
* Constructs a new WebFlux SSE server transport instance with the default SSE
* endpoint.
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
* of MCP messages. Must not be null.
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
* messages. This endpoint will be communicated to clients during SSE connection
* setup. Must not be null.
* @throws IllegalArgumentException if either parameter is null
*/
public WebFluxSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
}
/**
* Configures the message handler for this transport. In the WebFlux SSE
* implementation, this method stores the handler for processing incoming messages but
@ -243,7 +262,7 @@ public class WebFluxSseServerTransport implements ServerMcpTransport {
* <p>
* The router function defines two endpoints:
* <ul>
* <li>GET {SSE_ENDPOINT} - For establishing SSE connections</li>
* <li>GET {sseEndpoint} - For establishing SSE connections</li>
* <li>POST {messageEndpoint} - For receiving client messages</li>
* </ul>
* @return The configured {@link RouterFunction} for handling HTTP requests
@ -343,11 +362,7 @@ public class WebFluxSseServerTransport implements ServerMcpTransport {
.bodyValue(new McpError(error.getMessage()));
});
}
catch (IllegalArgumentException e) {
logger.error("Failed to deserialize message: {}", e.getMessage());
return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
}
catch (IOException e) {
catch (IllegalArgumentException | IOException e) {
logger.error("Failed to deserialize message: {}", e.getMessage());
return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
}