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:
parent
f3e3a65ca6
commit
c2ece20cc2
10 changed files with 111 additions and 58 deletions
|
|
@ -72,31 +72,31 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||||
*/
|
*/
|
||||||
public class WebFluxSseClientTransport implements ClientMcpTransport {
|
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
|
* 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.
|
* 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
|
* 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 this event when a client connects, providing the URI where the client should
|
||||||
* send its messages via HTTP POST.
|
* 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
|
* Default SSE endpoint path as specified by the MCP transport specification. This
|
||||||
* endpoint is used to establish the SSE connection with the server.
|
* 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.
|
* 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<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -57,32 +57,35 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
* {@link Sinks} for thread-safe message broadcasting.
|
* {@link Sinks} for thread-safe message broadcasting.
|
||||||
*
|
*
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
* @author Alexandros Pappas
|
||||||
* @see ServerMcpTransport
|
* @see ServerMcpTransport
|
||||||
* @see ServerSentEvent
|
* @see ServerSentEvent
|
||||||
*/
|
*/
|
||||||
public class WebFluxSseServerTransport implements ServerMcpTransport {
|
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.
|
* 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.
|
* 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.
|
* 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 ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final String messageEndpoint;
|
private final String messageEndpoint;
|
||||||
|
|
||||||
|
private final String sseEndpoint;
|
||||||
|
|
||||||
private final RouterFunction<?> routerFunction;
|
private final RouterFunction<?> routerFunction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,18 +109,34 @@ public class WebFluxSseServerTransport implements ServerMcpTransport {
|
||||||
* setup. Must not be null.
|
* setup. Must not be null.
|
||||||
* @throws IllegalArgumentException if either parameter is 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(objectMapper, "ObjectMapper must not be null");
|
||||||
Assert.notNull(messageEndpoint, "Message endpoint 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.objectMapper = objectMapper;
|
||||||
this.messageEndpoint = messageEndpoint;
|
this.messageEndpoint = messageEndpoint;
|
||||||
|
this.sseEndpoint = sseEndpoint;
|
||||||
this.routerFunction = RouterFunctions.route()
|
this.routerFunction = RouterFunctions.route()
|
||||||
.GET(SSE_ENDPOINT, this::handleSseConnection)
|
.GET(this.sseEndpoint, this::handleSseConnection)
|
||||||
.POST(messageEndpoint, this::handleMessage)
|
.POST(this.messageEndpoint, this::handleMessage)
|
||||||
.build();
|
.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
|
* Configures the message handler for this transport. In the WebFlux SSE
|
||||||
* implementation, this method stores the handler for processing incoming messages but
|
* implementation, this method stores the handler for processing incoming messages but
|
||||||
|
|
@ -243,7 +262,7 @@ public class WebFluxSseServerTransport implements ServerMcpTransport {
|
||||||
* <p>
|
* <p>
|
||||||
* The router function defines two endpoints:
|
* The router function defines two endpoints:
|
||||||
* <ul>
|
* <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>
|
* <li>POST {messageEndpoint} - For receiving client messages</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* @return The configured {@link RouterFunction} for handling HTTP requests
|
* @return The configured {@link RouterFunction} for handling HTTP requests
|
||||||
|
|
@ -343,11 +362,7 @@ public class WebFluxSseServerTransport implements ServerMcpTransport {
|
||||||
.bodyValue(new McpError(error.getMessage()));
|
.bodyValue(new McpError(error.getMessage()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException | IOException e) {
|
||||||
logger.error("Failed to deserialize message: {}", e.getMessage());
|
|
||||||
return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
logger.error("Failed to deserialize message: {}", e.getMessage());
|
logger.error("Failed to deserialize message: {}", e.getMessage());
|
||||||
return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
|
return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,32 +70,35 @@ import org.springframework.web.servlet.function.ServerResponse.SseBuilder;
|
||||||
* maintains its own SSE connection.
|
* maintains its own SSE connection.
|
||||||
*
|
*
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
* @author Alexandros Pappas
|
||||||
* @see ServerMcpTransport
|
* @see ServerMcpTransport
|
||||||
* @see RouterFunction
|
* @see RouterFunction
|
||||||
*/
|
*/
|
||||||
public class WebMvcSseServerTransport implements ServerMcpTransport {
|
public class WebMvcSseServerTransport implements ServerMcpTransport {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(WebMvcSseServerTransport.class);
|
private static final Logger logger = LoggerFactory.getLogger(WebMvcSseServerTransport.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event type for JSON-RPC messages sent through the SSE connection.
|
* 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.
|
* 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.
|
* 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 ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final String messageEndpoint;
|
private final String messageEndpoint;
|
||||||
|
|
||||||
|
private final String sseEndpoint;
|
||||||
|
|
||||||
private final RouterFunction<ServerResponse> routerFunction;
|
private final RouterFunction<ServerResponse> routerFunction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -122,18 +125,33 @@ public class WebMvcSseServerTransport implements ServerMcpTransport {
|
||||||
* SSE connection's initial endpoint event.
|
* SSE connection's initial endpoint event.
|
||||||
* @throws IllegalArgumentException if either objectMapper or messageEndpoint is null
|
* @throws IllegalArgumentException if either objectMapper or messageEndpoint is null
|
||||||
*/
|
*/
|
||||||
public WebMvcSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
|
public WebMvcSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
|
||||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||||
Assert.notNull(messageEndpoint, "Message endpoint 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.objectMapper = objectMapper;
|
||||||
this.messageEndpoint = messageEndpoint;
|
this.messageEndpoint = messageEndpoint;
|
||||||
|
this.sseEndpoint = sseEndpoint;
|
||||||
this.routerFunction = RouterFunctions.route()
|
this.routerFunction = RouterFunctions.route()
|
||||||
.GET(SSE_ENDPOINT, this::handleSseConnection)
|
.GET(this.sseEndpoint, this::handleSseConnection)
|
||||||
.POST(messageEndpoint, this::handleMessage)
|
.POST(this.messageEndpoint, this::handleMessage)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new WebMvcSseServerTransport instance with the default SSE endpoint.
|
||||||
|
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
|
||||||
|
* of messages.
|
||||||
|
* @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
|
||||||
|
* messages via HTTP POST. This endpoint will be communicated to clients through the
|
||||||
|
* SSE connection's initial endpoint event.
|
||||||
|
* @throws IllegalArgumentException if either objectMapper or messageEndpoint is null
|
||||||
|
*/
|
||||||
|
public WebMvcSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
|
||||||
|
this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the message handler for this transport. In the WebMVC SSE implementation,
|
* Sets up the message handler for this transport. In the WebMVC SSE implementation,
|
||||||
* this method only stores the handler for later use, as connections are initiated by
|
* this method only stores the handler for later use, as connections are initiated by
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ import org.springframework.ai.mcp.util.Utils;
|
||||||
*/
|
*/
|
||||||
public class McpAsyncClient {
|
public class McpAsyncClient {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(McpAsyncClient.class);
|
private static final Logger logger = LoggerFactory.getLogger(McpAsyncClient.class);
|
||||||
|
|
||||||
private static TypeReference<Void> VOID_TYPE_REFERENCE = new TypeReference<>() {
|
private static TypeReference<Void> VOID_TYPE_REFERENCE = new TypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
@ -498,7 +498,7 @@ public class McpAsyncClient {
|
||||||
logger.error("Error handling tools list change notification", error);
|
logger.error("Error handling tools list change notification", error);
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}).then(); // Convert to Mono<Void>
|
}).then(); // Convert to Mono<Void>
|
||||||
};
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Resources
|
// Resources
|
||||||
|
|
@ -619,7 +619,7 @@ public class McpAsyncClient {
|
||||||
logger.error("Error handling resources list change notification", error);
|
logger.error("Error handling resources list change notification", error);
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}).then();
|
}).then();
|
||||||
};
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Prompts
|
// Prompts
|
||||||
|
|
@ -680,7 +680,7 @@ public class McpAsyncClient {
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}).then(); // Convert to Mono<Void>
|
}).then(); // Convert to Mono<Void>
|
||||||
}; // @formatter:on
|
}; // @formatter:on
|
||||||
};
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Logging
|
// Logging
|
||||||
|
|
|
||||||
|
|
@ -70,16 +70,16 @@ import java.util.function.Function;
|
||||||
*/
|
*/
|
||||||
public class HttpClientSseClientTransport implements ClientMcpTransport {
|
public class HttpClientSseClientTransport implements ClientMcpTransport {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(HttpClientSseClientTransport.class);
|
private static final Logger logger = LoggerFactory.getLogger(HttpClientSseClientTransport.class);
|
||||||
|
|
||||||
/** SSE event type for JSON-RPC messages */
|
/** SSE event type for JSON-RPC messages */
|
||||||
private final static String MESSAGE_EVENT_TYPE = "message";
|
private static final String MESSAGE_EVENT_TYPE = "message";
|
||||||
|
|
||||||
/** SSE event type for endpoint discovery */
|
/** SSE event type for endpoint discovery */
|
||||||
private final static String ENDPOINT_EVENT_TYPE = "endpoint";
|
private static final String ENDPOINT_EVENT_TYPE = "endpoint";
|
||||||
|
|
||||||
/** Default SSE endpoint path */
|
/** Default SSE endpoint path */
|
||||||
private final static String SSE_ENDPOINT = "/sse";
|
private static final String SSE_ENDPOINT = "/sse";
|
||||||
|
|
||||||
/** Base URI for the MCP server */
|
/** Base URI for the MCP server */
|
||||||
private final String baseUri;
|
private final String baseUri;
|
||||||
|
|
@ -170,7 +170,7 @@ public class HttpClientSseClientTransport implements ClientMcpTransport {
|
||||||
handler.apply(Mono.just(message)).subscribe();
|
handler.apply(Mono.just(message)).subscribe();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.error("Received unrecognized SSE event type: " + event.type());
|
logger.error("Received unrecognized SSE event type: {}", event.type());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ import org.springframework.ai.mcp.util.Utils;
|
||||||
*/
|
*/
|
||||||
public class McpAsyncServer {
|
public class McpAsyncServer {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(McpAsyncServer.class);
|
private static final Logger logger = LoggerFactory.getLogger(McpAsyncServer.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The MCP session implementation that manages bidirectional JSON-RPC communication
|
* The MCP session implementation that manages bidirectional JSON-RPC communication
|
||||||
|
|
@ -279,7 +279,7 @@ public class McpAsyncServer {
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}).then();
|
}).then();
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Tool Management
|
// Tool Management
|
||||||
|
|
@ -352,9 +352,7 @@ public class McpAsyncServer {
|
||||||
private DefaultMcpSession.RequestHandler toolsListRequestHandler() {
|
private DefaultMcpSession.RequestHandler toolsListRequestHandler() {
|
||||||
return params -> {
|
return params -> {
|
||||||
|
|
||||||
List<Tool> tools = this.tools.stream().map(toolRegistration -> {
|
List<Tool> tools = this.tools.stream().map(ToolRegistration::tool).toList();
|
||||||
return toolRegistration.tool();
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
return Mono.just(new McpSchema.ListToolsResult(tools, null));
|
return Mono.just(new McpSchema.ListToolsResult(tools, null));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ public interface McpServer {
|
||||||
*/
|
*/
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final static McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
|
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
|
||||||
"1.0.0");
|
"1.0.0");
|
||||||
|
|
||||||
private final ServerMcpTransport transport;
|
private final ServerMcpTransport transport;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ import org.springframework.ai.mcp.spec.ServerMcpTransport;
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
|
* @author Alexandros Pappas
|
||||||
* @see ServerMcpTransport
|
* @see ServerMcpTransport
|
||||||
* @see HttpServlet
|
* @see HttpServlet
|
||||||
*/
|
*/
|
||||||
|
|
@ -73,8 +74,14 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
/** Logger for this class */
|
/** Logger for this class */
|
||||||
private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransport.class);
|
private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransport.class);
|
||||||
|
|
||||||
/** The endpoint path for SSE connections */
|
public static final String UTF_8 = "UTF-8";
|
||||||
public static final String SSE_ENDPOINT = "/sse";
|
|
||||||
|
public static final String APPLICATION_JSON = "application/json";
|
||||||
|
|
||||||
|
public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}";
|
||||||
|
|
||||||
|
/** Default endpoint path for SSE connections */
|
||||||
|
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
|
||||||
|
|
||||||
/** Event type for regular messages */
|
/** Event type for regular messages */
|
||||||
public static final String MESSAGE_EVENT_TYPE = "message";
|
public static final String MESSAGE_EVENT_TYPE = "message";
|
||||||
|
|
@ -88,6 +95,9 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
/** The endpoint path for handling client messages */
|
/** The endpoint path for handling client messages */
|
||||||
private final String messageEndpoint;
|
private final String messageEndpoint;
|
||||||
|
|
||||||
|
/** The endpoint path for handling SSE connections */
|
||||||
|
private final String sseEndpoint;
|
||||||
|
|
||||||
/** Map of active client sessions, keyed by session ID */
|
/** Map of active client sessions, keyed by session ID */
|
||||||
private final Map<String, ClientSession> sessions = new ConcurrentHashMap<>();
|
private final Map<String, ClientSession> sessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
@ -98,14 +108,26 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> connectHandler;
|
private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> connectHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new HttpServletSseServerTransport instance.
|
* Creates a new HttpServletSseServerTransport instance with a custom SSE endpoint.
|
||||||
|
* @param objectMapper The JSON object mapper to use for message
|
||||||
|
* serialization/deserialization
|
||||||
|
* @param messageEndpoint The endpoint path where clients will send their messages
|
||||||
|
* @param sseEndpoint The endpoint path where clients will establish SSE connections
|
||||||
|
*/
|
||||||
|
public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.messageEndpoint = messageEndpoint;
|
||||||
|
this.sseEndpoint = sseEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new HttpServletSseServerTransport instance with the default SSE endpoint.
|
||||||
* @param objectMapper The JSON object mapper to use for message
|
* @param objectMapper The JSON object mapper to use for message
|
||||||
* serialization/deserialization
|
* serialization/deserialization
|
||||||
* @param messageEndpoint The endpoint path where clients will send their messages
|
* @param messageEndpoint The endpoint path where clients will send their messages
|
||||||
*/
|
*/
|
||||||
public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
|
public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
|
||||||
this.objectMapper = objectMapper;
|
this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
|
||||||
this.messageEndpoint = messageEndpoint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -124,7 +146,7 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
String pathInfo = request.getPathInfo();
|
String pathInfo = request.getPathInfo();
|
||||||
if (!SSE_ENDPOINT.equals(pathInfo)) {
|
if (!sseEndpoint.equals(pathInfo)) {
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +157,7 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
}
|
}
|
||||||
|
|
||||||
response.setContentType("text/event-stream");
|
response.setContentType("text/event-stream");
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setCharacterEncoding(UTF_8);
|
||||||
response.setHeader("Cache-Control", "no-cache");
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
response.setHeader("Connection", "keep-alive");
|
response.setHeader("Connection", "keep-alive");
|
||||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
@ -191,8 +213,8 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
if (connectHandler != null) {
|
if (connectHandler != null) {
|
||||||
connectHandler.apply(Mono.just(message)).subscribe(responseMessage -> {
|
connectHandler.apply(Mono.just(message)).subscribe(responseMessage -> {
|
||||||
try {
|
try {
|
||||||
response.setContentType("application/json");
|
response.setContentType(APPLICATION_JSON);
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setCharacterEncoding(UTF_8);
|
||||||
String jsonResponse = objectMapper.writeValueAsString(responseMessage);
|
String jsonResponse = objectMapper.writeValueAsString(responseMessage);
|
||||||
PrintWriter writer = response.getWriter();
|
PrintWriter writer = response.getWriter();
|
||||||
writer.write(jsonResponse);
|
writer.write(jsonResponse);
|
||||||
|
|
@ -205,15 +227,15 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
"Error processing response: " + e.getMessage());
|
"Error processing response: " + e.getMessage());
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
logger.error("Failed to send error response: {}", ex.getMessage());
|
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, error -> {
|
}, error -> {
|
||||||
try {
|
try {
|
||||||
logger.error("Error processing message: {}", error.getMessage());
|
logger.error("Error processing message: {}", error.getMessage());
|
||||||
McpError mcpError = new McpError(error.getMessage());
|
McpError mcpError = new McpError(error.getMessage());
|
||||||
response.setContentType("application/json");
|
response.setContentType(APPLICATION_JSON);
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setCharacterEncoding(UTF_8);
|
||||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
String jsonError = objectMapper.writeValueAsString(mcpError);
|
String jsonError = objectMapper.writeValueAsString(mcpError);
|
||||||
PrintWriter writer = response.getWriter();
|
PrintWriter writer = response.getWriter();
|
||||||
|
|
@ -221,13 +243,13 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
writer.flush();
|
writer.flush();
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
logger.error("Failed to send error response: {}", e.getMessage());
|
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, e.getMessage());
|
||||||
try {
|
try {
|
||||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
||||||
"Error sending error response: " + e.getMessage());
|
"Error sending error response: " + e.getMessage());
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
logger.error("Failed to send error response: {}", ex.getMessage());
|
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -240,8 +262,8 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
logger.error("Invalid message format: {}", e.getMessage());
|
logger.error("Invalid message format: {}", e.getMessage());
|
||||||
try {
|
try {
|
||||||
McpError mcpError = new McpError("Invalid message format: " + e.getMessage());
|
McpError mcpError = new McpError("Invalid message format: " + e.getMessage());
|
||||||
response.setContentType("application/json");
|
response.setContentType(APPLICATION_JSON);
|
||||||
response.setCharacterEncoding("UTF-8");
|
response.setCharacterEncoding(UTF_8);
|
||||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
String jsonError = objectMapper.writeValueAsString(mcpError);
|
String jsonError = objectMapper.writeValueAsString(mcpError);
|
||||||
PrintWriter writer = response.getWriter();
|
PrintWriter writer = response.getWriter();
|
||||||
|
|
@ -249,7 +271,7 @@ public class HttpServletSseServerTransport extends HttpServlet implements Server
|
||||||
writer.flush();
|
writer.flush();
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
logger.error("Failed to send error response: {}", ex.getMessage());
|
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
|
||||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid message format");
|
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid message format");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ public final class McpSchema {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static TypeReference<HashMap<String, Object>> MAP_TYPE_REF = new TypeReference<>() {
|
private static final TypeReference<HashMap<String, Object>> MAP_TYPE_REF = new TypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
*/
|
*/
|
||||||
class DefaultMcpSessionTests {
|
class DefaultMcpSessionTests {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(DefaultMcpSessionTests.class);
|
private static final Logger logger = LoggerFactory.getLogger(DefaultMcpSessionTests.class);
|
||||||
|
|
||||||
private static final Duration TIMEOUT = Duration.ofSeconds(5);
|
private static final Duration TIMEOUT = Duration.ofSeconds(5);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue