feat: Introduce McpJsonMapper interface to decouple from Jackson (#543)

This pull request creates two modules, `mcp-json` and `mcp-json-jackson`. It removes the `com.fasterxml.jackson.core:jackson-databind` and `com.networknt:json-schema-validator` dependencies from the `mcp` module. The `mcp` module now only depends on `com.fasterxml.jackson.core:jackson-annotations`.

To use Jackson, you have to add `mcp-jackson` to your dependencies in addition to `mcp`. I added the dependency `mcp-jackson` to both `mcp-spring-mvc` and `mcp-spring-webflux` to avoid a breaking change in those modules.

It provides two [SPI](https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html) `JsonSchemaValidatorSupplier` and `JacksonJsonSchemaValidatorSupplier` to allow easy replacement for consumers who don't want to use Jackson.

This pull request also ensures no `McpJsonMapper` is instantiated if one is provided via a builder method. Only if the builders don't receive a `McpJsonMapper` mapper, one is instantiated in the `build` method of the builder.
The logic behind this is to allow frameworks to provide a `McpJsonMapper` mapper singleton implementation and feed it to the builders without paying the price of instantiating `McpJsonMappers`, which will not be used. The goal is to be able to use the `ObjectMapper` singleton of an application also for the MCP code.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Graeme Rocher 2025-09-10 12:26:17 +02:00 • committed by Christian Tzolov
commit 80d0ad82a6
18 changed files with 2043 additions and 1421 deletions

View file

@ -22,7 +22,13 @@
</scm>
<dependencies>
<dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-json-jackson2</artifactId>
<version>0.13.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.13.0-SNAPSHOT</version>

View file

@ -22,8 +22,8 @@ import org.springframework.web.reactive.function.client.ClientResponse;
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<ServerSentEvent<String>> 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<Consumer<Throwable>> 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 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
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 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
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 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return this.objectMapper.convertValue(data, typeRef);
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return this.jsonMapper.convertValue(data, typeRef);
}
private Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> parse(ServerSentEvent<String> event) {
@ -456,7 +455,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
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 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
*/
public static class Builder {
private ObjectMapper objectMapper;
private McpJsonMapper jsonMapper;
private WebClient.Builder webClientBuilder;
@ -490,13 +489,13 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
}
/**
* 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 class WebClientStreamableHttpTransport implements McpClientTransport {
* @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);
}
}

View file

@ -9,8 +9,8 @@ import java.util.List;
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 class WebFluxSseClientTransport implements McpClientTransport {
* 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 class WebFluxSseClientTransport implements McpClientTransport {
}
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 class WebFluxSseClientTransport implements McpClientTransport {
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 class WebFluxSseClientTransport implements McpClientTransport {
* type conversion capabilities to handle complex object structures.
* @param <T> 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> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return this.objectMapper.convertValue(data, typeRef);
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return this.jsonMapper.convertValue(data, typeRef);
}
/**
@ -377,7 +365,7 @@ public class WebFluxSseClientTransport implements McpClientTransport {
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 class WebFluxSseClientTransport implements McpClientTransport {
}
/**
* 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 class WebFluxSseClientTransport implements McpClientTransport {
* @return a new transport instance
*/
public WebFluxSseClientTransport build() {
return new WebFluxSseClientTransport(webClientBuilder, objectMapper, sseEndpoint);
return new WebFluxSseClientTransport(webClientBuilder,
jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, sseEndpoint);
}
}

View file

@ -9,8 +9,8 @@ import java.time.Duration;
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 class WebFluxSseServerTransportProvider implements McpServerTransportProv
* 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<ServerRequest> 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 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
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 class WebFluxSseServerTransportProvider implements McpServerTransportProv
public Mono<Void> 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 class WebFluxSseServerTransportProvider implements McpServerTransportProv
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return objectMapper.convertValue(data, typeRef);
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return jsonMapper.convertValue(data, typeRef);
}
@Override
@ -480,7 +408,7 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
*/
public static class Builder {
private ObjectMapper objectMapper;
private McpJsonMapper jsonMapper;
private String baseUrl = DEFAULT_BASE_URL;
@ -494,15 +422,15 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
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 class WebFluxSseServerTransportProvider implements McpServerTransportProv
* @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);
}
}

View file

