From 344f1b838340efbcf0cfcd428358bdb77e9a533c Mon Sep 17 00:00:00 2001
From: E550448
Date: Sun, 13 Apr 2025 12:54:48 +0200
Subject: [PATCH 001/290] 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 ca87736cd..9fc17306e 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 6b0f4a9fe..17693ab32 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 632d3844a..99cf2a625 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.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 HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques
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 Mono connect(Function, Mono> h
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 Mono sendMessage(JSONRPCMessage message) {
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 0f799ca0f..8e654e596 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 static boolean isEmpty(@Nullable Map, ?> map) {
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 e5178c0ee..762264de3 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 @@
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.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 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 @@ void testChainedCustomizations() {
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 aced20cbc..0f2e689b5 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 @@
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 @@ void testMapIsEmpty() {
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 ff485b75d..9be256ccf 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 002/290] 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 e7e338030..e77edb3b7 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 record CompleteArgument(
@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 003/290] 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 c1f42e3fb..9ed0d8edd 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 @@ private Mono handleIncomingRequest(McpSchema.JSONRPCR
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 46c356cdd..64315095b 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 @@ private Mono handleIncomingNotification(McpSchema.JSONRPCNotification noti
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 004/290] 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 9ed0d8edd..a25f38c5c 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 McpClientSession(Duration requestTimeout, McpClientTransport transport,
// 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 McpClientSession(Duration requestTimeout, McpClientTransport transport,
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 005/290] 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 a25f38c5c..6eca34757 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 McpClientSession(Duration requestTimeout, McpClientTransport transport,
// 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 006/290] 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 007/290] 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 e77edb3b7..8df8a1584 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 record JsonSchema( // @formatter:off
@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 a41fc095f..ff78c1bfc 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.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 @@ void testGetPromptResult() throws Exception {
// 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 @@ void testTool() throws Exception {
{"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 008/290] 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 cdd43e7ef..025cfeacf 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 @@
/**
* 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 c81e638c1..e313454bd 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 @@
/**
* 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 2bc74f258..e3a997ba3 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 Mono closeGracefully() {
* 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 64315095b..86906d859 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 009/290] 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 6eca34757..f577b493a 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 @@ private String generateRequestId() {
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 010/290] 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 660f814da..2ba047461 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 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
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 906cb9a08..3c112ad76 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.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 @@ private static class AsyncServerImpl extends 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 @@ private static class AsyncServerImpl extends McpAsyncServer {
this.resourceTemplates.addAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
this.completions.putAll(features.completions());
+ this.uriTemplateManagerFactory = uriTemplateManagerFactory;
Map> requestHandlers = new HashMap<>();
@@ -564,8 +574,26 @@ private McpServerSession.RequestHandler resources
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() {
@@ -574,11 +602,16 @@ private McpServerSession.RequestHandler resourcesR
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 @@ private McpServerSession.RequestHandler completionComp
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 84089703c..d6ec2cc30 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.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 @@ class AsyncSpecification {
private final McpServerTransportProvider transportProvider;
+ private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
+
private ObjectMapper objectMapper;
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
@@ -204,6 +208,19 @@ private AsyncSpecification(McpServerTransportProvider transportProvider) {
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 AsyncSpecification prompts(McpServerFeatures.AsyncPromptSpecification...
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 McpAsyncServer build() {
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 @@ class SyncSpecification {
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 @@ private SyncSpecification(McpServerTransportProvider transportProvider) {
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 McpSyncServer build() {
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 000000000..3870b76fc
--- /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 000000000..b2e9a5285
--- /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 000000000..19569e49f
--- /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 000000000..9644f9a6c
--- /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 000000000..6f041daa6
--- /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 011/290] 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 c91638a7e..a8fb979e1 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 McpSchema.Implementation getServerInfo() {
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 012/290] 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 63c32a8a8..86f46bf95 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 b59be6a03..82fbbf3e6 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 9be256ccf..638457406 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 013/290] 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 | 1082 ++++++++---------
1 file changed, 484 insertions(+), 598 deletions(-)
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index 3c112ad76..1efa13de3 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 McpSchema.ServerCapabilities getServerCapabilities() {
* @return The server implementation details
*/
public McpSchema.Implementation getServerInfo() {
- return this.delegate.getServerInfo();
+ return this.serverInfo;
}
/**
@@ -123,26 +241,66 @@ public McpSchema.Implementation getServerInfo() {
* @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 Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
* @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 Mono removeTool(String toolName) {
* @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 Mono addResource(McpServerFeatures.AsyncResourceSpecification resou
* @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 Mono removeResource(String resourceUri) {
* @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 Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpe
* @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 Mono removePrompt(String promptName) {
* @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 Mono notifyPromptsListChanged() {
*/
@Deprecated
public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) {
- return this.delegate.loggingNotification(loggingMessageNotification);
- }
-
- // ---------------------------------------
- // Sampling
- // ---------------------------------------
- /**
- * This method is package-private and used for test only. Should not be called by user
- * code.
- * @param protocolVersions the Client supported protocol versions.
- */
- void setProtocolVersions(List protocolVersions) {
- this.delegate.setProtocolVersions(protocolVersions);
- }
-
- private static class AsyncServerImpl extends McpAsyncServer {
-
- private final McpServerTransportProvider mcpTransportProvider;
-
- 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();
-
- AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
- Duration requestTimeout, McpServerFeatures.Async features,
- McpUriTemplateManagerFactory 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));
- });
- }
-
- public McpSchema.ServerCapabilities getServerCapabilities() {
- return this.serverCapabilities;
- }
-
- public McpSchema.Implementation getServerInfo() {
- return this.serverInfo;
- }
- @Override
- public Mono closeGracefully() {
- return this.mcpTransportProvider.closeGracefully();
+ if (loggingMessageNotification == null) {
+ return Mono.error(new McpError("Logging message must not be null"));
}
- @Override
- public void close() {
- this.mcpTransportProvider.close();
+ if (loggingMessageNotification.level().level() < minLoggingLevel.level()) {
+ return Mono.empty();
}
- 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
- // ---------------------------------------
-
- @Override
- public Mono addTool(McpServerFeatures.AsyncToolSpecification 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();
- });
- }
-
- @Override
- public Mono removeTool(String 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 this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_MESSAGE,
+ loggingMessageNotification);
+ }
+ private McpServerSession.RequestHandler