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-jackson2/pom.xml b/mcp-json-jackson2/pom.xml new file mode 100644 index 000000000..25159a9fe --- /dev/null +++ b/mcp-json-jackson2/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + io.modelcontextprotocol.sdk + mcp-parent + 0.13.0-SNAPSHOT + + mcp-json-jackson2 + 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-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java new file mode 100644 index 000000000..9bcceeae0 --- /dev/null +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapper.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +package io.modelcontextprotocol.json.jackson; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.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; + + /** + * 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"); + } + this.objectMapper = objectMapper; + } + + /** + * Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and + * deserialization. + * @return the ObjectMapper instance + */ + 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-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java new file mode 100644 index 000000000..018d73242 --- /dev/null +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +package io.modelcontextprotocol.json.jackson; + +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/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java similarity index 94% rename from mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java rename to mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java index f4bdc02eb..8d2837949 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java @@ -1,13 +1,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,8 +20,6 @@ import com.networknt.schema.SpecVersion; import com.networknt.schema.ValidationMessage; -import io.modelcontextprotocol.util.Assert; - /** * Default implementation of the {@link JsonSchemaValidator} interface. This class * provides methods to validate structured content against a JSON schema. It uses the @@ -52,9 +50,12 @@ public DefaultJsonSchemaValidator(ObjectMapper objectMapper) { @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-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java new file mode 100644 index 000000000..ef3413c58 --- /dev/null +++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/JacksonJsonSchemaValidatorSupplier.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +package io.modelcontextprotocol.json.schema.jackson; + +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-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier b/mcp-json-jackson2/src/main/resources/META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier new file mode 100644 index 000000000..8ea66d698 --- /dev/null +++ b/mcp-json-jackson2/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-jackson2/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 new file mode 100644 index 000000000..0fb0b7e5a --- /dev/null +++ b/mcp-json-jackson2/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..037ef2ac4 --- /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 Support + Java MCP SDK JSON Support 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-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..673cda3c2 --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonInternal.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +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 new file mode 100644 index 000000000..0d542d897 --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapper.java @@ -0,0 +1,109 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +package io.modelcontextprotocol.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; + + /** + * Returns the default {@link McpJsonMapper}. + * @return The default {@link McpJsonMapper} + * @throws IllegalStateException If no {@link McpJsonMapper} implementation exists on + * the classpath. + */ + static McpJsonMapper getDefault() { + return McpJsonInternal.getDefaultMapper(); + } + + /** + * 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/McpJsonMapperSupplier.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java new file mode 100644 index 000000000..c26bb8bf9 --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/McpJsonMapperSupplier.java @@ -0,0 +1,13 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +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-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java new file mode 100644 index 000000000..6ef53137c --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/TypeRef.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +package io.modelcontextprotocol.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; + + /** + * 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) { + throw new IllegalStateException("TypeRef constructed without actual type information"); + } + 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; + } + +} 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..4beecb51d --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaInternal.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +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.createDefaultValidator(); + } + 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/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java similarity index 59% rename from mcp/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java rename to mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java index 572d7c043..9b1a58db8 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/JsonSchemaValidator.java +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidator.java @@ -1,10 +1,12 @@ /* * Copyright 2024-2024 the original author or authors. */ - -package io.modelcontextprotocol.spec; +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 @@ -22,7 +24,7 @@ public interface JsonSchemaValidator { * @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) { + record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) { public static ValidationResponse asValid(String jsonStructuredOutput) { return new ValidationResponse(true, null, jsonStructuredOutput); @@ -42,4 +44,24 @@ public static ValidationResponse asInvalid(String message) { */ ValidationResponse validate(Map schema, Map structuredContent); + /** + * Creates the default {@link JsonSchemaValidator}. + * @return The default {@link JsonSchemaValidator} + * @throws IllegalStateException If no {@link JsonSchemaValidator} implementation + * exists on the classpath. + */ + static JsonSchemaValidator createDefault() { + return JsonSchemaInternal.createDefaultValidator(); + } + + /** + * 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 new file mode 100644 index 000000000..f24c76bd8 --- /dev/null +++ b/mcp-json/src/main/java/io/modelcontextprotocol/json/schema/JsonSchemaValidatorSupplier.java @@ -0,0 +1,18 @@ +/* + * Copyright 2025 - 2025 the original author or authors. + */ +package io.modelcontextprotocol.json.schema; + +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/pom.xml b/mcp-spring/mcp-spring-webflux/pom.xml index c2dac2bf9..eda38881a 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-jackson2 + 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 853aed2bf..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 @@ -22,8 +22,8 @@ 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.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.spec.DefaultMcpTransportSession; import io.modelcontextprotocol.spec.DefaultMcpTransportStream; @@ -88,7 +88,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 +104,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 +366,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 +426,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 +446,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 +455,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 +473,7 @@ private Tuple2, Iterable> parse(Serve */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private WebClient.Builder webClientBuilder; @@ -490,13 +489,13 @@ private Builder(WebClient.Builder webClientBuilder) { } /** - * Configure the {@link ObjectMapper} to use. - * @param objectMapper instance to use + * Configure the {@link McpJsonMapper} to use. + * @param jsonMapper instance to use * @return the builder instance */ - 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; } @@ -555,10 +554,8 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) { * @return a new instance of {@link WebClientStreamableHttpTransport} */ public WebClientStreamableHttpTransport build() { - ObjectMapper objectMapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper(); - - return new WebClientStreamableHttpTransport(objectMapper, this.webClientBuilder, endpoint, resumableStreams, - openConnectionOnStartup); + 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 51d21d18b..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 @@ -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.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.spec.HttpHeaders; import io.modelcontextprotocol.spec.McpClientTransport; @@ -100,10 +100,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 @@ -129,27 +129,16 @@ 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 ObjectMapper()); - } - /** * Constructs a new SseClientTransport with the specified WebClient builder and * ObjectMapper. Initializes both inbound and outbound message processing pipelines. * @param webClientBuilder the WebClient.Builder to use for creating the WebClient * instance - * @param 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 +146,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 +205,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 +243,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 +337,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 +365,7 @@ public static class Builder { private String sseEndpoint = DEFAULT_SSE_ENDPOINT; - private ObjectMapper objectMapper = new ObjectMapper(); + private McpJsonMapper jsonMapper; /** * Creates a new builder with the specified WebClient.Builder. @@ -400,13 +388,13 @@ public Builder sseEndpoint(String sseEndpoint) { } /** - * Sets the object mapper for JSON serialization/deserialization. - * @param objectMapper the object mapper + * Sets the JSON mapper for serialization/deserialization. + * @param jsonMapper the JsonMapper to use * @return this builder */ - 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; } @@ -415,7 +403,8 @@ public Builder objectMapper(ObjectMapper objectMapper) { * @return a new transport instance */ public WebFluxSseClientTransport build() { - return new WebFluxSseClientTransport(webClientBuilder, objectMapper, sseEndpoint); + return new WebFluxSseClientTransport(webClientBuilder, + 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 f64346265..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 @@ -9,8 +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.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -97,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 @@ -131,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 @@ -217,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; @@ -404,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 @@ -433,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); @@ -452,8 +380,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 @@ -480,7 +408,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String baseUrl = DEFAULT_BASE_URL; @@ -494,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; } @@ -577,11 +505,9 @@ 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.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 1f3d4c3bf..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 @@ -4,7 +4,7 @@ package io.modelcontextprotocol.server.transport; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpStatelessServerHandler; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -34,7 +34,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 +46,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 +106,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 +153,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -158,15 +165,15 @@ private Builder() { } /** - * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP + * Sets the JsonMapper to use for JSON serialization/deserialization of MCP * messages. - * @param objectMapper The ObjectMapper instance. Must not be null. + * @param jsonMapper The JsonMapper 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; } @@ -205,10 +212,9 @@ public Builder contextExtractor(McpTransportContextExtractor cont * @throws IllegalStateException if required parameters are not set */ public WebFluxStatelessServerTransport build() { - Assert.notNull(objectMapper, "ObjectMapper must be set"); Assert.notNull(mcpEndpoint, "Message endpoint must be set"); - - return new WebFluxStatelessServerTransport(objectMapper, mcpEndpoint, contextExtractor); + 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 44d89eaeb..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 @@ -4,8 +4,8 @@ package io.modelcontextprotocol.server.transport; -import com.fasterxml.jackson.core.type.TypeReference; -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.HttpHeaders; @@ -49,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; @@ -67,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; @@ -230,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()); @@ -243,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); @@ -349,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); @@ -369,8 +370,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 jsonMapper.convertValue(data, typeRef); } @Override @@ -397,7 +398,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -413,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; } @@ -482,13 +483,12 @@ 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.getDefault() : jsonMapper, mcpEndpoint, contextExtractor, disallowDelete, keepAliveInterval); } } -} \ No newline at end of file +} 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 06c95d145..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,6 +11,8 @@ import java.util.function.Function; import com.fasterxml.jackson.databind.ObjectMapper; +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; @@ -29,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; @@ -54,8 +57,6 @@ class WebFluxSseClientTransportTests { private WebClient.Builder webClientBuilder; - private ObjectMapper objectMapper; - // Test class to access protected methods static class TestSseClientTransport extends WebFluxSseClientTransport { @@ -63,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, objectMapper); + public TestSseClientTransport(WebClient.Builder webClientBuilder, McpJsonMapper jsonMapper) { + super(webClientBuilder, jsonMapper); } @Override @@ -112,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(); } @@ -131,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 @@ -148,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(); @@ -160,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..8c698487d 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-jackson2 + 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 85373b6fe..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 @@ -11,8 +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.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -92,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; @@ -118,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. @@ -209,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; @@ -399,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 @@ -456,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); } @@ -471,15 +395,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 McpJsonMapper. * @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); } /** @@ -541,7 +465,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper = new ObjectMapper(); + private McpJsonMapper jsonMapper; private String baseUrl = ""; @@ -556,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; } @@ -633,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.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 fc2da0439..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 @@ -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.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 3cc104dd4..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 @@ -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,8 +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.json.TypeRef; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpTransportContextExtractor; @@ -82,7 +82,7 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer */ private final boolean disallowDelete; - private final ObjectMapper objectMapper; + private final McpJsonMapper jsonMapper; private final RouterFunction routerFunction; @@ -104,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. @@ -113,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; @@ -325,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); @@ -516,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); @@ -539,15 +539,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 McpJsonMapper. * @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); } /** @@ -597,7 +597,7 @@ public static Builder builder() { */ public static class Builder { - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private String mcpEndpoint = "/mcp"; @@ -609,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; } @@ -678,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.getDefault() : 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 5484a63c2..cd8458311 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.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; 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 McpJsonMapper.getDefault().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..e9ec8900c --- /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.getDefault(); + +} \ 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 dc85a419e..6ba402a4d 100644 --- a/mcp/pom.xml +++ b/mcp/pom.xml @@ -65,6 +65,11 @@ + + io.modelcontextprotocol.sdk + mcp-json-jackson2 + 0.13.0-SNAPSHOT + org.slf4j @@ -74,7 +79,7 @@ com.fasterxml.jackson.core - jackson-databind + jackson-annotations ${jackson.version} @@ -83,11 +88,6 @@ reactor-core - - com.networknt - json-schema-validator - ${json-schema-validator.version} - @@ -216,8 +216,14 @@ 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..8d5bc34a6 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.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..661a41170 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -19,8 +19,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.json.McpJsonMapper; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; @@ -97,8 +97,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; @@ -117,81 +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 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, 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, - 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, ObjectMapper objectMapper) { - this(httpClient, requestBuilder, baseUri, sseEndpoint, objectMapper, McpAsyncHttpClientRequestCustomizer.NOOP); - } - /** * Creates a new transport instance with custom HTTP client builder, object mapper, * and headers. @@ -199,14 +124,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 +139,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 +170,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; private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); @@ -340,13 +265,13 @@ public Builder customizeRequest(final Consumer requestCusto } /** - * Sets the object mapper for JSON serialization/deserialization. - * @param objectMapper the object mapper + * Sets the JSON mapper implementation to use for serialization/deserialization. + * @param jsonMapper the JSON mapper * @return this builder */ - 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; } @@ -402,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, objectMapper, - httpRequestCustomizer); + return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint, + jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, httpRequestCustomizer); } } @@ -450,7 +375,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 +456,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 +507,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..c73515938 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -22,8 +22,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.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; @@ -106,7 +106,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 +124,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 +278,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 +393,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 +479,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 +508,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 +582,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 +593,7 @@ public static class Builder { private final String baseUri; - private ObjectMapper objectMapper; + private McpJsonMapper jsonMapper; private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1); @@ -663,13 +663,13 @@ public Builder customizeRequest(final Consumer requestCusto } /** - * Configure the {@link ObjectMapper} to use. - * @param objectMapper instance to use + * Configure a custom {@link McpJsonMapper} implementation to use. + * @param jsonMapper instance to use * @return the builder instance */ - 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; } @@ -763,12 +763,10 @@ 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, - 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/client/transport/StdioClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java index 009d415e0..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,8 +15,8 @@ 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.json.TypeRef; +import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; @@ -48,7 +48,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; @@ -70,29 +70,20 @@ public class StdioClientTransport implements McpClientTransport { private Consumer stdErrorHandler = error -> logger.info("STDERR Message received: {}", error); /** - * Creates a new StdioClientTransport with the specified parameters and default - * ObjectMapper. + * Creates a new StdioClientTransport with the specified parameters and JsonMapper. * @param params The parameters for configuring the server process + * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization */ - public StdioClientTransport(ServerParameters params) { - this(params, new ObjectMapper()); - } - - /** - * Creates a new StdioClientTransport with the specified parameters and ObjectMapper. - * @param params The parameters for configuring the server process - * @param objectMapper The ObjectMapper 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(); @@ -259,7 +250,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 +291,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 +383,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..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 com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +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; @@ -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..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 com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.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..ec86b5927 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -11,13 +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.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; @@ -67,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();
  * }
