fix the baseUrl is configured with a trailing slash
Signed-off-by: lance <leehaut@gmail.com> Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
67f8eabb7a
commit
86991c1e49
4 changed files with 221 additions and 34 deletions
|
|
@ -14,9 +14,9 @@ import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
|
||||||
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
|
@ -69,7 +69,9 @@ import reactor.core.publisher.Mono;
|
||||||
@WebServlet(asyncSupported = true)
|
@WebServlet(asyncSupported = true)
|
||||||
public class HttpServletSseServerTransportProvider extends HttpServlet implements McpServerTransportProvider {
|
public class HttpServletSseServerTransportProvider extends HttpServlet implements McpServerTransportProvider {
|
||||||
|
|
||||||
/** Logger for this class */
|
/**
|
||||||
|
* Logger for this class
|
||||||
|
*/
|
||||||
private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransportProvider.class);
|
private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransportProvider.class);
|
||||||
|
|
||||||
public static final String UTF_8 = "UTF-8";
|
public static final String UTF_8 = "UTF-8";
|
||||||
|
|
@ -78,38 +80,60 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
|
||||||
|
|
||||||
public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}";
|
public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}";
|
||||||
|
|
||||||
/** Default endpoint path for SSE connections */
|
/**
|
||||||
|
* Default endpoint path for SSE connections
|
||||||
|
*/
|
||||||
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
|
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";
|
||||||
|
|
||||||
/** Event type for endpoint information */
|
/**
|
||||||
|
* Event type for endpoint information
|
||||||
|
*/
|
||||||
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
|
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
|
||||||
|
|
||||||
|
public static final String SESSION_ID = "sessionId";
|
||||||
|
|
||||||
public static final String DEFAULT_BASE_URL = "";
|
public static final String DEFAULT_BASE_URL = "";
|
||||||
|
|
||||||
/** JSON mapper for serialization/deserialization */
|
/**
|
||||||
|
* JSON mapper for serialization/deserialization
|
||||||
|
*/
|
||||||
private final McpJsonMapper jsonMapper;
|
private final McpJsonMapper jsonMapper;
|
||||||
|
|
||||||
/** Base URL for the server transport */
|
/**
|
||||||
|
* Base URL for the server transport
|
||||||
|
*/
|
||||||
private final String baseUrl;
|
private final String baseUrl;
|
||||||
|
|
||||||
/** 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 */
|
/**
|
||||||
|
* The endpoint path for handling SSE connections
|
||||||
|
*/
|
||||||
private final String sseEndpoint;
|
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, McpServerSession> sessions = new ConcurrentHashMap<>();
|
private final Map<String, McpServerSession> sessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private McpTransportContextExtractor<HttpServletRequest> contextExtractor;
|
private McpTransportContextExtractor<HttpServletRequest> contextExtractor;
|
||||||
|
|
||||||
/** Flag indicating if the transport is in the process of shutting down */
|
/**
|
||||||
|
* Flag indicating if the transport is in the process of shutting down
|
||||||
|
*/
|
||||||
private final AtomicBoolean isClosing = new AtomicBoolean(false);
|
private final AtomicBoolean isClosing = new AtomicBoolean(false);
|
||||||
|
|
||||||
/** Session factory for creating new sessions */
|
/**
|
||||||
|
* Session factory for creating new sessions
|
||||||
|
*/
|
||||||
private McpServerSession.Factory sessionFactory;
|
private McpServerSession.Factory sessionFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -243,7 +267,22 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
|
||||||
this.sessions.put(sessionId, session);
|
this.sessions.put(sessionId, session);
|
||||||
|
|
||||||
// Send initial endpoint event
|
// Send initial endpoint event
|
||||||
this.sendEvent(writer, ENDPOINT_EVENT_TYPE, this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
|
this.sendEvent(writer, ENDPOINT_EVENT_TYPE, buildEndpointUrl(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the full message endpoint URL by combining the base URL, message path,
|
||||||
|
* and the required session_id query parameter.
|
||||||
|
* @param sessionId the unique session identifier
|
||||||
|
* @return the fully qualified endpoint URL as a string
|
||||||
|
*/
|
||||||
|
private String buildEndpointUrl(String sessionId) {
|
||||||
|
// for WebMVC compatibility
|
||||||
|
if (this.baseUrl.endsWith("/")) {
|
||||||
|
return this.baseUrl.substring(0, this.baseUrl.length() - 1) + this.messageEndpoint + "?sessionId="
|
||||||
|
+ sessionId;
|
||||||
|
}
|
||||||
|
return this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -434,8 +473,8 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
|
||||||
* Converts data from one type to another using the configured JsonMapper.
|
* Converts data from one type to another using the configured JsonMapper.
|
||||||
* @param data The source data object to convert
|
* @param data The source data object to convert
|
||||||
* @param typeRef The target type reference
|
* @param typeRef The target type reference
|
||||||
* @return The converted object of type T
|
|
||||||
* @param <T> The target type
|
* @param <T> The target type
|
||||||
|
* @return The converted object of type T
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,9 @@ import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
|
||||||
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
|
@ -22,7 +21,6 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||||
import io.modelcontextprotocol.spec.ProtocolVersions;
|
import io.modelcontextprotocol.spec.ProtocolVersions;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import io.modelcontextprotocol.util.KeepAliveScheduler;
|
import io.modelcontextprotocol.util.KeepAliveScheduler;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import reactor.core.Exceptions;
|
import reactor.core.Exceptions;
|
||||||
|
|
@ -37,6 +35,7 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server-side implementation of the MCP (Model Context Protocol) HTTP transport using
|
* Server-side implementation of the MCP (Model Context Protocol) HTTP transport using
|
||||||
|
|
@ -95,6 +94,8 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
*/
|
*/
|
||||||
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
|
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
|
||||||
|
|
||||||
|
public static final String SESSION_ID = "sessionId";
|
||||||
|
|
||||||
public static final String DEFAULT_BASE_URL = "";
|
public static final String DEFAULT_BASE_URL = "";
|
||||||
|
|
||||||
private final McpJsonMapper jsonMapper;
|
private final McpJsonMapper jsonMapper;
|
||||||
|
|
@ -224,6 +225,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
// FIXME: This javadoc makes claims about using isClosing flag but it's not
|
// FIXME: This javadoc makes claims about using isClosing flag but it's not
|
||||||
// actually
|
// actually
|
||||||
// doing that.
|
// doing that.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates a graceful shutdown of all the sessions. This method ensures all active
|
* Initiates a graceful shutdown of all the sessions. This method ensures all active
|
||||||
* sessions are properly closed and cleaned up.
|
* sessions are properly closed and cleaned up.
|
||||||
|
|
@ -286,10 +288,8 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
|
|
||||||
// Send initial endpoint event
|
// Send initial endpoint event
|
||||||
logger.debug("Sending initial endpoint event to session: {}", sessionId);
|
logger.debug("Sending initial endpoint event to session: {}", sessionId);
|
||||||
sink.next(ServerSentEvent.builder()
|
sink.next(
|
||||||
.event(ENDPOINT_EVENT_TYPE)
|
ServerSentEvent.builder().event(ENDPOINT_EVENT_TYPE).data(buildEndpointUrl(sessionId)).build());
|
||||||
.data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId)
|
|
||||||
.build());
|
|
||||||
sink.onCancel(() -> {
|
sink.onCancel(() -> {
|
||||||
logger.debug("Session {} cancelled", sessionId);
|
logger.debug("Session {} cancelled", sessionId);
|
||||||
sessions.remove(sessionId);
|
sessions.remove(sessionId);
|
||||||
|
|
@ -297,6 +297,21 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
|
||||||
}).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)), ServerSentEvent.class);
|
}).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)), ServerSentEvent.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the full message endpoint URL by combining the base URL, message path,
|
||||||
|
* and the required session_id query parameter.
|
||||||
|
* @param sessionId the unique session identifier
|
||||||
|
* @return the fully qualified endpoint URL as a string
|
||||||
|
*/
|
||||||
|
private String buildEndpointUrl(String sessionId) {
|
||||||
|
// for WebMVC compatibility
|
||||||
|
return UriComponentsBuilder.fromUriString(this.baseUrl)
|
||||||
|
.path(this.messageEndpoint)
|
||||||
|
.queryParam(SESSION_ID, sessionId)
|
||||||
|
.build()
|
||||||
|
.toUriString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles incoming JSON-RPC messages from clients. Deserializes the message and
|
* Handles incoming JSON-RPC messages from clients. Deserializes the message and
|
||||||
* processes it through the configured message handler.
|
* processes it through the configured message handler.
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,18 @@ import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import io.modelcontextprotocol.json.TypeRef;
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
|
|
||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
|
||||||
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpServerSession;
|
||||||
import io.modelcontextprotocol.spec.McpServerTransport;
|
import io.modelcontextprotocol.spec.McpServerTransport;
|
||||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||||
import io.modelcontextprotocol.spec.ProtocolVersions;
|
import io.modelcontextprotocol.spec.ProtocolVersions;
|
||||||
import io.modelcontextprotocol.spec.McpServerSession;
|
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import io.modelcontextprotocol.util.KeepAliveScheduler;
|
import io.modelcontextprotocol.util.KeepAliveScheduler;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
@ -36,6 +34,7 @@ import org.springframework.web.servlet.function.RouterFunctions;
|
||||||
import org.springframework.web.servlet.function.ServerRequest;
|
import org.springframework.web.servlet.function.ServerRequest;
|
||||||
import org.springframework.web.servlet.function.ServerResponse;
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
import org.springframework.web.servlet.function.ServerResponse.SseBuilder;
|
import org.springframework.web.servlet.function.ServerResponse.SseBuilder;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server-side implementation of the Model Context Protocol (MCP) transport layer using
|
* Server-side implementation of the Model Context Protocol (MCP) transport layer using
|
||||||
|
|
@ -87,6 +86,8 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
|
||||||
*/
|
*/
|
||||||
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
|
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
|
||||||
|
|
||||||
|
public static final String SESSION_ID = "sessionId";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default SSE endpoint path as specified by the MCP transport specification.
|
* Default SSE endpoint path as specified by the MCP transport specification.
|
||||||
*/
|
*/
|
||||||
|
|
@ -275,9 +276,7 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
|
||||||
this.sessions.put(sessionId, session);
|
this.sessions.put(sessionId, session);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sseBuilder.id(sessionId)
|
sseBuilder.id(sessionId).event(ENDPOINT_EVENT_TYPE).data(buildEndpointUrl(sessionId));
|
||||||
.event(ENDPOINT_EVENT_TYPE)
|
|
||||||
.data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
|
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
logger.error("Failed to send initial endpoint event: {}", e.getMessage());
|
logger.error("Failed to send initial endpoint event: {}", e.getMessage());
|
||||||
|
|
@ -292,6 +291,21 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the full message endpoint URL by combining the base URL, message path,
|
||||||
|
* and the required session_id query parameter.
|
||||||
|
* @param sessionId the unique session identifier
|
||||||
|
* @return the fully qualified endpoint URL as a string
|
||||||
|
*/
|
||||||
|
private String buildEndpointUrl(String sessionId) {
|
||||||
|
// for WebMVC compatibility
|
||||||
|
return UriComponentsBuilder.fromUriString(this.baseUrl)
|
||||||
|
.path(this.messageEndpoint)
|
||||||
|
.queryParam(SESSION_ID, sessionId)
|
||||||
|
.build()
|
||||||
|
.toUriString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles incoming JSON-RPC messages from clients. This method:
|
* Handles incoming JSON-RPC messages from clients. This method:
|
||||||
* <ul>
|
* <ul>
|
||||||
|
|
@ -308,11 +322,11 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
|
||||||
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.param("sessionId").isEmpty()) {
|
if (request.param(SESSION_ID).isEmpty()) {
|
||||||
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
|
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
|
||||||
}
|
}
|
||||||
|
|
||||||
String sessionId = request.param("sessionId").get();
|
String sessionId = request.param(SESSION_ID).get();
|
||||||
McpServerSession session = sessions.get(sessionId);
|
McpServerSession session = sessions.get(sessionId);
|
||||||
|
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
|
|
@ -327,9 +341,9 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
|
||||||
|
|
||||||
// Process the message through the session's handle method
|
// Process the message through the session's handle method
|
||||||
session.handle(message).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)).block(); // Block
|
session.handle(message).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)).block(); // Block
|
||||||
// for
|
// for
|
||||||
// WebMVC
|
// WebMVC
|
||||||
// compatibility
|
// compatibility
|
||||||
|
|
||||||
return ServerResponse.ok().build();
|
return ServerResponse.ok().build();
|
||||||
}
|
}
|
||||||
|
|
@ -398,8 +412,8 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
|
||||||
* Converts data from one type to another using the configured McpJsonMapper.
|
* Converts data from one type to another using the configured McpJsonMapper.
|
||||||
* @param data The source data object to convert
|
* @param data The source data object to convert
|
||||||
* @param typeRef The target type reference
|
* @param typeRef The target type reference
|
||||||
* @return The converted object of type T
|
|
||||||
* @param <T> The target type
|
* @param <T> The target type
|
||||||
|
* @return The converted object of type T
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 - 2025 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.server.transport;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.client.McpClient;
|
||||||
|
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
||||||
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
|
import io.modelcontextprotocol.server.McpServer;
|
||||||
|
import io.modelcontextprotocol.server.TestUtil;
|
||||||
|
import io.modelcontextprotocol.server.TomcatTestUtil;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import org.apache.catalina.LifecycleException;
|
||||||
|
import org.apache.catalina.LifecycleState;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for WebMvcSseServerTransportProvider
|
||||||
|
*
|
||||||
|
* @author lance
|
||||||
|
*/
|
||||||
|
class WebMvcSseServerTransportProviderTests {
|
||||||
|
|
||||||
|
private static final int PORT = TestUtil.findAvailablePort();
|
||||||
|
|
||||||
|
private static final String CUSTOM_CONTEXT_PATH = "";
|
||||||
|
|
||||||
|
private static final String MESSAGE_ENDPOINT = "/mcp/message";
|
||||||
|
|
||||||
|
private WebMvcSseServerTransportProvider mcpServerTransportProvider;
|
||||||
|
|
||||||
|
McpClient.SyncSpec clientBuilder;
|
||||||
|
|
||||||
|
private TomcatTestUtil.TomcatServer tomcatServer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void before() {
|
||||||
|
tomcatServer = TomcatTestUtil.createTomcatServer(CUSTOM_CONTEXT_PATH, PORT, TestConfig.class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
tomcatServer.tomcat().start();
|
||||||
|
assertThat(tomcatServer.tomcat().getServer().getState()).isEqualTo(LifecycleState.STARTED);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to start Tomcat", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder("http://localhost:" + PORT)
|
||||||
|
.sseEndpoint(WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
clientBuilder = McpClient.sync(transport);
|
||||||
|
mcpServerTransportProvider = tomcatServer.appContext().getBean(WebMvcSseServerTransportProvider.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validBaseUrl() {
|
||||||
|
McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build();
|
||||||
|
try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0"))
|
||||||
|
.build()) {
|
||||||
|
assertThat(client.initialize()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void after() {
|
||||||
|
if (mcpServerTransportProvider != null) {
|
||||||
|
mcpServerTransportProvider.closeGracefully().block();
|
||||||
|
}
|
||||||
|
if (tomcatServer.appContext() != null) {
|
||||||
|
tomcatServer.appContext().close();
|
||||||
|
}
|
||||||
|
if (tomcatServer.tomcat() != null) {
|
||||||
|
try {
|
||||||
|
tomcatServer.tomcat().stop();
|
||||||
|
tomcatServer.tomcat().destroy();
|
||||||
|
}
|
||||||
|
catch (LifecycleException e) {
|
||||||
|
throw new RuntimeException("Failed to stop Tomcat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebMvc
|
||||||
|
static class TestConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() {
|
||||||
|
|
||||||
|
return WebMvcSseServerTransportProvider.builder()
|
||||||
|
.baseUrl("http://localhost:" + PORT + "/")
|
||||||
|
.messageEndpoint(MESSAGE_ENDPOINT)
|
||||||
|
.sseEndpoint(WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT)
|
||||||
|
.jsonMapper(McpJsonMapper.getDefault())
|
||||||
|
.contextExtractor(req -> McpTransportContext.EMPTY)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RouterFunction<ServerResponse> routerFunction(WebMvcSseServerTransportProvider transportProvider) {
|
||||||
|
return transportProvider.getRouterFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue