From de986d2b2928894652766bb8608bd4741155d2fc Mon Sep 17 00:00:00 2001 From: "a.darafeyeu" Date: Mon, 21 Apr 2025 17:52:37 +0200 Subject: [PATCH 1/2] feat: adds mcp-spi --- mcp-bom/pom.xml | 11 ++- {mcp => mcp-reactor}/README.md | 0 {mcp => mcp-reactor}/pom.xml | 35 +++------- .../client/McpAsyncClient.java | 9 +-- .../client/McpClient.java | 2 +- .../client/McpClientFeatures.java | 0 .../client/McpSyncClient.java | 0 .../client/transport/FlowSseClient.java | 0 .../HttpClientSseClientTransport.java | 19 +++-- .../client/transport/ServerParameters.java | 0 .../transport/StdioClientTransport.java | 12 +++- .../server/McpAsyncServer.java | 19 ++--- .../server/McpAsyncServerExchange.java | 2 +- .../server/McpServer.java | 0 .../server/McpServerFeatures.java | 14 ++-- .../server/McpSyncServer.java | 0 .../server/McpSyncServerExchange.java | 1 - ...HttpServletSseServerTransportProvider.java | 14 ++-- .../StdioServerTransportProvider.java | 15 ++-- .../session}/McpClientSession.java | 32 +++++---- .../session}/McpServerSession.java | 46 +++++-------- .../io/modelcontextprotocol/util/Utils.java | 0 .../McpUriTemplateManagerTests.java | 0 .../MockMcpClientTransport.java | 10 ++- .../MockMcpServerTransport.java | 5 ++ .../MockMcpServerTransportProvider.java | 15 ++-- .../client/AbstractMcpAsyncClientTests.java | 2 +- .../client/AbstractMcpSyncClientTests.java | 2 +- .../client/HttpSseMcpAsyncClientTests.java | 0 .../client/HttpSseMcpSyncClientTests.java | 0 .../McpAsyncClientResponseHandlerTests.java | 0 .../client/McpClientProtocolVersionTests.java | 0 .../client/StdioMcpAsyncClientTests.java | 0 .../client/StdioMcpSyncClientTests.java | 0 .../HttpClientSseClientTransportTests.java | 60 ++++++++++++++-- .../server/AbstractMcpAsyncServerTests.java | 2 +- .../server/AbstractMcpSyncServerTests.java | 2 +- .../server/McpServerProtocolVersionTests.java | 0 .../server/ServletSseMcpAsyncServerTests.java | 0 .../server/ServletSseMcpSyncServerTests.java | 0 .../server/StdioMcpAsyncServerTests.java | 2 +- .../server/StdioMcpSyncServerTests.java | 0 ...ervletSseServerCustomContextPathTests.java | 0 ...rverTransportProviderIntegrationTests.java | 69 +++++++++++++------ .../StdioServerTransportProviderTests.java | 10 +-- .../server/transport/TomcatTestUtil.java | 0 .../session}/McpClientSessionTests.java | 5 +- .../modelcontextprotocol/util/UtilsTests.java | 0 .../src/test/resources/logback.xml | 6 +- mcp-spi/pom.xml | 63 +++++++++++++++++ .../spec/McpClientTransport.java | 8 +-- .../modelcontextprotocol/spec/McpError.java | 0 .../modelcontextprotocol/spec/McpSchema.java | 14 ++-- .../spec/McpServerTransport.java | 0 .../spec/McpServerTransportProvider.java | 31 ++++----- .../modelcontextprotocol/spec/McpSession.java | 16 ++--- .../spec/McpTransport.java | 22 +++--- .../spec/ServerSessionFactory.java | 21 ++++++ .../io/modelcontextprotocol/util/Assert.java | 14 ++-- .../spec}/AssertTests.java | 4 +- .../spec/McpSchemaTests.java | 22 +++--- mcp-spring/mcp-spring-webflux/README.md | 5 +- mcp-spring/mcp-spring-webflux/pom.xml | 11 ++- .../transport/WebFluxSseClientTransport.java | 9 ++- .../WebFluxSseServerTransportProvider.java | 16 +++-- mcp-spring/mcp-spring-webmvc/README.md | 5 +- mcp-spring/mcp-spring-webmvc/pom.xml | 11 ++- .../WebMvcSseServerTransportProvider.java | 27 +++++--- .../WebMvcSseAsyncServerTransportTests.java | 4 +- .../src/test/resources/logback.xml | 2 +- mcp-test/pom.xml | 10 ++- .../MockMcpTransport.java | 10 ++- .../server/BaseMcpAsyncServerTests.java | 5 -- pom.xml | 3 +- 74 files changed, 499 insertions(+), 255 deletions(-) rename {mcp => mcp-reactor}/README.md (100%) rename {mcp => mcp-reactor}/pom.xml (85%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java (98%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/McpClient.java (99%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java (96%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java (97%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java (97%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java (98%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/McpServer.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java (97%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java (98%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java (97%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java (96%) rename {mcp/src/main/java/io/modelcontextprotocol/spec => mcp-reactor/src/main/java/io/modelcontextprotocol/session}/McpClientSession.java (91%) rename {mcp/src/main/java/io/modelcontextprotocol/spec => mcp-reactor/src/main/java/io/modelcontextprotocol/session}/McpServerSession.java (92%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/util/Utils.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java (91%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java (96%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java (82%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java (99%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java (99%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java (90%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java (99%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java (99%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java (97%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java (100%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java (93%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java (96%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java (100%) rename {mcp/src/test/java/io/modelcontextprotocol/spec => mcp-reactor/src/test/java/io/modelcontextprotocol/session}/McpClientSessionTests.java (98%) rename {mcp => mcp-reactor}/src/test/java/io/modelcontextprotocol/util/UtilsTests.java (100%) rename {mcp => mcp-reactor}/src/test/resources/logback.xml (72%) create mode 100644 mcp-spi/pom.xml rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java (52%) rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/spec/McpError.java (100%) rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/spec/McpSchema.java (99%) rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/spec/McpServerTransport.java (100%) rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java (66%) rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/spec/McpSession.java (84%) rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/spec/McpTransport.java (78%) create mode 100644 mcp-spi/src/main/java/io/modelcontextprotocol/spec/ServerSessionFactory.java rename {mcp => mcp-spi}/src/main/java/io/modelcontextprotocol/util/Assert.java (87%) rename {mcp/src/test/java/io/modelcontextprotocol/util => mcp-spi/src/test/java/io/modelcontextprotocol/spec}/AssertTests.java (93%) rename {mcp => mcp-spi}/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java (97%) delete mode 100644 mcp/src/test/java/io/modelcontextprotocol/server/BaseMcpAsyncServerTests.java diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml index 4f24f719..f876bb7f 100644 --- a/mcp-bom/pom.xml +++ b/mcp-bom/pom.xml @@ -26,10 +26,17 @@ - + io.modelcontextprotocol.sdk - mcp + mcp-reactor + ${project.version} + + + + + io.modelcontextprotocol.sdk + mcp-reactor ${project.version} diff --git a/mcp/README.md b/mcp-reactor/README.md similarity index 100% rename from mcp/README.md rename to mcp-reactor/README.md diff --git a/mcp/pom.xml b/mcp-reactor/pom.xml similarity index 85% rename from mcp/pom.xml rename to mcp-reactor/pom.xml index 17693ab3..539a0b58 100644 --- a/mcp/pom.xml +++ b/mcp-reactor/pom.xml @@ -8,10 +8,10 @@ mcp-parent 0.10.0-SNAPSHOT - mcp + mcp-reactor jar - Java MCP SDK - Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools + Java MCP Reactor SDK + Java Reactor SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools https://github.com/modelcontextprotocol/java-sdk @@ -65,6 +65,11 @@ + + io.modelcontextprotocol.sdk + mcp-spi + ${project.version} + org.slf4j @@ -83,13 +88,6 @@ reactor-core - - org.springframework - spring-webmvc - ${springframework.version} - test - - io.projectreactor.netty @@ -97,23 +95,6 @@ test - - - org.springframework - spring-context - ${springframework.version} - test - - - - org.springframework - spring-test - ${springframework.version} - test - - org.assertj assertj-core diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java similarity index 98% rename from mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index e3a997ba..641bf0b7 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -14,9 +14,9 @@ import java.util.function.Function; import com.fasterxml.jackson.core.type.TypeReference; -import io.modelcontextprotocol.spec.McpClientSession; -import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler; -import io.modelcontextprotocol.spec.McpClientSession.RequestHandler; +import io.modelcontextprotocol.session.McpClientSession; +import io.modelcontextprotocol.session.McpClientSession.NotificationHandler; +import io.modelcontextprotocol.session.McpClientSession.RequestHandler; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; @@ -471,7 +471,8 @@ public Mono removeRoot(String rootUri) { */ public Mono rootsListChangedNotification() { return this.withInitializationCheck("sending roots list changed notification", - initResult -> this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED)); + initResult -> (Mono) this.mcpSession + .sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED)); } private RequestHandler rootsListRequestHandler() { diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClient.java similarity index 99% rename from mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClient.java index a1dc1168..f21d1430 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClient.java @@ -400,7 +400,7 @@ class AsyncSpec { private ClientCapabilities capabilities; - private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1"); + private Implementation clientInfo = new Implementation("Java Reactor AI MCP Client", "0.3.1"); private final Map roots = new HashMap<>(); diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/FlowSseClient.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java similarity index 96% rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index 99cf2a62..2c56ab20 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -22,17 +22,20 @@ import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpTransport; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Utils; + +import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import reactor.core.publisher.Mono; /** - * Server-Sent Events (SSE) implementation of the - * {@link io.modelcontextprotocol.spec.McpTransport} that follows the MCP HTTP with SSE - * transport specification, using Java's HttpClient. + * Server-Sent Events (SSE) implementation of the {@link McpTransport} that follows the + * MCP HTTP with SSE transport specification, using Java's HttpClient. * *

* This transport implementation establishes a bidirectional communication channel between @@ -337,7 +340,7 @@ public HttpClientSseClientTransport build() { * @return a Mono that completes when the connection is established */ @Override - public Mono connect(Function, Mono> handler) { + public Mono connect(Function, Publisher> handler) { CompletableFuture future = new CompletableFuture<>(); connectionFuture.set(future); @@ -358,7 +361,8 @@ public void onEvent(SseEvent event) { } else if (MESSAGE_EVENT_TYPE.equals(event.type())) { JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, event.data()); - handler.apply(Mono.just(message)).subscribe(); + Publisher result = handler.apply(Mono.just(message)); + Mono.from(result).subscribe(); } else { logger.error("Received unrecognized SSE event type: {}", event.type()); @@ -435,6 +439,11 @@ public Mono sendMessage(JSONRPCMessage message) { } } + @Override + public void close() { + this.closeGracefully().subscribe(); + } + /** * Gracefully closes the transport connection. * diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/ServerParameters.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java similarity index 97% rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java index 9d71cbb4..709a2f3b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java @@ -21,6 +21,8 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; import io.modelcontextprotocol.util.Assert; + +import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -110,7 +112,7 @@ public StdioClientTransport(ServerParameters params, ObjectMapper objectMapper) * are null */ @Override - public Mono connect(Function, Mono> handler) { + public Mono connect(Function, Publisher> handler) { return Mono.fromRunnable(() -> { handleIncomingMessages(handler); handleIncomingErrors(); @@ -218,7 +220,8 @@ private void startErrorProcessing() { }); } - private void handleIncomingMessages(Function, Mono> inboundMessageHandler) { + private void handleIncomingMessages( + Function, Publisher> inboundMessageHandler) { this.inboundSink.asFlux() .flatMap(message -> Mono.just(message) .transform(inboundMessageHandler) @@ -333,6 +336,11 @@ protected void handleOutbound(Function, Flux closeGracefully() { - return this.mcpTransportProvider.closeGracefully(); + return Mono.from(this.mcpTransportProvider.closeGracefully()); } @Override @@ -478,7 +478,8 @@ public Mono removeTool(String toolName) { @Override public Mono notifyToolsListChanged() { - return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null); + return Mono + .from(this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null)); } private McpServerSession.RequestHandler toolsListRequestHandler() { @@ -559,7 +560,8 @@ public Mono removeResource(String resourceUri) { @Override public Mono notifyResourcesListChanged() { - return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null); + return Mono.from(this.mcpTransportProvider + .notifyClients(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null)); } private McpServerSession.RequestHandler resourcesListRequestHandler() { @@ -675,7 +677,8 @@ public Mono removePrompt(String promptName) { @Override public Mono notifyPromptsListChanged() { - return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null); + return Mono.from( + this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null)); } private McpServerSession.RequestHandler promptsListRequestHandler() { @@ -725,8 +728,8 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN return Mono.empty(); } - return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_MESSAGE, - loggingMessageNotification); + return Mono.from(this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_MESSAGE, + loggingMessageNotification)); } private McpServerSession.RequestHandler setLoggerRequestHandler() { diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java similarity index 98% rename from mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index 889dc66d..3b214222 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -9,7 +9,7 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; -import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServer.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServer.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java similarity index 97% rename from mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java index 8311f5d4..08660245 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java @@ -276,7 +276,7 @@ static AsyncToolSpecification fromSync(SyncToolSpecification tool) { * @param readHandler The function that handles resource read requests. The function's * first argument is an {@link McpAsyncServerExchange} upon which the server can * interact with the connected client. The second arguments is a - * {@link io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest}. + * {@link McpSchema.ReadResourceRequest}. */ public record AsyncResourceSpecification(McpSchema.Resource resource, BiFunction> readHandler) { @@ -321,8 +321,7 @@ static AsyncResourceSpecification fromSync(SyncResourceSpecification resource) { * @param promptHandler The function that processes prompt requests and returns * formatted templates. The function's first argument is an * {@link McpAsyncServerExchange} upon which the server can interact with the - * connected client. The second arguments is a - * {@link io.modelcontextprotocol.spec.McpSchema.GetPromptRequest}. + * connected client. The second arguments is a {@link McpSchema.GetPromptRequest}. */ public record AsyncPromptSpecification(McpSchema.Prompt prompt, BiFunction> promptHandler) { @@ -353,7 +352,7 @@ static AsyncPromptSpecification fromSync(SyncPromptSpecification prompt) { * @param completionHandler The asynchronous function that processes completion * requests and returns results. The first argument is an * {@link McpAsyncServerExchange} used to interact with the client. The second - * argument is a {@link io.modelcontextprotocol.spec.McpSchema.CompleteRequest}. + * argument is a {@link McpSchema.CompleteRequest}. */ public record AsyncCompletionSpecification(McpSchema.CompleteReference referenceKey, BiFunction> completionHandler) { @@ -442,7 +441,7 @@ public record SyncToolSpecification(McpSchema.Tool tool, * @param readHandler The function that handles resource read requests. The function's * first argument is an {@link McpSyncServerExchange} upon which the server can * interact with the connected client. The second arguments is a - * {@link io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest}. + * {@link McpSchema.ReadResourceRequest}. */ public record SyncResourceSpecification(McpSchema.Resource resource, BiFunction readHandler) { @@ -476,8 +475,7 @@ public record SyncResourceSpecification(McpSchema.Resource resource, * @param promptHandler The function that processes prompt requests and returns * formatted templates. The function's first argument is an * {@link McpSyncServerExchange} upon which the server can interact with the connected - * client. The second arguments is a - * {@link io.modelcontextprotocol.spec.McpSchema.GetPromptRequest}. + * client. The second arguments is a {@link McpSchema.GetPromptRequest}. */ public record SyncPromptSpecification(McpSchema.Prompt prompt, BiFunction promptHandler) { @@ -490,7 +488,7 @@ public record SyncPromptSpecification(McpSchema.Prompt prompt, * @param completionHandler The synchronous function that processes completion * requests and returns results. The first argument is an * {@link McpSyncServerExchange} used to interact with the client. The second argument - * is a {@link io.modelcontextprotocol.spec.McpSchema.CompleteRequest}. + * is a {@link McpSchema.CompleteRequest}. */ public record SyncCompletionSpecification(McpSchema.CompleteReference referenceKey, BiFunction completionHandler) { diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java similarity index 98% rename from mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java index 52360e54..2417e4c9 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java @@ -5,7 +5,6 @@ package io.modelcontextprotocol.server; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; /** diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java similarity index 97% rename from mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index afdbff47..1dda2d5f 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -15,10 +15,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.spec.ServerSessionFactory; import jakarta.servlet.AsyncContext; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; @@ -101,7 +102,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement private final AtomicBoolean isClosing = new AtomicBoolean(false); /** Session factory for creating new sessions */ - private McpServerSession.Factory sessionFactory; + private ServerSessionFactory sessionFactory; /** * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE @@ -149,7 +150,7 @@ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String m * @param sessionFactory The session factory to use */ @Override - public void setSessionFactory(McpServerSession.Factory sessionFactory) { + public void setSessionFactory(ServerSessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } @@ -176,6 +177,11 @@ public Mono notifyClients(String method, Object params) { .then(); } + @Override + public void close() { + this.closeGracefully().subscribe(); + } + /** * Handles GET requests to establish SSE connections. *

@@ -219,7 +225,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) writer); // Create a new session using the session factory - McpServerSession session = sessionFactory.create(sessionTransport); + McpServerSession session = (McpServerSession) sessionFactory.create(sessionTransport); this.sessions.put(sessionId, session); // Send initial endpoint event diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java similarity index 96% rename from mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java index 819da977..adc234fb 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java @@ -9,9 +9,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.Reader; import java.nio.charset.StandardCharsets; -import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -21,12 +19,14 @@ import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; -import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import io.modelcontextprotocol.spec.ServerSessionFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -91,10 +91,10 @@ public StdioServerTransportProvider(ObjectMapper objectMapper, InputStream input } @Override - public void setSessionFactory(McpServerSession.Factory sessionFactory) { + public void setSessionFactory(ServerSessionFactory sessionFactory) { // Create a single session for the stdio connection var transport = new StdioMcpSessionTransport(); - this.session = sessionFactory.create(transport); + this.session = (McpServerSession) sessionFactory.create(transport); transport.initProcessing(); } @@ -107,6 +107,11 @@ public Mono notifyClients(String method, Object params) { .doOnError(e -> logger.error("Failed to send notification: {}", e.getMessage())); } + @Override + public void close() { + this.closeGracefully().subscribe(); + } + @Override public Mono closeGracefully() { if (this.session == null) { diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpClientSession.java similarity index 91% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpClientSession.java index f577b493..10f68860 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpClientSession.java @@ -2,7 +2,7 @@ * Copyright 2024-2024 the original author or authors. */ -package io.modelcontextprotocol.spec; +package io.modelcontextprotocol.session; import java.time.Duration; import java.util.Map; @@ -11,7 +11,13 @@ import java.util.concurrent.atomic.AtomicLong; import com.fasterxml.jackson.core.type.TypeReference; + +import io.modelcontextprotocol.spec.McpClientTransport; +import io.modelcontextprotocol.spec.McpError; +import io.modelcontextprotocol.spec.McpSession; +import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Disposable; @@ -122,7 +128,7 @@ 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(this::handle)).subscribe(); + this.connection = Mono.from(this.transport.connect(mono -> Mono.from(mono).doOnNext(this::handle))).subscribe(); } private void handle(McpSchema.JSONRPCMessage message) { @@ -142,8 +148,8 @@ else if (message instanceof McpSchema.JSONRPCRequest request) { 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(); + return Mono.from(this.transport.sendMessage(errorResponse)).then(Mono.empty()); + }).flatMap(it -> Mono.from(this.transport.sendMessage(it))).subscribe(); } else if (message instanceof McpSchema.JSONRPCNotification notification) { logger.debug("Received notification: {}", notification); @@ -224,7 +230,7 @@ private String generateRequestId() { * @param method The method name to call * @param requestParams The request parameters * @param typeRef Type reference for response deserialization - * @return A Mono containing the response + * @return A Publisher containing the response */ @Override public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { @@ -234,8 +240,8 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc this.pendingResponses.put(requestId, sink); McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method, requestId, requestParams); - this.transport.sendMessage(jsonrpcRequest) - .contextWrite(ctx) + Mono.from(this.transport.sendMessage(jsonrpcRequest)) + .contextWrite(ctx) // TODO: It's most efficient to create a dedicated Subscriber here .subscribe(v -> { }, error -> { @@ -262,25 +268,23 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc * Sends a JSON-RPC notification. * @param method The method name for the notification * @param params The notification parameters - * @return A Mono that completes when the notification is sent + * @return A Publisher that completes when the notification is sent */ @Override public Mono sendNotification(String method, Object params) { McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, method, params); - return this.transport.sendMessage(jsonrpcNotification); + return Mono.from(this.transport.sendMessage(jsonrpcNotification)); } /** * Closes the session gracefully, allowing pending operations to complete. - * @return A Mono that completes when the session is closed + * @return A Publisher that completes when the session is closed */ @Override public Mono closeGracefully() { - return Mono.defer(() -> { - this.connection.dispose(); - return transport.closeGracefully(); - }); + this.connection.dispose(); + return Mono.from(transport.closeGracefully()); } /** diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpServerSession.java similarity index 92% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpServerSession.java index 86906d85..ab4d26bc 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpServerSession.java @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.spec; +package io.modelcontextprotocol.session; import java.time.Duration; import java.util.Map; @@ -9,8 +9,16 @@ import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.server.McpAsyncServerExchange; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import io.modelcontextprotocol.spec.McpServerTransportProvider; +import io.modelcontextprotocol.spec.McpError; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerTransport; +import io.modelcontextprotocol.spec.McpSession; +import io.modelcontextprotocol.spec.ServerSessionFactory; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; import reactor.core.publisher.Sinks; @@ -60,9 +68,8 @@ public class McpServerSession implements McpSession { * Creates a new server session with the given parameters and the transport to use. * @param id session id * @param transport the transport to use - * @param initHandler called when a - * {@link io.modelcontextprotocol.spec.McpSchema.InitializeRequest} is received by the - * server + * @param initHandler called when a {@link McpSchema.InitializeRequest} is received by + * the server * @param initNotificationHandler called when a * {@link io.modelcontextprotocol.spec.McpSchema#METHOD_NOTIFICATION_INITIALIZED} is * received. @@ -116,7 +123,8 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc this.pendingResponses.put(requestId, sink); McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method, requestId, requestParams); - this.transport.sendMessage(jsonrpcRequest).subscribe(v -> { + + Mono.from(this.transport.sendMessage(jsonrpcRequest)).subscribe(v -> { }, error -> { this.pendingResponses.remove(requestId); sink.error(error); @@ -140,7 +148,7 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc public Mono sendNotification(String method, Object params) { McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, method, params); - return this.transport.sendMessage(jsonrpcNotification); + return Mono.from(this.transport.sendMessage(jsonrpcNotification)); } /** @@ -149,9 +157,9 @@ public Mono sendNotification(String method, Object params) { * specified by the MCP server implementation * ({@link io.modelcontextprotocol.server.McpAsyncServer} or * {@link io.modelcontextprotocol.server.McpSyncServer}) via - * {@link McpServerSession.Factory} that the server creates. + * {@link ServerSessionFactory} that the server creates. * @param message the incoming JSON-RPC message - * @return a Mono that completes when the message is processed + * @return a Publisher that completes when the message is processed */ public Mono handle(McpSchema.JSONRPCMessage message) { return Mono.defer(() -> { @@ -175,8 +183,8 @@ else if (message instanceof McpSchema.JSONRPCRequest request) { new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, error.getMessage(), null)); // TODO: Should the error go to SSE or back as POST return? - return this.transport.sendMessage(errorResponse).then(Mono.empty()); - }).flatMap(this.transport::sendMessage); + return Mono.from(this.transport.sendMessage(errorResponse)).then(Mono.empty()); + }).flatMap(it -> Mono.from(this.transport.sendMessage(it))); } else if (message instanceof McpSchema.JSONRPCNotification notification) { // TODO handle errors for communication to without initialization @@ -264,7 +272,7 @@ private MethodNotFoundError getMethodNotFoundError(String method) { @Override public Mono closeGracefully() { - return this.transport.closeGracefully(); + return Mono.from(this.transport.closeGracefully()); } @Override @@ -334,20 +342,4 @@ public interface RequestHandler { } - /** - * Factory for creating server sessions which delegate to a provided 1:1 transport - * with a connected client. - */ - @FunctionalInterface - public interface Factory { - - /** - * Creates a new 1:1 representation of the client-server interaction. - * @param sessionTransport the transport to use for communication with the client. - * @return a new server session. - */ - McpServerSession create(McpServerTransport sessionTransport); - - } - } diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/Utils.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/util/Utils.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/util/Utils.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/util/Utils.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java similarity index 91% rename from mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java index 482d0aac..cc6ec7b4 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java @@ -9,6 +9,8 @@ import java.util.function.BiConsumer; import java.util.function.Function; +import org.reactivestreams.Publisher; + import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpClientTransport; @@ -66,7 +68,8 @@ public McpSchema.JSONRPCMessage getLastSentMessage() { private volatile boolean connected = false; @Override - public Mono connect(Function, Mono> handler) { + public Mono connect( + Function, Publisher> handler) { if (connected) { return Mono.error(new IllegalStateException("Already connected")); } @@ -77,6 +80,11 @@ public Mono connect(Function, Mono closeGracefully() { return Mono.defer(() -> { diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java similarity index 96% rename from mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java index 4be680e1..12975ccc 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java @@ -53,6 +53,11 @@ public McpSchema.JSONRPCMessage getLastSentMessage() { return !sent.isEmpty() ? sent.get(sent.size() - 1) : null; } + @Override + public void close() { + this.closeGracefully().subscribe(); + } + @Override public Mono closeGracefully() { return Mono.empty(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java similarity index 82% rename from mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java index 20a8c0cf..d65a8eb3 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java @@ -15,11 +15,9 @@ */ package io.modelcontextprotocol; -import java.util.Map; - import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpServerSession; -import io.modelcontextprotocol.spec.McpServerSession.Factory; +import io.modelcontextprotocol.session.McpServerSession; +import io.modelcontextprotocol.spec.ServerSessionFactory; import io.modelcontextprotocol.spec.McpServerTransportProvider; import reactor.core.publisher.Mono; @@ -41,9 +39,9 @@ public MockMcpServerTransport getTransport() { } @Override - public void setSessionFactory(Factory sessionFactory) { + public void setSessionFactory(ServerSessionFactory sessionFactory) { - session = sessionFactory.create(transport); + session = (McpServerSession) sessionFactory.create(transport); } @Override @@ -51,6 +49,11 @@ public Mono notifyClients(String method, Object params) { return session.sendNotification(method, params); } + @Override + public void close() { + this.closeGracefully().subscribe(); + } + @Override public Mono closeGracefully() { return session.closeGracefully(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java similarity index 99% rename from mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 72b409af..9f334a78 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -49,7 +49,7 @@ // KEEP IN SYNC with the class in mcp-test module public abstract class AbstractMcpAsyncClientTests { - private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!"; + private static final String ECHO_TEST_MESSAGE = "Hello MCP Reactor AI!"; abstract protected McpClientTransport createMcpTransport(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java similarity index 99% rename from mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index 24c161eb..de456390 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -48,7 +48,7 @@ // KEEP IN SYNC with the class in mcp-test module public abstract class AbstractMcpSyncClientTests { - private static final String TEST_MESSAGE = "Hello MCP Spring AI!"; + private static final String TEST_MESSAGE = "Hello MCP Reactor AI!"; abstract protected McpClientTransport createMcpTransport(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/HttpSseMcpAsyncClientTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java similarity index 90% rename from mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java index 762264de..1d8b91d2 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java @@ -30,8 +30,6 @@ import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; -import org.springframework.http.codec.ServerSentEvent; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.ArgumentMatchers.any; @@ -44,6 +42,7 @@ * Tests for the {@link HttpClientSseClientTransport} class. * * @author Christian Tzolov + * @author Aliaksei Darafeyeu */ @Timeout(15) class HttpClientSseClientTransportTests { @@ -63,7 +62,7 @@ static class TestHttpClientSseClientTransport extends HttpClientSseClientTranspo private final AtomicInteger inboundMessageCount = new AtomicInteger(0); - private Sinks.Many> events = Sinks.many().unicast().onBackpressureBuffer(); + private final Sinks.Many events = Sinks.many().unicast().onBackpressureBuffer(); public TestHttpClientSseClientTransport(final String baseUri) { super(HttpClient.newHttpClient(), HttpRequest.newBuilder(), baseUri, "/sse", new ObjectMapper()); @@ -74,12 +73,12 @@ public int getInboundMessageCount() { } public void simulateEndpointEvent(String jsonMessage) { - events.tryEmitNext(ServerSentEvent.builder().event("endpoint").data(jsonMessage).build()); + events.tryEmitNext(new ServerSentEvent(null, "endpoint", jsonMessage)); inboundMessageCount.incrementAndGet(); } public void simulateMessageEvent(String jsonMessage) { - events.tryEmitNext(ServerSentEvent.builder().event("message").data(jsonMessage).build()); + events.tryEmitNext(new ServerSentEvent(null, "message", jsonMessage)); inboundMessageCount.incrementAndGet(); } @@ -391,4 +390,55 @@ void testResolvingClientEndpoint() { transport.closeGracefully().block(); } + static class ServerSentEvent { + + private final String id; + + private final String event; + + private final String data; + + public ServerSentEvent(String id, String event, String data) { + this.id = id; + this.event = event; + this.data = data; + } + + // Getters + public String id() { + return id; + } + + public String event() { + return event; + } + + public String data() { + return data; + } + + @Override + public String toString() { + return "id: " + id + "\nevent: " + event + "\ndata: " + data + "\n\n"; + } + + public static ServerSentEvent parse(String rawSSE) { + String[] lines = rawSSE.split("\n"); + String id = null, event = null, data = null; + for (String line : lines) { + if (line.startsWith("id:")) { + id = line.substring(3).trim(); + } + else if (line.startsWith("event:")) { + event = line.substring(6).trim(); + } + else if (line.startsWith("data:")) { + data = line.substring(5).trim(); + } + } + return new ServerSentEvent(id, event, data); + } + + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java similarity index 99% rename from mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index df0b0c72..c2a239fc 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp-reactor/src/test/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 McpServerTransportProvider} implementations. * * @author Christian Tzolov */ diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java similarity index 99% rename from mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 0b38da85..803ee3d8 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-reactor/src/test/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 McpServerTransportProvider} implementations. * * @author Christian Tzolov */ diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java similarity index 97% rename from mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java index 0381a43b..ec6d9252 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Timeout; /** - * Tests for {@link McpAsyncServer} using {@link StdioServerTransport}. + * Tests for {@link McpAsyncServer} using {@link StdioServerTransportProvider}. * * @author Christian Tzolov */ diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java similarity index 93% rename from mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index 2ff6325a..6ed64e33 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -3,6 +3,10 @@ */ package io.modelcontextprotocol.server.transport; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -40,8 +44,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import org.springframework.web.client.RestClient; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.awaitility.Awaitility.await; @@ -516,12 +518,21 @@ void testToolCallSuccess() { McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); + try { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest rq = HttpRequest.newBuilder() + .uri(URI.create( + "https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")) + .header("Content-Type", "application/json") + .GET() + .build(); + String response = client.send(rq, HttpResponse.BodyHandlers.ofString()).body(); + assertThat(response).isNotBlank(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + return callResponse; }); @@ -552,12 +563,20 @@ void testToolListChangeHandlingSuccess() { McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); + try { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest rq = HttpRequest.newBuilder() + .uri(URI.create( + "https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")) + .header("Content-Type", "application/json") + .GET() + .build(); + String response = client.send(rq, HttpResponse.BodyHandlers.ofString()).body(); + assertThat(response).isNotBlank(); + } + catch (Exception e) { + throw new RuntimeException(e); + } return callResponse; }); @@ -570,13 +589,21 @@ void testToolListChangeHandlingSuccess() { try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); + try { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest rq = HttpRequest.newBuilder() + .uri(URI.create( + "https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")) + .header("Content-Type", "application/json") + .GET() + .build(); + String response = client.send(rq, HttpResponse.BodyHandlers.ofString()).body(); + assertThat(response).isNotBlank(); + rootsRef.set(toolsUpdate); + } + catch (Exception e) { + throw new RuntimeException(e); + } }).build()) { InitializeResult initResult = mcpClient.initialize(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java similarity index 96% rename from mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java index 14987b5a..59337476 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java @@ -17,12 +17,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; + +import io.modelcontextprotocol.spec.ServerSessionFactory; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -52,7 +54,7 @@ class StdioServerTransportProviderTests { private ObjectMapper objectMapper; - private McpServerSession.Factory sessionFactory; + private ServerSessionFactory sessionFactory; private McpServerSession mockSession; @@ -68,7 +70,7 @@ void setUp() { // Create mocks for session factory and session mockSession = mock(McpServerSession.class); - sessionFactory = mock(McpServerSession.Factory.class); + sessionFactory = mock(ServerSessionFactory.class); // Configure mock behavior when(sessionFactory.create(any(McpServerTransport.class))).thenReturn(mockSession); @@ -110,7 +112,7 @@ void shouldHandleIncomingMessages() throws Exception { AtomicReference capturedMessage = new AtomicReference<>(); CountDownLatch messageLatch = new CountDownLatch(1); - McpServerSession.Factory realSessionFactory = transport -> { + ServerSessionFactory realSessionFactory = transport -> { McpServerSession session = mock(McpServerSession.class); when(session.handle(any())).thenAnswer(invocation -> { capturedMessage.set(invocation.getArgument(0)); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/TomcatTestUtil.java diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/session/McpClientSessionTests.java similarity index 98% rename from mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/session/McpClientSessionTests.java index f72be43e..4e3bb5ab 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/session/McpClientSessionTests.java @@ -2,7 +2,7 @@ * Copyright 2024-2024 the original author or authors. */ -package io.modelcontextprotocol.spec; +package io.modelcontextprotocol.session; import java.time.Duration; import java.util.Map; @@ -14,6 +14,9 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import io.modelcontextprotocol.spec.McpError; +import io.modelcontextprotocol.spec.McpSchema; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/UtilsTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/util/UtilsTests.java similarity index 100% rename from mcp/src/test/java/io/modelcontextprotocol/util/UtilsTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/util/UtilsTests.java diff --git a/mcp/src/test/resources/logback.xml b/mcp-reactor/src/test/resources/logback.xml similarity index 72% rename from mcp/src/test/resources/logback.xml rename to mcp-reactor/src/test/resources/logback.xml index 0246d6c7..95a5f59d 100644 --- a/mcp/src/test/resources/logback.xml +++ b/mcp-reactor/src/test/resources/logback.xml @@ -9,13 +9,13 @@ - + - + - + diff --git a/mcp-spi/pom.xml b/mcp-spi/pom.xml new file mode 100644 index 00000000..c5ab73f3 --- /dev/null +++ b/mcp-spi/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.10.0-SNAPSHOT + + + mcp-spi + jar + Java MCP SDK SPI + Java SDK SPI of the Model Context Protocol + https://github.com/modelcontextprotocol/java-sdk + + + 17 + 17 + UTF-8 + + + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + org.reactivestreams + reactive-streams + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.assertj + assertj-core + ${assert4j.version} + test + + + net.javacrumbs.json-unit + json-unit-assertj + ${json-unit-assertj.version} + test + + + + \ No newline at end of file diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java similarity index 52% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java index f2909124..c9c3ddae 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java @@ -1,20 +1,20 @@ /* -* Copyright 2024 - 2024 the original author or authors. +* Copyright 2025 - 2025 the original author or authors. */ package io.modelcontextprotocol.spec; import java.util.function.Function; - -import reactor.core.publisher.Mono; +import org.reactivestreams.Publisher; /** * Marker interface for the client-side MCP transport. * * @author Christian Tzolov * @author Dariusz Jędrzejczyk + * @author Aliksei Darafeyeu */ public interface McpClientTransport extends McpTransport { - Mono connect(Function, Mono> handler); + Publisher connect(Function, Publisher> handler); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpError.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpError.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpError.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpError.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSchema.java similarity index 99% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 8df8a158..ce4a4fc1 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -10,6 +10,9 @@ import java.util.List; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -18,9 +21,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + import io.modelcontextprotocol.util.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Based on the JSON-RPC 2.0 @@ -104,6 +106,10 @@ private McpSchema() { */ public static final class ErrorCodes { + ErrorCodes() { + // Prevent instantiation + } + /** * Invalid JSON was received by the server. */ @@ -1199,7 +1205,7 @@ public sealed interface CompleteReference permits PromptReference, ResourceRefer @JsonIgnoreProperties(ignoreUnknown = true) public record PromptReference(// @formatter:off @JsonProperty("type") String type, - @JsonProperty("name") String name) implements McpSchema.CompleteReference { + @JsonProperty("name") String name) implements CompleteReference { public PromptReference(String name) { this("ref/prompt", name); @@ -1215,7 +1221,7 @@ public String identifier() { @JsonIgnoreProperties(ignoreUnknown = true) public record ResourceReference(// @formatter:off @JsonProperty("type") String type, - @JsonProperty("uri") String uri) implements McpSchema.CompleteReference { + @JsonProperty("uri") String uri) implements CompleteReference { public ResourceReference(String uri) { this("ref/resource", uri); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransport.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpServerTransport.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransport.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpServerTransport.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java similarity index 66% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java index 5fdbd7ab..036ada1d 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProvider.java @@ -1,8 +1,6 @@ package io.modelcontextprotocol.spec; -import java.util.Map; - -import reactor.core.publisher.Mono; +import org.reactivestreams.Publisher; /** * The core building block providing the server-side MCP transport. Implement this @@ -15,17 +13,17 @@ * {@link io.modelcontextprotocol.server.McpServer#sync(McpServerTransportProvider)} or * {@link io.modelcontextprotocol.server.McpServer#async(McpServerTransportProvider)}. As * a result of the MCP server creation, the provider will be notified of a - * {@link McpServerSession.Factory} which will be used to handle a 1:1 communication - * between a newly connected client and the server. The provider's responsibility is to - * create instances of {@link McpServerTransport} that the session will utilise during the + * {@link ServerSessionFactory} which will be used to handle a 1:1 communication between a + * newly connected client and the server. The provider's responsibility is to create + * instances of {@link McpServerTransport} that the session will utilise during the * session lifetime. * *

* Finally, the {@link McpServerTransport}s can be closed in bulk when {@link #close()} or * {@link #closeGracefully()} are called as part of the normal application shutdown event. * Individual {@link McpServerTransport}s can also be closed on a per-session basis, where - * the {@link McpServerSession#close()} or {@link McpServerSession#closeGracefully()} - * closes the provided transport. + * the {@link McpSession#close()} or {@link McpSession#closeGracefully()} closes the + * provided transport. * * @author Dariusz Jędrzejczyk */ @@ -37,30 +35,29 @@ public interface McpServerTransportProvider { * take place. * @param sessionFactory the session factory to be used for initiating client sessions */ - void setSessionFactory(McpServerSession.Factory sessionFactory); + void setSessionFactory(ServerSessionFactory sessionFactory); /** * Sends a notification to all connected clients. * @param method the name of the notification method to be called on the clients * @param params parameters to be sent with the notification - * @return a Mono that completes when the notification has been broadcast - * @see McpSession#sendNotification(String, Map) + * @return a Publisher that completes when the notification has been broadcast + * @see McpSession#sendNotification(String, Object) */ - Mono notifyClients(String method, Object params); + Publisher notifyClients(String method, Object params); /** * Immediately closes all the transports with connected clients and releases any * associated resources. */ - default void close() { - this.closeGracefully().subscribe(); - } + void close(); /** * Gracefully closes all the transports with connected clients and releases any * associated resources asynchronously. - * @return a {@link Mono} that completes when the connections have been closed. + * @return a {@link Publisher} that completes when the connections have been + * closed. */ - Mono closeGracefully(); + Publisher closeGracefully(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSession.java similarity index 84% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSession.java index 473a860c..060e5091 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSession.java @@ -4,10 +4,9 @@ package io.modelcontextprotocol.spec; -import java.util.Map; +import org.reactivestreams.Publisher; import com.fasterxml.jackson.core.type.TypeReference; -import reactor.core.publisher.Mono; /** * Represents a Model Control Protocol (MCP) session that handles communication between @@ -15,13 +14,14 @@ * notifications, as well as managing the session lifecycle. * *

- * The session operates asynchronously using Project Reactor's {@link Mono} type for + * The session operates asynchronously using Project Reactor's {@link Publisher} type for * non-blocking operations. It supports both request-response patterns and one-way * notifications. *

* * @author Christian Tzolov * @author Dariusz Jędrzejczyk + * @author Aliksei Darafeyeu */ public interface McpSession { @@ -39,7 +39,7 @@ public interface McpSession { * @param typeRef the TypeReference describing the expected response type * @return a Mono that will emit the response when received */ - Mono sendRequest(String method, Object requestParams, TypeReference typeRef); + Publisher sendRequest(String method, Object requestParams, TypeReference typeRef); /** * Sends a notification to the model client or server without parameters. @@ -51,7 +51,7 @@ public interface McpSession { * @param method the name of the notification method to be called on the server * @return a Mono that completes when the notification has been sent */ - default Mono sendNotification(String method) { + default Publisher sendNotification(String method) { return sendNotification(method, null); } @@ -66,13 +66,13 @@ default Mono sendNotification(String method) { * @param params parameters to be sent with the notification * @return a Mono that completes when the notification has been sent */ - Mono sendNotification(String method, Object params); + Publisher sendNotification(String method, Object params); /** * Closes the session and releases any associated resources asynchronously. - * @return a {@link Mono} that completes when the session has been closed. + * @return a {@link Publisher} that completes when the session has been closed. */ - Mono closeGracefully(); + Publisher closeGracefully(); /** * Closes the session and releases any associated resources. diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpTransport.java similarity index 78% rename from mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpTransport.java index 40d9ba7a..5e838d5f 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpTransport.java @@ -1,12 +1,12 @@ /* - * Copyright 2024-2024 the original author or authors. + * Copyright 2025-2025 the original author or authors. */ package io.modelcontextprotocol.spec; +import org.reactivestreams.Publisher; + import com.fasterxml.jackson.core.type.TypeReference; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; -import reactor.core.publisher.Mono; /** * Defines the asynchronous transport layer for the Model Context Protocol (MCP). @@ -34,6 +34,7 @@ * * @author Christian Tzolov * @author Dariusz Jędrzejczyk + * @author Aliksei Darafeyeu */ public interface McpTransport { @@ -45,16 +46,15 @@ public interface McpTransport { * needed. It should handle the graceful shutdown of any active connections. *

*/ - default void close() { - this.closeGracefully().subscribe(); - } + void close(); /** * Closes the transport connection and releases any associated resources * asynchronously. - * @return a {@link Mono} that completes when the connection has been closed. + * @return a {@link Publisher} that completes when the connection has been + * closed. */ - Mono closeGracefully(); + Publisher closeGracefully(); /** * Sends a message to the peer asynchronously. @@ -63,10 +63,10 @@ default void close() { * This method handles the transmission of messages to the server in an asynchronous * manner. Messages are sent in JSON-RPC format as specified by the MCP protocol. *

- * @param message the {@link JSONRPCMessage} to be sent to the server - * @return a {@link Mono} that completes when the message has been sent + * @param message the {@link McpSchema.JSONRPCMessage} to be sent to the server + * @return a {@link Publisher} that completes when the message has been sent */ - Mono sendMessage(JSONRPCMessage message); + Publisher sendMessage(McpSchema.JSONRPCMessage message); /** * Unmarshals the given data into an object of the specified type. diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/ServerSessionFactory.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/ServerSessionFactory.java new file mode 100644 index 00000000..eb042f61 --- /dev/null +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/ServerSessionFactory.java @@ -0,0 +1,21 @@ +/* + * Copyright 2025-2025 the original author or authors. + */ + +package io.modelcontextprotocol.spec; + +/** + * Factory for creating server sessions which delegate to a provided 1:1 transport with a + * connected client. + */ +@FunctionalInterface +public interface ServerSessionFactory { + + /** + * Creates a new 1:1 representation of the client-server interaction. + * @param sessionTransport the transport to use for communication with the client. + * @return a new server session. + */ + McpSession create(McpServerTransport sessionTransport); + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/Assert.java b/mcp-spi/src/main/java/io/modelcontextprotocol/util/Assert.java similarity index 87% rename from mcp/src/main/java/io/modelcontextprotocol/util/Assert.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/util/Assert.java index d68188c6..9bf20476 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/util/Assert.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/util/Assert.java @@ -6,8 +6,6 @@ import java.util.Collection; -import reactor.util.annotation.Nullable; - /** * Assertion utility class that assists in validating arguments. * @@ -19,13 +17,17 @@ */ public final class Assert { + Assert() { + // Prevent instantiation + } + /** * Assert that the collection is not {@code null} and not empty. * @param collection the collection to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the collection is {@code null} or empty */ - public static void notEmpty(@Nullable Collection collection, String message) { + public static void notEmpty(Collection collection, String message) { if (collection == null || collection.isEmpty()) { throw new IllegalArgumentException(message); } @@ -41,7 +43,7 @@ public static void notEmpty(@Nullable Collection collection, String message) * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is {@code null} */ - public static void notNull(@Nullable Object object, String message) { + public static void notNull(Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); } @@ -55,7 +57,7 @@ public static void notNull(@Nullable Object object, String message) { * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the text does not contain valid text content */ - public static void hasText(@Nullable String text, String message) { + public static void hasText(String text, String message) { if (!hasText(text)) { throw new IllegalArgumentException(message); } @@ -72,7 +74,7 @@ public static void hasText(@Nullable String text, String message) { * greater than 0, and it does not contain whitespace only * @see Character#isWhitespace */ - public static boolean hasText(@Nullable String str) { + public static boolean hasText(String str) { return (str != null && !str.isBlank()); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/AssertTests.java b/mcp-spi/src/test/java/io/modelcontextprotocol/spec/AssertTests.java similarity index 93% rename from mcp/src/test/java/io/modelcontextprotocol/util/AssertTests.java rename to mcp-spi/src/test/java/io/modelcontextprotocol/spec/AssertTests.java index 08555fef..e55394d7 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/util/AssertTests.java +++ b/mcp-spi/src/test/java/io/modelcontextprotocol/spec/AssertTests.java @@ -2,7 +2,7 @@ * Copyright 2024-2024 the original author or authors. */ -package io.modelcontextprotocol.util; +package io.modelcontextprotocol.spec; import org.junit.jupiter.api.Test; @@ -10,6 +10,8 @@ import static org.junit.jupiter.api.Assertions.*; +import io.modelcontextprotocol.util.Assert; + class AssertTests { @Test diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-spi/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java similarity index 97% rename from mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java rename to mcp-spi/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index ff78c1bf..d5b1ab68 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-spi/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -3,28 +3,28 @@ */ package io.modelcontextprotocol.spec; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; + import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Test; + 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; import net.javacrumbs.jsonunit.core.Option; -import org.junit.jupiter.api.Test; - -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Christian Tzolov */ -public class McpSchemaTests { +class McpSchemaTests { ObjectMapper mapper = new ObjectMapper(); @@ -53,7 +53,7 @@ void testTextContentDeserialization() throws Exception { } @Test - void testContentDeserializationWrongType() throws Exception { + void testContentDeserializationWrongType() { assertThatThrownBy(() -> mapper.readValue(""" {"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class)) @@ -86,8 +86,8 @@ void testImageContentDeserialization() throws Exception { @Test void testEmbeddedResource() throws Exception { - McpSchema.TextResourceContents resourceContents = new McpSchema.TextResourceContents("resource://test", - "text/plain", "Sample resource content"); + TextResourceContents resourceContents = new TextResourceContents("resource://test", "text/plain", + "Sample resource content"); McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); @@ -343,7 +343,7 @@ void testReadResourceRequest() throws Exception { @Test void testReadResourceResult() throws Exception { - McpSchema.TextResourceContents contents1 = new McpSchema.TextResourceContents("resource://test1", "text/plain", + TextResourceContents contents1 = new TextResourceContents("resource://test1", "text/plain", "Sample text content"); McpSchema.BlobResourceContents contents2 = new McpSchema.BlobResourceContents("resource://test2", diff --git a/mcp-spring/mcp-spring-webflux/README.md b/mcp-spring/mcp-spring-webflux/README.md index e701e41e..8504fb68 100644 --- a/mcp-spring/mcp-spring-webflux/README.md +++ b/mcp-spring/mcp-spring-webflux/README.md @@ -1,9 +1,10 @@ # WebFlux SSE Transport ```xml + - io.modelcontextprotocol.sdk - mcp-spring-webflux + io.modelcontextprotocol.specio.modelcontextprotocol.spec + mcp-spring-webflux ``` diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml index 63c32a8a..34fb4c46 100644 --- a/mcp-spring/mcp-spring-webflux/pom.xml +++ b/mcp-spring/mcp-spring-webflux/pom.xml @@ -24,14 +24,19 @@ io.modelcontextprotocol.sdk - mcp - 0.10.0-SNAPSHOT + mcp-spi + ${project.version} + + + io.modelcontextprotocol.sdk + mcp-reactor + ${project.version} io.modelcontextprotocol.sdk mcp-test - 0.10.0-SNAPSHOT + ${project.version} test diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java index 37abe295..a78b18b8 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java @@ -14,6 +14,8 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; import io.modelcontextprotocol.util.Assert; + +import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Disposable; @@ -189,7 +191,7 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMappe * event type is received */ @Override - public Mono connect(Function, Mono> handler) { + public Mono connect(Function, Publisher> handler) { Flux> events = eventStream(); this.inboundSubscription = events.concatMap(event -> Mono.just(event).handle((e, s) -> { if (ENDPOINT_EVENT_TYPE.equals(event.event())) { @@ -302,6 +304,11 @@ protected Flux> eventStream() {// @formatter:off sink.error(retrySpec.failure()); }; + @Override + public void close() { + this.closeGracefully().subscribe(); + } + /** * Implements graceful shutdown of the transport. Cleans up all resources including * subscriptions and schedulers. Ensures orderly shutdown of both inbound and outbound diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java index 62264d9a..17e42d0f 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java @@ -1,19 +1,20 @@ package io.modelcontextprotocol.server.transport; import java.io.IOException; -import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import io.modelcontextprotocol.spec.ServerSessionFactory; import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -98,7 +99,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv private final RouterFunction routerFunction; - private McpServerSession.Factory sessionFactory; + private ServerSessionFactory sessionFactory; /** * Map of active client sessions, keyed by session ID. @@ -165,7 +166,7 @@ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseU } @Override - public void setSessionFactory(McpServerSession.Factory sessionFactory) { + public void setSessionFactory(ServerSessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } @@ -204,6 +205,11 @@ public Mono notifyClients(String method, Object params) { .then(); } + @Override + public void close() { + this.closeGracefully().subscribe(); + } + // FIXME: This javadoc makes claims about using isClosing flag but it's not // actually // doing that. @@ -261,7 +267,7 @@ private Mono handleSseConnection(ServerRequest request) { .body(Flux.>create(sink -> { WebFluxMcpSessionTransport sessionTransport = new WebFluxMcpSessionTransport(sink); - McpServerSession session = sessionFactory.create(sessionTransport); + McpServerSession session = (McpServerSession) sessionFactory.create(sessionTransport); String sessionId = session.getId(); logger.debug("Created new SSE connection for session: {}", sessionId); diff --git a/mcp-spring/mcp-spring-webmvc/README.md b/mcp-spring/mcp-spring-webmvc/README.md index 9adf5b2e..c3af00b9 100644 --- a/mcp-spring/mcp-spring-webmvc/README.md +++ b/mcp-spring/mcp-spring-webmvc/README.md @@ -1,9 +1,10 @@ # WebMVC SSE Server Transport ```xml + - io.modelcontextprotocol.sdk - mcp-spring-webmvc + io.modelcontextprotocol.specio.modelcontextprotocol.spec + mcp-spring-webmvc ``` diff --git a/mcp-spring/mcp-spring-webmvc/pom.xml b/mcp-spring/mcp-spring-webmvc/pom.xml index b59be6a0..c025e3bf 100644 --- a/mcp-spring/mcp-spring-webmvc/pom.xml +++ b/mcp-spring/mcp-spring-webmvc/pom.xml @@ -24,14 +24,19 @@ io.modelcontextprotocol.sdk - mcp - 0.10.0-SNAPSHOT + mcp-spi + ${project.version} + + + io.modelcontextprotocol.sdk + mcp-reactor + ${project.version} io.modelcontextprotocol.sdk mcp-test - 0.10.0-SNAPSHOT + ${project.version} test diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java index fc86cfaa..7eb06717 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java @@ -6,7 +6,7 @@ import java.io.IOException; import java.time.Duration; -import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -16,10 +16,12 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; -import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import io.modelcontextprotocol.spec.ServerSessionFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -95,7 +97,7 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi private final RouterFunction routerFunction; - private McpServerSession.Factory sessionFactory; + private ServerSessionFactory sessionFactory; /** * Map of active client sessions, keyed by session ID. @@ -165,7 +167,7 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUr } @Override - public void setSessionFactory(McpServerSession.Factory sessionFactory) { + public void setSessionFactory(ServerSessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } @@ -195,6 +197,11 @@ public Mono notifyClients(String method, Object params) { .then(); } + @Override + public void close() { + this.closeGracefully().subscribe(); + } + /** * Initiates a graceful shutdown of the transport. This method: *
    @@ -263,7 +270,7 @@ private ServerResponse handleSseConnection(ServerRequest request) { }); WebMvcMcpSessionTransport sessionTransport = new WebMvcMcpSessionTransport(sessionId, sseBuilder); - McpServerSession session = sessionFactory.create(sessionTransport); + McpServerSession session = (McpServerSession) sessionFactory.create(sessionTransport); this.sessions.put(sessionId, session); try { @@ -300,15 +307,15 @@ private ServerResponse handleMessage(ServerRequest request) { return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down"); } - if (request.param("sessionId").isEmpty()) { + Optional sessionId = request.param("sessionId"); + if (sessionId.isEmpty()) { return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint")); } - String sessionId = request.param("sessionId").get(); - McpServerSession session = sessions.get(sessionId); - + McpServerSession session = sessions.get(sessionId.get()); if (session == null) { - return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId)); + return ServerResponse.status(HttpStatus.NOT_FOUND) + .body(new McpError("Session not found: " + sessionId.get())); } try { diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java index 6a6ad17e..784cecc9 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java @@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import reactor.core.publisher.Mono; + import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; @@ -97,7 +99,7 @@ protected void onStart() { @Override protected void onClose() { if (transportProvider != null) { - transportProvider.closeGracefully().block(); + Mono.from(transportProvider.closeGracefully()).block(); } if (appContext != null) { appContext.close(); diff --git a/mcp-spring/mcp-spring-webmvc/src/test/resources/logback.xml b/mcp-spring/mcp-spring-webmvc/src/test/resources/logback.xml index bc1140bb..d286cee8 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/resources/logback.xml +++ b/mcp-spring/mcp-spring-webmvc/src/test/resources/logback.xml @@ -18,7 +18,7 @@ - + diff --git a/mcp-test/pom.xml b/mcp-test/pom.xml index f1484ae7..0c494a42 100644 --- a/mcp-test/pom.xml +++ b/mcp-test/pom.xml @@ -23,10 +23,14 @@ io.modelcontextprotocol.sdk - mcp - 0.10.0-SNAPSHOT + mcp-spi + ${project.version} + + + io.modelcontextprotocol.sdk + mcp-reactor + ${project.version} - org.slf4j slf4j-api diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java index 5484a63c..d881e1ed 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java @@ -9,6 +9,8 @@ import java.util.function.BiConsumer; import java.util.function.Function; +import org.reactivestreams.Publisher; + import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpClientTransport; @@ -71,7 +73,8 @@ public McpSchema.JSONRPCMessage getLastSentMessage() { private volatile boolean connected = false; @Override - public Mono connect(Function, Mono> handler) { + public Mono connect( + Function, Publisher> handler) { if (connected) { return Mono.error(new IllegalStateException("Already connected")); } @@ -82,6 +85,11 @@ public Mono connect(Function, Mono closeGracefully() { return Mono.defer(() -> { diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/BaseMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/BaseMcpAsyncServerTests.java deleted file mode 100644 index 208bcb71..00000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/BaseMcpAsyncServerTests.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.modelcontextprotocol.server; - -public abstract class BaseMcpAsyncServerTests { - -} diff --git a/pom.xml b/pom.xml index 9be256cc..6197f40a 100644 --- a/pom.xml +++ b/pom.xml @@ -99,7 +99,8 @@ mcp-bom - mcp + mcp-spi + mcp-reactor mcp-spring/mcp-spring-webflux mcp-spring/mcp-spring-webmvc mcp-test From cd74509644e05cd40bf33af01f04d31731be912c Mon Sep 17 00:00:00 2001 From: "a.darafeyeu" Date: Wed, 23 Apr 2025 13:30:34 +0200 Subject: [PATCH 2/2] feat: adds mcp-schema-jackson --- mcp-bom/pom.xml | 9 +- mcp-reactor/README.md | 2 +- mcp-reactor/pom.xml | 89 +- .../client/McpAsyncClient.java | 79 +- .../client/McpClient.java | 12 +- .../client/McpClientFeatures.java | 2 +- .../client/McpSyncClient.java | 10 +- .../HttpClientSseClientTransport.java | 67 +- .../transport/StdioClientTransport.java | 31 +- .../server/McpAsyncServer.java | 50 +- .../server/McpAsyncServerExchange.java | 16 +- .../server/McpServer.java | 6 +- .../server/McpServerFeatures.java | 2 +- .../server/McpSyncServer.java | 4 +- .../server/McpSyncServerExchange.java | 4 +- ...HttpServletSseServerTransportProvider.java | 26 +- .../StdioServerTransportProvider.java | 36 +- .../session/McpClientSession.java | 11 +- .../session/McpServerSession.java | 11 +- .../DeafaultMcpUriTemplateManagerFactory.java | 0 .../util/DefaultMcpUriTemplateManager.java | 0 .../util/McpUriTemplateManager.java | 0 .../util/McpUriTemplateManagerFactory.java | 0 .../MockMcpClientTransport.java | 16 +- .../MockMcpServerTransport.java | 16 +- .../MockMcpServerTransportProvider.java | 2 +- .../client/AbstractMcpAsyncClientTests.java | 24 +- .../client/AbstractMcpSyncClientTests.java | 28 +- .../McpAsyncClientResponseHandlerTests.java | 27 +- .../client/McpClientProtocolVersionTests.java | 4 +- .../HttpClientSseClientTransportTests.java | 7 +- .../server/AbstractMcpAsyncServerTests.java | 45 +- .../server/AbstractMcpSyncServerTests.java | 39 +- .../server/McpServerProtocolVersionTests.java | 2 +- ...ervletSseServerCustomContextPathTests.java | 2 +- ...rverTransportProviderIntegrationTests.java | 83 +- .../StdioServerTransportProviderTests.java | 2 +- .../session/McpClientSessionTests.java | 7 +- .../McpUriTemplateManagerTests.java | 5 +- mcp-schema-jackson/pom.xml | 68 ++ .../schema/GenericEnumDeserializer.java | 34 + .../schema/GenericEnumModule.java | 26 + .../schema/GenericEnumSerializer.java | 24 + .../schema/McpJacksonCodec.java | 110 +++ .../schema/McpJacksonSchema.java | 625 ++++++++++++++ .../schema}/McpSchemaTests.java | 151 ++-- mcp-spi/pom.xml | 35 +- .../{spec => schema}/McpSchema.java | 783 ++++++------------ .../schema/McpSchemaCodec.java | 28 + .../modelcontextprotocol/schema/McpType.java | 41 + .../spec/McpClientTransport.java | 2 + .../modelcontextprotocol/spec/McpError.java | 2 +- .../modelcontextprotocol/spec/McpSession.java | 6 +- .../spec/McpTransport.java | 5 +- .../transport/WebFluxSseClientTransport.java | 56 +- .../WebFluxSseServerTransportProvider.java | 56 +- .../WebFluxSseIntegrationTests.java | 61 +- .../WebFluxSseClientTransportTests.java | 8 +- .../WebMvcSseServerTransportProvider.java | 38 +- .../WebMvcSseCustomContextPathTests.java | 2 +- .../server/WebMvcSseIntegrationTests.java | 74 +- .../MockMcpTransport.java | 16 +- .../client/AbstractMcpAsyncClientTests.java | 26 +- .../client/AbstractMcpSyncClientTests.java | 28 +- .../server/AbstractMcpAsyncServerTests.java | 47 +- .../server/AbstractMcpSyncServerTests.java | 42 +- pom.xml | 1 + 67 files changed, 2033 insertions(+), 1138 deletions(-) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java (100%) rename {mcp => mcp-reactor}/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java (100%) rename mcp-reactor/src/test/java/io/modelcontextprotocol/{ => util}/McpUriTemplateManagerTests.java (92%) create mode 100644 mcp-schema-jackson/pom.xml create mode 100644 mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumDeserializer.java create mode 100644 mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumModule.java create mode 100644 mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumSerializer.java create mode 100644 mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonCodec.java create mode 100644 mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonSchema.java rename {mcp-spi/src/test/java/io/modelcontextprotocol/spec => mcp-schema-jackson/src/test/java/io/modelcontextprotocol/schema}/McpSchemaTests.java (83%) rename mcp-spi/src/main/java/io/modelcontextprotocol/{spec => schema}/McpSchema.java (50%) create mode 100644 mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpSchemaCodec.java create mode 100644 mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpType.java diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml index f876bb7f..1812ea51 100644 --- a/mcp-bom/pom.xml +++ b/mcp-bom/pom.xml @@ -29,7 +29,14 @@ io.modelcontextprotocol.sdk - mcp-reactor + mcp-spi + ${project.version} + + + + + io.modelcontextprotocol.sdk + mcp-schema-jackson ${project.version} diff --git a/mcp-reactor/README.md b/mcp-reactor/README.md index 7a9ff851..72bb46b3 100644 --- a/mcp-reactor/README.md +++ b/mcp-reactor/README.md @@ -1,5 +1,5 @@ # Java MCP SDK -Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools. +Java SDK Reactor implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools. For comprehensive guides and API documentation, visit the [MCP Java SDK Reference Documentation](https://modelcontextprotocol.io/sdk/java/mcp-overview). diff --git a/mcp-reactor/pom.xml b/mcp-reactor/pom.xml index 539a0b58..a8a7e0c1 100644 --- a/mcp-reactor/pom.xml +++ b/mcp-reactor/pom.xml @@ -10,8 +10,8 @@ mcp-reactor jar - Java MCP Reactor SDK - Java Reactor SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools + Java SDK MCP Reactor + Java SDK Reactor implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools https://github.com/modelcontextprotocol/java-sdk @@ -65,42 +65,53 @@ + io.modelcontextprotocol.sdk mcp-spi ${project.version} - - org.slf4j - slf4j-api - ${slf4j-api.version} + io.modelcontextprotocol.sdk + mcp-schema-jackson + ${project.version} - + - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} + org.reactivestreams + reactive-streams - io.projectreactor reactor-core - + - io.projectreactor.netty - reactor-netty-http - test + jakarta.servlet + jakarta.servlet-api + ${jakarta.servlet.version} + provided - + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + org.assertj assertj-core ${assert4j.version} test + + org.testcontainers + testcontainers + ${testcontainers.version} + test + org.junit.jupiter junit-jupiter-api @@ -119,56 +130,17 @@ ${mockito.version} test - - - - net.bytebuddy - byte-buddy - ${byte-buddy.version} - test - io.projectreactor reactor-test test - - org.testcontainers - junit-jupiter - ${testcontainers.version} - test - - org.awaitility awaitility ${awaitility.version} test - - - ch.qos.logback - logback-classic - ${logback.version} - test - - - - net.javacrumbs.json-unit - json-unit-assertj - ${json-unit-assertj.version} - test - - - - - - jakarta.servlet - jakarta.servlet-api - ${jakarta.servlet.version} - provided - - org.apache.tomcat.embed @@ -176,13 +148,6 @@ ${tomcat.version} test - - org.apache.tomcat.embed - tomcat-embed-websocket - ${tomcat.version} - test - - diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 641bf0b7..a4049348 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -13,23 +13,23 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.session.McpClientSession; import io.modelcontextprotocol.session.McpClientSession.NotificationHandler; import io.modelcontextprotocol.session.McpClientSession.RequestHandler; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult; -import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; -import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; -import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest; -import io.modelcontextprotocol.spec.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageRequest; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.schema.McpSchema.GetPromptRequest; +import io.modelcontextprotocol.schema.McpSchema.GetPromptResult; +import io.modelcontextprotocol.schema.McpSchema.ListPromptsResult; +import io.modelcontextprotocol.schema.McpSchema.LoggingLevel; +import io.modelcontextprotocol.schema.McpSchema.LoggingMessageNotification; +import io.modelcontextprotocol.schema.McpSchema.PaginatedRequest; +import io.modelcontextprotocol.schema.McpSchema.Root; import io.modelcontextprotocol.spec.McpTransport; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Utils; @@ -80,8 +80,7 @@ public class McpAsyncClient { private static final Logger logger = LoggerFactory.getLogger(McpAsyncClient.class); - private static TypeReference VOID_TYPE_REFERENCE = new TypeReference<>() { - }; + private static final McpType VOID_TYPE_REFERENCE = McpType.of(Void.class); protected final Sinks.One initializedSink = Sinks.one(); @@ -337,8 +336,7 @@ public Mono initialize() { this.clientInfo); // @formatter:on Mono result = this.mcpSession.sendRequest(McpSchema.METHOD_INITIALIZE, - initializeRequest, new TypeReference() { - }); + initializeRequest, McpType.of(McpSchema.InitializeResult.class)); return result.flatMap(initializeResult -> { @@ -389,8 +387,7 @@ private Mono withInitializationCheck(String actionName, */ public Mono ping() { return this.withInitializationCheck("pinging the server", initializedResult -> this.mcpSession - .sendRequest(McpSchema.METHOD_PING, null, new TypeReference() { - })); + .sendRequest(McpSchema.METHOD_PING, null, McpType.of(Object.class))); } // -------------------------- @@ -479,8 +476,7 @@ private RequestHandler rootsListRequestHandler() { return params -> { @SuppressWarnings("unused") McpSchema.PaginatedRequest request = transport.unmarshalFrom(params, - new TypeReference() { - }); + McpType.of(McpSchema.PaginatedRequest.class)); List roots = this.roots.values().stream().toList(); @@ -494,8 +490,7 @@ private RequestHandler rootsListRequestHandler() { private RequestHandler samplingCreateMessageHandler() { return params -> { McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params, - new TypeReference() { - }); + McpType.of(McpSchema.CreateMessageRequest.class)); return this.samplingHandler.apply(request); }; @@ -504,11 +499,11 @@ private RequestHandler samplingCreateMessageHandler() { // -------------------------- // Tools // -------------------------- - private static final TypeReference CALL_TOOL_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType CALL_TOOL_RESULT_TYPE_REF = McpType + .of(McpSchema.CallToolResult.class); - private static final TypeReference LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType LIST_TOOLS_RESULT_TYPE_REF = McpType + .of(McpSchema.ListToolsResult.class); /** * Calls a tool provided by the server. Tools enable servers to expose executable @@ -570,14 +565,14 @@ private NotificationHandler asyncToolsChangeNotificationHandler( // Resources // -------------------------- - private static final TypeReference LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType LIST_RESOURCES_RESULT_TYPE_REF = McpType + .of(McpSchema.ListResourcesResult.class); - private static final TypeReference READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType READ_RESOURCE_RESULT_TYPE_REF = McpType + .of(McpSchema.ReadResourceResult.class); - private static final TypeReference LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = McpType + .of(McpSchema.ListResourceTemplatesResult.class); /** * Retrieves the list of all resources provided by the server. Resources represent any @@ -712,11 +707,11 @@ private NotificationHandler asyncResourcesChangeNotificationHandler( // -------------------------- // Prompts // -------------------------- - private static final TypeReference LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType LIST_PROMPTS_RESULT_TYPE_REF = McpType + .of(McpSchema.ListPromptsResult.class); - private static final TypeReference GET_PROMPT_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType GET_PROMPT_RESULT_TYPE_REF = McpType + .of(McpSchema.GetPromptResult.class); /** * Retrieves the list of all prompts provided by the server. @@ -781,8 +776,7 @@ private NotificationHandler asyncLoggingNotificationHandler( return params -> { McpSchema.LoggingMessageNotification loggingMessageNotification = transport.unmarshalFrom(params, - new TypeReference() { - }); + McpType.of(McpSchema.LoggingMessageNotification.class)); return Flux.fromIterable(loggingConsumers) .flatMap(consumer -> consumer.apply(loggingMessageNotification)) @@ -804,8 +798,8 @@ public Mono setLoggingLevel(LoggingLevel loggingLevel) { return this.withInitializationCheck("setting logging level", initializedResult -> { var params = new McpSchema.SetLevelRequest(loggingLevel); - return this.mcpSession.sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, new TypeReference() { - }).then(); + return this.mcpSession.sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, McpType.of(Object.class)) + .then(); }); } @@ -816,13 +810,14 @@ public Mono setLoggingLevel(LoggingLevel loggingLevel) { */ void setProtocolVersions(List protocolVersions) { this.protocolVersions = protocolVersions; + } // -------------------------- // Completions // -------------------------- - private static final TypeReference COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType COMPLETION_COMPLETE_RESULT_TYPE_REF = McpType + .of(McpSchema.CompleteResult.class); /** * Sends a completion/complete request to generate value suggestions based on a given diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClient.java index f21d1430..35df78c3 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClient.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClient.java @@ -13,13 +13,13 @@ import java.util.function.Function; import io.modelcontextprotocol.spec.McpClientTransport; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.spec.McpTransport; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.Implementation; -import io.modelcontextprotocol.spec.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageRequest; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.schema.McpSchema.Implementation; +import io.modelcontextprotocol.schema.McpSchema.Root; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java index 284b93f8..6c5d362a 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpClientFeatures.java @@ -12,7 +12,7 @@ import java.util.function.Consumer; import java.util.function.Function; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Utils; import reactor.core.publisher.Mono; diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index c91638a7..a283ec1b 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -6,11 +6,11 @@ import java.time.Duration; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.GetPromptRequest; +import io.modelcontextprotocol.schema.McpSchema.GetPromptResult; +import io.modelcontextprotocol.schema.McpSchema.ListPromptsResult; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index 2c56ab20..d783f37d 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -16,14 +16,16 @@ import java.util.function.Consumer; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.FlowSseClient.SseEvent; +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchemaCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.spec.McpTransport; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCMessage; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Utils; @@ -90,8 +92,8 @@ public class HttpClientSseClientTransport implements McpClientTransport { /** HTTP request builder for building requests to send messages to the server */ private final HttpRequest.Builder requestBuilder; - /** JSON object mapper for message serialization/deserialization */ - protected ObjectMapper objectMapper; + /** McpSchemaCodec for message serialization/deserialization */ + protected McpSchemaCodec schemaCodec; /** Flag indicating if the transport is in closing state */ private volatile boolean isClosing = false; @@ -184,7 +186,33 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques Assert.notNull(requestBuilder, "requestBuilder must not be null"); this.baseUri = URI.create(baseUri); this.sseEndpoint = sseEndpoint; - this.objectMapper = objectMapper; + this.schemaCodec = new McpJacksonCodec(objectMapper); + this.httpClient = httpClient; + this.requestBuilder = requestBuilder; + + this.sseClient = new FlowSseClient(this.httpClient, requestBuilder); + } + + /** + * Creates a new transport instance with custom HTTP client builder, object mapper, + * and headers. + * @param httpClient the HTTP client to use + * @param requestBuilder the HTTP request builder to use + * @param baseUri the base URI of the MCP server + * @param sseEndpoint the SSE endpoint path + * @param schemaCodec the schemaCodec for JSON serialization/deserialization + * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null + */ + HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, + String sseEndpoint, McpSchemaCodec schemaCodec) { + Assert.notNull(schemaCodec, "ObjectMapper must not be null"); + Assert.hasText(baseUri, "baseUri must not be empty"); + 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 = URI.create(baseUri); + this.sseEndpoint = sseEndpoint; + this.schemaCodec = schemaCodec; this.httpClient = httpClient; this.requestBuilder = requestBuilder; @@ -215,6 +243,8 @@ public static class Builder { private ObjectMapper objectMapper = new ObjectMapper(); + private McpSchemaCodec schemaCodec; + private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .header("Content-Type", "application/json"); @@ -315,13 +345,28 @@ public Builder objectMapper(ObjectMapper objectMapper) { return this; } + /** + * Sets the schema codec for JSON serialization/deserialization. + * @param schemaCodec the McpSchemaCodec implementation + * @return this builder + */ + public Builder withSchemaCodec(final McpSchemaCodec schemaCodec) { + Assert.notNull(schemaCodec, "McpSchemaCodec must not be null"); + this.schemaCodec = schemaCodec; + return this; + } + /** * Builds a new {@link HttpClientSseClientTransport} instance. * @return a new transport instance */ public HttpClientSseClientTransport build() { + if (schemaCodec == null) { + schemaCodec = new McpJacksonCodec(objectMapper); + } + return new HttpClientSseClientTransport(clientBuilder.build(), requestBuilder, baseUri, sseEndpoint, - objectMapper); + schemaCodec); } } @@ -360,7 +405,7 @@ public void onEvent(SseEvent event) { future.complete(null); } else if (MESSAGE_EVENT_TYPE.equals(event.type())) { - JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, event.data()); + JSONRPCMessage message = schemaCodec.decodeFromString(event.data()); Publisher result = handler.apply(Mono.just(message)); Mono.from(result).subscribe(); } @@ -417,7 +462,7 @@ public Mono sendMessage(JSONRPCMessage message) { } try { - String jsonText = this.objectMapper.writeValueAsString(message); + String jsonText = this.schemaCodec.encodeAsString(message); URI requestUri = Utils.resolveUri(baseUri, endpoint); HttpRequest request = this.requestBuilder.uri(requestUri) .POST(HttpRequest.BodyPublishers.ofString(jsonText)) @@ -471,8 +516,8 @@ public Mono closeGracefully() { * @return the unmarshalled object */ @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return schemaCodec.decodeResult(data, typeRef); } } diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java index 709a2f3b..7a925d6b 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java @@ -15,11 +15,13 @@ import java.util.function.Consumer; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchemaCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpClientTransport; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCMessage; import io.modelcontextprotocol.util.Assert; import org.reactivestreams.Publisher; @@ -50,7 +52,7 @@ public class StdioClientTransport implements McpClientTransport { /** The server process being communicated with */ private Process process; - private ObjectMapper objectMapper; + private McpSchemaCodec schemaCodec; /** Scheduler for handling inbound messages from the server process */ private Scheduler inboundScheduler; @@ -86,15 +88,24 @@ public StdioClientTransport(ServerParameters params) { * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization */ public StdioClientTransport(ServerParameters params, ObjectMapper objectMapper) { + this(params, new McpJacksonCodec(objectMapper)); + } + + /** + * Creates a new StdioClientTransport with the specified parameters and ObjectMapper. + * @param params The parameters for configuring the server process + * @param schemaCodec The McpSchemaCodec to use for JSON serialization/deserialization + */ + public StdioClientTransport(ServerParameters params, McpSchemaCodec schemaCodec) { Assert.notNull(params, "The params can not be null"); - Assert.notNull(objectMapper, "The ObjectMapper can not be null"); + Assert.notNull(schemaCodec, "The ObjectMapper can not be null"); this.inboundSink = Sinks.many().unicast().onBackpressureBuffer(); this.outboundSink = Sinks.many().unicast().onBackpressureBuffer(); this.params = params; - this.objectMapper = objectMapper; + this.schemaCodec = schemaCodec; this.errorSink = Sinks.many().unicast().onBackpressureBuffer(); @@ -260,7 +271,7 @@ private void startInboundProcessing() { String line; while (!isClosing && (line = processReader.readLine()) != null) { try { - JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, line); + JSONRPCMessage message = schemaCodec.decodeFromString(line); if (!this.inboundSink.tryEmitNext(message).isSuccess()) { if (!isClosing) { logger.error("Failed to enqueue inbound message: {}", message); @@ -301,7 +312,7 @@ private void startOutboundProcessing() { .handle((message, s) -> { if (message != null && !isClosing) { try { - String jsonMessage = objectMapper.writeValueAsString(message); + String jsonMessage = schemaCodec.encodeAsString(message); // Escape any embedded newlines in the JSON message as per spec: // https://spec.modelcontextprotocol.io/specification/basic/transports/#stdio // - Messages are delimited by newlines, and MUST NOT contain @@ -395,8 +406,8 @@ public Sinks.Many getErrorSink() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return schemaCodec.decodeResult(data, typeRef); } } diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 87ef29e0..73845ba1 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -15,17 +15,20 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiFunction; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchemaCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.session.McpClientSession; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; -import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; -import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; -import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.LoggingLevel; +import io.modelcontextprotocol.schema.McpSchema.LoggingMessageNotification; +import io.modelcontextprotocol.schema.McpSchema.ResourceTemplate; +import io.modelcontextprotocol.schema.McpSchema.SetLevelRequest; +import io.modelcontextprotocol.schema.McpSchema.Tool; import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory; @@ -256,7 +259,7 @@ private static class AsyncServerImpl extends McpAsyncServer { private final McpServerTransportProvider mcpTransportProvider; - private final ObjectMapper objectMapper; + private final McpSchemaCodec schemaCodec; private final McpSchema.ServerCapabilities serverCapabilities; @@ -285,8 +288,15 @@ private static class AsyncServerImpl extends McpAsyncServer { AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper, Duration requestTimeout, McpServerFeatures.Async features, McpUriTemplateManagerFactory uriTemplateManagerFactory) { + this(mcpTransportProvider, new McpJacksonCodec(objectMapper), requestTimeout, features, + uriTemplateManagerFactory); + } + + AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, McpSchemaCodec schemaCodec, + Duration requestTimeout, McpServerFeatures.Async features, + McpUriTemplateManagerFactory uriTemplateManagerFactory) { this.mcpTransportProvider = mcpTransportProvider; - this.objectMapper = objectMapper; + this.schemaCodec = schemaCodec; this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities(); this.instructions = features.instructions(); @@ -492,9 +502,8 @@ private McpServerSession.RequestHandler toolsListRequ private McpServerSession.RequestHandler toolsCallRequestHandler() { return (exchange, params) -> { - McpSchema.CallToolRequest callToolRequest = objectMapper.convertValue(params, - new TypeReference() { - }); + McpSchema.CallToolRequest callToolRequest = schemaCodec.decodeResult(params, + McpType.of(McpSchema.CallToolRequest.class)); Optional toolSpecification = this.tools.stream() .filter(tr -> callToolRequest.name().equals(tr.tool().name())) @@ -600,9 +609,8 @@ private List getResourceTemplates() { private McpServerSession.RequestHandler resourcesReadRequestHandler() { return (exchange, params) -> { - McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params, - new TypeReference() { - }); + McpSchema.ReadResourceRequest resourceRequest = schemaCodec.decodeResult(params, + McpType.of(McpSchema.ReadResourceRequest.class)); var resourceUri = resourceRequest.uri(); McpServerFeatures.AsyncResourceSpecification specification = this.resources.values() @@ -699,9 +707,8 @@ private McpServerSession.RequestHandler promptsList private McpServerSession.RequestHandler promptsGetRequestHandler() { return (exchange, params) -> { - McpSchema.GetPromptRequest promptRequest = objectMapper.convertValue(params, - new TypeReference() { - }); + McpSchema.GetPromptRequest promptRequest = schemaCodec.decodeResult(params, + McpType.of(McpSchema.GetPromptRequest.class)); // Implement prompt retrieval logic here McpServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name()); @@ -736,9 +743,8 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() { return (exchange, params) -> { return Mono.defer(() -> { - SetLevelRequest newMinLoggingLevel = objectMapper.convertValue(params, - new TypeReference() { - }); + SetLevelRequest newMinLoggingLevel = schemaCodec.decodeResult(params, + McpType.of(SetLevelRequest.class)); exchange.setMinLoggingLevel(newMinLoggingLevel.level()); diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index 3b214222..7fe2c309 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -4,11 +4,11 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; -import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.LoggingLevel; +import io.modelcontextprotocol.schema.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; @@ -30,11 +30,11 @@ public class McpAsyncServerExchange { private volatile LoggingLevel minLoggingLevel = LoggingLevel.INFO; - private static final TypeReference CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType CREATE_MESSAGE_RESULT_TYPE_REF = McpType + .of(McpSchema.CreateMessageResult.class); - private static final TypeReference LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() { - }; + private static final McpType LIST_ROOTS_RESULT_TYPE_REF = McpType + .of(McpSchema.ListRootsResult.class); /** * Create a new asynchronous exchange with the client. diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServer.java index d6ec2cc3..82c9b8e9 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -14,9 +14,9 @@ import java.util.function.BiFunction; import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.ResourceTemplate; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory; diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java index 08660245..bce50887 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java @@ -11,7 +11,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Utils; import reactor.core.publisher.Mono; diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index bf310450..2ca159fd 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -4,8 +4,8 @@ package io.modelcontextprotocol.server; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; /** diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java index 2417e4c9..220380f5 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java @@ -4,8 +4,8 @@ package io.modelcontextprotocol.server; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.LoggingMessageNotification; /** * Represents a synchronous exchange with a Model Context Protocol (MCP) client. The diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index 1dda2d5f..26a3c0d9 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -11,10 +11,12 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; @@ -84,7 +86,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement public static final String DEFAULT_BASE_URL = ""; /** JSON object mapper for serialization/deserialization */ - private final ObjectMapper objectMapper; + private final McpJacksonCodec jacksonCodec; /** Base URL for the server transport */ private final String baseUrl; @@ -128,7 +130,7 @@ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String m */ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) { - this.objectMapper = objectMapper; + this.jacksonCodec = new McpJacksonCodec(objectMapper); this.baseUrl = baseUrl; this.messageEndpoint = messageEndpoint; this.sseEndpoint = sseEndpoint; @@ -264,7 +266,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.setContentType(APPLICATION_JSON); response.setCharacterEncoding(UTF_8); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - String jsonError = objectMapper.writeValueAsString(new McpError("Session ID missing in message endpoint")); + String jsonError = jacksonCodec.getMapper() + .writeValueAsString(new McpError("Session ID missing in message endpoint")); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -277,7 +280,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.setContentType(APPLICATION_JSON); response.setCharacterEncoding(UTF_8); response.setStatus(HttpServletResponse.SC_NOT_FOUND); - String jsonError = objectMapper.writeValueAsString(new McpError("Session not found: " + sessionId)); + String jsonError = jacksonCodec.getMapper() + .writeValueAsString(new McpError("Session not found: " + sessionId)); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -292,7 +296,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) body.append(line); } - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString()); + McpSchema.JSONRPCMessage message = jacksonCodec.decodeFromString(body.toString()); // Process the message through the session's handle method session.handle(message).block(); // Block for Servlet compatibility @@ -306,7 +310,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.setContentType(APPLICATION_JSON); response.setCharacterEncoding(UTF_8); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - String jsonError = objectMapper.writeValueAsString(mcpError); + String jsonError = jacksonCodec.getMapper().writeValueAsString(mcpError); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -396,7 +400,7 @@ private class HttpServletMcpSessionTransport implements McpServerTransport { public Mono sendMessage(McpSchema.JSONRPCMessage message) { return Mono.fromRunnable(() -> { try { - String jsonText = objectMapper.writeValueAsString(message); + String jsonText = jacksonCodec.getMapper().writeValueAsString(message); sendEvent(writer, MESSAGE_EVENT_TYPE, jsonText); logger.debug("Message sent to session {}", sessionId); } @@ -416,8 +420,8 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { * @param The target type */ @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return jacksonCodec.decodeResult(data, typeRef); } /** diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java index adc234fb..efd6a22b 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java @@ -14,11 +14,14 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchemaCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCMessage; import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; @@ -44,7 +47,7 @@ public class StdioServerTransportProvider implements McpServerTransportProvider private static final Logger logger = LoggerFactory.getLogger(StdioServerTransportProvider.class); - private final ObjectMapper objectMapper; + private final McpSchemaCodec schemaCodec; private final InputStream inputStream; @@ -81,11 +84,23 @@ public StdioServerTransportProvider(ObjectMapper objectMapper) { * @param outputStream The output stream to write to */ public StdioServerTransportProvider(ObjectMapper objectMapper, InputStream inputStream, OutputStream outputStream) { - Assert.notNull(objectMapper, "The ObjectMapper can not be null"); + this(new McpJacksonCodec(objectMapper), inputStream, outputStream); + } + + /** + * Creates a new StdioServerTransportProvider with the specified ObjectMapper and + * streams. + * @param schemaCodec The McpSchemaCodec to use for JSON serialization/deserialization + * @param inputStream The input stream to read from + * @param outputStream The output stream to write to + */ + public StdioServerTransportProvider(McpSchemaCodec schemaCodec, InputStream inputStream, + OutputStream outputStream) { + Assert.notNull(schemaCodec, "The ObjectMapper can not be null"); Assert.notNull(inputStream, "The InputStream can not be null"); Assert.notNull(outputStream, "The OutputStream can not be null"); - this.objectMapper = objectMapper; + this.schemaCodec = schemaCodec; this.inputStream = inputStream; this.outputStream = outputStream; } @@ -165,8 +180,8 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return schemaCodec.decodeResult(data, typeRef); } @Override @@ -219,8 +234,7 @@ private void startInboundProcessing() { logger.debug("Received JSON message: {}", line); try { - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, - line); + McpSchema.JSONRPCMessage message = schemaCodec.decodeFromString(line); if (!this.inboundSink.tryEmitNext(message).isSuccess()) { // logIfNotClosing("Failed to enqueue message"); break; @@ -263,7 +277,7 @@ private void startOutboundProcessing() { .handle((message, sink) -> { if (message != null && !isClosing.get()) { try { - String jsonMessage = objectMapper.writeValueAsString(message); + String jsonMessage = schemaCodec.encodeAsString(message); // Escape any embedded newlines in the JSON message as per spec jsonMessage = jsonMessage.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n"); diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpClientSession.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpClientSession.java index 10f68860..212b3822 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpClientSession.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpClientSession.java @@ -10,12 +10,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -import com.fasterxml.jackson.core.type.TypeReference; - +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSession; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; @@ -233,7 +232,7 @@ private String generateRequestId() { * @return A Publisher containing the response */ @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, McpType typeRef) { String requestId = this.generateRequestId(); return Mono.deferContextual(ctx -> Mono.create(sink -> { @@ -241,7 +240,7 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method, requestId, requestParams); Mono.from(this.transport.sendMessage(jsonrpcRequest)) - .contextWrite(ctx) + .contextWrite(ctx) // TODO: It's most efficient to create a dedicated Subscriber here .subscribe(v -> { }, error -> { @@ -254,7 +253,7 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc sink.error(new McpError(jsonRpcResponse.error())); } else { - if (typeRef.getType().equals(Void.class)) { + if (typeRef.getRawClass().equals(Void.class)) { sink.complete(); } else { diff --git a/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpServerSession.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpServerSession.java index ab4d26bc..693f8dd2 100644 --- a/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpServerSession.java +++ b/mcp-reactor/src/main/java/io/modelcontextprotocol/session/McpServerSession.java @@ -7,7 +7,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.server.McpAsyncServerExchange; import org.slf4j.Logger; @@ -15,7 +15,7 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpSession; import io.modelcontextprotocol.spec.ServerSessionFactory; @@ -116,7 +116,7 @@ private String generateRequestId() { } @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, McpType typeRef) { String requestId = this.generateRequestId(); return Mono.create(sink -> { @@ -134,7 +134,7 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc sink.error(new McpError(jsonRpcResponse.error())); } else { - if (typeRef.getType().equals(Void.class)) { + if (typeRef.getGenericType().equals(Void.class)) { sink.complete(); } else { @@ -212,8 +212,7 @@ private Mono handleIncomingRequest(McpSchema.JSONRPCR if (McpSchema.METHOD_INITIALIZE.equals(request.method())) { // TODO handle situation where already initialized! McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(request.params(), - new TypeReference() { - }); + McpType.of(McpSchema.InitializeRequest.class)); this.state.lazySet(STATE_INITIALIZING); this.init(initializeRequest.capabilities(), initializeRequest.clientInfo()); diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/util/DeafaultMcpUriTemplateManagerFactory.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/util/DefaultMcpUriTemplateManager.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManager.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java b/mcp-reactor/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java similarity index 100% rename from mcp/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java rename to mcp-reactor/src/main/java/io/modelcontextprotocol/util/McpUriTemplateManagerFactory.java diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java index cc6ec7b4..0aec4315 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java @@ -11,12 +11,12 @@ import org.reactivestreams.Publisher; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpClientTransport; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCNotification; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCRequest; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -31,6 +31,8 @@ public class MockMcpClientTransport implements McpClientTransport { private final BiConsumer interceptor; + private final McpJacksonCodec jacksonCodec = new McpJacksonCodec(); + public MockMcpClientTransport() { this((t, msg) -> { }); @@ -96,8 +98,8 @@ public Mono closeGracefully() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return new ObjectMapper().convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return jacksonCodec.decodeResult(data, typeRef); } } diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java index 12975ccc..19ef6eeb 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java @@ -8,11 +8,11 @@ import java.util.List; import java.util.function.BiConsumer; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCNotification; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpServerTransport; import reactor.core.publisher.Mono; @@ -25,6 +25,8 @@ public class MockMcpServerTransport implements McpServerTransport { private final BiConsumer interceptor; + private final McpJacksonCodec jacksonCodec = new McpJacksonCodec(); + public MockMcpServerTransport() { this((t, msg) -> { }); @@ -64,8 +66,8 @@ public Mono closeGracefully() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return new ObjectMapper().convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return jacksonCodec.decodeResult(data, typeRef); } } diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java index d65a8eb3..011eddc9 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java @@ -15,7 +15,7 @@ */ package io.modelcontextprotocol; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.ServerSessionFactory; import io.modelcontextprotocol.spec.McpServerTransportProvider; diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 9f334a78..b84967b1 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -14,18 +14,18 @@ import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolRequest; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageRequest; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.schema.McpSchema.GetPromptRequest; +import io.modelcontextprotocol.schema.McpSchema.Prompt; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema.SubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema.UnsubscribeRequest; import io.modelcontextprotocol.spec.McpTransport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index de456390..686f52f7 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -13,20 +13,20 @@ import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.ListResourceTemplatesResult; -import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult; -import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; -import io.modelcontextprotocol.spec.McpSchema.TextContent; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolRequest; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.ListResourceTemplatesResult; +import io.modelcontextprotocol.schema.McpSchema.ListResourcesResult; +import io.modelcontextprotocol.schema.McpSchema.ListToolsResult; +import io.modelcontextprotocol.schema.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema.SubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema.TextContent; +import io.modelcontextprotocol.schema.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema.UnsubscribeRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index 4510b152..1665af66 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -9,24 +9,25 @@ import java.util.Map; import java.util.function.Function; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.MockMcpClientTransport; +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; -import io.modelcontextprotocol.spec.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.InitializeResult; +import io.modelcontextprotocol.schema.McpSchema.Root; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; -import static io.modelcontextprotocol.spec.McpSchema.METHOD_INITIALIZE; +import static io.modelcontextprotocol.schema.McpSchema.METHOD_INITIALIZE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class McpAsyncClientResponseHandlerTests { + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(); + private static final McpSchema.Implementation SERVER_INFO = new McpSchema.Implementation("test-server", "1.0.0"); private static final McpSchema.ServerCapabilities SERVER_CAPABILITIES = McpSchema.ServerCapabilities.builder() @@ -90,7 +91,7 @@ void testSuccessfulInitialization() { } @Test - void testToolsChangeNotificationHandling() throws JsonProcessingException { + void testToolsChangeNotificationHandling() throws Exception { MockMcpClientTransport transport = initializationEnabledTransport(); // Create a list to store received tools for verification @@ -107,8 +108,9 @@ void testToolsChangeNotificationHandling() throws JsonProcessingException { // Create a mock tools list that the server will return Map inputSchema = Map.of("type", "object", "properties", Map.of(), "required", List.of()); - McpSchema.Tool mockTool = new McpSchema.Tool("test-tool", "Test Tool Description", - new ObjectMapper().writeValueAsString(inputSchema)); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(mcpJacksonCodec.getMapper().writeValueAsString(inputSchema), McpSchema.JsonSchema.class); + McpSchema.Tool mockTool = new McpSchema.Tool("test-tool", "Test Tool Description", jsonSchema); McpSchema.ListToolsResult mockToolsResult = new McpSchema.ListToolsResult(List.of(mockTool), null); // Simulate server sending tools/list_changed notification @@ -293,8 +295,7 @@ void testSamplingCreateMessageRequestHandling() { assertThat(response.error()).isNull(); McpSchema.CreateMessageResult result = transport.unmarshalFrom(response.result(), - new TypeReference() { - }); + McpType.of(McpSchema.CreateMessageResult.class)); assertThat(result).isNotNull(); assertThat(result.role()).isEqualTo(McpSchema.Role.ASSISTANT); assertThat(result.content()).isNotNull(); diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java index bf473849..d6990cdd 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java @@ -9,8 +9,8 @@ import io.modelcontextprotocol.MockMcpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.InitializeResult; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java index 1d8b91d2..713a5ae3 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java @@ -16,8 +16,9 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -65,7 +66,7 @@ static class TestHttpClientSseClientTransport extends HttpClientSseClientTranspo private final Sinks.Many events = Sinks.many().unicast().onBackpressureBuffer(); public TestHttpClientSseClientTransport(final String baseUri) { - super(HttpClient.newHttpClient(), HttpRequest.newBuilder(), baseUri, "/sse", new ObjectMapper()); + super(HttpClient.newHttpClient(), HttpRequest.newBuilder(), baseUri, "/sse", new McpJacksonCodec()); } public int getInboundMessageCount() { diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index c2a239fc..7f2842ea 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -7,16 +7,17 @@ import java.time.Duration; import java.util.List; +import io.modelcontextprotocol.schema.McpJacksonCodec; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.GetPromptResult; +import io.modelcontextprotocol.schema.McpSchema.Prompt; +import io.modelcontextprotocol.schema.McpSchema.PromptMessage; +import io.modelcontextprotocol.schema.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.schema.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,6 +43,8 @@ public abstract class AbstractMcpAsyncServerTests { private static final String TEST_PROMPT_NAME = "test-prompt"; + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(); + abstract protected McpServerTransportProvider createMcpTransportProvider(); protected void onStart() { @@ -101,8 +104,10 @@ void testImmediateClose() { """; @Test - void testAddTool() { - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + void testAddTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -116,8 +121,10 @@ void testAddTool() { } @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testAddDuplicateTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -137,8 +144,10 @@ void testAddDuplicateTool() { } @Test - void testRemoveTool() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testRemoveTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -166,8 +175,10 @@ void testRemoveNonexistentTool() { } @Test - void testNotifyToolsListChanged() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testNotifyToolsListChanged() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 803ee3d8..869fad93 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -6,16 +6,17 @@ import java.util.List; +import io.modelcontextprotocol.schema.McpJacksonCodec; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.GetPromptResult; +import io.modelcontextprotocol.schema.McpSchema.Prompt; +import io.modelcontextprotocol.schema.McpSchema.PromptMessage; +import io.modelcontextprotocol.schema.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.schema.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -39,6 +40,8 @@ public abstract class AbstractMcpSyncServerTests { private static final String TEST_PROMPT_NAME = "test-prompt"; + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(); + abstract protected McpServerTransportProvider createMcpTransportProvider(); protected void onStart() { @@ -108,13 +111,15 @@ void testGetAsyncServer() { """; @Test - void testAddTool() { + void testAddTool() throws Exception { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", jsonSchema); assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(newTool, (exchange, args) -> new CallToolResult(List.of(), false)))) .doesNotThrowAnyException(); @@ -123,8 +128,10 @@ void testAddTool() { } @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testAddDuplicateTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -141,8 +148,10 @@ void testAddDuplicateTool() { } @Test - void testRemoveTool() { - Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); + void testRemoveTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", jsonSchema); var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java index f643f1ba..3e3148c0 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java @@ -9,7 +9,7 @@ import io.modelcontextprotocol.MockMcpServerTransport; import io.modelcontextprotocol.MockMcpServerTransportProvider; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java index 2cd62889..737a24a9 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java @@ -8,7 +8,7 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.server.McpServer; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleState; import org.apache.catalina.startup.Tomcat; diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index 6ed64e33..6873ec66 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -20,20 +20,21 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; +import io.modelcontextprotocol.schema.McpJacksonCodec; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; -import io.modelcontextprotocol.spec.McpSchema.ModelPreferences; -import io.modelcontextprotocol.spec.McpSchema.Role; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageRequest; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.schema.McpSchema.InitializeResult; +import io.modelcontextprotocol.schema.McpSchema.ModelPreferences; +import io.modelcontextprotocol.schema.McpSchema.Role; +import io.modelcontextprotocol.schema.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.schema.McpSchema.Tool; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleState; import org.apache.catalina.startup.Tomcat; @@ -57,6 +58,8 @@ class HttpServletSseServerTransportProviderIntegrationTests { private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message"; + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(); + private HttpServletSseServerTransportProvider mcpServerTransportProvider; McpClient.SyncSpec clientBuilder; @@ -107,10 +110,11 @@ public void after() { // --------------------------------------- @Test @Disabled - void testCreateMessageWithoutSamplingCapabilities() { - + void testCreateMessageWithoutSamplingCapabilities() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); @@ -138,7 +142,7 @@ void testCreateMessageWithoutSamplingCapabilities() { } @Test - void testCreateMessageSuccess() { + void testCreateMessageSuccess() throws Exception { Function samplingHandler = request -> { assertThat(request.messages()).hasSize(1); @@ -151,8 +155,10 @@ void testCreateMessageSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var createMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -199,7 +205,7 @@ void testCreateMessageSuccess() { } @Test - void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { + void testCreateMessageWithRequestTimeoutSuccess() throws Exception { // Client @@ -226,8 +232,10 @@ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -271,7 +279,7 @@ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { } @Test - void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { + void testCreateMessageWithRequestTimeoutFail() throws Exception { // Client @@ -297,9 +305,10 @@ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -389,10 +398,11 @@ void testRootsSuccess() { } @Test - void testRootsWithoutCapability() { - + void testRootsWithoutCapability() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.SyncToolSpecification tool = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { exchange.listRoots(); // try to list roots @@ -512,11 +522,12 @@ void testRootsServerCloseWithActiveSubscription() { """; @Test - void testToolCallSuccess() { - + void testToolCallSuccess() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { // perform a blocking call to a remote service try { HttpClient client = HttpClient.newHttpClient(); @@ -557,11 +568,12 @@ void testToolCallSuccess() { } @Test - void testToolListChangeHandlingSuccess() { - + void testToolListChangeHandlingSuccess() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { // perform a blocking call to a remote service try { HttpClient client = HttpClient.newHttpClient(); @@ -628,8 +640,7 @@ void testToolListChangeHandlingSuccess() { // Add a new tool McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), - (exchange, request) -> callResponse); + new McpSchema.Tool("tool2", "tool2 description", jsonSchema), (exchange, request) -> callResponse); mcpServer.addTool(tool2); @@ -658,14 +669,14 @@ void testInitialize() { // Logging Tests // --------------------------------------- @Test - void testLoggingNotification() { + void testLoggingNotification() throws Exception { // Create a list to store received logging notifications List receivedNotifications = new ArrayList<>(); - + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); // Create server with a tool that sends logging notifications McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema), - (exchange, request) -> { + new McpSchema.Tool("logging-test", "Test logging notifications", jsonSchema), (exchange, request) -> { // Create and send notifications with different levels diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java index 59337476..aa2069b9 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java @@ -16,7 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import org.junit.jupiter.api.AfterEach; diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/session/McpClientSessionTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/session/McpClientSessionTests.java index 4e3bb5ab..f6041e4a 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/session/McpClientSessionTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/session/McpClientSessionTests.java @@ -7,7 +7,6 @@ import java.time.Duration; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.MockMcpClientTransport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -15,8 +14,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.test.StepVerifier; @@ -71,8 +71,7 @@ void testConstructorWithInvalidArguments() { .hasMessageContaining("transport can not be null"); } - TypeReference responseType = new TypeReference<>() { - }; + McpType responseType = McpType.of(String.class); @Test void testSendRequest() { diff --git a/mcp-reactor/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java b/mcp-reactor/src/test/java/io/modelcontextprotocol/util/McpUriTemplateManagerTests.java similarity index 92% rename from mcp-reactor/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java rename to mcp-reactor/src/test/java/io/modelcontextprotocol/util/McpUriTemplateManagerTests.java index 6f041daa..e2afb215 100644 --- a/mcp-reactor/src/test/java/io/modelcontextprotocol/McpUriTemplateManagerTests.java +++ b/mcp-reactor/src/test/java/io/modelcontextprotocol/util/McpUriTemplateManagerTests.java @@ -2,7 +2,7 @@ * Copyright 2025-2025 the original author or authors. */ -package io.modelcontextprotocol; +package io.modelcontextprotocol.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -12,9 +12,6 @@ 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; diff --git a/mcp-schema-jackson/pom.xml b/mcp-schema-jackson/pom.xml new file mode 100644 index 00000000..ab42095e --- /dev/null +++ b/mcp-schema-jackson/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.10.0-SNAPSHOT + ../pom.xml + + + mcp-schema-jackson + jar + Java SDK MCP Jackson Schema + Java SDK Jackson Schema of the Model Context Protocol + https://github.com/modelcontextprotocol/java-sdk + + + io.modelcontextprotocol.sdk + mcp-spi + ${project.version} + + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.assertj + assertj-core + ${assert4j.version} + test + + + net.javacrumbs.json-unit + json-unit-assertj + ${json-unit-assertj.version} + test + + + \ No newline at end of file diff --git a/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumDeserializer.java b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumDeserializer.java new file mode 100644 index 00000000..3eaaff45 --- /dev/null +++ b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumDeserializer.java @@ -0,0 +1,34 @@ +package io.modelcontextprotocol.schema; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +/** + * @author Aliaksei Darafeyeu + */ +public class GenericEnumDeserializer> extends JsonDeserializer { + + private final Class enumType; + + public GenericEnumDeserializer(Class enumType) { + this.enumType = enumType; + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String camel = p.getText(); + // Convert camelCase or kebab-case to UPPER_SNAKE_CASE + String snake = camel.replaceAll("([a-z])([A-Z])", "$1_$2").replace("-", "_").toUpperCase(); + + try { + return Enum.valueOf(enumType, snake); + } + catch (IllegalArgumentException ex) { + throw new IOException("Unknown enum value: " + camel + " for enum " + enumType.getSimpleName()); + } + } + +} diff --git a/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumModule.java b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumModule.java new file mode 100644 index 00000000..f797fe7c --- /dev/null +++ b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumModule.java @@ -0,0 +1,26 @@ +package io.modelcontextprotocol.schema; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * @author Aliaksei Darafeyeu + */ +public class GenericEnumModule extends SimpleModule { + + public GenericEnumModule() { + addSerializer(McpSchema.Role.class, new GenericEnumSerializer()); + addDeserializer(McpSchema.Role.class, new GenericEnumDeserializer<>(McpSchema.Role.class)); + + addSerializer(McpSchema.CreateMessageRequest.ContextInclusionStrategy.class, new GenericEnumSerializer()); + addDeserializer(McpSchema.CreateMessageRequest.ContextInclusionStrategy.class, + new GenericEnumDeserializer<>(McpSchema.CreateMessageRequest.ContextInclusionStrategy.class)); + + addSerializer(McpSchema.CreateMessageResult.StopReason.class, new GenericEnumSerializer()); + addDeserializer(McpSchema.CreateMessageResult.StopReason.class, + new GenericEnumDeserializer<>(McpSchema.CreateMessageResult.StopReason.class)); + + addSerializer(McpSchema.LoggingLevel.class, new GenericEnumSerializer()); + addDeserializer(McpSchema.LoggingLevel.class, new GenericEnumDeserializer<>(McpSchema.LoggingLevel.class)); + } + +} diff --git a/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumSerializer.java b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumSerializer.java new file mode 100644 index 00000000..1ad05591 --- /dev/null +++ b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/GenericEnumSerializer.java @@ -0,0 +1,24 @@ +package io.modelcontextprotocol.schema; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * @author Aliaksei Darafeyeu + */ +public class GenericEnumSerializer extends JsonSerializer> { + + @Override + public void serialize(Enum value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + final String[] parts = value.name().toLowerCase().split("_"); + final StringBuilder result = new StringBuilder(parts[0]); + for (int i = 1; i < parts.length; i++) { + result.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1)); + } + gen.writeString(result.toString()); + } + +} diff --git a/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonCodec.java b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonCodec.java new file mode 100644 index 00000000..3f9e1be3 --- /dev/null +++ b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonCodec.java @@ -0,0 +1,110 @@ +package io.modelcontextprotocol.schema; + +import java.io.IOException; +import java.util.HashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.util.Assert; + +/** + * @author Aliaksei Darafeyeu + */ +public class McpJacksonCodec implements McpSchemaCodec { + + private static final Logger logger = LoggerFactory.getLogger(McpJacksonCodec.class); + + private static final TypeReference> MAP_TYPE_REF = new TypeReference<>() { + }; + + private final ObjectMapper mapper; + + public McpJacksonCodec() { + this(new ObjectMapper()); + } + + public McpJacksonCodec(final ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "The ObjectMapper can not be null"); + this.mapper = objectMapper; + registerMixins(); + } + + private void registerMixins() { + mapper.registerModule(new GenericEnumModule()); + McpJacksonSchema.MIXINS.forEach(mapper::addMixIn); + } + + public ObjectMapper getMapper() { + return mapper; + } + + /** + * Deserializes a JSON string into a JSONRPCMessage object. + * @param jsonText The JSON string to deserialize + * @return A JSONRPCMessage instance using either the + * {@link McpSchema.JSONRPCRequest}, {@link McpSchema.JSONRPCNotification}, or + * {@link McpSchema.JSONRPCResponse} classes. + * @throws IOException If there's an error during deserialization + * @throws IllegalArgumentException If the JSON structure doesn't match any known + * message type + */ + public McpSchema.JSONRPCMessage decodeFromString(String jsonText) throws IOException { + + logger.debug("Received JSON message: {}", jsonText); + + var map = mapper.readValue(jsonText, MAP_TYPE_REF); + + // Determine message type based on specific JSON structure + if (map.containsKey("method") && map.containsKey("id")) { + return mapper.convertValue(map, McpSchema.JSONRPCRequest.class); + } + else if (map.containsKey("method") && !map.containsKey("id")) { + return mapper.convertValue(map, McpSchema.JSONRPCNotification.class); + } + else if (map.containsKey("result") || map.containsKey("error")) { + return mapper.convertValue(map, McpSchema.JSONRPCResponse.class); + } + + throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + jsonText); + } + + public byte[] encode(McpSchema.JSONRPCMessage message) { + try { + return mapper.writeValueAsBytes(message); + } + catch (final Exception e) { + throw new RuntimeException("Failed to serialize JSONRPCMessage", e); + } + } + + public String encodeAsString(Object message) throws IOException { + return mapper.writeValueAsString(message); + } + + public McpSchema.JSONRPCMessage decode(byte[] bytes) { + try { + return mapper.readValue(bytes, McpSchema.JSONRPCMessage.class); + } + catch (final Exception e) { + throw new RuntimeException("Failed to deserialize JSONRPCMessage", e); + } + } + + public T decodeResult(Object rawResult, McpType type) { + return mapper.convertValue(rawResult, mapper.constructType(type.getGenericType())); + } + + public T decodeBytes(byte[] bytes, McpType type) { + try { + return mapper.readValue(bytes, mapper.constructType(type.getGenericType())); + } + catch (final Exception e) { + throw new RuntimeException("Failed to deserialize JSON bytes", e); + } + } + +} diff --git a/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonSchema.java b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonSchema.java new file mode 100644 index 00000000..02bd0078 --- /dev/null +++ b/mcp-schema-jackson/src/main/java/io/modelcontextprotocol/schema/McpJacksonSchema.java @@ -0,0 +1,625 @@ +/* + * Copyright 2025-2025 the original author or authors. + */ + +package io.modelcontextprotocol.schema; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; + +/** + * @author Aliaksei Darafeyeu + */ +public final class McpJacksonSchema { + + public static final Map, Class> MIXINS = Map.ofEntries( + Map.entry(McpSchema.JSONRPCRequest.class, JSONRPCRequestMixin.class), + Map.entry(McpSchema.JSONRPCNotification.class, JSONRPCNotificationMixin.class), + Map.entry(McpSchema.JSONRPCResponse.class, JSONRPCResponseMixin.class), + Map.entry(McpSchema.JSONRPCResponse.JSONRPCError.class, JSONRPCErrorMixin.class), + Map.entry(McpSchema.InitializeRequest.class, InitializeRequestMixin.class), + Map.entry(McpSchema.InitializeResult.class, InitializeResultMixin.class), + Map.entry(McpSchema.ClientCapabilities.class, ClientCapabilitiesMixin.class), + Map.entry(McpSchema.ClientCapabilities.RootCapabilities.class, RootCapabilitiesMixin.class), + Map.entry(McpSchema.ClientCapabilities.Sampling.class, SamplingMixin.class), + Map.entry(McpSchema.ServerCapabilities.class, ServerCapabilitiesMixin.class), + Map.entry(McpSchema.ServerCapabilities.CompletionCapabilities.class, CompletionCapabilitiesMixin.class), + Map.entry(McpSchema.ServerCapabilities.LoggingCapabilities.class, LoggingCapabilitiesMixin.class), + Map.entry(McpSchema.ServerCapabilities.PromptCapabilities.class, PromptCapabilitiesMixin.class), + Map.entry(McpSchema.ServerCapabilities.ToolCapabilities.class, ToolCapabilitiesMixin.class), + Map.entry(McpSchema.Implementation.class, ImplementationMixin.class), + Map.entry(McpSchema.Annotations.class, AnnotationsMixin.class), + Map.entry(McpSchema.Resource.class, ResourceMixin.class), + Map.entry(McpSchema.ResourceTemplate.class, ResourceTemplateMixin.class), + Map.entry(McpSchema.ListResourcesResult.class, ListResourcesResultMixin.class), + Map.entry(McpSchema.ListResourceTemplatesResult.class, ListResourceTemplatesResultMixin.class), + Map.entry(McpSchema.ReadResourceRequest.class, ReadResourceRequestMixin.class), + Map.entry(McpSchema.ReadResourceResult.class, ReadResourceResultMixin.class), + Map.entry(McpSchema.SubscribeRequest.class, SubscribeRequestMixin.class), + Map.entry(McpSchema.UnsubscribeRequest.class, UnsubscribeRequestMixin.class), + Map.entry(McpSchema.ResourceContents.class, ResourceContentsMixin.class), + Map.entry(McpSchema.TextResourceContents.class, TextResourceContentsMixin.class), + Map.entry(McpSchema.BlobResourceContents.class, BlobResourceContentsMixin.class), + Map.entry(McpSchema.Prompt.class, PromptMixin.class), + Map.entry(McpSchema.PromptArgument.class, PromptArgumentMixin.class), + Map.entry(McpSchema.PromptMessage.class, PromptMessageMixin.class), + Map.entry(McpSchema.GetPromptRequest.class, GetPromptRequestMixin.class), + Map.entry(McpSchema.ListPromptsResult.class, ListPromptsResultMixin.class), + Map.entry(McpSchema.GetPromptResult.class, GetPromptResultMixin.class), + Map.entry(McpSchema.ListToolsResult.class, ListToolsResultMixin.class), + Map.entry(McpSchema.JsonSchema.class, JsonSchemaMixin.class), + Map.entry(McpSchema.Tool.class, ToolMixin.class), + Map.entry(McpSchema.CallToolRequest.class, CallToolRequestMixin.class), + Map.entry(McpSchema.CallToolResult.class, CallToolResultMixin.class), + Map.entry(McpSchema.ModelPreferences.class, ModelPreferencesMixin.class), + Map.entry(McpSchema.ModelHint.class, ModelHintMixin.class), + Map.entry(McpSchema.SamplingMessage.class, SamplingMessageMixin.class), + Map.entry(McpSchema.CreateMessageRequest.class, CreateMessageRequestMixin.class), + Map.entry(McpSchema.CreateMessageResult.class, CreateMessageResultMixin.class), + Map.entry(McpSchema.PaginatedRequest.class, PaginatedRequestMixin.class), + Map.entry(McpSchema.PaginatedResult.class, PaginatedResultMixin.class), + Map.entry(McpSchema.ProgressNotification.class, ProgressNotificationMixin.class), + Map.entry(McpSchema.LoggingMessageNotification.class, LoggingMessageNotificationMixin.class), + Map.entry(McpSchema.SetLevelRequest.class, SetLevelRequestMixin.class), + Map.entry(McpSchema.PromptReference.class, PromptReferenceMixin.class), + Map.entry(McpSchema.ResourceReference.class, ResourceReferenceMixin.class), + Map.entry(McpSchema.CompleteRequest.class, CompleteRequestMixin.class), + Map.entry(McpSchema.CompleteRequest.CompleteArgument.class, CompleteArgumentMixin.class), + Map.entry(McpSchema.CompleteResult.class, CompleteResultMixin.class), + Map.entry(McpSchema.CompleteResult.CompleteCompletion.class, CompleteCompletionMixin.class), + Map.entry(McpSchema.Content.class, ContentMixin.class), + Map.entry(McpSchema.TextContent.class, TextContentMixin.class), + Map.entry(McpSchema.ImageContent.class, ImageContentMixin.class), + Map.entry(McpSchema.EmbeddedResource.class, EmbeddedResourceMixin.class), + Map.entry(McpSchema.Root.class, RootMixin.class), + Map.entry(McpSchema.ListRootsResult.class, ListRootsResultMixin.class)); + + private McpJacksonSchema() { + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface JSONRPCRequestMixin { + + // @formatter:off + @JsonProperty("jsonrpc") String jsonrpc(); + @JsonProperty("method") String method(); + @JsonProperty("id") Object id(); + @JsonProperty("params") Object params(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface JSONRPCNotificationMixin { + + // @formatter:off + @JsonProperty("jsonrpc") String jsonrpc(); + @JsonProperty("method") String method(); + @JsonProperty("params") Object params(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface JSONRPCResponseMixin { + + // @formatter:off + @JsonProperty("jsonrpc") String jsonrpc(); + @JsonProperty("id") Object id(); + @JsonProperty("result") Object result(); + @JsonProperty("error") McpSchema.JSONRPCResponse.JSONRPCError error(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface JSONRPCErrorMixin {// @formatter:off + @JsonProperty("code") int code(); + @JsonProperty("message") String message(); + @JsonProperty("data") Object data(); + }// @formatter:on + + // --------------------------- + // Initialization + // --------------------------- + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface InitializeRequestMixin { + + // @formatter:off + @JsonProperty("protocolVersion") String protocolVersion(); + @JsonProperty("capabilities") McpSchema.ClientCapabilities capabilities(); + @JsonProperty("clientInfo") McpSchema.Implementation clientInfo(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface InitializeResultMixin { + + // @formatter:off + @JsonProperty("protocolVersion") String protocolVersion(); + @JsonProperty("capabilities") McpSchema.ServerCapabilities capabilities(); + @JsonProperty("serverInfo") McpSchema.Implementation serverInfo(); + @JsonProperty("instructions") String instructions(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ClientCapabilitiesMixin { + + // @formatter:off + @JsonProperty("experimental") Map experimental(); + @JsonProperty("roots") McpSchema.ClientCapabilities.RootCapabilities roots(); + @JsonProperty("sampling") McpSchema.ClientCapabilities.Sampling sampling(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface RootCapabilitiesMixin { + + // @formatter:off + @JsonProperty("listChanged") Boolean listChanged(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public interface SamplingMixin { + + // @formatter:off + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ServerCapabilitiesMixin { + + // @formatter:off + @JsonProperty("completions") McpSchema.ServerCapabilities.CompletionCapabilities completions(); + @JsonProperty("experimental") Map experimental(); + @JsonProperty("logging") McpSchema.ServerCapabilities.LoggingCapabilities logging(); + @JsonProperty("prompts") McpSchema.ServerCapabilities.PromptCapabilities prompts(); + @JsonProperty("resources") McpSchema.ServerCapabilities.ResourceCapabilities resources(); + @JsonProperty("tools") McpSchema.ServerCapabilities.ToolCapabilities tools(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public interface CompletionCapabilitiesMixin { + + // @formatter:off + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public interface LoggingCapabilitiesMixin { + + // @formatter:off + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public interface PromptCapabilitiesMixin { + + // @formatter:off + @JsonProperty("listChanged") Boolean listChanged(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public interface ResourceCapabilitiesMixin { + + // @formatter:off + @JsonProperty("subscribe") Boolean subscribe(); + @JsonProperty("listChanged") Boolean listChanged(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public interface ToolCapabilitiesMixin { + + // @formatter:off + @JsonProperty("listChanged") Boolean listChanged(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ImplementationMixin { + + // @formatter:off + @JsonProperty("name") String name(); + @JsonProperty("version") String version(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface AnnotationsMixin { + + // @formatter:off + @JsonProperty("audience") List audience(); + @JsonProperty("priority") Double priority(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ResourceMixin { + + // @formatter:off + @JsonProperty("uri") String uri(); + @JsonProperty("name") String name(); + @JsonProperty("description") String description(); + @JsonProperty("mimeType") String mimeType(); + @JsonProperty("annotations") McpSchema.Annotations annotations(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ResourceTemplateMixin { + + // @formatter:off + @JsonProperty("uriTemplate") String uriTemplate(); + @JsonProperty("name") String name(); + @JsonProperty("description") String description(); + @JsonProperty("mimeType") String mimeType(); + @JsonProperty("annotations") McpSchema.Annotations annotations(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ListResourcesResultMixin { + + // @formatter:off + @JsonProperty("resources") List resources(); + @JsonProperty("nextCursor") String nextCursor(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ListResourceTemplatesResultMixin { + + // @formatter:off + @JsonProperty("resourceTemplates") List resourceTemplates(); + @JsonProperty("nextCursor") String nextCursor(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ReadResourceRequestMixin { + + // @formatter:off + @JsonProperty("uri") String uri(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ReadResourceResultMixin { + + // @formatter:off + @JsonProperty("contents") List contents(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface SubscribeRequestMixin { + + // @formatter:off + @JsonProperty("uri") String uri(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface UnsubscribeRequestMixin { + + // @formatter:off + @JsonProperty("uri") String uri(); + } // @formatter:on + + /** + * The contents of a specific resource or sub-resource. + */ + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, include = As.PROPERTY) + @JsonSubTypes({ @JsonSubTypes.Type(value = McpSchema.TextResourceContents.class, name = "text"), + @JsonSubTypes.Type(value = McpSchema.BlobResourceContents.class, name = "blob") }) + public interface ResourceContentsMixin { + + // @formatter:off + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface TextResourceContentsMixin{ // @formatter:off + @JsonProperty("uri") String uri(); + @JsonProperty("mimeType") String mimeType(); + @JsonProperty("text") String text(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface BlobResourceContentsMixin { + + // @formatter:off + @JsonProperty("uri") String uri(); + @JsonProperty("mimeType") String mimeType(); + @JsonProperty("blob") String blob(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface PromptMixin { + + // @formatter:off + @JsonProperty("name") String name(); + @JsonProperty("description") String description(); + @JsonProperty("arguments") List arguments(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface PromptArgumentMixin { + + // @formatter:off + @JsonProperty("name") String name(); + @JsonProperty("description") String description(); + @JsonProperty("required") Boolean required(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface PromptMessageMixin { + + // @formatter:off + @JsonProperty("role") McpSchema.Role role(); + @JsonProperty("content") McpSchema.Content content(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ListPromptsResultMixin { + + // @formatter:off + @JsonProperty("prompts") List prompts(); + @JsonProperty("nextCursor") String nextCursor(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface GetPromptRequestMixin {// @formatter:off + @JsonProperty("name") String name(); + @JsonProperty("arguments") Map arguments(); + }// @formatter:off + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface GetPromptResultMixin{ // @formatter:off + @JsonProperty("description") String description(); + @JsonProperty("messages") List messages(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ListToolsResultMixin { + + // @formatter:off + @JsonProperty("tools") List tools(); + @JsonProperty("nextCursor") String nextCursor(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface JsonSchemaMixin { + + // @formatter:off + @JsonProperty("type") String type(); + @JsonProperty("properties") Map properties(); + @JsonProperty("required") List required(); + @JsonProperty("additionalProperties") Boolean additionalProperties(); + @JsonProperty("$defs") Map defs(); + @JsonProperty("definitions") Map definitions(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ToolMixin { + + // @formatter:off + @JsonProperty("name") String name(); + @JsonProperty("description") String description(); + @JsonProperty("inputSchema") McpSchema.JsonSchema inputSchema(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface CallToolRequestMixin {// @formatter:off + @JsonProperty("name") String name(); + @JsonProperty("arguments") Map arguments(); + }// @formatter:off + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface CallToolResultMixin{ // @formatter:off + @JsonProperty("content") List content(); + @JsonProperty("isError") Boolean isError(); + } // @formatter:on + + // --------------------------- + // Sampling Interfaces + // --------------------------- + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ModelPreferencesMixin {// @formatter:off + @JsonProperty("hints") List hints(); + @JsonProperty("costPriority") Double costPriority(); + @JsonProperty("speedPriority") Double speedPriority(); + @JsonProperty("intelligencePriority") Double intelligencePriority(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ModelHintMixin {// @formatter:off + @JsonProperty("name") String name(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface SamplingMessageMixin {// @formatter:off + @JsonProperty("role") McpSchema.Role role(); + @JsonProperty("content") McpSchema.Content content(); + } // @formatter:on + + // Sampling and Message Creation + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface CreateMessageRequestMixin {// @formatter:off + @JsonProperty("messages") List messages(); + @JsonProperty("modelPreferences") McpSchema.ModelPreferences modelPreferences(); + @JsonProperty("systemPrompt") String systemPrompt(); + @JsonProperty("includeContext") McpSchema.CreateMessageRequest.ContextInclusionStrategy includeContext(); + @JsonProperty("temperature") Double temperature(); + @JsonProperty("maxTokens") int maxTokens(); + @JsonProperty("stopSequences") List stopSequences(); + @JsonProperty("metadata") Map metadata(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface CreateMessageResultMixin {// @formatter:off + @JsonProperty("role") McpSchema.Role role(); + @JsonProperty("content") McpSchema.Content content(); + @JsonProperty("model") String model(); + @JsonProperty("stopReason") McpSchema.CreateMessageResult.StopReason stopReason(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface PaginatedRequestMixin {// @formatter:off + @JsonProperty("cursor") String cursor(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface PaginatedResultMixin {// @formatter:off + @JsonProperty("nextCursor") String nextCursor(); + }// @formatter:on + + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ProgressNotificationMixin {// @formatter:off + @JsonProperty("progressToken") String progressToken(); + @JsonProperty("progress") double progress(); + @JsonProperty("total") Double total(); + }// @formatter:on + + @JsonIgnoreProperties(ignoreUnknown = true) + public interface LoggingMessageNotificationMixin {// @formatter:off + @JsonProperty("level") McpSchema.LoggingLevel level(); + @JsonProperty("logger") String logger(); + @JsonProperty("data") String data(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface SetLevelRequestMixin { + + // @formatter:off + @JsonProperty("level") McpSchema.LoggingLevel level(); + } + + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface PromptReferenceMixin{// @formatter:off + @JsonProperty("type") String type(); + @JsonProperty("name") String name(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ResourceReferenceMixin {// @formatter:off + @JsonProperty("type") String type(); + @JsonProperty("uri") String uri(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface CompleteRequestMixin {// @formatter:off + @JsonProperty("ref") McpSchema.CompleteReference ref(); + @JsonProperty("argument") McpSchema.CompleteRequest.CompleteArgument argument(); + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface CompleteArgumentMixin{ + @JsonProperty("name") String name(); + @JsonProperty("value") String value(); + }// @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface CompleteResultMixin { + + // @formatter:off + @JsonProperty("completion") McpSchema.CompleteResult.CompleteCompletion completion(); + } // @formatter:on + + public interface CompleteCompletionMixin {// @formatter:off + @JsonProperty("values") List values(); + @JsonProperty("total") Integer total(); + @JsonProperty("hasMore") Boolean hasMore(); + }// @formatter:on + + // --------------------------- + // Content Types + // --------------------------- + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @JsonSubTypes({ @JsonSubTypes.Type(value = McpSchema.TextContent.class, name = "text"), + @JsonSubTypes.Type(value = McpSchema.ImageContent.class, name = "image"), + @JsonSubTypes.Type(value = McpSchema.EmbeddedResource.class, name = "resource") }) + public interface ContentMixin { + + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface TextContentMixin { + + // @formatter:off + @JsonProperty("audience") List audience(); + @JsonProperty("priority") Double priority(); + @JsonProperty("text") String text(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ImageContentMixin { + + // @formatter:off + @JsonProperty("audience") List audience(); + @JsonProperty("priority") Double priority(); + @JsonProperty("data") String data(); + @JsonProperty("mimeType") String mimeType(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface EmbeddedResourceMixin { + + // @formatter:off + @JsonProperty("audience") List audience(); + @JsonProperty("priority") Double priority(); + @JsonProperty("resource") McpSchema.ResourceContents resource(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface RootMixin { + + // @formatter:off + @JsonProperty("uri") String uri(); + @JsonProperty("name") String name(); + } // @formatter:on + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public interface ListRootsResultMixin { + + // @formatter:off + @JsonProperty("roots") List roots(); + } // @formatter:on + +} diff --git a/mcp-spi/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-schema-jackson/src/test/java/io/modelcontextprotocol/schema/McpSchemaTests.java similarity index 83% rename from mcp-spi/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java rename to mcp-schema-jackson/src/test/java/io/modelcontextprotocol/schema/McpSchemaTests.java index d5b1ab68..68a1fcb4 100644 --- a/mcp-spi/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-schema-jackson/src/test/java/io/modelcontextprotocol/schema/McpSchemaTests.java @@ -1,7 +1,7 @@ /* * Copyright 2025 - 2025 the original author or authors. */ -package io.modelcontextprotocol.spec; +package io.modelcontextprotocol.schema; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; import static org.assertj.core.api.Assertions.*; @@ -12,13 +12,13 @@ import java.util.List; import java.util.Map; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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; +import io.modelcontextprotocol.schema.McpSchema.TextResourceContents; import net.javacrumbs.jsonunit.core.Option; /** @@ -26,14 +26,20 @@ */ class McpSchemaTests { - ObjectMapper mapper = new ObjectMapper(); + private static final TypeReference> MAP_TYPE_REF = new TypeReference<>() { + }; - // Content Types Tests + McpJacksonCodec mcpJacksonCodec; + + @BeforeEach + void setUp() { + mcpJacksonCodec = new McpJacksonCodec(); + } @Test void testTextContent() throws Exception { McpSchema.TextContent test = new McpSchema.TextContent("XXX"); - String value = mapper.writeValueAsString(test); + String value = mcpJacksonCodec.getMapper().writeValueAsString(test); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -44,7 +50,7 @@ void testTextContent() throws Exception { @Test void testTextContentDeserialization() throws Exception { - McpSchema.TextContent textContent = mapper.readValue(""" + McpSchema.TextContent textContent = mcpJacksonCodec.getMapper().readValue(""" {"type":"text","text":"XXX"}""", McpSchema.TextContent.class); assertThat(textContent).isNotNull(); @@ -55,17 +61,17 @@ void testTextContentDeserialization() throws Exception { @Test void testContentDeserializationWrongType() { - assertThatThrownBy(() -> mapper.readValue(""" + assertThatThrownBy(() -> mcpJacksonCodec.getMapper().readValue(""" {"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class)) .isInstanceOf(InvalidTypeIdException.class) .hasMessageContaining( - "Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [image, resource, text]"); + "Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.schema.McpSchema$TextContent`: known type ids = [image, resource, text]"); } @Test void testImageContent() throws Exception { McpSchema.ImageContent test = new McpSchema.ImageContent(null, null, "base64encodeddata", "image/png"); - String value = mapper.writeValueAsString(test); + String value = mcpJacksonCodec.getMapper().writeValueAsString(test); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -76,7 +82,7 @@ void testImageContent() throws Exception { @Test void testImageContentDeserialization() throws Exception { - McpSchema.ImageContent imageContent = mapper.readValue(""" + McpSchema.ImageContent imageContent = mcpJacksonCodec.getMapper().readValue(""" {"type":"image","data":"base64encodeddata","mimeType":"image/png"}""", McpSchema.ImageContent.class); assertThat(imageContent).isNotNull(); assertThat(imageContent.type()).isEqualTo("image"); @@ -91,7 +97,7 @@ void testEmbeddedResource() throws Exception { McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); - String value = mapper.writeValueAsString(test); + String value = mcpJacksonCodec.getMapper().writeValueAsString(test); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -102,10 +108,11 @@ void testEmbeddedResource() throws Exception { @Test void testEmbeddedResourceDeserialization() throws Exception { - McpSchema.EmbeddedResource embeddedResource = mapper.readValue( - """ - {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}""", - McpSchema.EmbeddedResource.class); + McpSchema.EmbeddedResource embeddedResource = mcpJacksonCodec.getMapper() + .readValue( + """ + {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}""", + McpSchema.EmbeddedResource.class); assertThat(embeddedResource).isNotNull(); assertThat(embeddedResource.type()).isEqualTo("resource"); assertThat(embeddedResource.resource()).isNotNull(); @@ -121,7 +128,7 @@ void testEmbeddedResourceWithBlobContents() throws Exception { McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); - String value = mapper.writeValueAsString(test); + String value = mcpJacksonCodec.getMapper().writeValueAsString(test); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -132,10 +139,11 @@ void testEmbeddedResourceWithBlobContents() throws Exception { @Test void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { - McpSchema.EmbeddedResource embeddedResource = mapper.readValue( - """ - {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}""", - McpSchema.EmbeddedResource.class); + McpSchema.EmbeddedResource embeddedResource = mcpJacksonCodec.getMapper() + .readValue( + """ + {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}""", + McpSchema.EmbeddedResource.class); assertThat(embeddedResource).isNotNull(); assertThat(embeddedResource.type()).isEqualTo("resource"); assertThat(embeddedResource.resource()).isNotNull(); @@ -155,7 +163,7 @@ void testJSONRPCRequest() throws Exception { McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "method_name", 1, params); - String value = mapper.writeValueAsString(request); + String value = mcpJacksonCodec.getMapper().writeValueAsString(request); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -171,7 +179,7 @@ void testJSONRPCNotification() throws Exception { McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, "notification_method", params); - String value = mapper.writeValueAsString(notification); + String value = mcpJacksonCodec.getMapper().writeValueAsString(notification); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -186,7 +194,7 @@ void testJSONRPCResponse() throws Exception { McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, result, null); - String value = mapper.writeValueAsString(response); + String value = mcpJacksonCodec.getMapper().writeValueAsString(response); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -201,7 +209,7 @@ void testJSONRPCResponseWithError() throws Exception { McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, null, error); - String value = mapper.writeValueAsString(response); + String value = mcpJacksonCodec.getMapper().writeValueAsString(response); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -222,7 +230,7 @@ void testInitializeRequest() throws Exception { McpSchema.InitializeRequest request = new McpSchema.InitializeRequest("2024-11-05", capabilities, clientInfo); - String value = mapper.writeValueAsString(request); + String value = mcpJacksonCodec.getMapper().writeValueAsString(request); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -245,7 +253,7 @@ void testInitializeResult() throws Exception { McpSchema.InitializeResult result = new McpSchema.InitializeResult("2024-11-05", capabilities, serverInfo, "Server initialized successfully"); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -264,7 +272,7 @@ void testResource() throws Exception { McpSchema.Resource resource = new McpSchema.Resource("resource://test", "Test Resource", "A test resource", "text/plain", annotations); - String value = mapper.writeValueAsString(resource); + String value = mcpJacksonCodec.getMapper().writeValueAsString(resource); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -280,7 +288,7 @@ void testResourceTemplate() throws Exception { McpSchema.ResourceTemplate template = new McpSchema.ResourceTemplate("resource://{param}/test", "Test Template", "A test resource template", "text/plain", annotations); - String value = mapper.writeValueAsString(template); + String value = mcpJacksonCodec.getMapper().writeValueAsString(template); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -300,7 +308,7 @@ void testListResourcesResult() throws Exception { McpSchema.ListResourcesResult result = new McpSchema.ListResourcesResult(Arrays.asList(resource1, resource2), "next-cursor"); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -320,7 +328,7 @@ void testListResourceTemplatesResult() throws Exception { McpSchema.ListResourceTemplatesResult result = new McpSchema.ListResourceTemplatesResult( Arrays.asList(template1, template2), "next-cursor"); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -333,7 +341,7 @@ void testListResourceTemplatesResult() throws Exception { void testReadResourceRequest() throws Exception { McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test"); - String value = mapper.writeValueAsString(request); + String value = mcpJacksonCodec.getMapper().writeValueAsString(request); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -351,7 +359,7 @@ void testReadResourceResult() throws Exception { McpSchema.ReadResourceResult result = new McpSchema.ReadResourceResult(Arrays.asList(contents1, contents2)); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -370,7 +378,7 @@ void testPrompt() throws Exception { McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "A test prompt", Arrays.asList(arg1, arg2)); - String value = mapper.writeValueAsString(prompt); + String value = mcpJacksonCodec.getMapper().writeValueAsString(prompt); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -385,7 +393,7 @@ void testPromptMessage() throws Exception { McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content); - String value = mapper.writeValueAsString(message); + String value = mcpJacksonCodec.getMapper().writeValueAsString(message); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -404,7 +412,7 @@ void testListPromptsResult() throws Exception { McpSchema.ListPromptsResult result = new McpSchema.ListPromptsResult(Arrays.asList(prompt1, prompt2), "next-cursor"); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -421,7 +429,7 @@ void testGetPromptRequest() throws Exception { McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("test-prompt", arguments); - assertThat(mapper.readValue(""" + assertThat(mcpJacksonCodec.getMapper().readValue(""" {"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}""", McpSchema.GetPromptRequest.class)) .isEqualTo(request); } @@ -438,7 +446,7 @@ void testGetPromptResult() throws Exception { McpSchema.GetPromptResult result = new McpSchema.GetPromptResult("A test prompt result", Arrays.asList(message1, message2)); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -478,16 +486,17 @@ void testJsonSchema() throws Exception { """; // Deserialize the original string to a JsonSchema object - McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class); + McpSchema.JsonSchema schema = mcpJacksonCodec.getMapper().readValue(schemaJson, McpSchema.JsonSchema.class); // Serialize the object back to a string - String serialized = mapper.writeValueAsString(schema); + String serialized = mcpJacksonCodec.getMapper().writeValueAsString(schema); // Deserialize again - McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class); + McpSchema.JsonSchema deserialized = mcpJacksonCodec.getMapper() + .readValue(serialized, McpSchema.JsonSchema.class); // Serialize one more time and compare with the first serialization - String serializedAgain = mapper.writeValueAsString(deserialized); + String serializedAgain = mcpJacksonCodec.getMapper().writeValueAsString(deserialized); // The two serialized strings should be the same assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized)); @@ -521,16 +530,17 @@ void testJsonSchemaWithDefinitions() throws Exception { """; // Deserialize the original string to a JsonSchema object - McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class); + McpSchema.JsonSchema schema = mcpJacksonCodec.getMapper().readValue(schemaJson, McpSchema.JsonSchema.class); // Serialize the object back to a string - String serialized = mapper.writeValueAsString(schema); + String serialized = mcpJacksonCodec.getMapper().writeValueAsString(schema); // Deserialize again - McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class); + McpSchema.JsonSchema deserialized = mcpJacksonCodec.getMapper() + .readValue(serialized, McpSchema.JsonSchema.class); // Serialize one more time and compare with the first serialization - String serializedAgain = mapper.writeValueAsString(deserialized); + String serializedAgain = mcpJacksonCodec.getMapper().writeValueAsString(deserialized); // The two serialized strings should be the same assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized)); @@ -552,10 +562,10 @@ void testTool() throws Exception { "required": ["name"] } """; + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper().readValue(schemaJson, McpSchema.JsonSchema.class); + McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", jsonSchema); - McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson); - - String value = mapper.writeValueAsString(tool); + String value = mcpJacksonCodec.getMapper().writeValueAsString(tool); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -587,16 +597,18 @@ void testToolWithComplexSchema() throws Exception { } """; - McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", complexSchemaJson); + McpSchema.JsonSchema schema = mcpJacksonCodec.getMapper() + .readValue(complexSchemaJson, McpSchema.JsonSchema.class); + McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", schema); // Serialize the tool to a string - String serialized = mapper.writeValueAsString(tool); + String serialized = mcpJacksonCodec.getMapper().writeValueAsString(tool); // Deserialize back to a Tool object - McpSchema.Tool deserializedTool = mapper.readValue(serialized, McpSchema.Tool.class); + McpSchema.Tool deserializedTool = mcpJacksonCodec.getMapper().readValue(serialized, McpSchema.Tool.class); // Serialize again and compare with first serialization - String serializedAgain = mapper.writeValueAsString(deserializedTool); + String serializedAgain = mcpJacksonCodec.getMapper().writeValueAsString(deserializedTool); // The two serialized strings should be the same assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized)); @@ -614,7 +626,7 @@ void testCallToolRequest() throws Exception { McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", arguments); - String value = mapper.writeValueAsString(request); + String value = mcpJacksonCodec.getMapper().writeValueAsString(request); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -625,15 +637,16 @@ void testCallToolRequest() throws Exception { @Test void testCallToolRequestJsonArguments() throws Exception { - - McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", """ + String textArguments = """ { "name": "test", "value": 42 } - """); + """; + Map arguments = mcpJacksonCodec.getMapper().readValue(textArguments, MAP_TYPE_REF); + McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", arguments); - String value = mapper.writeValueAsString(request); + String value = mcpJacksonCodec.getMapper().writeValueAsString(request); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -648,7 +661,7 @@ void testCallToolResult() throws Exception { McpSchema.CallToolResult result = new McpSchema.CallToolResult(Collections.singletonList(content), false); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -664,7 +677,7 @@ void testCallToolResultBuilder() throws Exception { .isError(false) .build(); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -684,7 +697,7 @@ void testCallToolResultBuilderWithMultipleContents() throws Exception { .isError(false) .build(); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -702,7 +715,7 @@ void testCallToolResultBuilderWithContentList() throws Exception { McpSchema.CallToolResult result = McpSchema.CallToolResult.builder().content(contents).isError(true).build(); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -719,7 +732,7 @@ void testCallToolResultBuilderWithErrorResult() throws Exception { .isError(true) .build(); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -737,8 +750,8 @@ void testCallToolResultStringConstructor() throws Exception { .isError(false) .build(); - String value1 = mapper.writeValueAsString(result1); - String value2 = mapper.writeValueAsString(result2); + String value1 = mcpJacksonCodec.getMapper().writeValueAsString(result1); + String value2 = mcpJacksonCodec.getMapper().writeValueAsString(result2); // Both should produce the same JSON assertThat(value1).isEqualTo(value2); @@ -776,7 +789,7 @@ void testCreateMessageRequest() throws Exception { .metadata(metadata) .build(); - String value = mapper.writeValueAsString(request); + String value = mcpJacksonCodec.getMapper().writeValueAsString(request); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -797,7 +810,7 @@ void testCreateMessageResult() throws Exception { .stopReason(McpSchema.CreateMessageResult.StopReason.END_TURN) .build(); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) @@ -813,7 +826,7 @@ void testCreateMessageResult() throws Exception { void testRoot() throws Exception { McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root"); - String value = mapper.writeValueAsString(root); + String value = mcpJacksonCodec.getMapper().writeValueAsString(root); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() @@ -829,7 +842,7 @@ void testListRootsResult() throws Exception { McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(Arrays.asList(root1, root2)); - String value = mapper.writeValueAsString(result); + String value = mcpJacksonCodec.getMapper().writeValueAsString(result); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) diff --git a/mcp-spi/pom.xml b/mcp-spi/pom.xml index c5ab73f3..fbef35ed 100644 --- a/mcp-spi/pom.xml +++ b/mcp-spi/pom.xml @@ -11,53 +11,22 @@ mcp-spi jar - Java MCP SDK SPI + Java SDK MCP SPI Java SDK SPI of the Model Context Protocol https://github.com/modelcontextprotocol/java-sdk - - 17 - 17 - UTF-8 - - - - org.slf4j - slf4j-api - ${slf4j-api.version} - - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - org.reactivestreams reactive-streams - + org.junit.jupiter junit-jupiter-api ${junit.version} test - - org.assertj - assertj-core - ${assert4j.version} - test - - - net.javacrumbs.json-unit - json-unit-assertj - ${json-unit-assertj.version} - test - - \ No newline at end of file diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpSchema.java similarity index 50% rename from mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSchema.java rename to mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpSchema.java index ce4a4fc1..be75d27e 100644 --- a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpSchema.java @@ -2,26 +2,12 @@ * Copyright 2024-2024 the original author or authors. */ -package io.modelcontextprotocol.spec; +package io.modelcontextprotocol.schema; -import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeInfo.As; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.util.Assert; /** @@ -31,11 +17,10 @@ * Context Protocol Schema. * * @author Christian Tzolov + * @author Aliaksei Darafeyeu */ public final class McpSchema { - private static final Logger logger = LoggerFactory.getLogger(McpSchema.class); - private McpSchema() { } @@ -96,8 +81,6 @@ private McpSchema() { // Sampling Methods public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - // --------------------------- // JSON-RPC Error Codes // --------------------------- @@ -142,40 +125,6 @@ public sealed interface Request } - private static final TypeReference> MAP_TYPE_REF = new TypeReference<>() { - }; - - /** - * Deserializes a JSON string into a JSONRPCMessage object. - * @param objectMapper The ObjectMapper instance to use for deserialization - * @param jsonText The JSON string to deserialize - * @return A JSONRPCMessage instance using either the {@link JSONRPCRequest}, - * {@link JSONRPCNotification}, or {@link JSONRPCResponse} classes. - * @throws IOException If there's an error during deserialization - * @throws IllegalArgumentException If the JSON structure doesn't match any known - * message type - */ - public static JSONRPCMessage deserializeJsonRpcMessage(ObjectMapper objectMapper, String jsonText) - throws IOException { - - logger.debug("Received JSON message: {}", jsonText); - - var map = objectMapper.readValue(jsonText, MAP_TYPE_REF); - - // Determine message type based on specific JSON structure - if (map.containsKey("method") && map.containsKey("id")) { - return objectMapper.convertValue(map, JSONRPCRequest.class); - } - else if (map.containsKey("method") && !map.containsKey("id")) { - return objectMapper.convertValue(map, JSONRPCNotification.class); - } - else if (map.containsKey("result") || map.containsKey("error")) { - return objectMapper.convertValue(map, JSONRPCResponse.class); - } - - throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + jsonText); - } - // --------------------------- // JSON-RPC Message Types // --------------------------- @@ -185,59 +134,29 @@ public sealed interface JSONRPCMessage permits JSONRPCRequest, JSONRPCNotificati } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record JSONRPCRequest( // @formatter:off - @JsonProperty("jsonrpc") String jsonrpc, - @JsonProperty("method") String method, - @JsonProperty("id") Object id, - @JsonProperty("params") Object params) implements JSONRPCMessage { - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record JSONRPCNotification( // @formatter:off - @JsonProperty("jsonrpc") String jsonrpc, - @JsonProperty("method") String method, - @JsonProperty("params") Object params) implements JSONRPCMessage { - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record JSONRPCResponse( // @formatter:off - @JsonProperty("jsonrpc") String jsonrpc, - @JsonProperty("id") Object id, - @JsonProperty("result") Object result, - @JsonProperty("error") JSONRPCError error) implements JSONRPCMessage { - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record JSONRPCError( - @JsonProperty("code") int code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public record JSONRPCRequest(String jsonrpc, String method, Object id, Object params) implements JSONRPCMessage { + } + + public record JSONRPCNotification(String jsonrpc, String method, Object params) implements JSONRPCMessage { + } + + public record JSONRPCResponse(String jsonrpc, Object id, Object result, + JSONRPCError error) implements JSONRPCMessage { + + public record JSONRPCError(int code, String message, Object data) { } - }// @formatter:on + } // --------------------------- // Initialization // --------------------------- - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record InitializeRequest( // @formatter:off - @JsonProperty("protocolVersion") String protocolVersion, - @JsonProperty("capabilities") ClientCapabilities capabilities, - @JsonProperty("clientInfo") Implementation clientInfo) implements Request { - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record InitializeResult( // @formatter:off - @JsonProperty("protocolVersion") String protocolVersion, - @JsonProperty("capabilities") ServerCapabilities capabilities, - @JsonProperty("serverInfo") Implementation serverInfo, - @JsonProperty("instructions") String instructions) { - } // @formatter:on + public record InitializeRequest(String protocolVersion, ClientCapabilities capabilities, + Implementation clientInfo) implements Request { + } + + public record InitializeResult(String protocolVersion, ServerCapabilities capabilities, Implementation serverInfo, + String instructions) { + } /** * Clients can implement additional features to enrich connected MCP servers with @@ -253,39 +172,31 @@ public record InitializeResult( // @formatter:off * (“completions” or “generations”) from language models via clients. * */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ClientCapabilities( // @formatter:off - @JsonProperty("experimental") Map experimental, - @JsonProperty("roots") RootCapabilities roots, - @JsonProperty("sampling") Sampling sampling) { + + public record ClientCapabilities(Map experimental, RootCapabilities roots, Sampling sampling) { /** * Roots define the boundaries of where servers can operate within the filesystem, * allowing them to understand which directories and files they have access to. - * Servers can request the list of roots from supporting clients and - * receive notifications when that list changes. + * Servers can request the list of roots from supporting clients and receive + * notifications when that list changes. * - * @param listChanged Whether the client would send notification about roots - * has changed since the last time the server checked. + * @param listChanged Whether the client would send notification about roots has + * changed since the last time the server checked. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record RootCapabilities( - @JsonProperty("listChanged") Boolean listChanged) { + + public record RootCapabilities(Boolean listChanged) { } /** - * Provides a standardized way for servers to request LLM - * sampling ("completions" or "generations") from language - * models via clients. This flow allows clients to maintain - * control over model access, selection, and permissions - * while enabling servers to leverage AI capabilities—with - * no server API keys necessary. Servers can request text or - * image-based interactions and optionally include context + * Provides a standardized way for servers to request LLM sampling ("completions" + * or "generations") from language models via clients. This flow allows clients to + * maintain control over model access, selection, and permissions while enabling + * servers to leverage AI capabilities—with no server API keys necessary. Servers + * can request text or image-based interactions and optionally include context * from MCP servers in their prompts. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record Sampling() { } @@ -294,8 +205,11 @@ public static Builder builder() { } public static class Builder { + private Map experimental; + private RootCapabilities roots; + private Sampling sampling; public Builder experimental(Map experimental) { @@ -316,41 +230,27 @@ public Builder sampling() { public ClientCapabilities build() { return new ClientCapabilities(experimental, roots, sampling); } + } - }// @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ServerCapabilities( // @formatter:off - @JsonProperty("completions") CompletionCapabilities completions, - @JsonProperty("experimental") Map experimental, - @JsonProperty("logging") LoggingCapabilities logging, - @JsonProperty("prompts") PromptCapabilities prompts, - @JsonProperty("resources") ResourceCapabilities resources, - @JsonProperty("tools") ToolCapabilities tools) { - - @JsonInclude(JsonInclude.Include.NON_ABSENT) + } + + public record ServerCapabilities(CompletionCapabilities completions, Map experimental, + LoggingCapabilities logging, PromptCapabilities prompts, ResourceCapabilities resources, + ToolCapabilities tools) { + public record CompletionCapabilities() { } - - @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record LoggingCapabilities() { } - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - public record PromptCapabilities( - @JsonProperty("listChanged") Boolean listChanged) { + + public record PromptCapabilities(Boolean listChanged) { } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - public record ResourceCapabilities( - @JsonProperty("subscribe") Boolean subscribe, - @JsonProperty("listChanged") Boolean listChanged) { + public record ResourceCapabilities(Boolean subscribe, Boolean listChanged) { } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - public record ToolCapabilities( - @JsonProperty("listChanged") Boolean listChanged) { + public record ToolCapabilities(Boolean listChanged) { } public static Builder builder() { @@ -360,10 +260,15 @@ public static Builder builder() { public static class Builder { private CompletionCapabilities completions; + private Map experimental; + private LoggingCapabilities logging = new LoggingCapabilities(); + private PromptCapabilities prompts; + private ResourceCapabilities resources; + private ToolCapabilities tools; public Builder completions() { @@ -399,22 +304,19 @@ public Builder tools(Boolean listChanged) { public ServerCapabilities build() { return new ServerCapabilities(completions, experimental, logging, prompts, resources, tools); } + } - } // @formatter:on + } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record Implementation(// @formatter:off - @JsonProperty("name") String name, - @JsonProperty("version") String version) { - } // @formatter:on + public record Implementation(String name, String version) { + } // Existing Enums and Base Types (from previous implementation) - public enum Role {// @formatter:off + public enum Role { + + USER, ASSISTANT - @JsonProperty("user") USER, - @JsonProperty("assistant") ASSISTANT - }// @formatter:on + } // --------------------------- // Resource Interfaces @@ -441,12 +343,9 @@ public interface Annotated { * required, while 0 means "least important," and indicates that the data is entirely * optional. It is a number between 0 and 1. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record Annotations( // @formatter:off - @JsonProperty("audience") List audience, - @JsonProperty("priority") Double priority) { - } // @formatter:on + + public record Annotations(List audience, Double priority) { + } /** * A known resource that the server is capable of reading. @@ -461,15 +360,10 @@ public record Annotations( // @formatter:off * @param annotations Optional annotations for the client. The client can use * annotations to inform how objects are used or displayed. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record Resource( // @formatter:off - @JsonProperty("uri") String uri, - @JsonProperty("name") String name, - @JsonProperty("description") String description, - @JsonProperty("mimeType") String mimeType, - @JsonProperty("annotations") Annotations annotations) implements Annotated { - } // @formatter:on + + public record Resource(String uri, String name, String description, String mimeType, + Annotations annotations) implements Annotated { + } /** * Resource templates allow servers to expose parameterized resources using URI @@ -487,41 +381,22 @@ public record Resource( // @formatter:off * annotations to inform how objects are used or displayed. * @see RFC 6570 */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ResourceTemplate( // @formatter:off - @JsonProperty("uriTemplate") String uriTemplate, - @JsonProperty("name") String name, - @JsonProperty("description") String description, - @JsonProperty("mimeType") String mimeType, - @JsonProperty("annotations") Annotations annotations) implements Annotated { - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ListResourcesResult( // @formatter:off - @JsonProperty("resources") List resources, - @JsonProperty("nextCursor") String nextCursor) { - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ListResourceTemplatesResult( // @formatter:off - @JsonProperty("resourceTemplates") List resourceTemplates, - @JsonProperty("nextCursor") String nextCursor) { - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ReadResourceRequest( // @formatter:off - @JsonProperty("uri") String uri){ - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ReadResourceResult( // @formatter:off - @JsonProperty("contents") List contents){ - } // @formatter:on + + public record ResourceTemplate(String uriTemplate, String name, String description, String mimeType, + Annotations annotations) implements Annotated { + } + + public record ListResourcesResult(List resources, String nextCursor) { + } + + public record ListResourceTemplatesResult(List resourceTemplates, String nextCursor) { + } + + public record ReadResourceRequest(String uri) { + } + + public record ReadResourceResult(List contents) { + } /** * Sent from the client to request resources/updated notifications from the server @@ -530,24 +405,16 @@ public record ReadResourceResult( // @formatter:off * @param uri the URI of the resource to subscribe to. The URI can use any protocol; * it is up to the server how to interpret it. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record SubscribeRequest( // @formatter:off - @JsonProperty("uri") String uri){ - } // @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record UnsubscribeRequest( // @formatter:off - @JsonProperty("uri") String uri){ - } // @formatter:on + + public record SubscribeRequest(String uri) { + } + + public record UnsubscribeRequest(String uri) { + } /** * The contents of a specific resource or sub-resource. */ - @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, include = As.PROPERTY) - @JsonSubTypes({ @JsonSubTypes.Type(value = TextResourceContents.class, name = "text"), - @JsonSubTypes.Type(value = BlobResourceContents.class, name = "blob") }) public sealed interface ResourceContents permits TextResourceContents, BlobResourceContents { /** @@ -572,13 +439,9 @@ public sealed interface ResourceContents permits TextResourceContents, BlobResou * @param text the text of the resource. This must only be set if the resource can * actually be represented as text (not binary data). */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record TextResourceContents( // @formatter:off - @JsonProperty("uri") String uri, - @JsonProperty("mimeType") String mimeType, - @JsonProperty("text") String text) implements ResourceContents { - } // @formatter:on + + public record TextResourceContents(String uri, String mimeType, String text) implements ResourceContents { + } /** * Binary contents of a resource. @@ -589,13 +452,9 @@ public record TextResourceContents( // @formatter:off * This must only be set if the resource can actually be represented as binary data * (not text). */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record BlobResourceContents( // @formatter:off - @JsonProperty("uri") String uri, - @JsonProperty("mimeType") String mimeType, - @JsonProperty("blob") String blob) implements ResourceContents { - } // @formatter:on + + public record BlobResourceContents(String uri, String mimeType, String blob) implements ResourceContents { + } // --------------------------- // Prompt Interfaces @@ -607,13 +466,9 @@ public record BlobResourceContents( // @formatter:off * @param description An optional description of what this prompt provides. * @param arguments A list of arguments to use for templating the prompt. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record Prompt( // @formatter:off - @JsonProperty("name") String name, - @JsonProperty("description") String description, - @JsonProperty("arguments") List arguments) { - } // @formatter:on + + public record Prompt(String name, String description, List arguments) { + } /** * Describes an argument that a prompt can accept. @@ -622,13 +477,9 @@ public record Prompt( // @formatter:off * @param description A human-readable description of the argument. * @param required Whether this argument must be provided. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record PromptArgument( // @formatter:off - @JsonProperty("name") String name, - @JsonProperty("description") String description, - @JsonProperty("required") Boolean required) { - }// @formatter:on + + public record PromptArgument(String name, String description, Boolean required) { + } /** * Describes a message returned as part of a prompt. @@ -639,12 +490,9 @@ public record PromptArgument( // @formatter:off * @param role The sender or recipient of messages and data in a conversation. * @param content The content of the message of type {@link Content}. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record PromptMessage( // @formatter:off - @JsonProperty("role") Role role, - @JsonProperty("content") Content content) { - } // @formatter:on + + public record PromptMessage(Role role, Content content) { + } /** * The server's response to a prompts/list request from the client. @@ -653,12 +501,9 @@ public record PromptMessage( // @formatter:off * @param nextCursor An optional cursor for pagination. If present, indicates there * are more prompts available. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ListPromptsResult( // @formatter:off - @JsonProperty("prompts") List prompts, - @JsonProperty("nextCursor") String nextCursor) { - }// @formatter:on + + public record ListPromptsResult(List prompts, String nextCursor) { + } /** * Used by the client to get a prompt provided by the server. @@ -666,12 +511,9 @@ public record ListPromptsResult( // @formatter:off * @param name The name of the prompt or prompt template. * @param arguments Arguments to use for templating the prompt. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record GetPromptRequest(// @formatter:off - @JsonProperty("name") String name, - @JsonProperty("arguments") Map arguments) implements Request { - }// @formatter:off + + public record GetPromptRequest(String name, Map arguments) implements Request { + } /** * The server's response to a prompts/get request from the client. @@ -679,12 +521,9 @@ public record GetPromptRequest(// @formatter:off * @param description An optional description for the prompt. * @param messages A list of messages to display as part of the prompt. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record GetPromptResult( // @formatter:off - @JsonProperty("description") String description, - @JsonProperty("messages") List messages) { - } // @formatter:on + + public record GetPromptResult(String description, List messages) { + } // --------------------------- // Tool Interfaces @@ -696,23 +535,13 @@ public record GetPromptResult( // @formatter:off * @param nextCursor An optional cursor for pagination. If present, indicates there * are more tools available. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ListToolsResult( // @formatter:off - @JsonProperty("tools") List tools, - @JsonProperty("nextCursor") String nextCursor) { - }// @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record JsonSchema( // @formatter:off - @JsonProperty("type") String type, - @JsonProperty("properties") Map properties, - @JsonProperty("required") List required, - @JsonProperty("additionalProperties") Boolean additionalProperties, - @JsonProperty("$defs") Map defs, - @JsonProperty("definitions") Map definitions) { - } // @formatter:on + + public record ListToolsResult(List tools, String nextCursor) { + } + + public record JsonSchema(String type, Map properties, List required, + Boolean additionalProperties, Map defs, Map definitions) { + } /** * Represents a tool that the server provides. Tools enable servers to expose @@ -727,26 +556,9 @@ public record JsonSchema( // @formatter:off * the arguments when calling this tool. This allows clients to validate tool * arguments before sending them to the server. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record Tool( // @formatter:off - @JsonProperty("name") String name, - @JsonProperty("description") String description, - @JsonProperty("inputSchema") JsonSchema inputSchema) { - - public Tool(String name, String description, String schema) { - this(name, description, parseSchema(schema)); - } - - } // @formatter:on - private static JsonSchema parseSchema(String schema) { - try { - return OBJECT_MAPPER.readValue(schema, JsonSchema.class); - } - catch (IOException e) { - throw new IllegalArgumentException("Invalid schema: " + schema, e); - } + public record Tool(String name, String description, JsonSchema inputSchema) { + } /** @@ -757,48 +569,29 @@ private static JsonSchema parseSchema(String schema) { * @param arguments Arguments to pass to the tool. These must conform to the tool's * input schema. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record CallToolRequest(// @formatter:off - @JsonProperty("name") String name, - @JsonProperty("arguments") Map arguments) implements Request { - - public CallToolRequest(String name, String jsonArguments) { - this(name, parseJsonArguments(jsonArguments)); - } - private static Map parseJsonArguments(String jsonArguments) { - try { - return OBJECT_MAPPER.readValue(jsonArguments, MAP_TYPE_REF); - } - catch (IOException e) { - throw new IllegalArgumentException("Invalid arguments: " + jsonArguments, e); - } - } - }// @formatter:off + public record CallToolRequest(String name, Map arguments) implements Request { + } /** * The server's response to a tools/call request from the client. * - * @param content A list of content items representing the tool's output. Each item can be text, an image, - * or an embedded resource. - * @param isError If true, indicates that the tool execution failed and the content contains error information. - * If false or absent, indicates successful execution. + * @param content A list of content items representing the tool's output. Each item + * can be text, an image, or an embedded resource. + * @param isError If true, indicates that the tool execution failed and the content + * contains error information. If false or absent, indicates successful execution. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record CallToolResult( // @formatter:off - @JsonProperty("content") List content, - @JsonProperty("isError") Boolean isError) { + + public record CallToolResult(List content, Boolean isError) { /** * Creates a new instance of {@link CallToolResult} with a string containing the * tool result. - * - * @param content The content of the tool result. This will be mapped to a one-sized list - * with a {@link TextContent} element. - * @param isError If true, indicates that the tool execution failed and the content contains error information. - * If false or absent, indicates successful execution. + * @param content The content of the tool result. This will be mapped to a + * one-sized list with a {@link TextContent} element. + * @param isError If true, indicates that the tool execution failed and the + * content contains error information. If false or absent, indicates successful + * execution. */ public CallToolResult(String content, Boolean isError) { this(List.of(new TextContent(content)), isError); @@ -816,7 +609,9 @@ public static Builder builder() { * Builder for {@link CallToolResult}. */ public static class Builder { + private List content = new ArrayList<>(); + private Boolean isError; /** @@ -837,9 +632,7 @@ public Builder content(List content) { */ public Builder textContent(List textContent) { Assert.notNull(textContent, "textContent must not be null"); - textContent.stream() - .map(TextContent::new) - .forEach(this.content::add); + textContent.stream().map(TextContent::new).forEach(this.content::add); return this; } @@ -885,111 +678,108 @@ public Builder isError(Boolean isError) { public CallToolResult build() { return new CallToolResult(content, isError); } + } - } // @formatter:on + } // --------------------------- // Sampling Interfaces // --------------------------- - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ModelPreferences(// @formatter:off - @JsonProperty("hints") List hints, - @JsonProperty("costPriority") Double costPriority, - @JsonProperty("speedPriority") Double speedPriority, - @JsonProperty("intelligencePriority") Double intelligencePriority) { - - public static Builder builder() { - return new Builder(); - } - public static class Builder { - private List hints; - private Double costPriority; - private Double speedPriority; - private Double intelligencePriority; + public record ModelPreferences(List hints, Double costPriority, Double speedPriority, + Double intelligencePriority) { - public Builder hints(List hints) { - this.hints = hints; - return this; + public static Builder builder() { + return new Builder(); } - public Builder addHint(String name) { - if (this.hints == null) { - this.hints = new ArrayList<>(); + public static class Builder { + + private List hints; + + private Double costPriority; + + private Double speedPriority; + + private Double intelligencePriority; + + public Builder hints(List hints) { + this.hints = hints; + return this; } - this.hints.add(new ModelHint(name)); - return this; - } - public Builder costPriority(Double costPriority) { - this.costPriority = costPriority; - return this; - } + public Builder addHint(String name) { + if (this.hints == null) { + this.hints = new ArrayList<>(); + } + this.hints.add(new ModelHint(name)); + return this; + } - public Builder speedPriority(Double speedPriority) { - this.speedPriority = speedPriority; - return this; - } + public Builder costPriority(Double costPriority) { + this.costPriority = costPriority; + return this; + } - public Builder intelligencePriority(Double intelligencePriority) { - this.intelligencePriority = intelligencePriority; - return this; - } + public Builder speedPriority(Double speedPriority) { + this.speedPriority = speedPriority; + return this; + } + + public Builder intelligencePriority(Double intelligencePriority) { + this.intelligencePriority = intelligencePriority; + return this; + } + + public ModelPreferences build() { + return new ModelPreferences(hints, costPriority, speedPriority, intelligencePriority); + } - public ModelPreferences build() { - return new ModelPreferences(hints, costPriority, speedPriority, intelligencePriority); } } -} // @formatter:on - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ModelHint(@JsonProperty("name") String name) { + public record ModelHint(String name) { public static ModelHint of(String name) { return new ModelHint(name); } } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record SamplingMessage(// @formatter:off - @JsonProperty("role") Role role, - @JsonProperty("content") Content content) { - } // @formatter:on + public record SamplingMessage(Role role, Content content) { + } // Sampling and Message Creation - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record CreateMessageRequest(// @formatter:off - @JsonProperty("messages") List messages, - @JsonProperty("modelPreferences") ModelPreferences modelPreferences, - @JsonProperty("systemPrompt") String systemPrompt, - @JsonProperty("includeContext") ContextInclusionStrategy includeContext, - @JsonProperty("temperature") Double temperature, - @JsonProperty("maxTokens") int maxTokens, - @JsonProperty("stopSequences") List stopSequences, - @JsonProperty("metadata") Map metadata) implements Request { + + public record CreateMessageRequest(List messages, ModelPreferences modelPreferences, + String systemPrompt, ContextInclusionStrategy includeContext, Double temperature, int maxTokens, + List stopSequences, Map metadata) implements Request { public enum ContextInclusionStrategy { - @JsonProperty("none") NONE, - @JsonProperty("thisServer") THIS_SERVER, - @JsonProperty("allServers") ALL_SERVERS + + NONE, THIS_SERVER, ALL_SERVERS + } - + public static Builder builder() { return new Builder(); } public static class Builder { + private List messages; + private ModelPreferences modelPreferences; + private String systemPrompt; + private ContextInclusionStrategy includeContext; + private Double temperature; + private int maxTokens; + private List stopSequences; + private Map metadata; public Builder messages(List messages) { @@ -1033,24 +823,19 @@ public Builder metadata(Map metadata) { } public CreateMessageRequest build() { - return new CreateMessageRequest(messages, modelPreferences, systemPrompt, - includeContext, temperature, maxTokens, stopSequences, metadata); + return new CreateMessageRequest(messages, modelPreferences, systemPrompt, includeContext, temperature, + maxTokens, stopSequences, metadata); } + } - }// @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record CreateMessageResult(// @formatter:off - @JsonProperty("role") Role role, - @JsonProperty("content") Content content, - @JsonProperty("model") String model, - @JsonProperty("stopReason") StopReason stopReason) { - + } + + public record CreateMessageResult(Role role, Content content, String model, StopReason stopReason) { + public enum StopReason { - @JsonProperty("endTurn") END_TURN, - @JsonProperty("stopSequence") STOP_SEQUENCE, - @JsonProperty("maxTokens") MAX_TOKENS + + END_TURN, STOP_SEQUENCE, MAX_TOKENS + } public static Builder builder() { @@ -1058,9 +843,13 @@ public static Builder builder() { } public static class Builder { + private Role role = Role.ASSISTANT; + private Content content; + private String model; + private StopReason stopReason = StopReason.END_TURN; public Builder role(Role role) { @@ -1091,31 +880,26 @@ public Builder message(String message) { public CreateMessageResult build() { return new CreateMessageResult(role, content, model, stopReason); } + } - }// @formatter:on + } // --------------------------- // Pagination Interfaces // --------------------------- - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record PaginatedRequest(@JsonProperty("cursor") String cursor) { + + public record PaginatedRequest(String cursor) { } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record PaginatedResult(@JsonProperty("nextCursor") String nextCursor) { + public record PaginatedResult(String nextCursor) { } // --------------------------- // Progress and Logging // --------------------------- - @JsonIgnoreProperties(ignoreUnknown = true) - public record ProgressNotification(// @formatter:off - @JsonProperty("progressToken") String progressToken, - @JsonProperty("progress") double progress, - @JsonProperty("total") Double total) { - }// @formatter:on + + public record ProgressNotification(String progressToken, double progress, Double total) { + } /** * The Model Context Protocol (MCP) provides a standardized way for servers to send @@ -1127,19 +911,19 @@ public record ProgressNotification(// @formatter:off * @param logger The logger that generated the message. * @param data JSON-serializable logging data. */ - @JsonIgnoreProperties(ignoreUnknown = true) - public record LoggingMessageNotification(// @formatter:off - @JsonProperty("level") LoggingLevel level, - @JsonProperty("logger") String logger, - @JsonProperty("data") String data) { + + public record LoggingMessageNotification(LoggingLevel level, String logger, String data) { public static Builder builder() { return new Builder(); } public static class Builder { + private LoggingLevel level = LoggingLevel.INFO; + private String logger = "server"; + private String data; public Builder level(LoggingLevel level) { @@ -1160,18 +944,13 @@ public Builder data(String data) { public LoggingMessageNotification build() { return new LoggingMessageNotification(level, logger, data); } + } - }// @formatter:on - - public enum LoggingLevel {// @formatter:off - @JsonProperty("debug") DEBUG(0), - @JsonProperty("info") INFO(1), - @JsonProperty("notice") NOTICE(2), - @JsonProperty("warning") WARNING(3), - @JsonProperty("error") ERROR(4), - @JsonProperty("critical") CRITICAL(5), - @JsonProperty("alert") ALERT(6), - @JsonProperty("emergency") EMERGENCY(7); + } + + public enum LoggingLevel { + + DEBUG(0), INFO(1), NOTICE(2), WARNING(3), ERROR(4), CRITICAL(5), ALERT(6), EMERGENCY(7); private final int level; @@ -1183,11 +962,9 @@ public int level() { return level; } - } // @formatter:on + } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record SetLevelRequest(@JsonProperty("level") LoggingLevel level) { + public record SetLevelRequest(LoggingLevel level) { } // --------------------------- @@ -1201,11 +978,7 @@ public sealed interface CompleteReference permits PromptReference, ResourceRefer } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record PromptReference(// @formatter:off - @JsonProperty("type") String type, - @JsonProperty("name") String name) implements CompleteReference { + public record PromptReference(String type, String name) implements CompleteReference { public PromptReference(String name) { this("ref/prompt", name); @@ -1215,13 +988,9 @@ public PromptReference(String name) { public String identifier() { return name(); } - }// @formatter:on + } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ResourceReference(// @formatter:off - @JsonProperty("type") String type, - @JsonProperty("uri") String uri) implements CompleteReference { + public record ResourceReference(String type, String uri) implements CompleteReference { public ResourceReference(String uri) { this("ref/resource", uri); @@ -1231,38 +1000,23 @@ public ResourceReference(String uri) { public String identifier() { return uri(); } - }// @formatter:on - - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record CompleteRequest(// @formatter:off - @JsonProperty("ref") McpSchema.CompleteReference ref, - @JsonProperty("argument") CompleteArgument argument) implements Request { - - public record CompleteArgument( - @JsonProperty("name") String name, - @JsonProperty("value") String value) { - }// @formatter:on } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record CompleteResult(@JsonProperty("completion") CompleteCompletion completion) { // @formatter:off - - public record CompleteCompletion( - @JsonProperty("values") List values, - @JsonProperty("total") Integer total, - @JsonProperty("hasMore") Boolean hasMore) { - }// @formatter:on + public record CompleteRequest(McpSchema.CompleteReference ref, CompleteArgument argument) implements Request { + + public record CompleteArgument(String name, String value) { + } + } + + public record CompleteResult(CompleteCompletion completion) { + + public record CompleteCompletion(List values, Integer total, Boolean hasMore) { + } } // --------------------------- // Content Types // --------------------------- - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") - @JsonSubTypes({ @JsonSubTypes.Type(value = TextContent.class, name = "text"), - @JsonSubTypes.Type(value = ImageContent.class, name = "image"), - @JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") }) public sealed interface Content permits TextContent, ImageContent, EmbeddedResource { default String type() { @@ -1280,33 +1034,17 @@ else if (this instanceof EmbeddedResource) { } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record TextContent( // @formatter:off - @JsonProperty("audience") List audience, - @JsonProperty("priority") Double priority, - @JsonProperty("text") String text) implements Content { // @formatter:on + public record TextContent(List audience, Double priority, String text) implements Content { public TextContent(String content) { this(null, null, content); } } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ImageContent( // @formatter:off - @JsonProperty("audience") List audience, - @JsonProperty("priority") Double priority, - @JsonProperty("data") String data, - @JsonProperty("mimeType") String mimeType) implements Content { // @formatter:on + public record ImageContent(List audience, Double priority, String data, String mimeType) implements Content { } - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record EmbeddedResource( // @formatter:off - @JsonProperty("audience") List audience, - @JsonProperty("priority") Double priority, - @JsonProperty("resource") ResourceContents resource) implements Content { // @formatter:on + public record EmbeddedResource(List audience, Double priority, ResourceContents resource) implements Content { } // --------------------------- @@ -1322,12 +1060,8 @@ public record EmbeddedResource( // @formatter:off * human-readable identifier for the root, which may be useful for display purposes or * for referencing the root in other parts of the application. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record Root( // @formatter:off - @JsonProperty("uri") String uri, - @JsonProperty("name") String name) { - } // @formatter:on + public record Root(String uri, String name) { + } /** * The client's response to a roots/list request from the server. This result contains @@ -1337,10 +1071,7 @@ public record Root( // @formatter:off * @param roots An array of Root objects, each representing a root directory or file * that the server can operate on. */ - @JsonInclude(JsonInclude.Include.NON_ABSENT) - @JsonIgnoreProperties(ignoreUnknown = true) - public record ListRootsResult( // @formatter:off - @JsonProperty("roots") List roots) { - } // @formatter:on + public record ListRootsResult(List roots) { + } } diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpSchemaCodec.java b/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpSchemaCodec.java new file mode 100644 index 00000000..81317ad7 --- /dev/null +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpSchemaCodec.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.schema; + +import java.io.IOException; + +/** + * Codec interface for encoding and decoding JSON-RPC messages. + * + * @author Aliaksei Darafeyeu + */ +public interface McpSchemaCodec { + + byte[] encode(McpSchema.JSONRPCMessage message); + + String encodeAsString(Object message) throws IOException; + + McpSchema.JSONRPCMessage decode(byte[] json); + + McpSchema.JSONRPCMessage decodeFromString(String jsonText) throws IOException; + + T decodeResult(Object rawResult, McpType type); + + T decodeBytes(byte[] bytes, McpType type); + +} diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpType.java b/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpType.java new file mode 100644 index 00000000..72951a35 --- /dev/null +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/schema/McpType.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.schema; + +import java.lang.reflect.Type; + +/** + * @author Aliaksei Darafeyeu + */ +public interface McpType { + + Class getRawClass(); + + default Type getGenericType() { + return getRawClass(); + } + + static McpType of(final Class raw) { + return () -> raw; + } + + static McpType of(final Type type) { + return new McpType<>() { + @Override + public Class getRawClass() { + if (type instanceof Class) { + return (Class) type; + } + throw new UnsupportedOperationException("Raw class not available for generic type: " + type); + } + + @Override + public Type getGenericType() { + return type; + } + }; + } + +} diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java index c9c3ddae..93583dc4 100644 --- a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java @@ -6,6 +6,8 @@ import java.util.function.Function; import org.reactivestreams.Publisher; +import io.modelcontextprotocol.schema.McpSchema; + /** * Marker interface for the client-side MCP transport. * diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpError.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpError.java index 13e43240..9a5e607b 100644 --- a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpError.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpError.java @@ -3,7 +3,7 @@ */ package io.modelcontextprotocol.spec; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.JSONRPCError; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCResponse.JSONRPCError; public class McpError extends RuntimeException { diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSession.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSession.java index 060e5091..e78ab0ee 100644 --- a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSession.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpSession.java @@ -6,7 +6,7 @@ import org.reactivestreams.Publisher; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.schema.McpType; /** * Represents a Model Control Protocol (MCP) session that handles communication between @@ -36,10 +36,10 @@ public interface McpSession { * @param the type of the expected response * @param method the name of the method to be called on the counterparty * @param requestParams the parameters to be sent with the request - * @param typeRef the TypeReference describing the expected response type + * @param typeRef the McpType describing the expected response type * @return a Mono that will emit the response when received */ - Publisher sendRequest(String method, Object requestParams, TypeReference typeRef); + Publisher sendRequest(String method, Object requestParams, McpType typeRef); /** * Sends a notification to the model client or server without parameters. diff --git a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpTransport.java b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpTransport.java index 5e838d5f..d10a40d8 100644 --- a/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpTransport.java +++ b/mcp-spi/src/main/java/io/modelcontextprotocol/spec/McpTransport.java @@ -6,7 +6,8 @@ import org.reactivestreams.Publisher; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpType; /** * Defines the asynchronous transport layer for the Model Context Protocol (MCP). @@ -75,6 +76,6 @@ public interface McpTransport { * @param typeRef the type reference for the object to unmarshal * @return the unmarshalled object */ - T unmarshalFrom(Object data, TypeReference typeRef); + T unmarshalFrom(Object data, McpType typeRef); } diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java index a78b18b8..52de5eb3 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java @@ -7,12 +7,14 @@ import java.util.function.BiConsumer; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchemaCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCMessage; import io.modelcontextprotocol.util.Assert; import org.reactivestreams.Publisher; @@ -99,7 +101,7 @@ public class WebFluxSseClientTransport implements McpClientTransport { * ObjectMapper for serializing outbound messages and deserializing inbound messages. * Handles conversion between JSON-RPC messages and their string representation. */ - protected ObjectMapper objectMapper; + protected McpSchemaCodec schemaCodec; /** * Subscription for the SSE connection handling inbound messages. Used for cleanup @@ -159,11 +161,25 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMappe */ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMapper objectMapper, String sseEndpoint) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this(webClientBuilder, new McpJacksonCodec(objectMapper), sseEndpoint); + } + + /** + * Constructs a new SseClientTransport with the specified WebClient builder and + * ObjectMapper. Initializes both inbound and outbound message processing pipelines. + * @param webClientBuilder the WebClient.Builder to use for creating the WebClient + * instance + * @param schemaCodec the McpSchemaCodec to use for JSON processing + * @param sseEndpoint the SSE endpoint URI to use for establishing the connection + * @throws IllegalArgumentException if either parameter is null + */ + public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpSchemaCodec schemaCodec, + String sseEndpoint) { + Assert.notNull(schemaCodec, "schemaCodec must not be null"); Assert.notNull(webClientBuilder, "WebClient.Builder must not be null"); Assert.hasText(sseEndpoint, "SSE endpoint must not be null or empty"); - this.objectMapper = objectMapper; + this.schemaCodec = schemaCodec; this.webClient = webClientBuilder.build(); this.sseEndpoint = sseEndpoint; } @@ -207,7 +223,7 @@ public Mono connect(Function, Publisher sendMessage(JSONRPCMessage message) { return Mono.empty(); } try { - String jsonText = this.objectMapper.writeValueAsString(message); + String jsonText = this.schemaCodec.encodeAsString(message); return webClient.post() .uri(messageEndpointUri) .contentType(MediaType.APPLICATION_JSON) @@ -341,13 +357,13 @@ public Mono closeGracefully() { // @formatter:off * type conversion capabilities to handle complex object structures. * @param the target type to convert the data into * @param data the source object to convert - * @param typeRef the TypeReference describing the target type + * @param typeRef the McpType describing the target type * @return the unmarshalled object of type T * @throws IllegalArgumentException if the conversion cannot be performed */ @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return schemaCodec.decodeResult(data, typeRef); } /** @@ -371,6 +387,8 @@ public static class Builder { private ObjectMapper objectMapper = new ObjectMapper(); + private McpSchemaCodec schemaCodec; + /** * Creates a new builder with the specified WebClient.Builder. * @param webClientBuilder the WebClient.Builder to use @@ -402,12 +420,26 @@ public Builder objectMapper(ObjectMapper objectMapper) { return this; } + /** + * Sets the schema codec for JSON serialization/deserialization. + * @param schemaCodec the McpSchemaCodec implementation + * @return this builder + */ + public Builder withSchemaCodec(final McpSchemaCodec schemaCodec) { + Assert.notNull(schemaCodec, "McpSchemaCodec must not be null"); + this.schemaCodec = schemaCodec; + return this; + } + /** * Builds a new {@link WebFluxSseClientTransport} instance. * @return a new transport instance */ public WebFluxSseClientTransport build() { - return new WebFluxSseClientTransport(webClientBuilder, objectMapper, sseEndpoint); + if (schemaCodec == null) { + schemaCodec = new McpJacksonCodec(objectMapper); + } + return new WebFluxSseClientTransport(webClientBuilder, schemaCodec, sseEndpoint); } } diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java index 17e42d0f..d07e8e41 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java @@ -3,10 +3,14 @@ import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchemaCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.session.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; @@ -85,7 +89,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv public static final String DEFAULT_BASE_URL = ""; - private final ObjectMapper objectMapper; + private final McpSchemaCodec schemaCodec; /** * Base URL for the message endpoint. This is used to construct the full URL for @@ -150,12 +154,27 @@ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messa */ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this(new McpJacksonCodec(objectMapper), baseUrl, messageEndpoint, sseEndpoint); + } + + /** + * Constructs a new WebFlux SSE server transport provider instance. + * @param schemaCodec The McpSchemaCodec to use for JSON serialization/deserialization + * of MCP messages. Must not be null. + * @param baseUrl webflux message base path + * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC + * messages. This endpoint will be communicated to clients during SSE connection + * setup. Must not be null. + * @throws IllegalArgumentException if either parameter is null + */ + WebFluxSseServerTransportProvider(McpSchemaCodec schemaCodec, String baseUrl, String messageEndpoint, + String sseEndpoint) { + Assert.notNull(schemaCodec, "schemaCodec must not be null"); Assert.notNull(baseUrl, "Message base path must not be null"); Assert.notNull(messageEndpoint, "Message endpoint must not be null"); Assert.notNull(sseEndpoint, "SSE endpoint must not be null"); - this.objectMapper = objectMapper; + this.schemaCodec = schemaCodec; this.baseUrl = baseUrl; this.messageEndpoint = messageEndpoint; this.sseEndpoint = sseEndpoint; @@ -319,7 +338,7 @@ private Mono handleMessage(ServerRequest request) { return request.bodyToMono(String.class).flatMap(body -> { try { - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = schemaCodec.decodeFromString(body); return session.handle(message).flatMap(response -> ServerResponse.ok().build()).onErrorResume(error -> { logger.error("Error processing message: {}", error.getMessage()); // TODO: instead of signalling the error, just respond with 200 OK @@ -348,7 +367,7 @@ public WebFluxMcpSessionTransport(FluxSink> sink) { public Mono sendMessage(McpSchema.JSONRPCMessage message) { return Mono.fromSupplier(() -> { try { - return objectMapper.writeValueAsString(message); + return schemaCodec.encodeAsString(message); } catch (IOException e) { throw Exceptions.propagate(e); @@ -367,8 +386,8 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return schemaCodec.decodeResult(data, typeRef); } @Override @@ -395,6 +414,8 @@ public static Builder builder() { */ public static class Builder { + private McpSchemaCodec schemaCodec; + private ObjectMapper objectMapper; private String baseUrl = DEFAULT_BASE_URL; @@ -416,6 +437,17 @@ public Builder objectMapper(ObjectMapper objectMapper) { return this; } + /** + * Sets the schema codec for JSON serialization/deserialization. + * @param schemaCodec the McpSchemaCodec implementation + * @return this builder + */ + public Builder withSchemaCodec(final McpSchemaCodec schemaCodec) { + Assert.notNull(schemaCodec, "McpSchemaCodec must not be null"); + this.schemaCodec = schemaCodec; + return this; + } + /** * Sets the project basePath as endpoint prefix where clients should send their * JSON-RPC messages @@ -462,8 +494,10 @@ public Builder sseEndpoint(String sseEndpoint) { public WebFluxSseServerTransportProvider build() { Assert.notNull(objectMapper, "ObjectMapper must be set"); Assert.notNull(messageEndpoint, "Message endpoint must be set"); - - return new WebFluxSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint); + if (schemaCodec == null) { + schemaCodec = new McpJacksonCodec(objectMapper); + } + return new WebFluxSseServerTransportProvider(schemaCodec, baseUrl, messageEndpoint, sseEndpoint); } } 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 2ba04746..c39a4f7f 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -18,15 +18,16 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; +import io.modelcontextprotocol.schema.McpJacksonCodec; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.TestUtil; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.*; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.CompletionCapabilities; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.*; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -58,6 +59,8 @@ class WebFluxSseIntegrationTests { private WebFluxSseServerTransportProvider mcpServerTransportProvider; + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(new ObjectMapper()); + ConcurrentHashMap clientBuilders = new ConcurrentHashMap<>(); @BeforeEach @@ -97,12 +100,14 @@ public void after() { // --------------------------------------- @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testCreateMessageWithoutSamplingCapabilities(String clientType) { + void testCreateMessageWithoutSamplingCapabilities(String clientType) throws Exception { var clientBuilder = clientBuilders.get(clientType); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> exchange.createMessage(mock(CreateMessageRequest.class)) .thenReturn(mock(CallToolResult.class))); @@ -126,7 +131,7 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testCreateMessageSuccess(String clientType) { + void testCreateMessageSuccess(String clientType) throws Exception { var clientBuilder = clientBuilders.get(clientType); @@ -143,8 +148,10 @@ void testCreateMessageSuccess(String clientType) { AtomicReference samplingResult = new AtomicReference<>(); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var createMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -194,7 +201,7 @@ void testCreateMessageSuccess(String clientType) { @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws InterruptedException { + void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws Exception { // Client var clientBuilder = clientBuilders.get(clientType); @@ -219,8 +226,10 @@ void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws Interr AtomicReference samplingResult = new AtomicReference<>(); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -272,7 +281,7 @@ void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws Interr @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testCreateMessageWithRequestTimeoutFail(String clientType) throws InterruptedException { + void testCreateMessageWithRequestTimeoutFail(String clientType) throws Exception { // Client var clientBuilder = clientBuilders.get(clientType); @@ -295,8 +304,10 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -381,12 +392,14 @@ void testRootsSuccess(String clientType) { @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testRootsWithoutCapability(String clientType) { + void testRootsWithoutCapability(String clientType) throws Exception { var clientBuilder = clientBuilders.get(clientType); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.SyncToolSpecification tool = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { exchange.listRoots(); // try to list roots @@ -519,13 +532,15 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testToolCallSuccess(String clientType) { + void testToolCallSuccess(String clientType) throws Exception { var clientBuilder = clientBuilders.get(clientType); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { // perform a blocking call to a remote service String response = RestClient.create() .get() @@ -559,13 +574,15 @@ void testToolCallSuccess(String clientType) { @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testToolListChangeHandlingSuccess(String clientType) { + void testToolListChangeHandlingSuccess(String clientType) throws Exception { var clientBuilder = clientBuilders.get(clientType); var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { // perform a blocking call to a remote service String response = RestClient.create() .get() @@ -616,8 +633,7 @@ void testToolListChangeHandlingSuccess(String clientType) { // Add a new tool McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), - (exchange, request) -> callResponse); + new McpSchema.Tool("tool2", "tool2 description", jsonSchema), (exchange, request) -> callResponse); mcpServer.addTool(tool2); @@ -651,16 +667,17 @@ void testInitialize(String clientType) { @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) - void testLoggingNotification(String clientType) { + void testLoggingNotification(String clientType) throws Exception { // Create a list to store received logging notifications List receivedNotifications = new ArrayList<>(); var clientBuilder = clientBuilders.get(clientType); // Create server with a tool that sends logging notifications + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema), - (exchange, request) -> { + new McpSchema.Tool("logging-test", "Test logging notifications", jsonSchema), (exchange, request) -> { // Create and send notifications with different levels diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransportTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransportTests.java index c757d3da..3f9542b7 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransportTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransportTests.java @@ -10,8 +10,8 @@ import java.util.function.Function; import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -127,7 +127,7 @@ void constructorValidation() { assertThatThrownBy(() -> new WebFluxSseClientTransport(webClientBuilder, null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("ObjectMapper must not be null"); + .hasMessageContaining("The ObjectMapper can not be null"); } @Test @@ -252,7 +252,7 @@ void testGracefulShutdown() { StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); // Message count should remain 0 after shutdown - assertThat(transport.getInboundMessageCount()).isEqualTo(0); + assertThat(transport.getInboundMessageCount()).isZero(); } @Test diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java index 7eb06717..635a234a 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java @@ -10,10 +10,13 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpSchemaCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.session.McpServerSession; @@ -87,7 +90,7 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi */ public static final String DEFAULT_SSE_ENDPOINT = "/sse"; - private final ObjectMapper objectMapper; + private final McpSchemaCodec schemaCodec; private final String messageEndpoint; @@ -151,12 +154,29 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messag */ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this(new McpJacksonCodec(objectMapper), baseUrl, messageEndpoint, sseEndpoint); + } + + /** + * Constructs a new WebMvcSseServerTransportProvider instance. + * @param schemaCodec The McpSchemaCodec to use for JSON serialization/deserialization + * of messages. + * @param baseUrl The base URL for the message endpoint, used to construct the full + * endpoint URL for clients. + * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC + * messages via HTTP POST. This endpoint will be communicated to clients through the + * SSE connection's initial endpoint event. + * @param sseEndpoint The endpoint URI where clients establish their SSE connections. + * @throws IllegalArgumentException if any parameter is null + */ + public WebMvcSseServerTransportProvider(McpSchemaCodec schemaCodec, String baseUrl, String messageEndpoint, + String sseEndpoint) { + Assert.notNull(schemaCodec, "schemaCodec must not be null"); Assert.notNull(baseUrl, "Message base URL must not be null"); Assert.notNull(messageEndpoint, "Message endpoint must not be null"); Assert.notNull(sseEndpoint, "SSE endpoint must not be null"); - this.objectMapper = objectMapper; + this.schemaCodec = schemaCodec; this.baseUrl = baseUrl; this.messageEndpoint = messageEndpoint; this.sseEndpoint = sseEndpoint; @@ -320,7 +340,7 @@ private ServerResponse handleMessage(ServerRequest request) { try { String body = request.body(String.class); - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = schemaCodec.decodeFromString(body); // Process the message through the session's handle method session.handle(message).block(); // Block for WebMVC compatibility @@ -367,7 +387,7 @@ private class WebMvcMcpSessionTransport implements McpServerTransport { public Mono sendMessage(McpSchema.JSONRPCMessage message) { return Mono.fromRunnable(() -> { try { - String jsonText = objectMapper.writeValueAsString(message); + String jsonText = schemaCodec.encodeAsString(message); sseBuilder.id(sessionId).event(MESSAGE_EVENT_TYPE).data(jsonText); logger.debug("Message sent to session {}", sessionId); } @@ -386,8 +406,8 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { * @param The target type */ @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return schemaCodec.decodeResult(data, typeRef); } /** diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java index 1b5218cc..7de66f93 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java @@ -7,7 +7,7 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; -import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.schema.McpSchema; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleState; import org.junit.jupiter.api.AfterEach; diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java index b12d6843..1e58c1e7 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java @@ -13,19 +13,20 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; +import io.modelcontextprotocol.schema.McpJacksonCodec; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; -import io.modelcontextprotocol.spec.McpSchema.ModelPreferences; -import io.modelcontextprotocol.spec.McpSchema.Role; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageRequest; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.schema.McpSchema.InitializeResult; +import io.modelcontextprotocol.schema.McpSchema.ModelPreferences; +import io.modelcontextprotocol.schema.McpSchema.Role; +import io.modelcontextprotocol.schema.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.schema.McpSchema.Tool; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleState; import org.junit.jupiter.api.AfterEach; @@ -52,6 +53,8 @@ class WebMvcSseIntegrationTests { private static final String MESSAGE_ENDPOINT = "/mcp/message"; + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(new ObjectMapper()); + private WebMvcSseServerTransportProvider mcpServerTransportProvider; McpClient.SyncSpec clientBuilder; @@ -117,10 +120,12 @@ public void after() { // Sampling Tests // --------------------------------------- @Test - void testCreateMessageWithoutSamplingCapabilities() { + void testCreateMessageWithoutSamplingCapabilities() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); @@ -153,7 +158,7 @@ void testCreateMessageWithoutSamplingCapabilities() { } @Test - void testCreateMessageSuccess() { + void testCreateMessageSuccess() throws Exception { Function samplingHandler = request -> { assertThat(request.messages()).hasSize(1); @@ -166,8 +171,10 @@ void testCreateMessageSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var createMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -215,7 +222,7 @@ void testCreateMessageSuccess() { } @Test - void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { + void testCreateMessageWithRequestTimeoutSuccess() throws Exception { // Client @@ -242,8 +249,10 @@ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -279,15 +288,14 @@ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); + assertThat(response).isNotNull().isEqualTo(callResponse); mcpClient.close(); mcpServer.close(); } @Test - void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { + void testCreateMessageWithRequestTimeoutFail() throws Exception { // Client @@ -310,12 +318,13 @@ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { .build(); // Server - CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, @@ -405,10 +414,12 @@ void testRootsSuccess() { } @Test - void testRootsWithoutCapability() { + void testRootsWithoutCapability() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.SyncToolSpecification tool = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { exchange.listRoots(); // try to list roots @@ -531,11 +542,13 @@ void testRootsServerCloseWithActiveSubscription() { """; @Test - void testToolCallSuccess() { + void testToolCallSuccess() throws Exception { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { // perform a blocking call to a remote service String response = RestClient.create() .get() @@ -567,11 +580,13 @@ void testToolCallSuccess() { } @Test - void testToolListChangeHandlingSuccess() { + void testToolListChangeHandlingSuccess() throws Exception { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + new McpSchema.Tool("tool1", "tool1 description", jsonSchema), (exchange, request) -> { // perform a blocking call to a remote service String response = RestClient.create() .get() @@ -622,8 +637,7 @@ void testToolListChangeHandlingSuccess() { // Add a new tool McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), - (exchange, request) -> callResponse); + new McpSchema.Tool("tool2", "tool2 description", jsonSchema), (exchange, request) -> callResponse); mcpServer.addTool(tool2); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java index d881e1ed..5bfb2d18 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java @@ -11,12 +11,14 @@ import org.reactivestreams.Publisher; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.schema.McpJacksonCodec; +import io.modelcontextprotocol.schema.McpType; import io.modelcontextprotocol.spec.McpClientTransport; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCNotification; +import io.modelcontextprotocol.schema.McpSchema.JSONRPCRequest; import io.modelcontextprotocol.spec.McpServerTransport; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -36,6 +38,8 @@ public class MockMcpTransport implements McpClientTransport, McpServerTransport private final BiConsumer interceptor; + private final McpJacksonCodec jacksonCodec = new McpJacksonCodec(new ObjectMapper()); + public MockMcpTransport() { this((t, msg) -> { }); @@ -101,8 +105,8 @@ public Mono closeGracefully() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return new ObjectMapper().convertValue(data, typeRef); + public T unmarshalFrom(Object data, McpType typeRef) { + return jacksonCodec.decodeResult(data, typeRef); } } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 5452c8ea..71f74e70 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -14,18 +14,18 @@ import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolRequest; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageRequest; +import io.modelcontextprotocol.schema.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.schema.McpSchema.GetPromptRequest; +import io.modelcontextprotocol.schema.McpSchema.Prompt; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema.SubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema.UnsubscribeRequest; import io.modelcontextprotocol.spec.McpTransport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -50,7 +50,7 @@ public abstract class AbstractMcpAsyncClientTests { private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!"; - abstract protected McpClientTransport createMcpTransport(); + protected abstract McpClientTransport createMcpTransport(); protected void onStart() { } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index 128441f8..7a3bfce8 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -13,20 +13,20 @@ import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.ListResourceTemplatesResult; -import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult; -import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; -import io.modelcontextprotocol.spec.McpSchema.TextContent; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolRequest; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.schema.McpSchema.ListResourceTemplatesResult; +import io.modelcontextprotocol.schema.McpSchema.ListResourcesResult; +import io.modelcontextprotocol.schema.McpSchema.ListToolsResult; +import io.modelcontextprotocol.schema.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.Root; +import io.modelcontextprotocol.schema.McpSchema.SubscribeRequest; +import io.modelcontextprotocol.schema.McpSchema.TextContent; +import io.modelcontextprotocol.schema.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema.UnsubscribeRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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 025cfeac..14e05d32 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -7,16 +7,17 @@ import java.time.Duration; import java.util.List; +import io.modelcontextprotocol.schema.McpJacksonCodec; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.GetPromptResult; +import io.modelcontextprotocol.schema.McpSchema.Prompt; +import io.modelcontextprotocol.schema.McpSchema.PromptMessage; +import io.modelcontextprotocol.schema.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.schema.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -28,6 +29,8 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Test suite for the {@link McpAsyncServer} that can be used with different * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations. @@ -43,6 +46,8 @@ public abstract class AbstractMcpAsyncServerTests { private static final String TEST_PROMPT_NAME = "test-prompt"; + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(new ObjectMapper()); + abstract protected McpServerTransportProvider createMcpTransportProvider(); protected void onStart() { @@ -102,8 +107,10 @@ void testImmediateClose() { """; @Test - void testAddTool() { - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + void testAddTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -117,8 +124,10 @@ void testAddTool() { } @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testAddDuplicateTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -138,8 +147,10 @@ void testAddDuplicateTool() { } @Test - void testRemoveTool() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testRemoveTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -167,8 +178,10 @@ void testRemoveNonexistentTool() { } @Test - void testNotifyToolsListChanged() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testNotifyToolsListChanged() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") 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 e313454b..bbe1bb01 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -6,16 +6,17 @@ import java.util.List; +import io.modelcontextprotocol.schema.McpJacksonCodec; import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; +import io.modelcontextprotocol.schema.McpSchema; +import io.modelcontextprotocol.schema.McpSchema.CallToolResult; +import io.modelcontextprotocol.schema.McpSchema.GetPromptResult; +import io.modelcontextprotocol.schema.McpSchema.Prompt; +import io.modelcontextprotocol.schema.McpSchema.PromptMessage; +import io.modelcontextprotocol.schema.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.schema.McpSchema.Resource; +import io.modelcontextprotocol.schema.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.schema.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -25,6 +26,8 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Test suite for the {@link McpSyncServer} that can be used with different * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations. @@ -40,6 +43,8 @@ public abstract class AbstractMcpSyncServerTests { private static final String TEST_PROMPT_NAME = "test-prompt"; + private final McpJacksonCodec mcpJacksonCodec = new McpJacksonCodec(new ObjectMapper()); + abstract protected McpServerTransportProvider createMcpTransportProvider(); protected void onStart() { @@ -109,13 +114,14 @@ void testGetAsyncServer() { """; @Test - void testAddTool() { + void testAddTool() throws Exception { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); - - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", jsonSchema); assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(newTool, (exchange, args) -> new CallToolResult(List.of(), false)))) .doesNotThrowAnyException(); @@ -124,8 +130,10 @@ void testAddTool() { } @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + void testAddDuplicateTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", jsonSchema); var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -142,8 +150,10 @@ void testAddDuplicateTool() { } @Test - void testRemoveTool() { - Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); + void testRemoveTool() throws Exception { + McpSchema.JsonSchema jsonSchema = mcpJacksonCodec.getMapper() + .readValue(emptyJsonSchema, McpSchema.JsonSchema.class); + Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", jsonSchema); var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") diff --git a/pom.xml b/pom.xml index 6197f40a..a941c0b8 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,7 @@ mcp-bom mcp-spi + mcp-schema-jackson mcp-reactor mcp-spring/mcp-spring-webflux mcp-spring/mcp-spring-webmvc