@@ -75,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();
@@ -227,11 +227,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,
-					this.uriTemplateManagerFactory, jsonSchemaValidator);
+
+			var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
+					: JsonSchemaValidator.getDefault();
+
+			return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
 		}
 
 	}
@@ -254,11 +255,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 jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
-					: new DefaultJsonSchemaValidator(mapper);
-			return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout,
-					this.uriTemplateManagerFactory, jsonSchemaValidator);
+					: JsonSchemaValidator.getDefault();
+			return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
+					features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator);
 		}
 
 	}
@@ -270,7 +270,7 @@ abstract class AsyncSpecification> {
 
 		McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
 
-		ObjectMapper objectMapper;
+		McpJsonMapper jsonMapper;
 
 		McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
 
@@ -417,7 +417,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))
 		 * )
@@ -765,14 +765,14 @@ public AsyncSpecification rootsChangeHandlers(
 		}
 
 		/**
-		 * Sets the object mapper to use for serializing and deserializing JSON messages.
-		 * @param objectMapper the instance to use. Must not be null.
+		 * 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 objectMapper is null
+		 * @throws IllegalArgumentException if jsonMapper is null
 		 */
-		public AsyncSpecification objectMapper(ObjectMapper objectMapper) {
-			Assert.notNull(objectMapper, "ObjectMapper must not be null");
-			this.objectMapper = objectMapper;
+		public AsyncSpecification jsonMapper(McpJsonMapper jsonMapper) {
+			Assert.notNull(jsonMapper, "JsonMapper must not be null");
+			this.jsonMapper = jsonMapper;
 			return this;
 		}
 
@@ -813,13 +813,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 jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
-					: new DefaultJsonSchemaValidator(mapper);
-
-			var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout,
-					this.uriTemplateManagerFactory, jsonSchemaValidator);
 
