From f348a83e5acef05b6c8807c7000c59098b667d28 Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Thu, 10 Apr 2025 18:06:46 +0200
Subject: [PATCH 001/337] Next development version
Signed-off-by: Christian Tzolov
---
mcp-bom/pom.xml | 2 +-
mcp-spring/mcp-spring-webflux/pom.xml | 6 +++---
mcp-spring/mcp-spring-webmvc/pom.xml | 6 +++---
mcp-test/pom.xml | 4 ++--
mcp/pom.xml | 2 +-
pom.xml | 2 +-
6 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml
index 77d55da..4f24f71 100644
--- a/mcp-bom/pom.xml
+++ b/mcp-bom/pom.xml
@@ -7,7 +7,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTmcp-bom
diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml
index 186ade7..63c32a8 100644
--- a/mcp-spring/mcp-spring-webflux/pom.xml
+++ b/mcp-spring/mcp-spring-webflux/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOT../../pom.xmlmcp-spring-webflux
@@ -25,13 +25,13 @@
io.modelcontextprotocol.sdkmcp
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTio.modelcontextprotocol.sdkmcp-test
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTtest
diff --git a/mcp-spring/mcp-spring-webmvc/pom.xml b/mcp-spring/mcp-spring-webmvc/pom.xml
index 67e6b0a..b59be6a 100644
--- a/mcp-spring/mcp-spring-webmvc/pom.xml
+++ b/mcp-spring/mcp-spring-webmvc/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOT../../pom.xmlmcp-spring-webmvc
@@ -25,13 +25,13 @@
io.modelcontextprotocol.sdkmcp
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTio.modelcontextprotocol.sdkmcp-test
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTtest
diff --git a/mcp-test/pom.xml b/mcp-test/pom.xml
index 95f5dc3..f1484ae 100644
--- a/mcp-test/pom.xml
+++ b/mcp-test/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTmcp-testjar
@@ -24,7 +24,7 @@
io.modelcontextprotocol.sdkmcp
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOT
diff --git a/mcp/pom.xml b/mcp/pom.xml
index edb1c8f..6b0f4a9 100644
--- a/mcp/pom.xml
+++ b/mcp/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTmcpjar
diff --git a/pom.xml b/pom.xml
index 8e7cca2..ff485b7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.modelcontextprotocol.sdkmcp-parent
- 0.9.0-SNAPSHOT
+ 0.10.0-SNAPSHOTpomhttps://github.com/modelcontextprotocol/java-sdk
From 8068854227cb378e44dcfe2985c5b002b65626e2 Mon Sep 17 00:00:00 2001
From: James Ward
Date: Mon, 14 Apr 2025 23:41:41 -0600
Subject: [PATCH 002/337] add access to server instructions (#148)
---
.../client/McpAsyncClient.java | 15 +++++++++++++++
.../client/McpSyncClient.java | 9 +++++++++
2 files changed, 24 insertions(+)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index df09983..1a9c393 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -112,6 +112,11 @@ public class McpAsyncClient {
*/
private McpSchema.ServerCapabilities serverCapabilities;
+ /**
+ * Server instructions.
+ */
+ private String serverInstructions;
+
/**
* Server implementation information.
*/
@@ -240,6 +245,15 @@ public class McpAsyncClient {
return this.serverCapabilities;
}
+ /**
+ * Get the server instructions that provide guidance to the client on how to interact
+ * with this server.
+ * @return The server instructions
+ */
+ public String getServerInstructions() {
+ return this.serverInstructions;
+ }
+
/**
* Get the server implementation information.
* @return The server implementation details
@@ -328,6 +342,7 @@ public class McpAsyncClient {
return result.flatMap(initializeResult -> {
this.serverCapabilities = initializeResult.capabilities();
+ this.serverInstructions = initializeResult.instructions();
this.serverInfo = initializeResult.serverInfo();
logger.info("Server response with Protocol: {}, Capabilities: {}, Info: {} and Instructions {}",
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
index 32cf325..8544c36 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
@@ -79,6 +79,15 @@ public class McpSyncClient implements AutoCloseable {
return this.delegate.getServerCapabilities();
}
+ /**
+ * Get the server instructions that provide guidance to the client on how to interact
+ * with this server.
+ * @return The instructions
+ */
+ public String getServerInstructions() {
+ return this.delegate.getServerInstructions();
+ }
+
/**
* Get the server implementation information.
* @return The server implementation details
From 263e3741b285f92baf87ff166e8d6dc3eafe9124 Mon Sep 17 00:00:00 2001
From: "a.darafeyeu"
Date: Wed, 9 Apr 2025 19:43:29 +0200
Subject: [PATCH 003/337] feat(test): Use dynamic port allocation in
integration tests (#133)
- Add TestUtil class with findAvailablePort() method to the mcp-test module
- Add findAvailablePort() method to TomcatTestUtil classes
- Replace hardcoded port numbers with dynamic port allocation
Signed-off-by: Christian Tzolov
---
.../WebFluxSseIntegrationTests.java | 7 +++--
.../server/WebFluxSseMcpAsyncServerTests.java | 2 +-
.../server/WebFluxSseMcpSyncServerTests.java | 2 +-
.../server/TomcatTestUtil.java | 10 +++++-
.../WebMvcSseAsyncServerTransportTests.java | 3 +-
.../WebMvcSseCustomContextPathTests.java | 8 ++---
.../server/WebMvcSseIntegrationTests.java | 6 ++--
.../WebMvcSseSyncServerTransportTests.java | 3 +-
.../modelcontextprotocol/server/TestUtil.java | 31 +++++++++++++++++++
...ervletSseServerCustomContextPathTests.java | 7 +++--
...rverTransportProviderIntegrationTests.java | 9 +++---
.../server/transport/TomcatTestUtil.java | 28 ++++++++++++++---
12 files changed, 87 insertions(+), 29 deletions(-)
create mode 100644 mcp-test/src/main/java/io/modelcontextprotocol/server/TestUtil.java
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 76f908b..214b97f 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
@@ -18,6 +18,7 @@ import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.TestUtil;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
@@ -50,9 +51,9 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
-public class WebFluxSseIntegrationTests {
+class WebFluxSseIntegrationTests {
- private static final int PORT = 8182;
+ private static final int PORT = TestUtil.findAvailablePort();
private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
@@ -133,7 +134,7 @@ public class WebFluxSseIntegrationTests {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
- void testCreateMessageSuccess(String clientType) throws InterruptedException {
+ void testCreateMessageSuccess(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java
index 98844c7..cc33e7b 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java
@@ -23,7 +23,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
@Timeout(15) // Giving extra time beyond the client timeout
class WebFluxSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests {
- private static final int PORT = 8181;
+ private static final int PORT = TestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java
index 7107285..2fc1045 100644
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java
+++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java
@@ -23,7 +23,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
@Timeout(15) // Giving extra time beyond the client timeout
class WebFluxSseMcpSyncServerTests extends AbstractMcpSyncServerTests {
- private static final int PORT = 8182;
+ private static final int PORT = TestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
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
index fcd7fb4..ccf9e2d 100644
--- 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
@@ -3,6 +3,10 @@
*/
package io.modelcontextprotocol.server;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
@@ -14,10 +18,14 @@ import org.springframework.web.servlet.DispatcherServlet;
*/
public class TomcatTestUtil {
+ TomcatTestUtil() {
+ // Prevent instantiation
+ }
+
public record TomcatServer(Tomcat tomcat, AnnotationConfigWebApplicationContext appContext) {
}
- public TomcatServer createTomcatServer(String contextPath, int port, Class> componentClass) {
+ public static TomcatServer createTomcatServer(String contextPath, int port, Class> componentClass) {
// Set up Tomcat first
var tomcat = new Tomcat();
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java
index 08d5de6..6a6ad17 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java
@@ -25,7 +25,7 @@ class WebMvcSseAsyncServerTransportTests extends AbstractMcpAsyncServerTests {
private static final String MESSAGE_ENDPOINT = "/mcp/message";
- private static final int PORT = 8181;
+ private static final int PORT = TestUtil.findAvailablePort();
private Tomcat tomcat;
@@ -73,7 +73,6 @@ class WebMvcSseAsyncServerTransportTests extends AbstractMcpAsyncServerTests {
// 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);
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
index 0e81104..1b5218c 100644
--- 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
@@ -22,11 +22,11 @@ import org.springframework.web.servlet.function.ServerResponse;
import static org.assertj.core.api.Assertions.assertThat;
-public class WebMvcSseCustomContextPathTests {
+class WebMvcSseCustomContextPathTests {
private static final String CUSTOM_CONTEXT_PATH = "/app/1";
- private static final int PORT = 8183;
+ private static final int PORT = TestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
@@ -39,11 +39,11 @@ public class WebMvcSseCustomContextPathTests {
@BeforeEach
public void before() {
- tomcatServer = new TomcatTestUtil().createTomcatServer(CUSTOM_CONTEXT_PATH, PORT, TestConfig.class);
+ tomcatServer = TomcatTestUtil.createTomcatServer(CUSTOM_CONTEXT_PATH, PORT, TestConfig.class);
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);
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 be01365..df527f8 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
@@ -46,7 +46,7 @@ import static org.mockito.Mockito.mock;
class WebMvcSseIntegrationTests {
- private static final int PORT = 8183;
+ private static final int PORT = TestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
@@ -75,7 +75,7 @@ class WebMvcSseIntegrationTests {
@BeforeEach
public void before() {
- tomcatServer = new TomcatTestUtil().createTomcatServer("", PORT, TestConfig.class);
+ tomcatServer = TomcatTestUtil.createTomcatServer("", PORT, TestConfig.class);
try {
tomcatServer.tomcat().start();
@@ -151,7 +151,7 @@ class WebMvcSseIntegrationTests {
}
@Test
- void testCreateMessageSuccess() throws InterruptedException {
+ void testCreateMessageSuccess() {
Function samplingHandler = request -> {
assertThat(request.messages()).hasSize(1);
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java
index b85bed3..1964703 100644
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java
+++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java
@@ -24,7 +24,7 @@ class WebMvcSseSyncServerTransportTests extends AbstractMcpSyncServerTests {
private static final String MESSAGE_ENDPOINT = "/mcp/message";
- private static final int PORT = 8181;
+ private static final int PORT = TestUtil.findAvailablePort();
private Tomcat tomcat;
@@ -72,7 +72,6 @@ class WebMvcSseSyncServerTransportTests extends AbstractMcpSyncServerTests {
// 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);
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/TestUtil.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/TestUtil.java
new file mode 100644
index 0000000..0085f31
--- /dev/null
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/TestUtil.java
@@ -0,0 +1,31 @@
+/*
+* Copyright 2025 - 2025 the original author or authors.
+*/
+package io.modelcontextprotocol.server;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+
+public class TestUtil {
+
+ TestUtil() {
+ // Prevent instantiation
+ }
+
+ /**
+ * Finds an available port on the local machine.
+ * @return an available port number
+ * @throws IllegalStateException if no available port can be found
+ */
+ public static int findAvailablePort() {
+ try (final ServerSocket socket = new ServerSocket()) {
+ socket.bind(new InetSocketAddress(0));
+ return socket.getLocalPort();
+ }
+ catch (final IOException e) {
+ throw new IllegalStateException("Cannot bind to an available port!", e);
+ }
+ }
+
+}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
index 212a3c9..2cd6288 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
@@ -4,6 +4,7 @@
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;
@@ -17,9 +18,9 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
-public class HttpServletSseServerCustomContextPathTests {
+class HttpServletSseServerCustomContextPathTests {
- private static final int PORT = 8195;
+ private static final int PORT = TomcatTestUtil.findAvailablePort();
private static final String CUSTOM_CONTEXT_PATH = "/api/v1";
@@ -48,7 +49,7 @@ public class HttpServletSseServerCustomContextPathTests {
try {
tomcat.start();
- assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
+ assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
}
catch (Exception e) {
throw new RuntimeException("Failed to start Tomcat", e);
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 135de83..f25ce56 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
@@ -12,6 +12,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
+
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.server.McpServer;
@@ -44,9 +45,9 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
-public class HttpServletSseServerTransportProviderIntegrationTests {
+class HttpServletSseServerTransportProviderIntegrationTests {
- private static final int PORT = 8189;
+ private static final int PORT = TomcatTestUtil.findAvailablePort();
private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
@@ -70,7 +71,7 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
tomcat = TomcatTestUtil.createTomcatServer("", PORT, mcpServerTransportProvider);
try {
tomcat.start();
- assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
+ assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.STARTED);
}
catch (Exception e) {
throw new RuntimeException("Failed to start Tomcat", e);
@@ -133,7 +134,7 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
}
@Test
- void testCreateMessageSuccess() throws InterruptedException {
+ void testCreateMessageSuccess() {
Function samplingHandler = request -> {
assertThat(request.messages()).hasSize(1);
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java
index 6f922df..f61cdc4 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java
@@ -3,19 +3,23 @@
*/
package io.modelcontextprotocol.server.transport;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+
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 {
+ TomcatTestUtil() {
+ // Prevent instantiation
+ }
+
public static Tomcat createTomcatServer(String contextPath, int port, Servlet servlet) {
var tomcat = new Tomcat();
@@ -24,7 +28,6 @@ public class TomcatTestUtil {
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
@@ -42,4 +45,19 @@ public class TomcatTestUtil {
return tomcat;
}
+ /**
+ * Finds an available port on the local machine.
+ * @return an available port number
+ * @throws IllegalStateException if no available port can be found
+ */
+ public static int findAvailablePort() {
+ try (final ServerSocket socket = new ServerSocket()) {
+ socket.bind(new InetSocketAddress(0));
+ return socket.getLocalPort();
+ }
+ catch (final IOException e) {
+ throw new IllegalStateException("Cannot bind to an available port!", e);
+ }
+ }
+
}
From 472f07ec6da7a2233d0d73b2be8ff7b6f9a85233 Mon Sep 17 00:00:00 2001
From: mipengcheng3
Date: Tue, 8 Apr 2025 17:53:59 +0800
Subject: [PATCH 004/337] feat(mcp): add configurable request timeout to MCP
server (#134)
Adds the ability to configure request timeouts for MCP server operations. This enhancement
allows setting a custom duration to wait for server responses before timing out requests,
which applies to all requests made through the client including tool calls, resource access,
and prompt operations.
- Add requestTimeout parameter to McpServerSession constructor
- Add requestTimeout field and builder method to server classes
- Pass timeout configuration through to session creation
- Add tests for both success and failure scenarios across different transport implementations
- Default timeout is set to 10 seconds if not explicitly configured.
---
.../WebFluxSseIntegrationTests.java | 149 ++++++++++++++++++
.../server/WebMvcSseIntegrationTests.java | 145 +++++++++++++++++
.../server/McpAsyncServer.java | 13 +-
.../server/McpServer.java | 39 ++++-
.../spec/McpServerSession.java | 12 +-
...rverTransportProviderIntegrationTests.java | 145 +++++++++++++++++
6 files changed, 491 insertions(+), 12 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 214b97f..dab5437 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
@@ -8,6 +8,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -48,6 +49,7 @@ import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.server.RouterFunctions;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
@@ -196,6 +198,153 @@ class WebFluxSseIntegrationTests {
mcpServer.close();
}
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws InterruptedException {
+
+ // Client
+ var clientBuilder = clientBuilders.get(clientType);
+
+ Function samplingHandler = request -> {
+ assertThat(request.messages()).hasSize(1);
+ assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
+ 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);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Test message"))))
+ .modelPreferences(ModelPreferences.builder()
+ .hints(List.of())
+ .costPriority(1.0)
+ .speedPriority(1.0)
+ .intelligencePriority(1.0)
+ .build())
+ .build();
+
+ StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .requestTimeout(Duration.ofSeconds(4))
+ .serverInfo("test-server", "1.0.0")
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+
+ mcpClient.close();
+ mcpServer.close();
+ }
+
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testCreateMessageWithRequestTimeoutFail(String clientType) throws InterruptedException {
+
+ // Client
+ var clientBuilder = clientBuilders.get(clientType);
+
+ Function samplingHandler = request -> {
+ assertThat(request.messages()).hasSize(1);
+ assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
+ try {
+ TimeUnit.SECONDS.sleep(3);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
+ 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);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Test message"))))
+ .modelPreferences(ModelPreferences.builder()
+ .hints(List.of())
+ .costPriority(1.0)
+ .speedPriority(1.0)
+ .intelligencePriority(1.0)
+ .build())
+ .build();
+
+ StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .requestTimeout(Duration.ofSeconds(1))
+ .serverInfo("test-server", "1.0.0")
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }).withMessageContaining("Timeout");
+
+ mcpClient.close();
+ mcpServer.close();
+ }
+
// ---------------------------------------
// Roots Tests
// ---------------------------------------
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 df527f8..07b36c2 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
@@ -6,6 +6,7 @@ package io.modelcontextprotocol.server;
import java.time.Duration;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@@ -41,6 +42,7 @@ 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.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
@@ -212,6 +214,149 @@ class WebMvcSseIntegrationTests {
mcpServer.close();
}
+ @Test
+ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException {
+
+ // Client
+
+ Function samplingHandler = request -> {
+ assertThat(request.messages()).hasSize(1);
+ assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
+ 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);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Test message"))))
+ .modelPreferences(ModelPreferences.builder()
+ .hints(List.of())
+ .costPriority(1.0)
+ .speedPriority(1.0)
+ .intelligencePriority(1.0)
+ .build())
+ .build();
+
+ StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(4))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+
+ mcpClient.close();
+ mcpServer.close();
+ }
+
+ @Test
+ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException {
+
+ // Client
+
+ Function samplingHandler = request -> {
+ assertThat(request.messages()).hasSize(1);
+ assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
+ 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);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Test message"))))
+ .modelPreferences(ModelPreferences.builder()
+ .hints(List.of())
+ .costPriority(1.0)
+ .speedPriority(1.0)
+ .intelligencePriority(1.0)
+ .build())
+ .build();
+
+ StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(1))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }).withMessageContaining("Timeout");
+
+ mcpClient.close();
+ mcpServer.close();
+ }
+
// ---------------------------------------
// Roots Tests
// ---------------------------------------
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index 062de13..4f7d0e8 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -4,6 +4,7 @@
package io.modelcontextprotocol.server;
+import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -90,8 +91,8 @@ public class McpAsyncServer {
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
*/
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
- McpServerFeatures.Async features) {
- this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, features);
+ McpServerFeatures.Async features, Duration requestTimeout) {
+ this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features);
}
/**
@@ -271,7 +272,7 @@ public class McpAsyncServer {
private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
- McpServerFeatures.Async features) {
+ Duration requestTimeout, McpServerFeatures.Async features) {
this.mcpTransportProvider = mcpTransportProvider;
this.objectMapper = objectMapper;
this.serverInfo = features.serverInfo();
@@ -330,9 +331,9 @@ public class McpAsyncServer {
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
- mcpTransportProvider
- .setSessionFactory(transport -> new McpServerSession(UUID.randomUUID().toString(), transport,
- this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers));
+ mcpTransportProvider.setSessionFactory(
+ transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport,
+ this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers));
}
// ---------------------------------------
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
index d542733..60434a8 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
@@ -4,6 +4,7 @@
package io.modelcontextprotocol.server;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -193,11 +194,28 @@ public interface McpServer {
private final List, Mono>> rootsChangeHandlers = new ArrayList<>();
+ private Duration requestTimeout = Duration.ofSeconds(10); // Default timeout
+
private AsyncSpecification(McpServerTransportProvider transportProvider) {
Assert.notNull(transportProvider, "Transport provider must not be null");
this.transportProvider = transportProvider;
}
+ /**
+ * Sets the duration to wait for server responses before timing out requests. This
+ * timeout applies to all requests made through the client, including tool calls,
+ * resource access, and prompt operations.
+ * @param requestTimeout The duration to wait before timing out requests. Must not
+ * be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if requestTimeout is null
+ */
+ public AsyncSpecification requestTimeout(Duration requestTimeout) {
+ Assert.notNull(requestTimeout, "Request timeout must not be null");
+ this.requestTimeout = requestTimeout;
+ return this;
+ }
+
/**
* Sets the server implementation information that will be shared with clients
* during connection initialization. This helps with version compatibility,
@@ -565,7 +583,7 @@ public interface McpServer {
var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
this.resources, this.resourceTemplates, this.prompts, this.rootsChangeHandlers, this.instructions);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
- return new McpAsyncServer(this.transportProvider, mapper, features);
+ return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout);
}
}
@@ -619,11 +637,28 @@ public interface McpServer {
private final List>> rootsChangeHandlers = new ArrayList<>();
+ private Duration requestTimeout = Duration.ofSeconds(10); // Default timeout
+
private SyncSpecification(McpServerTransportProvider transportProvider) {
Assert.notNull(transportProvider, "Transport provider must not be null");
this.transportProvider = transportProvider;
}
+ /**
+ * Sets the duration to wait for server responses before timing out requests. This
+ * timeout applies to all requests made through the client, including tool calls,
+ * resource access, and prompt operations.
+ * @param requestTimeout The duration to wait before timing out requests. Must not
+ * be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if requestTimeout is null
+ */
+ public SyncSpecification requestTimeout(Duration requestTimeout) {
+ Assert.notNull(requestTimeout, "Request timeout must not be null");
+ this.requestTimeout = requestTimeout;
+ return this;
+ }
+
/**
* Sets the server implementation information that will be shared with clients
* during connection initialization. This helps with version compatibility,
@@ -992,7 +1027,7 @@ public interface McpServer {
this.instructions);
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
- var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures);
+ var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout);
return new McpSyncServer(asyncServer);
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
index 46014af..46c356c 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
@@ -27,6 +27,9 @@ public class McpServerSession implements McpSession {
private final String id;
+ /** Duration to wait for request responses before timing out */
+ private final Duration requestTimeout;
+
private final AtomicLong requestCounter = new AtomicLong(0);
private final InitRequestHandler initRequestHandler;
@@ -65,10 +68,11 @@ public class McpServerSession implements McpSession {
* @param requestHandlers map of request handlers to use
* @param notificationHandlers map of notification handlers to use
*/
- public McpServerSession(String id, McpServerTransport transport, InitRequestHandler initHandler,
- InitNotificationHandler initNotificationHandler, Map> requestHandlers,
- Map notificationHandlers) {
+ public McpServerSession(String id, Duration requestTimeout, McpServerTransport transport,
+ InitRequestHandler initHandler, InitNotificationHandler initNotificationHandler,
+ Map> requestHandlers, Map notificationHandlers) {
this.id = id;
+ this.requestTimeout = requestTimeout;
this.transport = transport;
this.initRequestHandler = initHandler;
this.initNotificationHandler = initNotificationHandler;
@@ -116,7 +120,7 @@ public class McpServerSession implements McpSession {
this.pendingResponses.remove(requestId);
sink.error(error);
});
- }).timeout(Duration.ofSeconds(10)).handle((jsonRpcResponse, sink) -> {
+ }).timeout(requestTimeout).handle((jsonRpcResponse, sink) -> {
if (jsonRpcResponse.error() != null) {
sink.error(new McpError(jsonRpcResponse.error()));
}
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 f25ce56..b8f040c 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
@@ -7,6 +7,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -42,6 +43,7 @@ import reactor.test.StepVerifier;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
@@ -194,6 +196,149 @@ class HttpServletSseServerTransportProviderIntegrationTests {
mcpServer.close();
}
+ @Test
+ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException {
+
+ // Client
+
+ Function samplingHandler = request -> {
+ assertThat(request.messages()).hasSize(1);
+ assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
+ 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);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Test message"))))
+ .modelPreferences(ModelPreferences.builder()
+ .hints(List.of())
+ .costPriority(1.0)
+ .speedPriority(1.0)
+ .intelligencePriority(1.0)
+ .build())
+ .build();
+
+ StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(3))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+
+ mcpClient.close();
+ mcpServer.close();
+ }
+
+ @Test
+ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException {
+
+ // Client
+
+ Function samplingHandler = request -> {
+ assertThat(request.messages()).hasSize(1);
+ assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
+ 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);
+
+ McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
+
+ var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
+ new McpSchema.TextContent("Test message"))))
+ .modelPreferences(ModelPreferences.builder()
+ .hints(List.of())
+ .costPriority(1.0)
+ .speedPriority(1.0)
+ .intelligencePriority(1.0)
+ .build())
+ .build();
+
+ StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ }).verifyComplete();
+
+ return Mono.just(callResponse);
+ });
+
+ var mcpServer = McpServer.async(mcpServerTransportProvider)
+ .serverInfo("test-server", "1.0.0")
+ .requestTimeout(Duration.ofSeconds(1))
+ .tools(tool)
+ .build();
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }).withMessageContaining("Timeout");
+
+ mcpClient.close();
+ mcpServer.close();
+ }
+
// ---------------------------------------
// Roots Tests
// ---------------------------------------
From f7f8ccd0acb6d39558b65ceb1ae4e5f71619a37c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?=
Date: Mon, 14 Apr 2025 12:08:30 +0200
Subject: [PATCH 005/337] Fix flaky test running blocking code in event loop
(#155)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Replace StepVerifier with assertWith for cleaner test assertions
- Add try-with-resources blocks for proper client resource management
- Use closeGracefully().block() for proper server shutdown
Signed-off-by: Dariusz Jędrzejczyk
---
.../WebFluxSseIntegrationTests.java | 135 ++++++++----------
1 file changed, 62 insertions(+), 73 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 dab5437..6ba0911 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
@@ -37,10 +37,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-import reactor.core.publisher.Mono;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
-import reactor.test.StepVerifier;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
@@ -50,6 +48,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertWith;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
@@ -109,12 +108,9 @@ class WebFluxSseIntegrationTests {
var clientBuilder = clientBuilders.get(clientType);
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
- new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
-
- exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block();
-
- return Mono.just(mock(CallToolResult.class));
- });
+ new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema),
+ (exchange, request) -> exchange.createMessage(mock(CreateMessageRequest.class))
+ .thenReturn(mock(CallToolResult.class)));
var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
@@ -151,6 +147,8 @@ class WebFluxSseIntegrationTests {
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
null);
+ AtomicReference samplingResult = new AtomicReference<>();
+
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
@@ -165,16 +163,9 @@ class WebFluxSseIntegrationTests {
.build())
.build();
- StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
- assertThat(result).isNotNull();
- assertThat(result.role()).isEqualTo(Role.USER);
- assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
- assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
- assertThat(result.model()).isEqualTo("MockModelName");
- assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
- }).verifyComplete();
-
- return Mono.just(callResponse);
+ return exchange.createMessage(craeteMessageRequest)
+ .doOnNext(samplingResult::set)
+ .thenReturn(callResponse);
});
var mcpServer = McpServer.async(mcpServerTransportProvider)
@@ -194,8 +185,17 @@ class WebFluxSseIntegrationTests {
assertThat(response).isNotNull();
assertThat(response).isEqualTo(callResponse);
+
+ assertWith(samplingResult.get(), result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ });
}
- mcpServer.close();
+ mcpServer.closeGracefully().block();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@@ -218,16 +218,13 @@ class WebFluxSseIntegrationTests {
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);
+ AtomicReference samplingResult = new AtomicReference<>();
+
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
@@ -242,16 +239,9 @@ class WebFluxSseIntegrationTests {
.build())
.build();
- StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
- assertThat(result).isNotNull();
- assertThat(result.role()).isEqualTo(Role.USER);
- assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
- assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
- assertThat(result.model()).isEqualTo("MockModelName");
- assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
- }).verifyComplete();
-
- return Mono.just(callResponse);
+ return exchange.createMessage(craeteMessageRequest)
+ .doOnNext(samplingResult::set)
+ .thenReturn(callResponse);
});
var mcpServer = McpServer.async(mcpServerTransportProvider)
@@ -260,16 +250,30 @@ class WebFluxSseIntegrationTests {
.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();
- mcpServer.close();
+ assertThat(response).isNotNull();
+ assertThat(response).isEqualTo(callResponse);
+
+ assertWith(samplingResult.get(), result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.role()).isEqualTo(Role.USER);
+ assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
+ assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
+ assertThat(result.model()).isEqualTo("MockModelName");
+ assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
+ });
+ }
+
+ mcpServer.closeGracefully().block();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@@ -283,7 +287,7 @@ class WebFluxSseIntegrationTests {
assertThat(request.messages()).hasSize(1);
assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
try {
- TimeUnit.SECONDS.sleep(3);
+ TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
@@ -292,11 +296,6 @@ class WebFluxSseIntegrationTests {
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")),
@@ -308,24 +307,9 @@ class WebFluxSseIntegrationTests {
var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Test message"))))
- .modelPreferences(ModelPreferences.builder()
- .hints(List.of())
- .costPriority(1.0)
- .speedPriority(1.0)
- .intelligencePriority(1.0)
- .build())
.build();
- StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
- assertThat(result).isNotNull();
- assertThat(result.role()).isEqualTo(Role.USER);
- assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
- assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
- assertThat(result.model()).isEqualTo("MockModelName");
- assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
- }).verifyComplete();
-
- return Mono.just(callResponse);
+ return exchange.createMessage(craeteMessageRequest).thenReturn(callResponse);
});
var mcpServer = McpServer.async(mcpServerTransportProvider)
@@ -334,15 +318,21 @@ class WebFluxSseIntegrationTests {
.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()) {
- assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
- mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
- }).withMessageContaining("Timeout");
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
- mcpClient.close();
- mcpServer.close();
+ assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
+ mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
+ }).withMessageContaining("within 1000ms");
+
+ }
+
+ mcpServer.closeGracefully().block();
}
// ---------------------------------------
@@ -412,9 +402,8 @@ class WebFluxSseIntegrationTests {
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
}).tools(tool).build();
- try (
- // Create client without roots capability
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) {
+ // Create client without roots capability
+ try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) {
assertThat(mcpClient.initialize()).isNotNull();
From 84adde16a539e2e482bf4deb2c6c3b17b3c148fd Mon Sep 17 00:00:00 2001
From: mackey0225
Date: Tue, 15 Apr 2025 10:48:19 +0900
Subject: [PATCH 006/337] fix: correct typos in variable names, method names,
and commentsj (#159)
---
.../WebFluxSseServerTransportProvider.java | 2 +-
.../WebFluxSseIntegrationTests.java | 6 +++---
.../server/WebMvcSseIntegrationTests.java | 6 +++---
.../server/AbstractMcpAsyncServerTests.java | 2 +-
.../server/AbstractMcpSyncServerTests.java | 16 ++++++++--------
.../modelcontextprotocol/client/McpClient.java | 4 ++--
.../server/McpAsyncServer.java | 2 +-
.../io/modelcontextprotocol/spec/McpSchema.java | 2 +-
.../server/AbstractMcpAsyncServerTests.java | 2 +-
.../server/AbstractMcpSyncServerTests.java | 16 ++++++++--------
...eServerTransportProviderIntegrationTests.java | 6 +++---
11 files changed, 32 insertions(+), 32 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 eed8a53..62264d9 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
@@ -141,7 +141,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
* 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 baseUrl webflux message 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.
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 6ba0911..80a1264 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
@@ -152,7 +152,7 @@ class WebFluxSseIntegrationTests {
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
- var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ var createMessageRequest = McpSchema.CreateMessageRequest.builder()
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Test message"))))
.modelPreferences(ModelPreferences.builder()
@@ -163,7 +163,7 @@ class WebFluxSseIntegrationTests {
.build())
.build();
- return exchange.createMessage(craeteMessageRequest)
+ return exchange.createMessage(createMessageRequest)
.doOnNext(samplingResult::set)
.thenReturn(callResponse);
});
@@ -421,7 +421,7 @@ class WebFluxSseIntegrationTests {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
- void testRootsNotifciationWithEmptyRootsList(String clientType) {
+ void testRootsNotificationWithEmptyRootsList(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
AtomicReference> rootsRef = new AtomicReference<>();
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 07b36c2..b12d684 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
@@ -169,7 +169,7 @@ class WebMvcSseIntegrationTests {
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
- var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ var createMessageRequest = McpSchema.CreateMessageRequest.builder()
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Test message"))))
.modelPreferences(ModelPreferences.builder()
@@ -180,7 +180,7 @@ class WebMvcSseIntegrationTests {
.build())
.build();
- StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ StepVerifier.create(exchange.createMessage(createMessageRequest)).consumeNextWith(result -> {
assertThat(result).isNotNull();
assertThat(result.role()).isEqualTo(Role.USER);
assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
@@ -438,7 +438,7 @@ class WebMvcSseIntegrationTests {
}
@Test
- void testRootsNotifciationWithEmptyRootsList() {
+ void testRootsNotificationWithEmptyRootsList() {
AtomicReference> rootsRef = new AtomicReference<>();
var mcpServer = McpServer.sync(mcpServerTransportProvider)
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 a91632c..cdd43e7 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
@@ -110,7 +110,7 @@ public abstract class AbstractMcpAsyncServerTests {
.build();
StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(newTool,
- (excnage, args) -> Mono.just(new CallToolResult(List.of(), false)))))
+ (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
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 9a63143..c81e638 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
@@ -200,16 +200,16 @@ public abstract class AbstractMcpSyncServerTests {
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
- McpServerFeatures.SyncResourceSpecification specificaiton = new McpServerFeatures.SyncResourceSpecification(
+ McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification(
resource, (exchange, req) -> new ReadResourceResult(List.of()));
- assertThatCode(() -> mcpSyncServer.addResource(specificaiton)).doesNotThrowAnyException();
+ assertThatCode(() -> mcpSyncServer.addResource(specification)).doesNotThrowAnyException();
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}
@Test
- void testAddResourceWithNullSpecifiation() {
+ void testAddResourceWithNullSpecification() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
@@ -279,11 +279,11 @@ public abstract class AbstractMcpSyncServerTests {
.build();
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
- McpServerFeatures.SyncPromptSpecification specificaiton = new McpServerFeatures.SyncPromptSpecification(prompt,
+ McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt,
(exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
- assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specificaiton)).isInstanceOf(McpError.class)
+ assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specification)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with prompt capabilities");
}
@@ -300,14 +300,14 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemovePrompt() {
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
- McpServerFeatures.SyncPromptSpecification specificaiton = new McpServerFeatures.SyncPromptSpecification(prompt,
+ McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt,
(exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
- .prompts(specificaiton)
+ .prompts(specification)
.build();
assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException();
@@ -340,7 +340,7 @@ public abstract class AbstractMcpSyncServerTests {
var singleConsumerServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
- .rootsChangeHandlers(List.of((exchage, roots) -> {
+ .rootsChangeHandlers(List.of((exchange, roots) -> {
consumerCalled[0] = true;
if (!roots.isEmpty()) {
rootsReceived[0] = roots.get(0);
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
index f7b1796..a1dc116 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
@@ -196,7 +196,7 @@ public interface McpClient {
}
/**
- * @param initializationTimeout The duration to wait for the initializaiton
+ * @param initializationTimeout The duration to wait for the initialization
* lifecycle step to complete.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if initializationTimeout is null
@@ -435,7 +435,7 @@ public interface McpClient {
}
/**
- * @param initializationTimeout The duration to wait for the initializaiton
+ * @param initializationTimeout The duration to wait for the initialization
* lifecycle step to complete.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if initializationTimeout is null
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index 4f7d0e8..28b63ce 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -359,7 +359,7 @@ public class McpAsyncServer {
}
else {
logger.warn(
- "Client requested unsupported protocol version: {}, so the server will sugggest the {} version instead",
+ "Client requested unsupported protocol version: {}, so the server will suggest the {} version instead",
initializeRequest.protocolVersion(), serverProtocolVersion);
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
index e621ac1..6eb5159 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -1103,7 +1103,7 @@ public final class McpSchema {
* setting minimum log levels, with servers sending notifications containing severity
* levels, optional logger names, and arbitrary JSON-serializable data.
*
- * @param level The severity levels. The mimimum log level is set by the client.
+ * @param level The severity levels. The minimum log level is set by the client.
* @param logger The logger that generated the message.
* @param data JSON-serializable logging data.
*/
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
index c7c69b5..df0b0c7 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
@@ -109,7 +109,7 @@ public abstract class AbstractMcpAsyncServerTests {
.build();
StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(newTool,
- (excnage, args) -> Mono.just(new CallToolResult(List.of(), false)))))
+ (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
index 8c9328c..0b38da8 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
@@ -199,16 +199,16 @@ public abstract class AbstractMcpSyncServerTests {
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
- McpServerFeatures.SyncResourceSpecification specificaiton = new McpServerFeatures.SyncResourceSpecification(
+ McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification(
resource, (exchange, req) -> new ReadResourceResult(List.of()));
- assertThatCode(() -> mcpSyncServer.addResource(specificaiton)).doesNotThrowAnyException();
+ assertThatCode(() -> mcpSyncServer.addResource(specification)).doesNotThrowAnyException();
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}
@Test
- void testAddResourceWithNullSpecifiation() {
+ void testAddResourceWithNullSpecification() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
@@ -278,11 +278,11 @@ public abstract class AbstractMcpSyncServerTests {
.build();
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
- McpServerFeatures.SyncPromptSpecification specificaiton = new McpServerFeatures.SyncPromptSpecification(prompt,
+ McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt,
(exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
- assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specificaiton)).isInstanceOf(McpError.class)
+ assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specification)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with prompt capabilities");
}
@@ -299,14 +299,14 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemovePrompt() {
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
- McpServerFeatures.SyncPromptSpecification specificaiton = new McpServerFeatures.SyncPromptSpecification(prompt,
+ McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt,
(exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
- .prompts(specificaiton)
+ .prompts(specification)
.build();
assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException();
@@ -339,7 +339,7 @@ public abstract class AbstractMcpSyncServerTests {
var singleConsumerServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
- .rootsChangeHandlers(List.of((exchage, roots) -> {
+ .rootsChangeHandlers(List.of((exchange, roots) -> {
consumerCalled[0] = true;
if (!roots.isEmpty()) {
rootsReceived[0] = roots.get(0);
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 b8f040c..2ff6325 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java
@@ -152,7 +152,7 @@ class HttpServletSseServerTransportProviderIntegrationTests {
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
- var craeteMessageRequest = McpSchema.CreateMessageRequest.builder()
+ var createMessageRequest = McpSchema.CreateMessageRequest.builder()
.messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Test message"))))
.modelPreferences(ModelPreferences.builder()
@@ -163,7 +163,7 @@ class HttpServletSseServerTransportProviderIntegrationTests {
.build())
.build();
- StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> {
+ StepVerifier.create(exchange.createMessage(createMessageRequest)).consumeNextWith(result -> {
assertThat(result).isNotNull();
assertThat(result.role()).isEqualTo(Role.USER);
assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
@@ -417,7 +417,7 @@ class HttpServletSseServerTransportProviderIntegrationTests {
}
@Test
- void testRootsNotifciationWithEmptyRootsList() {
+ void testRootsNotificationWithEmptyRootsList() {
AtomicReference> rootsRef = new AtomicReference<>();
var mcpServer = McpServer.sync(mcpServerTransportProvider)
From 7853efefdabeec61f093b4c048dda0eb95263348 Mon Sep 17 00:00:00 2001
From: jitokim
Date: Fri, 11 Apr 2025 13:36:37 +0900
Subject: [PATCH 007/337] feat(completion): implement completion API support
(#141)
Add completion API support to the MCP protocol implementation:
- Add CompleteRequest and CompleteResult schema classes
- Implement completion capabilities in ServerCapabilities
- Add completion handlers in McpAsyncServer and McpServer
- Add completion client methods in McpAsyncClient and McpSyncClient
- Add CompletionRefKey and completion specifications in McpServerFeatures
- Add integration test for completion functionality
- Fix isPresent() check to use isEmpty() in WebMvcSseServerTransportProvider
- Replace McpServerFeatures.CompletionRefKey by McpSchemaCompleteReference
Co-authored-by: Christian Tzolov
Signed-off-by: jitokim
---
.../WebFluxSseIntegrationTests.java | 65 +++++++++++---
.../WebMvcSseServerTransportProvider.java | 2 +-
.../client/McpAsyncClient.java | 22 +++++
.../client/McpSyncClient.java | 11 +++
.../server/McpAsyncServer.java | 84 +++++++++++++++++++
.../server/McpServer.java | 43 +++++++++-
.../server/McpServerFeatures.java | 71 +++++++++++++++-
.../spec/McpClientSession.java | 1 +
.../modelcontextprotocol/spec/McpSchema.java | 74 ++++++++++++----
9 files changed, 340 insertions(+), 33 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 80a1264..08619bd 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
@@ -10,6 +10,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -20,19 +21,12 @@ import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.TestUtil;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
-import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
-import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
-import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
-import io.modelcontextprotocol.spec.McpSchema.Role;
-import io.modelcontextprotocol.spec.McpSchema.Root;
-import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.Tool;
+import io.modelcontextprotocol.spec.McpSchema.*;
+import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.CompletionCapabilities;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
@@ -759,4 +753,53 @@ class WebFluxSseIntegrationTests {
mcpServer.close();
}
-}
+ // ---------------------------------------
+ // Completion Tests
+ // ---------------------------------------
+ @ParameterizedTest(name = "{0} : Completion call")
+ @ValueSource(strings = { "httpclient", "webflux" })
+ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
+ var clientBuilder = clientBuilders.get(clientType);
+
+ var expectedValues = List.of("python", "pytorch", "pyside");
+ var completionResponse = new McpSchema.CompleteResult(new CompleteResult.CompleteCompletion(expectedValues, 10, // total
+ true // hasMore
+ ));
+
+ AtomicReference samplingRequest = new AtomicReference<>();
+ BiFunction completionHandler = (mcpSyncServerExchange,
+ request) -> {
+ samplingRequest.set(request);
+ return completionResponse;
+ };
+
+ var mcpServer = McpServer.sync(mcpServerTransportProvider)
+ .capabilities(ServerCapabilities.builder().completions(new CompletionCapabilities()).build())
+ .prompts(new McpServerFeatures.SyncPromptSpecification(
+ new Prompt("code_review", "this is code review prompt", List.of()),
+ (mcpSyncServerExchange, getPromptRequest) -> null))
+ .completions(new McpServerFeatures.SyncCompletionSpecification(
+ new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))
+ .build();
+
+ try (var mcpClient = clientBuilder.build()) {
+
+ InitializeResult initResult = mcpClient.initialize();
+ assertThat(initResult).isNotNull();
+
+ CompleteRequest request = new CompleteRequest(new PromptReference("ref/prompt", "code_review"),
+ new CompleteRequest.CompleteArgument("language", "py"));
+
+ CompleteResult result = mcpClient.completeCompletion(request);
+
+ assertThat(result).isNotNull();
+
+ assertThat(samplingRequest.get().argument().name()).isEqualTo("language");
+ assertThat(samplingRequest.get().argument().value()).isEqualTo("py");
+ assertThat(samplingRequest.get().ref().type()).isEqualTo("ref/prompt");
+ }
+
+ mcpServer.close();
+ }
+
+}
\ No newline at end of file
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 7bd1aa6..fc86cfa 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
@@ -300,7 +300,7 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
}
- if (!request.param("sessionId").isPresent()) {
+ if (request.param("sessionId").isEmpty()) {
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index 1a9c393..2bc74f2 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -71,6 +71,7 @@ import reactor.core.publisher.Sinks;
*
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
+ * @author Jihoon Kim
* @see McpClient
* @see McpSchema
* @see McpClientSession
@@ -816,4 +817,25 @@ public class McpAsyncClient {
this.protocolVersions = protocolVersions;
}
+ // --------------------------
+ // Completions
+ // --------------------------
+ private static final TypeReference COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeReference<>() {
+ };
+
+ /**
+ * Sends a completion/complete request to generate value suggestions based on a given
+ * reference and argument. This is typically used to provide auto-completion options
+ * for user input fields.
+ * @param completeRequest The request containing the prompt or resource reference and
+ * argument for which to generate completions.
+ * @return A Mono that completes with the result containing completion suggestions.
+ * @see McpSchema.CompleteRequest
+ * @see McpSchema.CompleteResult
+ */
+ public Mono completeCompletion(McpSchema.CompleteRequest completeRequest) {
+ return this.withInitializationCheck("complete completions", initializedResult -> this.mcpSession
+ .sendRequest(McpSchema.METHOD_COMPLETION_COMPLETE, completeRequest, COMPLETION_COMPLETE_RESULT_TYPE_REF));
+ }
+
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
index 8544c36..c91638a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
@@ -46,6 +46,7 @@ import org.slf4j.LoggerFactory;
*
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
+ * @author Jihoon Kim
* @see McpClient
* @see McpAsyncClient
* @see McpSchema
@@ -334,4 +335,14 @@ public class McpSyncClient implements AutoCloseable {
this.delegate.setLoggingLevel(loggingLevel).block();
}
+ /**
+ * Send a completion/complete request.
+ * @param completeRequest the completion request contains the prompt or resource
+ * reference and arguments for generating suggestions.
+ * @return the completion result containing suggested values.
+ */
+ public McpSchema.CompleteResult completeCompletion(McpSchema.CompleteRequest completeRequest) {
+ return this.delegate.completeCompletion(completeRequest).block();
+ }
+
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index 28b63ce..906cb9a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -69,6 +69,7 @@ import reactor.core.publisher.Mono;
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
+ * @author Jihoon Kim
* @see McpServer
* @see McpSchema
* @see McpClientSession
@@ -269,6 +270,8 @@ public class McpAsyncServer {
// broadcasting loggingNotification.
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
+ private final ConcurrentHashMap completions = new ConcurrentHashMap<>();
+
private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
@@ -282,6 +285,7 @@ public class McpAsyncServer {
this.resources.putAll(features.resources());
this.resourceTemplates.addAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
+ this.completions.putAll(features.completions());
Map> requestHandlers = new HashMap<>();
@@ -314,6 +318,11 @@ public class McpAsyncServer {
requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
}
+ // Add completion API handlers if the completion capability is enabled
+ if (this.serverCapabilities.completions() != null) {
+ requestHandlers.put(McpSchema.METHOD_COMPLETION_COMPLETE, completionCompleteRequestHandler());
+ }
+
Map notificationHandlers = new HashMap<>();
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty());
@@ -706,6 +715,81 @@ public class McpAsyncServer {
};
}
+ private McpServerSession.RequestHandler completionCompleteRequestHandler() {
+ return (exchange, params) -> {
+ McpSchema.CompleteRequest request = parseCompletionParams(params);
+
+ if (request.ref() == null) {
+ return Mono.error(new McpError("ref must not be null"));
+ }
+
+ if (request.ref().type() == null) {
+ return Mono.error(new McpError("type must not be null"));
+ }
+
+ String type = request.ref().type();
+
+ // check if the referenced resource exists
+ if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
+ McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name());
+ if (prompt == null) {
+ return Mono.error(new McpError("Prompt not found: " + promptReference.name()));
+ }
+ }
+
+ if (type.equals("ref/resource")
+ && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
+ McpServerFeatures.AsyncResourceSpecification resource = this.resources.get(resourceReference.uri());
+ if (resource == null) {
+ return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
+ }
+ }
+
+ McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
+
+ if (specification == null) {
+ return Mono.error(new McpError("AsyncCompletionSpecification not found: " + request.ref()));
+ }
+
+ return specification.completionHandler().apply(exchange, request);
+ };
+ }
+
+ /**
+ * Parses the raw JSON-RPC request parameters into a
+ * {@link McpSchema.CompleteRequest} object.
+ *
+ * This method manually extracts the `ref` and `argument` fields from the input
+ * map, determines the correct reference type (either prompt or resource), and
+ * constructs a fully-typed {@code CompleteRequest} instance.
+ * @param object the raw request parameters, expected to be a Map containing "ref"
+ * and "argument" entries.
+ * @return a {@link McpSchema.CompleteRequest} representing the structured
+ * completion request.
+ * @throws IllegalArgumentException if the "ref" type is not recognized.
+ */
+ @SuppressWarnings("unchecked")
+ private McpSchema.CompleteRequest parseCompletionParams(Object object) {
+ Map params = (Map) object;
+ Map refMap = (Map) params.get("ref");
+ Map argMap = (Map) params.get("argument");
+
+ String refType = (String) refMap.get("type");
+
+ McpSchema.CompleteReference ref = switch (refType) {
+ case "ref/prompt" -> new McpSchema.PromptReference(refType, (String) refMap.get("name"));
+ case "ref/resource" -> new McpSchema.ResourceReference(refType, (String) refMap.get("uri"));
+ default -> throw new IllegalArgumentException("Invalid ref type: " + refType);
+ };
+
+ String argName = (String) argMap.get("name");
+ String argValue = (String) argMap.get("value");
+ McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument(
+ argName, argValue);
+
+ return new McpSchema.CompleteRequest(ref, argument);
+ }
+
// ---------------------------------------
// Sampling
// ---------------------------------------
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
index 60434a8..8408970 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
@@ -115,6 +115,7 @@ import reactor.core.publisher.Mono;
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
+ * @author Jihoon Kim
* @see McpAsyncServer
* @see McpSyncServer
* @see McpServerTransportProvider
@@ -192,6 +193,8 @@ public interface McpServer {
*/
private final Map prompts = new HashMap<>();
+ private final Map completions = new HashMap<>();
+
private final List, Mono>> rootsChangeHandlers = new ArrayList<>();
private Duration requestTimeout = Duration.ofSeconds(10); // Default timeout
@@ -581,7 +584,8 @@ public interface McpServer {
*/
public McpAsyncServer build() {
var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
- this.resources, this.resourceTemplates, this.prompts, this.rootsChangeHandlers, this.instructions);
+ this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
+ this.instructions);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout);
}
@@ -635,6 +639,8 @@ public interface McpServer {
*/
private final Map prompts = new HashMap<>();
+ private final Map completions = new HashMap<>();
+
private final List>> rootsChangeHandlers = new ArrayList<>();
private Duration requestTimeout = Duration.ofSeconds(10); // Default timeout
@@ -957,6 +963,37 @@ public interface McpServer {
return this;
}
+ /**
+ * Registers multiple completions with their handlers using a List. This method is
+ * useful when completions need to be added in bulk from a collection.
+ * @param completions List of completion specifications. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if completions is null
+ * @see #completions(McpServerFeatures.SyncCompletionSpecification...)
+ */
+ public SyncSpecification completions(List completions) {
+ Assert.notNull(completions, "Completions list must not be null");
+ for (McpServerFeatures.SyncCompletionSpecification completion : completions) {
+ this.completions.put(completion.referenceKey(), completion);
+ }
+ return this;
+ }
+
+ /**
+ * Registers multiple completions with their handlers using varargs. This method
+ * is useful when completions are defined inline and added directly.
+ * @param completions Array of completion specifications. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if completions is null
+ */
+ public SyncSpecification completions(McpServerFeatures.SyncCompletionSpecification... completions) {
+ Assert.notNull(completions, "Completions list must not be null");
+ for (McpServerFeatures.SyncCompletionSpecification completion : completions) {
+ this.completions.put(completion.referenceKey(), completion);
+ }
+ return this;
+ }
+
/**
* Registers a consumer that will be notified when the list of roots changes. This
* is useful for updating resource availability dynamically, such as when new
@@ -1023,8 +1060,8 @@ public interface McpServer {
*/
public McpSyncServer build() {
McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities,
- this.tools, this.resources, this.resourceTemplates, this.prompts, this.rootsChangeHandlers,
- this.instructions);
+ this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions,
+ this.rootsChangeHandlers, this.instructions);
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout);
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
index e0f337b..8311f5d 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
@@ -21,6 +21,7 @@ import reactor.core.scheduler.Schedulers;
* MCP server features specification that a particular server can choose to support.
*
* @author Dariusz Jędrzejczyk
+ * @author Jihoon Kim
*/
public class McpServerFeatures {
@@ -41,6 +42,7 @@ public class McpServerFeatures {
List tools, Map resources,
List resourceTemplates,
Map prompts,
+ Map completions,
List, Mono>> rootsChangeConsumers,
String instructions) {
@@ -60,6 +62,7 @@ public class McpServerFeatures {
List tools, Map resources,
List resourceTemplates,
Map prompts,
+ Map completions,
List, Mono>> rootsChangeConsumers,
String instructions) {
@@ -67,7 +70,8 @@ public class McpServerFeatures {
this.serverInfo = serverInfo;
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
- : new McpSchema.ServerCapabilities(null, // experimental
+ : new McpSchema.ServerCapabilities(null, // completions
+ null, // experimental
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
// logging
// by
@@ -81,6 +85,7 @@ public class McpServerFeatures {
this.resources = (resources != null) ? resources : Map.of();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : List.of();
this.prompts = (prompts != null) ? prompts : Map.of();
+ this.completions = (completions != null) ? completions : Map.of();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : List.of();
this.instructions = instructions;
}
@@ -109,6 +114,11 @@ public class McpServerFeatures {
prompts.put(key, AsyncPromptSpecification.fromSync(prompt));
});
+ Map completions = new HashMap<>();
+ syncSpec.completions().forEach((key, completion) -> {
+ completions.put(key, AsyncCompletionSpecification.fromSync(completion));
+ });
+
List, Mono>> rootChangeConsumers = new ArrayList<>();
for (var rootChangeConsumer : syncSpec.rootsChangeConsumers()) {
@@ -118,7 +128,7 @@ public class McpServerFeatures {
}
return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources,
- syncSpec.resourceTemplates(), prompts, rootChangeConsumers, syncSpec.instructions());
+ syncSpec.resourceTemplates(), prompts, completions, rootChangeConsumers, syncSpec.instructions());
}
}
@@ -140,6 +150,7 @@ public class McpServerFeatures {
Map resources,
List resourceTemplates,
Map prompts,
+ Map completions,
List>> rootsChangeConsumers, String instructions) {
/**
@@ -159,6 +170,7 @@ public class McpServerFeatures {
Map resources,
List resourceTemplates,
Map prompts,
+ Map completions,
List>> rootsChangeConsumers,
String instructions) {
@@ -166,7 +178,8 @@ public class McpServerFeatures {
this.serverInfo = serverInfo;
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
- : new McpSchema.ServerCapabilities(null, // experimental
+ : new McpSchema.ServerCapabilities(null, // completions
+ null, // experimental
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
// logging
// by
@@ -180,6 +193,7 @@ public class McpServerFeatures {
this.resources = (resources != null) ? resources : new HashMap<>();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : new ArrayList<>();
this.prompts = (prompts != null) ? prompts : new HashMap<>();
+ this.completions = (completions != null) ? completions : new HashMap<>();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : new ArrayList<>();
this.instructions = instructions;
}
@@ -325,6 +339,44 @@ public class McpServerFeatures {
}
}
+ /**
+ * Specification of a completion handler function with asynchronous execution support.
+ * Completions generate AI model outputs based on prompt or resource references and
+ * user-provided arguments. This abstraction enables:
+ *
+ *
Customizable response generation logic
+ *
Parameter-driven template expansion
+ *
Dynamic interaction with connected clients
+ *
+ *
+ * @param referenceKey The unique key representing the completion reference.
+ * @param completionHandler The asynchronous function that processes completion
+ * requests and returns results. The first argument is an
+ * {@link McpAsyncServerExchange} used to interact with the client. The second
+ * argument is a {@link io.modelcontextprotocol.spec.McpSchema.CompleteRequest}.
+ */
+ public record AsyncCompletionSpecification(McpSchema.CompleteReference referenceKey,
+ BiFunction> completionHandler) {
+
+ /**
+ * Converts a synchronous {@link SyncCompletionSpecification} into an
+ * {@link AsyncCompletionSpecification} by wrapping the handler in a bounded
+ * elastic scheduler for safe non-blocking execution.
+ * @param completion the synchronous completion specification
+ * @return an asynchronous wrapper of the provided sync specification, or
+ * {@code null} if input is null
+ */
+ static AsyncCompletionSpecification fromSync(SyncCompletionSpecification completion) {
+ if (completion == null) {
+ return null;
+ }
+ return new AsyncCompletionSpecification(completion.referenceKey(),
+ (exchange, request) -> Mono.fromCallable(
+ () -> completion.completionHandler().apply(new McpSyncServerExchange(exchange), request))
+ .subscribeOn(Schedulers.boundedElastic()));
+ }
+ }
+
/**
* Specification of a tool with its synchronous handler function. Tools are the
* primary way for MCP servers to expose functionality to AI models. Each tool
@@ -431,4 +483,17 @@ public class McpServerFeatures {
BiFunction promptHandler) {
}
+ /**
+ * Specification of a completion handler function with synchronous execution support.
+ *
+ * @param referenceKey The unique key representing the completion reference.
+ * @param completionHandler The synchronous function that processes completion
+ * requests and returns results. The first argument is an
+ * {@link McpSyncServerExchange} used to interact with the client. The second argument
+ * is a {@link io.modelcontextprotocol.spec.McpSchema.CompleteRequest}.
+ */
+ public record SyncCompletionSpecification(McpSchema.CompleteReference referenceKey,
+ BiFunction completionHandler) {
+ }
+
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index 0895e02..c1f42e3 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -238,6 +238,7 @@ public class McpClientSession implements McpSession {
});
}).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> {
if (jsonRpcResponse.error() != null) {
+ logger.error("Error handling request: {}", jsonRpcResponse.error());
sink.error(new McpError(jsonRpcResponse.error()));
}
else {
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
index 6eb5159..55fdc17 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -79,6 +79,8 @@ public final class McpSchema {
public static final String METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed";
+ public static final String METHOD_COMPLETION_COMPLETE = "completion/complete";
+
// Logging Methods
public static final String METHOD_LOGGING_SET_LEVEL = "logging/setLevel";
@@ -314,12 +316,16 @@ public final class McpSchema {
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ServerCapabilities( // @formatter:off
+ @JsonProperty("completions") CompletionCapabilities completions,
@JsonProperty("experimental") Map experimental,
@JsonProperty("logging") LoggingCapabilities logging,
@JsonProperty("prompts") PromptCapabilities prompts,
@JsonProperty("resources") ResourceCapabilities resources,
@JsonProperty("tools") ToolCapabilities tools) {
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ public record CompletionCapabilities() {
+ }
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record LoggingCapabilities() {
@@ -347,12 +353,18 @@ public final class McpSchema {
public static class Builder {
+ private CompletionCapabilities completions;
private Map experimental;
private LoggingCapabilities logging = new LoggingCapabilities();
private PromptCapabilities prompts;
private ResourceCapabilities resources;
private ToolCapabilities tools;
+ public Builder completions(CompletionCapabilities completions) {
+ this.completions = completions;
+ return this;
+ }
+
public Builder experimental(Map experimental) {
this.experimental = experimental;
return this;
@@ -379,7 +391,7 @@ public final class McpSchema {
}
public ServerCapabilities build() {
- return new ServerCapabilities(experimental, logging, prompts, resources, tools);
+ return new ServerCapabilities(completions, experimental, logging, prompts, resources, tools);
}
}
} // @formatter:on
@@ -1173,31 +1185,63 @@ public final class McpSchema {
// ---------------------------
// Autocomplete
// ---------------------------
- public record CompleteRequest(PromptOrResourceReference ref, CompleteArgument argument) implements Request {
- public sealed interface PromptOrResourceReference permits PromptReference, ResourceReference {
+ public sealed interface CompleteReference permits PromptReference, ResourceReference {
- String type();
+ String type();
+ String identifier();
+
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record PromptReference(// @formatter:off
+ @JsonProperty("type") String type,
+ @JsonProperty("name") String name) implements McpSchema.CompleteReference {
+
+ public PromptReference(String name) {
+ this("ref/prompt", name);
}
- public record PromptReference(// @formatter:off
- @JsonProperty("type") String type,
- @JsonProperty("name") String name) implements PromptOrResourceReference {
- }// @formatter:on
+ @Override
+ public String identifier() {
+ return name();
+ }
+ }// @formatter:on
- public record ResourceReference(// @formatter:off
- @JsonProperty("type") String type,
- @JsonProperty("uri") String uri) implements PromptOrResourceReference {
- }// @formatter:on
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record ResourceReference(// @formatter:off
+ @JsonProperty("type") String type,
+ @JsonProperty("uri") String uri) implements McpSchema.CompleteReference {
- public record CompleteArgument(// @formatter:off
+ public ResourceReference(String uri) {
+ this("ref/resource", uri);
+ }
+
+ @Override
+ public String identifier() {
+ return uri();
+ }
+ }// @formatter:on
+
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record CompleteRequest(// @formatter:off
+ @JsonProperty("ref") McpSchema.CompleteReference ref,
+ @JsonProperty("argument") CompleteArgument argument) implements Request {
+
+ public record CompleteArgument(
@JsonProperty("name") String name,
@JsonProperty("value") String value) {
}// @formatter:on
}
- public record CompleteResult(CompleteCompletion completion) {
- public record CompleteCompletion(// @formatter:off
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record CompleteResult(@JsonProperty("values") CompleteCompletion completion) { // @formatter:off
+
+ public record CompleteCompletion(
@JsonProperty("values") List values,
@JsonProperty("total") Integer total,
@JsonProperty("hasMore") Boolean hasMore) {
From 734d1732d6d3e74cd427ac5a4dba95a02ba08618 Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Thu, 17 Apr 2025 09:28:08 +0200
Subject: [PATCH 008/337] Refactor: Simplify ServerCapabilities builder API for
completions
Signed-off-by: Christian Tzolov
---
.../io/modelcontextprotocol/WebFluxSseIntegrationTests.java | 2 +-
mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java | 4 ++--
2 files changed, 3 insertions(+), 3 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 08619bd..660f814 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
@@ -774,7 +774,7 @@ class WebFluxSseIntegrationTests {
};
var mcpServer = McpServer.sync(mcpServerTransportProvider)
- .capabilities(ServerCapabilities.builder().completions(new CompletionCapabilities()).build())
+ .capabilities(ServerCapabilities.builder().completions().build())
.prompts(new McpServerFeatures.SyncPromptSpecification(
new Prompt("code_review", "this is code review prompt", List.of()),
(mcpSyncServerExchange, getPromptRequest) -> null))
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
index 55fdc17..e7e3380 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -360,8 +360,8 @@ public final class McpSchema {
private ResourceCapabilities resources;
private ToolCapabilities tools;
- public Builder completions(CompletionCapabilities completions) {
- this.completions = completions;
+ public Builder completions() {
+ this.completions = new CompletionCapabilities();
return this;
}
From 344f1b838340efbcf0cfcd428358bdb77e9a533c Mon Sep 17 00:00:00 2001
From: E550448
Date: Sun, 13 Apr 2025 12:54:48 +0200
Subject: [PATCH 009/337] feat(mcp): resolve absolute and relative message
endpoint URIs (#150)
Improve endpoint URI handling by supporting both relative paths
and properly validated absolute URIs.
- Implement URI resolution in HttpClientSseClientTransport:
- Change baseUri field from String to URI type
- Add Utils.resolveUri method to handle both absolute and relative URIs
- Resolve relative URIs against the base URI
- Validate absolute URIs to ensure they match base URI's scheme, authority, and path
- Add parameterized tests for various URI resolution scenarios
- Add ByteBuddy dependency for HttpClient mocking and update Mockito
Signed-off-by: Christian Tzolov
---
README.md | 2 +-
mcp/pom.xml | 14 +++++
.../HttpClientSseClientTransport.java | 11 ++--
.../io/modelcontextprotocol/util/Utils.java | 56 ++++++++++++++++++-
.../HttpClientSseClientTransportTests.java | 29 +++++++++-
.../modelcontextprotocol/util/UtilsTests.java | 29 ++++++++++
pom.xml | 5 +-
7 files changed, 136 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index ca87736..9fc1730 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
A set of projects that provide Java SDK integration for the [Model Context Protocol](https://modelcontextprotocol.org/docs/concepts/architecture).
This SDK enables Java applications to interact with AI models and tools through a standardized interface, supporting both synchronous and asynchronous communication patterns.
-## 📚 Reference Documentation
+## 📚 Reference Documentation
#### MCP Java SDK documentation
For comprehensive guides and SDK API documentation, visit the [MCP Java SDK Reference Documentation](https://modelcontextprotocol.io/sdk/java/mcp-overview).
diff --git a/mcp/pom.xml b/mcp/pom.xml
index 6b0f4a9..17693ab 100644
--- a/mcp/pom.xml
+++ b/mcp/pom.xml
@@ -126,12 +126,26 @@
${junit.version}test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+ org.mockitomockito-core${mockito.version}test
+
+
+
+ net.bytebuddy
+ byte-buddy
+ ${byte-buddy.version}
+ test
+ io.projectreactorreactor-test
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 632d384..99cf2a6 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
@@ -24,6 +24,7 @@ import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import io.modelcontextprotocol.util.Assert;
+import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
@@ -69,7 +70,7 @@ public class HttpClientSseClientTransport implements McpClientTransport {
private static final String DEFAULT_SSE_ENDPOINT = "/sse";
/** Base URI for the MCP server */
- private final String baseUri;
+ private final URI baseUri;
/** SSE endpoint path */
private final String sseEndpoint;
@@ -178,7 +179,7 @@ public class HttpClientSseClientTransport implements McpClientTransport {
Assert.hasText(sseEndpoint, "sseEndpoint must not be empty");
Assert.notNull(httpClient, "httpClient must not be null");
Assert.notNull(requestBuilder, "requestBuilder must not be null");
- this.baseUri = baseUri;
+ this.baseUri = URI.create(baseUri);
this.sseEndpoint = sseEndpoint;
this.objectMapper = objectMapper;
this.httpClient = httpClient;
@@ -340,7 +341,8 @@ public class HttpClientSseClientTransport implements McpClientTransport {
CompletableFuture future = new CompletableFuture<>();
connectionFuture.set(future);
- sseClient.subscribe(this.baseUri + this.sseEndpoint, new FlowSseClient.SseEventHandler() {
+ URI clientUri = Utils.resolveUri(this.baseUri, this.sseEndpoint);
+ sseClient.subscribe(clientUri.toString(), new FlowSseClient.SseEventHandler() {
@Override
public void onEvent(SseEvent event) {
if (isClosing) {
@@ -412,7 +414,8 @@ public class HttpClientSseClientTransport implements McpClientTransport {
try {
String jsonText = this.objectMapper.writeValueAsString(message);
- HttpRequest request = this.requestBuilder.uri(URI.create(this.baseUri + endpoint))
+ URI requestUri = Utils.resolveUri(baseUri, endpoint);
+ HttpRequest request = this.requestBuilder.uri(requestUri)
.POST(HttpRequest.BodyPublishers.ofString(jsonText))
.build();
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java b/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java
index 0f799ca..8e654e5 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java
@@ -4,11 +4,12 @@
package io.modelcontextprotocol.util;
+import reactor.util.annotation.Nullable;
+
+import java.net.URI;
import java.util.Collection;
import java.util.Map;
-import reactor.util.annotation.Nullable;
-
/**
* Miscellaneous utility methods.
*
@@ -52,4 +53,55 @@ public final class Utils {
return (map == null || map.isEmpty());
}
+ /**
+ * Resolves the given endpoint URL against the base URL.
+ *
+ *
If the endpoint URL is relative, it will be resolved against the base URL.
+ *
If the endpoint URL is absolute, it will be validated to ensure it matches the
+ * base URL's scheme, authority, and path prefix.
+ *
If validation fails for an absolute URL, an {@link IllegalArgumentException} is
+ * thrown.
+ *
+ * @param baseUrl The base URL (must be absolute)
+ * @param endpointUrl The endpoint URL (can be relative or absolute)
+ * @return The resolved endpoint URI
+ * @throws IllegalArgumentException If the absolute endpoint URL does not match the
+ * base URL or URI is malformed
+ */
+ public static URI resolveUri(URI baseUrl, String endpointUrl) {
+ URI endpointUri = URI.create(endpointUrl);
+ if (endpointUri.isAbsolute() && !isUnderBaseUri(baseUrl, endpointUri)) {
+ throw new IllegalArgumentException("Absolute endpoint URL does not match the base URL.");
+ }
+ else {
+ return baseUrl.resolve(endpointUri);
+ }
+ }
+
+ /**
+ * Checks if the given absolute endpoint URI falls under the base URI. It validates
+ * the scheme, authority (host and port), and ensures that the base path is a prefix
+ * of the endpoint path.
+ * @param baseUri The base URI
+ * @param endpointUri The endpoint URI to check
+ * @return true if endpointUri is within baseUri's hierarchy, false otherwise
+ */
+ private static boolean isUnderBaseUri(URI baseUri, URI endpointUri) {
+ if (!baseUri.getScheme().equals(endpointUri.getScheme())
+ || !baseUri.getAuthority().equals(endpointUri.getAuthority())) {
+ return false;
+ }
+
+ URI normalizedBase = baseUri.normalize();
+ URI normalizedEndpoint = endpointUri.normalize();
+
+ String basePath = normalizedBase.getPath();
+ String endpointPath = normalizedEndpoint.getPath();
+
+ if (basePath.endsWith("/")) {
+ basePath = basePath.substring(0, basePath.length() - 1);
+ }
+ return endpointPath.startsWith(basePath);
+ }
+
}
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 e5178c0..762264d 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
@@ -7,12 +7,13 @@ package io.modelcontextprotocol.client.transport;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
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;
@@ -21,6 +22,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import reactor.core.publisher.Mono;
@@ -31,6 +34,9 @@ import org.springframework.http.codec.ServerSentEvent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -364,4 +370,25 @@ class HttpClientSseClientTransportTests {
customizedTransport.closeGracefully().block();
}
+ @Test
+ @SuppressWarnings("unchecked")
+ void testResolvingClientEndpoint() {
+ HttpClient httpClient = Mockito.mock(HttpClient.class);
+ HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
+ CompletableFuture> future = new CompletableFuture<>();
+ future.complete(httpResponse);
+ when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
+
+ HttpClientSseClientTransport transport = new HttpClientSseClientTransport(httpClient, HttpRequest.newBuilder(),
+ "http://example.com", "http://example.com/sse", new ObjectMapper());
+
+ transport.connect(Function.identity());
+
+ ArgumentCaptor httpRequestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
+ verify(httpClient).sendAsync(httpRequestCaptor.capture(), any(HttpResponse.BodyHandler.class));
+ assertThat(httpRequestCaptor.getValue().uri()).isEqualTo(URI.create("http://example.com/sse"));
+
+ transport.closeGracefully().block();
+ }
+
}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/UtilsTests.java b/mcp/src/test/java/io/modelcontextprotocol/util/UtilsTests.java
index aced20c..0f2e689 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/util/UtilsTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/util/UtilsTests.java
@@ -6,12 +6,17 @@ package io.modelcontextprotocol.util;
import org.junit.jupiter.api.Test;
+import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
class UtilsTests {
@@ -37,4 +42,28 @@ class UtilsTests {
assertFalse(Utils.isEmpty(Map.of("key", "value")));
}
+ @ParameterizedTest
+ @CsvSource({
+ // relative endpoints
+ "http://localhost:8080/root, /api/v1, http://localhost:8080/api/v1",
+ "http://localhost:8080/root/, api, http://localhost:8080/root/api",
+ "http://localhost:8080, /api, http://localhost:8080/api",
+ // absolute endpoints matching base
+ "http://localhost:8080/root, http://localhost:8080/root/api/v1, http://localhost:8080/root/api/v1",
+ "http://localhost:8080/root, http://localhost:8080/root, http://localhost:8080/root" })
+ void testValidUriResolution(String baseUrl, String endpoint, String expectedResult) {
+ URI result = Utils.resolveUri(URI.create(baseUrl), endpoint);
+ assertThat(result.toString()).isEqualTo(expectedResult);
+ }
+
+ @ParameterizedTest
+ @CsvSource({ "http://localhost:8080/root, http://localhost:8080/other/api",
+ "http://localhost:8080/root, http://otherhost/api",
+ "http://localhost:8080/root, http://localhost:9090/root/api" })
+ void testAbsoluteUriNotMatchingBase(String baseUrl, String endpoint) {
+ assertThatThrownBy(() -> Utils.resolveUri(URI.create(baseUrl), endpoint))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("does not match the base URL");
+ }
+
}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ff485b7..9be256c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,8 +60,9 @@
3.26.35.10.2
- 5.11.0
+ 5.17.01.20.4
+ 1.17.52.0.161.5.15
@@ -356,4 +357,4 @@
-
\ No newline at end of file
+
From e4091f458a28e31f87a517f411fe9d18811027a6 Mon Sep 17 00:00:00 2001
From: "jie.bao"
Date: Fri, 18 Apr 2025 09:34:55 +0800
Subject: [PATCH 010/337] feat(completion): fix the schema about CompleteResult
/**
* The server's response to a completion/complete request
*/
export
interface CompleteResult extends Result {
completion: {
/**
*
An array of completion values. Must not exceed 100 items.
*/
values: string[];
/**
* The total number of completion options
available. This can exceed the number of values actually sent in the
response.
*/
total?: number;
/**
* Indicates whether
there are additional completion options beyond those provided in the
current response, even if the exact total is unknown.
*/
hasMore?: boolean;
};
}
---
mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
index e7e3380..e77edb3 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -1239,7 +1239,7 @@ public final class McpSchema {
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
- public record CompleteResult(@JsonProperty("values") CompleteCompletion completion) { // @formatter:off
+ public record CompleteResult(@JsonProperty("completion") CompleteCompletion completion) { // @formatter:off
public record CompleteCompletion(
@JsonProperty("values") List values,
From 41c6bd9af09462a87064dc035d5e123d7f1eae58 Mon Sep 17 00:00:00 2001
From: JermaineHua
Date: Thu, 17 Apr 2025 22:25:00 +0800
Subject: [PATCH 011/337] Fix method not found error msg for server
Signed-off-by: JermaineHua
---
.../io/modelcontextprotocol/spec/McpClientSession.java | 2 +-
.../io/modelcontextprotocol/spec/McpServerSession.java | 10 ++--------
2 files changed, 3 insertions(+), 9 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index c1f42e3..9ed0d8e 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -178,7 +178,7 @@ public class McpClientSession implements McpSession {
record MethodNotFoundError(String method, String message, Object data) {
}
- public static MethodNotFoundError getMethodNotFoundError(String method) {
+ private MethodNotFoundError getMethodNotFoundError(String method) {
switch (method) {
case McpSchema.METHOD_ROOTS_LIST:
return new MethodNotFoundError(method, "Roots not supported",
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
index 46c356c..6431509 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
@@ -257,14 +257,8 @@ public class McpServerSession implements McpSession {
record MethodNotFoundError(String method, String message, Object data) {
}
- static MethodNotFoundError getMethodNotFoundError(String method) {
- switch (method) {
- case McpSchema.METHOD_ROOTS_LIST:
- return new MethodNotFoundError(method, "Roots not supported",
- Map.of("reason", "Client does not have roots capability"));
- default:
- return new MethodNotFoundError(method, "Method not found: " + method, null);
- }
+ private MethodNotFoundError getMethodNotFoundError(String method) {
+ return new MethodNotFoundError(method, "Method not found: " + method, null);
}
@Override
From 04046ca05b6b90f9a6ec2f40236c69470b878fe6 Mon Sep 17 00:00:00 2001
From: JermaineHua
Date: Wed, 16 Apr 2025 23:10:59 +0800
Subject: [PATCH 012/337] Optimize client nested streams in McpClientSession
(#33)
Signed-off-by: JermaineHua
---
.../spec/McpClientSession.java | 31 ++++++++++++-------
1 file changed, 20 insertions(+), 11 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index 9ed0d8e..a25f38c 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -122,7 +122,12 @@ public class McpClientSession implements McpSession {
// Observation associated with the individual message - it can be used to
// create child Observation and emit it together with the message to the
// consumer
- this.connection = this.transport.connect(mono -> mono.doOnNext(message -> {
+ this.connection = this.transport.connect(mono -> mono.doOnNext(message -> handle(message).subscribe()))
+ .subscribe();
+ }
+
+ public Mono handle(McpSchema.JSONRPCMessage message) {
+ return Mono.defer(() -> {
if (message instanceof McpSchema.JSONRPCResponse response) {
logger.debug("Received Response: {}", response);
var sink = pendingResponses.remove(response.id());
@@ -132,23 +137,27 @@ public class McpClientSession implements McpSession {
else {
sink.success(response);
}
+ return Mono.empty();
}
else if (message instanceof McpSchema.JSONRPCRequest request) {
logger.debug("Received request: {}", request);
- handleIncomingRequest(request).subscribe(response -> transport.sendMessage(response).subscribe(),
- error -> {
- var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),
- null, new McpSchema.JSONRPCResponse.JSONRPCError(
- McpSchema.ErrorCodes.INTERNAL_ERROR, error.getMessage(), null));
- transport.sendMessage(errorResponse).subscribe();
- });
+ return handleIncomingRequest(request).onErrorResume(error -> {
+ var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
+ new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
+ error.getMessage(), null));
+ return this.transport.sendMessage(errorResponse).then(Mono.empty());
+ }).flatMap(this.transport::sendMessage);
}
else if (message instanceof McpSchema.JSONRPCNotification notification) {
logger.debug("Received notification: {}", notification);
- handleIncomingNotification(notification).subscribe(null,
- error -> logger.error("Error handling notification: {}", error.getMessage()));
+ return handleIncomingNotification(notification)
+ .doOnError(error -> logger.error("Error handling notification: {}", error.getMessage()));
}
- })).subscribe();
+ else {
+ logger.warn("Received unknown message type: {}", message);
+ return Mono.empty();
+ }
+ });
}
/**
From 866732c3833e863ea145c6e1dfa32b9d089211e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?=
Date: Wed, 23 Apr 2025 11:06:26 +0200
Subject: [PATCH 013/337] Polish #33
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Dariusz Jędrzejczyk
---
.../spec/McpClientSession.java | 58 +++++++++----------
1 file changed, 27 insertions(+), 31 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index a25f38c..6eca347 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -122,42 +122,38 @@ public class McpClientSession implements McpSession {
// Observation associated with the individual message - it can be used to
// create child Observation and emit it together with the message to the
// consumer
- this.connection = this.transport.connect(mono -> mono.doOnNext(message -> handle(message).subscribe()))
- .subscribe();
+ this.connection = this.transport.connect(mono -> mono.doOnNext(this::handle)).subscribe();
}
- public Mono handle(McpSchema.JSONRPCMessage message) {
- return Mono.defer(() -> {
- if (message instanceof McpSchema.JSONRPCResponse response) {
- logger.debug("Received Response: {}", response);
- var sink = pendingResponses.remove(response.id());
- if (sink == null) {
- logger.warn("Unexpected response for unknown id {}", response.id());
- }
- else {
- sink.success(response);
- }
- return Mono.empty();
- }
- else if (message instanceof McpSchema.JSONRPCRequest request) {
- logger.debug("Received request: {}", request);
- return handleIncomingRequest(request).onErrorResume(error -> {
- var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
- new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
- error.getMessage(), null));
- return this.transport.sendMessage(errorResponse).then(Mono.empty());
- }).flatMap(this.transport::sendMessage);
- }
- else if (message instanceof McpSchema.JSONRPCNotification notification) {
- logger.debug("Received notification: {}", notification);
- return handleIncomingNotification(notification)
- .doOnError(error -> logger.error("Error handling notification: {}", error.getMessage()));
+ private void handle(McpSchema.JSONRPCMessage message) {
+ if (message instanceof McpSchema.JSONRPCResponse response) {
+ logger.debug("Received Response: {}", response);
+ var sink = pendingResponses.remove(response.id());
+ if (sink == null) {
+ logger.warn("Unexpected response for unknown id {}", response.id());
}
else {
- logger.warn("Received unknown message type: {}", message);
- return Mono.empty();
+ sink.success(response);
}
- });
+ }
+ else if (message instanceof McpSchema.JSONRPCRequest request) {
+ logger.debug("Received request: {}", request);
+ handleIncomingRequest(request).onErrorResume(error -> {
+ var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
+ new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
+ error.getMessage(), null));
+ return this.transport.sendMessage(errorResponse).then(Mono.empty());
+ }).flatMap(this.transport::sendMessage).subscribe();
+ }
+ else if (message instanceof McpSchema.JSONRPCNotification notification) {
+ logger.debug("Received notification: {}", notification);
+ handleIncomingNotification(notification)
+ .doOnError(error -> logger.error("Error handling notification: {}", error.getMessage()))
+ .subscribe();
+ }
+ else {
+ logger.warn("Received unknown message type: {}", message);
+ }
}
/**
From 86e3e9048f53b706849a2a58a11aae70c3a1f391 Mon Sep 17 00:00:00 2001
From: jito
Date: Wed, 23 Apr 2025 23:27:10 +0900
Subject: [PATCH 014/337] Fix typo in WebFluxSseIntegrationTests (#142)
Signed-off-by: jitokim
From f70b98b4b4160ea590a0c845ee3e2a7357bdcae9 Mon Sep 17 00:00:00 2001
From: Richie Caputo <43445060+arcaputo3@users.noreply.github.com>
Date: Wed, 23 Apr 2025 10:47:48 -0400
Subject: [PATCH 015/337] feat(schema): add support for JSON Schema $defs and
definitions (#146)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Added support for $defs and definitions properties in JsonSchema record to handle JSON Schema references properly. Added tests to verify both formats work correctly.
The JsonSchema test approach uses serialization/deserialization round-trip validation instead of property-by-property assertions. This makes tests more maintainable and less likely to break when new properties are added.
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude
---
.../modelcontextprotocol/spec/McpSchema.java | 4 +-
.../spec/McpSchemaTests.java | 129 ++++++++++++++++++
2 files changed, 132 insertions(+), 1 deletion(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
index e77edb3..8df8a15 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -703,7 +703,9 @@ public final class McpSchema {
@JsonProperty("type") String type,
@JsonProperty("properties") Map properties,
@JsonProperty("required") List required,
- @JsonProperty("additionalProperties") Boolean additionalProperties) {
+ @JsonProperty("additionalProperties") Boolean additionalProperties,
+ @JsonProperty("$defs") Map defs,
+ @JsonProperty("definitions") Map definitions) {
} // @formatter:on
/**
diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java
index a41fc09..ff78c1b 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java
@@ -9,6 +9,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
@@ -449,6 +450,92 @@ public class McpSchemaTests {
// Tool Tests
+ @Test
+ void testJsonSchema() throws Exception {
+ String schemaJson = """
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "address": {
+ "$ref": "#/$defs/Address"
+ }
+ },
+ "required": ["name"],
+ "$defs": {
+ "Address": {
+ "type": "object",
+ "properties": {
+ "street": {"type": "string"},
+ "city": {"type": "string"}
+ },
+ "required": ["street", "city"]
+ }
+ }
+ }
+ """;
+
+ // Deserialize the original string to a JsonSchema object
+ McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
+
+ // Serialize the object back to a string
+ String serialized = mapper.writeValueAsString(schema);
+
+ // Deserialize again
+ McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class);
+
+ // Serialize one more time and compare with the first serialization
+ String serializedAgain = mapper.writeValueAsString(deserialized);
+
+ // The two serialized strings should be the same
+ assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
+ }
+
+ @Test
+ void testJsonSchemaWithDefinitions() throws Exception {
+ String schemaJson = """
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "address": {
+ "$ref": "#/definitions/Address"
+ }
+ },
+ "required": ["name"],
+ "definitions": {
+ "Address": {
+ "type": "object",
+ "properties": {
+ "street": {"type": "string"},
+ "city": {"type": "string"}
+ },
+ "required": ["street", "city"]
+ }
+ }
+ }
+ """;
+
+ // Deserialize the original string to a JsonSchema object
+ McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
+
+ // Serialize the object back to a string
+ String serialized = mapper.writeValueAsString(schema);
+
+ // Deserialize again
+ McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class);
+
+ // Serialize one more time and compare with the first serialization
+ String serializedAgain = mapper.writeValueAsString(deserialized);
+
+ // The two serialized strings should be the same
+ assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
+ }
+
@Test
void testTool() throws Exception {
String schemaJson = """
@@ -477,6 +564,48 @@ public class McpSchemaTests {
{"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}"""));
}
+ @Test
+ void testToolWithComplexSchema() throws Exception {
+ String complexSchemaJson = """
+ {
+ "type": "object",
+ "$defs": {
+ "Address": {
+ "type": "object",
+ "properties": {
+ "street": {"type": "string"},
+ "city": {"type": "string"}
+ },
+ "required": ["street", "city"]
+ }
+ },
+ "properties": {
+ "name": {"type": "string"},
+ "shippingAddress": {"$ref": "#/$defs/Address"}
+ },
+ "required": ["name", "shippingAddress"]
+ }
+ """;
+
+ McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", complexSchemaJson);
+
+ // Serialize the tool to a string
+ String serialized = mapper.writeValueAsString(tool);
+
+ // Deserialize back to a Tool object
+ McpSchema.Tool deserializedTool = mapper.readValue(serialized, McpSchema.Tool.class);
+
+ // Serialize again and compare with first serialization
+ String serializedAgain = mapper.writeValueAsString(deserializedTool);
+
+ // The two serialized strings should be the same
+ assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
+
+ // Just verify the basic structure was preserved
+ assertThat(deserializedTool.inputSchema().defs()).isNotNull();
+ assertThat(deserializedTool.inputSchema().defs()).containsKey("Address");
+ }
+
@Test
void testCallToolRequest() throws Exception {
Map arguments = new HashMap<>();
From 9c92a2b8bffe41f4c6df27ca1977bc8ee8343137 Mon Sep 17 00:00:00 2001
From: wangzhi <1277975348@qq.com>
Date: Wed, 23 Apr 2025 23:03:10 +0800
Subject: [PATCH 016/337] Fix javadoc references and formatting (#149)
---
.../server/AbstractMcpAsyncServerTests.java | 2 +-
.../server/AbstractMcpSyncServerTests.java | 2 +-
.../java/io/modelcontextprotocol/client/McpAsyncClient.java | 4 ++--
.../java/io/modelcontextprotocol/spec/McpServerSession.java | 3 ++-
4 files changed, 6 insertions(+), 5 deletions(-)
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 cdd43e7..025cfea 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
@@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Test suite for the {@link McpAsyncServer} that can be used with different
- * {@link McpTransportProvider} implementations.
+ * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations.
*
* @author Christian Tzolov
*/
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 c81e638..e313454 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
@@ -27,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Test suite for the {@link McpSyncServer} that can be used with different
- * {@link McpTransportProvider} implementations.
+ * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations.
*
* @author Christian Tzolov
*/
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index 2bc74f2..e3a997b 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -317,9 +317,9 @@ public class McpAsyncClient {
* The client MUST initiate this phase by sending an initialize request containing:
* The protocol version the client supports, client's capabilities and clients
* implementation information.
- *
+ *
* The server MUST respond with its own capabilities and information.
- *
+ *
* After successful initialization, the client MUST send an initialized notification
* to indicate it is ready to begin normal operations.
* @return the initialize result.
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
index 6431509..86906d8 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
@@ -64,7 +64,8 @@ public class McpServerSession implements McpSession {
* {@link io.modelcontextprotocol.spec.McpSchema.InitializeRequest} is received by the
* server
* @param initNotificationHandler called when a
- * {@link McpSchema.METHOD_NOTIFICATION_INITIALIZED} is received.
+ * {@link io.modelcontextprotocol.spec.McpSchema#METHOD_NOTIFICATION_INITIALIZED} is
+ * received.
* @param requestHandlers map of request handlers to use
* @param notificationHandlers map of notification handlers to use
*/
From 261554bb7f1cc630aefeb5487434c1740a72b856 Mon Sep 17 00:00:00 2001
From: Francis Hodianto <61911161+FH-30@users.noreply.github.com>
Date: Wed, 23 Apr 2025 23:09:41 +0800
Subject: [PATCH 017/337] fix: propagate Reactor Context into client transport
chain (#154)
---
.../java/io/modelcontextprotocol/spec/McpClientSession.java | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index 6eca347..f577b49 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -230,18 +230,19 @@ public class McpClientSession implements McpSession {
public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) {
String requestId = this.generateRequestId();
- return Mono.create(sink -> {
+ return Mono.deferContextual(ctx -> Mono.create(sink -> {
this.pendingResponses.put(requestId, sink);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,
requestId, requestParams);
this.transport.sendMessage(jsonrpcRequest)
+ .contextWrite(ctx)
// TODO: It's most efficient to create a dedicated Subscriber here
.subscribe(v -> {
}, error -> {
this.pendingResponses.remove(requestId);
sink.error(error);
});
- }).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> {
+ })).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> {
if (jsonRpcResponse.error() != null) {
logger.error("Error handling request: {}", jsonRpcResponse.error());
sink.error(new McpError(jsonRpcResponse.error()));
From e610d853f922e36ba474b2240f5c6546166e4840 Mon Sep 17 00:00:00 2001
From: Christian Tzolov
Date: Sun, 20 Apr 2025 10:58:51 +0300
Subject: [PATCH 018/337] feat: Add customizable URI template manager factory
to MCP server
Implement URI template functionality for MCP resources, allowing dynamic
resource URIs with variables in the format {variableName}.
- Enable resource URIs with variable placeholders (e.g., "/api/users/{userId}")
- Automatic extraction of variable values from request URIs
- Validation of template arguments in completions
- Matching of request URIs against templates
- Add new URI template management interfaces and implementations
- Enhanced resource template listing to include templated resources
- Updated resource request handling to support template matching
- Test coverage for URI template functionality
- Adding a configurable uriTemplateManagerFactory field to both AsyncSpecification and SyncSpecification classes
- Adding builder methods to allow setting a custom URI template manager factory
- Modifying constructors to pass the URI template manager factory to the server implementation
- Updating the server implementation to use the provided factory
- Add bulk registration methods for async completions
Signed-off-by: Christian Tzolov
---
.../WebFluxSseIntegrationTests.java | 3 +-
.../server/McpAsyncServer.java | 77 +++++++--
.../server/McpServer.java | 68 +++++++-
.../DeafaultMcpUriTemplateManagerFactory.java | 23 +++
.../util/DefaultMcpUriTemplateManager.java | 163 ++++++++++++++++++
.../util/McpUriTemplateManager.java | 52 ++++++
.../util/McpUriTemplateManagerFactory.java | 22 +++
.../McpUriTemplateManagerTests.java | 97 +++++++++++
8 files changed, 489 insertions(+), 16 deletions(-)
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java
create mode 100644 mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java
create mode 100644 mcp/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java
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 660f814..2ba0474 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
@@ -776,7 +776,8 @@ class WebFluxSseIntegrationTests {
var mcpServer = McpServer.sync(mcpServerTransportProvider)
.capabilities(ServerCapabilities.builder().completions().build())
.prompts(new McpServerFeatures.SyncPromptSpecification(
- new Prompt("code_review", "this is code review prompt", List.of()),
+ new Prompt("code_review", "this is code review prompt",
+ List.of(new PromptArgument("language", "string", false))),
(mcpSyncServerExchange, getPromptRequest) -> null))
.completions(new McpServerFeatures.SyncCompletionSpecification(
new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler))
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index 906cb9a..3c112ad 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -5,6 +5,7 @@
package io.modelcontextprotocol.server;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -22,10 +23,13 @@ import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
+import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
+import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
+import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -92,8 +96,10 @@ public class McpAsyncServer {
* @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
*/
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
- McpServerFeatures.Async features, Duration requestTimeout) {
- this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features);
+ McpServerFeatures.Async features, Duration requestTimeout,
+ McpUriTemplateManagerFactory uriTemplateManagerFactory) {
+ this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features,
+ uriTemplateManagerFactory);
}
/**
@@ -274,8 +280,11 @@ public class McpAsyncServer {
private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
+ private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
+
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
- Duration requestTimeout, McpServerFeatures.Async features) {
+ Duration requestTimeout, McpServerFeatures.Async features,
+ McpUriTemplateManagerFactory uriTemplateManagerFactory) {
this.mcpTransportProvider = mcpTransportProvider;
this.objectMapper = objectMapper;
this.serverInfo = features.serverInfo();
@@ -286,6 +295,7 @@ public class McpAsyncServer {
this.resourceTemplates.addAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
this.completions.putAll(features.completions());
+ this.uriTemplateManagerFactory = uriTemplateManagerFactory;
Map> requestHandlers = new HashMap<>();
@@ -564,21 +574,44 @@ public class McpAsyncServer {
private McpServerSession.RequestHandler resourceTemplateListRequestHandler() {
return (exchange, params) -> Mono
- .just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
+ .just(new McpSchema.ListResourceTemplatesResult(this.getResourceTemplates(), null));
}
+ private List getResourceTemplates() {
+ var list = new ArrayList<>(this.resourceTemplates);
+ List resourceTemplates = this.resources.keySet()
+ .stream()
+ .filter(uri -> uri.contains("{"))
+ .map(uri -> {
+ var resource = this.resources.get(uri).resource();
+ var template = new McpSchema.ResourceTemplate(resource.uri(), resource.name(),
+ resource.description(), resource.mimeType(), resource.annotations());
+ return template;
+ })
+ .toList();
+
+ list.addAll(resourceTemplates);
+
+ return list;
+ }
+
private McpServerSession.RequestHandler resourcesReadRequestHandler() {
return (exchange, params) -> {
McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
new TypeReference() {
});
var resourceUri = resourceRequest.uri();
- McpServerFeatures.AsyncResourceSpecification specification = this.resources.get(resourceUri);
- if (specification != null) {
- return specification.readHandler().apply(exchange, resourceRequest);
- }
- return Mono.error(new McpError("Resource not found: " + resourceUri));
+
+ McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
+ .stream()
+ .filter(resourceSpecification -> this.uriTemplateManagerFactory
+ .create(resourceSpecification.resource().uri())
+ .matches(resourceUri))
+ .findFirst()
+ .orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
+
+ return specification.readHandler().apply(exchange, resourceRequest);
};
}
@@ -729,20 +762,38 @@ public class McpAsyncServer {
String type = request.ref().type();
+ String argumentName = request.argument().name();
+
// check if the referenced resource exists
if (type.equals("ref/prompt") && request.ref() instanceof McpSchema.PromptReference promptReference) {
- McpServerFeatures.AsyncPromptSpecification prompt = this.prompts.get(promptReference.name());
- if (prompt == null) {
+ McpServerFeatures.AsyncPromptSpecification promptSpec = this.prompts.get(promptReference.name());
+ if (promptSpec == null) {
return Mono.error(new McpError("Prompt not found: " + promptReference.name()));
}
+ if (!promptSpec.prompt()
+ .arguments()
+ .stream()
+ .filter(arg -> arg.name().equals(argumentName))
+ .findFirst()
+ .isPresent()) {
+
+ return Mono.error(new McpError("Argument not found: " + argumentName));
+ }
}
if (type.equals("ref/resource")
&& request.ref() instanceof McpSchema.ResourceReference resourceReference) {
- McpServerFeatures.AsyncResourceSpecification resource = this.resources.get(resourceReference.uri());
- if (resource == null) {
+ McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
+ .get(resourceReference.uri());
+ if (resourceSpec == null) {
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
}
+ if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
+ .getVariableNames()
+ .contains(argumentName)) {
+ return Mono.error(new McpError("Argument not found: " + argumentName));
+ }
+
}
McpServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
index 8408970..d6ec2cc 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
@@ -19,6 +19,8 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
+import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
+import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import reactor.core.publisher.Mono;
/**
@@ -156,6 +158,8 @@ public interface McpServer {
private final McpServerTransportProvider transportProvider;
+ private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
+
private ObjectMapper objectMapper;
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
@@ -204,6 +208,19 @@ public interface McpServer {
this.transportProvider = transportProvider;
}
+ /**
+ * Sets the URI template manager factory to use for creating URI templates. This
+ * allows for custom URI template parsing and variable extraction.
+ * @param uriTemplateManagerFactory The factory to use. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if uriTemplateManagerFactory is null
+ */
+ public AsyncSpecification uriTemplateManagerFactory(McpUriTemplateManagerFactory uriTemplateManagerFactory) {
+ Assert.notNull(uriTemplateManagerFactory, "URI template manager factory must not be null");
+ this.uriTemplateManagerFactory = uriTemplateManagerFactory;
+ return this;
+ }
+
/**
* Sets the duration to wait for server responses before timing out requests. This
* timeout applies to all requests made through the client, including tool calls,
@@ -517,6 +534,36 @@ public interface McpServer {
return this;
}
+ /**
+ * Registers multiple completions with their handlers using a List. This method is
+ * useful when completions need to be added in bulk from a collection.
+ * @param completions List of completion specifications. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if completions is null
+ */
+ public AsyncSpecification completions(List completions) {
+ Assert.notNull(completions, "Completions list must not be null");
+ for (McpServerFeatures.AsyncCompletionSpecification completion : completions) {
+ this.completions.put(completion.referenceKey(), completion);
+ }
+ return this;
+ }
+
+ /**
+ * Registers multiple completions with their handlers using varargs. This method
+ * is useful when completions are defined inline and added directly.
+ * @param completions Array of completion specifications. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if completions is null
+ */
+ public AsyncSpecification completions(McpServerFeatures.AsyncCompletionSpecification... completions) {
+ Assert.notNull(completions, "Completions list must not be null");
+ for (McpServerFeatures.AsyncCompletionSpecification completion : completions) {
+ this.completions.put(completion.referenceKey(), completion);
+ }
+ return this;
+ }
+
/**
* Registers a consumer that will be notified when the list of roots changes. This
* is useful for updating resource availability dynamically, such as when new
@@ -587,7 +634,8 @@ public interface McpServer {
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
this.instructions);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
- return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout);
+ return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout,
+ this.uriTemplateManagerFactory);
}
}
@@ -600,6 +648,8 @@ public interface McpServer {
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
"1.0.0");
+ private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
+
private final McpServerTransportProvider transportProvider;
private ObjectMapper objectMapper;
@@ -650,6 +700,19 @@ public interface McpServer {
this.transportProvider = transportProvider;
}
+ /**
+ * Sets the URI template manager factory to use for creating URI templates. This
+ * allows for custom URI template parsing and variable extraction.
+ * @param uriTemplateManagerFactory The factory to use. Must not be null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if uriTemplateManagerFactory is null
+ */
+ public SyncSpecification uriTemplateManagerFactory(McpUriTemplateManagerFactory uriTemplateManagerFactory) {
+ Assert.notNull(uriTemplateManagerFactory, "URI template manager factory must not be null");
+ this.uriTemplateManagerFactory = uriTemplateManagerFactory;
+ return this;
+ }
+
/**
* Sets the duration to wait for server responses before timing out requests. This
* timeout applies to all requests made through the client, including tool calls,
@@ -1064,7 +1127,8 @@ public interface McpServer {
this.rootsChangeHandlers, this.instructions);
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
- var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout);
+ var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout,
+ this.uriTemplateManagerFactory);
return new McpSyncServer(asyncServer);
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java b/mcp/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java
new file mode 100644
index 0000000..3870b76
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java
@@ -0,0 +1,23 @@
+/*
+* Copyright 2025 - 2025 the original author or authors.
+*/
+package io.modelcontextprotocol.util;
+
+/**
+ * @author Christian Tzolov
+ */
+public class DeafaultMcpUriTemplateManagerFactory implements McpUriTemplateManagerFactory {
+
+ /**
+ * Creates a new instance of {@link McpUriTemplateManager} with the specified URI
+ * template.
+ * @param uriTemplate The URI template to be used for variable extraction
+ * @return A new instance of {@link McpUriTemplateManager}
+ * @throws IllegalArgumentException if the URI template is null or empty
+ */
+ @Override
+ public McpUriTemplateManager create(String uriTemplate) {
+ return new DefaultMcpUriTemplateManager(uriTemplate);
+ }
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java b/mcp/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java
new file mode 100644
index 0000000..b2e9a52
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Default implementation of the UriTemplateUtils interface.
+ *
+ * This class provides methods for extracting variables from URI templates and matching
+ * them against actual URIs.
+ *
+ * @author Christian Tzolov
+ */
+public class DefaultMcpUriTemplateManager implements McpUriTemplateManager {
+
+ /**
+ * Pattern to match URI variables in the format {variableName}.
+ */
+ private static final Pattern URI_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
+
+ private final String uriTemplate;
+
+ /**
+ * Constructor for DefaultMcpUriTemplateManager.
+ * @param uriTemplate The URI template to be used for variable extraction
+ */
+ public DefaultMcpUriTemplateManager(String uriTemplate) {
+ if (uriTemplate == null || uriTemplate.isEmpty()) {
+ throw new IllegalArgumentException("URI template must not be null or empty");
+ }
+ this.uriTemplate = uriTemplate;
+ }
+
+ /**
+ * Extract URI variable names from a URI template.
+ * @param uriTemplate The URI template containing variables in the format
+ * {variableName}
+ * @return A list of variable names extracted from the template
+ * @throws IllegalArgumentException if duplicate variable names are found
+ */
+ @Override
+ public List getVariableNames() {
+ if (uriTemplate == null || uriTemplate.isEmpty()) {
+ return List.of();
+ }
+
+ List variables = new ArrayList<>();
+ Matcher matcher = URI_VARIABLE_PATTERN.matcher(this.uriTemplate);
+
+ while (matcher.find()) {
+ String variableName = matcher.group(1);
+ if (variables.contains(variableName)) {
+ throw new IllegalArgumentException("Duplicate URI variable name in template: " + variableName);
+ }
+ variables.add(variableName);
+ }
+
+ return variables;
+ }
+
+ /**
+ * Extract URI variable values from the actual request URI.
+ *
+ * This method converts the URI template into a regex pattern, then uses that pattern
+ * to extract variable values from the request URI.
+ * @param requestUri The actual URI from the request
+ * @return A map of variable names to their values
+ * @throws IllegalArgumentException if the URI template is invalid or the request URI
+ * doesn't match the template pattern
+ */
+ @Override
+ public Map extractVariableValues(String requestUri) {
+ Map variableValues = new HashMap<>();
+ List uriVariables = this.getVariableNames();
+
+ if (requestUri == null || uriVariables.isEmpty()) {
+ return variableValues;
+ }
+
+ try {
+ // Create a regex pattern by replacing each {variableName} with a capturing
+ // group
+ StringBuilder patternBuilder = new StringBuilder("^");
+
+ // Find all variable placeholders and their positions
+ Matcher variableMatcher = URI_VARIABLE_PATTERN.matcher(uriTemplate);
+ int lastEnd = 0;
+
+ while (variableMatcher.find()) {
+ // Add the text between the last variable and this one, escaped for regex
+ String textBefore = uriTemplate.substring(lastEnd, variableMatcher.start());
+ patternBuilder.append(Pattern.quote(textBefore));
+
+ // Add a capturing group for the variable
+ patternBuilder.append("([^/]+)");
+
+ lastEnd = variableMatcher.end();
+ }
+
+ // Add any remaining text after the last variable
+ if (lastEnd < uriTemplate.length()) {
+ patternBuilder.append(Pattern.quote(uriTemplate.substring(lastEnd)));
+ }
+
+ patternBuilder.append("$");
+
+ // Compile the pattern and match against the request URI
+ Pattern pattern = Pattern.compile(patternBuilder.toString());
+ Matcher matcher = pattern.matcher(requestUri);
+
+ if (matcher.find() && matcher.groupCount() == uriVariables.size()) {
+ for (int i = 0; i < uriVariables.size(); i++) {
+ String value = matcher.group(i + 1);
+ if (value == null || value.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Empty value for URI variable '" + uriVariables.get(i) + "' in URI: " + requestUri);
+ }
+ variableValues.put(uriVariables.get(i), value);
+ }
+ }
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException("Error parsing URI template: " + uriTemplate + " for URI: " + requestUri,
+ e);
+ }
+
+ return variableValues;
+ }
+
+ /**
+ * Check if a URI matches the uriTemplate with variables.
+ * @param uri The URI to check
+ * @return true if the URI matches the pattern, false otherwise
+ */
+ @Override
+ public boolean matches(String uri) {
+ // If the uriTemplate doesn't contain variables, do a direct comparison
+ if (!this.isUriTemplate(this.uriTemplate)) {
+ return uri.equals(this.uriTemplate);
+ }
+
+ // Convert the pattern to a regex
+ String regex = this.uriTemplate.replaceAll("\\{[^/]+?\\}", "([^/]+?)");
+ regex = regex.replace("/", "\\/");
+
+ // Check if the URI matches the regex
+ return Pattern.compile(regex).matcher(uri).matches();
+ }
+
+ @Override
+ public boolean isUriTemplate(String uri) {
+ return URI_VARIABLE_PATTERN.matcher(uri).find();
+ }
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java b/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java
new file mode 100644
index 0000000..19569e4
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.util;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for working with URI templates.
+ *
+ * This interface provides methods for extracting variables from URI templates and
+ * matching them against actual URIs.
+ *
+ * @author Christian Tzolov
+ */
+public interface McpUriTemplateManager {
+
+ /**
+ * Extract URI variable names from this URI template.
+ * @return A list of variable names extracted from the template
+ * @throws IllegalArgumentException if duplicate variable names are found
+ */
+ List getVariableNames();
+
+ /**
+ * Extract URI variable values from the actual request URI.
+ *
+ * This method converts the URI template into a regex pattern, then uses that pattern
+ * to extract variable values from the request URI.
+ * @param uri The actual URI from the request
+ * @return A map of variable names to their values
+ * @throws IllegalArgumentException if the URI template is invalid or the request URI
+ * doesn't match the template pattern
+ */
+ Map extractVariableValues(String uri);
+
+ /**
+ * Indicate whether the given URI matches this template.
+ * @param uri the URI to match to
+ * @return {@code true} if it matches; {@code false} otherwise
+ */
+ boolean matches(String uri);
+
+ /**
+ * Check if the given URI is a URI template.
+ * @return Returns true if the URI contains variables in the format {variableName}
+ */
+ public boolean isUriTemplate(String uri);
+
+}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java b/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java
new file mode 100644
index 0000000..9644f9a
--- /dev/null
+++ b/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java
@@ -0,0 +1,22 @@
+/*
+* Copyright 2025 - 2025 the original author or authors.
+*/
+package io.modelcontextprotocol.util;
+
+/**
+ * Factory interface for creating instances of {@link McpUriTemplateManager}.
+ *
+ * @author Christian Tzolov
+ */
+public interface McpUriTemplateManagerFactory {
+
+ /**
+ * Creates a new instance of {@link McpUriTemplateManager} with the specified URI
+ * template.
+ * @param uriTemplate The URI template to be used for variable extraction
+ * @return A new instance of {@link McpUriTemplateManager}
+ * @throws IllegalArgumentException if the URI template is null or empty
+ */
+ McpUriTemplateManager create(String uriTemplate);
+
+}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java b/mcp/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java
new file mode 100644
index 0000000..6f041da
--- /dev/null
+++ b/mcp/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+
+import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
+import io.modelcontextprotocol.util.McpUriTemplateManager;
+import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link McpUriTemplateManager} and its implementations.
+ *
+ * @author Christian Tzolov
+ */
+public class McpUriTemplateManagerTests {
+
+ private McpUriTemplateManagerFactory uriTemplateFactory;
+
+ @BeforeEach
+ void setUp() {
+ this.uriTemplateFactory = new DeafaultMcpUriTemplateManagerFactory();
+ }
+
+ @Test
+ void shouldExtractVariableNamesFromTemplate() {
+ List variables = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}")
+ .getVariableNames();
+ assertEquals(2, variables.size());
+ assertEquals("userId", variables.get(0));
+ assertEquals("postId", variables.get(1));
+ }
+
+ @Test
+ void shouldReturnEmptyListWhenTemplateHasNoVariables() {
+ List variables = this.uriTemplateFactory.create("/api/users/all").getVariableNames();
+ assertEquals(0, variables.size());
+ }
+
+ @Test
+ void shouldThrowExceptionWhenExtractingVariablesFromNullTemplate() {
+ assertThrows(IllegalArgumentException.class, () -> this.uriTemplateFactory.create(null).getVariableNames());
+ }
+
+ @Test
+ void shouldThrowExceptionWhenExtractingVariablesFromEmptyTemplate() {
+ assertThrows(IllegalArgumentException.class, () -> this.uriTemplateFactory.create("").getVariableNames());
+ }
+
+ @Test
+ void shouldThrowExceptionWhenTemplateContainsDuplicateVariables() {
+ assertThrows(IllegalArgumentException.class,
+ () -> this.uriTemplateFactory.create("/api/users/{userId}/posts/{userId}").getVariableNames());
+ }
+
+ @Test
+ void shouldExtractVariableValuesFromRequestUri() {
+ Map values = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}")
+ .extractVariableValues("/api/users/123/posts/456");
+ assertEquals(2, values.size());
+ assertEquals("123", values.get("userId"));
+ assertEquals("456", values.get("postId"));
+ }
+
+ @Test
+ void shouldReturnEmptyMapWhenTemplateHasNoVariables() {
+ Map values = this.uriTemplateFactory.create("/api/users/all")
+ .extractVariableValues("/api/users/all");
+ assertEquals(0, values.size());
+ }
+
+ @Test
+ void shouldReturnEmptyMapWhenRequestUriIsNull() {
+ Map values = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}")
+ .extractVariableValues(null);
+ assertEquals(0, values.size());
+ }
+
+ @Test
+ void shouldMatchUriAgainstTemplatePattern() {
+ var uriTemplateManager = this.uriTemplateFactory.create("/api/users/{userId}/posts/{postId}");
+
+ assertTrue(uriTemplateManager.matches("/api/users/123/posts/456"));
+ assertFalse(uriTemplateManager.matches("/api/users/123/comments/456"));
+ }
+
+}
From e34babbe56b730514d35191118d5e66bc9c51b9a Mon Sep 17 00:00:00 2001
From: jito
Date: Thu, 8 May 2025 18:31:17 +0900
Subject: [PATCH 019/337] Add missing isInitialized method to McpSyncClient
(#181)
The isInitialized method is present in McpAsyncClient and needs to be
mirrored in McpSyncClient.
Signed-off-by: jitokim
---
.../io/modelcontextprotocol/client/McpSyncClient.java | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
index c91638a..a8fb979 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
@@ -97,6 +97,14 @@ public class McpSyncClient implements AutoCloseable {
return this.delegate.getServerInfo();
}
+ /**
+ * Check if the client-server connection is initialized.
+ * @return true if the client-server connection is initialized
+ */
+ public boolean isInitialized() {
+ return this.delegate.isInitialized();
+ }
+
/**
* Get the client capabilities that define the supported features and functionality.
* @return The client capabilities
From eae3840e7d44932c60c131cb7a346b5367b788ff Mon Sep 17 00:00:00 2001
From: Dennis Kawurek
Date: Fri, 25 Apr 2025 18:47:54 +0200
Subject: [PATCH 020/337] fix: Mockito inline mocking for Java 21+ (#207)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Before this fix the execution of the maven surefire
plugin with Java 21 logged warnings that mockito should be added as
a java agent, because the self-attaching won't be supported in future
java releases.
In Java 24 the test just broke.
This problem is solved by modifying the pom.xml of the parent and doing
this changes:
* Adding mockito as a java agent.
* Removing the surefireArgLine from the properties. This can be
added back when it's needed (for example when JaCoCo will be used).
Furthermore, the pom.xml in the mcp-spring-* modules now have
the byte-buddy dependency included, as the test would otherwise break
when trying to mock McpSchema#CreateMessageRequest.
Fixes #187
Co-authored-by: Dariusz Jędrzejczyk
---
mcp-spring/mcp-spring-webflux/pom.xml | 6 ++++++
mcp-spring/mcp-spring-webmvc/pom.xml | 6 ++++++
pom.xml | 15 +++++++++++++--
3 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml
index 63c32a8..86f46bf 100644
--- a/mcp-spring/mcp-spring-webflux/pom.xml
+++ b/mcp-spring/mcp-spring-webflux/pom.xml
@@ -82,6 +82,12 @@
${mockito.version}test
+
+ net.bytebuddy
+ byte-buddy
+ ${byte-buddy.version}
+ test
+ io.projectreactorreactor-test
diff --git a/mcp-spring/mcp-spring-webmvc/pom.xml b/mcp-spring/mcp-spring-webmvc/pom.xml
index b59be6a..82fbbf3 100644
--- a/mcp-spring/mcp-spring-webmvc/pom.xml
+++ b/mcp-spring/mcp-spring-webmvc/pom.xml
@@ -77,6 +77,12 @@
${mockito.version}test
+
+ net.bytebuddy
+ byte-buddy
+ ${byte-buddy.version}
+ test
+ org.testcontainersjunit-jupiter
diff --git a/pom.xml b/pom.xml
index 9be256c..6384574 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,7 @@
171717
+ 3.26.35.10.2
@@ -163,13 +164,23 @@
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+ properties
+
+
+
+ org.apache.maven.pluginsmaven-surefire-plugin${maven-surefire-plugin.version}
- ${surefireArgLine}
-
+ ${surefireArgLine} -javaagent:${org.mockito:mockito-core:jar}falsefalse
From 0069c977ef88b91162b08899bb8040a0ffcb8653 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?=
Date: Fri, 9 May 2025 12:57:36 +0200
Subject: [PATCH 021/337] Remove temporary delegate impl from McpAsyncServer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Dariusz Jędrzejczyk
---
.../server/McpAsyncServer.java | 1138 ++++++++---------
1 file changed, 512 insertions(+), 626 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index 3c112ad..1efa13d 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -82,11 +82,33 @@ public class McpAsyncServer {
private static final Logger logger = LoggerFactory.getLogger(McpAsyncServer.class);
- private final McpAsyncServer delegate;
+ private final McpServerTransportProvider mcpTransportProvider;
- McpAsyncServer() {
- this.delegate = null;
- }
+ private final ObjectMapper objectMapper;
+
+ private final McpSchema.ServerCapabilities serverCapabilities;
+
+ private final McpSchema.Implementation serverInfo;
+
+ private final String instructions;
+
+ private final CopyOnWriteArrayList tools = new CopyOnWriteArrayList<>();
+
+ private final CopyOnWriteArrayList resourceTemplates = new CopyOnWriteArrayList<>();
+
+ private final ConcurrentHashMap resources = new ConcurrentHashMap<>();
+
+ private final ConcurrentHashMap prompts = new ConcurrentHashMap<>();
+
+ // FIXME: this field is deprecated and should be remvoed together with the
+ // broadcasting loggingNotification.
+ private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
+
+ private final ConcurrentHashMap completions = new ConcurrentHashMap<>();
+
+ private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
+
+ private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
/**
* Create a new McpAsyncServer with the given transport provider and capabilities.
@@ -98,8 +120,104 @@ public class McpAsyncServer {
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory) {
- this.delegate = new AsyncServerImpl(mcpTransportProvider, objectMapper, requestTimeout, features,
- uriTemplateManagerFactory);
+ this.mcpTransportProvider = mcpTransportProvider;
+ this.objectMapper = objectMapper;
+ this.serverInfo = features.serverInfo();
+ this.serverCapabilities = features.serverCapabilities();
+ this.instructions = features.instructions();
+ this.tools.addAll(features.tools());
+ this.resources.putAll(features.resources());
+ this.resourceTemplates.addAll(features.resourceTemplates());
+ this.prompts.putAll(features.prompts());
+ this.completions.putAll(features.completions());
+ this.uriTemplateManagerFactory = uriTemplateManagerFactory;
+
+ Map> requestHandlers = new HashMap<>();
+
+ // Initialize request handlers for standard MCP methods
+
+ // Ping MUST respond with an empty data, but not NULL response.
+ requestHandlers.put(McpSchema.METHOD_PING, (exchange, params) -> Mono.just(Map.of()));
+
+ // Add tools API handlers if the tool capability is enabled
+ if (this.serverCapabilities.tools() != null) {
+ requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
+ }
+
+ // Add resources API handlers if provided
+ if (this.serverCapabilities.resources() != null) {
+ requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
+ }
+
+ // Add prompts API handlers if provider exists
+ if (this.serverCapabilities.prompts() != null) {
+ requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
+ requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
+ }
+
+ // Add logging API handlers if the logging capability is enabled
+ if (this.serverCapabilities.logging() != null) {
+ requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
+ }
+
+ // Add completion API handlers if the completion capability is enabled
+ if (this.serverCapabilities.completions() != null) {
+ requestHandlers.put(McpSchema.METHOD_COMPLETION_COMPLETE, completionCompleteRequestHandler());
+ }
+
+ Map notificationHandlers = new HashMap<>();
+
+ notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty());
+
+ List, Mono>> rootsChangeConsumers = features
+ .rootsChangeConsumers();
+
+ if (Utils.isEmpty(rootsChangeConsumers)) {
+ rootsChangeConsumers = List.of((exchange, roots) -> Mono.fromRunnable(() -> logger
+ .warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots)));
+ }
+
+ notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
+ asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
+
+ mcpTransportProvider.setSessionFactory(
+ transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport,
+ this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers));
+ }
+
+ // ---------------------------------------
+ // Lifecycle Management
+ // ---------------------------------------
+ private Mono asyncInitializeRequestHandler(
+ McpSchema.InitializeRequest initializeRequest) {
+ return Mono.defer(() -> {
+ logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
+ initializeRequest.protocolVersion(), initializeRequest.capabilities(),
+ initializeRequest.clientInfo());
+
+ // The server MUST respond with the highest protocol version it supports
+ // if
+ // it does not support the requested (e.g. Client) version.
+ String serverProtocolVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
+
+ if (this.protocolVersions.contains(initializeRequest.protocolVersion())) {
+ // If the server supports the requested protocol version, it MUST
+ // respond
+ // with the same version.
+ serverProtocolVersion = initializeRequest.protocolVersion();
+ }
+ else {
+ logger.warn(
+ "Client requested unsupported protocol version: {}, so the server will suggest the {} version instead",
+ initializeRequest.protocolVersion(), serverProtocolVersion);
+ }
+
+ return Mono.just(new McpSchema.InitializeResult(serverProtocolVersion, this.serverCapabilities,
+ this.serverInfo, this.instructions));
+ });
}
/**
@@ -107,7 +225,7 @@ public class McpAsyncServer {
* @return The server capabilities
*/
public McpSchema.ServerCapabilities getServerCapabilities() {
- return this.delegate.getServerCapabilities();
+ return this.serverCapabilities;
}
/**
@@ -115,7 +233,7 @@ public class McpAsyncServer {
* @return The server implementation details
*/
public McpSchema.Implementation getServerInfo() {
- return this.delegate.getServerInfo();
+ return this.serverInfo;
}
/**
@@ -123,26 +241,66 @@ public class McpAsyncServer {
* @return A Mono that completes when the server has been closed
*/
public Mono closeGracefully() {
- return this.delegate.closeGracefully();
+ return this.mcpTransportProvider.closeGracefully();
}
/**
* Close the server immediately.
*/
public void close() {
- this.delegate.close();
+ this.mcpTransportProvider.close();
+ }
+
+ private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler(
+ List, Mono>> rootsChangeConsumers) {
+ return (exchange, params) -> exchange.listRoots()
+ .flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
+ .flatMap(consumer -> consumer.apply(exchange, listRootsResult.roots()))
+ .onErrorResume(error -> {
+ logger.error("Error handling roots list change notification", error);
+ return Mono.empty();
+ })
+ .then());
}
// ---------------------------------------
// Tool Management
// ---------------------------------------
+
/**
* Add a new tool specification at runtime.
* @param toolSpecification The tool specification to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
- return this.delegate.addTool(toolSpecification);
+ if (toolSpecification == null) {
+ return Mono.error(new McpError("Tool specification must not be null"));
+ }
+ if (toolSpecification.tool() == null) {
+ return Mono.error(new McpError("Tool must not be null"));
+ }
+ if (toolSpecification.call() == null) {
+ return Mono.error(new McpError("Tool call handler must not be null"));
+ }
+ if (this.serverCapabilities.tools() == null) {
+ return Mono.error(new McpError("Server must be configured with tool capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ // Check for duplicate tool names
+ if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolSpecification.tool().name()))) {
+ return Mono
+ .error(new McpError("Tool with name '" + toolSpecification.tool().name() + "' already exists"));
+ }
+
+ this.tools.add(toolSpecification);
+ logger.debug("Added tool handler: {}", toolSpecification.tool().name());
+
+ if (this.serverCapabilities.tools().listChanged()) {
+ return notifyToolsListChanged();
+ }
+ return Mono.empty();
+ });
}
/**
@@ -151,7 +309,25 @@ public class McpAsyncServer {
* @return Mono that completes when clients have been notified of the change
*/
public Mono removeTool(String toolName) {
- return this.delegate.removeTool(toolName);
+ if (toolName == null) {
+ return Mono.error(new McpError("Tool name must not be null"));
+ }
+ if (this.serverCapabilities.tools() == null) {
+ return Mono.error(new McpError("Server must be configured with tool capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ boolean removed = this.tools
+ .removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName));
+ if (removed) {
+ logger.debug("Removed tool handler: {}", toolName);
+ if (this.serverCapabilities.tools().listChanged()) {
+ return notifyToolsListChanged();
+ }
+ return Mono.empty();
+ }
+ return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
+ });
}
/**
@@ -159,19 +335,65 @@ public class McpAsyncServer {
* @return A Mono that completes when all clients have been notified
*/
public Mono notifyToolsListChanged() {
- return this.delegate.notifyToolsListChanged();
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
+ }
+
+ private McpServerSession.RequestHandler toolsListRequestHandler() {
+ return (exchange, params) -> {
+ List tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
+
+ return Mono.just(new McpSchema.ListToolsResult(tools, null));
+ };
+ }
+
+ private McpServerSession.RequestHandler toolsCallRequestHandler() {
+ return (exchange, params) -> {
+ McpSchema.CallToolRequest callToolRequest = objectMapper.convertValue(params,
+ new TypeReference() {
+ });
+
+ Optional toolSpecification = this.tools.stream()
+ .filter(tr -> callToolRequest.name().equals(tr.tool().name()))
+ .findAny();
+
+ if (toolSpecification.isEmpty()) {
+ return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
+ }
+
+ return toolSpecification.map(tool -> tool.call().apply(exchange, callToolRequest.arguments()))
+ .orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
+ };
}
// ---------------------------------------
// Resource Management
// ---------------------------------------
+
/**
* Add a new resource handler at runtime.
- * @param resourceHandler The resource handler to add
+ * @param resourceSpecification The resource handler to add
* @return Mono that completes when clients have been notified of the change
*/
- public Mono addResource(McpServerFeatures.AsyncResourceSpecification resourceHandler) {
- return this.delegate.addResource(resourceHandler);
+ public Mono addResource(McpServerFeatures.AsyncResourceSpecification resourceSpecification) {
+ if (resourceSpecification == null || resourceSpecification.resource() == null) {
+ return Mono.error(new McpError("Resource must not be null"));
+ }
+
+ if (this.serverCapabilities.resources() == null) {
+ return Mono.error(new McpError("Server must be configured with resource capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ if (this.resources.putIfAbsent(resourceSpecification.resource().uri(), resourceSpecification) != null) {
+ return Mono.error(new McpError(
+ "Resource with URI '" + resourceSpecification.resource().uri() + "' already exists"));
+ }
+ logger.debug("Added resource handler: {}", resourceSpecification.resource().uri());
+ if (this.serverCapabilities.resources().listChanged()) {
+ return notifyResourcesListChanged();
+ }
+ return Mono.empty();
+ });
}
/**
@@ -180,7 +402,24 @@ public class McpAsyncServer {
* @return Mono that completes when clients have been notified of the change
*/
public Mono removeResource(String resourceUri) {
- return this.delegate.removeResource(resourceUri);
+ if (resourceUri == null) {
+ return Mono.error(new McpError("Resource URI must not be null"));
+ }
+ if (this.serverCapabilities.resources() == null) {
+ return Mono.error(new McpError("Server must be configured with resource capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ McpServerFeatures.AsyncResourceSpecification removed = this.resources.remove(resourceUri);
+ if (removed != null) {
+ logger.debug("Removed resource handler: {}", resourceUri);
+ if (this.serverCapabilities.resources().listChanged()) {
+ return notifyResourcesListChanged();
+ }
+ return Mono.empty();
+ }
+ return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
+ });
}
/**
@@ -188,19 +427,97 @@ public class McpAsyncServer {
* @return A Mono that completes when all clients have been notified
*/
public Mono notifyResourcesListChanged() {
- return this.delegate.notifyResourcesListChanged();
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
+ }
+
+ private McpServerSession.RequestHandler resourcesListRequestHandler() {
+ return (exchange, params) -> {
+ var resourceList = this.resources.values()
+ .stream()
+ .map(McpServerFeatures.AsyncResourceSpecification::resource)
+ .toList();
+ return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
+ };
+ }
+
+ private McpServerSession.RequestHandler resourceTemplateListRequestHandler() {
+ return (exchange, params) -> Mono
+ .just(new McpSchema.ListResourceTemplatesResult(this.getResourceTemplates(), null));
+
+ }
+
+ private List getResourceTemplates() {
+ var list = new ArrayList<>(this.resourceTemplates);
+ List resourceTemplates = this.resources.keySet()
+ .stream()
+ .filter(uri -> uri.contains("{"))
+ .map(uri -> {
+ var resource = this.resources.get(uri).resource();
+ var template = new McpSchema.ResourceTemplate(resource.uri(), resource.name(), resource.description(),
+ resource.mimeType(), resource.annotations());
+ return template;
+ })
+ .toList();
+
+ list.addAll(resourceTemplates);
+
+ return list;
+ }
+
+ private McpServerSession.RequestHandler resourcesReadRequestHandler() {
+ return (exchange, params) -> {
+ McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
+ new TypeReference() {
+ });
+ var resourceUri = resourceRequest.uri();
+
+ McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
+ .stream()
+ .filter(resourceSpecification -> this.uriTemplateManagerFactory
+ .create(resourceSpecification.resource().uri())
+ .matches(resourceUri))
+ .findFirst()
+ .orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
+
+ return specification.readHandler().apply(exchange, resourceRequest);
+ };
}
// ---------------------------------------
// Prompt Management
// ---------------------------------------
+
/**
* Add a new prompt handler at runtime.
* @param promptSpecification The prompt handler to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
- return this.delegate.addPrompt(promptSpecification);
+ if (promptSpecification == null) {
+ return Mono.error(new McpError("Prompt specification must not be null"));
+ }
+ if (this.serverCapabilities.prompts() == null) {
+ return Mono.error(new McpError("Server must be configured with prompt capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ McpServerFeatures.AsyncPromptSpecification specification = this.prompts
+ .putIfAbsent(promptSpecification.prompt().name(), promptSpecification);
+ if (specification != null) {
+ return Mono.error(
+ new McpError("Prompt with name '" + promptSpecification.prompt().name() + "' already exists"));
+ }
+
+ logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());
+
+ // Servers that declared the listChanged capability SHOULD send a
+ // notification,
+ // when the list of available prompts changes
+ if (this.serverCapabilities.prompts().listChanged()) {
+ return notifyPromptsListChanged();
+ }
+ return Mono.empty();
+ });
}
/**
@@ -209,7 +526,27 @@ public class McpAsyncServer {
* @return Mono that completes when clients have been notified of the change
*/
public Mono removePrompt(String promptName) {
- return this.delegate.removePrompt(promptName);
+ if (promptName == null) {
+ return Mono.error(new McpError("Prompt name must not be null"));
+ }
+ if (this.serverCapabilities.prompts() == null) {
+ return Mono.error(new McpError("Server must be configured with prompt capabilities"));
+ }
+
+ return Mono.defer(() -> {
+ McpServerFeatures.AsyncPromptSpecification removed = this.prompts.remove(promptName);
+
+ if (removed != null) {
+ logger.debug("Removed prompt handler: {}", promptName);
+ // Servers that declared the listChanged capability SHOULD send a
+ // notification, when the list of available prompts changes
+ if (this.serverCapabilities.prompts().listChanged()) {
+ return this.notifyPromptsListChanged();
+ }
+ return Mono.empty();
+ }
+ return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
+ });
}
/**
@@ -217,7 +554,39 @@ public class McpAsyncServer {
* @return A Mono that completes when all clients have been notified
*/
public Mono notifyPromptsListChanged() {
- return this.delegate.notifyPromptsListChanged();
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
+ }
+
+ private McpServerSession.RequestHandler promptsListRequestHandler() {
+ return (exchange, params) -> {
+ // TODO: Implement pagination
+ // McpSchema.PaginatedRequest request = objectMapper.convertValue(params,
+ // new TypeReference() {
+ // });
+
+ var promptList = this.prompts.values()
+ .stream()
+ .map(McpServerFeatures.AsyncPromptSpecification::prompt)
+ .toList();
+
+ return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
+ };
+ }
+
+ private McpServerSession.RequestHandler promptsGetRequestHandler() {
+ return (exchange, params) -> {
+ McpSchema.GetPromptRequest promptRequest = objectMapper.convertValue(params,
+ new TypeReference() {
+ });
+
+ // Implement prompt retrieval logic here
+ McpServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name());
+ if (specification == null) {
+ return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
+ }
+
+ return specification.promptHandler().apply(exchange, promptRequest);
+ };
}
// ---------------------------------------
@@ -237,619 +606,136 @@ public class McpAsyncServer {
*/
@Deprecated
public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) {
- return this.delegate.loggingNotification(loggingMessageNotification);
+
+ if (loggingMessageNotification == null) {
+ return Mono.error(new McpError("Logging message must not be null"));
+ }
+
+ if (loggingMessageNotification.level().level() < minLoggingLevel.level()) {
+ return Mono.empty();
+ }
+
+ return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_MESSAGE,
+ loggingMessageNotification);
+ }
+
+ private McpServerSession.RequestHandler