@ -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<ServerRequest> 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 @@ public class WebFluxStatelessServerTransport implements McpStatelessServerTransp
return request.bodyToMono(String.class).<ServerResponse>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 class WebFluxStatelessServerTransport implements McpStatelessServerTransp
*/
public static class Builder {
private ObjectMapper objectMapper;
private McpJsonMapper jsonMapper;
private String mcpEndpoint = "/mcp";
@ -158,15 +165,15 @@ public class WebFluxStatelessServerTransport implements McpStatelessServerTransp
}
/**
* 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 class WebFluxStatelessServerTransport implements McpStatelessServerTransp
* @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);
}
}

View file

@ -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<ServerRequest> 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 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
return request.bodyToMono(String.class).<ServerResponse>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<McpSchema.InitializeRequest>() {
});
var typeReference = new TypeRef<McpSchema.InitializeRequest>() {
};
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 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
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 class WebFluxStreamableServerTransportProvider implements McpStreamableSe
public Mono<Void> 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 class WebFluxStreamableServerTransportProvider implements McpStreamableSe
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return objectMapper.convertValue(data, typeRef);
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return jsonMapper.convertValue(data, typeRef);
}
@Override
@ -397,7 +398,7 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
*/
public static class Builder {
private ObjectMapper objectMapper;
private McpJsonMapper jsonMapper;
private String mcpEndpoint = "/mcp";
@ -413,15 +414,15 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
}
/**
* 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 class WebFluxStreamableServerTransportProvider implements McpStreamableSe
* @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);
}
}
}
}

View file

@ -16,8 +16,6 @@ import org.springframework.web.reactive.function.client.WebClient;
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 @@ class WebFluxSseIntegrationTests extends AbstractMcpClientServerIntegrationTests
public void before() {
this.mcpServerTransportProvider = new WebFluxSseServerTransportProvider.Builder()
.objectMapper(new ObjectMapper())
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
.sseEndpoint(CUSTOM_SSE_ENDPOINT)
.contextExtractor(TEST_CONTEXT_EXTRACTOR)

View file

@ -13,9 +13,6 @@ import org.springframework.http.server.reactive.HttpHandler;
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 @@ class WebFluxStatelessIntegrationTests extends AbstractStatelessIntegrationTests
@BeforeEach
public void before() {
this.mcpStreamableServerTransport = WebFluxStatelessServerTransport.builder()
.objectMapper(new ObjectMapper())
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
.build();

View file

@ -16,8 +16,6 @@ import org.springframework.web.reactive.function.client.WebClient;
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 @@ class WebFluxStreamableIntegrationTests extends AbstractMcpClientServerIntegrati
public void before() {
this.mcpStreamableServerTransportProvider = WebFluxStreamableServerTransportProvider.builder()
.objectMapper(new ObjectMapper())
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
.contextExtractor(TEST_CONTEXT_EXTRACTOR)
.build();

View file

@ -13,7 +13,6 @@ import org.junit.jupiter.api.BeforeAll;
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;
/**

View file

@ -11,6 +11,8 @@ import java.util.concurrent.atomic.AtomicInteger;
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 reactor.test.StepVerifier;
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 @@ class WebFluxSseClientTransportTests {
private Sinks.Many<ServerSentEvent<String>> 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 @@ class WebFluxSseClientTransportTests {
@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 @@ class WebFluxSseClientTransportTests {
@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 @@ class WebFluxSseClientTransportTests {
// 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 @@ class WebFluxSseClientTransportTests {
// Test builder with all custom parameters
WebFluxSseClientTransport transport4 = WebFluxSseClientTransport.builder(webClientBuilder)
.objectMapper(customMapper)
.sseEndpoint("/custom-sse")
.build();
assertThatCode(() -> transport4.closeGracefully().block()).doesNotThrowAnyException();

View file

@ -7,7 +7,6 @@ package io.modelcontextprotocol.common;
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();

View file

@ -8,7 +8,6 @@ import java.util.Map;
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();

View file

@ -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());

View file

@ -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 @@ class WebFluxSseMcpSyncServerTests extends AbstractMcpSyncServerTests {
}
private McpServerTransportProvider createMcpTransportProvider() {
transportProvider = new WebFluxSseServerTransportProvider.Builder().objectMapper(new ObjectMapper())
.messageEndpoint(MESSAGE_ENDPOINT)
.build();
transportProvider = new WebFluxSseServerTransportProvider.Builder().messageEndpoint(MESSAGE_ENDPOINT).build();
return transportProvider;
}

View file

@ -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();

View file

@ -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();

View file

@ -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();
}