+			var asyncServer = new McpAsyncServer(transportProvider,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, asyncFeatures, requestTimeout,
+					uriTemplateManagerFactory,
+					jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault());
 			return new McpSyncServer(asyncServer, this.immediateExecution);
 		}
 
@@ -846,13 +844,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 jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
-					: new DefaultJsonSchemaValidator(mapper);
-
-			var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout,
+					: JsonSchemaValidator.getDefault();
+			var asyncServer = new McpAsyncServer(transportProvider,
+					jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, asyncFeatures, this.requestTimeout,
 					this.uriTemplateManagerFactory, jsonSchemaValidator);
-
 			return new McpSyncServer(asyncServer, this.immediateExecution);
 		}
 
@@ -865,7 +861,7 @@ abstract class SyncSpecification> {
 
 		McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
 
-		ObjectMapper objectMapper;
+		McpJsonMapper jsonMapper;
 
 		McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
 
@@ -1014,7 +1010,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))
 		 * )
 		 * }
@@ -1363,14 +1359,14 @@ public SyncSpecification rootsChangeHandlers( } /** - * Sets the object mapper to use for serializing and deserializing JSON messages. - * @param objectMapper the instance to use. Must not be null. + * 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 objectMapper is null + * @throws IllegalArgumentException if jsonMapper is null */ - public SyncSpecification objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public SyncSpecification jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -1404,7 +1400,7 @@ class StatelessAsyncSpecification { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - ObjectMapper objectMapper; + McpJsonMapper jsonMapper; McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -1821,14 +1817,14 @@ public StatelessAsyncSpecification completions( } /** - * Sets the object mapper to use for serializing and deserializing JSON messages. - * @param objectMapper the instance to use. Must not be null. + * 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 objectMapper is null + * @throws IllegalArgumentException if jsonMapper is null */ - public StatelessAsyncSpecification objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public StatelessAsyncSpecification jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -1849,11 +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); - 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, - this.uriTemplateManagerFactory, jsonSchemaValidator); + return new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, + features, requestTimeout, uriTemplateManagerFactory, + jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault()); } } @@ -1866,7 +1860,7 @@ class StatelessSyncSpecification { McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); - ObjectMapper objectMapper; + McpJsonMapper jsonMapper; McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; @@ -2283,14 +2277,14 @@ public StatelessSyncSpecification completions( } /** - * Sets the object mapper to use for serializing and deserializing JSON messages. - * @param objectMapper the instance to use. Must not be null. + * 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 objectMapper is null + * @throws IllegalArgumentException if jsonMapper is null */ - public StatelessSyncSpecification objectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; + public StatelessSyncSpecification jsonMapper(McpJsonMapper jsonMapper) { + Assert.notNull(jsonMapper, "JsonMapper must not be null"); + this.jsonMapper = jsonMapper; return this; } @@ -2325,31 +2319,13 @@ 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); - */ 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 asyncServer = new McpStatelessAsyncServer(transport, + jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, asyncFeatures, requestTimeout, + uriTemplateManagerFactory, + this.jsonSchemaValidator != null ? this.jsonSchemaValidator : JsonSchemaValidator.getDefault()); 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 50d45b14c..ea1423c77 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
@@ -4,16 +4,15 @@
 
 package io.modelcontextprotocol.server;
 
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
+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;
 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..4739e231a 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 com.fasterxml.jackson.core.type.TypeReference;
