From 13960841940243fcee8e2077d9696e3b95dcb704 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 10 Sep 2025 12:26:17 +0200 Subject: [PATCH 1/7] Introduce McpJsonMapper interface to decouple from Jackson --- .../WebClientStreamableHttpTransport.java | 44 ++++-- .../transport/WebFluxSseClientTransport.java | 59 +++++--- .../WebFluxSseServerTransportProvider.java | 6 +- .../WebFluxStatelessServerTransport.java | 50 ++++-- ...FluxStreamableServerTransportProvider.java | 8 +- .../WebFluxSseClientTransportTests.java | 3 +- .../WebMvcSseServerTransportProvider.java | 6 +- ...bMvcStreamableServerTransportProvider.java | 6 +- .../MockMcpTransport.java | 8 +- mcp/pom.xml | 9 +- .../client/McpAsyncClient.java | 37 +++-- .../HttpClientSseClientTransport.java | 60 +++++--- .../HttpClientStreamableHttpTransport.java | 44 ++++-- .../transport/StdioClientTransport.java | 37 +++-- .../server/McpAsyncServer.java | 36 +++-- .../server/McpAsyncServerExchange.java | 10 +- .../server/McpServer.java | 141 +++++++++++------ .../server/McpStatelessAsyncServer.java | 25 ++- ...HttpServletSseServerTransportProvider.java | 60 +++++--- .../HttpServletStatelessServerTransport.java | 42 ++++-- ...vletStreamableServerTransportProvider.java | 60 +++++--- .../StdioServerTransportProvider.java | 30 ++-- .../spec/DefaultJsonSchemaValidator.java | 20 +++ .../spec/McpClientSession.java | 4 +- .../modelcontextprotocol/spec/McpSchema.java | 47 ++++-- .../spec/McpServerSession.java | 6 +- .../modelcontextprotocol/spec/McpSession.java | 4 +- .../spec/McpStreamableServerSession.java | 6 +- .../spec/McpTransport.java | 4 +- .../spec/MissingMcpTransportSession.java | 4 +- .../spec/json/McpJsonMapper.java | 86 +++++++++++ .../spec/json/TypeRef.java | 26 ++++ .../json/jackson/JacksonMcpJsonMapper.java | 72 +++++++++ .../util/KeepAliveScheduler.java | 4 +- .../MockMcpClientTransport.java | 8 +- .../MockMcpServerTransport.java | 8 +- .../McpAsyncClientResponseHandlerTests.java | 10 +- .../client/McpAsyncClientTests.java | 11 +- .../HttpClientSseClientTransportTests.java | 6 +- .../server/McpAsyncServerExchangeTests.java | 60 ++++---- .../server/McpSyncServerExchangeTests.java | 60 ++++---- .../StdioServerTransportProviderTests.java | 10 +- .../spec/McpClientSessionTests.java | 4 +- .../spec/json/gson/GsonMcpJsonMapper.java | 97 ++++++++++++ .../json/gson/GsonMcpJsonMapperTests.java | 142 ++++++++++++++++++ .../util/KeepAliveSchedulerTests.java | 4 +- 46 files changed, 1078 insertions(+), 406 deletions(-) create mode 100644 mcp/src/main/java/io/modelcontextprotocol/spec/json/McpJsonMapper.java create mode 100644 mcp/src/main/java/io/modelcontextprotocol/spec/json/TypeRef.java create mode 100644 mcp/src/main/java/io/modelcontextprotocol/spec/json/jackson/JacksonMcpJsonMapper.java create mode 100644 mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java create mode 100644 mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java index 853aed2bf..aec9caea7 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java @@ -22,8 +22,9 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.DefaultMcpTransportSession; import io.modelcontextprotocol.spec.DefaultMcpTransportStream; @@ -88,7 +89,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport { private static final ParameterizedTypeReference> PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference<>() { }; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final WebClient webClient; @@ -104,9 +105,9 @@ public class WebClientStreamableHttpTransport implements McpClientTransport { private final AtomicReference> exceptionHandler = new AtomicReference<>(); - private WebClientStreamableHttpTransport(ObjectMapper objectMapper, WebClient.Builder webClientBuilder, + private WebClientStreamableHttpTransport(McpJsonMapper jsonMapper, WebClient.Builder webClientBuilder, String endpoint, boolean resumableStreams, boolean openConnectionOnStartup) { - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.webClient = webClientBuilder.build(); this.endpoint = endpoint; this.resumableStreams = resumableStreams; @@ -366,8 +367,7 @@ private Flux extractError(ClientResponse response, Str McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = null; Exception toPropagate; try { - McpSchema.JSONRPCResponse jsonRpcResponse = objectMapper.readValue(body, - McpSchema.JSONRPCResponse.class); + McpSchema.JSONRPCResponse jsonRpcResponse = jsonMapper.readValue(body, McpSchema.JSONRPCResponse.class); jsonRpcError = jsonRpcResponse.error(); toPropagate = jsonRpcError != null ? new McpError(jsonRpcError) : new McpTransportException("Can't parse the jsonResponse " + jsonRpcResponse); @@ -427,7 +427,7 @@ private Flux directResponseFlux(McpSchema.JSONRPCMessa s.complete(); } else { - McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(objectMapper, + McpSchema.JSONRPCMessage jsonRpcResponse = McpSchema.deserializeJsonRpcMessage(jsonMapper, responseMessage); s.next(List.of(jsonRpcResponse)); } @@ -447,8 +447,8 @@ private Flux newEventStream(ClientResponse response, S } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return this.jsonMapper.convertValue(data, typeRef); } private Tuple2, Iterable> parse(ServerSentEvent event) { @@ -456,7 +456,7 @@ private Tuple2, Iterable> parse(Serve try { // We don't support batching ATM and probably won't since the next version // considers removing it. - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, event.data()); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.jsonMapper, event.data()); return Tuples.of(Optional.ofNullable(event.id()), List.of(message)); } catch (IOException ioException) { @@ -474,7 +474,7 @@ private Tuple2, Iterable> parse(Serve */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private WebClient.Builder webClientBuilder; @@ -494,9 +494,20 @@ private Builder(WebClient.Builder webClientBuilder) { * @param objectMapper instance to use * @return the builder instance */ - public Builder objectMapper(ObjectMapper objectMapper) { + public Builder objectMapper(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Configure the {@link McpJsonMapper} to use. + * @param jsonMapper instance to use + * @return the builder instance + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -555,9 +566,10 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) { * @return a new instance of {@link WebClientStreamableHttpTransport} */ public WebClientStreamableHttpTransport build() { - ObjectMapper objectMapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); + McpJsonMapper jsonMapper = this.jsonMapper != null ? this.jsonMapper + : new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); - return new WebClientStreamableHttpTransport(objectMapper, this.webClientBuilder, endpoint, resumableStreams, + return new WebClientStreamableHttpTransport(jsonMapper, this.webClientBuilder, endpoint, resumableStreams, openConnectionOnStartup); } 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 51d21d18b..79ae26469 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 @@ -9,8 +9,9 @@ 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.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.HttpHeaders; import io.modelcontextprotocol.spec.McpClientTransport; @@ -100,10 +101,10 @@ public class WebFluxSseClientTransport implements McpClientTransport { private final WebClient webClient; /** - * ObjectMapper for serializing outbound messages and deserializing inbound messages. + * JSON mapper for serializing outbound messages and deserializing inbound messages. * Handles conversion between JSON-RPC messages and their string representation. */ - protected ObjectMapper objectMapper; + protected McpJsonMapper jsonMapper; /** * Subscription for the SSE connection handling inbound messages. Used for cleanup @@ -137,7 +138,7 @@ public class WebFluxSseClientTransport implements McpClientTransport { * @throws IllegalArgumentException if webClientBuilder is null */ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder) { - this(webClientBuilder, new ObjectMapper()); + this(webClientBuilder, new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper())); } /** @@ -145,11 +146,11 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder) { * ObjectMapper. Initializes both inbound and outbound message processing pipelines. * @param webClientBuilder the WebClient.Builder to use for creating the WebClient * instance - * @param objectMapper the ObjectMapper to use for JSON processing + * @param jsonMapper the ObjectMapper to use for JSON processing * @throws IllegalArgumentException if either parameter is null */ - public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMapper objectMapper) { - this(webClientBuilder, objectMapper, DEFAULT_SSE_ENDPOINT); + public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper) { + this(webClientBuilder, jsonMapper, DEFAULT_SSE_ENDPOINT); } /** @@ -157,17 +158,16 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMappe * ObjectMapper. Initializes both inbound and outbound message processing pipelines. * @param webClientBuilder the WebClient.Builder to use for creating the WebClient * instance - * @param objectMapper the ObjectMapper to use for JSON processing + * @param jsonMapper the ObjectMapper 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, ObjectMapper objectMapper, - String sseEndpoint) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper, String sseEndpoint) { + Assert.notNull(jsonMapper, "jsonMapper 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.jsonMapper = jsonMapper; this.webClient = webClientBuilder.build(); this.sseEndpoint = sseEndpoint; } @@ -217,7 +217,7 @@ public Mono connect(Function, Mono> h } else if (MESSAGE_EVENT_TYPE.equals(event.event())) { try { - JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, event.data()); + JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.jsonMapper, event.data()); s.next(message); } catch (IOException ioException) { @@ -255,7 +255,7 @@ public Mono sendMessage(JSONRPCMessage message) { return Mono.empty(); } try { - String jsonText = this.objectMapper.writeValueAsString(message); + String jsonText = this.jsonMapper.writeValueAsString(message); return webClient.post() .uri(messageEndpointUri) .contentType(MediaType.APPLICATION_JSON) @@ -349,13 +349,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 TypeRef 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, TypeRef typeRef) { + return this.jsonMapper.convertValue(data, typeRef); } /** @@ -377,7 +377,7 @@ public static class Builder { private String sseEndpoint = DEFAULT_SSE_ENDPOINT; - private ObjectMapper objectMapper = new ObjectMapper(); + private McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); /** * Creates a new builder with the specified WebClient.Builder. @@ -401,12 +401,25 @@ public Builder sseEndpoint(String sseEndpoint) { /** * Sets the object mapper for JSON serialization/deserialization. - * @param objectMapper the object mapper + * @param objectMapper the Jackson ObjectMapper * @return this builder + * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead */ - public Builder objectMapper(ObjectMapper objectMapper) { + @Deprecated(forRemoval = true) + public Builder objectMapper(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { Assert.notNull(objectMapper, "objectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JSON mapper for serialization/deserialization. + * @param jsonMapper the JsonMapper to use + * @return this builder + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "jsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -415,7 +428,7 @@ public Builder objectMapper(ObjectMapper objectMapper) { * @return a new transport instance */ public WebFluxSseClientTransport build() { - return new WebFluxSseClientTransport(webClientBuilder, objectMapper, sseEndpoint); + return new WebFluxSseClientTransport(webClientBuilder, jsonMapper, 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 f64346265..8ff443ddb 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 @@ -11,6 +11,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -452,8 +454,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, TypeRef typeRef) { + return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); } @Override diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java index 1f3d4c3bf..62f3c7254 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java @@ -5,6 +5,8 @@ package io.modelcontextprotocol.server.transport; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpStatelessServerHandler; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -34,7 +36,7 @@ public class WebFluxStatelessServerTransport implements McpStatelessServerTransp private static final Logger logger = LoggerFactory.getLogger(WebFluxStatelessServerTransport.class); - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final String mcpEndpoint; @@ -46,13 +48,13 @@ public class WebFluxStatelessServerTransport implements McpStatelessServerTransp private volatile boolean isClosing = false; - private WebFluxStatelessServerTransport(ObjectMapper objectMapper, String mcpEndpoint, + private WebFluxStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint, McpTransportContextExtractor contextExtractor) { - Assert.notNull(objectMapper, "objectMapper must not be null"); + Assert.notNull(jsonMapper, "jsonMapper must not be null"); Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null"); Assert.notNull(contextExtractor, "contextExtractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.mcpEndpoint = mcpEndpoint; this.contextExtractor = contextExtractor; this.routerFunction = RouterFunctions.route() @@ -106,13 +108,20 @@ private Mono handlePost(ServerRequest request) { return request.bodyToMono(String.class).flatMap(body -> { try { - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body); if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { - return this.mcpHandler.handleRequest(transportContext, jsonrpcRequest) - .flatMap(jsonrpcResponse -> ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(jsonrpcResponse)); + return this.mcpHandler.handleRequest(transportContext, jsonrpcRequest).flatMap(jsonrpcResponse -> { + try { + String json = jsonMapper.writeValueAsString(jsonrpcResponse); + return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(json); + } + catch (IOException e) { + logger.error("Failed to serialize response: {}", e.getMessage()); + return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) + .bodyValue(new McpError("Failed to serialize response")); + } + }); } else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) { return this.mcpHandler.handleNotification(transportContext, jsonrpcNotification) @@ -146,7 +155,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -166,7 +175,20 @@ private Builder() { */ public Builder objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JsonMapper to use for JSON serialization/deserialization of MCP + * messages. + * @param jsonMapper The JsonMapper instance. Must not be null. + * @return this builder instance + * @throws IllegalArgumentException if jsonMapper is null + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -205,10 +227,12 @@ public Builder contextExtractor(McpTransportContextExtractor cont * @throws IllegalStateException if required parameters are not set */ public WebFluxStatelessServerTransport build() { - Assert.notNull(objectMapper, "ObjectMapper must be set"); + if (this.jsonMapper == null) { + throw new IllegalStateException("JsonMapper must be set"); + } Assert.notNull(mcpEndpoint, "Message endpoint must be set"); - return new WebFluxStatelessServerTransport(objectMapper, mcpEndpoint, contextExtractor); + return new WebFluxStatelessServerTransport(jsonMapper, mcpEndpoint, contextExtractor); } } diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java index 44d89eaeb..273539127 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java @@ -6,6 +6,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; import io.modelcontextprotocol.spec.HttpHeaders; @@ -369,8 +371,8 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); } @Override @@ -491,4 +493,4 @@ public WebFluxStreamableServerTransportProvider build() { } -} \ No newline at end of file +} 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 06c95d145..ebe423f41 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 @@ -11,6 +11,7 @@ import java.util.function.Function; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; import org.junit.jupiter.api.AfterAll; @@ -64,7 +65,7 @@ static class TestSseClientTransport extends WebFluxSseClientTransport { private Sinks.Many> events = Sinks.many().unicast().onBackpressureBuffer(); public TestSseClientTransport(WebClient.Builder webClientBuilder, ObjectMapper objectMapper) { - super(webClientBuilder, objectMapper); + super(webClientBuilder, new JacksonMcpJsonMapper(objectMapper)); } @Override 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 85373b6fe..f32e43304 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 @@ -13,6 +13,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -478,8 +480,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, TypeRef typeRef) { + return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); } /** diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java index 3cc104dd4..aaa03a1cf 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -546,8 +548,8 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId * @param The target type */ @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); } /** diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java index 5484a63c2..6b49f6b7b 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java @@ -9,8 +9,8 @@ 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.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; @@ -93,8 +93,8 @@ public Mono closeGracefully() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return new ObjectMapper().convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()).convertValue(data, typeRef); } } diff --git a/mcp/pom.xml b/mcp/pom.xml index dc85a419e..6cea79f62 100644 --- a/mcp/pom.xml +++ b/mcp/pom.xml @@ -216,8 +216,15 @@ test + + + com.google.code.gson + gson + 2.10.1 + test + - \ No newline at end of file + diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index eb6d42f68..047331d11 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -18,11 +18,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.spec.McpClientSession; 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; @@ -85,25 +84,25 @@ public class McpAsyncClient { private static final Logger logger = LoggerFactory.getLogger(McpAsyncClient.class); - private static final TypeReference VOID_TYPE_REFERENCE = new TypeReference<>() { + private static final TypeRef VOID_TYPE_REFERENCE = new TypeRef<>() { }; - public static final TypeReference OBJECT_TYPE_REF = new TypeReference<>() { + public static final TypeRef OBJECT_TYPE_REF = new TypeRef<>() { }; - public static final TypeReference PAGINATED_REQUEST_TYPE_REF = new TypeReference<>() { + public static final TypeRef PAGINATED_REQUEST_TYPE_REF = new TypeRef<>() { }; - public static final TypeReference INITIALIZE_RESULT_TYPE_REF = new TypeReference<>() { + public static final TypeRef INITIALIZE_RESULT_TYPE_REF = new TypeRef<>() { }; - public static final TypeReference CREATE_MESSAGE_REQUEST_TYPE_REF = new TypeReference<>() { + public static final TypeRef CREATE_MESSAGE_REQUEST_TYPE_REF = new TypeRef<>() { }; - public static final TypeReference LOGGING_MESSAGE_NOTIFICATION_TYPE_REF = new TypeReference<>() { + public static final TypeRef LOGGING_MESSAGE_NOTIFICATION_TYPE_REF = new TypeRef<>() { }; - public static final TypeReference PROGRESS_NOTIFICATION_TYPE_REF = new TypeReference<>() { + public static final TypeRef PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() { }; /** @@ -512,7 +511,7 @@ private RequestHandler samplingCreateMessageHandler() { // -------------------------- private RequestHandler elicitationCreateHandler() { return params -> { - ElicitRequest request = transport.unmarshalFrom(params, new TypeReference<>() { + ElicitRequest request = transport.unmarshalFrom(params, new TypeRef<>() { }); return this.elicitationHandler.apply(request); @@ -522,10 +521,10 @@ private RequestHandler elicitationCreateHandler() { // -------------------------- // Tools // -------------------------- - private static final TypeReference CALL_TOOL_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef CALL_TOOL_RESULT_TYPE_REF = new TypeRef<>() { }; - private static final TypeReference LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef LIST_TOOLS_RESULT_TYPE_REF = new TypeRef<>() { }; /** @@ -596,13 +595,13 @@ private NotificationHandler asyncToolsChangeNotificationHandler( // Resources // -------------------------- - private static final TypeReference LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef LIST_RESOURCES_RESULT_TYPE_REF = new TypeRef<>() { }; - private static final TypeReference READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef READ_RESOURCE_RESULT_TYPE_REF = new TypeRef<>() { }; - private static final TypeReference LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeRef<>() { }; /** @@ -756,7 +755,7 @@ private NotificationHandler asyncResourcesUpdatedNotificationHandler( List, Mono>> resourcesUpdateConsumers) { return params -> { McpSchema.ResourcesUpdatedNotification resourcesUpdatedNotification = transport.unmarshalFrom(params, - new TypeReference<>() { + new TypeRef<>() { }); return readResource(new McpSchema.ReadResourceRequest(resourcesUpdatedNotification.uri())) @@ -773,10 +772,10 @@ private NotificationHandler asyncResourcesUpdatedNotificationHandler( // -------------------------- // Prompts // -------------------------- - private static final TypeReference LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef LIST_PROMPTS_RESULT_TYPE_REF = new TypeRef<>() { }; - private static final TypeReference GET_PROMPT_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef GET_PROMPT_RESULT_TYPE_REF = new TypeRef<>() { }; /** @@ -911,7 +910,7 @@ void setProtocolVersions(List protocolVersions) { // -------------------------- // Completions // -------------------------- - private static final TypeReference COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef COMPLETION_COMPLETE_RESULT_TYPE_REF = new TypeRef<>() { }; /** diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index c2c74dcae..9414fd858 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -16,11 +16,13 @@ import java.util.function.Consumer; import java.util.function.Function; +import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; @@ -97,8 +99,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; + /** JSON mapper for message serialization/deserialization */ + protected McpJsonMapper jsonMapper; /** Flag indicating if the transport is in closing state */ private volatile boolean isClosing = false; @@ -125,7 +127,7 @@ public class HttpClientSseClientTransport implements McpClientTransport { */ @Deprecated(forRemoval = true) public HttpClientSseClientTransport(String baseUri) { - this(HttpClient.newBuilder(), baseUri, new ObjectMapper()); + this(HttpClient.newBuilder(), baseUri, new com.fasterxml.jackson.databind.ObjectMapper()); } /** @@ -138,7 +140,8 @@ public HttpClientSseClientTransport(String baseUri) { * constructor will be removed in future versions. */ @Deprecated(forRemoval = true) - public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, ObjectMapper objectMapper) { + public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, + com.fasterxml.jackson.databind.ObjectMapper objectMapper) { this(clientBuilder, baseUri, DEFAULT_SSE_ENDPOINT, objectMapper); } @@ -154,7 +157,7 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String bas */ @Deprecated(forRemoval = true) public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, String sseEndpoint, - ObjectMapper objectMapper) { + com.fasterxml.jackson.databind.ObjectMapper objectMapper) { this(clientBuilder, HttpRequest.newBuilder(), baseUri, sseEndpoint, objectMapper); } @@ -188,8 +191,9 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques */ @Deprecated(forRemoval = true) HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, - String sseEndpoint, ObjectMapper objectMapper) { - this(httpClient, requestBuilder, baseUri, sseEndpoint, objectMapper, McpAsyncHttpClientRequestCustomizer.NOOP); + String sseEndpoint, com.fasterxml.jackson.databind.ObjectMapper objectMapper) { + this(httpClient, requestBuilder, baseUri, sseEndpoint, new JacksonMcpJsonMapper(objectMapper), + McpAsyncHttpClientRequestCustomizer.NOOP); } /** @@ -199,14 +203,14 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques * @param requestBuilder the HTTP request builder to use * @param baseUri the base URI of the MCP server * @param sseEndpoint the SSE endpoint path - * @param objectMapper the object mapper for JSON serialization/deserialization + * @param jsonMapper the object mapper for JSON serialization/deserialization * @param httpRequestCustomizer customizer for the requestBuilder before executing * requests * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null */ HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, - String sseEndpoint, ObjectMapper objectMapper, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + String sseEndpoint, McpJsonMapper jsonMapper, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer) { + Assert.notNull(jsonMapper, "jsonMapper 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"); @@ -214,7 +218,7 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques Assert.notNull(httpRequestCustomizer, "httpRequestCustomizer must not be null"); this.baseUri = URI.create(baseUri); this.sseEndpoint = sseEndpoint; - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.httpClient = httpClient; this.requestBuilder = requestBuilder; this.httpRequestCustomizer = httpRequestCustomizer; @@ -245,7 +249,7 @@ public static class Builder { private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1); - private ObjectMapper objectMapper = new ObjectMapper(); + private McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); @@ -343,10 +347,24 @@ public Builder customizeRequest(final Consumer requestCusto * Sets the object mapper for JSON serialization/deserialization. * @param objectMapper the object mapper * @return this builder + * @deprecated Prefer {@link #jsonMapper(McpJsonMapper)}. This method will be + * removed in a future release. */ - public Builder objectMapper(ObjectMapper objectMapper) { + @Deprecated(forRemoval = true) + public Builder objectMapper(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { Assert.notNull(objectMapper, "objectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JSON mapper implementation to use for serialization/deserialization. + * @param jsonMapper the JSON mapper + * @return this builder + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "jsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -402,7 +420,7 @@ public Builder connectTimeout(Duration connectTimeout) { */ public HttpClientSseClientTransport build() { HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); - return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint, objectMapper, + return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint, jsonMapper, httpRequestCustomizer); } @@ -450,7 +468,7 @@ public Mono connect(Function, Mono> h } } else if (MESSAGE_EVENT_TYPE.equals(responseEvent.sseEvent().event())) { - JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, + JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, responseEvent.sseEvent().data()); sink.success(); return Flux.just(message); @@ -531,7 +549,7 @@ public Mono sendMessage(JSONRPCMessage message) { private Mono serializeMessage(final JSONRPCMessage message) { return Mono.defer(() -> { try { - return Mono.just(objectMapper.writeValueAsString(message)); + return Mono.just(jsonMapper.writeValueAsString(message)); } catch (IOException e) { return Mono.error(new McpTransportException("Failed to serialize message", e)); @@ -582,8 +600,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, TypeRef typeRef) { + return this.jsonMapper.convertValue(data, typeRef); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index 4b1ff0d8b..c98a3cb6b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -12,6 +12,7 @@ import java.net.http.HttpResponse.BodyHandler; import java.time.Duration; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicReference; @@ -22,8 +23,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; @@ -106,7 +109,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { public static int BAD_REQUEST = 400; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final URI baseUri; @@ -124,10 +127,10 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { private final AtomicReference> exceptionHandler = new AtomicReference<>(); - private HttpClientStreamableHttpTransport(ObjectMapper objectMapper, HttpClient httpClient, + private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams, boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer) { - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.httpClient = httpClient; this.requestBuilder = requestBuilder; this.baseUri = URI.create(baseUri); @@ -278,7 +281,7 @@ private Mono reconnect(McpTransportStream stream) { // won't since the next version considers // removing it. McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage( - this.objectMapper, responseEvent.sseEvent().data()); + this.jsonMapper, responseEvent.sseEvent().data()); Tuple2, Iterable> idWithMessages = Tuples .of(Optional.ofNullable(responseEvent.sseEvent().id()), @@ -393,7 +396,7 @@ else if (contentType.contains(APPLICATION_JSON)) { public String toString(McpSchema.JSONRPCMessage message) { try { - return this.objectMapper.writeValueAsString(message); + return this.jsonMapper.writeValueAsString(message); } catch (IOException e) { throw new RuntimeException("Failed to serialize JSON-RPC message", e); @@ -479,7 +482,7 @@ else if (contentType.contains(TEXT_EVENT_STREAM)) { // since the // next version considers removing it. McpSchema.JSONRPCMessage message = McpSchema - .deserializeJsonRpcMessage(this.objectMapper, sseEvent.data()); + .deserializeJsonRpcMessage(this.jsonMapper, sseEvent.data()); Tuple2, Iterable> idWithMessages = Tuples .of(Optional.ofNullable(sseEvent.id()), List.of(message)); @@ -508,7 +511,7 @@ else if (contentType.contains(APPLICATION_JSON)) { } try { - return Mono.just(McpSchema.deserializeJsonRpcMessage(objectMapper, data)); + return Mono.just(McpSchema.deserializeJsonRpcMessage(jsonMapper, data)); } catch (IOException e) { return Mono.error(new McpTransportException( @@ -582,8 +585,8 @@ private static String sessionIdOrPlaceholder(McpTransportSession transportSes } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return this.jsonMapper.convertValue(data, typeRef); } /** @@ -593,7 +596,7 @@ public static class Builder { private final String baseUri; - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper()); private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1); @@ -666,10 +669,23 @@ public Builder customizeRequest(final Consumer requestCusto * Configure the {@link ObjectMapper} to use. * @param objectMapper instance to use * @return the builder instance + * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead */ + @Deprecated(forRemoval = true) public Builder objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Configure a custom {@link McpJsonMapper} implementation to use. + * @param jsonMapper instance to use + * @return the builder instance + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "jsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -763,11 +779,9 @@ public Builder connectTimeout(Duration connectTimeout) { * @return a new instance of {@link HttpClientStreamableHttpTransport} */ public HttpClientStreamableHttpTransport build() { - ObjectMapper objectMapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); - HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); - return new HttpClientStreamableHttpTransport(objectMapper, httpClient, requestBuilder, baseUri, endpoint, + return new HttpClientStreamableHttpTransport(this.jsonMapper, httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup, httpRequestCustomizer); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java index 009d415e0..035c3d600 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java @@ -15,8 +15,9 @@ 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.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; @@ -48,7 +49,7 @@ public class StdioClientTransport implements McpClientTransport { /** The server process being communicated with */ private Process process; - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; /** Scheduler for handling inbound messages from the server process */ private Scheduler inboundScheduler; @@ -75,24 +76,24 @@ public class StdioClientTransport implements McpClientTransport { * @param params The parameters for configuring the server process */ public StdioClientTransport(ServerParameters params) { - this(params, new ObjectMapper()); + this(params, new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper())); } /** - * Creates a new StdioClientTransport with the specified parameters and ObjectMapper. + * Creates a new StdioClientTransport with the specified parameters and JsonMapper. * @param params The parameters for configuring the server process - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization */ - public StdioClientTransport(ServerParameters params, ObjectMapper objectMapper) { + public StdioClientTransport(ServerParameters params, McpJsonMapper jsonMapper) { Assert.notNull(params, "The params can not be null"); - Assert.notNull(objectMapper, "The ObjectMapper can not be null"); + Assert.notNull(jsonMapper, "The JsonMapper can not be null"); this.inboundSink = Sinks.many().unicast().onBackpressureBuffer(); this.outboundSink = Sinks.many().unicast().onBackpressureBuffer(); this.params = params; - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.errorSink = Sinks.many().unicast().onBackpressureBuffer(); @@ -102,6 +103,16 @@ public StdioClientTransport(ServerParameters params, ObjectMapper objectMapper) this.errorScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "error"); } + /** + * Creates a new StdioClientTransport with the specified parameters and ObjectMapper. + * @deprecated Use {@link #StdioClientTransport(ServerParameters, McpJsonMapper)} + * instead. + */ + @Deprecated(forRemoval = true) + public StdioClientTransport(ServerParameters params, com.fasterxml.jackson.databind.ObjectMapper objectMapper) { + this(params, new JacksonMcpJsonMapper(objectMapper)); + } + /** * Starts the server process and initializes the message processing streams. This * method sets up the process with the configured command, arguments, and environment, @@ -259,7 +270,7 @@ private void startInboundProcessing() { String line; while (!isClosing && (line = processReader.readLine()) != null) { try { - JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, line); + JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.jsonMapper, line); if (!this.inboundSink.tryEmitNext(message).isSuccess()) { if (!isClosing) { logger.error("Failed to enqueue inbound message: {}", message); @@ -300,7 +311,7 @@ private void startOutboundProcessing() { .handle((message, s) -> { if (message != null && !isClosing) { try { - String jsonMessage = objectMapper.writeValueAsString(message); + String jsonMessage = jsonMapper.writeValueAsString(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 @@ -392,8 +403,8 @@ public Sinks.Many getErrorSink() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return this.jsonMapper.convertValue(data, typeRef); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 3c8057a72..ee60bf36d 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -21,8 +21,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.spec.JsonSchemaValidator; import io.modelcontextprotocol.spec.McpClientSession; @@ -34,7 +34,6 @@ import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest; -import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransportProvider; @@ -93,7 +92,7 @@ public class McpAsyncServer { private final McpServerTransportProviderBase mcpTransportProvider; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final JsonSchemaValidator jsonSchemaValidator; @@ -126,13 +125,13 @@ public class McpAsyncServer { * @param mcpTransportProvider The transport layer implementation for MCP * communication. * @param features The MCP server supported features. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization */ - McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper, + McpAsyncServer(McpServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper, McpServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) { this.mcpTransportProvider = mcpTransportProvider; - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities().mutate().logging().build(); this.instructions = features.instructions(); @@ -153,11 +152,11 @@ public class McpAsyncServer { requestTimeout, transport, this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers)); } - McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper, + McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper, McpServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) { this.mcpTransportProvider = mcpTransportProvider; - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities().mutate().logging().build(); this.instructions = features.instructions(); @@ -505,8 +504,8 @@ private McpRequestHandler toolsListRequestHandler() { private McpRequestHandler toolsCallRequestHandler() { return (exchange, params) -> { - McpSchema.CallToolRequest callToolRequest = objectMapper.convertValue(params, - new TypeReference() { + McpSchema.CallToolRequest callToolRequest = jsonMapper.convertValue(params, + new TypeRef() { }); Optional toolSpecification = this.tools.stream() @@ -633,8 +632,8 @@ private List getResourceTemplates() { private McpRequestHandler resourcesReadRequestHandler() { return (exchange, params) -> { - McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params, - new TypeReference() { + McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, + new TypeRef() { }); var resourceUri = resourceRequest.uri(); @@ -742,8 +741,8 @@ private McpRequestHandler promptsListRequestHandler private McpRequestHandler promptsGetRequestHandler() { return (exchange, params) -> { - McpSchema.GetPromptRequest promptRequest = objectMapper.convertValue(params, - new TypeReference() { + McpSchema.GetPromptRequest promptRequest = jsonMapper.convertValue(params, + new TypeRef() { }); // Implement prompt retrieval logic here @@ -790,9 +789,8 @@ private McpRequestHandler setLoggerRequestHandler() { return (exchange, params) -> { return Mono.defer(() -> { - SetLevelRequest newMinLoggingLevel = objectMapper.convertValue(params, - new TypeReference() { - }); + SetLevelRequest newMinLoggingLevel = jsonMapper.convertValue(params, new TypeRef() { + }); exchange.setMinLoggingLevel(newMinLoggingLevel.level()); @@ -914,4 +912,4 @@ void setProtocolVersions(List protocolVersions) { this.protocolVersions = protocolVersions; } -} \ No newline at end of file +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index 1f0aebf02..835f43bc0 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -8,7 +8,7 @@ import java.util.ArrayList; import java.util.Collections; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpLoggableSession; import io.modelcontextprotocol.spec.McpSchema; @@ -37,16 +37,16 @@ public class McpAsyncServerExchange { private final McpTransportContext transportContext; - private static final TypeReference CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef CREATE_MESSAGE_RESULT_TYPE_REF = new TypeRef<>() { }; - private static final TypeReference LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef LIST_ROOTS_RESULT_TYPE_REF = new TypeRef<>() { }; - private static final TypeReference ELICITATION_RESULT_TYPE_REF = new TypeReference<>() { + private static final TypeRef ELICITATION_RESULT_TYPE_REF = new TypeRef<>() { }; - public static final TypeReference OBJECT_TYPE_REF = new TypeReference<>() { + public static final TypeRef OBJECT_TYPE_REF = new TypeRef<>() { }; /** diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index 76a0de76b..863a90045 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -11,10 +11,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.BiFunction; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.DefaultJsonSchemaValidator; import io.modelcontextprotocol.spec.JsonSchemaValidator; @@ -227,10 +230,12 @@ public McpAsyncServer build() { var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers, this.instructions); - var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); - var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(mapper); - return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout, + + var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator + : new DefaultJsonSchemaValidator( + this.jsonMapper != null ? this.jsonMapper : new JacksonMcpJsonMapper(new ObjectMapper())); + + return new McpAsyncServer(this.transportProvider, jsonMapper, features, this.requestTimeout, this.uriTemplateManagerFactory, jsonSchemaValidator); } @@ -254,10 +259,10 @@ public McpAsyncServer build() { var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers, this.instructions); - var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); + var jsonMapper = this.jsonMapper == null ? new JacksonMcpJsonMapper(new ObjectMapper()) : this.jsonMapper; var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(mapper); - return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout, + : new DefaultJsonSchemaValidator(jsonMapper); + return new McpAsyncServer(this.transportProvider, jsonMapper, features, this.requestTimeout, this.uriTemplateManagerFactory, jsonSchemaValidator); } @@ -270,7 +275,7 @@ abstract class AsyncSpecification> { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - ObjectMapper objectMapper; + McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -769,10 +774,24 @@ public AsyncSpecification rootsChangeHandlers( * @param objectMapper the instance to use. Must not be null. * @return This builder instance for method chaining. * @throws IllegalArgumentException if objectMapper is null + * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead */ + @Deprecated(forRemoval = true) public AsyncSpecification objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JsonMapper to use for serializing and deserializing JSON messages. + * @param jsonMapper the mapper to use. Must not be null. + * @return This builder instance for method chaining. + * @throws IllegalArgumentException if jsonMapper is null + */ + public AsyncSpecification jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -808,16 +827,17 @@ private SingleSessionSyncSpecification(McpServerTransportProvider transportProvi */ @Override public McpSyncServer build() { + Objects.requireNonNull(this.jsonMapper, "JsonMapper must be set"); McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities, this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers, this.instructions); McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution); - var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); - var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(mapper); - var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout, + var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator + : new DefaultJsonSchemaValidator(this.jsonMapper); + + var asyncServer = new McpAsyncServer(this.transportProvider, jsonMapper, asyncFeatures, this.requestTimeout, this.uriTemplateManagerFactory, jsonSchemaValidator); return new McpSyncServer(asyncServer, this.immediateExecution); @@ -846,11 +866,11 @@ public McpSyncServer build() { this.rootsChangeHandlers, this.instructions); McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution); - var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); + var jsonMapper = this.jsonMapper == null ? new JacksonMcpJsonMapper(new ObjectMapper()) : this.jsonMapper; var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(mapper); + : new DefaultJsonSchemaValidator(jsonMapper); - var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout, + var asyncServer = new McpAsyncServer(this.transportProvider, jsonMapper, asyncFeatures, this.requestTimeout, this.uriTemplateManagerFactory, jsonSchemaValidator); return new McpSyncServer(asyncServer, this.immediateExecution); @@ -865,7 +885,7 @@ abstract class SyncSpecification> { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - ObjectMapper objectMapper; + McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -1367,10 +1387,24 @@ public SyncSpecification rootsChangeHandlers( * @param objectMapper the instance to use. Must not be null. * @return This builder instance for method chaining. * @throws IllegalArgumentException if objectMapper is null + * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead */ + @Deprecated(forRemoval = true) public SyncSpecification objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JsonMapper to use for serializing and deserializing JSON messages. + * @param jsonMapper the mapper to use. Must not be null. + * @return This builder instance for method chaining. + * @throws IllegalArgumentException if jsonMapper is null + */ + public SyncSpecification jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -1404,7 +1438,7 @@ class StatelessAsyncSpecification { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - ObjectMapper objectMapper; + McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -1828,7 +1862,19 @@ public StatelessAsyncSpecification completions( */ public StatelessAsyncSpecification objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JsonMapper to use for serializing and deserializing JSON messages. + * @param jsonMapper the mapper to use. Must not be null. + * @return This builder instance for method chaining. + * @throws IllegalArgumentException if jsonMapper is null + */ + public StatelessAsyncSpecification jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -1849,10 +1895,10 @@ public StatelessAsyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonS public McpStatelessAsyncServer build() { var features = new McpStatelessServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions); - var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); - var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(mapper); - return new McpStatelessAsyncServer(this.transport, mapper, features, this.requestTimeout, + var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator + : new DefaultJsonSchemaValidator(this.jsonMapper); + + return new McpStatelessAsyncServer(this.transport, this.jsonMapper, features, this.requestTimeout, this.uriTemplateManagerFactory, jsonSchemaValidator); } @@ -1866,7 +1912,7 @@ class StatelessSyncSpecification { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - ObjectMapper objectMapper; + McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -2287,10 +2333,24 @@ public StatelessSyncSpecification completions( * @param objectMapper the instance to use. Must not be null. * @return This builder instance for method chaining. * @throws IllegalArgumentException if objectMapper is null + * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead */ + @Deprecated(forRemoval = true) public StatelessSyncSpecification objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JsonMapper to use for serializing and deserializing JSON messages. + * @param jsonMapper the mapper to use. Must not be null. + * @return This builder instance for method chaining. + * @throws IllegalArgumentException if jsonMapper is null + */ + public StatelessSyncSpecification jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -2325,31 +2385,16 @@ public StatelessSyncSpecification immediateExecution(boolean immediateExecution) } public McpStatelessSyncServer build() { - /* - * McpServerFeatures.Sync syncFeatures = new - * McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities, - * this.tools, this.resources, this.resourceTemplates, this.prompts, - * this.completions, this.rootsChangeHandlers, this.instructions); - * McpServerFeatures.Async asyncFeatures = - * McpServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution); - * var mapper = this.objectMapper != null ? this.objectMapper : new - * ObjectMapper(); var jsonSchemaValidator = this.jsonSchemaValidator != null - * ? this.jsonSchemaValidator : new DefaultJsonSchemaValidator(mapper); - * - * var asyncServer = new McpAsyncServer(this.transportProvider, mapper, - * asyncFeatures, this.requestTimeout, this.uriTemplateManagerFactory, - * jsonSchemaValidator); - * - * return new McpSyncServer(asyncServer, this.immediateExecution); - */ + Objects.requireNonNull(this.jsonMapper, "JsonMapper must be set"); var syncFeatures = new McpStatelessServerFeatures.Sync(this.serverInfo, this.serverCapabilities, this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions); var asyncFeatures = McpStatelessServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution); - var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); - var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(mapper); - var asyncServer = new McpStatelessAsyncServer(this.transport, mapper, asyncFeatures, this.requestTimeout, - this.uriTemplateManagerFactory, jsonSchemaValidator); + + var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator + : new DefaultJsonSchemaValidator(this.jsonMapper); + + var asyncServer = new McpStatelessAsyncServer(this.transport, this.jsonMapper, asyncFeatures, + this.requestTimeout, this.uriTemplateManagerFactory, jsonSchemaValidator); return new McpStatelessSyncServer(asyncServer, this.immediateExecution); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index 50d45b14c..0335f386c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -4,8 +4,8 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.McpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.JsonSchemaValidator; import io.modelcontextprotocol.spec.McpError; @@ -13,7 +13,6 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; -import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpStatelessServerTransport; import io.modelcontextprotocol.util.Assert; @@ -48,7 +47,7 @@ public class McpStatelessAsyncServer { private final McpStatelessServerTransport mcpTransportProvider; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final McpSchema.ServerCapabilities serverCapabilities; @@ -72,11 +71,11 @@ public class McpStatelessAsyncServer { private final JsonSchemaValidator jsonSchemaValidator; - McpStatelessAsyncServer(McpStatelessServerTransport mcpTransport, ObjectMapper objectMapper, + McpStatelessAsyncServer(McpStatelessServerTransport mcpTransport, McpJsonMapper jsonMapper, McpStatelessServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) { this.mcpTransportProvider = mcpTransport; - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities(); this.instructions = features.instructions(); @@ -132,7 +131,7 @@ public class McpStatelessAsyncServer { // --------------------------------------- private McpStatelessRequestHandler asyncInitializeRequestHandler() { return (ctx, req) -> Mono.defer(() -> { - McpSchema.InitializeRequest initializeRequest = this.objectMapper.convertValue(req, + McpSchema.InitializeRequest initializeRequest = this.jsonMapper.convertValue(req, McpSchema.InitializeRequest.class); logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}", @@ -373,8 +372,8 @@ private McpStatelessRequestHandler toolsListRequestHa private McpStatelessRequestHandler toolsCallRequestHandler() { return (ctx, params) -> { - McpSchema.CallToolRequest callToolRequest = objectMapper.convertValue(params, - new TypeReference() { + McpSchema.CallToolRequest callToolRequest = jsonMapper.convertValue(params, + new TypeRef() { }); Optional toolSpecification = this.tools.stream() @@ -476,8 +475,8 @@ private List getResourceTemplates() { private McpStatelessRequestHandler resourcesReadRequestHandler() { return (ctx, params) -> { - McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params, - new TypeReference() { + McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, + new TypeRef() { }); var resourceUri = resourceRequest.uri(); @@ -566,8 +565,8 @@ private McpStatelessRequestHandler promptsListReque private McpStatelessRequestHandler promptsGetRequestHandler() { return (ctx, params) -> { - McpSchema.GetPromptRequest promptRequest = objectMapper.convertValue(params, - new TypeReference() { + McpSchema.GetPromptRequest promptRequest = jsonMapper.convertValue(params, + new TypeRef() { }); // Implement prompt retrieval logic here diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index bbc1edf24..9b562124a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -14,7 +14,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -25,6 +25,8 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.spec.ProtocolVersions; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.util.KeepAliveScheduler; import jakarta.servlet.AsyncContext; import jakarta.servlet.ServletException; @@ -89,8 +91,8 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement public static final String DEFAULT_BASE_URL = ""; - /** JSON object mapper for serialization/deserialization */ - private final ObjectMapper objectMapper; + /** JSON mapper for serialization/deserialization */ + private final McpJsonMapper jsonMapper; /** Base URL for the server transport */ private final String baseUrl; @@ -131,7 +133,8 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement @Deprecated public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) { - this(objectMapper, DEFAULT_BASE_URL, messageEndpoint, sseEndpoint); + this(new JacksonMcpJsonMapper(objectMapper), DEFAULT_BASE_URL, messageEndpoint, sseEndpoint, null, + (serverRequest) -> McpTransportContext.EMPTY); } /** @@ -148,7 +151,8 @@ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String m @Deprecated public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) { - this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, null, (serverRequest) -> McpTransportContext.EMPTY); + this(new JacksonMcpJsonMapper(objectMapper), baseUrl, messageEndpoint, sseEndpoint, null, + (serverRequest) -> McpTransportContext.EMPTY); } /** @@ -167,14 +171,14 @@ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String b @Deprecated public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint, Duration keepAliveInterval) { - this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, + this(new JacksonMcpJsonMapper(objectMapper), baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, (serverRequest) -> McpTransportContext.EMPTY); } /** * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE * endpoint. - * @param objectMapper The JSON object mapper to use for message + * @param jsonMapper The JSON object mapper to use for message * serialization/deserialization * @param baseUrl The base URL for the server transport * @param messageEndpoint The endpoint path where clients will send their messages @@ -185,16 +189,16 @@ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String b * @deprecated Use the builder {@link #builder()} instead for better configuration * options. */ - private HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, + private HttpServletSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint, String sseEndpoint, Duration keepAliveInterval, McpTransportContextExtractor contextExtractor) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Assert.notNull(jsonMapper, "JsonMapper must not be null"); Assert.notNull(messageEndpoint, "messageEndpoint must not be null"); Assert.notNull(sseEndpoint, "sseEndpoint must not be null"); Assert.notNull(contextExtractor, "Context extractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.baseUrl = baseUrl; this.messageEndpoint = messageEndpoint; this.sseEndpoint = sseEndpoint; @@ -342,7 +346,7 @@ 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 = jsonMapper.writeValueAsString(new McpError("Session ID missing in message endpoint")); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -355,7 +359,7 @@ 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 = jsonMapper.writeValueAsString(new McpError("Session not found: " + sessionId)); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -371,7 +375,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } final McpTransportContext transportContext = this.contextExtractor.extract(request); - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString()); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString()); // Process the message through the session's handle method // Block for Servlet compatibility @@ -386,7 +390,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 = jsonMapper.writeValueAsString(mcpError); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -482,7 +486,7 @@ private class HttpServletMcpSessionTransport implements McpServerTransport { public Mono sendMessage(McpSchema.JSONRPCMessage message) { return Mono.fromRunnable(() -> { try { - String jsonText = objectMapper.writeValueAsString(message); + String jsonText = jsonMapper.writeValueAsString(message); sendEvent(writer, MESSAGE_EVENT_TYPE, jsonText); logger.debug("Message sent to session {}", sessionId); } @@ -495,15 +499,15 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { } /** - * Converts data from one type to another using the configured ObjectMapper. + * Converts data from one type to another using the configured JsonMapper. * @param data The source data object to convert * @param typeRef The target type reference * @return The converted object of type T * @param The target type */ @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return jsonMapper.convertValue(data, typeRef); } /** @@ -561,6 +565,8 @@ public static class Builder { private ObjectMapper objectMapper = new ObjectMapper(); + private McpJsonMapper jsonMapper; + private String baseUrl = DEFAULT_BASE_URL; private String messageEndpoint; @@ -577,12 +583,26 @@ public static class Builder { * @param objectMapper The object mapper to use * @return This builder instance for method chaining */ + @Deprecated(forRemoval = true) public Builder objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); this.objectMapper = objectMapper; return this; } + /** + * Sets the JsonMapper implementation to use for serialization/deserialization. If + * not specified, a JacksonJsonMapper will be created from the configured + * ObjectMapper. + * @param jsonMapper The JsonMapper to use + * @return This builder instance for method chaining + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; + return this; + } + /** * Sets the base URL for the server transport. * @param baseUrl The base URL to use @@ -657,7 +677,9 @@ public HttpServletSseServerTransportProvider build() { if (messageEndpoint == null) { throw new IllegalStateException("MessageEndpoint must be set"); } - return new HttpServletSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint, + McpJsonMapper effectiveMapper = (this.jsonMapper != null) ? this.jsonMapper + : new JacksonMcpJsonMapper(objectMapper); + return new HttpServletSseServerTransportProvider(effectiveMapper, baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, contextExtractor); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java index 9a8f6cbb9..24695732a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java @@ -8,10 +8,12 @@ import java.io.IOException; import java.io.PrintWriter; +import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpStatelessServerHandler; @@ -48,7 +50,7 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}"; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final String mcpEndpoint; @@ -58,13 +60,13 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements private volatile boolean isClosing = false; - private HttpServletStatelessServerTransport(ObjectMapper objectMapper, String mcpEndpoint, + private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint, McpTransportContextExtractor contextExtractor) { - Assert.notNull(objectMapper, "objectMapper must not be null"); + Assert.notNull(jsonMapper, "jsonMapper must not be null"); Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null"); Assert.notNull(contextExtractor, "contextExtractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.mcpEndpoint = mcpEndpoint; this.contextExtractor = contextExtractor; } @@ -139,7 +141,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) body.append(line); } - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString()); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString()); if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { try { @@ -152,7 +154,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.setCharacterEncoding(UTF_8); response.setStatus(HttpServletResponse.SC_OK); - String jsonResponseText = objectMapper.writeValueAsString(jsonrpcResponse); + String jsonResponseText = jsonMapper.writeValueAsString(jsonrpcResponse); PrintWriter writer = response.getWriter(); writer.write(jsonResponseText); writer.flush(); @@ -203,7 +205,7 @@ private void responseError(HttpServletResponse response, int httpCode, McpError response.setContentType(APPLICATION_JSON); response.setCharacterEncoding(UTF_8); response.setStatus(httpCode); - String jsonError = objectMapper.writeValueAsString(mcpError); + String jsonError = jsonMapper.writeValueAsString(mcpError); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -236,7 +238,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -254,9 +256,23 @@ private Builder() { * @return this builder instance * @throws IllegalArgumentException if objectMapper is null */ + @Deprecated(forRemoval = true) public Builder objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JsonMapper to use for JSON serialization/deserialization of MCP + * messages. + * @param jsonMapper The JsonMapper instance. Must not be null. + * @return this builder instance + * @throws IllegalArgumentException if jsonMapper is null + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -295,10 +311,12 @@ public Builder contextExtractor(McpTransportContextExtractor * @throws IllegalStateException if required parameters are not set */ public HttpServletStatelessServerTransport build() { - Assert.notNull(objectMapper, "ObjectMapper must be set"); + if (this.jsonMapper == null) { + throw new IllegalStateException("JsonMapper must be set"); + } Assert.notNull(mcpEndpoint, "Message endpoint must be set"); - return new HttpServletStatelessServerTransport(objectMapper, mcpEndpoint, contextExtractor); + return new HttpServletStatelessServerTransport(jsonMapper, mcpEndpoint, contextExtractor); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 3cb8d7b15..10f4d6d50 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -16,7 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.common.McpTransportContext; @@ -29,6 +29,8 @@ import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; import io.modelcontextprotocol.spec.ProtocolVersions; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.util.KeepAliveScheduler; import jakarta.servlet.AsyncContext; import jakarta.servlet.ServletException; @@ -97,7 +99,7 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet */ private final boolean disallowDelete; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private McpStreamableServerSession.Factory sessionFactory; @@ -121,22 +123,22 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet /** * Constructs a new HttpServletStreamableServerTransportProvider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of messages. + * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization of + * messages. * @param mcpEndpoint The endpoint URI where clients should send their JSON-RPC * messages via HTTP. This endpoint will handle GET, POST, and DELETE requests. * @param disallowDelete Whether to disallow DELETE requests on the endpoint. * @param contextExtractor The extractor for transport context from the request. * @throws IllegalArgumentException if any parameter is null */ - private HttpServletStreamableServerTransportProvider(ObjectMapper objectMapper, String mcpEndpoint, + private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint, boolean disallowDelete, McpTransportContextExtractor contextExtractor, Duration keepAliveInterval) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Assert.notNull(jsonMapper, "JsonMapper must not be null"); Assert.notNull(mcpEndpoint, "MCP endpoint must not be null"); Assert.notNull(contextExtractor, "Context extractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.mcpEndpoint = mcpEndpoint; this.disallowDelete = disallowDelete; this.contextExtractor = contextExtractor; @@ -392,7 +394,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) body.append(line); } - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString()); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString()); // Handle initialization request if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest @@ -403,8 +405,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) return; } - McpSchema.InitializeRequest initializeRequest = objectMapper.convertValue(jsonrpcRequest.params(), - new TypeReference() { + McpSchema.InitializeRequest initializeRequest = jsonMapper.convertValue(jsonrpcRequest.params(), + new TypeRef() { }); McpStreamableServerSession.McpStreamableServerSessionInit init = this.sessionFactory .startSession(initializeRequest); @@ -418,7 +420,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.setHeader(HttpHeaders.MCP_SESSION_ID, init.session().getId()); response.setStatus(HttpServletResponse.SC_OK); - String jsonResponse = objectMapper.writeValueAsString(new McpSchema.JSONRPCResponse( + String jsonResponse = jsonMapper.writeValueAsString(new McpSchema.JSONRPCResponse( McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null)); PrintWriter writer = response.getWriter(); @@ -578,7 +580,7 @@ public void responseError(HttpServletResponse response, int httpCode, McpError m response.setContentType(APPLICATION_JSON); response.setCharacterEncoding(UTF_8); response.setStatus(httpCode); - String jsonError = objectMapper.writeValueAsString(mcpError); + String jsonError = jsonMapper.writeValueAsString(mcpError); PrintWriter writer = response.getWriter(); writer.write(jsonError); writer.flush(); @@ -685,7 +687,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId return; } - String jsonText = objectMapper.writeValueAsString(message); + String jsonText = jsonMapper.writeValueAsString(message); HttpServletStreamableServerTransportProvider.this.sendEvent(writer, MESSAGE_EVENT_TYPE, jsonText, messageId != null ? messageId : this.sessionId); logger.debug("Message sent to session {} with ID {}", this.sessionId, messageId); @@ -702,15 +704,15 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId } /** - * Converts data from one type to another using the configured ObjectMapper. + * Converts data from one type to another using the configured JsonMapper. * @param data The source data object to convert * @param typeRef The target type reference * @return The converted object of type T * @param The target type */ @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return jsonMapper.convertValue(data, typeRef); } /** @@ -762,7 +764,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -780,9 +782,23 @@ public static class Builder { * @return this builder instance * @throws IllegalArgumentException if objectMapper is null */ + @Deprecated(forRemoval = true) public Builder objectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); + return this; + } + + /** + * Sets the JsonMapper to use for JSON serialization/deserialization of MCP + * messages. + * @param jsonMapper The JsonMapper instance. Must not be null. + * @return this builder instance + * @throws IllegalArgumentException if JsonMapper is null + */ + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -839,11 +855,13 @@ public Builder keepAliveInterval(Duration keepAliveInterval) { * @throws IllegalStateException if required parameters are not set */ public HttpServletStreamableServerTransportProvider build() { - Assert.notNull(this.objectMapper, "ObjectMapper must be set"); + if (this.jsonMapper == null) { + throw new IllegalStateException("JsonMapper must be set"); + } Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set"); - return new HttpServletStreamableServerTransportProvider(this.objectMapper, this.mcpEndpoint, - this.disallowDelete, this.contextExtractor, this.keepAliveInterval); + return new HttpServletStreamableServerTransportProvider(jsonMapper, this.mcpEndpoint, this.disallowDelete, + this.contextExtractor, this.keepAliveInterval); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java index af602f610..357db189c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java @@ -15,7 +15,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; @@ -25,6 +25,8 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.spec.ProtocolVersions; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -44,7 +46,7 @@ public class StdioServerTransportProvider implements McpServerTransportProvider private static final Logger logger = LoggerFactory.getLogger(StdioServerTransportProvider.class); - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final InputStream inputStream; @@ -61,31 +63,35 @@ public class StdioServerTransportProvider implements McpServerTransportProvider * streams. */ public StdioServerTransportProvider() { - this(new ObjectMapper()); + this(new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()), System.in, System.out); } /** * Creates a new StdioServerTransportProvider with the specified ObjectMapper and * System streams. * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * @deprecated Prefer + * {@link StdioServerTransportProvider#StdioServerTransportProvider(McpJsonMapper, InputStream, OutputStream)} + * or use the no-arg constructor which defaults to a JacksonJsonMapper internally. */ + @Deprecated(forRemoval = true) public StdioServerTransportProvider(ObjectMapper objectMapper) { - this(objectMapper, System.in, System.out); + this(new JacksonMcpJsonMapper(objectMapper), System.in, System.out); } /** * Creates a new StdioServerTransportProvider with the specified ObjectMapper and * streams. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization * @param inputStream The input stream to read from * @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"); + public StdioServerTransportProvider(McpJsonMapper jsonMapper, InputStream inputStream, OutputStream outputStream) { + Assert.notNull(jsonMapper, "The JsonMapper 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.jsonMapper = jsonMapper; this.inputStream = inputStream; this.outputStream = outputStream; } @@ -165,8 +171,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, TypeRef typeRef) { + return jsonMapper.convertValue(data, typeRef); } @Override @@ -219,7 +225,7 @@ private void startInboundProcessing() { logger.debug("Received JSON message: {}", line); try { - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, line); if (!this.inboundSink.tryEmitNext(message).isSuccess()) { // logIfNotClosing("Failed to enqueue message"); @@ -263,7 +269,7 @@ private void startOutboundProcessing() { .handle((message, sink) -> { if (message != null && !isClosing.get()) { try { - String jsonMessage = objectMapper.writeValueAsString(message); + String jsonMessage = jsonMapper.writeValueAsString(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/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java b/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java index f4bdc02eb..5d3d677a9 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java @@ -21,6 +21,8 @@ import com.networknt.schema.ValidationMessage; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; /** * Default implementation of the {@link JsonSchemaValidator} interface. This class @@ -50,6 +52,24 @@ public DefaultJsonSchemaValidator(ObjectMapper objectMapper) { this.schemaCache = new ConcurrentHashMap<>(); } + /** + * Alternate constructor that accepts a JsonMapper. If the mapper is backed by + * Jackson, the underlying ObjectMapper will be reused; otherwise a new ObjectMapper + * will be created for schema validation only. + */ + public DefaultJsonSchemaValidator(McpJsonMapper jsonMapper) { + ObjectMapper mapper; + if (jsonMapper instanceof JacksonMcpJsonMapper jacksonJsonMapper) { + mapper = jacksonJsonMapper.getObjectMapper(); + } + else { + mapper = new ObjectMapper(); + } + this.objectMapper = mapper; + this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + this.schemaCache = new ConcurrentHashMap<>(); + } + @Override public ValidationResponse validate(Map schema, Map structuredContent) { diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java index 6ac8defa0..d5da1b3df 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java @@ -4,7 +4,7 @@ package io.modelcontextprotocol.spec; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.util.Assert; import org.reactivestreams.Publisher; import org.slf4j.Logger; @@ -254,7 +254,7 @@ private String generateRequestId() { * @return A Mono containing the response */ @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, TypeRef typeRef) { String requestId = this.generateRequestId(); return Mono.deferContextual(ctx -> Mono.create(pendingResponseSink -> { diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 3f8150271..04cde7c46 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -21,8 +21,9 @@ 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.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.util.Assert; @@ -111,7 +112,19 @@ private McpSchema() { // Elicitation Methods public static final String METHOD_ELICITATION_CREATE = "elicitation/create"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static volatile McpJsonMapper JSON_MAPPER = new JacksonMcpJsonMapper( + new com.fasterxml.jackson.databind.ObjectMapper()); + + /** + * Allows overriding the default JSON mapper used internally by schema helper methods. + * This is optional; callers can also pass a JsonMapper directly to + * deserializeJsonRpcMessage. + * @param mapper The JsonMapper to use + */ + public static void setJsonMapper(McpJsonMapper mapper) { + Assert.notNull(mapper, "jsonMapper must not be null"); + JSON_MAPPER = mapper; + } // --------------------------- // JSON-RPC Error Codes @@ -178,12 +191,12 @@ public sealed interface Notification } - private static final TypeReference> MAP_TYPE_REF = new TypeReference<>() { + private static final TypeRef> MAP_TYPE_REF = new TypeRef<>() { }; /** * Deserializes a JSON string into a JSONRPCMessage object. - * @param objectMapper The ObjectMapper instance to use for deserialization + * @param jsonMapper The JsonMapper 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. @@ -191,27 +204,33 @@ public sealed interface Notification * @throws IllegalArgumentException If the JSON structure doesn't match any known * message type */ - public static JSONRPCMessage deserializeJsonRpcMessage(ObjectMapper objectMapper, String jsonText) + public static JSONRPCMessage deserializeJsonRpcMessage(McpJsonMapper jsonMapper, String jsonText) throws IOException { logger.debug("Received JSON message: {}", jsonText); - var map = objectMapper.readValue(jsonText, MAP_TYPE_REF); + var map = jsonMapper.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); + return jsonMapper.convertValue(map, JSONRPCRequest.class); } else if (map.containsKey("method") && !map.containsKey("id")) { - return objectMapper.convertValue(map, JSONRPCNotification.class); + return jsonMapper.convertValue(map, JSONRPCNotification.class); } else if (map.containsKey("result") || map.containsKey("error")) { - return objectMapper.convertValue(map, JSONRPCResponse.class); + return jsonMapper.convertValue(map, JSONRPCResponse.class); } throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + jsonText); } + @Deprecated(forRemoval = true) + public static JSONRPCMessage deserializeJsonRpcMessage(com.fasterxml.jackson.databind.ObjectMapper objectMapper, + String jsonText) throws IOException { + return deserializeJsonRpcMessage(new JacksonMcpJsonMapper(objectMapper), jsonText); + } + // --------------------------- // JSON-RPC Message Types // --------------------------- @@ -1394,7 +1413,7 @@ public Tool build() { private static Map schemaToMap(String schema) { try { - return OBJECT_MAPPER.readValue(schema, MAP_TYPE_REF); + return JSON_MAPPER.readValue(schema, MAP_TYPE_REF); } catch (IOException e) { throw new IllegalArgumentException("Invalid schema: " + schema, e); @@ -1403,7 +1422,7 @@ private static Map schemaToMap(String schema) { private static JsonSchema parseSchema(String schema) { try { - return OBJECT_MAPPER.readValue(schema, JsonSchema.class); + return JSON_MAPPER.readValue(schema, JsonSchema.class); } catch (IOException e) { throw new IllegalArgumentException("Invalid schema: " + schema, e); @@ -1437,7 +1456,7 @@ public CallToolRequest(String name, Map arguments) { private static Map parseJsonArguments(String jsonArguments) { try { - return OBJECT_MAPPER.readValue(jsonArguments, MAP_TYPE_REF); + return JSON_MAPPER.readValue(jsonArguments, MAP_TYPE_REF); } catch (IOException e) { throw new IllegalArgumentException("Invalid arguments: " + jsonArguments, e); @@ -1575,7 +1594,7 @@ public Builder structuredContent(Map structuredContent) { public Builder structuredContent(String structuredContent) { Assert.hasText(structuredContent, "structuredContent must not be empty"); try { - this.structuredContent = OBJECT_MAPPER.readValue(structuredContent, MAP_TYPE_REF); + this.structuredContent = JSON_MAPPER.readValue(structuredContent, MAP_TYPE_REF); } catch (IOException e) { throw new IllegalArgumentException("Invalid structured content: " + structuredContent, e); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java index 747b45490..d12602c99 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java @@ -11,12 +11,12 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpInitRequestHandler; import io.modelcontextprotocol.server.McpNotificationHandler; import io.modelcontextprotocol.server.McpRequestHandler; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -153,7 +153,7 @@ public boolean isNotificationForLevelAllowed(McpSchema.LoggingLevel loggingLevel } @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, TypeRef typeRef) { String requestId = this.generateRequestId(); return Mono.create(sink -> { @@ -259,7 +259,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() { + new TypeRef() { }); this.state.lazySet(STATE_INITIALIZING); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java index 3473a4da8..53fde1702 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java @@ -4,7 +4,7 @@ package io.modelcontextprotocol.spec; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import reactor.core.publisher.Mono; /** @@ -37,7 +37,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); + Mono sendRequest(String method, Object requestParams, TypeRef typeRef); /** * Sends a notification to the model client or server without parameters. diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java index 53b56c70f..d3abb5c7c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java @@ -15,7 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpAsyncServerExchange; @@ -110,7 +110,7 @@ private String generateRequestId() { } @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, TypeRef typeRef) { return Mono.defer(() -> { McpLoggableSession listeningStream = this.listeningStreamRef.get(); return listeningStream.sendRequest(method, requestParams, typeRef); @@ -347,7 +347,7 @@ public boolean isNotificationForLevelAllowed(McpSchema.LoggingLevel loggingLevel } @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, TypeRef typeRef) { String requestId = McpStreamableServerSession.this.generateRequestId(); McpStreamableServerSession.this.requestIdToStream.put(requestId, this); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java index 1922548a6..df8d4fc2c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java @@ -6,8 +6,8 @@ import java.util.List; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; +import io.modelcontextprotocol.spec.json.TypeRef; import reactor.core.publisher.Mono; /** @@ -77,7 +77,7 @@ default void close() { * @param typeRef the type reference for the object to unmarshal * @return the unmarshalled object */ - T unmarshalFrom(Object data, TypeReference typeRef); + T unmarshalFrom(Object data, TypeRef typeRef); default List protocolVersions() { return List.of(ProtocolVersions.MCP_2024_11_05); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java index aa33a8167..e4acf1b8a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java @@ -4,7 +4,7 @@ package io.modelcontextprotocol.spec; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; @@ -31,7 +31,7 @@ public MissingMcpTransportSession(String sessionId) { } @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, TypeRef typeRef) { return Mono.error(new IllegalStateException("Stream unavailable for session " + this.sessionId)); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/json/McpJsonMapper.java b/mcp/src/main/java/io/modelcontextprotocol/spec/json/McpJsonMapper.java new file mode 100644 index 000000000..f48a525b6 --- /dev/null +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/json/McpJsonMapper.java @@ -0,0 +1,86 @@ +package io.modelcontextprotocol.spec.json; + +import java.io.IOException; + +/** + * Abstraction for JSON serialization/deserialization to decouple the SDK from any + * specific JSON library. A default implementation backed by Jackson is provided in + * io.modelcontextprotocol.spec.json.jackson.JacksonJsonMapper. + */ +public interface McpJsonMapper { + + /** + * Deserialize JSON string into a target type. + * @param content JSON as String + * @param type target class + * @return deserialized instance + * @param generic type + * @throws IOException on parse errors + */ + T readValue(String content, Class type) throws IOException; + + /** + * Deserialize JSON bytes into a target type. + * @param content JSON as bytes + * @param type target class + * @return deserialized instance + * @param generic type + * @throws IOException on parse errors + */ + T readValue(byte[] content, Class type) throws IOException; + + /** + * Deserialize JSON string into a parameterized target type. + * @param content JSON as String + * @param type parameterized type reference + * @return deserialized instance + * @param generic type + * @throws IOException on parse errors + */ + T readValue(String content, TypeRef type) throws IOException; + + /** + * Deserialize JSON bytes into a parameterized target type. + * @param content JSON as bytes + * @param type parameterized type reference + * @return deserialized instance + * @param generic type + * @throws IOException on parse errors + */ + T readValue(byte[] content, TypeRef type) throws IOException; + + /** + * Convert a value to a given type, useful for mapping nested JSON structures. + * @param fromValue source value + * @param type target class + * @return converted value + * @param generic type + */ + T convertValue(Object fromValue, Class type); + + /** + * Convert a value to a given parameterized type. + * @param fromValue source value + * @param type target type reference + * @return converted value + * @param generic type + */ + T convertValue(Object fromValue, TypeRef type); + + /** + * Serialize an object to JSON string. + * @param value object to serialize + * @return JSON as String + * @throws IOException on serialization errors + */ + String writeValueAsString(Object value) throws IOException; + + /** + * Serialize an object to JSON bytes. + * @param value object to serialize + * @return JSON as bytes + * @throws IOException on serialization errors + */ + byte[] writeValueAsBytes(Object value) throws IOException; + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/json/TypeRef.java b/mcp/src/main/java/io/modelcontextprotocol/spec/json/TypeRef.java new file mode 100644 index 000000000..61dcdaebc --- /dev/null +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/json/TypeRef.java @@ -0,0 +1,26 @@ +package io.modelcontextprotocol.spec.json; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * Captures generic type information at runtime for parameterized JSON (de)serialization. + * Usage: TypeRef> ref = new TypeRef<>(){}; + */ +public abstract class TypeRef { + + private final Type type; + + protected TypeRef() { + Type superClass = getClass().getGenericSuperclass(); + if (superClass instanceof Class) { + throw new IllegalStateException("TypeRef constructed without actual type information"); + } + this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + + public Type getType() { + return type; + } + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/json/jackson/JacksonMcpJsonMapper.java b/mcp/src/main/java/io/modelcontextprotocol/spec/json/jackson/JacksonMcpJsonMapper.java new file mode 100644 index 000000000..46dbf7e8a --- /dev/null +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/json/jackson/JacksonMcpJsonMapper.java @@ -0,0 +1,72 @@ +package io.modelcontextprotocol.spec.json.jackson; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.TypeRef; + +import java.io.IOException; + +/** + * Jackson-based implementation of JsonMapper. Wraps a Jackson ObjectMapper but keeps the + * SDK decoupled from Jackson at the API level. + */ +public final class JacksonMcpJsonMapper implements McpJsonMapper { + + private final ObjectMapper objectMapper; + + public JacksonMcpJsonMapper(ObjectMapper objectMapper) { + if (objectMapper == null) { + throw new IllegalArgumentException("ObjectMapper must not be null"); + } + this.objectMapper = objectMapper; + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + @Override + public T readValue(String content, Class type) throws IOException { + return objectMapper.readValue(content, type); + } + + @Override + public T readValue(byte[] content, Class type) throws IOException { + return objectMapper.readValue(content, type); + } + + @Override + public T readValue(String content, TypeRef type) throws IOException { + JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType()); + return objectMapper.readValue(content, javaType); + } + + @Override + public T readValue(byte[] content, TypeRef type) throws IOException { + JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType()); + return objectMapper.readValue(content, javaType); + } + + @Override + public T convertValue(Object fromValue, Class type) { + return objectMapper.convertValue(fromValue, type); + } + + @Override + public T convertValue(Object fromValue, TypeRef type) { + JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType()); + return objectMapper.convertValue(fromValue, javaType); + } + + @Override + public String writeValueAsString(Object value) throws IOException { + return objectMapper.writeValueAsString(value); + } + + @Override + public byte[] writeValueAsBytes(Object value) throws IOException { + return objectMapper.writeValueAsBytes(value); + } + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java b/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java index 9d411cd41..2bdee48c0 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java +++ b/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java @@ -11,7 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSession; @@ -33,7 +33,7 @@ public class KeepAliveScheduler { private static final Logger logger = LoggerFactory.getLogger(KeepAliveScheduler.class); - private static final TypeReference OBJECT_TYPE_REF = new TypeReference<>() { + private static final TypeRef OBJECT_TYPE_REF = new TypeRef<>() { }; /** Initial delay before the first keepAlive call */ diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java index b1113a6d0..dbb55650b 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java +++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java @@ -9,8 +9,8 @@ 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.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; @@ -99,8 +99,8 @@ public Mono closeGracefully() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return new ObjectMapper().convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()).convertValue(data, typeRef); } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java index 778746faa..3e076e1dc 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java +++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java @@ -8,8 +8,8 @@ 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.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; @@ -67,8 +67,8 @@ public Mono closeGracefully() { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return new ObjectMapper().convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()).convertValue(data, typeRef); } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index cab847512..5dd3c146c 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -10,7 +10,7 @@ import java.util.function.Function; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.MockMcpClientTransport; import io.modelcontextprotocol.spec.McpSchema; @@ -321,7 +321,7 @@ void testSamplingCreateMessageRequestHandling() { assertThat(response.error()).isNull(); McpSchema.CreateMessageResult result = transport.unmarshalFrom(response.result(), - new TypeReference() { + new TypeRef() { }); assertThat(result).isNotNull(); assertThat(result.role()).isEqualTo(McpSchema.Role.ASSISTANT); @@ -425,7 +425,7 @@ void testElicitationCreateRequestHandling() { assertThat(response.id()).isEqualTo("test-id"); assertThat(response.error()).isNull(); - McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeReference<>() { + McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeRef<>() { }); assertThat(result).isNotNull(); assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); @@ -470,7 +470,7 @@ void testElicitationFailRequestHandling(McpSchema.ElicitResult.Action action) { assertThat(response.id()).isEqualTo("test-id"); assertThat(response.error()).isNull(); - McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeReference<>() { + McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeRef<>() { }); assertThat(result).isNotNull(); assertThat(result.action()).isEqualTo(action); @@ -551,4 +551,4 @@ void testPingMessageRequestHandling() { asyncMcpClient.closeGracefully(); } -} \ No newline at end of file +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java index ae33898b7..459687797 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java @@ -4,7 +4,7 @@ package io.modelcontextprotocol.client; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; @@ -73,8 +73,13 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { } @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return OBJECT_MAPPER.convertValue(data, typeRef); + public T unmarshalFrom(Object data, TypeRef typeRef) { + return OBJECT_MAPPER.convertValue(data, new com.fasterxml.jackson.core.type.TypeReference() { + @Override + public java.lang.reflect.Type getType() { + return typeRef.getType(); + } + }); } }; diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java index e088b8773..d3f59824d 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java @@ -14,14 +14,13 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -79,7 +78,8 @@ static class TestHttpClientSseClientTransport extends HttpClientSseClientTranspo public TestHttpClientSseClientTransport(final String baseUri) { super(HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(), HttpRequest.newBuilder().header("Content-Type", "application/json"), baseUri, "/sse", - new ObjectMapper(), McpAsyncHttpClientRequestCustomizer.NOOP); + new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()), + McpAsyncHttpClientRequestCustomizer.NOOP); } public int getInboundMessageCount() { diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java index 104349116..80ec8462b 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java @@ -10,10 +10,10 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.spec.json.TypeRef; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -66,7 +66,7 @@ void testListRootsWithSinglePage() { McpSchema.ListRootsResult singlePageResult = new McpSchema.ListRootsResult(roots, null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(singlePageResult)); StepVerifier.create(exchange.listRoots()).assertNext(result -> { @@ -94,11 +94,11 @@ void testListRootsWithMultiplePages() { McpSchema.ListRootsResult page2Result = new McpSchema.ListRootsResult(page2Roots, null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest(null)), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page1Result)); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor1")), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page2Result)); StepVerifier.create(exchange.listRoots()).assertNext(result -> { @@ -120,7 +120,7 @@ void testListRootsWithEmptyResult() { McpSchema.ListRootsResult emptyResult = new McpSchema.ListRootsResult(new ArrayList<>(), null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(emptyResult)); StepVerifier.create(exchange.listRoots()).assertNext(result -> { @@ -140,7 +140,7 @@ void testListRootsWithSpecificCursor() { McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(roots, "nextCursor"); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("someCursor")), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(result)); StepVerifier.create(exchange.listRoots("someCursor")).assertNext(listResult -> { @@ -154,7 +154,7 @@ void testListRootsWithSpecificCursor() { void testListRootsWithError() { when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.error(new RuntimeException("Network error"))); // When & Then @@ -175,11 +175,11 @@ void testListRootsUnmodifiabilityAfterAccumulation() { McpSchema.ListRootsResult page2Result = new McpSchema.ListRootsResult(page2Roots, null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest(null)), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page1Result)); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor1")), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page2Result)); StepVerifier.create(exchange.listRoots()).assertNext(result -> { @@ -314,8 +314,7 @@ void testCreateElicitationWithNullCapabilities() { }); // Verify that sendRequest was never called due to null capabilities - verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), - any(TypeReference.class)); + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), any(TypeRef.class)); } @Test @@ -339,8 +338,7 @@ void testCreateElicitationWithoutElicitationCapabilities() { // Verify that sendRequest was never called due to missing elicitation // capabilities - verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), - any(TypeReference.class)); + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), any(TypeRef.class)); } @Test @@ -374,8 +372,7 @@ void testCreateElicitationWithComplexRequest() { .content(responseContent) .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); StepVerifier.create(exchangeWithElicitation.createElicitation(elicitRequest)).assertNext(result -> { @@ -405,8 +402,7 @@ void testCreateElicitationWithDeclineAction() { .message(McpSchema.ElicitResult.Action.DECLINE) .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); StepVerifier.create(exchangeWithElicitation.createElicitation(elicitRequest)).assertNext(result -> { @@ -433,8 +429,7 @@ void testCreateElicitationWithCancelAction() { .message(McpSchema.ElicitResult.Action.CANCEL) .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); StepVerifier.create(exchangeWithElicitation.createElicitation(elicitRequest)).assertNext(result -> { @@ -457,8 +452,7 @@ void testCreateElicitationWithSessionError() { .message("Please provide your name") .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.error(new RuntimeException("Session communication error"))); StepVerifier.create(exchangeWithElicitation.createElicitation(elicitRequest)).verifyErrorSatisfies(error -> { @@ -488,7 +482,7 @@ void testCreateMessageWithNullCapabilities() { // Verify that sendRequest was never called due to null capabilities verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), any(), - any(TypeReference.class)); + any(TypeRef.class)); } @Test @@ -513,7 +507,7 @@ void testCreateMessageWithoutSamplingCapabilities() { // Verify that sendRequest was never called due to missing sampling capabilities verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), any(), - any(TypeReference.class)); + any(TypeRef.class)); } @Test @@ -539,7 +533,7 @@ void testCreateMessageWithBasicRequest() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); StepVerifier.create(exchangeWithSampling.createMessage(createMessageRequest)).assertNext(result -> { @@ -577,7 +571,7 @@ void testCreateMessageWithImageContent() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); StepVerifier.create(exchangeWithSampling.createMessage(createMessageRequest)).assertNext(result -> { @@ -603,7 +597,7 @@ void testCreateMessageWithSessionError() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.error(new RuntimeException("Session communication error"))); StepVerifier.create(exchangeWithSampling.createMessage(createMessageRequest)).verifyErrorSatisfies(error -> { @@ -635,7 +629,7 @@ void testCreateMessageWithIncludeContext() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); StepVerifier.create(exchangeWithSampling.createMessage(createMessageRequest)).assertNext(result -> { @@ -653,7 +647,7 @@ void testPingWithSuccessfulResponse() { java.util.Map expectedResponse = java.util.Map.of(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class))) .thenReturn(Mono.just(expectedResponse)); StepVerifier.create(exchange.ping()).assertNext(result -> { @@ -662,14 +656,14 @@ void testPingWithSuccessfulResponse() { }).verifyComplete(); // Verify that sendRequest was called with correct parameters - verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)); + verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class)); } @Test void testPingWithMcpError() { // Given - Mock an MCP-specific error during ping McpError mcpError = new McpError("Server unavailable"); - when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class))) .thenReturn(Mono.error(mcpError)); // When & Then @@ -677,13 +671,13 @@ void testPingWithMcpError() { assertThat(error).isInstanceOf(McpError.class).hasMessage("Server unavailable"); }); - verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)); + verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class)); } @Test void testPingMultipleCalls() { - when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class))) .thenReturn(Mono.just(Map.of())) .thenReturn(Mono.just(Map.of())); @@ -698,7 +692,7 @@ void testPingMultipleCalls() { }).verifyComplete(); // Verify that sendRequest was called twice - verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)); + verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class)); } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java index a73ec7209..96071834e 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java @@ -9,10 +9,10 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.spec.json.TypeRef; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -66,7 +66,7 @@ void testListRootsWithSinglePage() { McpSchema.ListRootsResult singlePageResult = new McpSchema.ListRootsResult(roots, null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(singlePageResult)); McpSchema.ListRootsResult result = exchange.listRoots(); @@ -94,11 +94,11 @@ void testListRootsWithMultiplePages() { McpSchema.ListRootsResult page2Result = new McpSchema.ListRootsResult(page2Roots, null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest(null)), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page1Result)); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor1")), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page2Result)); McpSchema.ListRootsResult result = exchange.listRoots(); @@ -120,7 +120,7 @@ void testListRootsWithEmptyResult() { McpSchema.ListRootsResult emptyResult = new McpSchema.ListRootsResult(new ArrayList<>(), null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(emptyResult)); McpSchema.ListRootsResult result = exchange.listRoots(); @@ -140,7 +140,7 @@ void testListRootsWithSpecificCursor() { McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(roots, "nextCursor"); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("someCursor")), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(result)); McpSchema.ListRootsResult listResult = exchange.listRoots("someCursor"); @@ -154,7 +154,7 @@ void testListRootsWithSpecificCursor() { void testListRootsWithError() { when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.error(new RuntimeException("Network error"))); // When & Then @@ -173,11 +173,11 @@ void testListRootsUnmodifiabilityAfterAccumulation() { McpSchema.ListRootsResult page2Result = new McpSchema.ListRootsResult(page2Roots, null); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest(null)), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page1Result)); when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor1")), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(page2Result)); McpSchema.ListRootsResult result = exchange.listRoots(); @@ -308,8 +308,7 @@ void testCreateElicitationWithNullCapabilities() { .hasMessage("Client must be initialized. Call the initialize method first!"); // Verify that sendRequest was never called due to null capabilities - verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), - any(TypeReference.class)); + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), any(TypeRef.class)); } @Test @@ -333,8 +332,7 @@ void testCreateElicitationWithoutElicitationCapabilities() { // Verify that sendRequest was never called due to missing elicitation // capabilities - verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), - any(TypeReference.class)); + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), any(TypeRef.class)); } @Test @@ -369,8 +367,7 @@ void testCreateElicitationWithComplexRequest() { .content(responseContent) .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); McpSchema.ElicitResult result = exchangeWithElicitation.createElicitation(elicitRequest); @@ -401,8 +398,7 @@ void testCreateElicitationWithDeclineAction() { .message(McpSchema.ElicitResult.Action.DECLINE) .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); McpSchema.ElicitResult result = exchangeWithElicitation.createElicitation(elicitRequest); @@ -430,8 +426,7 @@ void testCreateElicitationWithCancelAction() { .message(McpSchema.ElicitResult.Action.CANCEL) .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); McpSchema.ElicitResult result = exchangeWithElicitation.createElicitation(elicitRequest); @@ -455,8 +450,7 @@ void testCreateElicitationWithSessionError() { .message("Please provide your name") .build(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), - any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) .thenReturn(Mono.error(new RuntimeException("Session communication error"))); assertThatThrownBy(() -> exchangeWithElicitation.createElicitation(elicitRequest)) @@ -487,7 +481,7 @@ void testCreateMessageWithNullCapabilities() { // Verify that sendRequest was never called due to null capabilities verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), any(), - any(TypeReference.class)); + any(TypeRef.class)); } @Test @@ -512,7 +506,7 @@ void testCreateMessageWithoutSamplingCapabilities() { // Verify that sendRequest was never called due to missing sampling capabilities verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), any(), - any(TypeReference.class)); + any(TypeRef.class)); } @Test @@ -539,7 +533,7 @@ void testCreateMessageWithBasicRequest() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); McpSchema.CreateMessageResult result = exchangeWithSampling.createMessage(createMessageRequest); @@ -578,7 +572,7 @@ void testCreateMessageWithImageContent() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); McpSchema.CreateMessageResult result = exchangeWithSampling.createMessage(createMessageRequest); @@ -605,7 +599,7 @@ void testCreateMessageWithSessionError() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.error(new RuntimeException("Session communication error"))); assertThatThrownBy(() -> exchangeWithSampling.createMessage(createMessageRequest)) @@ -638,7 +632,7 @@ void testCreateMessageWithIncludeContext() { .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), - any(TypeReference.class))) + any(TypeRef.class))) .thenReturn(Mono.just(expectedResult)); McpSchema.CreateMessageResult result = exchangeWithSampling.createMessage(createMessageRequest); @@ -656,32 +650,32 @@ void testPingWithSuccessfulResponse() { java.util.Map expectedResponse = java.util.Map.of(); - when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class))) .thenReturn(Mono.just(expectedResponse)); exchange.ping(); // Verify that sendRequest was called with correct parameters - verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)); + verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class)); } @Test void testPingWithMcpError() { // Given - Mock an MCP-specific error during ping McpError mcpError = new McpError("Server unavailable"); - when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class))) .thenReturn(Mono.error(mcpError)); // When & Then assertThatThrownBy(() -> exchange.ping()).isInstanceOf(McpError.class).hasMessage("Server unavailable"); - verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)); + verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class)); } @Test void testPingMultipleCalls() { - when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class))) + when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class))) .thenReturn(Mono.just(Map.of())) .thenReturn(Mono.just(Map.of())); @@ -692,7 +686,7 @@ void testPingMultipleCalls() { exchange.ping(); // Verify that sendRequest was called twice - verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)); + verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeRef.class)); } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java index 14987b5ac..1d6dde66b 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java @@ -19,6 +19,7 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -75,7 +76,8 @@ void setUp() { when(mockSession.closeGracefully()).thenReturn(Mono.empty()); when(mockSession.sendNotification(any(), any())).thenReturn(Mono.empty()); - transportProvider = new StdioServerTransportProvider(objectMapper, System.in, testOutPrintStream); + transportProvider = new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper), System.in, + testOutPrintStream); } @AfterEach @@ -105,7 +107,8 @@ void shouldHandleIncomingMessages() throws Exception { String jsonMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"test\",\"params\":{},\"id\":1}\n"; InputStream stream = new ByteArrayInputStream(jsonMessage.getBytes(StandardCharsets.UTF_8)); - transportProvider = new StdioServerTransportProvider(objectMapper, stream, System.out); + transportProvider = new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper), stream, + System.out); // Set up a real session to capture the message AtomicReference capturedMessage = new AtomicReference<>(); CountDownLatch messageLatch = new CountDownLatch(1); @@ -200,7 +203,8 @@ void shouldHandleInvalidJsonMessage() throws Exception { String jsonMessage = "{invalid json}\n"; InputStream stream = new ByteArrayInputStream(jsonMessage.getBytes(StandardCharsets.UTF_8)); - transportProvider = new StdioServerTransportProvider(objectMapper, stream, testOutPrintStream); + transportProvider = new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper), stream, + testOutPrintStream); // Set up a session factory transportProvider.setSessionFactory(sessionFactory); diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java index 85dcd26c2..af20f96e2 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java @@ -7,8 +7,8 @@ import java.time.Duration; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.MockMcpClientTransport; +import io.modelcontextprotocol.spec.json.TypeRef; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -68,7 +68,7 @@ void testConstructorWithInvalidArguments() { .hasMessageContaining("transport can not be null"); } - TypeReference responseType = new TypeReference<>() { + TypeRef responseType = new TypeRef<>() { }; @Test diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java new file mode 100644 index 000000000..d799b3d22 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java @@ -0,0 +1,97 @@ +package io.modelcontextprotocol.spec.json.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import io.modelcontextprotocol.spec.json.McpJsonMapper; +import io.modelcontextprotocol.spec.json.TypeRef; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Test-only Gson-based implementation of McpJsonMapper. This lives under src/test/java so + * it doesn't affect production code or dependencies. + */ +public final class GsonMcpJsonMapper implements McpJsonMapper { + + private final Gson gson; + + public GsonMcpJsonMapper() { + this(new GsonBuilder().serializeNulls() + // Ensure numeric values in untyped (Object) fields preserve integral numbers + // as Long + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .create()); + } + + public GsonMcpJsonMapper(Gson gson) { + if (gson == null) { + throw new IllegalArgumentException("Gson must not be null"); + } + this.gson = gson; + } + + public Gson getGson() { + return gson; + } + + @Override + public T readValue(String content, Class type) throws IOException { + try { + return gson.fromJson(content, type); + } + catch (Exception e) { + throw new IOException("Failed to deserialize JSON", e); + } + } + + @Override + public T readValue(byte[] content, Class type) throws IOException { + return readValue(new String(content, StandardCharsets.UTF_8), type); + } + + @Override + public T readValue(String content, TypeRef type) throws IOException { + try { + return gson.fromJson(content, type.getType()); + } + catch (Exception e) { + throw new IOException("Failed to deserialize JSON", e); + } + } + + @Override + public T readValue(byte[] content, TypeRef type) throws IOException { + return readValue(new String(content, StandardCharsets.UTF_8), type); + } + + @Override + public T convertValue(Object fromValue, Class type) { + String json = gson.toJson(fromValue); + return gson.fromJson(json, type); + } + + @Override + public T convertValue(Object fromValue, TypeRef type) { + String json = gson.toJson(fromValue); + return gson.fromJson(json, type.getType()); + } + + @Override + public String writeValueAsString(Object value) throws IOException { + try { + return gson.toJson(value); + } + catch (Exception e) { + throw new IOException("Failed to serialize to JSON", e); + } + } + + @Override + public byte[] writeValueAsBytes(Object value) throws IOException { + return writeValueAsString(value).getBytes(StandardCharsets.UTF_8); + } + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java new file mode 100644 index 000000000..7e88097f8 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java @@ -0,0 +1,142 @@ +package io.modelcontextprotocol.spec.json.gson; + +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class GsonMcpJsonMapperTests { + + record Person(String name, int age) { + } + + @Test + void roundTripSimplePojo() throws IOException { + var mapper = new GsonMcpJsonMapper(); + + var input = new Person("Alice", 30); + String json = mapper.writeValueAsString(input); + assertNotNull(json); + assertTrue(json.contains("\"Alice\"")); + assertTrue(json.contains("\"age\"")); + + var decoded = mapper.readValue(json, Person.class); + assertEquals(input, decoded); + + byte[] bytes = mapper.writeValueAsBytes(input); + assertNotNull(bytes); + var decodedFromBytes = mapper.readValue(bytes, Person.class); + assertEquals(input, decodedFromBytes); + } + + @Test + void readWriteParameterizedTypeWithTypeRef() throws IOException { + var mapper = new GsonMcpJsonMapper(); + String json = "[\"a\", \"b\", \"c\"]"; + + List list = mapper.readValue(json, new TypeRef>() { + }); + assertEquals(List.of("a", "b", "c"), list); + + String encoded = mapper.writeValueAsString(list); + assertTrue(encoded.startsWith("[")); + assertTrue(encoded.contains("\"a\"")); + } + + @Test + void convertValueMapToRecordAndParameterized() { + var mapper = new GsonMcpJsonMapper(); + Map src = Map.of("name", "Bob", "age", 42); + + // Convert to simple record + Person person = mapper.convertValue(src, Person.class); + assertEquals(new Person("Bob", 42), person); + + // Convert to parameterized Map + Map toMap = mapper.convertValue(person, new TypeRef>() { + }); + assertEquals("Bob", toMap.get("name")); + assertEquals(42.0, ((Number) toMap.get("age")).doubleValue(), 0.0); // Gson may + // emit double + // for + // primitives + } + + @Test + void deserializeJsonRpcMessageRequestUsingCustomMapper() throws IOException { + var mapper = new GsonMcpJsonMapper(); + + String json = """ + { + "jsonrpc": "2.0", + "id": 1, + "method": "ping", + "params": { "x": 1, "y": "z" } + } + """; + + var msg = McpSchema.deserializeJsonRpcMessage(mapper, json); + assertTrue(msg instanceof McpSchema.JSONRPCRequest); + + var req = (McpSchema.JSONRPCRequest) msg; + assertEquals("2.0", req.jsonrpc()); + assertEquals("ping", req.method()); + assertNotNull(req.id()); + assertEquals("1", req.id().toString()); + + assertNotNull(req.params()); + assertInstanceOf(Map.class, req.params()); + @SuppressWarnings("unchecked") + var params = (Map) req.params(); + assertEquals(1.0, ((Number) params.get("x")).doubleValue(), 0.0); + assertEquals("z", params.get("y")); + } + + @Test + void integrateWithMcpSchemaStaticMapperForStringParsing() { + var gsonMapper = new GsonMcpJsonMapper(); + + // Save and restore static mapper to avoid affecting other tests + var originalMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); + try { + McpSchema.setJsonMapper(gsonMapper); + + // Tool builder parsing of input/output schema strings + var tool = McpSchema.Tool.builder().name("echo").description("Echo tool").inputSchema(""" + { + "type": "object", + "properties": { "x": { "type": "integer" } }, + "required": ["x"] + } + """).outputSchema(""" + { + "type": "object", + "properties": { "y": { "type": "string" } } + } + """).build(); + + assertNotNull(tool.inputSchema()); + assertNotNull(tool.outputSchema()); + assertTrue(tool.outputSchema().containsKey("properties")); + + // CallToolRequest builder parsing of JSON arguments string + var call = McpSchema.CallToolRequest.builder().name("echo").arguments("{\"x\": 123}").build(); + + assertEquals("echo", call.name()); + assertNotNull(call.arguments()); + assertTrue(call.arguments().get("x") instanceof Number); + assertEquals(123.0, ((Number) call.arguments().get("x")).doubleValue(), 0.0); + } + finally { + // restore to a Jackson-backed default to avoid side effects + McpSchema.setJsonMapper(originalMapper); + } + } + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java b/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java index 4de9363c2..26a44f199 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java @@ -16,7 +16,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.spec.json.TypeRef; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSession; @@ -259,7 +259,7 @@ private static class MockMcpSession implements McpSession { private boolean shouldFailPing = false; @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { + public Mono sendRequest(String method, Object requestParams, TypeRef typeRef) { if (McpSchema.METHOD_PING.equals(method)) { pingCount.incrementAndGet(); if (shouldFailPing) { From 6eb70cf6527c4bfca73d922fd0934720f7f96ede Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 12 Sep 2025 09:42:41 +0200 Subject: [PATCH 2/7] Make JSON Mapper SPI (#1) This pull request creates two modules, `mcp-json` and `mcp-json-jackson`. It removes the `com.fasterxml.jackson.core:jackson-databind` and `com.networknt:json-schema-validator` dependencies from the `mcp` module. The `mcp` module now only depends on `com.fasterxml.jackson.core:jackson-annotations`. To use Jackson, you have to add `mcp-jackson` to your dependencies in addition to `mcp`. I added the dependency `mcp-jackson` to both `mcp-spring-mvc` and `mcp-spring-webflux` to avoid a breaking change in those modules. It provides two [SPI](https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html) `JsonSchemaValidatorSupplier` and `JacksonJsonSchemaValidatorSupplier` to allow easy replacement for consumers who don't want to use Jackson. This pull request also ensures no `McpJsonMapper` is instantiated if one is provided via a builder method. Only if the builders don't receive a `McpJsonMapper` mapper, one is instantiated in the `build` method of the builder. The logic behind this is to allow frameworks to provide a `McpJsonMapper` mapper singleton implementation and feed it to the builders without paying the price of instantiating `McpJsonMappers`, which will not be used. The goal is to be able to use the `ObjectMapper` singleton of an application also for the MCP code. ## Breaking changes - This pull request also removes the deprecated `Tool` constructors, and it updates every test to use the builder API to instantiate tools. - Several constructors taking an ObjectMapper or constructor which forced the instantiation of a generic `McpJsonMapper` have been removed. --- mcp-json-jackson/pom.xml | 53 ++++ .../json/jackson/JacksonMcpJsonMapper.java | 6 +- .../jackson/JacksonMcpJsonMapperSupplier.java | 13 + .../jackson}/DefaultJsonSchemaValidator.java | 34 +-- .../JacksonJsonSchemaValidatorSupplier.java | 13 + ...contextprotocol.json.McpJsonMapperSupplier | 1 + ...ol.json.schema.JsonSchemaValidatorSupplier | 1 + mcp-json/pom.xml | 39 +++ .../json/McpJsonMapper.java | 52 +++- .../json/McpJsonMapperSupplier.java | 10 + .../modelcontextprotocol}/json/TypeRef.java | 2 +- .../json/schema/JsonSchemaValidator.java | 95 ++++++++ .../schema/JsonSchemaValidatorSupplier.java | 7 + mcp-spring/mcp-spring-webflux/pom.xml | 8 +- .../WebClientStreamableHttpTransport.java | 23 +- .../transport/WebFluxSseClientTransport.java | 34 +-- .../WebFluxSseServerTransportProvider.java | 117 ++------- .../WebFluxStatelessServerTransport.java | 24 +- ...FluxStreamableServerTransportProvider.java | 50 ++-- .../WebFluxSseIntegrationTests.java | 3 - .../WebFluxStatelessIntegrationTests.java | 4 - .../WebFluxStreamableIntegrationTests.java | 3 - .../client/WebFluxSseMcpSyncClientTests.java | 1 - .../WebFluxSseClientTransportTests.java | 21 +- ...erMcpTransportContextIntegrationTests.java | 4 - ...erMcpTransportContextIntegrationTests.java | 4 - .../server/WebFluxSseMcpAsyncServerTests.java | 4 +- .../server/WebFluxSseMcpSyncServerTests.java | 5 +- .../WebFluxStreamableMcpAsyncServerTests.java | 2 - .../WebFluxStreamableMcpSyncServerTests.java | 2 - .../utils/McpJsonMapperUtils.java | 12 + mcp-spring/mcp-spring-webmvc/pom.xml | 8 +- .../WebMvcSseServerTransportProvider.java | 116 ++------- .../WebMvcStatelessServerTransport.java | 29 ++- ...bMvcStreamableServerTransportProvider.java | 49 ++-- .../McpTransportContextIntegrationTests.java | 13 +- ...cpStreamableAsyncServerTransportTests.java | 7 +- ...McpStreamableSyncServerTransportTests.java | 7 +- .../WebMvcSseAsyncServerTransportTests.java | 6 +- .../WebMvcSseCustomContextPathTests.java | 2 - .../server/WebMvcSseIntegrationTests.java | 3 - .../WebMvcSseSyncServerTransportTests.java | 6 +- .../WebMvcStatelessIntegrationTests.java | 7 +- .../WebMvcStreamableIntegrationTests.java | 3 - ...stractMcpClientServerIntegrationTests.java | 44 ++-- .../AbstractStatelessIntegrationTests.java | 18 +- .../MockMcpTransport.java | 6 +- .../client/AbstractMcpAsyncClientTests.java | 11 +- .../client/AbstractMcpSyncClientTests.java | 2 - .../server/AbstractMcpAsyncServerTests.java | 57 +++-- .../server/AbstractMcpSyncServerTests.java | 47 ++-- .../utils/McpJsonMapperUtils.java | 12 + .../utils/ToolsUtils.java | 15 ++ mcp/pom.xml | 34 +-- .../client/McpAsyncClient.java | 2 +- .../HttpClientSseClientTransport.java | 103 +------- .../HttpClientStreamableHttpTransport.java | 28 +-- .../transport/StdioClientTransport.java | 24 +- .../server/McpAsyncServer.java | 6 +- .../server/McpAsyncServerExchange.java | 2 +- .../server/McpServer.java | 132 +++------- .../server/McpServerFeatures.java | 11 +- .../server/McpStatelessAsyncServer.java | 6 +- ...HttpServletSseServerTransportProvider.java | 98 +------- .../HttpServletStatelessServerTransport.java | 25 +- ...vletStreamableServerTransportProvider.java | 29 +-- .../StdioServerTransportProvider.java | 24 +- .../spec/JsonSchemaValidator.java | 45 ---- .../spec/McpClientSession.java | 2 +- .../modelcontextprotocol/spec/McpSchema.java | 104 ++------ .../spec/McpServerSession.java | 2 +- .../modelcontextprotocol/spec/McpSession.java | 2 +- .../spec/McpStreamableServerSession.java | 2 +- .../spec/McpTransport.java | 2 +- .../spec/MissingMcpTransportSession.java | 2 +- .../util/KeepAliveScheduler.java | 2 +- .../MockMcpClientTransport.java | 6 +- .../MockMcpServerTransport.java | 6 +- .../client/AbstractMcpAsyncClientTests.java | 10 +- .../client/AbstractMcpSyncClientTests.java | 2 - ...pClientStreamableHttpAsyncClientTests.java | 1 - .../McpAsyncClientResponseHandlerTests.java | 23 +- .../client/McpAsyncClientTests.java | 8 +- .../client/ServerParameterUtils.java | 19 ++ .../client/StdioMcpAsyncClientTests.java | 16 +- .../client/StdioMcpSyncClientTests.java | 16 +- .../HttpClientSseClientTransportTests.java | 5 +- ...HttpClientStreamableHttpTransportTest.java | 1 - ...erMcpTransportContextIntegrationTests.java | 4 - ...erMcpTransportContextIntegrationTests.java | 4 - .../server/AbstractMcpAsyncServerTests.java | 66 +++-- ...stractMcpClientServerIntegrationTests.java | 45 ++-- .../server/AbstractMcpSyncServerTests.java | 60 +++-- .../AsyncToolSpecificationBuilderTest.java | 50 +++- .../HttpServletSseIntegrationTests.java | 2 - .../HttpServletStatelessIntegrationTests.java | 20 +- ...HttpServletStreamableAsyncServerTests.java | 7 +- ...HttpServletStreamableIntegrationTests.java | 2 - .../HttpServletStreamableSyncServerTests.java | 7 +- .../server/McpAsyncServerExchangeTests.java | 2 +- .../server/McpCompletionTests.java | 4 +- .../server/McpSyncServerExchangeTests.java | 2 +- .../server/StdioMcpAsyncServerTests.java | 4 +- .../server/StdioMcpSyncServerTests.java | 4 +- .../SyncToolSpecificationBuilderTest.java | 19 +- ...ervletSseServerCustomContextPathTests.java | 3 - .../StdioServerTransportProviderTests.java | 18 +- .../spec/DefaultJsonSchemaValidatorTests.java | 3 +- .../spec/McpClientSessionTests.java | 2 +- .../spec/McpSchemaTests.java | 228 ++++++++++-------- .../spec/json/gson/GsonMcpJsonMapper.java | 4 +- .../json/gson/GsonMcpJsonMapperTests.java | 70 +++--- .../util/KeepAliveSchedulerTests.java | 2 +- .../util/McpJsonMapperUtils.java | 12 + .../modelcontextprotocol/util/ToolsUtils.java | 15 ++ pom.xml | 2 + 116 files changed, 1138 insertions(+), 1438 deletions(-) create mode 100644 mcp-json-jackson/pom.xml rename {mcp/src/main/java/io/modelcontextprotocol/spec => mcp-json-jackson/src/main/java/io/modelcontextprotocol}/json/jackson/JacksonMcpJsonMapper.java (92%) create mode 100644 mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java rename {mcp/src/main/java/io/modelcontextprotocol/spec => mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson}/DefaultJsonSchemaValidator.java (84%) create mode 100644 mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java create mode 100644 mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier create mode 100644 mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier create mode 100644 mcp-json/pom.xml rename {mcp/src/main/java/io/modelcontextprotocol/spec => mcp-json/src/main/java/io/modelcontextprotocol}/json/McpJsonMapper.java (63%) create mode 100644 mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java rename {mcp/src/main/java/io/modelcontextprotocol/spec => mcp-json/src/main/java/io/modelcontextprotocol}/json/TypeRef.java (93%) create mode 100644 mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java create mode 100644 mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java create mode 100644 mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java create mode 100644 mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java create mode 100644 mcp-test/src/main/java/io/modelcontextprotocol/utils/ToolsUtils.java delete mode 100644 mcp/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java create mode 100644 mcp/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java create mode 100644 mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java create mode 100644 mcp/src/test/java/io/modelcontextprotocol/util/ToolsUtils.java diff --git a/mcp-json-jackson/pom.xml b/mcp-json-jackson/pom.xml new file mode 100644 index 000000000..4cdc318b4 --- /dev/null +++ b/mcp-json-jackson/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.13.0-SNAPSHOT + + mcp-json-jackson + jar + Java MCP SDK JSON Jackson + Java MCP SDK JSON implementation based on Jackson + https://github.com/modelcontextprotocol/java-sdk + + https://github.com/modelcontextprotocol/java-sdk + git://github.com/modelcontextprotocol/java-sdk.git + git@github.com/modelcontextprotocol/java-sdk.git + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + + + + + + + io.modelcontextprotocol.sdk + mcp-json + 0.13.0-SNAPSHOT + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.networknt + json-schema-validator + ${json-schema-validator.version} + + + diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/json/jackson/JacksonMcpJsonMapper.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java similarity index 92% rename from mcp/src/main/java/io/modelcontextprotocol/spec/json/jackson/JacksonMcpJsonMapper.java rename to mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java index 46dbf7e8a..2323af0ba 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/json/jackson/JacksonMcpJsonMapper.java +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java @@ -1,9 +1,9 @@ -package io.modelcontextprotocol.spec.json.jackson; +package io.modelcontextprotocol.json.jackson; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import java.io.IOException; diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java new file mode 100644 index 000000000..2c03d8002 --- /dev/null +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java @@ -0,0 +1,13 @@ +package io.modelcontextprotocol.json.jackson; + +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapperSupplier; + +public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier { + + @Override + public McpJsonMapper get() { + return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); + } + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java similarity index 84% rename from mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java rename to mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java index 5d3d677a9..48891fa7b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java @@ -2,12 +2,13 @@ * Copyright 2024-2024 the original author or authors. */ -package io.modelcontextprotocol.spec; +package io.modelcontextprotocol.json.schema.jackson; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,10 +21,6 @@ import com.networknt.schema.SpecVersion; import com.networknt.schema.ValidationMessage; -import io.modelcontextprotocol.util.Assert; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; - /** * Default implementation of the {@link JsonSchemaValidator} interface. This class * provides methods to validate structured content against a JSON schema. It uses the @@ -52,29 +49,14 @@ public DefaultJsonSchemaValidator(ObjectMapper objectMapper) { this.schemaCache = new ConcurrentHashMap<>(); } - /** - * Alternate constructor that accepts a JsonMapper. If the mapper is backed by - * Jackson, the underlying ObjectMapper will be reused; otherwise a new ObjectMapper - * will be created for schema validation only. - */ - public DefaultJsonSchemaValidator(McpJsonMapper jsonMapper) { - ObjectMapper mapper; - if (jsonMapper instanceof JacksonMcpJsonMapper jacksonJsonMapper) { - mapper = jacksonJsonMapper.getObjectMapper(); - } - else { - mapper = new ObjectMapper(); - } - this.objectMapper = mapper; - this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); - this.schemaCache = new ConcurrentHashMap<>(); - } - @Override public ValidationResponse validate(Map schema, Map structuredContent) { - - Assert.notNull(schema, "Schema must not be null"); - Assert.notNull(structuredContent, "Structured content must not be null"); + if (schema == null) { + throw new IllegalArgumentException("Schema must not be null"); + } + if (structuredContent == null) { + throw new IllegalArgumentException("Structured content must not be null"); + } try { diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java new file mode 100644 index 000000000..5b454ecb4 --- /dev/null +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java @@ -0,0 +1,13 @@ +package io.modelcontextprotocol.json.schema.jackson; + +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; +import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier; + +public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier { + + @Override + public JsonSchemaValidator get() { + return new DefaultJsonSchemaValidator(); + } + +} diff --git a/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier b/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier new file mode 100644 index 000000000..8ea66d698 --- /dev/null +++ b/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier @@ -0,0 +1 @@ +io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapperSupplier \ No newline at end of file diff --git a/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier b/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier new file mode 100644 index 000000000..0fb0b7e5a --- /dev/null +++ b/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier @@ -0,0 +1 @@ +io.modelcontextprotocol.json.schema.jackson.JacksonJsonSchemaValidatorSupplier \ No newline at end of file diff --git a/mcp-json/pom.xml b/mcp-json/pom.xml new file mode 100644 index 000000000..05b15b5ff --- /dev/null +++ b/mcp-json/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.13.0-SNAPSHOT + + mcp-json + jar + Java MCP SDK JSON Schema + Java MCP SDK JSON Schema API + https://github.com/modelcontextprotocol/java-sdk + + https://github.com/modelcontextprotocol/java-sdk + git://github.com/modelcontextprotocol/java-sdk.git + git@github.com/modelcontextprotocol/java-sdk.git + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + + + + + + + + diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/json/McpJsonMapper.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java similarity index 63% rename from mcp/src/main/java/io/modelcontextprotocol/spec/json/McpJsonMapper.java rename to mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java index f48a525b6..899faa80e 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/json/McpJsonMapper.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java @@ -1,6 +1,9 @@ -package io.modelcontextprotocol.spec.json; +package io.modelcontextprotocol.json; import java.io.IOException; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; /** * Abstraction for JSON serialization/deserialization to decouple the SDK from any @@ -83,4 +86,51 @@ public interface McpJsonMapper { */ byte[] writeValueAsBytes(Object value) throws IOException; + /** + * Resolves the default {@link McpJsonMapper}. + * @return The default {@link McpJsonMapper} + * @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on + * the classpath. + */ + static McpJsonMapper createDefault() { + AtomicReference ex = new AtomicReference<>(); + return ServiceLoader.load(McpJsonMapperSupplier.class).stream().flatMap(p -> { + try { + McpJsonMapperSupplier supplier = p.get(); + return Stream.ofNullable(supplier); + } + catch (Exception e) { + addException(ex, e); + return Stream.empty(); + } + }).flatMap(jsonMapperSupplier -> { + try { + return Stream.of(jsonMapperSupplier.get()); + } + catch (Exception e) { + addException(ex, e); + return Stream.empty(); + } + }).findFirst().orElseThrow(() -> { + if (ex.get() != null) { + return ex.get(); + } + else { + return new IllegalStateException("No default McpJsonMapper implementation found"); + } + }); + } + + private static void addException(AtomicReference ref, Exception toAdd) { + ref.updateAndGet(existing -> { + if (existing == null) { + return new IllegalStateException("Failed to initialize default McpJsonMapper", toAdd); + } + else { + existing.addSuppressed(toAdd); + return existing; + } + }); + } + } diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java new file mode 100644 index 000000000..c2c917af1 --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java @@ -0,0 +1,10 @@ +package io.modelcontextprotocol.json; + +import java.util.function.Supplier; + +/** + * Strategy interface for resolving a {@link McpJsonMapper}. + */ +public interface McpJsonMapperSupplier extends Supplier { + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/json/TypeRef.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java similarity index 93% rename from mcp/src/main/java/io/modelcontextprotocol/spec/json/TypeRef.java rename to mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java index 61dcdaebc..efde45070 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/json/TypeRef.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.spec.json; +package io.modelcontextprotocol.json; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java new file mode 100644 index 000000000..6517d4728 --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.json.schema; + +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +/** + * Interface for validating structured content against a JSON schema. This interface + * defines a method to validate structured content based on the provided output schema. + * + * @author Christian Tzolov + */ +public interface JsonSchemaValidator { + + /** + * Represents the result of a validation operation. + * + * @param valid Indicates whether the validation was successful. + * @param errorMessage An error message if the validation failed, otherwise null. + * @param jsonStructuredOutput The text structured content in JSON format if the + * validation was successful, otherwise null. + */ + record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) { + + public static ValidationResponse asValid(String jsonStructuredOutput) { + return new ValidationResponse(true, null, jsonStructuredOutput); + } + + public static ValidationResponse asInvalid(String message) { + return new ValidationResponse(false, message, null); + } + } + + /** + * Validates the structured content against the provided JSON schema. + * @param schema The JSON schema to validate against. + * @param structuredContent The structured content to validate. + * @return A ValidationResponse indicating whether the validation was successful or + * not. + */ + ValidationResponse validate(Map schema, Map structuredContent); + + /** + * Resolves the default {@link JsonSchemaValidator}. + * @return The default {@link JsonSchemaValidator} + * @throws IllegalStateException If no {@link JsonSchemaValidator} implementation + * exists on the classpath. + */ + static JsonSchemaValidator createDefault() { + AtomicReference ex = new AtomicReference<>(); + return ServiceLoader.load(JsonSchemaValidatorSupplier.class).stream().flatMap(p -> { + try { + JsonSchemaValidatorSupplier supplier = p.get(); + return Stream.ofNullable(supplier); + } + catch (Exception e) { + addException(ex, e); + return Stream.empty(); + } + }).flatMap(jsonMapperSupplier -> { + try { + return Stream.of(jsonMapperSupplier.get()); + } + catch (Exception e) { + addException(ex, e); + return Stream.empty(); + } + }).findFirst().orElseThrow(() -> { + if (ex.get() != null) { + return ex.get(); + } + else { + return new IllegalStateException("No default JsonSchemaValidatorSupplier implementation found"); + } + }); + } + + private static void addException(AtomicReference ref, Exception toAdd) { + ref.updateAndGet(existing -> { + if (existing == null) { + return new IllegalStateException("Failed to initialize default JsonSchemaValidatorSupplier", toAdd); + } + else { + existing.addSuppressed(toAdd); + return existing; + } + }); + } + +} diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java new file mode 100644 index 000000000..249b6ef4c --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java @@ -0,0 +1,7 @@ +package io.modelcontextprotocol.json.schema; + +import java.util.function.Supplier; + +public interface JsonSchemaValidatorSupplier extends Supplier { + +} diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml index c2dac2bf9..d81e9017e 100644 --- a/mcp-spring/mcp-spring-webflux/pom.xml +++ b/mcp-spring/mcp-spring-webflux/pom.xml @@ -22,7 +22,13 @@ - + + io.modelcontextprotocol.sdk + mcp-json-jackson + 0.13.0-SNAPSHOT + + + io.modelcontextprotocol.sdk mcp 0.13.0-SNAPSHOT diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java index aec9caea7..2b4d872c1 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java @@ -22,9 +22,8 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.spec.DefaultMcpTransportSession; import io.modelcontextprotocol.spec.DefaultMcpTransportStream; @@ -489,17 +488,6 @@ private Builder(WebClient.Builder webClientBuilder) { this.webClientBuilder = webClientBuilder; } - /** - * Configure the {@link ObjectMapper} to use. - * @param objectMapper instance to use - * @return the builder instance - */ - public Builder objectMapper(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Configure the {@link McpJsonMapper} to use. * @param jsonMapper instance to use @@ -566,11 +554,8 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) { * @return a new instance of {@link WebClientStreamableHttpTransport} */ public WebClientStreamableHttpTransport build() { - McpJsonMapper jsonMapper = this.jsonMapper != null ? this.jsonMapper - : new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); - - return new WebClientStreamableHttpTransport(jsonMapper, this.webClientBuilder, endpoint, resumableStreams, - openConnectionOnStartup); + return new WebClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, + webClientBuilder, endpoint, resumableStreams, openConnectionOnStartup); } } 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 79ae26469..714d830dc 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 @@ -9,9 +9,8 @@ import java.util.function.BiConsumer; import java.util.function.Function; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.HttpHeaders; import io.modelcontextprotocol.spec.McpClientTransport; @@ -130,17 +129,6 @@ public class WebFluxSseClientTransport implements McpClientTransport { */ private String sseEndpoint; - /** - * Constructs a new SseClientTransport with the specified WebClient builder. Uses a - * default ObjectMapper instance for JSON processing. - * @param webClientBuilder the WebClient.Builder to use for creating the WebClient - * instance - * @throws IllegalArgumentException if webClientBuilder is null - */ - public WebFluxSseClientTransport(WebClient.Builder webClientBuilder) { - this(webClientBuilder, new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper())); - } - /** * Constructs a new SseClientTransport with the specified WebClient builder and * ObjectMapper. Initializes both inbound and outbound message processing pipelines. @@ -377,7 +365,7 @@ public static class Builder { private String sseEndpoint = DEFAULT_SSE_ENDPOINT; - private McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); + private McpJsonMapper jsonMapper; /** * Creates a new builder with the specified WebClient.Builder. @@ -399,19 +387,6 @@ public Builder sseEndpoint(String sseEndpoint) { return this; } - /** - * Sets the object mapper for JSON serialization/deserialization. - * @param objectMapper the Jackson ObjectMapper - * @return this builder - * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead - */ - @Deprecated(forRemoval = true) - public Builder objectMapper(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "objectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Sets the JSON mapper for serialization/deserialization. * @param jsonMapper the JsonMapper to use @@ -428,7 +403,8 @@ public Builder jsonMapper(McpJsonMapper jsonMapper) { * @return a new transport instance */ public WebFluxSseClientTransport build() { - return new WebFluxSseClientTransport(webClientBuilder, jsonMapper, sseEndpoint); + return new WebFluxSseClientTransport(webClientBuilder, + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, 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 8ff443ddb..374bf85e9 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 @@ -9,10 +9,8 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -99,7 +97,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv public static final String DEFAULT_BASE_URL = ""; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; /** * Base URL for the message endpoint. This is used to construct the full URL for @@ -133,82 +131,10 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv */ private KeepAliveScheduler keepAliveScheduler; - /** - * Constructs a new WebFlux SSE server transport provider instance with the default - * SSE endpoint. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of MCP messages. Must not be null. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages. This endpoint will be communicated to clients during SSE connection - * setup. Must not be null. - * @throws IllegalArgumentException if either parameter is null - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) { - this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT); - } - - /** - * Constructs a new WebFlux SSE server transport provider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of MCP messages. Must not be null. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages. This endpoint will be communicated to clients during SSE connection - * setup. Must not be null. - * @throws IllegalArgumentException if either parameter is null - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) { - this(objectMapper, DEFAULT_BASE_URL, messageEndpoint, sseEndpoint); - } - - /** - * Constructs a new WebFlux SSE server transport provider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of MCP messages. Must not be null. - * @param baseUrl webflux 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 - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, - String sseEndpoint) { - this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, null); - } - - /** - * Constructs a new WebFlux SSE server transport provider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of MCP messages. Must not be null. - * @param baseUrl webflux 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. - * @param sseEndpoint The SSE endpoint path. Must not be null. - * @param keepAliveInterval The interval for sending keep-alive pings to clients. - * @throws IllegalArgumentException if either parameter is null - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, - String sseEndpoint, Duration keepAliveInterval) { - this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, - (serverRequest) -> McpTransportContext.EMPTY); - } - /** * Constructs a new WebFlux SSE server transport provider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of MCP messages. Must not be null. + * @param jsonMapper The ObjectMapper 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 @@ -219,16 +145,16 @@ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseU * context from HTTP requests. Must not be null. * @throws IllegalArgumentException if either parameter is null */ - private WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, + private WebFluxSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint, String sseEndpoint, Duration keepAliveInterval, McpTransportContextExtractor contextExtractor) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Assert.notNull(jsonMapper, "ObjectMapper must not be null"); Assert.notNull(baseUrl, "Message base path must not be null"); Assert.notNull(messageEndpoint, "Message endpoint must not be null"); Assert.notNull(sseEndpoint, "SSE endpoint must not be null"); Assert.notNull(contextExtractor, "Context extractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.baseUrl = baseUrl; this.messageEndpoint = messageEndpoint; this.sseEndpoint = sseEndpoint; @@ -406,7 +332,7 @@ private Mono handleMessage(ServerRequest request) { return request.bodyToMono(String.class).flatMap(body -> { try { - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, 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 @@ -435,7 +361,7 @@ public WebFluxMcpSessionTransport(FluxSink> sink) { public Mono sendMessage(McpSchema.JSONRPCMessage message) { return Mono.fromSupplier(() -> { try { - return objectMapper.writeValueAsString(message); + return jsonMapper.writeValueAsString(message); } catch (IOException e) { throw Exceptions.propagate(e); @@ -455,7 +381,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { @Override public T unmarshalFrom(Object data, TypeRef typeRef) { - return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); + return jsonMapper.convertValue(data, typeRef); } @Override @@ -482,7 +408,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String baseUrl = DEFAULT_BASE_URL; @@ -496,15 +422,15 @@ public static class Builder { serverRequest) -> McpTransportContext.EMPTY; /** - * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP + * Sets the McpJsonMapper to use for JSON serialization/deserialization of MCP * messages. - * @param objectMapper The ObjectMapper instance. Must not be null. + * @param jsonMapper The McpJsonMapper instance. Must not be null. * @return this builder instance - * @throws IllegalArgumentException if objectMapper is null + * @throws IllegalArgumentException if jsonMapper is null */ - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -579,11 +505,10 @@ public Builder contextExtractor(McpTransportContextExtractor cont * @throws IllegalStateException if required parameters are not set */ 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, - keepAliveInterval, contextExtractor); + return new WebFluxSseServerTransportProvider( + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, baseUrl, messageEndpoint, + sseEndpoint, keepAliveInterval, contextExtractor); } } diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java index 62f3c7254..0a1c3d688 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java @@ -4,9 +4,7 @@ package io.modelcontextprotocol.server.transport; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpStatelessServerHandler; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -166,19 +164,6 @@ private Builder() { // used by a static method } - /** - * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP - * messages. - * @param objectMapper The ObjectMapper instance. Must not be null. - * @return this builder instance - * @throws IllegalArgumentException if objectMapper is null - */ - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Sets the JsonMapper to use for JSON serialization/deserialization of MCP * messages. @@ -227,12 +212,9 @@ public Builder contextExtractor(McpTransportContextExtractor cont * @throws IllegalStateException if required parameters are not set */ public WebFluxStatelessServerTransport build() { - if (this.jsonMapper == null) { - throw new IllegalStateException("JsonMapper must be set"); - } Assert.notNull(mcpEndpoint, "Message endpoint must be set"); - - return new WebFluxStatelessServerTransport(jsonMapper, mcpEndpoint, contextExtractor); + return new WebFluxStatelessServerTransport(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, + mcpEndpoint, contextExtractor); } } diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java index 273539127..a327d756d 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java @@ -4,10 +4,8 @@ package io.modelcontextprotocol.server.transport; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; import io.modelcontextprotocol.spec.HttpHeaders; @@ -51,7 +49,7 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe public static final String MESSAGE_EVENT_TYPE = "message"; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final String mcpEndpoint; @@ -69,14 +67,14 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe private KeepAliveScheduler keepAliveScheduler; - private WebFluxStreamableServerTransportProvider(ObjectMapper objectMapper, String mcpEndpoint, + private WebFluxStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint, McpTransportContextExtractor contextExtractor, boolean disallowDelete, Duration keepAliveInterval) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Assert.notNull(jsonMapper, "JsonMapper must not be null"); Assert.notNull(mcpEndpoint, "Message endpoint must not be null"); Assert.notNull(contextExtractor, "Context extractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.mcpEndpoint = mcpEndpoint; this.contextExtractor = contextExtractor; this.disallowDelete = disallowDelete; @@ -232,12 +230,13 @@ private Mono handlePost(ServerRequest request) { return request.bodyToMono(String.class).flatMap(body -> { try { - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body); if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest && jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) { - McpSchema.InitializeRequest initializeRequest = objectMapper.convertValue(jsonrpcRequest.params(), - new TypeReference() { - }); + var typeReference = new TypeRef() { + }; + McpSchema.InitializeRequest initializeRequest = jsonMapper.convertValue(jsonrpcRequest.params(), + typeReference); McpStreamableServerSession.McpStreamableServerSessionInit init = this.sessionFactory .startSession(initializeRequest); sessions.put(init.session().getId(), init.session()); @@ -245,7 +244,7 @@ private Mono handlePost(ServerRequest request) { McpSchema.JSONRPCResponse jsonrpcResponse = new McpSchema.JSONRPCResponse( McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initializeResult, null); try { - return this.objectMapper.writeValueAsString(jsonrpcResponse); + return this.jsonMapper.writeValueAsString(jsonrpcResponse); } catch (IOException e) { logger.warn("Failed to serialize initResponse", e); @@ -351,7 +350,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId) { return Mono.fromSupplier(() -> { try { - return objectMapper.writeValueAsString(message); + return jsonMapper.writeValueAsString(message); } catch (IOException e) { throw Exceptions.propagate(e); @@ -372,7 +371,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId @Override public T unmarshalFrom(Object data, TypeRef typeRef) { - return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); + return jsonMapper.convertValue(data, typeRef); } @Override @@ -399,7 +398,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -415,15 +414,15 @@ private Builder() { } /** - * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP - * messages. - * @param objectMapper The ObjectMapper instance. Must not be null. + * Sets the {@link McpJsonMapper} to use for JSON serialization/deserialization of + * MCP messages. + * @param jsonMapper The {@link McpJsonMapper} instance. Must not be null. * @return this builder instance - * @throws IllegalArgumentException if objectMapper is null + * @throws IllegalArgumentException if jsonMapper is null */ - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "McpJsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -484,10 +483,9 @@ public Builder keepAliveInterval(Duration keepAliveInterval) { * @throws IllegalStateException if required parameters are not set */ public WebFluxStreamableServerTransportProvider build() { - Assert.notNull(objectMapper, "ObjectMapper must be set"); Assert.notNull(mcpEndpoint, "Message endpoint must be set"); - - return new WebFluxStreamableServerTransportProvider(objectMapper, mcpEndpoint, contextExtractor, + return new WebFluxStreamableServerTransportProvider( + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, contextExtractor, disallowDelete, keepAliveInterval); } 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 f8f0f7a3a..f580b59e8 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 @@ -16,8 +16,6 @@ import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; @@ -79,7 +77,6 @@ protected SingleSessionSyncSpecification prepareSyncServerBuilder() { public void before() { this.mcpServerTransportProvider = new WebFluxSseServerTransportProvider.Builder() - .objectMapper(new ObjectMapper()) .messageEndpoint(CUSTOM_MESSAGE_ENDPOINT) .sseEndpoint(CUSTOM_SSE_ENDPOINT) .contextExtractor(TEST_CONTEXT_EXTRACTOR) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStatelessIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStatelessIntegrationTests.java index 5516e55b7..a00e24b55 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStatelessIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStatelessIntegrationTests.java @@ -13,9 +13,6 @@ import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.server.RouterFunctions; - -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; @@ -67,7 +64,6 @@ protected StatelessSyncSpecification prepareSyncServerBuilder() { @BeforeEach public void before() { this.mcpStreamableServerTransport = WebFluxStatelessServerTransport.builder() - .objectMapper(new ObjectMapper()) .messageEndpoint(CUSTOM_MESSAGE_ENDPOINT) .build(); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStreamableIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStreamableIntegrationTests.java index 933ddf39d..e4bcef829 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStreamableIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxStreamableIntegrationTests.java @@ -16,8 +16,6 @@ import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; @@ -76,7 +74,6 @@ protected SyncSpecification prepareSyncServerBuilder() { public void before() { this.mcpStreamableServerTransportProvider = WebFluxStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .messageEndpoint(CUSTOM_MESSAGE_ENDPOINT) .contextExtractor(TEST_CONTEXT_EXTRACTOR) .build(); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java index 804feb135..0f35f9f0d 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.Timeout; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; - import org.springframework.web.reactive.function.client.WebClient; /** 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 ebe423f41..3dacb62d8 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 @@ -11,7 +11,8 @@ import java.util.function.Function; import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; import org.junit.jupiter.api.AfterAll; @@ -30,6 +31,7 @@ import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.reactive.function.client.WebClient; +import static io.modelcontextprotocol.utils.McpJsonMapperUtils.JSON_MAPPER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -55,8 +57,6 @@ class WebFluxSseClientTransportTests { private WebClient.Builder webClientBuilder; - private ObjectMapper objectMapper; - // Test class to access protected methods static class TestSseClientTransport extends WebFluxSseClientTransport { @@ -64,8 +64,8 @@ static class TestSseClientTransport extends WebFluxSseClientTransport { private Sinks.Many> events = Sinks.many().unicast().onBackpressureBuffer(); - public TestSseClientTransport(WebClient.Builder webClientBuilder, ObjectMapper objectMapper) { - super(webClientBuilder, new JacksonMcpJsonMapper(objectMapper)); + public TestSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper) { + super(webClientBuilder, jsonMapper); } @Override @@ -113,8 +113,7 @@ static void cleanup() { @BeforeEach void setUp() { webClientBuilder = WebClient.builder().baseUrl(host); - objectMapper = new ObjectMapper(); - transport = new TestSseClientTransport(webClientBuilder, objectMapper); + transport = new TestSseClientTransport(webClientBuilder, JSON_MAPPER); transport.connect(Function.identity()).block(); } @@ -132,12 +131,13 @@ void testEndpointEventHandling() { @Test void constructorValidation() { - assertThatThrownBy(() -> new WebFluxSseClientTransport(null)).isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> new WebFluxSseClientTransport(null, JSON_MAPPER)) + .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("WebClient.Builder must not be null"); assertThatThrownBy(() -> new WebFluxSseClientTransport(webClientBuilder, null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("ObjectMapper must not be null"); + .hasMessageContaining("jsonMapper must not be null"); } @Test @@ -149,7 +149,7 @@ void testBuilderPattern() { // Test builder with custom ObjectMapper ObjectMapper customMapper = new ObjectMapper(); WebFluxSseClientTransport transport2 = WebFluxSseClientTransport.builder(webClientBuilder) - .objectMapper(customMapper) + .jsonMapper(new JacksonMcpJsonMapper(customMapper)) .build(); assertThatCode(() -> transport2.closeGracefully().block()).doesNotThrowAnyException(); @@ -161,7 +161,6 @@ void testBuilderPattern() { // Test builder with all custom parameters WebFluxSseClientTransport transport4 = WebFluxSseClientTransport.builder(webClientBuilder) - .objectMapper(customMapper) .sseEndpoint("/custom-sse") .build(); assertThatCode(() -> transport4.closeGracefully().block()).doesNotThrowAnyException(); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java index f3e2d3626..3db0bbd3a 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java @@ -7,7 +7,6 @@ import java.util.Map; import java.util.function.BiFunction; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpAsyncClient; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; @@ -110,18 +109,15 @@ public class AsyncServerMcpTransportContextIntegrationTests { // Server transports private final WebFluxStatelessServerTransport statelessServerTransport = WebFluxStatelessServerTransport.builder() - .objectMapper(new ObjectMapper()) .contextExtractor(serverContextExtractor) .build(); private final WebFluxStreamableServerTransportProvider streamableServerTransport = WebFluxStreamableServerTransportProvider .builder() - .objectMapper(new ObjectMapper()) .contextExtractor(serverContextExtractor) .build(); private final WebFluxSseServerTransportProvider sseServerTransport = WebFluxSseServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .contextExtractor(serverContextExtractor) .messageEndpoint("/mcp/message") .build(); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java index 865192489..94e16e73e 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java @@ -8,7 +8,6 @@ import java.util.function.BiFunction; import java.util.function.Supplier; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; @@ -105,18 +104,15 @@ public class SyncServerMcpTransportContextIntegrationTests { }; private final WebFluxStatelessServerTransport statelessServerTransport = WebFluxStatelessServerTransport.builder() - .objectMapper(new ObjectMapper()) .contextExtractor(serverContextExtractor) .build(); private final WebFluxStreamableServerTransportProvider streamableServerTransport = WebFluxStreamableServerTransportProvider .builder() - .objectMapper(new ObjectMapper()) .contextExtractor(serverContextExtractor) .build(); private final WebFluxSseServerTransportProvider sseServerTransport = WebFluxSseServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .contextExtractor(serverContextExtractor) .messageEndpoint("/mcp/message") .build(); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java index a3bdf10b0..fe0314687 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.Timeout; @@ -30,8 +29,7 @@ class WebFluxSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests { private DisposableServer httpServer; private McpServerTransportProvider createMcpTransportProvider() { - var transportProvider = new WebFluxSseServerTransportProvider.Builder().objectMapper(new ObjectMapper()) - .messageEndpoint(MESSAGE_ENDPOINT) + var transportProvider = new WebFluxSseServerTransportProvider.Builder().messageEndpoint(MESSAGE_ENDPOINT) .build(); HttpHandler httpHandler = RouterFunctions.toHttpHandler(transportProvider.getRouterFunction()); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java index 3e28e96b8..67ef90bdf 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.Timeout; @@ -37,9 +36,7 @@ protected McpServer.SyncSpecification prepareSyncServerBuilder() { } private McpServerTransportProvider createMcpTransportProvider() { - transportProvider = new WebFluxSseServerTransportProvider.Builder().objectMapper(new ObjectMapper()) - .messageEndpoint(MESSAGE_ENDPOINT) - .build(); + transportProvider = new WebFluxSseServerTransportProvider.Builder().messageEndpoint(MESSAGE_ENDPOINT).build(); return transportProvider; } diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpAsyncServerTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpAsyncServerTests.java index 959f2f472..9b5a80f16 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpAsyncServerTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpAsyncServerTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; import org.junit.jupiter.api.Timeout; @@ -32,7 +31,6 @@ class WebFluxStreamableMcpAsyncServerTests extends AbstractMcpAsyncServerTests { private McpStreamableServerTransportProvider createMcpTransportProvider() { var transportProvider = WebFluxStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .messageEndpoint(MESSAGE_ENDPOINT) .build(); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpSyncServerTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpSyncServerTests.java index 3396d489c..6a47ba3ae 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpSyncServerTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxStreamableMcpSyncServerTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; import org.junit.jupiter.api.Timeout; @@ -32,7 +31,6 @@ class WebFluxStreamableMcpSyncServerTests extends AbstractMcpSyncServerTests { private McpStreamableServerTransportProvider createMcpTransportProvider() { var transportProvider = WebFluxStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .messageEndpoint(MESSAGE_ENDPOINT) .build(); diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java new file mode 100644 index 000000000..67347573c --- /dev/null +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java @@ -0,0 +1,12 @@ +package io.modelcontextprotocol.utils; + +import io.modelcontextprotocol.json.McpJsonMapper; + +public final class McpJsonMapperUtils { + + private McpJsonMapperUtils() { + } + + public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.createDefault(); + +} \ No newline at end of file diff --git a/mcp-spring/mcp-spring-webmvc/pom.xml b/mcp-spring/mcp-spring-webmvc/pom.xml index 4bd9f87aa..cdd3cdae5 100644 --- a/mcp-spring/mcp-spring-webmvc/pom.xml +++ b/mcp-spring/mcp-spring-webmvc/pom.xml @@ -22,7 +22,13 @@ - + + io.modelcontextprotocol.sdk + mcp-json-jackson + 0.13.0-SNAPSHOT + + + io.modelcontextprotocol.sdk mcp 0.13.0-SNAPSHOT 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 f32e43304..4b90824d0 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 @@ -11,10 +11,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -94,7 +92,7 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi */ public static final String DEFAULT_SSE_ENDPOINT = "/sse"; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final String messageEndpoint; @@ -120,85 +118,9 @@ public class WebMvcSseServerTransportProvider implements McpServerTransportProvi private KeepAliveScheduler keepAliveScheduler; - /** - * Constructs a new WebMvcSseServerTransportProvider instance with the default SSE - * endpoint. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of messages. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages via HTTP POST. This endpoint will be communicated to clients through the - * SSE connection's initial endpoint event. - * @throws IllegalArgumentException if either objectMapper or messageEndpoint is null - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) { - this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT); - } - - /** - * Constructs a new WebMvcSseServerTransportProvider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of messages. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages via HTTP POST. This endpoint will be communicated to clients through the - * SSE connection's initial endpoint event. - * @param sseEndpoint The endpoint URI where clients establish their SSE connections. - * @throws IllegalArgumentException if any parameter is null - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) { - this(objectMapper, "", messageEndpoint, sseEndpoint); - } - - /** - * Constructs a new WebMvcSseServerTransportProvider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of messages. - * @param 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 - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, - String sseEndpoint) { - this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, null); - } - - /** - * Constructs a new WebMvcSseServerTransportProvider instance. - * @param objectMapper The ObjectMapper 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. - * @param keepAliveInterval The interval for sending keep-alive messages to clients. - * @throws IllegalArgumentException if any parameter is null - * @deprecated Use the builder {@link #builder()} instead for better configuration - * options. - */ - @Deprecated - public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, - String sseEndpoint, Duration keepAliveInterval) { - this(objectMapper, baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, - (serverRequest) -> McpTransportContext.EMPTY); - } - /** * Constructs a new WebMvcSseServerTransportProvider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * @param jsonMapper The McpJsonMapper 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. @@ -211,16 +133,16 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUr * {@link McpTransportContext}. * @throws IllegalArgumentException if any parameter is null */ - private WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, + private WebMvcSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint, String sseEndpoint, Duration keepAliveInterval, McpTransportContextExtractor contextExtractor) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Assert.notNull(jsonMapper, "McpJsonMapper 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"); Assert.notNull(contextExtractor, "Context extractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.baseUrl = baseUrl; this.messageEndpoint = messageEndpoint; this.sseEndpoint = sseEndpoint; @@ -401,7 +323,7 @@ private ServerResponse handleMessage(ServerRequest request) { final McpTransportContext transportContext = this.contextExtractor.extract(request); String body = request.body(String.class); - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body); // Process the message through the session's handle method session.handle(message).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)).block(); // Block @@ -458,7 +380,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { return Mono.fromRunnable(() -> { sseBuilderLock.lock(); try { - String jsonText = objectMapper.writeValueAsString(message); + String jsonText = jsonMapper.writeValueAsString(message); sseBuilder.id(sessionId).event(MESSAGE_EVENT_TYPE).data(jsonText); logger.debug("Message sent to session {}", sessionId); } @@ -473,7 +395,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { } /** - * Converts data from one type to another using the configured ObjectMapper. + * Converts data from one type to another using the configured McpJsonMapper. * @param data The source data object to convert * @param typeRef The target type reference * @return The converted object of type T @@ -481,7 +403,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { */ @Override public T unmarshalFrom(Object data, TypeRef typeRef) { - return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); + return jsonMapper.convertValue(data, typeRef); } /** @@ -543,7 +465,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper = new ObjectMapper(); + private McpJsonMapper jsonMapper; private String baseUrl = ""; @@ -558,12 +480,12 @@ public static class Builder { /** * Sets the JSON object mapper to use for message serialization/deserialization. - * @param objectMapper The object mapper to use + * @param jsonMapper The object mapper to use * @return This builder instance for method chaining */ - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "McpJsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -635,14 +557,14 @@ public Builder contextExtractor(McpTransportContextExtractor cont * Builds a new instance of WebMvcSseServerTransportProvider with the configured * settings. * @return A new WebMvcSseServerTransportProvider instance - * @throws IllegalStateException if objectMapper or messageEndpoint is not set + * @throws IllegalStateException if jsonMapper or messageEndpoint is not set */ public WebMvcSseServerTransportProvider build() { if (messageEndpoint == null) { throw new IllegalStateException("MessageEndpoint must be set"); } - return new WebMvcSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint, - keepAliveInterval, contextExtractor); + return new WebMvcSseServerTransportProvider(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, + baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, contextExtractor); } } diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java index fc2da0439..04b5d717c 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java @@ -4,8 +4,8 @@ package io.modelcontextprotocol.server.transport; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.server.McpStatelessServerHandler; import io.modelcontextprotocol.server.McpTransportContextExtractor; import io.modelcontextprotocol.spec.McpError; @@ -38,7 +38,7 @@ public class WebMvcStatelessServerTransport implements McpStatelessServerTranspo private static final Logger logger = LoggerFactory.getLogger(WebMvcStatelessServerTransport.class); - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final String mcpEndpoint; @@ -50,13 +50,13 @@ public class WebMvcStatelessServerTransport implements McpStatelessServerTranspo private volatile boolean isClosing = false; - private WebMvcStatelessServerTransport(ObjectMapper objectMapper, String mcpEndpoint, + private WebMvcStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint, McpTransportContextExtractor contextExtractor) { - Assert.notNull(objectMapper, "objectMapper must not be null"); + Assert.notNull(jsonMapper, "jsonMapper must not be null"); Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null"); Assert.notNull(contextExtractor, "contextExtractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.mcpEndpoint = mcpEndpoint; this.contextExtractor = contextExtractor; this.routerFunction = RouterFunctions.route() @@ -110,7 +110,7 @@ private ServerResponse handlePost(ServerRequest request) { try { String body = request.body(String.class); - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body); if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { try { @@ -171,7 +171,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -185,13 +185,13 @@ private Builder() { /** * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP * messages. - * @param objectMapper The ObjectMapper instance. Must not be null. + * @param jsonMapper The ObjectMapper instance. Must not be null. * @return this builder instance - * @throws IllegalArgumentException if objectMapper is null + * @throws IllegalArgumentException if jsonMapper is null */ - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "ObjectMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -230,10 +230,9 @@ public Builder contextExtractor(McpTransportContextExtractor cont * @throws IllegalStateException if required parameters are not set */ public WebMvcStatelessServerTransport build() { - Assert.notNull(objectMapper, "ObjectMapper must be set"); Assert.notNull(mcpEndpoint, "Message endpoint must be set"); - - return new WebMvcStatelessServerTransport(objectMapper, mcpEndpoint, contextExtractor); + return new WebMvcStatelessServerTransport(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, + mcpEndpoint, contextExtractor); } } diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java index aaa03a1cf..6bc6ce545 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java @@ -10,6 +10,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; +import io.modelcontextprotocol.json.McpJsonMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -20,10 +21,7 @@ import org.springframework.web.servlet.function.ServerResponse; import org.springframework.web.servlet.function.ServerResponse.SseBuilder; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -84,7 +82,7 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer */ private final boolean disallowDelete; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final RouterFunction routerFunction; @@ -106,7 +104,7 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer /** * Constructs a new WebMvcStreamableServerTransportProvider instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization + * @param jsonMapper The McpJsonMapper 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. @@ -115,14 +113,14 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer * @param disallowDelete Whether to disallow DELETE requests on the endpoint. * @throws IllegalArgumentException if any parameter is null */ - private WebMvcStreamableServerTransportProvider(ObjectMapper objectMapper, String mcpEndpoint, + private WebMvcStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint, boolean disallowDelete, McpTransportContextExtractor contextExtractor, Duration keepAliveInterval) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Assert.notNull(jsonMapper, "McpJsonMapper must not be null"); Assert.notNull(mcpEndpoint, "MCP endpoint must not be null"); Assert.notNull(contextExtractor, "McpTransportContextExtractor must not be null"); - this.objectMapper = objectMapper; + this.jsonMapper = jsonMapper; this.mcpEndpoint = mcpEndpoint; this.disallowDelete = disallowDelete; this.contextExtractor = contextExtractor; @@ -327,13 +325,13 @@ private ServerResponse handlePost(ServerRequest request) { try { String body = request.body(String.class); - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body); // Handle initialization request if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest && jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) { - McpSchema.InitializeRequest initializeRequest = objectMapper.convertValue(jsonrpcRequest.params(), - new TypeReference() { + McpSchema.InitializeRequest initializeRequest = jsonMapper.convertValue(jsonrpcRequest.params(), + new TypeRef() { }); McpStreamableServerSession.McpStreamableServerSessionInit init = this.sessionFactory .startSession(initializeRequest); @@ -518,7 +516,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId return; } - String jsonText = objectMapper.writeValueAsString(message); + String jsonText = jsonMapper.writeValueAsString(message); this.sseBuilder.id(messageId != null ? messageId : this.sessionId) .event(MESSAGE_EVENT_TYPE) .data(jsonText); @@ -541,7 +539,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId } /** - * Converts data from one type to another using the configured ObjectMapper. + * Converts data from one type to another using the configured McpJsonMapper. * @param data The source data object to convert * @param typeRef The target type reference * @return The converted object of type T @@ -549,7 +547,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message, String messageId */ @Override public T unmarshalFrom(Object data, TypeRef typeRef) { - return new JacksonMcpJsonMapper(objectMapper).convertValue(data, typeRef); + return jsonMapper.convertValue(data, typeRef); } /** @@ -599,7 +597,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -611,15 +609,15 @@ public static class Builder { private Duration keepAliveInterval; /** - * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP + * Sets the McpJsonMapper to use for JSON serialization/deserialization of MCP * messages. - * @param objectMapper The ObjectMapper instance. Must not be null. + * @param jsonMapper The McpJsonMapper instance. Must not be null. * @return this builder instance - * @throws IllegalArgumentException if objectMapper is null + * @throws IllegalArgumentException if jsonMapper is null */ - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public Builder jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "McpJsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -680,11 +678,10 @@ public Builder keepAliveInterval(Duration keepAliveInterval) { * @throws IllegalStateException if required parameters are not set */ public WebMvcStreamableServerTransportProvider build() { - Assert.notNull(this.objectMapper, "ObjectMapper must be set"); Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set"); - - return new WebMvcStreamableServerTransportProvider(this.objectMapper, this.mcpEndpoint, this.disallowDelete, - this.contextExtractor, this.keepAliveInterval); + return new WebMvcStreamableServerTransportProvider( + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, disallowDelete, + contextExtractor, keepAliveInterval); } } diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/common/McpTransportContextIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/common/McpTransportContextIntegrationTests.java index 1f5f1cc0c..cc9945436 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/common/McpTransportContextIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/common/McpTransportContextIntegrationTests.java @@ -8,7 +8,6 @@ import java.util.function.BiFunction; import java.util.function.Supplier; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; @@ -40,7 +39,6 @@ import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -223,10 +221,7 @@ static class TestStatelessConfig { @Bean public WebMvcStatelessServerTransport webMvcStatelessServerTransport() { - return WebMvcStatelessServerTransport.builder() - .objectMapper(new ObjectMapper()) - .contextExtractor(serverContextExtractor) - .build(); + return WebMvcStatelessServerTransport.builder().contextExtractor(serverContextExtractor).build(); } @Bean @@ -251,10 +246,7 @@ static class TestStreamableHttpConfig { @Bean public WebMvcStreamableServerTransportProvider webMvcStreamableServerTransport() { - return WebMvcStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) - .contextExtractor(serverContextExtractor) - .build(); + return WebMvcStreamableServerTransportProvider.builder().contextExtractor(serverContextExtractor).build(); } @Bean @@ -281,7 +273,6 @@ static class TestSseConfig { public WebMvcSseServerTransportProvider webMvcSseServerTransport() { return WebMvcSseServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .contextExtractor(serverContextExtractor) .messageEndpoint("/mcp/message") .build(); diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableAsyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableAsyncServerTransportTests.java index 66349216d..ae1f4f4d1 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableAsyncServerTransportTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableAsyncServerTransportTests.java @@ -16,8 +16,6 @@ import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider; import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; import reactor.netty.DisposableServer; @@ -48,10 +46,7 @@ static class TestConfig { @Bean public WebMvcStreamableServerTransportProvider webMvcSseServerTransportProvider() { - return WebMvcStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) - .mcpEndpoint(MCP_ENDPOINT) - .build(); + return WebMvcStreamableServerTransportProvider.builder().mcpEndpoint(MCP_ENDPOINT).build(); } @Bean diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableSyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableSyncServerTransportTests.java index cab487f12..c8c24b8a7 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableSyncServerTransportTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMcpStreamableSyncServerTransportTests.java @@ -16,8 +16,6 @@ import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.server.transport.WebMvcStreamableServerTransportProvider; import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; import reactor.netty.DisposableServer; @@ -48,10 +46,7 @@ static class TestConfig { @Bean public WebMvcStreamableServerTransportProvider webMvcSseServerTransportProvider() { - return WebMvcStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) - .mcpEndpoint(MCP_ENDPOINT) - .build(); + return WebMvcStreamableServerTransportProvider.builder().mcpEndpoint(MCP_ENDPOINT).build(); } @Bean 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 bb4c2bf37..ccf3170c9 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 @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.apache.catalina.Context; @@ -37,7 +36,10 @@ static class TestConfig { @Bean public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() { - return new WebMvcSseServerTransportProvider(new ObjectMapper(), MESSAGE_ENDPOINT); + return WebMvcSseServerTransportProvider.builder() + .messageEndpoint(MESSAGE_ENDPOINT) + .sseEndpoint(WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT) + .build(); } @Bean 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 cce36d191..d8d26af48 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 @@ -3,7 +3,6 @@ */ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; @@ -92,7 +91,6 @@ static class TestConfig { public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() { return WebMvcSseServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .baseUrl(CUSTOM_CONTEXT_PATH) .messageEndpoint(MESSAGE_ENDPOINT) .sseEndpoint(WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT) 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 18a9d0063..e780b8e51 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 @@ -21,8 +21,6 @@ import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.AbstractMcpClientServerIntegrationTests; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; @@ -64,7 +62,6 @@ static class TestConfig { @Bean public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() { return WebMvcSseServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .messageEndpoint(MESSAGE_ENDPOINT) .contextExtractor(TEST_CONTEXT_EXTRACTOR) .build(); diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java index 101a067ad..66d6d3ae9 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; @@ -36,10 +35,7 @@ static class TestConfig { @Bean public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() { - return WebMvcSseServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) - .messageEndpoint(MESSAGE_ENDPOINT) - .build(); + return WebMvcSseServerTransportProvider.builder().messageEndpoint(MESSAGE_ENDPOINT).build(); } @Bean diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStatelessIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStatelessIntegrationTests.java index c7c1e710d..9633dfbd1 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStatelessIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStatelessIntegrationTests.java @@ -19,8 +19,6 @@ import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.AbstractStatelessIntegrationTests; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; @@ -46,10 +44,7 @@ static class TestConfig { @Bean public WebMvcStatelessServerTransport webMvcStatelessServerTransport() { - return WebMvcStatelessServerTransport.builder() - .objectMapper(new ObjectMapper()) - .messageEndpoint(MESSAGE_ENDPOINT) - .build(); + return WebMvcStatelessServerTransport.builder().messageEndpoint(MESSAGE_ENDPOINT).build(); } diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStreamableIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStreamableIntegrationTests.java index 3f1716f89..abdd82967 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStreamableIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcStreamableIntegrationTests.java @@ -21,8 +21,6 @@ import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.modelcontextprotocol.AbstractMcpClientServerIntegrationTests; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; @@ -52,7 +50,6 @@ static class TestConfig { @Bean public WebMvcStreamableServerTransportProvider webMvcStreamableServerTransportProvider() { return WebMvcStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) .contextExtractor(TEST_CONTEXT_EXTRACTOR) .mcpEndpoint(MESSAGE_ENDPOINT) .build(); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java index dd3bc59da..923a7ee36 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java @@ -54,6 +54,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import static io.modelcontextprotocol.utils.ToolsUtils.EMPTY_JSON_SCHEMA; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.assertj.core.api.Assertions.assertThat; @@ -81,7 +82,6 @@ void simple(String clientType) { var server = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .requestTimeout(Duration.ofSeconds(1000)) .build(); - try ( // Create client without sampling capabilities var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) @@ -106,7 +106,7 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { var clientBuilder = clientBuilders.get(clientType); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { return exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)) .then(Mono.just(mock(CallToolResult.class))); @@ -155,7 +155,7 @@ void testCreateMessageSuccess(String clientType) { AtomicReference samplingResult = new AtomicReference<>(); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { var createMessageRequest = McpSchema.CreateMessageRequest.builder() @@ -233,7 +233,7 @@ void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws Interr AtomicReference samplingResult = new AtomicReference<>(); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { var createMessageRequest = McpSchema.CreateMessageRequest.builder() @@ -307,7 +307,7 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt null); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { var createMessageRequest = McpSchema.CreateMessageRequest.builder() @@ -357,7 +357,7 @@ void testCreateElicitationWithoutElicitationCapabilities(String clientType) { var clientBuilder = clientBuilders.get(clientType); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> exchange.createElicitation(mock(ElicitRequest.class)) .then(Mono.just(mock(CallToolResult.class)))) .build(); @@ -400,7 +400,7 @@ void testCreateElicitationSuccess(String clientType) { null); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { var elicitationRequest = McpSchema.ElicitRequest.builder() @@ -457,7 +457,7 @@ void testCreateElicitationWithRequestTimeoutSuccess(String clientType) { AtomicReference resultRef = new AtomicReference<>(); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { var elicitationRequest = McpSchema.ElicitRequest.builder() @@ -528,7 +528,7 @@ void testCreateElicitationWithRequestTimeoutFail(String clientType) { AtomicReference resultRef = new AtomicReference<>(); McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { var elicitationRequest = ElicitRequest.builder() @@ -626,7 +626,7 @@ void testRootsWithoutCapability(String clientType) { var clientBuilder = clientBuilders.get(clientType); McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { exchange.listRoots(); // try to list roots @@ -757,14 +757,6 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { // --------------------------------------- // Tools Tests // --------------------------------------- - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) void testToolCallSuccess(String clientType) { @@ -774,7 +766,7 @@ void testToolCallSuccess(String clientType) { var responseBodyIsNullOrBlank = new AtomicBoolean(false); var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { try { @@ -828,7 +820,7 @@ void testThrowingToolCallIsCaughtBeforeTimeout(String clientType) { .tool(Tool.builder() .name("tool1") .description("tool1 description") - .inputSchema(emptyJsonSchema) + .inputSchema(EMPTY_JSON_SCHEMA) .build()) .callHandler((exchange, request) -> { // We trigger a timeout on blocking read, raising an exception @@ -867,7 +859,7 @@ void testToolCallSuccessWithTranportContextExtraction(String clientType) { var expectedCallResponse = new McpSchema.CallToolResult( List.of(new McpSchema.TextContent("CALL RESPONSE; ctx=value")), null); McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { McpTransportContext transportContext = exchange.transportContext(); @@ -919,7 +911,7 @@ void testToolListChangeHandlingSuccess(String clientType) { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { // perform a blocking call to a remote service try { @@ -988,7 +980,7 @@ void testToolListChangeHandlingSuccess(String clientType) { .tool(Tool.builder() .name("tool2") .description("tool2 description") - .inputSchema(emptyJsonSchema) + .inputSchema(EMPTY_JSON_SCHEMA) .build()) .callHandler((exchange, request) -> callResponse) .build(); @@ -1040,7 +1032,7 @@ void testLoggingNotification(String clientType) throws InterruptedException { .tool(Tool.builder() .name("logging-test") .description("Test logging notifications") - .inputSchema(emptyJsonSchema) + .inputSchema(EMPTY_JSON_SCHEMA) .build()) .callHandler((exchange, request) -> { @@ -1154,7 +1146,7 @@ void testProgressNotification(String clientType) throws InterruptedException { .tool(McpSchema.Tool.builder() .name("progress-test") .description("Test progress notifications") - .inputSchema(emptyJsonSchema) + .inputSchema(EMPTY_JSON_SCHEMA) .build()) .callHandler((exchange, request) -> { @@ -1308,7 +1300,7 @@ void testPingSuccess(String clientType) { .tool(Tool.builder() .name("ping-async-test") .description("Test ping async behavior") - .inputSchema(emptyJsonSchema) + .inputSchema(EMPTY_JSON_SCHEMA) .build()) .callHandler((exchange, request) -> { diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractStatelessIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractStatelessIntegrationTests.java index c96f10eda..e17c01f90 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractStatelessIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractStatelessIntegrationTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; +import static io.modelcontextprotocol.utils.ToolsUtils.EMPTY_JSON_SCHEMA; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.assertj.core.api.Assertions.assertThat; @@ -74,15 +75,6 @@ void simple(String clientType) { // --------------------------------------- // Tools Tests // --------------------------------------- - - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) void testToolCallSuccess(String clientType) { @@ -92,7 +84,7 @@ void testToolCallSuccess(String clientType) { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); McpStatelessServerFeatures.SyncToolSpecification tool1 = McpStatelessServerFeatures.SyncToolSpecification .builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((ctx, request) -> { try { @@ -145,7 +137,7 @@ void testThrowingToolCallIsCaughtBeforeTimeout(String clientType) { .tool(Tool.builder() .name("tool1") .description("tool1 description") - .inputSchema(emptyJsonSchema) + .inputSchema(EMPTY_JSON_SCHEMA) .build()) .callHandler((context, request) -> { // We trigger a timeout on blocking read, raising an exception @@ -180,7 +172,7 @@ void testToolListChangeHandlingSuccess(String clientType) { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); McpStatelessServerFeatures.SyncToolSpecification tool1 = McpStatelessServerFeatures.SyncToolSpecification .builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) + .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((ctx, request) -> { // perform a blocking call to a remote service try { @@ -241,7 +233,7 @@ void testToolListChangeHandlingSuccess(String clientType) { .tool(Tool.builder() .name("tool2") .description("tool2 description") - .inputSchema(emptyJsonSchema) + .inputSchema(EMPTY_JSON_SCHEMA) .build()) .callHandler((exchange, request) -> callResponse) .build(); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java index 6b49f6b7b..4a1a8e056 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java @@ -9,8 +9,8 @@ import java.util.function.BiConsumer; import java.util.function.Function; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; @@ -94,7 +94,7 @@ public Mono closeGracefully() { @Override public T unmarshalFrom(Object data, TypeRef typeRef) { - return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()).convertValue(data, typeRef); + return McpJsonMapper.createDefault().convertValue(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 8902a53b3..8a0b3e0d9 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -4,6 +4,7 @@ package io.modelcontextprotocol.client; +import static io.modelcontextprotocol.utils.McpJsonMapperUtils.JSON_MAPPER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -22,8 +23,7 @@ import java.util.function.Consumer; import java.util.function.Function; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import io.modelcontextprotocol.json.McpJsonMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -176,7 +176,12 @@ void testListAllToolsReturnsImmutableList() { .consumeNextWith(result -> { assertThat(result.tools()).isNotNull(); // Verify that the returned list is immutable - assertThatThrownBy(() -> result.tools().add(new Tool("test", "test", "{\"type\":\"object\"}"))) + assertThatThrownBy(() -> result.tools() + .add(Tool.builder() + .name("test") + .title("test") + .inputSchema(JSON_MAPPER, "{\"type\":\"object\"}") + .build())) .isInstanceOf(UnsupportedOperationException.class); }) .verifyComplete(); 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 8eb6ec248..e1ffd2c75 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -22,8 +22,6 @@ import java.util.function.Consumer; import java.util.function.Function; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; 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 1e87d4420..b0701911a 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import static io.modelcontextprotocol.utils.ToolsUtils.EMPTY_JSON_SCHEMA; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -95,18 +96,10 @@ void testImmediateClose() { // --------------------------------------- // Tools Tests // --------------------------------------- - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - @Test @Deprecated void testAddTool() { - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + Tool newTool = Tool.builder().name("new-tool").title("New test tool").inputSchema(EMPTY_JSON_SCHEMA).build(); var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); @@ -120,7 +113,7 @@ void testAddTool() { @Test void testAddToolCall() { - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + Tool newTool = Tool.builder().name("new-tool").title("New test tool").inputSchema(EMPTY_JSON_SCHEMA).build(); var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); @@ -136,7 +129,11 @@ void testAddToolCall() { @Test @Deprecated void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + Tool duplicateTool = McpSchema.Tool.builder() + .name(TEST_TOOL_NAME) + .title("Duplicate tool") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -156,7 +153,11 @@ void testAddDuplicateTool() { @Test void testAddDuplicateToolCall() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + Tool duplicateTool = McpSchema.Tool.builder() + .name(TEST_TOOL_NAME) + .title("Duplicate tool") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -176,8 +177,11 @@ void testAddDuplicateToolCall() { @Test void testDuplicateToolCallDuringBuilding() { - Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building", - emptyJsonSchema); + Tool duplicateTool = McpSchema.Tool.builder() + .name("duplicate-build-toolcall") + .title("Duplicate toolcall during building") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -189,7 +193,11 @@ void testDuplicateToolCallDuringBuilding() { @Test void testDuplicateToolsInBatchListRegistration() { - Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema); + Tool duplicateTool = McpSchema.Tool.builder() + .name("batch-list-tool") + .title("Duplicate tool in batch list") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); List specs = List.of( McpServerFeatures.AsyncToolSpecification.builder() .tool(duplicateTool) @@ -210,7 +218,11 @@ void testDuplicateToolsInBatchListRegistration() { @Test void testDuplicateToolsInBatchVarargsRegistration() { - Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema); + Tool duplicateTool = McpSchema.Tool.builder() + .name("batch-varargs-tool") + .title("Duplicate tool in batch varargs") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -229,8 +241,11 @@ void testDuplicateToolsInBatchVarargsRegistration() { @Test void testRemoveTool() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - + Tool too = McpSchema.Tool.builder() + .name(TEST_TOOL_NAME) + .title("Duplicate tool") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .toolCall(too, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) @@ -256,7 +271,11 @@ void testRemoveNonexistentTool() { @Test void testNotifyToolsListChanged() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + Tool too = McpSchema.Tool.builder() + .name(TEST_TOOL_NAME) + .title("Duplicate tool") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) 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 5d70ae4c0..d804de43b 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -4,6 +4,7 @@ package io.modelcontextprotocol.server; +import static io.modelcontextprotocol.utils.ToolsUtils.EMPTY_JSON_SCHEMA; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -99,15 +100,6 @@ void testGetAsyncServer() { // --------------------------------------- // Tools Tests // --------------------------------------- - - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - @Test @Deprecated void testAddTool() { @@ -115,7 +107,7 @@ void testAddTool() { .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + Tool newTool = Tool.builder().name("new-tool").title("New test tool").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(newTool, (exchange, args) -> new CallToolResult(List.of(), false)))) .doesNotThrowAnyException(); @@ -129,7 +121,7 @@ void testAddToolCall() { .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + Tool newTool = Tool.builder().name("new-tool").title("New test tool").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() .tool(newTool) .callHandler((exchange, request) -> new CallToolResult(List.of(), false)) @@ -141,7 +133,11 @@ void testAddToolCall() { @Test @Deprecated void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + Tool duplicateTool = Tool.builder() + .name(TEST_TOOL_NAME) + .title("Duplicate tool") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -158,7 +154,11 @@ void testAddDuplicateTool() { @Test void testAddDuplicateToolCall() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + Tool duplicateTool = Tool.builder() + .name(TEST_TOOL_NAME) + .title("Duplicate tool") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -176,8 +176,11 @@ void testAddDuplicateToolCall() { @Test void testDuplicateToolCallDuringBuilding() { - Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building", - emptyJsonSchema); + Tool duplicateTool = Tool.builder() + .name("duplicate-build-toolcall") + .title("Duplicate toolcall during building") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -189,7 +192,11 @@ void testDuplicateToolCallDuringBuilding() { @Test void testDuplicateToolsInBatchListRegistration() { - Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema); + Tool duplicateTool = Tool.builder() + .name("batch-list-tool") + .title("Duplicate tool in batch list") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); List specs = List.of( McpServerFeatures.SyncToolSpecification.builder() .tool(duplicateTool) @@ -210,7 +217,11 @@ void testDuplicateToolsInBatchListRegistration() { @Test void testDuplicateToolsInBatchVarargsRegistration() { - Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema); + Tool duplicateTool = Tool.builder() + .name("batch-varargs-tool") + .title("Duplicate tool in batch varargs") + .inputSchema(EMPTY_JSON_SCHEMA) + .build(); assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -229,7 +240,7 @@ void testDuplicateToolsInBatchVarargsRegistration() { @Test void testRemoveTool() { - Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); + Tool tool = Tool.builder().name(TEST_TOOL_NAME).title("Test tool").inputSchema(EMPTY_JSON_SCHEMA).build(); var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java b/mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java new file mode 100644 index 000000000..67347573c --- /dev/null +++ b/mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java @@ -0,0 +1,12 @@ +package io.modelcontextprotocol.utils; + +import io.modelcontextprotocol.json.McpJsonMapper; + +public final class McpJsonMapperUtils { + + private McpJsonMapperUtils() { + } + + public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.createDefault(); + +} \ No newline at end of file diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/utils/ToolsUtils.java b/mcp-test/src/main/java/io/modelcontextprotocol/utils/ToolsUtils.java new file mode 100644 index 000000000..ec603aac1 --- /dev/null +++ b/mcp-test/src/main/java/io/modelcontextprotocol/utils/ToolsUtils.java @@ -0,0 +1,15 @@ +package io.modelcontextprotocol.utils; + +import io.modelcontextprotocol.spec.McpSchema; + +import java.util.Collections; + +public final class ToolsUtils { + + private ToolsUtils() { + } + + public static final McpSchema.JsonSchema EMPTY_JSON_SCHEMA = new McpSchema.JsonSchema("object", + Collections.emptyMap(), null, null, null, null); + +} diff --git a/mcp/pom.xml b/mcp/pom.xml index 6cea79f62..4b3a5b078 100644 --- a/mcp/pom.xml +++ b/mcp/pom.xml @@ -65,6 +65,18 @@ + + io.modelcontextprotocol.sdk + mcp-json + 0.13.0-SNAPSHOT + + + + io.modelcontextprotocol.sdk + mcp-json-jackson + 0.13.0-SNAPSHOT + test + org.slf4j @@ -74,7 +86,7 @@ com.fasterxml.jackson.core - jackson-databind + jackson-annotations ${jackson.version} @@ -83,11 +95,6 @@ reactor-core - - com.networknt - json-schema-validator - ${json-schema-validator.version} - @@ -216,14 +223,13 @@ test - - - com.google.code.gson - gson - 2.10.1 - test - - + + + com.google.code.gson + gson + 2.10.1 + test + diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 047331d11..8d5bc34a6 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -18,7 +18,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpClientSession; import io.modelcontextprotocol.spec.McpClientTransport; diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index 9414fd858..22171ddbe 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -16,13 +16,11 @@ import java.util.function.Consumer; import java.util.function.Function; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; @@ -119,83 +117,6 @@ public class HttpClientSseClientTransport implements McpClientTransport { */ private final McpAsyncHttpClientRequestCustomizer httpRequestCustomizer; - /** - * Creates a new transport instance with default HTTP client and object mapper. - * @param baseUri the base URI of the MCP server - * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This - * constructor will be removed in future versions. - */ - @Deprecated(forRemoval = true) - public HttpClientSseClientTransport(String baseUri) { - this(HttpClient.newBuilder(), baseUri, new com.fasterxml.jackson.databind.ObjectMapper()); - } - - /** - * Creates a new transport instance with custom HTTP client builder and object mapper. - * @param clientBuilder the HTTP client builder to use - * @param baseUri the base URI of the MCP server - * @param objectMapper the object mapper for JSON serialization/deserialization - * @throws IllegalArgumentException if objectMapper or clientBuilder is null - * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This - * constructor will be removed in future versions. - */ - @Deprecated(forRemoval = true) - public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, - com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - this(clientBuilder, baseUri, DEFAULT_SSE_ENDPOINT, objectMapper); - } - - /** - * Creates a new transport instance with custom HTTP client builder and object mapper. - * @param clientBuilder the HTTP client builder to use - * @param baseUri the base URI of the MCP server - * @param sseEndpoint the SSE endpoint path - * @param objectMapper the object mapper for JSON serialization/deserialization - * @throws IllegalArgumentException if objectMapper or clientBuilder is null - * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This - * constructor will be removed in future versions. - */ - @Deprecated(forRemoval = true) - public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, String baseUri, String sseEndpoint, - com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - this(clientBuilder, HttpRequest.newBuilder(), baseUri, sseEndpoint, objectMapper); - } - - /** - * Creates a new transport instance with custom HTTP client builder, object mapper, - * and headers. - * @param clientBuilder the HTTP client builder to use - * @param requestBuilder the HTTP request builder to use - * @param baseUri the base URI of the MCP server - * @param sseEndpoint the SSE endpoint path - * @param objectMapper the object mapper for JSON serialization/deserialization - * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null - * @deprecated Use {@link HttpClientSseClientTransport#builder(String)} instead. This - * constructor will be removed in future versions. - */ - @Deprecated(forRemoval = true) - public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpRequest.Builder requestBuilder, - String baseUri, String sseEndpoint, ObjectMapper objectMapper) { - this(clientBuilder.build(), requestBuilder, baseUri, sseEndpoint, objectMapper); - } - - /** - * Creates a new transport instance with custom HTTP client builder, object mapper, - * and headers. - * @param httpClient the HTTP client to use - * @param requestBuilder the HTTP request builder to use - * @param baseUri the base URI of the MCP server - * @param sseEndpoint the SSE endpoint path - * @param objectMapper the object mapper for JSON serialization/deserialization - * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null - */ - @Deprecated(forRemoval = true) - HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, - String sseEndpoint, com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - this(httpClient, requestBuilder, baseUri, sseEndpoint, new JacksonMcpJsonMapper(objectMapper), - McpAsyncHttpClientRequestCustomizer.NOOP); - } - /** * Creates a new transport instance with custom HTTP client builder, object mapper, * and headers. @@ -249,7 +170,7 @@ public static class Builder { private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1); - private McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); + private McpJsonMapper jsonMapper; private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); @@ -343,20 +264,6 @@ public Builder customizeRequest(final Consumer requestCusto return this; } - /** - * Sets the object mapper for JSON serialization/deserialization. - * @param objectMapper the object mapper - * @return this builder - * @deprecated Prefer {@link #jsonMapper(McpJsonMapper)}. This method will be - * removed in a future release. - */ - @Deprecated(forRemoval = true) - public Builder objectMapper(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "objectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Sets the JSON mapper implementation to use for serialization/deserialization. * @param jsonMapper the JSON mapper @@ -420,8 +327,8 @@ public Builder connectTimeout(Duration connectTimeout) { */ public HttpClientSseClientTransport build() { HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); - return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint, jsonMapper, - httpRequestCustomizer); + return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint, + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, httpRequestCustomizer); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index c98a3cb6b..ba953de2d 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -12,7 +12,6 @@ import java.net.http.HttpResponse.BodyHandler; import java.time.Duration; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicReference; @@ -23,10 +22,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.modelcontextprotocol.spec.json.TypeRef; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; @@ -596,7 +593,7 @@ public static class Builder { private final String baseUri; - private McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper()); + private McpJsonMapper jsonMapper; private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1); @@ -665,19 +662,6 @@ public Builder customizeRequest(final Consumer requestCusto return this; } - /** - * Configure the {@link ObjectMapper} to use. - * @param objectMapper instance to use - * @return the builder instance - * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead - */ - @Deprecated(forRemoval = true) - public Builder objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Configure a custom {@link McpJsonMapper} implementation to use. * @param jsonMapper instance to use @@ -780,9 +764,9 @@ public Builder connectTimeout(Duration connectTimeout) { */ public HttpClientStreamableHttpTransport build() { HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); - - return new HttpClientStreamableHttpTransport(this.jsonMapper, httpClient, requestBuilder, baseUri, endpoint, - resumableStreams, openConnectionOnStartup, httpRequestCustomizer); + return new HttpClientStreamableHttpTransport( + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, httpClient, requestBuilder, + baseUri, endpoint, resumableStreams, openConnectionOnStartup, httpRequestCustomizer); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java index 035c3d600..1b4eaca97 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java @@ -15,9 +15,8 @@ import java.util.function.Consumer; import java.util.function.Function; -import io.modelcontextprotocol.spec.json.TypeRef; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; @@ -70,15 +69,6 @@ public class StdioClientTransport implements McpClientTransport { // visible for tests private Consumer stdErrorHandler = error -> logger.info("STDERR Message received: {}", error); - /** - * Creates a new StdioClientTransport with the specified parameters and default - * ObjectMapper. - * @param params The parameters for configuring the server process - */ - public StdioClientTransport(ServerParameters params) { - this(params, new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper())); - } - /** * Creates a new StdioClientTransport with the specified parameters and JsonMapper. * @param params The parameters for configuring the server process @@ -103,16 +93,6 @@ public StdioClientTransport(ServerParameters params, McpJsonMapper jsonMapper) { this.errorScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "error"); } - /** - * Creates a new StdioClientTransport with the specified parameters and ObjectMapper. - * @deprecated Use {@link #StdioClientTransport(ServerParameters, McpJsonMapper)} - * instead. - */ - @Deprecated(forRemoval = true) - public StdioClientTransport(ServerParameters params, com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - this(params, new JacksonMcpJsonMapper(objectMapper)); - } - /** * Starts the server process and initializes the message processing streams. This * method sets up the process with the configured command, arguments, and environment, diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index ee60bf36d..7170f9b09 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -21,10 +21,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; -import io.modelcontextprotocol.spec.JsonSchemaValidator; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import io.modelcontextprotocol.spec.McpClientSession; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index 835f43bc0..a15c58cd5 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -8,7 +8,7 @@ import java.util.ArrayList; import java.util.Collections; -import io.modelcontextprotocol.spec.json.TypeRef; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpLoggableSession; import io.modelcontextprotocol.spec.McpSchema; diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index 863a90045..bb98ec745 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -15,12 +15,9 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.json.McpJsonMapper; -import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.McpJsonMapper; -import io.modelcontextprotocol.spec.DefaultJsonSchemaValidator; -import io.modelcontextprotocol.spec.JsonSchemaValidator; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; @@ -70,7 +67,7 @@ * Example of creating a basic synchronous server:
{@code
  * McpServer.sync(transportProvider)
  *     .serverInfo("my-server", "1.0.0")
- *     .tool(new Tool("calculator", "Performs calculations", schema),
+ *     .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
  *           (exchange, args) -> new CallToolResult("Result: " + calculate(args)))
  *     .build();
  * }
@@ -78,7 +75,7 @@ * Example of creating a basic asynchronous server:
{@code
  * McpServer.async(transportProvider)
  *     .serverInfo("my-server", "1.0.0")
- *     .tool(new Tool("calculator", "Performs calculations", schema),
+ *     .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
  *           (exchange, args) -> Mono.fromSupplier(() -> calculate(args))
  *               .map(result -> new CallToolResult("Result: " + result)))
  *     .build();
@@ -232,11 +229,11 @@ public McpAsyncServer build() {
 					this.instructions);
 
 			var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
-					: new DefaultJsonSchemaValidator(
-							this.jsonMapper != null ? this.jsonMapper : new JacksonMcpJsonMapper(new ObjectMapper()));
+					: JsonSchemaValidator.createDefault();
 
-			return new McpAsyncServer(this.transportProvider, jsonMapper, features, this.requestTimeout,
-					this.uriTemplateManagerFactory, jsonSchemaValidator);
+			return new McpAsyncServer(transportProvider,
+					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, features, requestTimeout,
+					uriTemplateManagerFactory, jsonSchemaValidator);
 		}
 
 	}
@@ -259,11 +256,11 @@ public McpAsyncServer build() {
 			var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
 					this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
 					this.instructions);
-			var jsonMapper = this.jsonMapper == null ? new JacksonMcpJsonMapper(new ObjectMapper()) : this.jsonMapper;
 			var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
-					: new DefaultJsonSchemaValidator(jsonMapper);
-			return new McpAsyncServer(this.transportProvider, jsonMapper, features, this.requestTimeout,
-					this.uriTemplateManagerFactory, jsonSchemaValidator);
+					: JsonSchemaValidator.createDefault();
+			return new McpAsyncServer(transportProvider,
+					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, features, requestTimeout,
+					uriTemplateManagerFactory, jsonSchemaValidator);
 		}
 
 	}
@@ -275,7 +272,7 @@ abstract class AsyncSpecification> {
 
 		McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
 
-		McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
+		McpJsonMapper jsonMapper;
 
 		McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
 
@@ -422,7 +419,7 @@ public AsyncSpecification capabilities(McpSchema.ServerCapabilities serverCap
 		 * 

* Example usage:

{@code
 		 * .tool(
-		 *     new Tool("calculator", "Performs calculations", schema),
+		 *     Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
 		 *     (exchange, args) -> Mono.fromSupplier(() -> calculate(args))
 		 *         .map(result -> new CallToolResult("Result: " + result))
 		 * )
@@ -769,20 +766,6 @@ public AsyncSpecification rootsChangeHandlers(
 			return this.rootsChangeHandlers(Arrays.asList(handlers));
 		}
 
-		/**
-		 * Sets the object mapper to use for serializing and deserializing JSON messages.
-		 * @param objectMapper the instance to use. Must not be null.
-		 * @return This builder instance for method chaining.
-		 * @throws IllegalArgumentException if objectMapper is null
-		 * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead
-		 */
-		@Deprecated(forRemoval = true)
-		public AsyncSpecification objectMapper(ObjectMapper objectMapper) {
-			Assert.notNull(objectMapper, "ObjectMapper must not be null");
-			this.jsonMapper = new JacksonMcpJsonMapper(objectMapper);
-			return this;
-		}
-
 		/**
 		 * Sets the JsonMapper to use for serializing and deserializing JSON messages.
 		 * @param jsonMapper the mapper to use. Must not be null.
@@ -827,19 +810,16 @@ private SingleSessionSyncSpecification(McpServerTransportProvider transportProvi
 		 */
 		@Override
 		public McpSyncServer build() {
-			Objects.requireNonNull(this.jsonMapper, "JsonMapper must be set");
 			McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities,
 					this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions,
 					this.rootsChangeHandlers, this.instructions);
 			McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures,
 					this.immediateExecution);
 
-			var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
-					: new DefaultJsonSchemaValidator(this.jsonMapper);
-
-			var asyncServer = new McpAsyncServer(this.transportProvider, jsonMapper, asyncFeatures, this.requestTimeout,
-					this.uriTemplateManagerFactory, jsonSchemaValidator);
-
+			var asyncServer = new McpAsyncServer(transportProvider,
+					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, asyncFeatures, requestTimeout,
+					uriTemplateManagerFactory,
+					jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.createDefault());
 			return new McpSyncServer(asyncServer, this.immediateExecution);
 		}
 
@@ -866,13 +846,11 @@ public McpSyncServer build() {
 					this.rootsChangeHandlers, this.instructions);
 			McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures,
 					this.immediateExecution);
-			var jsonMapper = this.jsonMapper == null ? new JacksonMcpJsonMapper(new ObjectMapper()) : this.jsonMapper;
 			var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
-					: new DefaultJsonSchemaValidator(jsonMapper);
-
-			var asyncServer = new McpAsyncServer(this.transportProvider, jsonMapper, asyncFeatures, this.requestTimeout,
+					: JsonSchemaValidator.createDefault();
+			var asyncServer = new McpAsyncServer(transportProvider,
+					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, asyncFeatures, this.requestTimeout,
 					this.uriTemplateManagerFactory, jsonSchemaValidator);
-
 			return new McpSyncServer(asyncServer, this.immediateExecution);
 		}
 
@@ -885,7 +863,7 @@ abstract class SyncSpecification> {
 
 		McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
 
-		McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
+		McpJsonMapper jsonMapper;
 
 		McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
 
@@ -1034,7 +1012,7 @@ public SyncSpecification capabilities(McpSchema.ServerCapabilities serverCapa
 		 * 

* Example usage:

{@code
 		 * .tool(
-		 *     new Tool("calculator", "Performs calculations", schema),
+		 *     Tool.builder().name("calculator").title("Performs calculations".inputSchema(schema).build(),
 		 *     (exchange, args) -> new CallToolResult("Result: " + calculate(args))
 		 * )
 		 * }
@@ -1382,20 +1360,6 @@ public SyncSpecification rootsChangeHandlers( return this.rootsChangeHandlers(List.of(handlers)); } - /** - * Sets the object mapper to use for serializing and deserializing JSON messages. - * @param objectMapper the instance to use. Must not be null. - * @return This builder instance for method chaining. - * @throws IllegalArgumentException if objectMapper is null - * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead - */ - @Deprecated(forRemoval = true) - public SyncSpecification objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Sets the JsonMapper to use for serializing and deserializing JSON messages. * @param jsonMapper the mapper to use. Must not be null. @@ -1438,7 +1402,7 @@ class StatelessAsyncSpecification { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); + McpJsonMapper jsonMapper; McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -1854,18 +1818,6 @@ public StatelessAsyncSpecification completions( return this; } - /** - * Sets the object mapper to use for serializing and deserializing JSON messages. - * @param objectMapper the instance to use. Must not be null. - * @return This builder instance for method chaining. - * @throws IllegalArgumentException if objectMapper is null - */ - public StatelessAsyncSpecification objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Sets the JsonMapper to use for serializing and deserializing JSON messages. * @param jsonMapper the mapper to use. Must not be null. @@ -1895,11 +1847,10 @@ public StatelessAsyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonS public McpStatelessAsyncServer build() { var features = new McpStatelessServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions); - var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(this.jsonMapper); - - return new McpStatelessAsyncServer(this.transport, this.jsonMapper, features, this.requestTimeout, - this.uriTemplateManagerFactory, jsonSchemaValidator); + return new McpStatelessAsyncServer(transport, + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, features, requestTimeout, + uriTemplateManagerFactory, + jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.createDefault()); } } @@ -1912,7 +1863,7 @@ class StatelessSyncSpecification { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); + McpJsonMapper jsonMapper; McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -2328,20 +2279,6 @@ public StatelessSyncSpecification completions( return this; } - /** - * Sets the object mapper to use for serializing and deserializing JSON messages. - * @param objectMapper the instance to use. Must not be null. - * @return This builder instance for method chaining. - * @throws IllegalArgumentException if objectMapper is null - * @deprecated Use {@link #jsonMapper(McpJsonMapper)} instead - */ - @Deprecated(forRemoval = true) - public StatelessSyncSpecification objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.jsonMapper = new JacksonMcpJsonMapper(objectMapper); - return this; - } - /** * Sets the JsonMapper to use for serializing and deserializing JSON messages. * @param jsonMapper the mapper to use. Must not be null. @@ -2385,16 +2322,13 @@ public StatelessSyncSpecification immediateExecution(boolean immediateExecution) } public McpStatelessSyncServer build() { - Objects.requireNonNull(this.jsonMapper, "JsonMapper must be set"); var syncFeatures = new McpStatelessServerFeatures.Sync(this.serverInfo, this.serverCapabilities, this.tools, this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions); var asyncFeatures = McpStatelessServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution); - - var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator - : new DefaultJsonSchemaValidator(this.jsonMapper); - - var asyncServer = new McpStatelessAsyncServer(this.transport, this.jsonMapper, asyncFeatures, - this.requestTimeout, this.uriTemplateManagerFactory, jsonSchemaValidator); + var asyncServer = new McpStatelessAsyncServer(transport, + jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, asyncFeatures, requestTimeout, + uriTemplateManagerFactory, + this.jsonSchemaValidator != null ? this.jsonSchemaValidator : JsonSchemaValidator.createDefault()); return new McpStatelessSyncServer(asyncServer, this.immediateExecution); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java index 12edfb341..cc3fae689 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java @@ -453,12 +453,13 @@ static AsyncCompletionSpecification fromSync(SyncCompletionSpecification complet * *
{@code
 	 * McpServerFeatures.SyncToolSpecification.builder()
-	 * 		.tool(new Tool(
-	 * 				"calculator",
-	 * 				"Performs mathematical calculations",
-	 * 				new JsonSchemaObject()
+	 * 		.tool(Tool.builder()
+	 * 				.name("calculator")
+	 * 				.title("Performs mathematical calculations")
+	 * 				.inputSchema(new JsonSchemaObject()
 	 * 						.required("expression")
-	 * 						.property("expression", JsonSchemaType.STRING)))
+	 * 						.property("expression", JsonSchemaType.STRING))
+	 * 				.build()
 	 * 		.toolHandler((exchange, req) -> {
 	 * 			String expr = (String) req.arguments().get("expression");
 	 * 			return new CallToolResult("Result: " + evaluate(expr));
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
index 0335f386c..ea1423c77 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
@@ -4,10 +4,10 @@
 
 package io.modelcontextprotocol.server;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
-import io.modelcontextprotocol.spec.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.json.McpJsonMapper;
 import io.modelcontextprotocol.common.McpTransportContext;
-import io.modelcontextprotocol.spec.JsonSchemaValidator;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
 import io.modelcontextprotocol.spec.McpError;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
index 9b562124a..4ebfb9e2e 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
@@ -14,8 +14,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.common.McpTransportContext;
 import io.modelcontextprotocol.server.McpTransportContextExtractor;
 import io.modelcontextprotocol.spec.McpError;
@@ -25,8 +25,6 @@
 import io.modelcontextprotocol.spec.McpServerTransportProvider;
 import io.modelcontextprotocol.spec.ProtocolVersions;
 import io.modelcontextprotocol.util.Assert;
-import io.modelcontextprotocol.spec.json.McpJsonMapper;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
 import io.modelcontextprotocol.util.KeepAliveScheduler;
 import jakarta.servlet.AsyncContext;
 import jakarta.servlet.ServletException;
@@ -120,61 +118,6 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
 	 */
 	private KeepAliveScheduler keepAliveScheduler;
 
-	/**
-	 * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
-	 * endpoint.
-	 * @param objectMapper The JSON object mapper to use for message
-	 * serialization/deserialization
-	 * @param messageEndpoint The endpoint path where clients will send their messages
-	 * @param sseEndpoint The endpoint path where clients will establish SSE connections
-	 * @deprecated Use the builder {@link #builder()} instead for better configuration
-	 * options.
-	 */
-	@Deprecated
-	public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint,
-			String sseEndpoint) {
-		this(new JacksonMcpJsonMapper(objectMapper), DEFAULT_BASE_URL, messageEndpoint, sseEndpoint, null,
-				(serverRequest) -> McpTransportContext.EMPTY);
-	}
-
-	/**
-	 * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
-	 * endpoint.
-	 * @param objectMapper The JSON object mapper to use for message
-	 * serialization/deserialization
-	 * @param baseUrl The base URL for the server transport
-	 * @param messageEndpoint The endpoint path where clients will send their messages
-	 * @param sseEndpoint The endpoint path where clients will establish SSE connections
-	 * @deprecated Use the builder {@link #builder()} instead for better configuration
-	 * options.
-	 */
-	@Deprecated
-	public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
-			String sseEndpoint) {
-		this(new JacksonMcpJsonMapper(objectMapper), baseUrl, messageEndpoint, sseEndpoint, null,
-				(serverRequest) -> McpTransportContext.EMPTY);
-	}
-
-	/**
-	 * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
-	 * endpoint.
-	 * @param objectMapper The JSON object mapper to use for message
-	 * serialization/deserialization
-	 * @param baseUrl The base URL for the server transport
-	 * @param messageEndpoint The endpoint path where clients will send their messages
-	 * @param sseEndpoint The endpoint path where clients will establish SSE connections
-	 * @param keepAliveInterval The interval for keep-alive pings, or null to disable
-	 * keep-alive functionality
-	 * @deprecated Use the builder {@link #builder()} instead for better configuration
-	 * options.
-	 */
-	@Deprecated
-	public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
-			String sseEndpoint, Duration keepAliveInterval) {
-		this(new JacksonMcpJsonMapper(objectMapper), baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval,
-				(serverRequest) -> McpTransportContext.EMPTY);
-	}
-
 	/**
 	 * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
 	 * endpoint.
@@ -221,17 +164,6 @@ public List protocolVersions() {
 		return List.of(ProtocolVersions.MCP_2024_11_05);
 	}
 
-	/**
-	 * Creates a new HttpServletSseServerTransportProvider instance with the default SSE
-	 * endpoint.
-	 * @param objectMapper The JSON object mapper to use for message
-	 * serialization/deserialization
-	 * @param messageEndpoint The endpoint path where clients will send their messages
-	 */
-	public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String messageEndpoint) {
-		this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
-	}
-
 	/**
 	 * Sets the session factory for creating new sessions.
 	 * @param sessionFactory The session factory to use
@@ -563,8 +495,6 @@ public static Builder builder() {
 	 */
 	public static class Builder {
 
-		private ObjectMapper objectMapper = new ObjectMapper();
-
 		private McpJsonMapper jsonMapper;
 
 		private String baseUrl = DEFAULT_BASE_URL;
@@ -578,18 +508,6 @@ public static class Builder {
 
 		private Duration keepAliveInterval;
 
-		/**
-		 * Sets the JSON object mapper to use for message serialization/deserialization.
-		 * @param objectMapper The object mapper to use
-		 * @return This builder instance for method chaining
-		 */
-		@Deprecated(forRemoval = true)
-		public Builder objectMapper(ObjectMapper objectMapper) {
-			Assert.notNull(objectMapper, "ObjectMapper must not be null");
-			this.objectMapper = objectMapper;
-			return this;
-		}
-
 		/**
 		 * Sets the JsonMapper implementation to use for serialization/deserialization. If
 		 * not specified, a JacksonJsonMapper will be created from the configured
@@ -668,19 +586,15 @@ public Builder keepAliveInterval(Duration keepAliveInterval) {
 		 * Builds a new instance of HttpServletSseServerTransportProvider with the
 		 * configured settings.
 		 * @return A new HttpServletSseServerTransportProvider instance
-		 * @throws IllegalStateException if objectMapper or messageEndpoint is not set
+		 * @throws IllegalStateException if jsonMapper or messageEndpoint is not set
 		 */
 		public HttpServletSseServerTransportProvider build() {
-			if (objectMapper == null) {
-				throw new IllegalStateException("ObjectMapper must be set");
-			}
 			if (messageEndpoint == null) {
 				throw new IllegalStateException("MessageEndpoint must be set");
 			}
-			McpJsonMapper effectiveMapper = (this.jsonMapper != null) ? this.jsonMapper
-					: new JacksonMcpJsonMapper(objectMapper);
-			return new HttpServletSseServerTransportProvider(effectiveMapper, baseUrl, messageEndpoint, sseEndpoint,
-					keepAliveInterval, contextExtractor);
+			return new HttpServletSseServerTransportProvider(
+					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, baseUrl, messageEndpoint,
+					sseEndpoint, keepAliveInterval, contextExtractor);
 		}
 
 	}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
index 24695732a..bccd4c848 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
@@ -8,12 +8,10 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.modelcontextprotocol.spec.json.McpJsonMapper;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
 
 import io.modelcontextprotocol.common.McpTransportContext;
 import io.modelcontextprotocol.server.McpStatelessServerHandler;
@@ -249,20 +247,6 @@ private Builder() {
 			// used by a static method
 		}
 
-		/**
-		 * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
-		 * messages.
-		 * @param objectMapper The ObjectMapper instance. Must not be null.
-		 * @return this builder instance
-		 * @throws IllegalArgumentException if objectMapper is null
-		 */
-		@Deprecated(forRemoval = true)
-		public Builder objectMapper(ObjectMapper objectMapper) {
-			Assert.notNull(objectMapper, "ObjectMapper must not be null");
-			this.jsonMapper = new JacksonMcpJsonMapper(objectMapper);
-			return this;
-		}
-
 		/**
 		 * Sets the JsonMapper to use for JSON serialization/deserialization of MCP
 		 * messages.
@@ -311,12 +295,9 @@ public Builder contextExtractor(McpTransportContextExtractor
 		 * @throws IllegalStateException if required parameters are not set
 		 */
 		public HttpServletStatelessServerTransport build() {
-			if (this.jsonMapper == null) {
-				throw new IllegalStateException("JsonMapper must be set");
-			}
 			Assert.notNull(mcpEndpoint, "Message endpoint must be set");
-
-			return new HttpServletStatelessServerTransport(jsonMapper, mcpEndpoint, contextExtractor);
+			return new HttpServletStatelessServerTransport(
+					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, contextExtractor);
 		}
 
 	}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
index 10f4d6d50..fb87d3d7b 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
@@ -16,8 +16,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.TypeRef;
 
 import io.modelcontextprotocol.common.McpTransportContext;
 import io.modelcontextprotocol.server.McpTransportContextExtractor;
@@ -29,8 +28,7 @@
 import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
 import io.modelcontextprotocol.spec.ProtocolVersions;
 import io.modelcontextprotocol.util.Assert;
-import io.modelcontextprotocol.spec.json.McpJsonMapper;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
 import io.modelcontextprotocol.util.KeepAliveScheduler;
 import jakarta.servlet.AsyncContext;
 import jakarta.servlet.ServletException;
@@ -775,20 +773,6 @@ public static class Builder {
 
 		private Duration keepAliveInterval;
 
-		/**
-		 * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
-		 * messages.
-		 * @param objectMapper The ObjectMapper instance. Must not be null.
-		 * @return this builder instance
-		 * @throws IllegalArgumentException if objectMapper is null
-		 */
-		@Deprecated(forRemoval = true)
-		public Builder objectMapper(ObjectMapper objectMapper) {
-			Assert.notNull(objectMapper, "ObjectMapper must not be null");
-			this.jsonMapper = new JacksonMcpJsonMapper(objectMapper);
-			return this;
-		}
-
 		/**
 		 * Sets the JsonMapper to use for JSON serialization/deserialization of MCP
 		 * messages.
@@ -855,13 +839,10 @@ public Builder keepAliveInterval(Duration keepAliveInterval) {
 		 * @throws IllegalStateException if required parameters are not set
 		 */
 		public HttpServletStreamableServerTransportProvider build() {
-			if (this.jsonMapper == null) {
-				throw new IllegalStateException("JsonMapper must be set");
-			}
 			Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set");
-
-			return new HttpServletStreamableServerTransportProvider(jsonMapper, this.mcpEndpoint, this.disallowDelete,
-					this.contextExtractor, this.keepAliveInterval);
+			return new HttpServletStreamableServerTransportProvider(
+					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, disallowDelete,
+					contextExtractor, keepAliveInterval);
 		}
 
 	}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java
index 357db189c..68be62931 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java
@@ -15,8 +15,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.spec.McpError;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
@@ -25,8 +24,7 @@
 import io.modelcontextprotocol.spec.McpServerTransportProvider;
 import io.modelcontextprotocol.spec.ProtocolVersions;
 import io.modelcontextprotocol.util.Assert;
-import io.modelcontextprotocol.spec.json.McpJsonMapper;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import reactor.core.publisher.Flux;
@@ -58,25 +56,13 @@ public class StdioServerTransportProvider implements McpServerTransportProvider
 
 	private final Sinks.One inboundReady = Sinks.one();
 
-	/**
-	 * Creates a new StdioServerTransportProvider with a default ObjectMapper and System
-	 * streams.
-	 */
-	public StdioServerTransportProvider() {
-		this(new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()), System.in, System.out);
-	}
-
 	/**
 	 * Creates a new StdioServerTransportProvider with the specified ObjectMapper and
 	 * System streams.
-	 * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
-	 * @deprecated Prefer
-	 * {@link StdioServerTransportProvider#StdioServerTransportProvider(McpJsonMapper, InputStream, OutputStream)}
-	 * or use the no-arg constructor which defaults to a JacksonJsonMapper internally.
+	 * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
 	 */
-	@Deprecated(forRemoval = true)
-	public StdioServerTransportProvider(ObjectMapper objectMapper) {
-		this(new JacksonMcpJsonMapper(objectMapper), System.in, System.out);
+	public StdioServerTransportProvider(McpJsonMapper jsonMapper) {
+		this(jsonMapper, System.in, System.out);
 	}
 
 	/**
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java b/mcp/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java
deleted file mode 100644
index 572d7c043..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.spec;
-
-import java.util.Map;
-
-/**
- * Interface for validating structured content against a JSON schema. This interface
- * defines a method to validate structured content based on the provided output schema.
- *
- * @author Christian Tzolov
- */
-public interface JsonSchemaValidator {
-
-	/**
-	 * Represents the result of a validation operation.
-	 *
-	 * @param valid Indicates whether the validation was successful.
-	 * @param errorMessage An error message if the validation failed, otherwise null.
-	 * @param jsonStructuredOutput The text structured content in JSON format if the
-	 * validation was successful, otherwise null.
-	 */
-	public record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) {
-
-		public static ValidationResponse asValid(String jsonStructuredOutput) {
-			return new ValidationResponse(true, null, jsonStructuredOutput);
-		}
-
-		public static ValidationResponse asInvalid(String message) {
-			return new ValidationResponse(false, message, null);
-		}
-	}
-
-	/**
-	 * Validates the structured content against the provided JSON schema.
-	 * @param schema The JSON schema to validate against.
-	 * @param structuredContent The structured content to validate.
-	 * @return A ValidationResponse indicating whether the validation was successful or
-	 * not.
-	 */
-	ValidationResponse validate(Map schema, Map structuredContent);
-
-}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index d5da1b3df..bc3f53467 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -4,7 +4,7 @@
 
 package io.modelcontextprotocol.spec;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.util.Assert;
 import org.reactivestreams.Publisher;
 import org.slf4j.Logger;
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
index 04cde7c46..14bd5f28d 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -21,9 +21,8 @@
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
-import io.modelcontextprotocol.spec.json.McpJsonMapper;
-import io.modelcontextprotocol.spec.json.TypeRef;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 
 import io.modelcontextprotocol.util.Assert;
 
@@ -112,20 +111,6 @@ private McpSchema() {
 	// Elicitation Methods
 	public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
 
-	private static volatile McpJsonMapper JSON_MAPPER = new JacksonMcpJsonMapper(
-			new com.fasterxml.jackson.databind.ObjectMapper());
-
-	/**
-	 * Allows overriding the default JSON mapper used internally by schema helper methods.
-	 * This is optional; callers can also pass a JsonMapper directly to
-	 * deserializeJsonRpcMessage.
-	 * @param mapper The JsonMapper to use
-	 */
-	public static void setJsonMapper(McpJsonMapper mapper) {
-		Assert.notNull(mapper, "jsonMapper must not be null");
-		JSON_MAPPER = mapper;
-	}
-
 	// ---------------------------
 	// JSON-RPC Error Codes
 	// ---------------------------
@@ -225,12 +210,6 @@ else if (map.containsKey("result") || map.containsKey("error")) {
 		throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + jsonText);
 	}
 
-	@Deprecated(forRemoval = true)
-	public static JSONRPCMessage deserializeJsonRpcMessage(com.fasterxml.jackson.databind.ObjectMapper objectMapper,
-			String jsonText) throws IOException {
-		return deserializeJsonRpcMessage(new JacksonMcpJsonMapper(objectMapper), jsonText);
-	}
-
 	// ---------------------------
 	// JSON-RPC Message Types
 	// ---------------------------
@@ -1291,53 +1270,6 @@ public record Tool( // @formatter:off
 		@JsonProperty("annotations") ToolAnnotations annotations,
 		@JsonProperty("_meta") Map meta) { // @formatter:on
 
-		/**
-		 * @deprecated Only exists for backwards-compatibility purposes. Use
-		 * {@link Tool#builder()} instead.
-		 */
-		@Deprecated
-		public Tool(String name, String description, JsonSchema inputSchema, ToolAnnotations annotations) {
-			this(name, null, description, inputSchema, null, annotations, null);
-		}
-
-		/**
-		 * @deprecated Only exists for backwards-compatibility purposes. Use
-		 * {@link Tool#builder()} instead.
-		 */
-		@Deprecated
-		public Tool(String name, String description, String inputSchema) {
-			this(name, null, description, parseSchema(inputSchema), null, null, null);
-		}
-
-		/**
-		 * @deprecated Only exists for backwards-compatibility purposes. Use
-		 * {@link Tool#builder()} instead.
-		 */
-		@Deprecated
-		public Tool(String name, String description, String schema, ToolAnnotations annotations) {
-			this(name, null, description, parseSchema(schema), null, annotations, null);
-		}
-
-		/**
-		 * @deprecated Only exists for backwards-compatibility purposes. Use
-		 * {@link Tool#builder()} instead.
-		 */
-		@Deprecated
-		public Tool(String name, String description, String inputSchema, String outputSchema,
-				ToolAnnotations annotations) {
-			this(name, null, description, parseSchema(inputSchema), schemaToMap(outputSchema), annotations, null);
-		}
-
-		/**
-		 * @deprecated Only exists for backwards-compatibility purposes. Use
-		 * {@link Tool#builder()} instead.
-		 */
-		@Deprecated
-		public Tool(String name, String title, String description, String inputSchema, String outputSchema,
-				ToolAnnotations annotations) {
-			this(name, title, description, parseSchema(inputSchema), schemaToMap(outputSchema), annotations, null);
-		}
-
 		public static Builder builder() {
 			return new Builder();
 		}
@@ -1378,8 +1310,8 @@ public Builder inputSchema(JsonSchema inputSchema) {
 				return this;
 			}
 
-			public Builder inputSchema(String inputSchema) {
-				this.inputSchema = parseSchema(inputSchema);
+			public Builder inputSchema(McpJsonMapper jsonMapper, String inputSchema) {
+				this.inputSchema = parseSchema(jsonMapper, inputSchema);
 				return this;
 			}
 
@@ -1388,8 +1320,8 @@ public Builder outputSchema(Map outputSchema) {
 				return this;
 			}
 
-			public Builder outputSchema(String outputSchema) {
-				this.outputSchema = schemaToMap(outputSchema);
+			public Builder outputSchema(McpJsonMapper jsonMapper, String outputSchema) {
+				this.outputSchema = schemaToMap(jsonMapper, outputSchema);
 				return this;
 			}
 
@@ -1411,18 +1343,18 @@ public Tool build() {
 		}
 	}
 
-	private static Map schemaToMap(String schema) {
+	private static Map schemaToMap(McpJsonMapper jsonMapper, String schema) {
 		try {
-			return JSON_MAPPER.readValue(schema, MAP_TYPE_REF);
+			return jsonMapper.readValue(schema, MAP_TYPE_REF);
 		}
 		catch (IOException e) {
 			throw new IllegalArgumentException("Invalid schema: " + schema, e);
 		}
 	}
 
-	private static JsonSchema parseSchema(String schema) {
+	private static JsonSchema parseSchema(McpJsonMapper jsonMapper, String schema) {
 		try {
-			return JSON_MAPPER.readValue(schema, JsonSchema.class);
+			return jsonMapper.readValue(schema, JsonSchema.class);
 		}
 		catch (IOException e) {
 			throw new IllegalArgumentException("Invalid schema: " + schema, e);
@@ -1446,17 +1378,17 @@ public record CallToolRequest( // @formatter:off
 		@JsonProperty("arguments") Map arguments,
 		@JsonProperty("_meta") Map meta) implements Request { // @formatter:on
 
-		public CallToolRequest(String name, String jsonArguments) {
-			this(name, parseJsonArguments(jsonArguments), null);
+		public CallToolRequest(McpJsonMapper jsonMapper, String name, String jsonArguments) {
+			this(name, parseJsonArguments(jsonMapper, jsonArguments), null);
 		}
 
 		public CallToolRequest(String name, Map arguments) {
 			this(name, arguments, null);
 		}
 
-		private static Map parseJsonArguments(String jsonArguments) {
+		private static Map parseJsonArguments(McpJsonMapper jsonMapper, String jsonArguments) {
 			try {
-				return JSON_MAPPER.readValue(jsonArguments, MAP_TYPE_REF);
+				return jsonMapper.readValue(jsonArguments, MAP_TYPE_REF);
 			}
 			catch (IOException e) {
 				throw new IllegalArgumentException("Invalid arguments: " + jsonArguments, e);
@@ -1485,8 +1417,8 @@ public Builder arguments(Map arguments) {
 				return this;
 			}
 
-			public Builder arguments(String jsonArguments) {
-				this.arguments = parseJsonArguments(jsonArguments);
+			public Builder arguments(McpJsonMapper jsonMapper, String jsonArguments) {
+				this.arguments = parseJsonArguments(jsonMapper, jsonArguments);
 				return this;
 			}
 
@@ -1591,10 +1523,10 @@ public Builder structuredContent(Map structuredContent) {
 				return this;
 			}
 
-			public Builder structuredContent(String structuredContent) {
+			public Builder structuredContent(McpJsonMapper jsonMapper, String structuredContent) {
 				Assert.hasText(structuredContent, "structuredContent must not be empty");
 				try {
-					this.structuredContent = JSON_MAPPER.readValue(structuredContent, MAP_TYPE_REF);
+					this.structuredContent = jsonMapper.readValue(structuredContent, MAP_TYPE_REF);
 				}
 				catch (IOException e) {
 					throw new IllegalArgumentException("Invalid structured content: " + structuredContent, e);
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
index d12602c99..b9ff041a9 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java
@@ -16,7 +16,7 @@
 import io.modelcontextprotocol.server.McpInitRequestHandler;
 import io.modelcontextprotocol.server.McpNotificationHandler;
 import io.modelcontextprotocol.server.McpRequestHandler;
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.util.Assert;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java
index 53fde1702..767ed673e 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSession.java
@@ -4,7 +4,7 @@
 
 package io.modelcontextprotocol.spec;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import reactor.core.publisher.Mono;
 
 /**
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java
index d3abb5c7c..ec03dd424 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java
@@ -15,7 +15,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 
 import io.modelcontextprotocol.common.McpTransportContext;
 import io.modelcontextprotocol.server.McpAsyncServerExchange;
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java
index df8d4fc2c..0a732bab6 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java
@@ -7,7 +7,7 @@
 import java.util.List;
 
 import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import reactor.core.publisher.Mono;
 
 /**
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java
index e4acf1b8a..0bf70d5b8 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/MissingMcpTransportSession.java
@@ -4,7 +4,7 @@
 
 package io.modelcontextprotocol.spec;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.util.Assert;
 import reactor.core.publisher.Mono;
 
diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java b/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java
index 2bdee48c0..6d53ed516 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java
@@ -11,7 +11,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSession;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
index dbb55650b..aabdd7cd2 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
@@ -9,8 +9,8 @@
 import java.util.function.BiConsumer;
 import java.util.function.Function;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.spec.McpClientTransport;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification;
@@ -100,7 +100,7 @@ public Mono closeGracefully() {
 
 	@Override
 	public  T unmarshalFrom(Object data, TypeRef typeRef) {
-		return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()).convertValue(data, typeRef);
+		return McpJsonMapper.createDefault().convertValue(data, typeRef);
 	}
 
 }
diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java
index 3e076e1dc..f42324faf 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java
@@ -8,8 +8,8 @@
 import java.util.List;
 import java.util.function.BiConsumer;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification;
 import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest;
@@ -68,7 +68,7 @@ public Mono closeGracefully() {
 
 	@Override
 	public  T unmarshalFrom(Object data, TypeRef typeRef) {
-		return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()).convertValue(data, typeRef);
+		return McpJsonMapper.createDefault().convertValue(data, typeRef);
 	}
 
 }
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java
index af802df48..859dc5f82 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java
@@ -4,6 +4,7 @@
 
 package io.modelcontextprotocol.client;
 
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -22,8 +23,6 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
@@ -177,7 +176,12 @@ void testListAllToolsReturnsImmutableList() {
 				.consumeNextWith(result -> {
 					assertThat(result.tools()).isNotNull();
 					// Verify that the returned list is immutable
-					assertThatThrownBy(() -> result.tools().add(new Tool("test", "test", "{\"type\":\"object\"}")))
+					assertThatThrownBy(() -> result.tools()
+						.add(Tool.builder()
+							.name("test")
+							.title("test")
+							.inputSchema(JSON_MAPPER, "{\"type\":\"object\"}")
+							.build()))
 						.isInstanceOf(UnsupportedOperationException.class);
 				})
 				.verifyComplete();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java
index 4f6551199..6ccf56d73 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java
@@ -22,8 +22,6 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpAsyncClientTests.java
index 89848c549..1e015ca5e 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpAsyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpAsyncClientTests.java
@@ -28,7 +28,6 @@ public class HttpClientStreamableHttpAsyncClientTests extends AbstractMcpAsyncCl
 
 	@Override
 	protected McpClientTransport createMcpTransport() {
-
 		return HttpClientStreamableHttpTransport.builder(host).build();
 	}
 
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java
index 5dd3c146c..612a65898 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java
@@ -4,14 +4,13 @@
 
 package io.modelcontextprotocol.client;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import io.modelcontextprotocol.spec.json.TypeRef;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.MockMcpClientTransport;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
@@ -24,6 +23,7 @@
 import reactor.core.publisher.Mono;
 
 import static io.modelcontextprotocol.spec.McpSchema.METHOD_INITIALIZE;
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
@@ -93,7 +93,7 @@ void testSuccessfulInitialization() {
 	}
 
 	@Test
-	void testToolsChangeNotificationHandling() throws JsonProcessingException {
+	void testToolsChangeNotificationHandling() throws IOException {
 		MockMcpClientTransport transport = initializationEnabledTransport();
 
 		// Create a list to store received tools for verification
@@ -110,8 +110,11 @@ 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-1", "Test Tool 1 Description",
-				new ObjectMapper().writeValueAsString(inputSchema));
+		McpSchema.Tool mockTool = McpSchema.Tool.builder()
+			.name("test-tool-1")
+			.description("Test Tool 1 Description")
+			.inputSchema(JSON_MAPPER, JSON_MAPPER.writeValueAsString(inputSchema))
+			.build();
 
 		// Create page 1 response with nextPageToken
 		String nextPageToken = "page2Token";
@@ -131,9 +134,11 @@ void testToolsChangeNotificationHandling() throws JsonProcessingException {
 		transport.simulateIncomingMessage(toolsListResponse1);
 
 		// Create mock tools for page 2
-		McpSchema.Tool mockTool2 = new McpSchema.Tool("test-tool-2", "Test Tool 2 Description",
-				new ObjectMapper().writeValueAsString(inputSchema));
-
+		McpSchema.Tool mockTool2 = McpSchema.Tool.builder()
+			.name("test-tool-2")
+			.description("Test Tool 2 Description")
+			.inputSchema(JSON_MAPPER, JSON_MAPPER.writeValueAsString(inputSchema))
+			.build();
 		// Create page 2 response with no nextPageToken (last page)
 		McpSchema.ListToolsResult mockToolsResult2 = new McpSchema.ListToolsResult(List.of(mockTool2), null);
 
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java
index 459687797..3e29e89ab 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java
@@ -4,8 +4,7 @@
 
 package io.modelcontextprotocol.client;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.spec.McpClientTransport;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.ProtocolVersions;
@@ -16,6 +15,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
 import static org.assertj.core.api.Assertions.assertThatCode;
 
 class McpAsyncClientTests {
@@ -31,8 +31,6 @@ class McpAsyncClientTests {
 
 	private static final String CONTEXT_KEY = "context.key";
 
-	private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
 	@Test
 	void validateContextPassedToTransportConnect() {
 		McpClientTransport transport = new McpClientTransport() {
@@ -74,7 +72,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) {
 
 			@Override
 			public  T unmarshalFrom(Object data, TypeRef typeRef) {
-				return OBJECT_MAPPER.convertValue(data, new com.fasterxml.jackson.core.type.TypeReference() {
+				return JSON_MAPPER.convertValue(data, new TypeRef<>() {
 					@Override
 					public java.lang.reflect.Type getType() {
 						return typeRef.getType();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java b/mcp/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java
new file mode 100644
index 000000000..63ec015fe
--- /dev/null
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java
@@ -0,0 +1,19 @@
+package io.modelcontextprotocol.client;
+
+import io.modelcontextprotocol.client.transport.ServerParameters;
+
+public final class ServerParameterUtils {
+
+	private ServerParameterUtils() {
+	}
+
+	public static ServerParameters createServerParameters() {
+		if (System.getProperty("os.name").toLowerCase().contains("win")) {
+			return ServerParameters.builder("cmd.exe")
+				.args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "stdio")
+				.build();
+		}
+		return ServerParameters.builder("npx").args("-y", "@modelcontextprotocol/server-everything", "stdio").build();
+	}
+
+}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java
index ef404c9ae..aa8aaa397 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpAsyncClientTests.java
@@ -11,6 +11,9 @@
 import io.modelcontextprotocol.spec.McpClientTransport;
 import org.junit.jupiter.api.Timeout;
 
+import static io.modelcontextprotocol.client.ServerParameterUtils.createServerParameters;
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
+
 /**
  * Tests for the {@link McpAsyncClient} with {@link StdioClientTransport}.
  *
@@ -29,18 +32,7 @@ class StdioMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
 
 	@Override
 	protected McpClientTransport createMcpTransport() {
-		ServerParameters stdioParams;
-		if (System.getProperty("os.name").toLowerCase().contains("win")) {
-			stdioParams = ServerParameters.builder("cmd.exe")
-				.args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "stdio")
-				.build();
-		}
-		else {
-			stdioParams = ServerParameters.builder("npx")
-				.args("-y", "@modelcontextprotocol/server-everything", "stdio")
-				.build();
-		}
-		return new StdioClientTransport(stdioParams);
+		return new StdioClientTransport(createServerParameters(), JSON_MAPPER);
 	}
 
 	protected Duration getInitializationTimeout() {
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
index 95c1e2947..b1e567989 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
@@ -17,6 +17,8 @@
 import reactor.core.publisher.Sinks;
 import reactor.test.StepVerifier;
 
+import static io.modelcontextprotocol.client.ServerParameterUtils.createServerParameters;
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
@@ -37,18 +39,8 @@ class StdioMcpSyncClientTests extends AbstractMcpSyncClientTests {
 
 	@Override
 	protected McpClientTransport createMcpTransport() {
-		ServerParameters stdioParams;
-		if (System.getProperty("os.name").toLowerCase().contains("win")) {
-			stdioParams = ServerParameters.builder("cmd.exe")
-				.args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything", "stdio")
-				.build();
-		}
-		else {
-			stdioParams = ServerParameters.builder("npx")
-				.args("-y", "@modelcontextprotocol/server-everything", "stdio")
-				.build();
-		}
-		return new StdioClientTransport(stdioParams);
+		ServerParameters stdioParams = createServerParameters();
+		return new StdioClientTransport(stdioParams, JSON_MAPPER);
 	}
 
 	@Test
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
index d3f59824d..c5c365798 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
@@ -20,7 +20,6 @@
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest;
 
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
@@ -37,6 +36,7 @@
 import org.springframework.http.codec.ServerSentEvent;
 import org.springframework.web.util.UriComponentsBuilder;
 
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.mockito.ArgumentMatchers.any;
@@ -77,8 +77,7 @@ static class TestHttpClientSseClientTransport extends HttpClientSseClientTranspo
 
 		public TestHttpClientSseClientTransport(final String baseUri) {
 			super(HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(),
-					HttpRequest.newBuilder().header("Content-Type", "application/json"), baseUri, "/sse",
-					new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()),
+					HttpRequest.newBuilder().header("Content-Type", "application/json"), baseUri, "/sse", JSON_MAPPER,
 					McpAsyncHttpClientRequestCustomizer.NOOP);
 		}
 
diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java
index 398b1540b..234874834 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java
@@ -20,7 +20,6 @@
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
-import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java
index fb19c62f7..8b2dea462 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/common/AsyncServerMcpTransportContextIntegrationTests.java
@@ -7,7 +7,6 @@
 import java.util.Map;
 import java.util.function.BiFunction;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import io.modelcontextprotocol.client.McpAsyncClient;
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
@@ -100,19 +99,16 @@ public class AsyncServerMcpTransportContextIntegrationTests {
 
 	private final HttpServletStatelessServerTransport statelessServerTransport = HttpServletStatelessServerTransport
 		.builder()
-		.objectMapper(new ObjectMapper())
 		.contextExtractor(serverContextExtractor)
 		.build();
 
 	private final HttpServletStreamableServerTransportProvider streamableServerTransport = HttpServletStreamableServerTransportProvider
 		.builder()
-		.objectMapper(new ObjectMapper())
 		.contextExtractor(serverContextExtractor)
 		.build();
 
 	private final HttpServletSseServerTransportProvider sseServerTransport = HttpServletSseServerTransportProvider
 		.builder()
-		.objectMapper(new ObjectMapper())
 		.contextExtractor(serverContextExtractor)
 		.messageEndpoint("/message")
 		.build();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java
index 42747f717..cc8f4c4be 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/common/SyncServerMcpTransportContextIntegrationTests.java
@@ -4,7 +4,6 @@
 
 package io.modelcontextprotocol.common;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.McpClient.SyncSpec;
 import io.modelcontextprotocol.client.McpSyncClient;
@@ -87,19 +86,16 @@ public class SyncServerMcpTransportContextIntegrationTests {
 
 	private final HttpServletStatelessServerTransport statelessServerTransport = HttpServletStatelessServerTransport
 		.builder()
-		.objectMapper(new ObjectMapper())
 		.contextExtractor(serverContextExtractor)
 		.build();
 
 	private final HttpServletStreamableServerTransportProvider streamableServerTransport = HttpServletStreamableServerTransportProvider
 		.builder()
-		.objectMapper(new ObjectMapper())
 		.contextExtractor(serverContextExtractor)
 		.build();
 
 	private final HttpServletSseServerTransportProvider sseServerTransport = HttpServletSseServerTransportProvider
 		.builder()
-		.objectMapper(new ObjectMapper())
 		.contextExtractor(serverContextExtractor)
 		.messageEndpoint("/message")
 		.build();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
index 0ba8bf929..93e49bc1c 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
@@ -4,6 +4,7 @@
 
 package io.modelcontextprotocol.server;
 
+import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -92,18 +93,14 @@ void testImmediateClose() {
 	// ---------------------------------------
 	// Tools Tests
 	// ---------------------------------------
-	String emptyJsonSchema = """
-			{
-				"$schema": "http://json-schema.org/draft-07/schema#",
-				"type": "object",
-				"properties": {}
-			}
-			""";
-
 	@Test
 	@Deprecated
 	void testAddTool() {
-		Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
+		Tool newTool = McpSchema.Tool.builder()
+			.name("new-tool")
+			.title("New test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
 			.build();
@@ -117,7 +114,12 @@ void testAddTool() {
 
 	@Test
 	void testAddToolCall() {
-		Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
+		Tool newTool = McpSchema.Tool.builder()
+			.name("new-tool")
+			.title("New test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
+
 		var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
 			.build();
@@ -133,7 +135,11 @@ void testAddToolCall() {
 	@Test
 	@Deprecated
 	void testAddDuplicateTool() {
-		Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name(TEST_TOOL_NAME)
+			.title("Duplicate tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -153,7 +159,11 @@ void testAddDuplicateTool() {
 
 	@Test
 	void testAddDuplicateToolCall() {
-		Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name(TEST_TOOL_NAME)
+			.title("Duplicate tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -173,8 +183,11 @@ void testAddDuplicateToolCall() {
 
 	@Test
 	void testDuplicateToolCallDuringBuilding() {
-		Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building",
-				emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name("duplicate-build-toolcall")
+			.title("Duplicate toolcall during building")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -186,7 +199,12 @@ void testDuplicateToolCallDuringBuilding() {
 
 	@Test
 	void testDuplicateToolsInBatchListRegistration() {
-		Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name("batch-list-tool")
+			.title("Duplicate tool in batch list")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
+
 		List specs = List.of(
 				McpServerFeatures.AsyncToolSpecification.builder()
 					.tool(duplicateTool)
@@ -207,7 +225,11 @@ void testDuplicateToolsInBatchListRegistration() {
 
 	@Test
 	void testDuplicateToolsInBatchVarargsRegistration() {
-		Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name("batch-varargs-tool")
+			.title("Duplicate tool in batch varargs")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -226,7 +248,11 @@ void testDuplicateToolsInBatchVarargsRegistration() {
 
 	@Test
 	void testRemoveTool() {
-		Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
+		Tool too = McpSchema.Tool.builder()
+			.name(TEST_TOOL_NAME)
+			.title("Duplicate tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -253,7 +279,11 @@ void testRemoveNonexistentTool() {
 
 	@Test
 	void testNotifyToolsListChanged() {
-		Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
+		Tool too = McpSchema.Tool.builder()
+			.name(TEST_TOOL_NAME)
+			.title("Duplicate tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java
index 8dae452f0..f754c2365 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java
@@ -50,6 +50,7 @@
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
+import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -102,7 +103,7 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) {
 		var clientBuilder = clientBuilders.get(clientType);
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 				return exchange.createMessage(mock(McpSchema.CreateMessageRequest.class))
 					.then(Mono.just(mock(CallToolResult.class)));
@@ -151,7 +152,7 @@ void testCreateMessageSuccess(String clientType) {
 		AtomicReference samplingResult = new AtomicReference<>();
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				var createMessageRequest = McpSchema.CreateMessageRequest.builder()
@@ -229,7 +230,7 @@ void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws Interr
 		AtomicReference samplingResult = new AtomicReference<>();
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				var createMessageRequest = McpSchema.CreateMessageRequest.builder()
@@ -303,7 +304,7 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt
 				null);
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				var createMessageRequest = McpSchema.CreateMessageRequest.builder()
@@ -353,7 +354,7 @@ void testCreateElicitationWithoutElicitationCapabilities(String clientType) {
 		var clientBuilder = clientBuilders.get(clientType);
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> exchange.createElicitation(mock(ElicitRequest.class))
 				.then(Mono.just(mock(CallToolResult.class))))
 			.build();
@@ -396,7 +397,7 @@ void testCreateElicitationSuccess(String clientType) {
 				null);
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				var elicitationRequest = McpSchema.ElicitRequest.builder()
@@ -453,7 +454,7 @@ void testCreateElicitationWithRequestTimeoutSuccess(String clientType) {
 		AtomicReference resultRef = new AtomicReference<>();
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				var elicitationRequest = McpSchema.ElicitRequest.builder()
@@ -524,7 +525,7 @@ void testCreateElicitationWithRequestTimeoutFail(String clientType) {
 		AtomicReference resultRef = new AtomicReference<>();
 
 		McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				var elicitationRequest = ElicitRequest.builder()
@@ -622,7 +623,7 @@ void testRootsWithoutCapability(String clientType) {
 		var clientBuilder = clientBuilders.get(clientType);
 
 		McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				exchange.listRoots(); // try to list roots
@@ -753,14 +754,6 @@ void testRootsServerCloseWithActiveSubscription(String clientType) {
 	// ---------------------------------------
 	// Tools Tests
 	// ---------------------------------------
-	String emptyJsonSchema = """
-			{
-				"$schema": "http://json-schema.org/draft-07/schema#",
-				"type": "object",
-				"properties": {}
-			}
-			""";
-
 	@ParameterizedTest(name = "{0} : {displayName} ")
 	@ValueSource(strings = { "httpclient" })
 	void testToolCallSuccess(String clientType) {
@@ -770,7 +763,7 @@ void testToolCallSuccess(String clientType) {
 		var responseBodyIsNullOrBlank = new AtomicBoolean(false);
 		var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
 		McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				try {
@@ -824,7 +817,7 @@ void testThrowingToolCallIsCaughtBeforeTimeout(String clientType) {
 				.tool(Tool.builder()
 					.name("tool1")
 					.description("tool1 description")
-					.inputSchema(emptyJsonSchema)
+					.inputSchema(EMPTY_JSON_SCHEMA)
 					.build())
 				.callHandler((exchange, request) -> {
 					// We trigger a timeout on blocking read, raising an exception
@@ -863,7 +856,7 @@ void testToolCallSuccessWithTranportContextExtraction(String clientType) {
 		var expectedCallResponse = new McpSchema.CallToolResult(
 				List.of(new McpSchema.TextContent("CALL RESPONSE; ctx=value")), null);
 		McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 
 				McpTransportContext transportContext = exchange.transportContext();
@@ -915,7 +908,7 @@ void testToolListChangeHandlingSuccess(String clientType) {
 
 		var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
 		McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
-			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
+			.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
 			.callHandler((exchange, request) -> {
 				// perform a blocking call to a remote service
 				try {
@@ -984,7 +977,7 @@ void testToolListChangeHandlingSuccess(String clientType) {
 				.tool(Tool.builder()
 					.name("tool2")
 					.description("tool2 description")
-					.inputSchema(emptyJsonSchema)
+					.inputSchema(EMPTY_JSON_SCHEMA)
 					.build())
 				.callHandler((exchange, request) -> callResponse)
 				.build();
@@ -1036,7 +1029,7 @@ void testLoggingNotification(String clientType) throws InterruptedException {
 			.tool(Tool.builder()
 				.name("logging-test")
 				.description("Test logging notifications")
-				.inputSchema(emptyJsonSchema)
+				.inputSchema(EMPTY_JSON_SCHEMA)
 				.build())
 			.callHandler((exchange, request) -> {
 
@@ -1150,7 +1143,7 @@ void testProgressNotification(String clientType) throws InterruptedException {
 			.tool(McpSchema.Tool.builder()
 				.name("progress-test")
 				.description("Test progress notifications")
-				.inputSchema(emptyJsonSchema)
+				.inputSchema(EMPTY_JSON_SCHEMA)
 				.build())
 			.callHandler((exchange, request) -> {
 
@@ -1263,7 +1256,7 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
 							List.of(new PromptArgument("language", "Language", "string", false))),
 					(mcpSyncServerExchange, getPromptRequest) -> null))
 			.completions(new McpServerFeatures.SyncCompletionSpecification(
-					new McpSchema.PromptReference("ref/prompt", "code_review", "Code review"), completionHandler))
+					new PromptReference("ref/prompt", "code_review", "Code review"), completionHandler))
 			.build();
 
 		try (var mcpClient = clientBuilder.build()) {
@@ -1304,7 +1297,7 @@ void testPingSuccess(String clientType) {
 			.tool(Tool.builder()
 				.name("ping-async-test")
 				.description("Test ping async behavior")
-				.inputSchema(emptyJsonSchema)
+				.inputSchema(EMPTY_JSON_SCHEMA)
 				.build())
 			.callHandler((exchange, request) -> {
 
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
index 67579ce72..dae2e38f9 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java
@@ -4,6 +4,7 @@
 
 package io.modelcontextprotocol.server;
 
+import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -101,14 +102,6 @@ void testGetAsyncServer() {
 	// Tools Tests
 	// ---------------------------------------
 
-	String emptyJsonSchema = """
-			{
-				"$schema": "http://json-schema.org/draft-07/schema#",
-				"type": "object",
-				"properties": {}
-			}
-			""";
-
 	@Test
 	@Deprecated
 	void testAddTool() {
@@ -116,7 +109,11 @@ void testAddTool() {
 			.capabilities(ServerCapabilities.builder().tools(true).build())
 			.build();
 
-		Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
+		Tool newTool = McpSchema.Tool.builder()
+			.name("new-tool")
+			.title("New test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(newTool,
 				(exchange, args) -> new CallToolResult(List.of(), false))))
 			.doesNotThrowAnyException();
@@ -130,7 +127,12 @@ void testAddToolCall() {
 			.capabilities(ServerCapabilities.builder().tools(true).build())
 			.build();
 
-		Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
+		Tool newTool = McpSchema.Tool.builder()
+			.name("new-tool")
+			.title("New test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
+
 		assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder()
 			.tool(newTool)
 			.callHandler((exchange, request) -> new CallToolResult(List.of(), false))
@@ -142,7 +144,11 @@ void testAddToolCall() {
 	@Test
 	@Deprecated
 	void testAddDuplicateTool() {
-		Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name(TEST_TOOL_NAME)
+			.title("Duplicate tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -159,7 +165,11 @@ void testAddDuplicateTool() {
 
 	@Test
 	void testAddDuplicateToolCall() {
-		Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name(TEST_TOOL_NAME)
+			.title("Duplicate tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -177,8 +187,11 @@ void testAddDuplicateToolCall() {
 
 	@Test
 	void testDuplicateToolCallDuringBuilding() {
-		Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building",
-				emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name("duplicate-build-toolcall")
+			.title("Duplicate toolcall during building")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -190,7 +203,11 @@ void testDuplicateToolCallDuringBuilding() {
 
 	@Test
 	void testDuplicateToolsInBatchListRegistration() {
-		Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name("batch-list-tool")
+			.title("Duplicate tool in batch list")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		List specs = List.of(
 				McpServerFeatures.SyncToolSpecification.builder()
 					.tool(duplicateTool)
@@ -211,7 +228,11 @@ void testDuplicateToolsInBatchListRegistration() {
 
 	@Test
 	void testDuplicateToolsInBatchVarargsRegistration() {
-		Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema);
+		Tool duplicateTool = McpSchema.Tool.builder()
+			.name("batch-varargs-tool")
+			.title("Duplicate tool in batch varargs")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -230,7 +251,11 @@ void testDuplicateToolsInBatchVarargsRegistration() {
 
 	@Test
 	void testRemoveTool() {
-		Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name(TEST_TOOL_NAME)
+			.title("Test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
 			.capabilities(ServerCapabilities.builder().tools(true).build())
@@ -429,7 +454,6 @@ void testRootsChangeHandlers() {
 				}
 			}))
 			.build();
-
 		assertThat(singleConsumerServer).isNotNull();
 		assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException();
 		onClose();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java b/mcp/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java
index 6744826c9..8fe8e6fb0 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java
@@ -4,12 +4,14 @@
 
 package io.modelcontextprotocol.server;
 
+import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.List;
 import java.util.Map;
 
+import io.modelcontextprotocol.spec.McpSchema;
 import org.junit.jupiter.api.Test;
 
 import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
@@ -26,16 +28,14 @@
  */
 class AsyncToolSpecificationBuilderTest {
 
-	String emptyJsonSchema = """
-			{
-				"type": "object"
-			}
-			""";
-
 	@Test
 	void builderShouldCreateValidAsyncToolSpecification() {
 
-		Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name("test-tool")
+			.title("A test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		McpServerFeatures.AsyncToolSpecification specification = McpServerFeatures.AsyncToolSpecification.builder()
 			.tool(tool)
@@ -58,7 +58,11 @@ void builderShouldThrowExceptionWhenToolIsNull() {
 
 	@Test
 	void builderShouldThrowExceptionWhenCallToolIsNull() {
-		Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name("test-tool")
+			.title("A test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 
 		assertThatThrownBy(() -> McpServerFeatures.AsyncToolSpecification.builder().tool(tool).build())
 			.isInstanceOf(IllegalArgumentException.class)
@@ -67,7 +71,11 @@ void builderShouldThrowExceptionWhenCallToolIsNull() {
 
 	@Test
 	void builderShouldAllowMethodChaining() {
-		Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name("test-tool")
+			.title("A test tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		McpServerFeatures.AsyncToolSpecification.Builder builder = McpServerFeatures.AsyncToolSpecification.builder();
 
 		// Then - verify method chaining returns the same builder instance
@@ -78,7 +86,11 @@ void builderShouldAllowMethodChaining() {
 
 	@Test
 	void builtSpecificationShouldExecuteCallToolCorrectly() {
-		Tool tool = new Tool("calculator", "Simple calculator", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name("calculator")
+			.title("Simple calculator")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		String expectedResult = "42";
 
 		McpServerFeatures.AsyncToolSpecification specification = McpServerFeatures.AsyncToolSpecification.builder()
@@ -103,7 +115,11 @@ void builtSpecificationShouldExecuteCallToolCorrectly() {
 	@Test
 	@SuppressWarnings("deprecation")
 	void deprecatedConstructorShouldWorkCorrectly() {
-		Tool tool = new Tool("deprecated-tool", "A deprecated tool", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name("deprecated-tool")
+			.title("A deprecated tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		String expectedResult = "deprecated result";
 
 		// Test the deprecated constructor that takes a 'call' function
@@ -143,7 +159,11 @@ void deprecatedConstructorShouldWorkCorrectly() {
 
 	@Test
 	void fromSyncShouldConvertSyncToolSpecificationCorrectly() {
-		Tool tool = new Tool("sync-tool", "A sync tool", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name("sync-tool")
+			.title("A sync tool")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		String expectedResult = "sync result";
 
 		// Create a sync tool specification
@@ -178,7 +198,11 @@ void fromSyncShouldConvertSyncToolSpecificationCorrectly() {
 	@Test
 	@SuppressWarnings("deprecation")
 	void fromSyncShouldConvertSyncToolSpecificationWithDeprecatedCallCorrectly() {
-		Tool tool = new Tool("sync-deprecated-tool", "A sync tool with deprecated call", emptyJsonSchema);
+		Tool tool = McpSchema.Tool.builder()
+			.name("sync-deprecated-tool")
+			.title("A sync tool with deprecated call")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		String expectedResult = "sync deprecated result";
 		McpAsyncServerExchange nullExchange = null; // Mock or create a suitable exchange
 													// if needed
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java
index 8e618b9a8..fd05b593b 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java
@@ -7,7 +7,6 @@
 import java.time.Duration;
 import java.util.Map;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
 import io.modelcontextprotocol.common.McpTransportContext;
@@ -42,7 +41,6 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT
 	public void before() {
 		// Create and configure the transport provider
 		mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
-			.objectMapper(new ObjectMapper())
 			.contextExtractor(TEST_CONTEXT_EXTRACTOR)
 			.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
 			.sseEndpoint(CUSTOM_SSE_ENDPOINT)
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java
index 5cc7d61be..ea5f04e8c 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java
@@ -11,7 +11,6 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiFunction;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
 import io.modelcontextprotocol.common.McpTransportContext;
@@ -48,6 +47,8 @@
 
 import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
 import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
+import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -69,7 +70,6 @@ class HttpServletStatelessIntegrationTests {
 	@BeforeEach
 	public void before() {
 		this.mcpStatelessServerTransport = HttpServletStatelessServerTransport.builder()
-			.objectMapper(new ObjectMapper())
 			.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
 			.build();
 
@@ -108,15 +108,6 @@ public void after() {
 	// ---------------------------------------
 	// Tools Tests
 	// ---------------------------------------
-
-	String emptyJsonSchema = """
-			{
-			"$schema": "http://json-schema.org/draft-07/schema#",
-			"type": "object",
-			"properties": {}
-			}
-			""";
-
 	@ParameterizedTest(name = "{0} : {displayName} ")
 	@ValueSource(strings = { "httpclient" })
 	void testToolCallSuccess(String clientType) {
@@ -125,7 +116,8 @@ void testToolCallSuccess(String clientType) {
 
 		var callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
 		McpStatelessServerFeatures.SyncToolSpecification tool1 = new McpStatelessServerFeatures.SyncToolSpecification(
-				new Tool("tool1", "tool1 description", emptyJsonSchema), (transportContext, request) -> {
+				Tool.builder().name("tool1").title("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build(),
+				(transportContext, request) -> {
 					// perform a blocking call to a remote service
 					String response = RestClient.create()
 						.get()
@@ -560,7 +552,7 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception {
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT);
 		MockHttpServletResponse response = new MockHttpServletResponse();
 
-		byte[] content = new ObjectMapper().writeValueAsBytes(jsonrpcRequest);
+		byte[] content = JSON_MAPPER.writeValueAsBytes(jsonrpcRequest);
 		request.setContent(content);
 		request.addHeader("Content-Type", "application/json");
 		request.addHeader("Content-Length", Integer.toString(content.length));
@@ -572,7 +564,7 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception {
 
 		mcpStatelessServerTransport.service(request, response);
 
-		McpSchema.JSONRPCResponse jsonrpcResponse = new ObjectMapper().readValue(response.getContentAsByteArray(),
+		McpSchema.JSONRPCResponse jsonrpcResponse = JSON_MAPPER.readValue(response.getContentAsByteArray(),
 				McpSchema.JSONRPCResponse.class);
 
 		assertThat(jsonrpcResponse).isNotNull();
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableAsyncServerTests.java
index 327ec1b21..96f1524b7 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableAsyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableAsyncServerTests.java
@@ -6,8 +6,6 @@
 
 import org.junit.jupiter.api.Timeout;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
 import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
 
@@ -21,10 +19,7 @@
 class HttpServletStreamableAsyncServerTests extends AbstractMcpAsyncServerTests {
 
 	protected McpStreamableServerTransportProvider createMcpTransportProvider() {
-		return HttpServletStreamableServerTransportProvider.builder()
-			.objectMapper(new ObjectMapper())
-			.mcpEndpoint("/mcp/message")
-			.build();
+		return HttpServletStreamableServerTransportProvider.builder().mcpEndpoint("/mcp/message").build();
 	}
 
 	@Override
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java
index 1f6a1fe58..223c78a94 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java
@@ -7,7 +7,6 @@
 import java.time.Duration;
 import java.util.Map;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
 import io.modelcontextprotocol.common.McpTransportContext;
@@ -40,7 +39,6 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg
 	public void before() {
 		// Create and configure the transport provider
 		mcpServerTransportProvider = HttpServletStreamableServerTransportProvider.builder()
-			.objectMapper(new ObjectMapper())
 			.contextExtractor(TEST_CONTEXT_EXTRACTOR)
 			.mcpEndpoint(MESSAGE_ENDPOINT)
 			.keepAliveInterval(Duration.ofSeconds(1))
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableSyncServerTests.java
index 66fa2b2ac..87c0712dc 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableSyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableSyncServerTests.java
@@ -6,8 +6,6 @@
 
 import org.junit.jupiter.api.Timeout;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
 import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
 
@@ -21,10 +19,7 @@
 class HttpServletStreamableSyncServerTests extends AbstractMcpSyncServerTests {
 
 	protected McpStreamableServerTransportProvider createMcpTransportProvider() {
-		return HttpServletStreamableServerTransportProvider.builder()
-			.objectMapper(new ObjectMapper())
-			.mcpEndpoint("/mcp/message")
-			.build();
+		return HttpServletStreamableServerTransportProvider.builder().mcpEndpoint("/mcp/message").build();
 	}
 
 	@Override
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java
index 80ec8462b..640d34c9c 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java
@@ -13,7 +13,7 @@
 import io.modelcontextprotocol.spec.McpError;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpServerSession;
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java
index f915895be..194c37000 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java
@@ -12,14 +12,13 @@
 import org.apache.catalina.LifecycleException;
 import org.apache.catalina.LifecycleState;
 import org.apache.catalina.startup.Tomcat;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
 import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
@@ -59,7 +58,6 @@ class McpCompletionTests {
 	public void before() {
 		// Create and con figure the transport provider
 		mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
-			.objectMapper(new ObjectMapper())
 			.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
 			.build();
 
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java
index 96071834e..069d0f896 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java
@@ -12,7 +12,7 @@
 import io.modelcontextprotocol.spec.McpError;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpServerSession;
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java
index 97db5fa06..b2dfbea25 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java
@@ -8,6 +8,8 @@
 import io.modelcontextprotocol.spec.McpServerTransportProvider;
 import org.junit.jupiter.api.Timeout;
 
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
+
 /**
  * Tests for {@link McpAsyncServer} using {@link StdioServerTransport}.
  *
@@ -17,7 +19,7 @@
 class StdioMcpAsyncServerTests extends AbstractMcpAsyncServerTests {
 
 	protected McpServerTransportProvider createMcpTransportProvider() {
-		return new StdioServerTransportProvider();
+		return new StdioServerTransportProvider(JSON_MAPPER);
 	}
 
 	@Override
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java
index 1e01962e9..c97c75d38 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerTests.java
@@ -8,6 +8,8 @@
 import io.modelcontextprotocol.spec.McpServerTransportProvider;
 import org.junit.jupiter.api.Timeout;
 
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
+
 /**
  * Tests for {@link McpSyncServer} using {@link StdioServerTransportProvider}.
  *
@@ -17,7 +19,7 @@
 class StdioMcpSyncServerTests extends AbstractMcpSyncServerTests {
 
 	protected McpServerTransportProvider createMcpTransportProvider() {
-		return new StdioServerTransportProvider();
+		return new StdioServerTransportProvider(JSON_MAPPER);
 	}
 
 	@Override
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java b/mcp/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java
index 4aac46952..cd643c600 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java
@@ -4,6 +4,7 @@
 
 package io.modelcontextprotocol.server;
 
+import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
@@ -24,16 +25,10 @@
  */
 class SyncToolSpecificationBuilderTest {
 
-	String emptyJsonSchema = """
-			{
-				"type": "object"
-			}
-			""";
-
 	@Test
 	void builderShouldCreateValidSyncToolSpecification() {
 
-		Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema);
+		Tool tool = Tool.builder().name("test-tool").title("A test tool").inputSchema(EMPTY_JSON_SCHEMA).build();
 
 		McpServerFeatures.SyncToolSpecification specification = McpServerFeatures.SyncToolSpecification.builder()
 			.tool(tool)
@@ -55,7 +50,7 @@ void builderShouldThrowExceptionWhenToolIsNull() {
 
 	@Test
 	void builderShouldThrowExceptionWhenCallToolIsNull() {
-		Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema);
+		Tool tool = Tool.builder().name("test-tool").description("A test tool").inputSchema(EMPTY_JSON_SCHEMA).build();
 
 		assertThatThrownBy(() -> McpServerFeatures.SyncToolSpecification.builder().tool(tool).build())
 			.isInstanceOf(IllegalArgumentException.class)
@@ -64,7 +59,7 @@ void builderShouldThrowExceptionWhenCallToolIsNull() {
 
 	@Test
 	void builderShouldAllowMethodChaining() {
-		Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema);
+		Tool tool = Tool.builder().name("test-tool").description("A test tool").inputSchema(EMPTY_JSON_SCHEMA).build();
 		McpServerFeatures.SyncToolSpecification.Builder builder = McpServerFeatures.SyncToolSpecification.builder();
 
 		// Then - verify method chaining returns the same builder instance
@@ -74,7 +69,11 @@ void builderShouldAllowMethodChaining() {
 
 	@Test
 	void builtSpecificationShouldExecuteCallToolCorrectly() {
-		Tool tool = new Tool("calculator", "Simple calculator", emptyJsonSchema);
+		Tool tool = Tool.builder()
+			.name("calculator")
+			.description("Simple calculator")
+			.inputSchema(EMPTY_JSON_SCHEMA)
+			.build();
 		String expectedResult = "42";
 
 		McpServerFeatures.SyncToolSpecification specification = McpServerFeatures.SyncToolSpecification.builder()
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
index 0462cbafe..be88097b3 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java
@@ -4,8 +4,6 @@
 
 package io.modelcontextprotocol.server.transport;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
 import io.modelcontextprotocol.server.McpServer;
@@ -40,7 +38,6 @@ public void before() {
 
 		// Create and configure the transport provider
 		mcpServerTransportProvider = HttpServletSseServerTransportProvider.builder()
-			.objectMapper(new ObjectMapper())
 			.baseUrl(CUSTOM_CONTEXT_PATH)
 			.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
 			.sseEndpoint(CUSTOM_SSE_ENDPOINT)
diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java
index 1d6dde66b..6a70af33d 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java
@@ -14,12 +14,10 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import io.modelcontextprotocol.spec.McpError;
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpServerSession;
 import io.modelcontextprotocol.spec.McpServerTransport;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
@@ -27,6 +25,7 @@
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
@@ -51,8 +50,6 @@ class StdioServerTransportProviderTests {
 
 	private StdioServerTransportProvider transportProvider;
 
-	private ObjectMapper objectMapper;
-
 	private McpServerSession.Factory sessionFactory;
 
 	private McpServerSession mockSession;
@@ -65,8 +62,6 @@ void setUp() {
 		System.setOut(testOutPrintStream);
 		System.setErr(testOutPrintStream);
 
-		objectMapper = new ObjectMapper();
-
 		// Create mocks for session factory and session
 		mockSession = mock(McpServerSession.class);
 		sessionFactory = mock(McpServerSession.Factory.class);
@@ -76,8 +71,7 @@ void setUp() {
 		when(mockSession.closeGracefully()).thenReturn(Mono.empty());
 		when(mockSession.sendNotification(any(), any())).thenReturn(Mono.empty());
 
-		transportProvider = new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper), System.in,
-				testOutPrintStream);
+		transportProvider = new StdioServerTransportProvider(JSON_MAPPER, System.in, testOutPrintStream);
 	}
 
 	@AfterEach
@@ -107,8 +101,7 @@ void shouldHandleIncomingMessages() throws Exception {
 		String jsonMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"test\",\"params\":{},\"id\":1}\n";
 		InputStream stream = new ByteArrayInputStream(jsonMessage.getBytes(StandardCharsets.UTF_8));
 
-		transportProvider = new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper), stream,
-				System.out);
+		transportProvider = new StdioServerTransportProvider(JSON_MAPPER, stream, System.out);
 		// Set up a real session to capture the message
 		AtomicReference capturedMessage = new AtomicReference<>();
 		CountDownLatch messageLatch = new CountDownLatch(1);
@@ -188,7 +181,7 @@ void shouldHandleMultipleCloseGracefullyCalls() {
 	@Test
 	void shouldHandleNotificationBeforeSessionFactoryIsSet() {
 
-		transportProvider = new StdioServerTransportProvider(objectMapper);
+		transportProvider = new StdioServerTransportProvider(JSON_MAPPER);
 		// Send notification before setting session factory
 		StepVerifier.create(transportProvider.notifyClients("testNotification", Map.of("key", "value")))
 			.verifyErrorSatisfies(error -> {
@@ -203,8 +196,7 @@ void shouldHandleInvalidJsonMessage() throws Exception {
 		String jsonMessage = "{invalid json}\n";
 		InputStream stream = new ByteArrayInputStream(jsonMessage.getBytes(StandardCharsets.UTF_8));
 
-		transportProvider = new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper), stream,
-				testOutPrintStream);
+		transportProvider = new StdioServerTransportProvider(JSON_MAPPER, stream, testOutPrintStream);
 
 		// Set up a session factory
 		transportProvider.setSessionFactory(sessionFactory);
diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidatorTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidatorTests.java
index 30158543d..76ca29684 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidatorTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidatorTests.java
@@ -16,6 +16,7 @@
 import java.util.Map;
 import java.util.stream.Stream;
 
+import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -27,7 +28,7 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
-import io.modelcontextprotocol.spec.JsonSchemaValidator.ValidationResponse;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
 
 /**
  * Tests for {@link DefaultJsonSchemaValidator}.
diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
index af20f96e2..86912b4bf 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
@@ -8,7 +8,7 @@
 import java.util.Map;
 
 import io.modelcontextprotocol.MockMcpClientTransport;
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java
index a5b2137fd..c9459234e 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java
@@ -4,6 +4,7 @@
 
 package io.modelcontextprotocol.spec;
 
+import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -17,7 +18,6 @@
 
 import org.junit.jupiter.api.Test;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
 
 import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
@@ -29,14 +29,12 @@
  */
 public class McpSchemaTests {
 
-	ObjectMapper mapper = new ObjectMapper();
-
 	// Content Types Tests
 
 	@Test
 	void testTextContent() throws Exception {
 		McpSchema.TextContent test = new McpSchema.TextContent("XXX");
-		String value = mapper.writeValueAsString(test);
+		String value = JSON_MAPPER.writeValueAsString(test);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -47,7 +45,7 @@ void testTextContent() throws Exception {
 
 	@Test
 	void testTextContentDeserialization() throws Exception {
-		McpSchema.TextContent textContent = mapper.readValue("""
+		McpSchema.TextContent textContent = JSON_MAPPER.readValue("""
 				{"type":"text","text":"XXX","_meta":{"metaKey":"metaValue"}}""", McpSchema.TextContent.class);
 
 		assertThat(textContent).isNotNull();
@@ -59,7 +57,7 @@ void testTextContentDeserialization() throws Exception {
 	@Test
 	void testContentDeserializationWrongType() throws Exception {
 
-		assertThatThrownBy(() -> mapper.readValue("""
+		assertThatThrownBy(() -> JSON_MAPPER.readValue("""
 				{"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class))
 			.isInstanceOf(InvalidTypeIdException.class)
 			.hasMessageContaining(
@@ -69,7 +67,7 @@ void testContentDeserializationWrongType() throws Exception {
 	@Test
 	void testImageContent() throws Exception {
 		McpSchema.ImageContent test = new McpSchema.ImageContent(null, null, "base64encodeddata", "image/png");
-		String value = mapper.writeValueAsString(test);
+		String value = JSON_MAPPER.writeValueAsString(test);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -80,7 +78,7 @@ void testImageContent() throws Exception {
 
 	@Test
 	void testImageContentDeserialization() throws Exception {
-		McpSchema.ImageContent imageContent = mapper.readValue("""
+		McpSchema.ImageContent imageContent = JSON_MAPPER.readValue("""
 				{"type":"image","data":"base64encodeddata","mimeType":"image/png","_meta":{"metaKey":"metaValue"}}""",
 				McpSchema.ImageContent.class);
 		assertThat(imageContent).isNotNull();
@@ -93,7 +91,7 @@ void testImageContentDeserialization() throws Exception {
 	@Test
 	void testAudioContent() throws Exception {
 		McpSchema.AudioContent audioContent = new McpSchema.AudioContent(null, "base64encodeddata", "audio/wav");
-		String value = mapper.writeValueAsString(audioContent);
+		String value = JSON_MAPPER.writeValueAsString(audioContent);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -104,7 +102,7 @@ void testAudioContent() throws Exception {
 
 	@Test
 	void testAudioContentDeserialization() throws Exception {
-		McpSchema.AudioContent audioContent = mapper.readValue("""
+		McpSchema.AudioContent audioContent = JSON_MAPPER.readValue("""
 				{"type":"audio","data":"base64encodeddata","mimeType":"audio/wav","_meta":{"metaKey":"metaValue"}}""",
 				McpSchema.AudioContent.class);
 		assertThat(audioContent).isNotNull();
@@ -140,7 +138,7 @@ void testCreateMessageRequestWithMeta() throws Exception {
 			.meta(meta)
 			.build();
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -158,7 +156,7 @@ void testEmbeddedResource() throws Exception {
 
 		McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents);
 
-		String value = mapper.writeValueAsString(test);
+		String value = JSON_MAPPER.writeValueAsString(test);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -169,7 +167,7 @@ void testEmbeddedResource() throws Exception {
 
 	@Test
 	void testEmbeddedResourceDeserialization() throws Exception {
-		McpSchema.EmbeddedResource embeddedResource = mapper.readValue(
+		McpSchema.EmbeddedResource embeddedResource = JSON_MAPPER.readValue(
 				"""
 						{"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"},"_meta":{"metaKey":"metaValue"}}""",
 				McpSchema.EmbeddedResource.class);
@@ -189,7 +187,7 @@ void testEmbeddedResourceWithBlobContents() throws Exception {
 
 		McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents);
 
-		String value = mapper.writeValueAsString(test);
+		String value = JSON_MAPPER.writeValueAsString(test);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -200,7 +198,7 @@ void testEmbeddedResourceWithBlobContents() throws Exception {
 
 	@Test
 	void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception {
-		McpSchema.EmbeddedResource embeddedResource = mapper.readValue(
+		McpSchema.EmbeddedResource embeddedResource = JSON_MAPPER.readValue(
 				"""
 						{"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob","_meta":{"metaKey":"metaValue"}}}""",
 				McpSchema.EmbeddedResource.class);
@@ -219,7 +217,7 @@ void testResourceLink() throws Exception {
 		McpSchema.ResourceLink resourceLink = new McpSchema.ResourceLink("main.rs", "Main file",
 				"file:///project/src/main.rs", "Primary application entry point", "text/x-rust", null, null,
 				Map.of("metaKey", "metaValue"));
-		String value = mapper.writeValueAsString(resourceLink);
+		String value = JSON_MAPPER.writeValueAsString(resourceLink);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -231,7 +229,7 @@ void testResourceLink() throws Exception {
 
 	@Test
 	void testResourceLinkDeserialization() throws Exception {
-		McpSchema.ResourceLink resourceLink = mapper.readValue(
+		McpSchema.ResourceLink resourceLink = JSON_MAPPER.readValue(
 				"""
 						{"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust","_meta":{"metaKey":"metaValue"}}""",
 				McpSchema.ResourceLink.class);
@@ -254,7 +252,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 = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -270,7 +268,7 @@ void testJSONRPCNotification() throws Exception {
 		McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
 				"notification_method", params);
 
-		String value = mapper.writeValueAsString(notification);
+		String value = JSON_MAPPER.writeValueAsString(notification);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -285,7 +283,7 @@ void testJSONRPCResponse() throws Exception {
 
 		McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, result, null);
 
-		String value = mapper.writeValueAsString(response);
+		String value = JSON_MAPPER.writeValueAsString(response);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -300,7 +298,7 @@ void testJSONRPCResponseWithError() throws Exception {
 
 		McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, null, error);
 
-		String value = mapper.writeValueAsString(response);
+		String value = JSON_MAPPER.writeValueAsString(response);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -323,7 +321,7 @@ void testInitializeRequest() throws Exception {
 		McpSchema.InitializeRequest request = new McpSchema.InitializeRequest(ProtocolVersions.MCP_2024_11_05,
 				capabilities, clientInfo, meta);
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -346,7 +344,7 @@ void testInitializeResult() throws Exception {
 		McpSchema.InitializeResult result = new McpSchema.InitializeResult(ProtocolVersions.MCP_2024_11_05,
 				capabilities, serverInfo, "Server initialized successfully");
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -365,7 +363,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 = JSON_MAPPER.writeValueAsString(resource);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -389,7 +387,7 @@ void testResourceBuilder() throws Exception {
 			.meta(Map.of("metaKey", "metaValue"))
 			.build();
 
-		String value = mapper.writeValueAsString(resource);
+		String value = JSON_MAPPER.writeValueAsString(resource);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -436,7 +434,7 @@ void testResourceTemplate() throws Exception {
 		McpSchema.ResourceTemplate template = new McpSchema.ResourceTemplate("resource://{param}/test", "Test Template",
 				"Test Template", "A test resource template", "text/plain", annotations, meta);
 
-		String value = mapper.writeValueAsString(template);
+		String value = JSON_MAPPER.writeValueAsString(template);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -458,7 +456,7 @@ void testListResourcesResult() throws Exception {
 		McpSchema.ListResourcesResult result = new McpSchema.ListResourcesResult(Arrays.asList(resource1, resource2),
 				"next-cursor", meta);
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -478,7 +476,7 @@ void testListResourceTemplatesResult() throws Exception {
 		McpSchema.ListResourceTemplatesResult result = new McpSchema.ListResourceTemplatesResult(
 				Arrays.asList(template1, template2), "next-cursor");
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -492,7 +490,7 @@ void testReadResourceRequest() throws Exception {
 		McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test",
 				Map.of("metaKey", "metaValue"));
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -507,7 +505,7 @@ void testReadResourceRequestWithMeta() throws Exception {
 
 		McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test", meta);
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -521,7 +519,7 @@ void testReadResourceRequestWithMeta() throws Exception {
 
 	@Test
 	void testReadResourceRequestDeserialization() throws Exception {
-		McpSchema.ReadResourceRequest request = mapper.readValue("""
+		McpSchema.ReadResourceRequest request = JSON_MAPPER.readValue("""
 				{"uri":"resource://test","_meta":{"progressToken":"test-token"}}""",
 				McpSchema.ReadResourceRequest.class);
 
@@ -541,7 +539,7 @@ void testReadResourceResult() throws Exception {
 		McpSchema.ReadResourceResult result = new McpSchema.ReadResourceResult(Arrays.asList(contents1, contents2),
 				Map.of("metaKey", "metaValue"));
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -562,7 +560,7 @@ void testPrompt() throws Exception {
 		McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "Test Prompt", "A test prompt",
 				Arrays.asList(arg1, arg2), Map.of("metaKey", "metaValue"));
 
-		String value = mapper.writeValueAsString(prompt);
+		String value = JSON_MAPPER.writeValueAsString(prompt);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -577,7 +575,7 @@ void testPromptMessage() throws Exception {
 
 		McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content);
 
-		String value = mapper.writeValueAsString(message);
+		String value = JSON_MAPPER.writeValueAsString(message);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -598,7 +596,7 @@ void testListPromptsResult() throws Exception {
 		McpSchema.ListPromptsResult result = new McpSchema.ListPromptsResult(Arrays.asList(prompt1, prompt2),
 				"next-cursor");
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -615,7 +613,7 @@ void testGetPromptRequest() throws Exception {
 
 		McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("test-prompt", arguments);
 
-		assertThat(mapper.readValue("""
+		assertThat(JSON_MAPPER.readValue("""
 				{"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}""", McpSchema.GetPromptRequest.class))
 			.isEqualTo(request);
 	}
@@ -631,7 +629,7 @@ void testGetPromptRequestWithMeta() throws Exception {
 
 		McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("test-prompt", arguments, meta);
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -656,7 +654,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 = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -696,16 +694,16 @@ void testJsonSchema() throws Exception {
 				""";
 
 		// Deserialize the original string to a JsonSchema object
-		McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
+		McpSchema.JsonSchema schema = JSON_MAPPER.readValue(schemaJson, McpSchema.JsonSchema.class);
 
 		// Serialize the object back to a string
-		String serialized = mapper.writeValueAsString(schema);
+		String serialized = JSON_MAPPER.writeValueAsString(schema);
 
 		// Deserialize again
-		McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class);
+		McpSchema.JsonSchema deserialized = JSON_MAPPER.readValue(serialized, McpSchema.JsonSchema.class);
 
 		// Serialize one more time and compare with the first serialization
-		String serializedAgain = mapper.writeValueAsString(deserialized);
+		String serializedAgain = JSON_MAPPER.writeValueAsString(deserialized);
 
 		// The two serialized strings should be the same
 		assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
@@ -739,16 +737,16 @@ void testJsonSchemaWithDefinitions() throws Exception {
 				""";
 
 		// Deserialize the original string to a JsonSchema object
-		McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
+		McpSchema.JsonSchema schema = JSON_MAPPER.readValue(schemaJson, McpSchema.JsonSchema.class);
 
 		// Serialize the object back to a string
-		String serialized = mapper.writeValueAsString(schema);
+		String serialized = JSON_MAPPER.writeValueAsString(schema);
 
 		// Deserialize again
-		McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class);
+		McpSchema.JsonSchema deserialized = JSON_MAPPER.readValue(serialized, McpSchema.JsonSchema.class);
 
 		// Serialize one more time and compare with the first serialization
-		String serializedAgain = mapper.writeValueAsString(deserialized);
+		String serializedAgain = JSON_MAPPER.writeValueAsString(deserialized);
 
 		// The two serialized strings should be the same
 		assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
@@ -771,9 +769,13 @@ void testTool() throws Exception {
 				}
 				""";
 
-		McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson);
+		McpSchema.Tool tool = McpSchema.Tool.builder()
+			.name("test-tool")
+			.description("A test tool")
+			.inputSchema(JSON_MAPPER, schemaJson)
+			.build();
 
-		String value = mapper.writeValueAsString(tool);
+		String value = JSON_MAPPER.writeValueAsString(tool);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -805,16 +807,20 @@ void testToolWithComplexSchema() throws Exception {
 				}
 				""";
 
-		McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", complexSchemaJson);
+		McpSchema.Tool tool = McpSchema.Tool.builder()
+			.name("addressTool")
+			.title("Handles addresses")
+			.inputSchema(JSON_MAPPER, complexSchemaJson)
+			.build();
 
 		// Serialize the tool to a string
-		String serialized = mapper.writeValueAsString(tool);
+		String serialized = JSON_MAPPER.writeValueAsString(tool);
 
 		// Deserialize back to a Tool object
-		McpSchema.Tool deserializedTool = mapper.readValue(serialized, McpSchema.Tool.class);
+		McpSchema.Tool deserializedTool = JSON_MAPPER.readValue(serialized, McpSchema.Tool.class);
 
 		// Serialize again and compare with first serialization
-		String serializedAgain = mapper.writeValueAsString(deserializedTool);
+		String serializedAgain = JSON_MAPPER.writeValueAsString(deserializedTool);
 
 		// The two serialized strings should be the same
 		assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
@@ -841,11 +847,16 @@ void testToolWithMeta() throws Exception {
 				}
 				""";
 
-		McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
+		McpSchema.JsonSchema schema = JSON_MAPPER.readValue(schemaJson, McpSchema.JsonSchema.class);
 		Map meta = Map.of("metaKey", "metaValue");
 
-		McpSchema.Tool tool = new McpSchema.Tool("addressTool", "addressTool", "Handles addresses", schema, null, null,
-				meta);
+		McpSchema.Tool tool = McpSchema.Tool.builder()
+			.name("addressTool")
+			.title("addressTool")
+			.description("Handles addresses")
+			.inputSchema(schema)
+			.meta(meta)
+			.build();
 
 		// Verify that meta value was preserved
 		assertThat(tool.meta()).isNotNull();
@@ -871,9 +882,14 @@ void testToolWithAnnotations() throws Exception {
 		McpSchema.ToolAnnotations annotations = new McpSchema.ToolAnnotations("A test tool", false, false, false, false,
 				false);
 
-		McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson, annotations);
+		McpSchema.Tool tool = McpSchema.Tool.builder()
+			.name("test-tool")
+			.description("A test tool")
+			.inputSchema(JSON_MAPPER, schemaJson)
+			.annotations(annotations)
+			.build();
 
-		String value = mapper.writeValueAsString(tool);
+		String value = JSON_MAPPER.writeValueAsString(tool);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -934,9 +950,14 @@ void testToolWithOutputSchema() throws Exception {
 				}
 				""";
 
-		McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", inputSchemaJson, outputSchemaJson, null);
+		McpSchema.Tool tool = McpSchema.Tool.builder()
+			.name("test-tool")
+			.description("A test tool")
+			.inputSchema(JSON_MAPPER, inputSchemaJson)
+			.outputSchema(JSON_MAPPER, outputSchemaJson)
+			.build();
 
-		String value = mapper.writeValueAsString(tool);
+		String value = JSON_MAPPER.writeValueAsString(tool);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -996,10 +1017,15 @@ void testToolWithOutputSchemaAndAnnotations() throws Exception {
 		McpSchema.ToolAnnotations annotations = new McpSchema.ToolAnnotations("A test tool with output", true, false,
 				true, false, true);
 
-		McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", inputSchemaJson, outputSchemaJson,
-				annotations);
+		McpSchema.Tool tool = McpSchema.Tool.builder()
+			.name("test-tool")
+			.description("A test tool")
+			.inputSchema(JSON_MAPPER, inputSchemaJson)
+			.outputSchema(JSON_MAPPER, outputSchemaJson)
+			.annotations(annotations)
+			.build();
 
-		String value = mapper.writeValueAsString(tool);
+		String value = JSON_MAPPER.writeValueAsString(tool);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1063,7 +1089,7 @@ void testToolDeserialization() throws Exception {
 				}
 				""";
 
-		McpSchema.Tool tool = mapper.readValue(toolJson, McpSchema.Tool.class);
+		McpSchema.Tool tool = JSON_MAPPER.readValue(toolJson, McpSchema.Tool.class);
 
 		assertThat(tool).isNotNull();
 		assertThat(tool.name()).isEqualTo("test-tool");
@@ -1097,7 +1123,7 @@ void testToolDeserializationWithoutOutputSchema() throws Exception {
 				}
 				""";
 
-		McpSchema.Tool tool = mapper.readValue(toolJson, McpSchema.Tool.class);
+		McpSchema.Tool tool = JSON_MAPPER.readValue(toolJson, McpSchema.Tool.class);
 
 		assertThat(tool).isNotNull();
 		assertThat(tool.name()).isEqualTo("test-tool");
@@ -1115,7 +1141,7 @@ void testCallToolRequest() throws Exception {
 
 		McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", arguments);
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1127,14 +1153,14 @@ void testCallToolRequest() throws Exception {
 	@Test
 	void testCallToolRequestJsonArguments() throws Exception {
 
-		McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", """
+		McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(JSON_MAPPER, "test-tool", """
 				{
 					"name": "test",
 					"value": 42
 				}
 				""");
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1151,7 +1177,7 @@ void testCallToolRequestWithMeta() throws Exception {
 			.arguments(Map.of("name", "test", "value", 42))
 			.progressToken("tool-progress-123")
 			.build();
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1170,14 +1196,18 @@ void testCallToolRequestBuilderWithJsonArguments() throws Exception {
 		Map meta = new HashMap<>();
 		meta.put("progressToken", "json-builder-789");
 
-		McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder().name("test-tool").arguments("""
-				{
-					"name": "test",
-					"value": 42
-				}
-				""").meta(meta).build();
+		McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
+			.name("test-tool")
+			.arguments(JSON_MAPPER, """
+					{
+						"name": "test",
+						"value": 42
+					}
+					""")
+			.meta(meta)
+			.build();
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1208,7 +1238,7 @@ void testCallToolResult() throws Exception {
 
 		McpSchema.CallToolResult result = new McpSchema.CallToolResult(Collections.singletonList(content), false);
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1224,7 +1254,7 @@ void testCallToolResultBuilder() throws Exception {
 			.isError(false)
 			.build();
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1244,7 +1274,7 @@ void testCallToolResultBuilderWithMultipleContents() throws Exception {
 			.isError(false)
 			.build();
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1262,7 +1292,7 @@ void testCallToolResultBuilderWithContentList() throws Exception {
 
 		McpSchema.CallToolResult result = McpSchema.CallToolResult.builder().content(contents).isError(true).build();
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1279,7 +1309,7 @@ void testCallToolResultBuilderWithErrorResult() throws Exception {
 			.isError(true)
 			.build();
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1297,8 +1327,8 @@ void testCallToolResultStringConstructor() throws Exception {
 			.isError(false)
 			.build();
 
-		String value1 = mapper.writeValueAsString(result1);
-		String value2 = mapper.writeValueAsString(result2);
+		String value1 = JSON_MAPPER.writeValueAsString(result1);
+		String value2 = JSON_MAPPER.writeValueAsString(result2);
 
 		// Both should produce the same JSON
 		assertThat(value1).isEqualTo(value2);
@@ -1336,7 +1366,7 @@ void testCreateMessageRequest() throws Exception {
 			.metadata(metadata)
 			.build();
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1357,7 +1387,7 @@ void testCreateMessageResult() throws Exception {
 			.stopReason(McpSchema.CreateMessageResult.StopReason.END_TURN)
 			.build();
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1372,7 +1402,7 @@ void testCreateMessageResultUnknownStopReason() throws Exception {
 		String input = """
 				{"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"arbitrary value"}""";
 
-		McpSchema.CreateMessageResult value = mapper.readValue(input, McpSchema.CreateMessageResult.class);
+		McpSchema.CreateMessageResult value = JSON_MAPPER.readValue(input, McpSchema.CreateMessageResult.class);
 
 		McpSchema.TextContent expectedContent = new McpSchema.TextContent("Assistant response");
 		McpSchema.CreateMessageResult expected = McpSchema.CreateMessageResult.builder()
@@ -1393,7 +1423,7 @@ void testCreateElicitationRequest() throws Exception {
 					Map.of("foo", Map.of("type", "string"))))
 			.build();
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1409,7 +1439,7 @@ void testCreateElicitationResult() throws Exception {
 			.message(McpSchema.ElicitResult.Action.ACCEPT)
 			.build();
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1432,7 +1462,7 @@ void testElicitRequestWithMeta() throws Exception {
 			.meta(meta)
 			.build();
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1449,7 +1479,7 @@ void testElicitRequestWithMeta() throws Exception {
 	void testPaginatedRequestNoArgs() throws Exception {
 		McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest();
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1465,7 +1495,7 @@ void testPaginatedRequestNoArgs() throws Exception {
 	void testPaginatedRequestWithCursor() throws Exception {
 		McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest("cursor123");
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1484,7 +1514,7 @@ void testPaginatedRequestWithMeta() throws Exception {
 
 		McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest("cursor123", meta);
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1498,7 +1528,7 @@ void testPaginatedRequestWithMeta() throws Exception {
 
 	@Test
 	void testPaginatedRequestDeserialization() throws Exception {
-		McpSchema.PaginatedRequest request = mapper.readValue("""
+		McpSchema.PaginatedRequest request = JSON_MAPPER.readValue("""
 				{"cursor":"test-cursor","_meta":{"progressToken":"test-token"}}""", McpSchema.PaginatedRequest.class);
 
 		assertThat(request.cursor()).isEqualTo("test-cursor");
@@ -1516,7 +1546,7 @@ void testCompleteRequest() throws Exception {
 
 		McpSchema.CompleteRequest request = new McpSchema.CompleteRequest(promptRef, argument);
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1540,7 +1570,7 @@ void testCompleteRequestWithMeta() throws Exception {
 
 		McpSchema.CompleteRequest request = new McpSchema.CompleteRequest(resourceRef, argument, meta, null);
 
-		String value = mapper.writeValueAsString(request);
+		String value = JSON_MAPPER.writeValueAsString(request);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1559,7 +1589,7 @@ void testCompleteRequestWithMeta() throws Exception {
 	void testRoot() throws Exception {
 		McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root", Map.of("metaKey", "metaValue"));
 
-		String value = mapper.writeValueAsString(root);
+		String value = JSON_MAPPER.writeValueAsString(root);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1575,7 +1605,7 @@ void testListRootsResult() throws Exception {
 
 		McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(Arrays.asList(root1, root2), "next-cursor");
 
-		String value = mapper.writeValueAsString(result);
+		String value = JSON_MAPPER.writeValueAsString(result);
 
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
@@ -1593,7 +1623,7 @@ void testProgressNotificationWithMessage() throws Exception {
 		McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-123", 0.5, 1.0,
 				"Processing file 1 of 2", Map.of("key", "value"));
 
-		String value = mapper.writeValueAsString(notification);
+		String value = JSON_MAPPER.writeValueAsString(notification);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
@@ -1604,7 +1634,7 @@ void testProgressNotificationWithMessage() throws Exception {
 
 	@Test
 	void testProgressNotificationDeserialization() throws Exception {
-		McpSchema.ProgressNotification notification = mapper.readValue(
+		McpSchema.ProgressNotification notification = JSON_MAPPER.readValue(
 				"""
 						{"progressToken":"token-456","progress":0.75,"total":1.0,"message":"Almost done","_meta":{"key":"value"}}""",
 				McpSchema.ProgressNotification.class);
@@ -1621,7 +1651,7 @@ void testProgressNotificationWithoutMessage() throws Exception {
 		McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-789", 0.25,
 				null, null);
 
-		String value = mapper.writeValueAsString(notification);
+		String value = JSON_MAPPER.writeValueAsString(notification);
 		assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
 			.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
 			.isObject()
diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java
index d799b3d22..ef7cd2737 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapper.java
@@ -3,8 +3,8 @@
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.ToNumberPolicy;
-import io.modelcontextprotocol.spec.json.McpJsonMapper;
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java
index 7e88097f8..4f1dffe1d 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java
@@ -1,8 +1,7 @@
 package io.modelcontextprotocol.spec.json.gson;
 
 import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.json.TypeRef;
-import io.modelcontextprotocol.spec.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
@@ -63,9 +62,9 @@ void convertValueMapToRecordAndParameterized() {
 		});
 		assertEquals("Bob", toMap.get("name"));
 		assertEquals(42.0, ((Number) toMap.get("age")).doubleValue(), 0.0); // Gson may
-																			// emit double
-																			// for
-																			// primitives
+		// emit double
+		// for
+		// primitives
 	}
 
 	@Test
@@ -102,41 +101,32 @@ void deserializeJsonRpcMessageRequestUsingCustomMapper() throws IOException {
 	void integrateWithMcpSchemaStaticMapperForStringParsing() {
 		var gsonMapper = new GsonMcpJsonMapper();
 
-		// Save and restore static mapper to avoid affecting other tests
-		var originalMapper = new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
-		try {
-			McpSchema.setJsonMapper(gsonMapper);
-
-			// Tool builder parsing of input/output schema strings
-			var tool = McpSchema.Tool.builder().name("echo").description("Echo tool").inputSchema("""
-					{
-					  "type": "object",
-					  "properties": { "x": { "type": "integer" } },
-					  "required": ["x"]
-					}
-					""").outputSchema("""
-					{
-					  "type": "object",
-					  "properties": { "y": { "type": "string" } }
-					}
-					""").build();
-
-			assertNotNull(tool.inputSchema());
-			assertNotNull(tool.outputSchema());
-			assertTrue(tool.outputSchema().containsKey("properties"));
-
-			// CallToolRequest builder parsing of JSON arguments string
-			var call = McpSchema.CallToolRequest.builder().name("echo").arguments("{\"x\": 123}").build();
-
-			assertEquals("echo", call.name());
-			assertNotNull(call.arguments());
-			assertTrue(call.arguments().get("x") instanceof Number);
-			assertEquals(123.0, ((Number) call.arguments().get("x")).doubleValue(), 0.0);
-		}
-		finally {
-			// restore to a Jackson-backed default to avoid side effects
-			McpSchema.setJsonMapper(originalMapper);
-		}
+		// Tool builder parsing of input/output schema strings
+		var tool = McpSchema.Tool.builder().name("echo").description("Echo tool").inputSchema(gsonMapper, """
+				{
+				  "type": "object",
+				  "properties": { "x": { "type": "integer" } },
+				  "required": ["x"]
+				}
+				""").outputSchema(gsonMapper, """
+				{
+				  "type": "object",
+				  "properties": { "y": { "type": "string" } }
+				}
+				""").build();
+
+		assertNotNull(tool.inputSchema());
+		assertNotNull(tool.outputSchema());
+		assertTrue(tool.outputSchema().containsKey("properties"));
+
+		// CallToolRequest builder parsing of JSON arguments string
+		var call = McpSchema.CallToolRequest.builder().name("echo").arguments(gsonMapper, "{\"x\": 123}").build();
+
+		assertEquals("echo", call.name());
+		assertNotNull(call.arguments());
+		assertTrue(call.arguments().get("x") instanceof Number);
+		assertEquals(123.0, ((Number) call.arguments().get("x")).doubleValue(), 0.0);
+
 	}
 
 }
diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java b/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java
index 26a44f199..d5ef8a91c 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/util/KeepAliveSchedulerTests.java
@@ -16,7 +16,7 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import io.modelcontextprotocol.spec.json.TypeRef;
+import io.modelcontextprotocol.json.TypeRef;
 
 import io.modelcontextprotocol.spec.McpSchema;
 import io.modelcontextprotocol.spec.McpSession;
diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java b/mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java
new file mode 100644
index 000000000..27c3d3b07
--- /dev/null
+++ b/mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java
@@ -0,0 +1,12 @@
+package io.modelcontextprotocol.util;
+
+import io.modelcontextprotocol.json.McpJsonMapper;
+
+public final class McpJsonMapperUtils {
+
+	private McpJsonMapperUtils() {
+	}
+
+	public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.createDefault();
+
+}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/ToolsUtils.java b/mcp/src/test/java/io/modelcontextprotocol/util/ToolsUtils.java
new file mode 100644
index 000000000..ce8755223
--- /dev/null
+++ b/mcp/src/test/java/io/modelcontextprotocol/util/ToolsUtils.java
@@ -0,0 +1,15 @@
+package io.modelcontextprotocol.util;
+
+import io.modelcontextprotocol.spec.McpSchema;
+
+import java.util.Collections;
+
+public final class ToolsUtils {
+
+	private ToolsUtils() {
+	}
+
+	public static final McpSchema.JsonSchema EMPTY_JSON_SCHEMA = new McpSchema.JsonSchema("object",
+			Collections.emptyMap(), null, null, null, null);
+
+}
diff --git a/pom.xml b/pom.xml
index 9990a4663..7d9f8a2c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,8 @@
 	
 		mcp-bom
 		mcp
+        mcp-json-jackson
+        mcp-json
 		mcp-spring/mcp-spring-webflux
 		mcp-spring/mcp-spring-webmvc
 		mcp-test

From f429a45505455fe7cefaa44c3ab94331a410f50d Mon Sep 17 00:00:00 2001
From: Graeme Rocher 
Date: Fri, 12 Sep 2025 10:12:18 +0200
Subject: [PATCH 3/7] cleanup and cache default mapper/validator

---
 .../json/McpJsonInternal.java                 | 80 +++++++++++++++++++
 .../json/McpJsonMapper.java                   | 52 +++---------
 .../json/schema/JsonSchemaInternal.java       | 79 ++++++++++++++++++
 .../json/schema/JsonSchemaValidator.java      | 47 +++--------
 .../schema/JsonSchemaValidatorSupplier.java   |  8 ++
 .../WebClientStreamableHttpTransport.java     |  2 +-
 .../transport/WebFluxSseClientTransport.java  |  2 +-
 .../WebFluxSseServerTransportProvider.java    |  5 +-
 .../WebFluxStatelessServerTransport.java      |  2 +-
 ...FluxStreamableServerTransportProvider.java |  2 +-
 .../WebMvcSseServerTransportProvider.java     |  2 +-
 .../WebMvcStatelessServerTransport.java       |  2 +-
 ...bMvcStreamableServerTransportProvider.java |  2 +-
 .../MockMcpTransport.java                     |  2 +-
 .../utils/McpJsonMapperUtils.java             |  2 +-
 .../HttpClientSseClientTransport.java         |  2 +-
 .../HttpClientStreamableHttpTransport.java    |  6 +-
 .../server/McpServer.java                     | 33 ++++----
 ...HttpServletSseServerTransportProvider.java |  4 +-
 .../HttpServletStatelessServerTransport.java  |  4 +-
 ...vletStreamableServerTransportProvider.java |  2 +-
 .../MockMcpClientTransport.java               |  2 +-
 .../MockMcpServerTransport.java               |  2 +-
 .../util/McpJsonMapperUtils.java              |  2 +-
 24 files changed, 226 insertions(+), 120 deletions(-)
 create mode 100644 mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java
 create mode 100644 mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java

diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java
new file mode 100644
index 000000000..fb666a503
--- /dev/null
+++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java
@@ -0,0 +1,80 @@
+package io.modelcontextprotocol.json;
+
+import java.util.ServiceLoader;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+/**
+ * Utility class for creating a default {@link McpJsonMapper} instance. This class
+ * provides a single method to create a default mapper using the {@link ServiceLoader}
+ * mechanism.
+ */
+final class McpJsonInternal {
+
+	private static McpJsonMapper defaultJsonMapper = null;
+
+	/**
+	 * Returns the cached default {@link McpJsonMapper} instance. If the default mapper
+	 * has not been created yet, it will be initialized using the
+	 * {@link #createDefaultMapper()} method.
+	 * @return the default {@link McpJsonMapper} instance
+	 * @throws IllegalStateException if no default {@link McpJsonMapper} implementation is
+	 * found
+	 */
+	static McpJsonMapper getDefaultMapper() {
+		if (defaultJsonMapper == null) {
+			defaultJsonMapper = McpJsonInternal.createDefaultMapper();
+		}
+		return defaultJsonMapper;
+	}
+
+	/**
+	 * Creates a default {@link McpJsonMapper} instance using the {@link ServiceLoader}
+	 * mechanism. The default mapper is resolved by loading the first available
+	 * {@link McpJsonMapperSupplier} implementation on the classpath.
+	 * @return the default {@link McpJsonMapper} instance
+	 * @throws IllegalStateException if no default {@link McpJsonMapper} implementation is
+	 * found
+	 */
+	static McpJsonMapper createDefaultMapper() {
+		AtomicReference ex = new AtomicReference<>();
+		return ServiceLoader.load(McpJsonMapperSupplier.class).stream().flatMap(p -> {
+			try {
+				McpJsonMapperSupplier supplier = p.get();
+				return Stream.ofNullable(supplier);
+			}
+			catch (Exception e) {
+				addException(ex, e);
+				return Stream.empty();
+			}
+		}).flatMap(jsonMapperSupplier -> {
+			try {
+				return Stream.ofNullable(jsonMapperSupplier.get());
+			}
+			catch (Exception e) {
+				addException(ex, e);
+				return Stream.empty();
+			}
+		}).findFirst().orElseThrow(() -> {
+			if (ex.get() != null) {
+				return ex.get();
+			}
+			else {
+				return new IllegalStateException("No default McpJsonMapper implementation found");
+			}
+		});
+	}
+
+	private static void addException(AtomicReference ref, Exception toAdd) {
+		ref.updateAndGet(existing -> {
+			if (existing == null) {
+				return new IllegalStateException("Failed to initialize default McpJsonMapper", toAdd);
+			}
+			else {
+				existing.addSuppressed(toAdd);
+				return existing;
+			}
+		});
+	}
+
+}
diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java
index 899faa80e..3fb030d7b 100644
--- a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java
+++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java
@@ -1,9 +1,6 @@
 package io.modelcontextprotocol.json;
 
 import java.io.IOException;
-import java.util.ServiceLoader;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.stream.Stream;
 
 /**
  * Abstraction for JSON serialization/deserialization to decouple the SDK from any
@@ -87,50 +84,23 @@ public interface McpJsonMapper {
 	byte[] writeValueAsBytes(Object value) throws IOException;
 
 	/**
-	 * Resolves the default {@link McpJsonMapper}.
+	 * Returns the default {@link McpJsonMapper}.
 	 * @return The default {@link McpJsonMapper}
 	 * @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on
 	 * the classpath.
 	 */
-	static McpJsonMapper createDefault() {
-		AtomicReference ex = new AtomicReference<>();
-		return ServiceLoader.load(McpJsonMapperSupplier.class).stream().flatMap(p -> {
-			try {
-				McpJsonMapperSupplier supplier = p.get();
-				return Stream.ofNullable(supplier);
-			}
-			catch (Exception e) {
-				addException(ex, e);
-				return Stream.empty();
-			}
-		}).flatMap(jsonMapperSupplier -> {
-			try {
-				return Stream.of(jsonMapperSupplier.get());
-			}
-			catch (Exception e) {
-				addException(ex, e);
-				return Stream.empty();
-			}
-		}).findFirst().orElseThrow(() -> {
-			if (ex.get() != null) {
-				return ex.get();
-			}
-			else {
-				return new IllegalStateException("No default McpJsonMapper implementation found");
-			}
-		});
+	static McpJsonMapper getDefault() {
+		return McpJsonInternal.getDefaultMapper();
 	}
 
-	private static void addException(AtomicReference ref, Exception toAdd) {
-		ref.updateAndGet(existing -> {
-			if (existing == null) {
-				return new IllegalStateException("Failed to initialize default McpJsonMapper", toAdd);
-			}
-			else {
-				existing.addSuppressed(toAdd);
-				return existing;
-			}
-		});
+	/**
+	 * Creates a new default {@link McpJsonMapper}.
+	 * @return The default {@link McpJsonMapper}
+	 * @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on
+	 * the classpath.
+	 */
+	static McpJsonMapper createDefault() {
+		return McpJsonInternal.createDefaultMapper();
 	}
 
 }
diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java
new file mode 100644
index 000000000..9f78c2ed3
--- /dev/null
+++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java
@@ -0,0 +1,79 @@
+package io.modelcontextprotocol.json.schema;
+
+import java.util.ServiceLoader;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+/**
+ * Internal utility class for creating a default {@link JsonSchemaValidator} instance.
+ * This class uses the {@link ServiceLoader} to discover and instantiate a
+ * {@link JsonSchemaValidatorSupplier} implementation.
+ */
+final class JsonSchemaInternal {
+
+	private static JsonSchemaValidator defaultValidator = null;
+
+	/**
+	 * Returns the default {@link JsonSchemaValidator} instance. If the default validator
+	 * has not been initialized, it will be created using the {@link ServiceLoader} to
+	 * discover and instantiate a {@link JsonSchemaValidatorSupplier} implementation.
+	 * @return The default {@link JsonSchemaValidator} instance.
+	 * @throws IllegalStateException If no {@link JsonSchemaValidatorSupplier}
+	 * implementation exists on the classpath or if an error occurs during instantiation.
+	 */
+	static JsonSchemaValidator getDefaultValidator() {
+		if (defaultValidator == null) {
+			defaultValidator = JsonSchemaInternal.getDefaultValidator();
+		}
+		return defaultValidator;
+	}
+
+	/**
+	 * Creates a default {@link JsonSchemaValidator} instance by loading a
+	 * {@link JsonSchemaValidatorSupplier} implementation using the {@link ServiceLoader}.
+	 * @return A default {@link JsonSchemaValidator} instance.
+	 * @throws IllegalStateException If no {@link JsonSchemaValidatorSupplier}
+	 * implementation is found or if an error occurs during instantiation.
+	 */
+	static JsonSchemaValidator createDefaultValidator() {
+		AtomicReference ex = new AtomicReference<>();
+		return ServiceLoader.load(JsonSchemaValidatorSupplier.class).stream().flatMap(p -> {
+			try {
+				JsonSchemaValidatorSupplier supplier = p.get();
+				return Stream.ofNullable(supplier);
+			}
+			catch (Exception e) {
+				addException(ex, e);
+				return Stream.empty();
+			}
+		}).flatMap(jsonMapperSupplier -> {
+			try {
+				return Stream.of(jsonMapperSupplier.get());
+			}
+			catch (Exception e) {
+				addException(ex, e);
+				return Stream.empty();
+			}
+		}).findFirst().orElseThrow(() -> {
+			if (ex.get() != null) {
+				return ex.get();
+			}
+			else {
+				return new IllegalStateException("No default JsonSchemaValidatorSupplier implementation found");
+			}
+		});
+	}
+
+	private static void addException(AtomicReference ref, Exception toAdd) {
+		ref.updateAndGet(existing -> {
+			if (existing == null) {
+				return new IllegalStateException("Failed to initialize default JsonSchemaValidatorSupplier", toAdd);
+			}
+			else {
+				existing.addSuppressed(toAdd);
+				return existing;
+			}
+		});
+	}
+
+}
diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java
index 6517d4728..2091b94c7 100644
--- a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java
+++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java
@@ -46,50 +46,23 @@ public static ValidationResponse asInvalid(String message) {
 	ValidationResponse validate(Map schema, Map structuredContent);
 
 	/**
-	 * Resolves the default {@link JsonSchemaValidator}.
+	 * Creates the default {@link JsonSchemaValidator}.
 	 * @return The default {@link JsonSchemaValidator}
 	 * @throws IllegalStateException If no {@link JsonSchemaValidator} implementation
 	 * exists on the classpath.
 	 */
 	static JsonSchemaValidator createDefault() {
-		AtomicReference ex = new AtomicReference<>();
-		return ServiceLoader.load(JsonSchemaValidatorSupplier.class).stream().flatMap(p -> {
-			try {
-				JsonSchemaValidatorSupplier supplier = p.get();
-				return Stream.ofNullable(supplier);
-			}
-			catch (Exception e) {
-				addException(ex, e);
-				return Stream.empty();
-			}
-		}).flatMap(jsonMapperSupplier -> {
-			try {
-				return Stream.of(jsonMapperSupplier.get());
-			}
-			catch (Exception e) {
-				addException(ex, e);
-				return Stream.empty();
-			}
-		}).findFirst().orElseThrow(() -> {
-			if (ex.get() != null) {
-				return ex.get();
-			}
-			else {
-				return new IllegalStateException("No default JsonSchemaValidatorSupplier implementation found");
-			}
-		});
+		return JsonSchemaInternal.createDefaultValidator();
 	}
 
-	private static void addException(AtomicReference ref, Exception toAdd) {
-		ref.updateAndGet(existing -> {
-			if (existing == null) {
-				return new IllegalStateException("Failed to initialize default JsonSchemaValidatorSupplier", toAdd);
-			}
-			else {
-				existing.addSuppressed(toAdd);
-				return existing;
-			}
-		});
+	/**
+	 * Returns the default {@link JsonSchemaValidator}.
+	 * @return The default {@link JsonSchemaValidator}
+	 * @throws IllegalStateException If no {@link JsonSchemaValidator} implementation
+	 * exists on the classpath.
+	 */
+	static JsonSchemaValidator getDefault() {
+		return JsonSchemaInternal.getDefaultValidator();
 	}
 
 }
diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java
index 249b6ef4c..f4bb4e187 100644
--- a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java
+++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java
@@ -2,6 +2,14 @@
 
 import java.util.function.Supplier;
 
+/**
+ * A supplier interface that provides a {@link JsonSchemaValidator} instance.
+ * Implementations of this interface are expected to return a new or cached instance of
+ * {@link JsonSchemaValidator} when {@link #get()} is invoked.
+ *
+ * @see JsonSchemaValidator
+ * @see Supplier
+ */
 public interface JsonSchemaValidatorSupplier extends Supplier {
 
 }
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java
index 2b4d872c1..154eb4703 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java
@@ -554,7 +554,7 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) {
 		 * @return a new instance of {@link WebClientStreamableHttpTransport}
 		 */
 		public WebClientStreamableHttpTransport build() {
-			return new WebClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper,
+			return new WebClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
 					webClientBuilder, endpoint, resumableStreams, openConnectionOnStartup);
 		}
 
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 714d830dc..91b89d6d2 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
@@ -404,7 +404,7 @@ public Builder jsonMapper(McpJsonMapper jsonMapper) {
 		 */
 		public WebFluxSseClientTransport build() {
 			return new WebFluxSseClientTransport(webClientBuilder,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, sseEndpoint);
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, 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 374bf85e9..95355c0f2 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
@@ -506,9 +506,8 @@ public Builder contextExtractor(McpTransportContextExtractor cont
 		 */
 		public WebFluxSseServerTransportProvider build() {
 			Assert.notNull(messageEndpoint, "Message endpoint must be set");
-			return new WebFluxSseServerTransportProvider(
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, baseUrl, messageEndpoint,
-					sseEndpoint, keepAliveInterval, contextExtractor);
+			return new WebFluxSseServerTransportProvider(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, contextExtractor);
 		}
 
 	}
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java
index 0a1c3d688..400be341e 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java
@@ -213,7 +213,7 @@ public Builder contextExtractor(McpTransportContextExtractor cont
 		 */
 		public WebFluxStatelessServerTransport build() {
 			Assert.notNull(mcpEndpoint, "Message endpoint must be set");
-			return new WebFluxStatelessServerTransport(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper,
+			return new WebFluxStatelessServerTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
 					mcpEndpoint, contextExtractor);
 		}
 
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java
index a327d756d..b6cc20864 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java
@@ -485,7 +485,7 @@ public Builder keepAliveInterval(Duration keepAliveInterval) {
 		public WebFluxStreamableServerTransportProvider build() {
 			Assert.notNull(mcpEndpoint, "Message endpoint must be set");
 			return new WebFluxStreamableServerTransportProvider(
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, contextExtractor,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint, contextExtractor,
 					disallowDelete, keepAliveInterval);
 		}
 
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 4b90824d0..0b71ddc1f 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
@@ -563,7 +563,7 @@ public WebMvcSseServerTransportProvider build() {
 			if (messageEndpoint == null) {
 				throw new IllegalStateException("MessageEndpoint must be set");
 			}
-			return new WebMvcSseServerTransportProvider(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper,
+			return new WebMvcSseServerTransportProvider(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
 					baseUrl, messageEndpoint, sseEndpoint, keepAliveInterval, contextExtractor);
 		}
 
diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java
index 04b5d717c..4223084ff 100644
--- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java
+++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStatelessServerTransport.java
@@ -231,7 +231,7 @@ public Builder contextExtractor(McpTransportContextExtractor cont
 		 */
 		public WebMvcStatelessServerTransport build() {
 			Assert.notNull(mcpEndpoint, "Message endpoint must be set");
-			return new WebMvcStatelessServerTransport(jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper,
+			return new WebMvcStatelessServerTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
 					mcpEndpoint, contextExtractor);
 		}
 
diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java
index 6bc6ce545..9bb9bfa86 100644
--- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java
@@ -680,7 +680,7 @@ public Builder keepAliveInterval(Duration keepAliveInterval) {
 		public WebMvcStreamableServerTransportProvider build() {
 			Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set");
 			return new WebMvcStreamableServerTransportProvider(
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, disallowDelete,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint, disallowDelete,
 					contextExtractor, keepAliveInterval);
 		}
 
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java
index 4a1a8e056..cd8458311 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java
@@ -94,7 +94,7 @@ public Mono closeGracefully() {
 
 	@Override
 	public  T unmarshalFrom(Object data, TypeRef typeRef) {
-		return McpJsonMapper.createDefault().convertValue(data, typeRef);
+		return McpJsonMapper.getDefault().convertValue(data, typeRef);
 	}
 
 }
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java b/mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java
index 67347573c..e9ec8900c 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/utils/McpJsonMapperUtils.java
@@ -7,6 +7,6 @@ public final class McpJsonMapperUtils {
 	private McpJsonMapperUtils() {
 	}
 
-	public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.createDefault();
+	public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.getDefault();
 
 }
\ No newline at end of file
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
index 22171ddbe..661a41170 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java
@@ -328,7 +328,7 @@ public Builder connectTimeout(Duration connectTimeout) {
 		public HttpClientSseClientTransport build() {
 			HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
 			return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, httpRequestCustomizer);
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, httpRequestCustomizer);
 		}
 
 	}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
index ba953de2d..c73515938 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java
@@ -764,9 +764,9 @@ public Builder connectTimeout(Duration connectTimeout) {
 		 */
 		public HttpClientStreamableHttpTransport build() {
 			HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
-			return new HttpClientStreamableHttpTransport(
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, httpClient, requestBuilder,
-					baseUri, endpoint, resumableStreams, openConnectionOnStartup, httpRequestCustomizer);
+			return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
+					httpRequestCustomizer);
 		}
 
 	}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
index bb98ec745..ec86b5927 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
@@ -229,11 +229,10 @@ public McpAsyncServer build() {
 					this.instructions);
 
 			var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
-					: JsonSchemaValidator.createDefault();
+					: JsonSchemaValidator.getDefault();
 
-			return new McpAsyncServer(transportProvider,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, features, requestTimeout,
-					uriTemplateManagerFactory, jsonSchemaValidator);
+			return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
 		}
 
 	}
@@ -257,10 +256,9 @@ public McpAsyncServer build() {
 					this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
 					this.instructions);
 			var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
-					: JsonSchemaValidator.createDefault();
-			return new McpAsyncServer(transportProvider,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, features, requestTimeout,
-					uriTemplateManagerFactory, jsonSchemaValidator);
+					: JsonSchemaValidator.getDefault();
+			return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
 		}
 
 	}
@@ -817,9 +815,9 @@ public McpSyncServer build() {
 					this.immediateExecution);
 
 			var asyncServer = new McpAsyncServer(transportProvider,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, asyncFeatures, requestTimeout,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, asyncFeatures, requestTimeout,
 					uriTemplateManagerFactory,
-					jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.createDefault());
+					jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault());
 			return new McpSyncServer(asyncServer, this.immediateExecution);
 		}
 
@@ -847,9 +845,9 @@ public McpSyncServer build() {
 			McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures,
 					this.immediateExecution);
 			var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
-					: JsonSchemaValidator.createDefault();
+					: JsonSchemaValidator.getDefault();
 			var asyncServer = new McpAsyncServer(transportProvider,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, asyncFeatures, this.requestTimeout,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, asyncFeatures, this.requestTimeout,
 					this.uriTemplateManagerFactory, jsonSchemaValidator);
 			return new McpSyncServer(asyncServer, this.immediateExecution);
 		}
@@ -1847,10 +1845,9 @@ public StatelessAsyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonS
 		public McpStatelessAsyncServer build() {
 			var features = new McpStatelessServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
 					this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
-			return new McpStatelessAsyncServer(transport,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, features, requestTimeout,
-					uriTemplateManagerFactory,
-					jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.createDefault());
+			return new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					features, requestTimeout, uriTemplateManagerFactory,
+					jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault());
 		}
 
 	}
@@ -2326,9 +2323,9 @@ public McpStatelessSyncServer build() {
 					this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
 			var asyncFeatures = McpStatelessServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution);
 			var asyncServer = new McpStatelessAsyncServer(transport,
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, asyncFeatures, requestTimeout,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, asyncFeatures, requestTimeout,
 					uriTemplateManagerFactory,
-					this.jsonSchemaValidator != null ? this.jsonSchemaValidator : JsonSchemaValidator.createDefault());
+					this.jsonSchemaValidator != null ? this.jsonSchemaValidator : JsonSchemaValidator.getDefault());
 			return new McpStatelessSyncServer(asyncServer, this.immediateExecution);
 		}
 
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
index 4ebfb9e2e..4739e231a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java
@@ -593,8 +593,8 @@ public HttpServletSseServerTransportProvider build() {
 				throw new IllegalStateException("MessageEndpoint must be set");
 			}
 			return new HttpServletSseServerTransportProvider(
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, baseUrl, messageEndpoint,
-					sseEndpoint, keepAliveInterval, contextExtractor);
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, baseUrl, messageEndpoint, sseEndpoint,
+					keepAliveInterval, contextExtractor);
 		}
 
 	}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
index bccd4c848..40767f416 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
@@ -296,8 +296,8 @@ public Builder contextExtractor(McpTransportContextExtractor
 		 */
 		public HttpServletStatelessServerTransport build() {
 			Assert.notNull(mcpEndpoint, "Message endpoint must be set");
-			return new HttpServletStatelessServerTransport(
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, contextExtractor);
+			return new HttpServletStatelessServerTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					mcpEndpoint, contextExtractor);
 		}
 
 	}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
index fb87d3d7b..137015876 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java
@@ -841,7 +841,7 @@ public Builder keepAliveInterval(Duration keepAliveInterval) {
 		public HttpServletStreamableServerTransportProvider build() {
 			Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set");
 			return new HttpServletStreamableServerTransportProvider(
-					jsonMapper == null ? McpJsonMapper.createDefault() : jsonMapper, mcpEndpoint, disallowDelete,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, mcpEndpoint, disallowDelete,
 					contextExtractor, keepAliveInterval);
 		}
 
diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
index aabdd7cd2..9854de210 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
@@ -100,7 +100,7 @@ public Mono closeGracefully() {
 
 	@Override
 	public  T unmarshalFrom(Object data, TypeRef typeRef) {
-		return McpJsonMapper.createDefault().convertValue(data, typeRef);
+		return McpJsonMapper.getDefault().convertValue(data, typeRef);
 	}
 
 }
diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java
index f42324faf..f3d6b77a7 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java
@@ -68,7 +68,7 @@ public Mono closeGracefully() {
 
 	@Override
 	public  T unmarshalFrom(Object data, TypeRef typeRef) {
-		return McpJsonMapper.createDefault().convertValue(data, typeRef);
+		return McpJsonMapper.getDefault().convertValue(data, typeRef);
 	}
 
 }
diff --git a/mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java b/mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java
index 27c3d3b07..911506e01 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/util/McpJsonMapperUtils.java
@@ -7,6 +7,6 @@ public final class McpJsonMapperUtils {
 	private McpJsonMapperUtils() {
 	}
 
-	public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.createDefault();
+	public static final McpJsonMapper JSON_MAPPER = McpJsonMapper.getDefault();
 
 }

From 82259bdcbc9627c0b66888e08e482116d6608e3c Mon Sep 17 00:00:00 2001
From: Graeme Rocher 
Date: Fri, 12 Sep 2025 10:16:37 +0200
Subject: [PATCH 4/7] cleanup

---
 .../json/jackson/JacksonMcpJsonMapper.java        | 12 ++++++++++++
 .../jackson/JacksonMcpJsonMapperSupplier.java     | 12 ++++++++++++
 .../JacksonJsonSchemaValidatorSupplier.java       | 13 +++++++++++++
 mcp-json/pom.xml                                  |  4 ++--
 .../io/modelcontextprotocol/json/TypeRef.java     | 15 +++++++++++++++
 5 files changed, 54 insertions(+), 2 deletions(-)

diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java
index 2323af0ba..960ea79c0 100644
--- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java
+++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java
@@ -15,6 +15,13 @@ public final class JacksonMcpJsonMapper implements McpJsonMapper {
 
 	private final ObjectMapper objectMapper;
 
+    /**
+     * Constructs a new JacksonMcpJsonMapper instance with the given ObjectMapper.
+     *
+     * @param objectMapper the ObjectMapper to be used for JSON serialization and deserialization.
+     *                     Must not be null.
+     * @throws IllegalArgumentException if the provided ObjectMapper is null.
+     */
 	public JacksonMcpJsonMapper(ObjectMapper objectMapper) {
 		if (objectMapper == null) {
 			throw new IllegalArgumentException("ObjectMapper must not be null");
@@ -22,6 +29,11 @@ public JacksonMcpJsonMapper(ObjectMapper objectMapper) {
 		this.objectMapper = objectMapper;
 	}
 
+    /**
+     * Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and deserialization.
+     *
+     * @return the ObjectMapper instance
+     */
 	public ObjectMapper getObjectMapper() {
 		return objectMapper;
 	}
diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java
index 2c03d8002..3d1398fee 100644
--- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java
+++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java
@@ -3,8 +3,20 @@
 import io.modelcontextprotocol.json.McpJsonMapper;
 import io.modelcontextprotocol.json.McpJsonMapperSupplier;
 
+/**
+ * A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON serialization and deserialization.
+ * 

+ * This implementation provides a {@link McpJsonMapper} backed by a Jackson {@link com.fasterxml.jackson.databind.ObjectMapper}. + */ public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier { + /** + * Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for JSON serialization and deserialization. + *

+ * The returned {@link McpJsonMapper} is backed by a new instance of {@link com.fasterxml.jackson.databind.ObjectMapper}. + * + * @return a new {@link McpJsonMapper} instance + */ @Override public McpJsonMapper get() { return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java index 5b454ecb4..4221eb16f 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java @@ -3,8 +3,21 @@ import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier; +/** + * A concrete implementation of {@link JsonSchemaValidatorSupplier} that provides a + * {@link JsonSchemaValidator} instance based on the Jackson library. + * + * @see JsonSchemaValidatorSupplier + * @see JsonSchemaValidator + */ public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier { + /** + * Returns a new instance of {@link JsonSchemaValidator} that uses the Jackson library + * for JSON schema validation. + * + * @return A {@link JsonSchemaValidator} instance. + */ @Override public JsonSchemaValidator get() { return new DefaultJsonSchemaValidator(); diff --git a/mcp-json/pom.xml b/mcp-json/pom.xml index 05b15b5ff..037ef2ac4 100644 --- a/mcp-json/pom.xml +++ b/mcp-json/pom.xml @@ -10,8 +10,8 @@ mcp-json jar - Java MCP SDK JSON Schema - Java MCP SDK JSON Schema API + Java MCP SDK JSON Support + Java MCP SDK JSON Support API https://github.com/modelcontextprotocol/java-sdk https://github.com/modelcontextprotocol/java-sdk diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java index efde45070..f2067daf3 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java @@ -11,6 +11,16 @@ public abstract class TypeRef { private final Type type; + /** + * Constructs a new TypeRef instance, capturing the generic type information + * of the subclass. This constructor should be called from an anonymous subclass + * to capture the actual type arguments. For example: + *

+     * TypeRef<List<Foo>> ref = new TypeRef<>(){};
+     * 
+ * + * @throws IllegalStateException if TypeRef is not subclassed with actual type information + */ protected TypeRef() { Type superClass = getClass().getGenericSuperclass(); if (superClass instanceof Class) { @@ -19,6 +29,11 @@ protected TypeRef() { this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; } + /** + * Returns the captured type information. + * + * @return the Type representing the actual type argument captured by this TypeRef instance + */ public Type getType() { return type; } From 81c2566614e09568a5fc89ef9392d27917bcbec2 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 12 Sep 2025 14:44:08 +0200 Subject: [PATCH 5/7] formatting --- .../json/jackson/JacksonMcpJsonMapper.java | 23 +++++++-------- .../jackson/JacksonMcpJsonMapperSupplier.java | 21 ++++++++------ .../JacksonJsonSchemaValidatorSupplier.java | 11 ++++--- .../io/modelcontextprotocol/json/TypeRef.java | 29 +++++++++---------- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java index 960ea79c0..3ffc90a56 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java @@ -15,13 +15,12 @@ public final class JacksonMcpJsonMapper implements McpJsonMapper { private final ObjectMapper objectMapper; - /** - * Constructs a new JacksonMcpJsonMapper instance with the given ObjectMapper. - * - * @param objectMapper the ObjectMapper to be used for JSON serialization and deserialization. - * Must not be null. - * @throws IllegalArgumentException if the provided ObjectMapper is null. - */ + /** + * Constructs a new JacksonMcpJsonMapper instance with the given ObjectMapper. + * @param objectMapper the ObjectMapper to be used for JSON serialization and + * deserialization. Must not be null. + * @throws IllegalArgumentException if the provided ObjectMapper is null. + */ public JacksonMcpJsonMapper(ObjectMapper objectMapper) { if (objectMapper == null) { throw new IllegalArgumentException("ObjectMapper must not be null"); @@ -29,11 +28,11 @@ public JacksonMcpJsonMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } - /** - * Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and deserialization. - * - * @return the ObjectMapper instance - */ + /** + * Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and + * deserialization. + * @return the ObjectMapper instance + */ public ObjectMapper getObjectMapper() { return objectMapper; } diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java index 3d1398fee..4ca311f1f 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java @@ -4,19 +4,22 @@ import io.modelcontextprotocol.json.McpJsonMapperSupplier; /** - * A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON serialization and deserialization. + * A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON + * serialization and deserialization. *

- * This implementation provides a {@link McpJsonMapper} backed by a Jackson {@link com.fasterxml.jackson.databind.ObjectMapper}. + * This implementation provides a {@link McpJsonMapper} backed by a Jackson + * {@link com.fasterxml.jackson.databind.ObjectMapper}. */ public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier { - /** - * Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for JSON serialization and deserialization. - *

- * The returned {@link McpJsonMapper} is backed by a new instance of {@link com.fasterxml.jackson.databind.ObjectMapper}. - * - * @return a new {@link McpJsonMapper} instance - */ + /** + * Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for + * JSON serialization and deserialization. + *

+ * The returned {@link McpJsonMapper} is backed by a new instance of + * {@link com.fasterxml.jackson.databind.ObjectMapper}. + * @return a new {@link McpJsonMapper} instance + */ @Override public McpJsonMapper get() { return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper()); diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java index 4221eb16f..439da81b6 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java +++ b/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java @@ -12,12 +12,11 @@ */ public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier { - /** - * Returns a new instance of {@link JsonSchemaValidator} that uses the Jackson library - * for JSON schema validation. - * - * @return A {@link JsonSchemaValidator} instance. - */ + /** + * Returns a new instance of {@link JsonSchemaValidator} that uses the Jackson library + * for JSON schema validation. + * @return A {@link JsonSchemaValidator} instance. + */ @Override public JsonSchemaValidator get() { return new DefaultJsonSchemaValidator(); diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java index f2067daf3..af4b6b46c 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java @@ -11,16 +11,15 @@ public abstract class TypeRef { private final Type type; - /** - * Constructs a new TypeRef instance, capturing the generic type information - * of the subclass. This constructor should be called from an anonymous subclass - * to capture the actual type arguments. For example: - *

-     * TypeRef<List<Foo>> ref = new TypeRef<>(){};
-     * 
- * - * @throws IllegalStateException if TypeRef is not subclassed with actual type information - */ + /** + * Constructs a new TypeRef instance, capturing the generic type information of the + * subclass. This constructor should be called from an anonymous subclass to capture + * the actual type arguments. For example:
+	 * TypeRef<List<Foo>> ref = new TypeRef<>(){};
+	 * 
+ * @throws IllegalStateException if TypeRef is not subclassed with actual type + * information + */ protected TypeRef() { Type superClass = getClass().getGenericSuperclass(); if (superClass instanceof Class) { @@ -29,11 +28,11 @@ protected TypeRef() { this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; } - /** - * Returns the captured type information. - * - * @return the Type representing the actual type argument captured by this TypeRef instance - */ + /** + * Returns the captured type information. + * @return the Type representing the actual type argument captured by this TypeRef + * instance + */ public Type getType() { return type; } From aae734740c3a0b60f4307defe46bf131fdee4a56 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 15 Sep 2025 10:00:22 +0200 Subject: [PATCH 6/7] correct --- .../io/modelcontextprotocol/json/schema/JsonSchemaInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java index 9f78c2ed3..9ef2b3a0e 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java @@ -23,7 +23,7 @@ final class JsonSchemaInternal { */ static JsonSchemaValidator getDefaultValidator() { if (defaultValidator == null) { - defaultValidator = JsonSchemaInternal.getDefaultValidator(); + defaultValidator = JsonSchemaInternal.createDefaultValidator(); } return defaultValidator; } From 06403ced730a5bd4c7564e255960217bb5f79988 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 15 Sep 2025 14:58:00 +0200 Subject: [PATCH 7/7] Address pr feedback (#2) * add mcp-json and mcp-json-jackson to BOM * add license header * add mcp-json-jackon as a compile dependency * rename to jackson 2 --- mcp-bom/pom.xml | 14 ++++++++++++++ {mcp-json-jackson => mcp-json-jackson2}/pom.xml | 2 +- .../json/jackson/JacksonMcpJsonMapper.java | 3 +++ .../json/jackson/JacksonMcpJsonMapperSupplier.java | 3 +++ .../schema/jackson/DefaultJsonSchemaValidator.java | 1 - .../JacksonJsonSchemaValidatorSupplier.java | 3 +++ ...modelcontextprotocol.json.McpJsonMapperSupplier | 0 ...rotocol.json.schema.JsonSchemaValidatorSupplier | 0 .../modelcontextprotocol/json/McpJsonInternal.java | 3 +++ .../modelcontextprotocol/json/McpJsonMapper.java | 3 +++ .../json/McpJsonMapperSupplier.java | 3 +++ .../java/io/modelcontextprotocol/json/TypeRef.java | 3 +++ .../json/schema/JsonSchemaInternal.java | 3 +++ .../json/schema/JsonSchemaValidator.java | 1 - .../json/schema/JsonSchemaValidatorSupplier.java | 3 +++ mcp-spring/mcp-spring-webflux/pom.xml | 2 +- mcp-spring/mcp-spring-webmvc/pom.xml | 2 +- mcp/pom.xml | 9 +-------- pom.xml | 2 +- 19 files changed, 46 insertions(+), 14 deletions(-) rename {mcp-json-jackson => mcp-json-jackson2}/pom.xml (97%) rename {mcp-json-jackson => mcp-json-jackson2}/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java (97%) rename {mcp-json-jackson => mcp-json-jackson2}/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java (93%) rename {mcp-json-jackson => mcp-json-jackson2}/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java (99%) rename {mcp-json-jackson => mcp-json-jackson2}/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java (92%) rename {mcp-json-jackson => mcp-json-jackson2}/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier (100%) rename {mcp-json-jackson => mcp-json-jackson2}/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier (100%) diff --git a/mcp-bom/pom.xml b/mcp-bom/pom.xml index ac68e55bc..b7ea52639 100644 --- a/mcp-bom/pom.xml +++ b/mcp-bom/pom.xml @@ -33,6 +33,20 @@ ${project.version} + + + io.modelcontextprotocol.sdk + mcp-json + ${project.version} + + + + + io.modelcontextprotocol.sdk + mcp-json-jackson2 + ${project.version} + + io.modelcontextprotocol.sdk diff --git a/mcp-json-jackson/pom.xml b/mcp-json-jackson2/pom.xml similarity index 97% rename from mcp-json-jackson/pom.xml rename to mcp-json-jackson2/pom.xml index 4cdc318b4..25159a9fe 100644 --- a/mcp-json-jackson/pom.xml +++ b/mcp-json-jackson2/pom.xml @@ -8,7 +8,7 @@ mcp-parent 0.13.0-SNAPSHOT - mcp-json-jackson + mcp-json-jackson2 jar Java MCP SDK JSON Jackson Java MCP SDK JSON implementation based on Jackson diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java similarity index 97% rename from mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java rename to mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java index 3ffc90a56..9bcceeae0 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json.jackson; import com.fasterxml.jackson.databind.JavaType; diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java similarity index 93% rename from mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java rename to mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java index 4ca311f1f..018d73242 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json.jackson; import io.modelcontextprotocol.json.McpJsonMapper; diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java similarity index 99% rename from mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java rename to mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java index 48891fa7b..8d2837949 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java @@ -1,7 +1,6 @@ /* * Copyright 2024-2024 the original author or authors. */ - package io.modelcontextprotocol.json.schema.jackson; import java.util.Map; diff --git a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java similarity index 92% rename from mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java rename to mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java index 439da81b6..ef3413c58 100644 --- a/mcp-json-jackson/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json.schema.jackson; import io.modelcontextprotocol.json.schema.JsonSchemaValidator; diff --git a/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier b/mcp-json-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier similarity index 100% rename from mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier rename to mcp-json-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier diff --git a/mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier b/mcp-json-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier similarity index 100% rename from mcp-json-jackson/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier rename to mcp-json-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java index fb666a503..673cda3c2 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json; import java.util.ServiceLoader; diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java index 3fb030d7b..0d542d897 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json; import java.io.IOException; diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java index c2c917af1..c26bb8bf9 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json; import java.util.function.Supplier; diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java index af4b6b46c..6ef53137c 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json; import java.lang.reflect.ParameterizedType; diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java index 9ef2b3a0e..4beecb51d 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json.schema; import java.util.ServiceLoader; diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java index 2091b94c7..9b1a58db8 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java @@ -1,7 +1,6 @@ /* * Copyright 2024-2024 the original author or authors. */ - package io.modelcontextprotocol.json.schema; import java.util.Map; diff --git a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java index f4bb4e187..f24c76bd8 100644 --- a/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java @@ -1,3 +1,6 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ package io.modelcontextprotocol.json.schema; import java.util.function.Supplier; diff --git a/mcp-spring/mcp-spring-webflux/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml index d81e9017e..eda38881a 100644 --- a/mcp-spring/mcp-spring-webflux/pom.xml +++ b/mcp-spring/mcp-spring-webflux/pom.xml @@ -24,7 +24,7 @@ io.modelcontextprotocol.sdk - mcp-json-jackson + mcp-json-jackson2 0.13.0-SNAPSHOT diff --git a/mcp-spring/mcp-spring-webmvc/pom.xml b/mcp-spring/mcp-spring-webmvc/pom.xml index cdd3cdae5..8c698487d 100644 --- a/mcp-spring/mcp-spring-webmvc/pom.xml +++ b/mcp-spring/mcp-spring-webmvc/pom.xml @@ -24,7 +24,7 @@ io.modelcontextprotocol.sdk - mcp-json-jackson + mcp-json-jackson2 0.13.0-SNAPSHOT diff --git a/mcp/pom.xml b/mcp/pom.xml index 4b3a5b078..6ba402a4d 100644 --- a/mcp/pom.xml +++ b/mcp/pom.xml @@ -67,17 +67,10 @@ io.modelcontextprotocol.sdk - mcp-json + mcp-json-jackson2 0.13.0-SNAPSHOT - - io.modelcontextprotocol.sdk - mcp-json-jackson - 0.13.0-SNAPSHOT - test - - org.slf4j slf4j-api diff --git a/pom.xml b/pom.xml index 7d9f8a2c7..f60a9aca1 100644 --- a/pom.xml +++ b/pom.xml @@ -103,7 +103,7 @@ mcp-bom mcp - mcp-json-jackson + mcp-json-jackson2 mcp-json mcp-spring/mcp-spring-webflux mcp-spring/mcp-spring-webmvc