From eb8e3744a7903932ab02982e506d6a75e79fe3ab Mon Sep 17 00:00:00 2001
From: Renxia Wang
Date: Sat, 29 Mar 2025 20:49:51 -0400
Subject: [PATCH 001/249] feat(transport): Add customizable HTTP request
builder support (#86)
Enhances FlowSseClient and HttpClientSseClientTransport to accept a custom HttpRequest.Builder,
allowing for greater flexibility when configuring HTTP requests.
This enables clients to customize headers, timeouts, and other request properties across
all SSE connections and message sending operations.
Signed-off-by: Christian Tzolov
---
.../client/transport/FlowSseClient.java | 15 ++++++-
.../HttpClientSseClientTransport.java | 41 +++++++++++++++++--
2 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java
index 7fc679937..50af35c70 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java
@@ -39,6 +39,8 @@ public class FlowSseClient {
private final HttpClient httpClient;
+ private final HttpRequest.Builder requestBuilder;
+
/**
* Pattern to extract the data content from SSE data field lines. Matches lines
* starting with "data:" and captures the remaining content.
@@ -92,7 +94,17 @@ public interface SseEventHandler {
* @param httpClient the {@link HttpClient} instance to use for SSE connections
*/
public FlowSseClient(HttpClient httpClient) {
+ this(httpClient, HttpRequest.newBuilder());
+ }
+
+ /**
+ * Creates a new FlowSseClient with the specified HTTP client and request builder.
+ * @param httpClient the {@link HttpClient} instance to use for SSE connections
+ * @param requestBuilder the {@link HttpRequest.Builder} to use for SSE requests
+ */
+ public FlowSseClient(HttpClient httpClient, HttpRequest.Builder requestBuilder) {
this.httpClient = httpClient;
+ this.requestBuilder = requestBuilder;
}
/**
@@ -109,8 +121,7 @@ public FlowSseClient(HttpClient httpClient) {
* @throws RuntimeException if the connection fails with a non-200 status code
*/
public void subscribe(String url, SseEventHandler eventHandler) {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
+ HttpRequest request = this.requestBuilder.uri(URI.create(url))
.header("Accept", "text/event-stream")
.header("Cache-Control", "no-cache")
.GET()
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
index 696efdffd..0b482533d 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
@@ -82,6 +82,9 @@ public class HttpClientSseClientTransport implements McpClientTransport {
*/
private final HttpClient httpClient;
+ /** HTTP request builder for building requests to send messages to the server */
+ private final HttpRequest.Builder requestBuilder;
+
/** JSON object mapper for message serialization/deserialization */
protected ObjectMapper objectMapper;
@@ -126,15 +129,33 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String bas
*/
public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, String sseEndpoint,
ObjectMapper objectMapper) {
+ this(clientBuilder, HttpRequest.newBuilder(), baseUri, sseEndpoint, objectMapper);
+ }
+
+ /**
+ * Creates a new transport instance with custom HTTP client builder, object mapper,
+ * and headers.
+ * @param clientBuilder the HTTP client builder to use
+ * @param requestBuilder the HTTP request builder to use
+ * @param baseUri the base URI of the MCP server
+ * @param sseEndpoint the SSE endpoint path
+ * @param objectMapper the object mapper for JSON serialization/deserialization
+ * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
+ */
+ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpRequest.Builder requestBuilder,
+ String baseUri, String sseEndpoint, ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.hasText(baseUri, "baseUri must not be empty");
Assert.hasText(sseEndpoint, "sseEndpoint must not be empty");
Assert.notNull(clientBuilder, "clientBuilder must not be null");
+ Assert.notNull(requestBuilder, "requestBuilder must not be null");
this.baseUri = baseUri;
this.sseEndpoint = sseEndpoint;
this.objectMapper = objectMapper;
this.httpClient = clientBuilder.connectTimeout(Duration.ofSeconds(10)).build();
- this.sseClient = new FlowSseClient(this.httpClient);
+ this.requestBuilder = requestBuilder;
+
+ this.sseClient = new FlowSseClient(this.httpClient, requestBuilder);
}
/**
@@ -159,6 +180,8 @@ public static class Builder {
private ObjectMapper objectMapper = new ObjectMapper();
+ private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
+
/**
* Creates a new builder with the specified base URI.
* @param baseUri the base URI of the MCP server
@@ -190,6 +213,17 @@ public Builder clientBuilder(HttpClient.Builder clientBuilder) {
return this;
}
+ /**
+ * Sets the HTTP request builder.
+ * @param requestBuilder the HTTP request builder
+ * @return this builder
+ */
+ public Builder requestBuilder(HttpRequest.Builder requestBuilder) {
+ Assert.notNull(requestBuilder, "requestBuilder must not be null");
+ this.requestBuilder = requestBuilder;
+ return this;
+ }
+
/**
* Sets the object mapper for JSON serialization/deserialization.
* @param objectMapper the object mapper
@@ -206,7 +240,7 @@ public Builder objectMapper(ObjectMapper objectMapper) {
* @return a new transport instance
*/
public HttpClientSseClientTransport build() {
- return new HttpClientSseClientTransport(clientBuilder, baseUri, sseEndpoint, objectMapper);
+ return new HttpClientSseClientTransport(clientBuilder, requestBuilder, baseUri, sseEndpoint, objectMapper);
}
}
@@ -301,8 +335,7 @@ public Mono sendMessage(JSONRPCMessage message) {
try {
String jsonText = this.objectMapper.writeValueAsString(message);
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(this.baseUri + endpoint))
+ HttpRequest request = this.requestBuilder.uri(URI.create(this.baseUri + endpoint))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonText))
.build();
From c3a7c1ac1e04c141e95df1b1a77dc127b7ce0311 Mon Sep 17 00:00:00 2001
From: jitokim
Date: Sun, 6 Apr 2025 02:12:34 +0900
Subject: [PATCH 002/249] perf(webflux): optimize session broadcasting with
Flux.fromIterable (#109)
Replace Flux.fromStream(sessions.values().stream()) with more efficient
Flux.fromIterable(sessions.values()) to eliminate unnecessary stream
conversion when broadcasting messages to active sessions
Signed-off-by: jitokim
---
.../server/transport/WebFluxSseServerTransportProvider.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
index 85a39a82f..af2ff06a3 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
@@ -171,10 +171,10 @@ public Mono notifyClients(String method, Map params) {
logger.debug("Attempting to broadcast message to {} active sessions", sessions.size());
- return Flux.fromStream(sessions.values().stream())
+ return Flux.fromIterable(sessions.values())
.flatMap(session -> session.sendNotification(method, params)
- .doOnError(e -> logger.error("Failed to " + "send message to session " + "{}: {}", session.getId(),
- e.getMessage()))
+ .doOnError(
+ e -> logger.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
.onErrorComplete())
.then();
}
From cd624a7d5719db9648711c986be9bc9a149a34e4 Mon Sep 17 00:00:00 2001
From: Oleksandr Popov
Date: Sun, 6 Apr 2025 10:29:29 +0200
Subject: [PATCH 003/249] fix: correct typos and improve documentation (#35)
Signed-off-by: Christian Tzolov
---
mcp/pom.xml | 2 +-
.../io/modelcontextprotocol/client/McpAsyncClient.java | 10 +++++++++-
.../client/transport/HttpClientSseClientTransport.java | 2 +-
.../client/transport/StdioClientTransport.java | 2 +-
.../io/modelcontextprotocol/spec/McpClientSession.java | 4 ++--
.../spec/McpClientSessionTests.java | 2 +-
6 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/mcp/pom.xml b/mcp/pom.xml
index f6e93b39c..edb1c8f07 100644
--- a/mcp/pom.xml
+++ b/mcp/pom.xml
@@ -97,7 +97,7 @@
test
-
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index 379b47e23..ce49b0a5e 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -364,7 +364,7 @@ private Mono withInitializationCheck(String actionName,
}
// --------------------------
- // Basic Utilites
+ // Basic Utilities
// --------------------------
/**
@@ -751,6 +751,14 @@ private NotificationHandler asyncPromptsChangeNotificationHandler(
// --------------------------
// Logging
// --------------------------
+ /**
+ * Create a notification handler for logging notifications from the server. This
+ * handler automatically distributes logging messages to all registered consumers.
+ * @param loggingConsumers List of consumers that will be notified when a logging
+ * message is received. Each consumer receives the logging message notification.
+ * @return A NotificationHandler that processes log notifications by distributing the
+ * message to all registered consumers
+ */
private NotificationHandler asyncLoggingNotificationHandler(
List>> loggingConsumers) {
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
index 0b482533d..a5bdd43e2 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
@@ -376,7 +376,7 @@ public Mono closeGracefully() {
}
/**
- * Unmarshals data to the specified type using the configured object mapper.
+ * Unmarshal data to the specified type using the configured object mapper.
* @param data the data to unmarshal
* @param typeRef the type reference for the target type
* @param the target type
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
index f9a97849f..9d71cbb48 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java
@@ -292,7 +292,7 @@ private void startInboundProcessing() {
*/
private void startOutboundProcessing() {
this.handleOutbound(messages -> messages
- // this bit is important since writes come from user threads and we
+ // this bit is important since writes come from user threads, and we
// want to ensure that the actual writing happens on a dedicated thread
.publishOn(outboundScheduler)
.handle((message, s) -> {
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index e29646e6a..719a78001 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -107,7 +107,7 @@ public interface NotificationHandler {
public McpClientSession(Duration requestTimeout, McpClientTransport transport,
Map> requestHandlers, Map notificationHandlers) {
- Assert.notNull(requestTimeout, "The requstTimeout can not be null");
+ Assert.notNull(requestTimeout, "The requestTimeout can not be null");
Assert.notNull(transport, "The transport can not be null");
Assert.notNull(requestHandlers, "The requestHandlers can not be null");
Assert.notNull(notificationHandlers, "The notificationHandlers can not be null");
@@ -127,7 +127,7 @@ public McpClientSession(Duration requestTimeout, McpClientTransport transport,
logger.debug("Received Response: {}", response);
var sink = pendingResponses.remove(response.id());
if (sink == null) {
- logger.warn("Unexpected response for unkown id {}", response.id());
+ logger.warn("Unexpected response for unknown id {}", response.id());
}
else {
sink.success(response);
diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
index 715d6651e..f72be43e0 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
@@ -61,7 +61,7 @@ void tearDown() {
void testConstructorWithInvalidArguments() {
assertThatThrownBy(() -> new McpClientSession(null, transport, Map.of(), Map.of()))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("requstTimeout can not be null");
+ .hasMessageContaining("The requestTimeout can not be null");
assertThatThrownBy(() -> new McpClientSession(TIMEOUT, null, Map.of(), Map.of()))
.isInstanceOf(IllegalArgumentException.class)
From 734153a445585cd452f041520583e6517f3674f3 Mon Sep 17 00:00:00 2001
From: jitokim
Date: Sun, 6 Apr 2025 02:10:43 +0900
Subject: [PATCH 004/249] fix typo in WebFluxSseIntegrationTests
Signed-off-by: jitokim
---
.../WebFluxSseIntegrationTests.java | 28 +++++++++----------
1 file changed, 13 insertions(+), 15 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
index 2be2f81f2..dbfad821f 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
@@ -52,8 +52,6 @@ public class WebFluxSseIntegrationTests {
private static final int PORT = 8182;
- // private static final String MESSAGE_ENDPOINT = "/mcp/message";
-
private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
@@ -62,7 +60,7 @@ public class WebFluxSseIntegrationTests {
private WebFluxSseServerTransportProvider mcpServerTransportProvider;
- ConcurrentHashMap clientBulders = new ConcurrentHashMap<>();
+ ConcurrentHashMap clientBuilders = new ConcurrentHashMap<>();
@BeforeEach
public void before() {
@@ -77,11 +75,11 @@ public void before() {
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
this.httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
- clientBulders.put("httpclient",
+ clientBuilders.put("httpclient",
McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT)
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
.build()));
- clientBulders.put("webflux",
+ clientBuilders.put("webflux",
McpClient
.sync(WebFluxSseClientTransport.builder(WebClient.builder().baseUrl("http://localhost:" + PORT))
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
@@ -103,7 +101,7 @@ public void after() {
@ValueSource(strings = { "httpclient", "webflux" })
void testCreateMessageWithoutSamplingCapabilities(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
@@ -134,7 +132,7 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) {
void testCreateMessageSuccess(String clientType) throws InterruptedException {
// Client
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
Function samplingHandler = request -> {
assertThat(request.messages()).hasSize(1);
@@ -203,7 +201,7 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testRootsSuccess(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
@@ -250,7 +248,7 @@ void testRootsSuccess(String clientType) {
@ValueSource(strings = { "httpclient", "webflux" })
void testRootsWithoutCapability(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
McpServerFeatures.SyncToolSpecification tool = new McpServerFeatures.SyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
@@ -284,7 +282,7 @@ void testRootsWithoutCapability(String clientType) {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testRootsNotifciationWithEmptyRootsList(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
AtomicReference> rootsRef = new AtomicReference<>();
var mcpServer = McpServer.sync(mcpServerTransportProvider)
@@ -311,7 +309,7 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testRootsWithMultipleHandlers(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
List roots = List.of(new Root("uri1://", "root1"));
@@ -345,7 +343,7 @@ void testRootsWithMultipleHandlers(String clientType) {
@ValueSource(strings = { "httpclient", "webflux" })
void testRootsServerCloseWithActiveSubscription(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
List roots = List.of(new Root("uri1://", "root1"));
@@ -390,7 +388,7 @@ void testRootsServerCloseWithActiveSubscription(String clientType) {
@ValueSource(strings = { "httpclient", "webflux" })
void testToolCallSuccess(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification(
@@ -430,7 +428,7 @@ void testToolCallSuccess(String clientType) {
@ValueSource(strings = { "httpclient", "webflux" })
void testToolListChangeHandlingSuccess(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification(
@@ -500,7 +498,7 @@ void testToolListChangeHandlingSuccess(String clientType) {
@ValueSource(strings = { "httpclient", "webflux" })
void testInitialize(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
+ var clientBuilder = clientBuilders.get(clientType);
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
From 0db4c0f70d28c72ec94750c289e001d54a5bace6 Mon Sep 17 00:00:00 2001
From: minguncle <57527858+minguncle@users.noreply.github.com>
Date: Thu, 27 Mar 2025 15:53:30 +0800
Subject: [PATCH 005/249] feat(webmvc): Add support for custom context paths in
WebMvcSseServerTransportProvider
Adds the ability to specify a base URL for message endpoints in WebMvcSseServerTransportProvider,
enabling proper handling of custom servlet context paths in Spring WebMVC applications.
This ensures that clients receive the correct full endpoint URL when connecting through SSE.
- Add messageBaseUrl field to WebMvcSseServerTransportProvider
- Create new constructor that accepts messageBaseUrl parameter
- Update endpoint event to include base URL in the message endpoint
- Add TomcatTestUtil class to simplify test server creation
- Add WebMvcSseCustomContextPathTests to verify custom context path functionality
- Refactor WebMvcSseIntegrationTests to use the new TomcatTestUtil
Co-authored-by: Christian Tzolov
Signed-off-by: Christian Tzolov
---
.../WebMvcSseServerTransportProvider.java | 51 ++++++---
.../server/TomcatTestUtil.java | 60 ++++++++++
.../WebMvcSseCustomContextPathTests.java | 105 ++++++++++++++++++
.../server/WebMvcSseIntegrationTests.java | 62 +++--------
mcp-test/pom.xml | 1 +
5 files changed, 216 insertions(+), 63 deletions(-)
create mode 100644 mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java
create mode 100644 mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java
diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
index 65416b256..f6dbd4779 100644
--- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
@@ -91,6 +91,8 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
private final String sseEndpoint;
+ private final String messageBaseUrl;
+
private final RouterFunction routerFunction;
private McpServerSession.Factory sessionFactory;
@@ -105,6 +107,20 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
*/
private volatile boolean isClosing = false;
+ /**
+ * Constructs a new WebMvcSseServerTransportProvider 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 WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) {
+ this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
+ }
+
/**
* Constructs a new WebMvcSseServerTransportProvider instance.
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
@@ -116,11 +132,30 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
* @throws IllegalArgumentException if any parameter is null
*/
public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
+ this(objectMapper, "", messageEndpoint, sseEndpoint);
+ }
+
+ /**
+ * Constructs a new WebMvcSseServerTransportProvider instance.
+ * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
+ * of messages.
+ * @param messageBaseUrl The base URL for the message endpoint, used to construct the
+ * full endpoint URL for clients.
+ * @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.
+ * @param sseEndpoint The endpoint URI where clients establish their SSE connections.
+ * @throws IllegalArgumentException if any parameter is null
+ */
+ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageBaseUrl, String messageEndpoint,
+ String sseEndpoint) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
+ Assert.notNull(messageBaseUrl, "Message base URL 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.messageBaseUrl = messageBaseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route()
@@ -129,20 +164,6 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messag
.build();
}
- /**
- * Constructs a new WebMvcSseServerTransportProvider 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 WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) {
- this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
- }
-
@Override
public void setSessionFactory(McpServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;
@@ -248,7 +269,7 @@ private ServerResponse handleSseConnection(ServerRequest request) {
try {
sseBuilder.id(sessionId)
.event(ENDPOINT_EVENT_TYPE)
- .data(messageEndpoint + "?sessionId=" + sessionId);
+ .data(this.messageBaseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
}
catch (Exception e) {
logger.error("Failed to send initial endpoint event: {}", e.getMessage());
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java
new file mode 100644
index 000000000..fcd7fb4dc
--- /dev/null
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java
@@ -0,0 +1,60 @@
+/*
+* Copyright 2025 - 2025 the original author or authors.
+*/
+package io.modelcontextprotocol.server;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.startup.Tomcat;
+
+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
+import org.springframework.web.servlet.DispatcherServlet;
+
+/**
+ * @author Christian Tzolov
+ */
+public class TomcatTestUtil {
+
+ public record TomcatServer(Tomcat tomcat, AnnotationConfigWebApplicationContext appContext) {
+ }
+
+ public TomcatServer createTomcatServer(String contextPath, int port, Class> componentClass) {
+
+ // Set up Tomcat first
+ var tomcat = new Tomcat();
+ tomcat.setPort(port);
+
+ // Set Tomcat base directory to java.io.tmpdir to avoid permission issues
+ String baseDir = System.getProperty("java.io.tmpdir");
+ tomcat.setBaseDir(baseDir);
+
+ // Use the same directory for document base
+ Context context = tomcat.addContext(contextPath, baseDir);
+
+ // Create and configure Spring WebMvc context
+ var appContext = new AnnotationConfigWebApplicationContext();
+ appContext.register(componentClass);
+ appContext.setServletContext(context.getServletContext());
+ appContext.refresh();
+
+ // Create DispatcherServlet with our Spring context
+ DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
+
+ // Add servlet to Tomcat and get the wrapper
+ var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet);
+ wrapper.setLoadOnStartup(1);
+ wrapper.setAsyncSupported(true);
+ context.addServletMappingDecoded("/*", "dispatcherServlet");
+
+ try {
+ // Configure and start the connector with async support
+ var connector = tomcat.getConnector();
+ connector.setAsyncTimeout(3000); // 3 seconds timeout for async requests
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Failed to start Tomcat", e);
+ }
+
+ return new TomcatServer(tomcat, appContext);
+ }
+
+}
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java
new file mode 100644
index 000000000..0e81104b9
--- /dev/null
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 - 2024 the original author or authors.
+ */
+package io.modelcontextprotocol.server;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
+import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
+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;
+
+public class WebMvcSseCustomContextPathTests {
+
+ private static final String CUSTOM_CONTEXT_PATH = "/app/1";
+
+ private static final int PORT = 8183;
+
+ private static final String MESSAGE_ENDPOINT = "/mcp/message";
+
+ private WebMvcSseServerTransportProvider mcpServerTransportProvider;
+
+ McpClient.SyncSpec clientBuilder;
+
+ private TomcatTestUtil.TomcatServer tomcatServer;
+
+ @BeforeEach
+ public void before() {
+
+ tomcatServer = new TomcatTestUtil().createTomcatServer(CUSTOM_CONTEXT_PATH, PORT, TestConfig.class);
+
+ try {
+ tomcatServer.tomcat().start();
+ assertThat(tomcatServer.tomcat().getServer().getState() == LifecycleState.STARTED);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Failed to start Tomcat", e);
+ }
+
+ var clientTransport = HttpClientSseClientTransport.builder("http://localhost:" + PORT)
+ .sseEndpoint(CUSTOM_CONTEXT_PATH + WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT)
+ .build();
+
+ clientBuilder = McpClient.sync(clientTransport);
+
+ mcpServerTransportProvider = tomcatServer.appContext().getBean(WebMvcSseServerTransportProvider.class);
+ }
+
+ @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);
+ }
+ }
+ }
+
+ @Test
+ void testCustomContextPath() {
+ McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build();
+ var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build();
+ assertThat(client.initialize()).isNotNull();
+ }
+
+ @Configuration
+ @EnableWebMvc
+ static class TestConfig {
+
+ @Bean
+ public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() {
+
+ return new WebMvcSseServerTransportProvider(new ObjectMapper(), CUSTOM_CONTEXT_PATH, MESSAGE_ENDPOINT,
+ WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT);
+ }
+
+ @Bean
+ public RouterFunction routerFunction(WebMvcSseServerTransportProvider transportProvider) {
+ return transportProvider.getRouterFunction();
+ }
+
+ }
+
+}
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
index 3ff755ca9..f9190fd70 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
@@ -25,10 +25,8 @@
import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.Tool;
-import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
-import org.apache.catalina.startup.Tomcat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -38,15 +36,12 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
-import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
-import org.springframework.web.servlet.DispatcherServlet;
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;
import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
public class WebMvcSseIntegrationTests {
@@ -75,55 +70,26 @@ public RouterFunction routerFunction(WebMvcSseServerTransportPro
}
- private Tomcat tomcat;
-
- private AnnotationConfigWebApplicationContext appContext;
+ private TomcatTestUtil.TomcatServer tomcatServer;
@BeforeEach
public void before() {
- // Set up Tomcat first
- tomcat = new Tomcat();
- tomcat.setPort(PORT);
-
- // Set Tomcat base directory to java.io.tmpdir to avoid permission issues
- String baseDir = System.getProperty("java.io.tmpdir");
- tomcat.setBaseDir(baseDir);
-
- // Use the same directory for document base
- Context context = tomcat.addContext("", baseDir);
-
- // Create and configure Spring WebMvc context
- appContext = new AnnotationConfigWebApplicationContext();
- appContext.register(TestConfig.class);
- appContext.setServletContext(context.getServletContext());
- appContext.refresh();
-
- // Get the transport from Spring context
- mcpServerTransportProvider = appContext.getBean(WebMvcSseServerTransportProvider.class);
-
- // Create DispatcherServlet with our Spring context
- DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
- // dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
-
- // Add servlet to Tomcat and get the wrapper
- var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet);
- wrapper.setLoadOnStartup(1);
- wrapper.setAsyncSupported(true);
- context.addServletMappingDecoded("/*", "dispatcherServlet");
+ tomcatServer = new TomcatTestUtil().createTomcatServer("", PORT, TestConfig.class);
try {
- // Configure and start the connector with async support
- var connector = tomcat.getConnector();
- connector.setAsyncTimeout(3000); // 3 seconds timeout for async requests
- tomcat.start();
- assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
+ tomcatServer.tomcat().start();
+ assertThat(tomcatServer.tomcat().getServer().getState() == LifecycleState.STARTED);
}
catch (Exception e) {
throw new RuntimeException("Failed to start Tomcat", e);
}
- this.clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT));
+ clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT));
+
+ // Get the transport from Spring context
+ mcpServerTransportProvider = tomcatServer.appContext().getBean(WebMvcSseServerTransportProvider.class);
+
}
@AfterEach
@@ -131,13 +97,13 @@ public void after() {
if (mcpServerTransportProvider != null) {
mcpServerTransportProvider.closeGracefully().block();
}
- if (appContext != null) {
- appContext.close();
+ if (tomcatServer.appContext() != null) {
+ tomcatServer.appContext().close();
}
- if (tomcat != null) {
+ if (tomcatServer.tomcat() != null) {
try {
- tomcat.stop();
- tomcat.destroy();
+ tomcatServer.tomcat().stop();
+ tomcatServer.tomcat().destroy();
}
catch (LifecycleException e) {
throw new RuntimeException("Failed to stop Tomcat", e);
diff --git a/mcp-test/pom.xml b/mcp-test/pom.xml
index b995618af..95f5dc30a 100644
--- a/mcp-test/pom.xml
+++ b/mcp-test/pom.xml
@@ -80,6 +80,7 @@
logback-classic
${logback.version}
+
From 3fa415228757c78bdbcabdfeaf548b3b09882b2f Mon Sep 17 00:00:00 2001
From: zhangzhenhua
Date: Wed, 2 Apr 2025 13:54:14 +0800
Subject: [PATCH 006/249] feat(webflux): Add base URL support to
WebFluxSseServerTransportProvider (#102)
Adds the ability to specify a base URL prefix for message endpoints in the
WebFlux SSE server transport provider. This enhancement allows for proper
URL construction when the server is running behind a proxy or in a context
with a base path.
- Add new constructor with baseUrl parameter
- Add basePath() method to Builder class
- Modify SSE endpoint event to include baseUrl prefix
Signed-off-by: Christian Tzolov
---
.../WebFluxSseServerTransportProvider.java | 75 ++++++++++++++-----
1 file changed, 58 insertions(+), 17 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
index af2ff06a3..df8dd0211 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
@@ -82,8 +82,16 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
*/
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
+ public static final String DEFAULT_BASE_URL = "";
+
private final ObjectMapper objectMapper;
+ /**
+ * Base URL for the message endpoint. This is used to construct the full URL for
+ * clients to send their JSON-RPC messages.
+ */
+ private final String baseUrl;
+
private final String messageEndpoint;
private final String sseEndpoint;
@@ -102,6 +110,20 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
*/
private volatile boolean isClosing = false;
+ /**
+ * Constructs a new WebFlux SSE server transport provider 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 WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) {
+ this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
+ }
+
/**
* Constructs a new WebFlux SSE server transport provider instance.
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
@@ -112,11 +134,28 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
* @throws IllegalArgumentException if either parameter is null
*/
public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
+ this(objectMapper, DEFAULT_BASE_URL, messageEndpoint, sseEndpoint);
+ }
+
+ /**
+ * Constructs a new WebFlux SSE server transport provider instance.
+ * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
+ * of MCP messages. Must not be null.
+ * @param baseUrl webflux messag base path
+ * @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 WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
+ String sseEndpoint) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
+ Assert.notNull(baseUrl, "Message base path 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.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route()
@@ -125,20 +164,6 @@ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messa
.build();
}
- /**
- * Constructs a new WebFlux SSE server transport provider 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 WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) {
- this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
- }
-
@Override
public void setSessionFactory(McpServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;
@@ -179,7 +204,8 @@ public Mono notifyClients(String method, Map params) {
.then();
}
- // FIXME: This javadoc makes claims about using isClosing flag but it's not actually
+ // FIXME: This javadoc makes claims about using isClosing flag but it's not
+ // actually
// doing that.
/**
* Initiates a graceful shutdown of all the sessions. This method ensures all active
@@ -245,7 +271,7 @@ private Mono handleSseConnection(ServerRequest request) {
logger.debug("Sending initial endpoint event to session: {}", sessionId);
sink.next(ServerSentEvent.builder()
.event(ENDPOINT_EVENT_TYPE)
- .data(messageEndpoint + "?sessionId=" + sessionId)
+ .data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId)
.build());
sink.onCancel(() -> {
logger.debug("Session {} cancelled", sessionId);
@@ -360,6 +386,8 @@ public static class Builder {
private ObjectMapper objectMapper;
+ private String baseUrl = DEFAULT_BASE_URL;
+
private String messageEndpoint;
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
@@ -377,6 +405,19 @@ public Builder objectMapper(ObjectMapper objectMapper) {
return this;
}
+ /**
+ * Sets the project basePath as endpoint prefix where clients should send their
+ * JSON-RPC messages
+ * @param baseUrl the message basePath . Must not be null.
+ * @return this builder instance
+ * @throws IllegalArgumentException if basePath is null
+ */
+ public Builder basePath(String baseUrl) {
+ Assert.notNull(baseUrl, "basePath must not be null");
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
/**
* Sets the endpoint URI where clients should send their JSON-RPC messages.
* @param messageEndpoint The message endpoint URI. Must not be null.
@@ -411,7 +452,7 @@ public WebFluxSseServerTransportProvider build() {
Assert.notNull(objectMapper, "ObjectMapper must be set");
Assert.notNull(messageEndpoint, "Message endpoint must be set");
- return new WebFluxSseServerTransportProvider(objectMapper, messageEndpoint, sseEndpoint);
+ return new WebFluxSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint);
}
}
From b21cfab10ec9d51c8f57541767337dfd790a43b2 Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Sun, 6 Apr 2025 16:33:25 +0200
Subject: [PATCH 007/249] refactor(webmvc): Rename messageBaseUrl to baseUrl
for consistency
Signed-off-by: Christian Tzolov
---
.../WebMvcSseServerTransportProvider.java | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
index f6dbd4779..fa2e357f9 100644
--- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
@@ -91,7 +91,7 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
private final String sseEndpoint;
- private final String messageBaseUrl;
+ private final String baseUrl;
private final RouterFunction routerFunction;
@@ -139,23 +139,23 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messag
* Constructs a new WebMvcSseServerTransportProvider instance.
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
* of messages.
- * @param messageBaseUrl The base URL for the message endpoint, used to construct the
- * full endpoint URL for clients.
+ * @param baseUrl The base URL for the message endpoint, used to construct the full
+ * endpoint URL for clients.
* @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.
* @param sseEndpoint The endpoint URI where clients establish their SSE connections.
* @throws IllegalArgumentException if any parameter is null
*/
- public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageBaseUrl, String messageEndpoint,
+ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
String sseEndpoint) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
- Assert.notNull(messageBaseUrl, "Message base URL must not be null");
+ Assert.notNull(baseUrl, "Message base URL 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.messageBaseUrl = messageBaseUrl;
+ this.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route()
@@ -269,7 +269,7 @@ private ServerResponse handleSseConnection(ServerRequest request) {
try {
sseBuilder.id(sessionId)
.event(ENDPOINT_EVENT_TYPE)
- .data(this.messageBaseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
+ .data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
}
catch (Exception e) {
logger.error("Failed to send initial endpoint event: {}", e.getMessage());
From 8fc72aed88616cfe4ba4fe8adae038b32fcc9f8b Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Sun, 6 Apr 2025 18:41:25 +0200
Subject: [PATCH 008/249] feat(mcp): Add support for custom context paths in
HTTP Servlet SSE server transport
Enhance HttpServletSseServerTransportProvider to support deployment under non-root context paths by:
- Adding baseUrl field and DEFAULT_BASE_URL constant
- Creating new constructor that accepts a baseUrl parameter
- Extending Builder with baseUrl configuration method
- Prepending baseUrl to message endpoint in SSE events
- Add HttpServletSseServerCustomContextPathTests to verify custom context path functionality
- Extract common Tomcat server setup code to TomcatTestUtil for test reuse
Related to #79
Signed-off-by: Christian Tzolov
---
...HttpServletSseServerTransportProvider.java | 37 +++++++-
...ervletSseServerCustomContextPathTests.java | 86 +++++++++++++++++++
...rverTransportProviderIntegrationTests.java | 21 +----
.../server/transport/TomcatTestUtil.java | 45 ++++++++++
4 files changed, 167 insertions(+), 22 deletions(-)
create mode 100644 mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
create mode 100644 mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
index a64b4a353..e52fc88b7 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
@@ -80,9 +80,14 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
/** Event type for endpoint information */
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
+ public static final String DEFAULT_BASE_URL = "";
+
/** JSON object mapper for serialization/deserialization */
private final ObjectMapper objectMapper;
+ /** Base URL for the server transport */
+ private final String baseUrl;
+
/** The endpoint path for handling client messages */
private final String messageEndpoint;
@@ -108,7 +113,22 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
*/
public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint,
String sseEndpoint) {
+ this(objectMapper, DEFAULT_BASE_URL, messageEndpoint, sseEndpoint);
+ }
+
+ /**
+ * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
+ * endpoint.
+ * @param objectMapper The JSON object mapper to use for message
+ * serialization/deserialization
+ * @param baseUrl The base URL for the server transport
+ * @param messageEndpoint The endpoint path where clients will send their messages
+ * @param sseEndpoint The endpoint path where clients will establish SSE connections
+ */
+ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
+ String sseEndpoint) {
this.objectMapper = objectMapper;
+ this.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
}
@@ -203,7 +223,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
this.sessions.put(sessionId, session);
// Send initial endpoint event
- this.sendEvent(writer, ENDPOINT_EVENT_TYPE, messageEndpoint + "?sessionId=" + sessionId);
+ this.sendEvent(writer, ENDPOINT_EVENT_TYPE, this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
}
/**
@@ -449,6 +469,8 @@ public static class Builder {
private ObjectMapper objectMapper = new ObjectMapper();
+ private String baseUrl = DEFAULT_BASE_URL;
+
private String messageEndpoint;
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
@@ -464,6 +486,17 @@ public Builder objectMapper(ObjectMapper objectMapper) {
return this;
}
+ /**
+ * Sets the base URL for the server transport.
+ * @param baseUrl The base URL to use
+ * @return This builder instance for method chaining
+ */
+ public Builder baseUrl(String baseUrl) {
+ Assert.notNull(baseUrl, "Base URL must not be null");
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
/**
* Sets the endpoint path where clients will send their messages.
* @param messageEndpoint The message endpoint path
@@ -502,7 +535,7 @@ public HttpServletSseServerTransportProvider build() {
if (messageEndpoint == null) {
throw new IllegalStateException("MessageEndpoint must be set");
}
- return new HttpServletSseServerTransportProvider(objectMapper, messageEndpoint, sseEndpoint);
+ return new HttpServletSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint);
}
}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
new file mode 100644
index 000000000..1254e2ad8
--- /dev/null
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 - 2024 the original author or authors.
+ */
+package io.modelcontextprotocol.server.transport;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleState;
+import org.apache.catalina.startup.Tomcat;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HttpServletSseServerCustomContextPathTests {
+
+ private static final int PORT = 8195;
+
+ private static final String CUSTOM_CONTEXT_PATH = "/api/v1";
+
+ private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
+
+ private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
+
+ private HttpServletSseServerTransportProvider mcpServerTransportProvider;
+
+ McpClient.SyncSpec clientBuilder;
+
+ private Tomcat tomcat;
+
+ @BeforeEach
+ public void before() {
+
+ // Create and configure the transport provider
+ mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
+ .objectMapper(new ObjectMapper())
+ .baseUrl(CUSTOM_CONTEXT_PATH)
+ .messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
+ .sseEndpoint(CUSTOM_SSE_ENDPOINT)
+ .build();
+
+ tomcat = TomcatTestUtil.createTomcatServer(CUSTOM_CONTEXT_PATH, PORT, mcpServerTransportProvider);
+
+ try {
+ tomcat.start();
+ assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Failed to start Tomcat", e);
+ }
+
+ this.clientBuilder = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT)
+ .sseEndpoint(CUSTOM_CONTEXT_PATH + CUSTOM_SSE_ENDPOINT)
+ .build());
+ }
+
+ @AfterEach
+ public void after() {
+ if (mcpServerTransportProvider != null) {
+ mcpServerTransportProvider.closeGracefully().block();
+ }
+ if (tomcat != null) {
+ try {
+ tomcat.stop();
+ tomcat.destroy();
+ }
+ catch (LifecycleException e) {
+ throw new RuntimeException("Failed to stop Tomcat", e);
+ }
+ }
+ }
+
+ @Test
+ void testCustomContextPath() {
+ McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build();
+ var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build();
+ assertThat(client.initialize()).isNotNull();
+ }
+
+}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
index 1cd395e74..b04940c79 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
@@ -26,7 +26,6 @@
import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.Tool;
-import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.startup.Tomcat;
@@ -59,14 +58,6 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
@BeforeEach
public void before() {
- tomcat = new Tomcat();
- tomcat.setPort(PORT);
-
- String baseDir = System.getProperty("java.io.tmpdir");
- tomcat.setBaseDir(baseDir);
-
- Context context = tomcat.addContext("", baseDir);
-
// Create and configure the transport provider
mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
.objectMapper(new ObjectMapper())
@@ -74,18 +65,8 @@ public void before() {
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
.build();
- // Add transport servlet to Tomcat
- org.apache.catalina.Wrapper wrapper = context.createWrapper();
- wrapper.setName("mcpServlet");
- wrapper.setServlet(mcpServerTransportProvider);
- wrapper.setLoadOnStartup(1);
- wrapper.setAsyncSupported(true);
- context.addChild(wrapper);
- context.addServletMappingDecoded("/*", "mcpServlet");
-
+ tomcat = TomcatTestUtil.createTomcatServer("", PORT, mcpServerTransportProvider);
try {
- var connector = tomcat.getConnector();
- connector.setAsyncTimeout(3000);
tomcat.start();
assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java
new file mode 100644
index 000000000..6f922dfa6
--- /dev/null
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java
@@ -0,0 +1,45 @@
+/*
+* Copyright 2025 - 2025 the original author or authors.
+*/
+package io.modelcontextprotocol.server.transport;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.Servlet;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleState;
+import org.apache.catalina.startup.Tomcat;
+
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author Christian Tzolov
+ */
+public class TomcatTestUtil {
+
+ public static Tomcat createTomcatServer(String contextPath, int port, Servlet servlet) {
+
+ var tomcat = new Tomcat();
+ tomcat.setPort(port);
+
+ String baseDir = System.getProperty("java.io.tmpdir");
+ tomcat.setBaseDir(baseDir);
+
+ // Context context = tomcat.addContext("", baseDir);
+ Context context = tomcat.addContext(contextPath, baseDir);
+
+ // Add transport servlet to Tomcat
+ org.apache.catalina.Wrapper wrapper = context.createWrapper();
+ wrapper.setName("mcpServlet");
+ wrapper.setServlet(servlet);
+ wrapper.setLoadOnStartup(1);
+ wrapper.setAsyncSupported(true);
+ context.addChild(wrapper);
+ context.addServletMappingDecoded("/*", "mcpServlet");
+
+ var connector = tomcat.getConnector();
+ connector.setAsyncTimeout(3000);
+
+ return tomcat;
+ }
+
+}
From 13c4474b3ea00e75be47b653d315ba9de7125cb3 Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Wed, 9 Apr 2025 16:05:07 +0200
Subject: [PATCH 009/249] Change the URLs used to test blocking rest calls
Signed-off-by: Christian Tzolov
---
.../io/modelcontextprotocol/WebFluxSseIntegrationTests.java | 6 +++---
.../server/WebMvcSseIntegrationTests.java | 6 +++---
...tpServletSseServerTransportProviderIntegrationTests.java | 6 +++---
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
index dbfad821f..ac487b6f5 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
@@ -396,7 +396,7 @@ void testToolCallSuccess(String clientType) {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
@@ -436,7 +436,7 @@ void testToolListChangeHandlingSuccess(String clientType) {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
@@ -453,7 +453,7 @@ void testToolListChangeHandlingSuccess(String clientType) {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
index f9190fd70..420f4b987 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
@@ -388,7 +388,7 @@ void testToolCallSuccess() {
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
// perform a blocking call to a remote service
String response = RestClient.create()
- .get()
+ .get()https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md
.uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
.retrieve()
.body(String.class);
@@ -424,7 +424,7 @@ void testToolListChangeHandlingSuccess() {
McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
// perform a blocking call to a remote service
- String response = RestClient.create()
+ String https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md
.get()
.uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
.retrieve()
@@ -441,7 +441,7 @@ void testToolListChangeHandlingSuccess() {
AtomicReference> rootsRef = new AtomicReference<>();
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
// perform a blocking call to a remote service
- String response = RestClient.create()
+ String https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md
.get()
.uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
.retrieve()
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
index b04940c79..e34baf9d6 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
@@ -374,7 +374,7 @@ void testToolCallSuccess() {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
@@ -411,7 +411,7 @@ void testToolListChangeHandlingSuccess() {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
@@ -428,7 +428,7 @@ void testToolListChangeHandlingSuccess() {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
From fbea833384c097a46927624f1f7cbb9562c15e74 Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Wed, 9 Apr 2025 16:17:16 +0200
Subject: [PATCH 010/249] Fix compilation issue introduced by the previous
commit
Signed-off-by: Christian Tzolov
---
.../server/WebMvcSseIntegrationTests.java | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
index 420f4b987..c203e3bd5 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
@@ -388,8 +388,8 @@ void testToolCallSuccess() {
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
// perform a blocking call to a remote service
String response = RestClient.create()
- .get()https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .get()
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
@@ -424,9 +424,9 @@ void testToolListChangeHandlingSuccess() {
McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
// perform a blocking call to a remote service
- String https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md
+ String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
@@ -441,9 +441,9 @@ void testToolListChangeHandlingSuccess() {
AtomicReference> rootsRef = new AtomicReference<>();
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
// perform a blocking call to a remote service
- String https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md
+ String response = RestClient.create()
.get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
+ .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
From fab434c088e7e90ad4cbbedd55b28c553536c7de Mon Sep 17 00:00:00 2001
From: "a.darafeyeu"
Date: Mon, 7 Apr 2025 14:30:04 +0200
Subject: [PATCH 011/249] refactor(client): enhance
HttpClientSseClientTransport with flexible customization API (#117)
- Add builder customizeClient() and customizeRequest() methods
- Enable HTTP client and request configuration through consumer-based customization
- Deprecate direct constructors in favor of the more flexible builder approach
- Add test coverage for customization capabilities
Co-authored-by: Christian Tzolov
Signed-off-by: Christian Tzolov
---
.../server/WebMvcSseIntegrationTests.java | 12 +--
.../HttpClientSseClientTransport.java | 92 ++++++++++++++++--
.../client/HttpSseMcpAsyncClientTests.java | 4 +-
.../client/HttpSseMcpSyncClientTests.java | 2 +-
.../HttpClientSseClientTransportTests.java | 97 ++++++++++++++++++-
5 files changed, 185 insertions(+), 22 deletions(-)
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
index c203e3bd5..d5c9f90ff 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
@@ -44,7 +44,7 @@
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
-public class WebMvcSseIntegrationTests {
+class WebMvcSseIntegrationTests {
private static final int PORT = 8183;
@@ -79,13 +79,13 @@ public void before() {
try {
tomcatServer.tomcat().start();
- assertThat(tomcatServer.tomcat().getServer().getState() == LifecycleState.STARTED);
+ assertThat(tomcatServer.tomcat().getServer().getState()).isEqualTo(LifecycleState.STARTED);
}
catch (Exception e) {
throw new RuntimeException("Failed to start Tomcat", e);
}
- clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT));
+ clientBuilder = McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT).build());
// Get the transport from Spring context
mcpServerTransportProvider = tomcatServer.appContext().getBean(WebMvcSseServerTransportProvider.class);
@@ -200,8 +200,7 @@ void testCreateMessageSuccess() throws InterruptedException {
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- assertThat(response).isNotNull();
- assertThat(response).isEqualTo(callResponse);
+ assertThat(response).isNotNull().isEqualTo(callResponse);
mcpClient.close();
mcpServer.close();
@@ -410,8 +409,7 @@ void testToolCallSuccess() {
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- assertThat(response).isNotNull();
- assertThat(response).isEqualTo(callResponse);
+ assertThat(response).isNotNull().isEqualTo(callResponse);
mcpClient.close();
mcpServer.close();
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
index a5bdd43e2..632d3844a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
@@ -13,6 +13,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -103,7 +104,10 @@ public class HttpClientSseClientTransport implements McpClientTransport {
/**
* Creates a new transport instance with default HTTP client and object mapper.
* @param baseUri the base URI of the MCP server
+ * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
+ * constructor will be removed in future versions.
*/
+ @Deprecated(forRemoval = true)
public HttpClientSseClientTransport(String baseUri) {
this(HttpClient.newBuilder(), baseUri, new ObjectMapper());
}
@@ -114,7 +118,10 @@ public HttpClientSseClientTransport(String baseUri) {
* @param baseUri the base URI of the MCP server
* @param objectMapper the object mapper for JSON serialization/deserialization
* @throws IllegalArgumentException if objectMapper or clientBuilder is null
+ * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
+ * constructor will be removed in future versions.
*/
+ @Deprecated(forRemoval = true)
public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, ObjectMapper objectMapper) {
this(clientBuilder, baseUri, DEFAULT_SSE_ENDPOINT, objectMapper);
}
@@ -126,7 +133,10 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String bas
* @param sseEndpoint the SSE endpoint path
* @param objectMapper the object mapper for JSON serialization/deserialization
* @throws IllegalArgumentException if objectMapper or clientBuilder is null
+ * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
+ * constructor will be removed in future versions.
*/
+ @Deprecated(forRemoval = true)
public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, String sseEndpoint,
ObjectMapper objectMapper) {
this(clientBuilder, HttpRequest.newBuilder(), baseUri, sseEndpoint, objectMapper);
@@ -141,18 +151,37 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String bas
* @param sseEndpoint the SSE endpoint path
* @param objectMapper the object mapper for JSON serialization/deserialization
* @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
+ * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This
+ * constructor will be removed in future versions.
*/
+ @Deprecated(forRemoval = true)
public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpRequest.Builder requestBuilder,
String baseUri, String sseEndpoint, ObjectMapper objectMapper) {
+ this(clientBuilder.connectTimeout(Duration.ofSeconds(10)).build(), requestBuilder, baseUri, sseEndpoint,
+ objectMapper);
+ }
+
+ /**
+ * Creates a new transport instance with custom HTTP client builder, object mapper,
+ * and headers.
+ * @param httpClient the HTTP client to use
+ * @param requestBuilder the HTTP request builder to use
+ * @param baseUri the base URI of the MCP server
+ * @param sseEndpoint the SSE endpoint path
+ * @param objectMapper the object mapper for JSON serialization/deserialization
+ * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
+ */
+ HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri,
+ String sseEndpoint, ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.hasText(baseUri, "baseUri must not be empty");
Assert.hasText(sseEndpoint, "sseEndpoint must not be empty");
- Assert.notNull(clientBuilder, "clientBuilder must not be null");
+ Assert.notNull(httpClient, "httpClient must not be null");
Assert.notNull(requestBuilder, "requestBuilder must not be null");
this.baseUri = baseUri;
this.sseEndpoint = sseEndpoint;
this.objectMapper = objectMapper;
- this.httpClient = clientBuilder.connectTimeout(Duration.ofSeconds(10)).build();
+ this.httpClient = httpClient;
this.requestBuilder = requestBuilder;
this.sseClient = new FlowSseClient(this.httpClient, requestBuilder);
@@ -164,7 +193,7 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques
* @return a new builder instance
*/
public static Builder builder(String baseUri) {
- return new Builder(baseUri);
+ return new Builder().baseUri(baseUri);
}
/**
@@ -172,25 +201,50 @@ public static Builder builder(String baseUri) {
*/
public static class Builder {
- private final String baseUri;
+ private String baseUri;
private String sseEndpoint = DEFAULT_SSE_ENDPOINT;
- private HttpClient.Builder clientBuilder = HttpClient.newBuilder();
+ private HttpClient.Builder clientBuilder = HttpClient.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .connectTimeout(Duration.ofSeconds(10));
private ObjectMapper objectMapper = new ObjectMapper();
- private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
+ private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
+ .header("Content-Type", "application/json");
+
+ /**
+ * Creates a new builder instance.
+ */
+ Builder() {
+ // Default constructor
+ }
/**
* Creates a new builder with the specified base URI.
* @param baseUri the base URI of the MCP server
+ * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead.
+ * This constructor is deprecated and will be removed or made {@code protected} or
+ * {@code private} in a future release.
*/
+ @Deprecated(forRemoval = true)
public Builder(String baseUri) {
Assert.hasText(baseUri, "baseUri must not be empty");
this.baseUri = baseUri;
}
+ /**
+ * Sets the base URI.
+ * @param baseUri the base URI
+ * @return this builder
+ */
+ Builder baseUri(String baseUri) {
+ Assert.hasText(baseUri, "baseUri must not be empty");
+ this.baseUri = baseUri;
+ return this;
+ }
+
/**
* Sets the SSE endpoint path.
* @param sseEndpoint the SSE endpoint path
@@ -213,6 +267,17 @@ public Builder clientBuilder(HttpClient.Builder clientBuilder) {
return this;
}
+ /**
+ * Customizes the HTTP client builder.
+ * @param clientCustomizer the consumer to customize the HTTP client builder
+ * @return this builder
+ */
+ public Builder customizeClient(final Consumer clientCustomizer) {
+ Assert.notNull(clientCustomizer, "clientCustomizer must not be null");
+ clientCustomizer.accept(clientBuilder);
+ return this;
+ }
+
/**
* Sets the HTTP request builder.
* @param requestBuilder the HTTP request builder
@@ -224,6 +289,17 @@ public Builder requestBuilder(HttpRequest.Builder requestBuilder) {
return this;
}
+ /**
+ * Customizes the HTTP client builder.
+ * @param requestCustomizer the consumer to customize the HTTP request builder
+ * @return this builder
+ */
+ public Builder customizeRequest(final Consumer requestCustomizer) {
+ Assert.notNull(requestCustomizer, "requestCustomizer must not be null");
+ requestCustomizer.accept(requestBuilder);
+ return this;
+ }
+
/**
* Sets the object mapper for JSON serialization/deserialization.
* @param objectMapper the object mapper
@@ -240,7 +316,8 @@ public Builder objectMapper(ObjectMapper objectMapper) {
* @return a new transport instance
*/
public HttpClientSseClientTransport build() {
- return new HttpClientSseClientTransport(clientBuilder, requestBuilder, baseUri, sseEndpoint, objectMapper);
+ return new HttpClientSseClientTransport(clientBuilder.build(), requestBuilder, baseUri, sseEndpoint,
+ objectMapper);
}
}
@@ -336,7 +413,6 @@ public Mono sendMessage(JSONRPCMessage message) {
try {
String jsonText = this.objectMapper.writeValueAsString(message);
HttpRequest request = this.requestBuilder.uri(URI.create(this.baseUri + endpoint))
- .header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonText))
.build();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java
index 15749d4ff..fdff4b777 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java
@@ -15,7 +15,7 @@
*
* @author Christian Tzolov
*/
-@Timeout(15) // Giving extra time beyond the client timeout
+@Timeout(15)
class HttpSseMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
String host = "http://localhost:3004";
@@ -29,7 +29,7 @@ class HttpSseMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
@Override
protected McpClientTransport createMcpTransport() {
- return new HttpClientSseClientTransport(host);
+ return HttpClientSseClientTransport.builder(host).build();
}
@Override
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java
index 067f92957..204cf2984 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java
@@ -29,7 +29,7 @@ class HttpSseMcpSyncClientTests extends AbstractMcpSyncClientTests {
@Override
protected McpClientTransport createMcpTransport() {
- return new HttpClientSseClientTransport(host);
+ return HttpClientSseClientTransport.builder(host).build();
}
@Override
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
index 294056fbe..e5178c0ee 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
@@ -4,9 +4,15 @@
package io.modelcontextprotocol.client.transport;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
import java.util.function.Function;
import io.modelcontextprotocol.spec.McpSchema;
@@ -26,6 +32,8 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
/**
* Tests for the {@link HttpClientSseClientTransport} class.
*
@@ -51,8 +59,8 @@ static class TestHttpClientSseClientTransport extends HttpClientSseClientTranspo
private Sinks.Many> events = Sinks.many().unicast().onBackpressureBuffer();
- public TestHttpClientSseClientTransport(String baseUri) {
- super(baseUri);
+ public TestHttpClientSseClientTransport(final String baseUri) {
+ super(HttpClient.newHttpClient(), HttpRequest.newBuilder(), baseUri, "/sse", new ObjectMapper());
}
public int getInboundMessageCount() {
@@ -191,13 +199,14 @@ void testGracefulShutdown() {
StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete();
// Message count should remain 0 after shutdown
- assertThat(transport.getInboundMessageCount()).isEqualTo(0);
+ assertThat(transport.getInboundMessageCount()).isZero();
}
@Test
void testRetryBehavior() {
// Create a client that simulates connection failures
- HttpClientSseClientTransport failingTransport = new HttpClientSseClientTransport("http://non-existent-host");
+ HttpClientSseClientTransport failingTransport = HttpClientSseClientTransport.builder("http://non-existent-host")
+ .build();
// Verify that the transport attempts to reconnect
StepVerifier.create(Mono.delay(Duration.ofSeconds(2))).expectNextCount(1).verifyComplete();
@@ -275,4 +284,84 @@ void testMessageOrderPreservation() {
assertThat(transport.getInboundMessageCount()).isEqualTo(3);
}
+ @Test
+ void testCustomizeClient() {
+ // Create an atomic boolean to verify the customizer was called
+ AtomicBoolean customizerCalled = new AtomicBoolean(false);
+
+ // Create a transport with the customizer
+ HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host)
+ .customizeClient(builder -> {
+ builder.version(HttpClient.Version.HTTP_2);
+ customizerCalled.set(true);
+ })
+ .build();
+
+ // Verify the customizer was called
+ assertThat(customizerCalled.get()).isTrue();
+
+ // Clean up
+ customizedTransport.closeGracefully().block();
+ }
+
+ @Test
+ void testCustomizeRequest() {
+ // Create an atomic boolean to verify the customizer was called
+ AtomicBoolean customizerCalled = new AtomicBoolean(false);
+
+ // Create a reference to store the custom header value
+ AtomicReference headerName = new AtomicReference<>();
+ AtomicReference headerValue = new AtomicReference<>();
+
+ // Create a transport with the customizer
+ HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host)
+ // Create a request customizer that adds a custom header
+ .customizeRequest(builder -> {
+ builder.header("X-Custom-Header", "test-value");
+ customizerCalled.set(true);
+
+ // Create a new request to verify the header was set
+ HttpRequest request = builder.uri(URI.create("http://example.com")).build();
+ headerName.set("X-Custom-Header");
+ headerValue.set(request.headers().firstValue("X-Custom-Header").orElse(null));
+ })
+ .build();
+
+ // Verify the customizer was called
+ assertThat(customizerCalled.get()).isTrue();
+
+ // Verify the header was set correctly
+ assertThat(headerName.get()).isEqualTo("X-Custom-Header");
+ assertThat(headerValue.get()).isEqualTo("test-value");
+
+ // Clean up
+ customizedTransport.closeGracefully().block();
+ }
+
+ @Test
+ void testChainedCustomizations() {
+ // Create atomic booleans to verify both customizers were called
+ AtomicBoolean clientCustomizerCalled = new AtomicBoolean(false);
+ AtomicBoolean requestCustomizerCalled = new AtomicBoolean(false);
+
+ // Create a transport with both customizers chained
+ HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host)
+ .customizeClient(builder -> {
+ builder.connectTimeout(Duration.ofSeconds(30));
+ clientCustomizerCalled.set(true);
+ })
+ .customizeRequest(builder -> {
+ builder.header("X-Api-Key", "test-api-key");
+ requestCustomizerCalled.set(true);
+ })
+ .build();
+
+ // Verify both customizers were called
+ assertThat(clientCustomizerCalled.get()).isTrue();
+ assertThat(requestCustomizerCalled.get()).isTrue();
+
+ // Clean up
+ customizedTransport.closeGracefully().block();
+ }
+
}
From 391ec19fdc346c6d0ebf369f692c370a48339d3d Mon Sep 17 00:00:00 2001
From: Christian Tzolov <1351573+tzolov@users.noreply.github.com>
Date: Thu, 10 Apr 2025 12:26:29 +0200
Subject: [PATCH 012/249] refactor: change notification params type from
Map to Object (#137)
* refactor: change notification params type from Map to Object
This change generalizes the parameter type for notification methods across the MCP framework,
allowing for more flexible parameter passing. Instead of requiring parameters to be structured
as a Map, the API now accepts any Object as parameters.
The primary motivation is to simplify client usage by allowing direct passing of strongly-typed
objects without requiring conversion to a Map first, as demonstrated in the McpAsyncServer
logging notification implementation.
Affected components:
- McpSession interface and implementations
- McpServerTransportProvider interface and implementations
- McpSchema JSONRPCNotification record
---------
Signed-off-by: Christian Tzolov
---
.../transport/WebFluxSseServerTransportProvider.java | 2 +-
.../server/transport/WebMvcSseServerTransportProvider.java | 2 +-
.../io/modelcontextprotocol/server/McpAsyncServer.java | 7 ++-----
.../transport/HttpServletSseServerTransportProvider.java | 2 +-
.../server/transport/StdioServerTransportProvider.java | 2 +-
.../io/modelcontextprotocol/spec/McpClientSession.java | 2 +-
.../main/java/io/modelcontextprotocol/spec/McpSchema.java | 2 +-
.../io/modelcontextprotocol/spec/McpServerSession.java | 2 +-
.../spec/McpServerTransportProvider.java | 4 ++--
.../main/java/io/modelcontextprotocol/spec/McpSession.java | 4 ++--
.../MockMcpServerTransportProvider.java | 2 +-
11 files changed, 14 insertions(+), 17 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
index df8dd0211..be30bd72f 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
@@ -188,7 +188,7 @@ public void setSessionFactory(McpServerSession.Factory sessionFactory) {
* errors if any session fails to receive the message
*/
@Override
- public Mono notifyClients(String method, Map params) {
+ public Mono notifyClients(String method, Object params) {
if (sessions.isEmpty()) {
logger.debug("No active sessions to broadcast message to");
return Mono.empty();
diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
index fa2e357f9..7bd1aa6c9 100644
--- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java
@@ -179,7 +179,7 @@ public void setSessionFactory(McpServerSession.Factory sessionFactory) {
* @return A Mono that completes when the broadcast attempt is finished
*/
@Override
- public Mono notifyClients(String method, Map params) {
+ public Mono notifyClients(String method, Object params) {
if (sessions.isEmpty()) {
logger.debug("No active sessions to broadcast message to");
return Mono.empty();
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index df9386685..ec2a04c9e 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -669,15 +669,12 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN
return Mono.error(new McpError("Logging message must not be null"));
}
- Map params = this.objectMapper.convertValue(loggingMessageNotification,
- new TypeReference
* @param method the name of the notification method to be sent to the counterparty
- * @param params a map of parameters to be sent with the notification
+ * @param params parameters to be sent with the notification
* @return a Mono that completes when the notification has been sent
*/
- Mono sendNotification(String method, Map params);
+ Mono sendNotification(String method, Object params);
/**
* Closes the session and releases any associated resources asynchronously.
diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java
index 3fb19180b..20a8c0cf5 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java
@@ -47,7 +47,7 @@ public void setSessionFactory(Factory sessionFactory) {
}
@Override
- public Mono notifyClients(String method, Map params) {
+ public Mono notifyClients(String method, Object params) {
return session.sendNotification(method, params);
}
From 2895d1589ac3c81366eccfc584c6c733d5846127 Mon Sep 17 00:00:00 2001
From: Christian Tzolov <1351573+tzolov@users.noreply.github.com>
Date: Thu, 10 Apr 2025 13:18:55 +0200
Subject: [PATCH 013/249] fix: Add null check for session in
WebFluxSseServerTransportProvider (#138)
Add error handling to return a 404 NOT_FOUND response when a request
is made with a non-existent session ID. This prevents potential
NullPointerExceptions when processing requests with invalid session IDs.
Signed-off-by: Christian Tzolov
---
.../server/transport/WebFluxSseServerTransportProvider.java | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
index be30bd72f..eed8a53af 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
@@ -306,6 +306,11 @@ private Mono handleMessage(ServerRequest request) {
McpServerSession session = sessions.get(request.queryParam("sessionId").get());
+ if (session == null) {
+ return ServerResponse.status(HttpStatus.NOT_FOUND)
+ .bodyValue(new McpError("Session not found: " + request.queryParam("sessionId").get()));
+ }
+
return request.bodyToMono(String.class).flatMap(body -> {
try {
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
From c88ac937f3e195c7e767c61e5024737c3417ad72 Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Wed, 9 Apr 2025 11:15:16 +0200
Subject: [PATCH 014/249] feat(mcp): refactor logging to use exchange for
targeted client notifications (#132)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refactors the MCP logging system to use the exchange mechanism for sending
logging notifications only to specific client sessions rather than broadcasting to all clients.
- Move logging notification delivery from server-wide broadcast to per-session exchange
- Implement per-session minimum logging level tracking and filtering
- Add proper logging level filtering at the exchange level
- Change setLoggingLevel from notification to request/response pattern (breaking change)
- Deprecate global server.loggingNotification in favor of exchange.loggingNotification
- Add SetLevelRequest record to McpSchema
- Add integration test demonstrating filtered logging notifications
Resolves #131
Signed-off-by: Christian Tzolov
Co-authored-by: Dariusz Jędrzejczyk
---
.../WebFluxSseIntegrationTests.java | 356 +++++++++++------
.../server/WebMvcSseIntegrationTests.java | 260 +++++++------
.../client/AbstractMcpAsyncClientTests.java | 14 +-
.../server/AbstractMcpAsyncServerTests.java | 49 ---
.../server/AbstractMcpSyncServerTests.java | 49 ---
.../client/McpAsyncClient.java | 7 +-
.../client/McpSyncClient.java | 1 -
.../server/McpAsyncServer.java | 31 +-
.../server/McpAsyncServerExchange.java | 44 +++
.../server/McpSyncServer.java | 13 +-
.../server/McpSyncServerExchange.java | 17 +-
.../modelcontextprotocol/spec/McpSchema.java | 5 +
.../client/AbstractMcpAsyncClientTests.java | 14 +-
.../server/AbstractMcpAsyncServerTests.java | 49 ---
.../server/AbstractMcpSyncServerTests.java | 49 ---
...ervletSseServerCustomContextPathTests.java | 11 +-
...rverTransportProviderIntegrationTests.java | 365 ++++++++++++------
17 files changed, 721 insertions(+), 613 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
index ac487b6f5..d71fe1ab0 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java
@@ -4,6 +4,7 @@
package io.modelcontextprotocol;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -111,27 +112,28 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) {
return Mono.just(mock(CallToolResult.class));
});
- McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
+ var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
- // Create client without sampling capabilities
- var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build();
+ try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0"))
+ .build();) {
- assertThat(client.initialize()).isNotNull();
+ assertThat(client.initialize()).isNotNull();
- try {
- client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- }
- catch (McpError e) {
- assertThat(e).isInstanceOf(McpError.class)
- .hasMessage("Client must be configured with sampling capabilities");
+ try {
+ client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }
+ catch (McpError e) {
+ assertThat(e).isInstanceOf(McpError.class)
+ .hasMessage("Client must be configured with sampling capabilities");
+ }
}
+ server.close();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testCreateMessageSuccess(String clientType) throws InterruptedException {
- // Client
var clientBuilder = clientBuilders.get(clientType);
Function samplingHandler = request -> {
@@ -142,13 +144,6 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException {
CreateMessageResult.StopReason.STOP_SEQUENCE);
};
- var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
- .capabilities(ClientCapabilities.builder().sampling().build())
- .sampling(samplingHandler)
- .build();
-
- // Server
-
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
null);
@@ -183,15 +178,19 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException {
.tools(tool)
.build();
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().sampling().build())
+ .sampling(samplingHandler)
+ .build()) {
- CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(response).isNotNull();
- assertThat(response).isEqualTo(callResponse);
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- mcpClient.close();
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+ }
mcpServer.close();
}
@@ -206,41 +205,42 @@ void testRootsSuccess(String clientType) {
List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
AtomicReference> rootsRef = new AtomicReference<>();
+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(roots)
- .build();
+ .build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(rootsRef.get()).isNull();
+ assertThat(rootsRef.get()).isNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(roots);
+ });
- // Remove a root
- mcpClient.removeRoot(roots.get(0).uri());
+ // Remove a root
+ mcpClient.removeRoot(roots.get(0).uri());
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1)));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(roots.get(1)));
+ });
- // Add a new root
- var root3 = new Root("uri3://", "root3");
- mcpClient.addRoot(root3);
+ // Add a new root
+ var root3 = new Root("uri3://", "root3");
+ mcpClient.addRoot(root3);
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -261,21 +261,21 @@ void testRootsWithoutCapability(String clientType) {
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
}).tools(tool).build();
- // Create client without roots capability
- // No roots capability
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build();
+ try (
+ // Create client without roots capability
+ var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) {
- assertThat(mcpClient.initialize()).isNotNull();
+ assertThat(mcpClient.initialize()).isNotNull();
- // Attempt to list roots should fail
- try {
- mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- }
- catch (McpError e) {
- assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
+ // Attempt to list roots should fail
+ try {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }
+ catch (McpError e) {
+ assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
+ }
}
- mcpClient.close();
mcpServer.close();
}
@@ -285,30 +285,31 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
AtomicReference> rootsRef = new AtomicReference<>();
+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(List.of()) // Empty roots list
- .build();
+ .build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ assertThat(mcpClient.initialize()).isNotNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).isEmpty();
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testRootsWithMultipleHandlers(String clientType) {
+
var clientBuilder = clientBuilders.get(clientType);
List roots = List.of(new Root("uri1://", "root1"));
@@ -321,21 +322,21 @@ void testRootsWithMultipleHandlers(String clientType) {
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(roots)
- .build();
+ .build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef1.get()).containsAll(roots);
- assertThat(rootsRef2.get()).containsAll(roots);
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef1.get()).containsAll(roots);
+ assertThat(rootsRef2.get()).containsAll(roots);
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -348,28 +349,26 @@ void testRootsServerCloseWithActiveSubscription(String clientType) {
List roots = List.of(new Root("uri1://", "root1"));
AtomicReference> rootsRef = new AtomicReference<>();
+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(roots)
- .build();
+ .build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(roots);
+ });
+ }
- // Close server while subscription is active
mcpServer.close();
-
- // Verify client can handle server closure gracefully
- mcpClient.close();
}
// ---------------------------------------
@@ -378,9 +377,9 @@ void testRootsServerCloseWithActiveSubscription(String clientType) {
String emptyJsonSchema = """
{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {}
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {}
}
""";
@@ -408,19 +407,19 @@ void testToolCallSuccess(String clientType) {
.tools(tool1)
.build();
- var mcpClient = clientBuilder.build();
+ try (var mcpClient = clientBuilder.build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
+ assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
- CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- assertThat(response).isNotNull();
- assertThat(response).isEqualTo(callResponse);
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -443,13 +442,14 @@ void testToolListChangeHandlingSuccess(String clientType) {
return callResponse;
});
+ AtomicReference> rootsRef = new AtomicReference<>();
+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool1)
.build();
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
+ try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
@@ -458,39 +458,40 @@ void testToolListChangeHandlingSuccess(String clientType) {
.body(String.class);
assertThat(response).isNotBlank();
rootsRef.set(toolsUpdate);
- }).build();
+ }).build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(rootsRef.get()).isNull();
+ assertThat(rootsRef.get()).isNull();
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
+ assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
- mcpServer.notifyToolsListChanged();
+ mcpServer.notifyToolsListChanged();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool1.tool()));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(tool1.tool()));
+ });
- // Remove a tool
- mcpServer.removeTool("tool1");
+ // Remove a tool
+ mcpServer.removeTool("tool1");
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).isEmpty();
+ });
- // Add a new tool
- McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
- new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse);
+ // Add a new tool
+ McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
+ new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
+ (exchange, request) -> callResponse);
- mcpServer.addTool(tool2);
+ mcpServer.addTool(tool2);
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -502,12 +503,115 @@ void testInitialize(String clientType) {
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
- var mcpClient = clientBuilder.build();
+ try (var mcpClient = clientBuilder.build()) {
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+ }
+
+ mcpServer.close();
+ }
+
+ // ---------------------------------------
+ // Logging Tests
+ // ---------------------------------------
+
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testLoggingNotification(String clientType) {
+ // Create a list to store received logging notifications
+ List receivedNotifications = new ArrayList<>();
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ var clientBuilder = clientBuilders.get(clientType);
+
+ // Create server with a tool that sends logging notifications
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema),
+ (exchange, request) -> {
+
+ // Create and send notifications with different levels
+
+ //@formatter:off
+ return exchange // This should be filtered out (DEBUG < NOTICE)
+ .loggingNotification(McpSchema.LoggingMessageNotification.builder()
+ .level(McpSchema.LoggingLevel.DEBUG)
+ .logger("test-logger")
+ .data("Debug message")
+ .build())
+ .then(exchange // This should be sent (NOTICE >= NOTICE)
+ .loggingNotification(McpSchema.LoggingMessageNotification.builder()
+ .level(McpSchema.LoggingLevel.NOTICE)
+ .logger("test-logger")
+ .data("Notice message")
+ .build()))
+ .then(exchange // This should be sent (ERROR > NOTICE)
+ .loggingNotification(McpSchema.LoggingMessageNotification.builder()
+ .level(McpSchema.LoggingLevel.ERROR)
+ .logger("test-logger")
+ .data("Error message")
+ .build()))
+ .then(exchange // This should be filtered out (INFO < NOTICE)
+ .loggingNotification(McpSchema.LoggingMessageNotification.builder()
+ .level(McpSchema.LoggingLevel.INFO)
+ .logger("test-logger")
+ .data("Another info message")
+ .build()))
+ .then(exchange // This should be sent (ERROR >= NOTICE)
+ .loggingNotification(McpSchema.LoggingMessageNotification.builder()
+ .level(McpSchema.LoggingLevel.ERROR)
+ .logger("test-logger")
+ .data("Another error message")
+ .build()))
+ .thenReturn(new CallToolResult("Logging test completed", false));
+ //@formatter:on
+ });
- mcpClient.close();
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .capabilities(ServerCapabilities.builder().logging().tools(true).build())
+ .tools(tool)
+ .build();
+
+ try (
+ // Create client with logging notification handler
+ var mcpClient = clientBuilder.loggingConsumer(notification -> {
+ receivedNotifications.add(notification);
+ }).build()) {
+
+ // Initialize client
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ // Set minimum logging level to NOTICE
+ mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE);
+
+ // Call the tool that sends logging notifications
+ CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of()));
+ assertThat(result).isNotNull();
+ assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed");
+
+ // Wait for notifications to be processed
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+
+ // Should have received 3 notifications (1 NOTICE and 2 ERROR)
+ assertThat(receivedNotifications).hasSize(3);
+
+ // First notification should be NOTICE level
+ assertThat(receivedNotifications.get(0).level()).isEqualTo(McpSchema.LoggingLevel.NOTICE);
+ assertThat(receivedNotifications.get(0).logger()).isEqualTo("test-logger");
+ assertThat(receivedNotifications.get(0).data()).isEqualTo("Notice message");
+
+ // Second notification should be ERROR level
+ assertThat(receivedNotifications.get(1).level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
+ assertThat(receivedNotifications.get(1).logger()).isEqualTo("test-logger");
+ assertThat(receivedNotifications.get(1).data()).isEqualTo("Error message");
+
+ // Third notification should be ERROR level
+ assertThat(receivedNotifications.get(2).level()).isEqualTo(McpSchema.LoggingLevel.ERROR);
+ assertThat(receivedNotifications.get(2).logger()).isEqualTo("test-logger");
+ assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message");
+ });
+ }
mcpServer.close();
}
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
index d5c9f90ff..be01365a1 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java
@@ -125,27 +125,34 @@ void testCreateMessageWithoutSamplingCapabilities() {
return Mono.just(mock(CallToolResult.class));
});
- McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
+ //@formatter:off
+ var server = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .tools(tool)
+ .build();
+
+ try (
+ // Create client without sampling capabilities
+ var client = clientBuilder
+ .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0"))
+ .build()) {//@formatter:on
+
+ assertThat(client.initialize()).isNotNull();
- // Create client without sampling capabilities
- var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build();
-
- assertThat(client.initialize()).isNotNull();
-
- try {
- client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- }
- catch (McpError e) {
- assertThat(e).isInstanceOf(McpError.class)
- .hasMessage("Client must be configured with sampling capabilities");
+ try {
+ client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }
+ catch (McpError e) {
+ assertThat(e).isInstanceOf(McpError.class)
+ .hasMessage("Client must be configured with sampling capabilities");
+ }
}
+ server.close();
}
@Test
void testCreateMessageSuccess() throws InterruptedException {
- // Client
-
Function samplingHandler = request -> {
assertThat(request.messages()).hasSize(1);
assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
@@ -154,13 +161,6 @@ void testCreateMessageSuccess() throws InterruptedException {
CreateMessageResult.StopReason.STOP_SEQUENCE);
};
- var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
- .capabilities(ClientCapabilities.builder().sampling().build())
- .sampling(samplingHandler)
- .build();
-
- // Server
-
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
null);
@@ -190,19 +190,25 @@ void testCreateMessageSuccess() throws InterruptedException {
return Mono.just(callResponse);
});
+ //@formatter:off
var mcpServer = McpServer.async(mcpServerTransportProvider)
- .serverInfo("test-server", "1.0.0")
- .tools(tool)
- .build();
+ .serverInfo("test-server", "1.0.0")
+ .tools(tool)
+ .build();
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ try (
+ var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
+ .capabilities(ClientCapabilities.builder().sampling().build())
+ .sampling(samplingHandler)
+ .build()) {//@formatter:on
- CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(response).isNotNull().isEqualTo(callResponse);
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- mcpClient.close();
+ assertThat(response).isNotNull().isEqualTo(callResponse);
+ }
mcpServer.close();
}
@@ -214,41 +220,42 @@ void testRootsSuccess() {
List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
AtomicReference> rootsRef = new AtomicReference<>();
+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(roots)
- .build();
+ .build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(rootsRef.get()).isNull();
+ assertThat(rootsRef.get()).isNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(roots);
+ });
- // Remove a root
- mcpClient.removeRoot(roots.get(0).uri());
+ // Remove a root
+ mcpClient.removeRoot(roots.get(0).uri());
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1)));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(roots.get(1)));
+ });
- // Add a new root
- var root3 = new Root("uri3://", "root3");
- mcpClient.addRoot(root3);
+ // Add a new root
+ var root3 = new Root("uri3://", "root3");
+ mcpClient.addRoot(root3);
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -266,21 +273,22 @@ void testRootsWithoutCapability() {
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
}).tools(tool).build();
- // Create client without roots capability
- // No roots capability
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build();
+ try (
+ // Create client without roots capability
+ // No roots capability
+ var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) {
- assertThat(mcpClient.initialize()).isNotNull();
+ assertThat(mcpClient.initialize()).isNotNull();
- // Attempt to list roots should fail
- try {
- mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- }
- catch (McpError e) {
- assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
+ // Attempt to list roots should fail
+ try {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }
+ catch (McpError e) {
+ assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
+ }
}
- mcpClient.close();
mcpServer.close();
}
@@ -292,20 +300,20 @@ void testRootsNotifciationWithEmptyRootsList() {
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(List.of()) // Empty roots list
- .build();
+ .build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).isEmpty();
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -321,20 +329,20 @@ void testRootsWithMultipleHandlers() {
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(roots)
- .build();
+ .build()) {
- assertThat(mcpClient.initialize()).isNotNull();
+ assertThat(mcpClient.initialize()).isNotNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef1.get()).containsAll(roots);
- assertThat(rootsRef2.get()).containsAll(roots);
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef1.get()).containsAll(roots);
+ assertThat(rootsRef2.get()).containsAll(roots);
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -343,28 +351,26 @@ void testRootsServerCloseWithActiveSubscription() {
List roots = List.of(new Root("uri1://", "root1"));
AtomicReference> rootsRef = new AtomicReference<>();
+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
.build();
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
.roots(roots)
- .build();
+ .build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- mcpClient.rootsListChangedNotification();
+ mcpClient.rootsListChangedNotification();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(roots);
+ });
+ }
- // Close server while subscription is active
mcpServer.close();
-
- // Verify client can handle server closure gracefully
- mcpClient.close();
}
// ---------------------------------------
@@ -400,18 +406,18 @@ void testToolCallSuccess() {
.tools(tool1)
.build();
- var mcpClient = clientBuilder.build();
+ try (var mcpClient = clientBuilder.build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
+ assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
- CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- assertThat(response).isNotNull().isEqualTo(callResponse);
+ assertThat(response).isNotNull().isEqualTo(callResponse);
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -431,13 +437,14 @@ void testToolListChangeHandlingSuccess() {
return callResponse;
});
+ AtomicReference> rootsRef = new AtomicReference<>();
+
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool1)
.build();
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
+ try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
@@ -446,39 +453,40 @@ void testToolListChangeHandlingSuccess() {
.body(String.class);
assertThat(response).isNotBlank();
rootsRef.set(toolsUpdate);
- }).build();
+ }).build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- assertThat(rootsRef.get()).isNull();
+ assertThat(rootsRef.get()).isNull();
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
+ assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
- mcpServer.notifyToolsListChanged();
+ mcpServer.notifyToolsListChanged();
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool1.tool()));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(tool1.tool()));
+ });
- // Remove a tool
- mcpServer.removeTool("tool1");
+ // Remove a tool
+ mcpServer.removeTool("tool1");
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).isEmpty();
+ });
- // Add a new tool
- McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
- new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse);
+ // Add a new tool
+ McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
+ new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
+ (exchange, request) -> callResponse);
- mcpServer.addTool(tool2);
+ mcpServer.addTool(tool2);
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
- });
+ await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+ assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
+ });
+ }
- mcpClient.close();
mcpServer.close();
}
@@ -487,12 +495,12 @@ void testInitialize() {
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
- var mcpClient = clientBuilder.build();
+ try (var mcpClient = clientBuilder.build()) {
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+ }
- mcpClient.close();
mcpServer.close();
}
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java
index 713563519..5452c8eac 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java
@@ -31,6 +31,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -453,15 +454,10 @@ void testLoggingLevelsWithoutInitialization() {
@Test
void testLoggingLevels() {
withClient(createMcpTransport(), mcpAsyncClient -> {
- Mono testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> {
- Mono chain = Mono.empty();
- for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
- chain = chain.then(mcpAsyncClient.setLoggingLevel(level));
- }
- return chain;
- }));
-
- StepVerifier.create(testAllLevels).verifyComplete();
+ StepVerifier
+ .create(mcpAsyncClient.initialize()
+ .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel)))
+ .verifyComplete();
});
}
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
index 7bcb9a8b2..a91632c6c 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
@@ -416,53 +416,4 @@ void testRootsChangeHandlers() {
.doesNotThrowAnyException();
}
- // ---------------------------------------
- // Logging Tests
- // ---------------------------------------
-
- @Test
- void testLoggingLevels() {
- var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- // Test all logging levels
- for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(level)
- .logger("test-logger")
- .data("Test message with level " + level)
- .build();
-
- StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete();
- }
- }
-
- @Test
- void testLoggingWithoutCapability() {
- var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().build()) // No logging capability
- .build();
-
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(McpSchema.LoggingLevel.INFO)
- .logger("test-logger")
- .data("Test log message")
- .build();
-
- StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete();
- }
-
- @Test
- void testLoggingWithNullNotification() {
- var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class);
- }
-
}
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
index 7846e053b..9a63143c9 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
@@ -388,53 +388,4 @@ void testRootsChangeHandlers() {
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
}
- // ---------------------------------------
- // Logging Tests
- // ---------------------------------------
-
- @Test
- void testLoggingLevels() {
- var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- // Test all logging levels
- for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(level)
- .logger("test-logger")
- .data("Test message with level " + level)
- .build();
-
- assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException();
- }
- }
-
- @Test
- void testLoggingWithoutCapability() {
- var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().build()) // No logging capability
- .build();
-
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(McpSchema.LoggingLevel.INFO)
- .logger("test-logger")
- .data("Test log message")
- .build();
-
- assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException();
- }
-
- @Test
- void testLoggingWithNullNotification() {
- var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class);
- }
-
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index ce49b0a5e..df099836d 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -786,10 +786,9 @@ public Mono setLoggingLevel(LoggingLevel loggingLevel) {
}
return this.withInitializationCheck("setting logging level", initializedResult -> {
- String levelName = this.transport.unmarshalFrom(loggingLevel, new TypeReference() {
- });
- Map params = Map.of("level", levelName);
- return this.mcpSession.sendNotification(McpSchema.METHOD_LOGGING_SET_LEVEL, params);
+ var params = new McpSchema.SetLevelRequest(loggingLevel);
+ return this.mcpSession.sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, new TypeReference