-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;
@@ -89,8 +89,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;
@@ -121,60 +121,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
 	/**
 	 * 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(objectMapper, DEFAULT_BASE_URL, messageEndpoint, sseEndpoint);
-	}
-
-	/**
-	 * Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
-	 * endpoint.
-	 * @param objectMapper The JSON object mapper to use for message
-	 * serialization/deserialization
-	 * @param baseUrl The base URL for the server transport
-	 * @param messageEndpoint The endpoint path where clients will send their messages
-	 * @param sseEndpoint The endpoint path where clients will establish SSE connections
-	 * @deprecated Use the builder {@link #builder()} instead for better configuration
-	 * options.
-	 */
-	@Deprecated
-	public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,
-			String sseEndpoint) {
-		this(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(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 +132,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;
@@ -217,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
@@ -342,7 +278,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 +291,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 +307,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 +322,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 +418,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 +431,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);
 		}
 
 		/**
@@ -559,7 +495,7 @@ public static Builder builder() {
 	 */
 	public static class Builder {
 
-		private ObjectMapper objectMapper = new ObjectMapper();
+		private McpJsonMapper jsonMapper;
 
 		private String baseUrl = DEFAULT_BASE_URL;
 
@@ -573,13 +509,15 @@ 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
+		 * 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 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;
 		}
 
@@ -648,16 +586,14 @@ 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");
 			}
-			return new HttpServletSseServerTransportProvider(objectMapper, baseUrl, messageEndpoint, sseEndpoint,
+			return new HttpServletSseServerTransportProvider(
+					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 9a8f6cbb9..40767f416 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java
@@ -11,7 +11,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
 
 import io.modelcontextprotocol.common.McpTransportContext;
 import io.modelcontextprotocol.server.McpStatelessServerHandler;
@@ -48,7 +48,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 +58,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 +139,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 +152,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 +203,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 +236,7 @@ public static Builder builder() {
 	 */
 	public static class Builder {
 
-		private ObjectMapper objectMapper;
+		private McpJsonMapper jsonMapper;
 
 		private String mcpEndpoint = "/mcp";
 
@@ -248,15 +248,15 @@ private Builder() {
 		}
 
 		/**
-		 * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
+		 * Sets the JsonMapper to use for JSON serialization/deserialization of MCP
 		 * messages.
-		 * @param objectMapper The ObjectMapper instance. Must not be null.
+		 * @param jsonMapper The JsonMapper 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;
 		}
 
@@ -295,10 +295,9 @@ public Builder contextExtractor(McpTransportContextExtractor
 		 * @throws IllegalStateException if required parameters are not set
 		 */
 		public HttpServletStatelessServerTransport build() {
-			Assert.notNull(objectMapper, "ObjectMapper must be set");
 			Assert.notNull(mcpEndpoint, "Message endpoint must be set");
-
-			return new HttpServletStatelessServerTransport(objectMapper, 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 3cb8d7b15..137015876 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 com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.TypeRef;
 
 import io.modelcontextprotocol.common.McpTransportContext;
 import io.modelcontextprotocol.server.McpTransportContextExtractor;
@@ -29,6 +28,7 @@
 import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
 import io.modelcontextprotocol.spec.ProtocolVersions;
 import io.modelcontextprotocol.util.Assert;
+import io.modelcontextprotocol.json.McpJsonMapper;
 import io.modelcontextprotocol.util.KeepAliveScheduler;
 import jakarta.servlet.AsyncContext;
 import jakarta.servlet.ServletException;
@@ -97,7 +97,7 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
 	 */
 	private final boolean disallowDelete;
 
-	private final ObjectMapper objectMapper;
+	private final McpJsonMapper jsonMapper;
 
 	private McpStreamableServerSession.Factory sessionFactory;
 
@@ -121,22 +121,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 +392,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 +403,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 +418,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 +578,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 +685,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 +702,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 +762,7 @@ public static Builder builder() {
 	 */
 	public static class Builder {
 
-		private ObjectMapper objectMapper;
+		private McpJsonMapper jsonMapper;
 
 		private String mcpEndpoint = "/mcp";
 
@@ -774,15 +774,15 @@ public static class Builder {
 		private Duration keepAliveInterval;
 
 		/**
-		 * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
+		 * Sets the JsonMapper to use for JSON serialization/deserialization of MCP
 		 * messages.
-		 * @param objectMapper The ObjectMapper instance. Must not be null.
+		 * @param jsonMapper The JsonMapper 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;
 		}
 
@@ -839,11 +839,10 @@ public Builder keepAliveInterval(Duration keepAliveInterval) {
 		 * @throws IllegalStateException if required parameters are not set
 		 */
 		public HttpServletStreamableServerTransportProvider build() {
-			Assert.notNull(this.objectMapper, "ObjectMapper 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 == null ? McpJsonMapper.getDefault() : 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 af602f610..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 com.fasterxml.jackson.core.type.TypeReference;
-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,6 +24,7 @@
 import io.modelcontextprotocol.spec.McpServerTransportProvider;
 import io.modelcontextprotocol.spec.ProtocolVersions;
 import io.modelcontextprotocol.util.Assert;
+import io.modelcontextprotocol.json.McpJsonMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import reactor.core.publisher.Flux;
@@ -44,7 +44,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;
 
@@ -56,36 +56,28 @@ 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 ObjectMapper());
-	}
-
 	/**
 	 * Creates a new StdioServerTransportProvider with the specified ObjectMapper and
 	 * System streams.
-	 * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
+	 * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
 	 */
-	public StdioServerTransportProvider(ObjectMapper objectMapper) {
-		this(objectMapper, System.in, System.out);
+	public StdioServerTransportProvider(McpJsonMapper jsonMapper) {
+		this(jsonMapper, 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 +157,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 +211,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 +255,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/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index 6ac8defa0..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 com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.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..14bd5f28d 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java
@@ -21,8 +21,8 @@
 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.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 
 import io.modelcontextprotocol.util.Assert;
 
@@ -111,8 +111,6 @@ private McpSchema() {
 	// Elicitation Methods
 	public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
 
-	private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
 	// ---------------------------
 	// JSON-RPC Error Codes
 	// ---------------------------
@@ -178,12 +176,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,22 +189,22 @@ 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);
@@ -1272,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();
 		}
@@ -1359,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;
 			}
 
@@ -1369,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;
 			}
 
@@ -1392,18 +1343,18 @@ public Tool build() {
 		}
 	}
 
-	private static Map schemaToMap(String schema) {
+	private static Map schemaToMap(McpJsonMapper jsonMapper, String schema) {
 		try {
-			return OBJECT_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 OBJECT_MAPPER.readValue(schema, JsonSchema.class);
+			return jsonMapper.readValue(schema, JsonSchema.class);
 		}
 		catch (IOException e) {
 			throw new IllegalArgumentException("Invalid schema: " + schema, e);
@@ -1427,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 OBJECT_MAPPER.readValue(jsonArguments, MAP_TYPE_REF);
+				return jsonMapper.readValue(jsonArguments, MAP_TYPE_REF);
 			}
 			catch (IOException e) {
 				throw new IllegalArgumentException("Invalid arguments: " + jsonArguments, e);
@@ -1466,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;
 			}
 
@@ -1572,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 = OBJECT_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 747b45490..b9ff041a9 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.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..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 com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.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..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 com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.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..0a732bab6 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.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..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 com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.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/util/KeepAliveScheduler.java b/mcp/src/main/java/io/modelcontextprotocol/util/KeepAliveScheduler.java
index 9d411cd41..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 com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.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..9854de210 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.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 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 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 778746faa..f3d6b77a7 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.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
 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 McpJsonMapper.getDefault().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 cab847512..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 com.fasterxml.jackson.core.type.TypeReference;
-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);
 
@@ -321,7 +326,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 +430,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 +475,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 +556,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..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 com.fasterxml.jackson.core.type.TypeReference;
-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() {
@@ -73,8 +71,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 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 e088b8773..c5c365798 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java
@@ -14,8 +14,6 @@
 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;
@@ -38,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;
@@ -78,8 +77,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);
+					HttpRequest.newBuilder().header("Content-Type", "application/json"), baseUri, "/sse", JSON_MAPPER,
+					McpAsyncHttpClientRequestCustomizer.NOOP);
 		}
 
 		public int getInboundMessageCount() {
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 104349116..640d34c9c 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.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/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 a73ec7209..069d0f896 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.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/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 14987b5ac..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,7 +14,6 @@
 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;
@@ -26,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;
@@ -50,8 +50,6 @@ class StdioServerTransportProviderTests {
 
 	private StdioServerTransportProvider transportProvider;
 
-	private ObjectMapper objectMapper;
-
 	private McpServerSession.Factory sessionFactory;
 
 	private McpServerSession mockSession;
@@ -64,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);
@@ -75,7 +71,7 @@ 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(JSON_MAPPER, System.in, testOutPrintStream);
 	}
 
 	@AfterEach
@@ -105,7 +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(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);
@@ -185,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 -> {
@@ -200,7 +196,7 @@ 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(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 85dcd26c2..86912b4bf 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.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/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
new file mode 100644
index 000000000..ef7cd2737
--- /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.json.McpJsonMapper;
+import io.modelcontextprotocol.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..4f1dffe1d
--- /dev/null
+++ b/mcp/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java
@@ -0,0 +1,132 @@
+package io.modelcontextprotocol.spec.json.gson;
+
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.json.TypeRef;
+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();
+
+		// 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 4de9363c2..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 com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.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) {
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..911506e01
--- /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.getDefault();
+
+}
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..f60a9aca1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,8 @@
 	
 		mcp-bom
 		mcp
+        mcp-json-jackson2
+        mcp-json
 		mcp-spring/mcp-spring-webflux
 		mcp-spring/mcp-spring-webmvc
 		mcp-test