Skip to content

Commit 80d0ad8

Browse files
graemerochertzolov
authored andcommitted
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 <[email protected]>
1 parent d8959ef commit 80d0ad8

File tree

118 files changed

+2043
-1421
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+2043
-1421
lines changed

mcp-bom/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@
3333
<version>${project.version}</version>
3434
</dependency>
3535

36+
<!-- MCP JSON -->
37+
<dependency>
38+
<groupId>io.modelcontextprotocol.sdk</groupId>
39+
<artifactId>mcp-json</artifactId>
40+
<version>${project.version}</version>
41+
</dependency>
42+
43+
<!-- MCP JSON Jackson -->
44+
<dependency>
45+
<groupId>io.modelcontextprotocol.sdk</groupId>
46+
<artifactId>mcp-json-jackson2</artifactId>
47+
<version>${project.version}</version>
48+
</dependency>
49+
3650
<!-- MCP Test -->
3751
<dependency>
3852
<groupId>io.modelcontextprotocol.sdk</groupId>

mcp-json-jackson2/pom.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>io.modelcontextprotocol.sdk</groupId>
8+
<artifactId>mcp-parent</artifactId>
9+
<version>0.13.0-SNAPSHOT</version>
10+
</parent>
11+
<artifactId>mcp-json-jackson2</artifactId>
12+
<packaging>jar</packaging>
13+
<name>Java MCP SDK JSON Jackson</name>
14+
<description>Java MCP SDK JSON implementation based on Jackson</description>
15+
<url>https://github.com/modelcontextprotocol/java-sdk</url>
16+
<scm>
17+
<url>https://github.com/modelcontextprotocol/java-sdk</url>
18+
<connection>git://github.com/modelcontextprotocol/java-sdk.git</connection>
19+
<developerConnection>[email protected]/modelcontextprotocol/java-sdk.git</developerConnection>
20+
</scm>
21+
<build>
22+
<plugins>
23+
<plugin>
24+
<groupId>org.apache.maven.plugins</groupId>
25+
<artifactId>maven-jar-plugin</artifactId>
26+
<configuration>
27+
<archive>
28+
<manifest>
29+
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
30+
</manifest>
31+
</archive>
32+
</configuration>
33+
</plugin>
34+
</plugins>
35+
</build>
36+
<dependencies>
37+
<dependency>
38+
<groupId>io.modelcontextprotocol.sdk</groupId>
39+
<artifactId>mcp-json</artifactId>
40+
<version>0.13.0-SNAPSHOT</version>
41+
</dependency>
42+
<dependency>
43+
<groupId>com.fasterxml.jackson.core</groupId>
44+
<artifactId>jackson-databind</artifactId>
45+
<version>${jackson.version}</version>
46+
</dependency>
47+
<dependency>
48+
<groupId>com.networknt</groupId>
49+
<artifactId>json-schema-validator</artifactId>
50+
<version>${json-schema-validator.version}</version>
51+
</dependency>
52+
</dependencies>
53+
</project>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json.jackson;
6+
7+
import com.fasterxml.jackson.databind.JavaType;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import io.modelcontextprotocol.json.McpJsonMapper;
10+
import io.modelcontextprotocol.json.TypeRef;
11+
12+
import java.io.IOException;
13+
14+
/**
15+
* Jackson-based implementation of JsonMapper. Wraps a Jackson ObjectMapper but keeps the
16+
* SDK decoupled from Jackson at the API level.
17+
*/
18+
public final class JacksonMcpJsonMapper implements McpJsonMapper {
19+
20+
private final ObjectMapper objectMapper;
21+
22+
/**
23+
* Constructs a new JacksonMcpJsonMapper instance with the given ObjectMapper.
24+
* @param objectMapper the ObjectMapper to be used for JSON serialization and
25+
* deserialization. Must not be null.
26+
* @throws IllegalArgumentException if the provided ObjectMapper is null.
27+
*/
28+
public JacksonMcpJsonMapper(ObjectMapper objectMapper) {
29+
if (objectMapper == null) {
30+
throw new IllegalArgumentException("ObjectMapper must not be null");
31+
}
32+
this.objectMapper = objectMapper;
33+
}
34+
35+
/**
36+
* Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and
37+
* deserialization.
38+
* @return the ObjectMapper instance
39+
*/
40+
public ObjectMapper getObjectMapper() {
41+
return objectMapper;
42+
}
43+
44+
@Override
45+
public <T> T readValue(String content, Class<T> type) throws IOException {
46+
return objectMapper.readValue(content, type);
47+
}
48+
49+
@Override
50+
public <T> T readValue(byte[] content, Class<T> type) throws IOException {
51+
return objectMapper.readValue(content, type);
52+
}
53+
54+
@Override
55+
public <T> T readValue(String content, TypeRef<T> type) throws IOException {
56+
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
57+
return objectMapper.readValue(content, javaType);
58+
}
59+
60+
@Override
61+
public <T> T readValue(byte[] content, TypeRef<T> type) throws IOException {
62+
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
63+
return objectMapper.readValue(content, javaType);
64+
}
65+
66+
@Override
67+
public <T> T convertValue(Object fromValue, Class<T> type) {
68+
return objectMapper.convertValue(fromValue, type);
69+
}
70+
71+
@Override
72+
public <T> T convertValue(Object fromValue, TypeRef<T> type) {
73+
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
74+
return objectMapper.convertValue(fromValue, javaType);
75+
}
76+
77+
@Override
78+
public String writeValueAsString(Object value) throws IOException {
79+
return objectMapper.writeValueAsString(value);
80+
}
81+
82+
@Override
83+
public byte[] writeValueAsBytes(Object value) throws IOException {
84+
return objectMapper.writeValueAsBytes(value);
85+
}
86+
87+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json.jackson;
6+
7+
import io.modelcontextprotocol.json.McpJsonMapper;
8+
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
9+
10+
/**
11+
* A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON
12+
* serialization and deserialization.
13+
* <p>
14+
* This implementation provides a {@link McpJsonMapper} backed by a Jackson
15+
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
16+
*/
17+
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
18+
19+
/**
20+
* Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for
21+
* JSON serialization and deserialization.
22+
* <p>
23+
* The returned {@link McpJsonMapper} is backed by a new instance of
24+
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
25+
* @return a new {@link McpJsonMapper} instance
26+
*/
27+
@Override
28+
public McpJsonMapper get() {
29+
return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
30+
}
31+
32+
}

mcp/src/main/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidator.java renamed to mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/*
22
* Copyright 2024-2024 the original author or authors.
33
*/
4-
5-
package io.modelcontextprotocol.spec;
4+
package io.modelcontextprotocol.json.schema.jackson;
65

76
import java.util.Map;
87
import java.util.Set;
98
import java.util.concurrent.ConcurrentHashMap;
109

10+
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
1111
import org.slf4j.Logger;
1212
import org.slf4j.LoggerFactory;
1313

@@ -20,8 +20,6 @@
2020
import com.networknt.schema.SpecVersion;
2121
import com.networknt.schema.ValidationMessage;
2222

23-
import io.modelcontextprotocol.util.Assert;
24-
2523
/**
2624
* Default implementation of the {@link JsonSchemaValidator} interface. This class
2725
* provides methods to validate structured content against a JSON schema. It uses the
@@ -53,8 +51,12 @@ public DefaultJsonSchemaValidator(ObjectMapper objectMapper) {
5351
@Override
5452
public ValidationResponse validate(Map<String, Object> schema, Object structuredContent) {
5553

56-
Assert.notNull(schema, "Schema must not be null");
57-
Assert.notNull(structuredContent, "Structured content must not be null");
54+
if (schema == null) {
55+
throw new IllegalArgumentException("Schema must not be null");
56+
}
57+
if (structuredContent == null) {
58+
throw new IllegalArgumentException("Structured content must not be null");
59+
}
5860

5961
try {
6062

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json.schema.jackson;
6+
7+
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
8+
import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier;
9+
10+
/**
11+
* A concrete implementation of {@link JsonSchemaValidatorSupplier} that provides a
12+
* {@link JsonSchemaValidator} instance based on the Jackson library.
13+
*
14+
* @see JsonSchemaValidatorSupplier
15+
* @see JsonSchemaValidator
16+
*/
17+
public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier {
18+
19+
/**
20+
* Returns a new instance of {@link JsonSchemaValidator} that uses the Jackson library
21+
* for JSON schema validation.
22+
* @return A {@link JsonSchemaValidator} instance.
23+
*/
24+
@Override
25+
public JsonSchemaValidator get() {
26+
return new DefaultJsonSchemaValidator();
27+
}
28+
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapperSupplier
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.modelcontextprotocol.json.schema.jackson.JacksonJsonSchemaValidatorSupplier

mcp-json/pom.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>io.modelcontextprotocol.sdk</groupId>
8+
<artifactId>mcp-parent</artifactId>
9+
<version>0.13.0-SNAPSHOT</version>
10+
</parent>
11+
<artifactId>mcp-json</artifactId>
12+
<packaging>jar</packaging>
13+
<name>Java MCP SDK JSON Support</name>
14+
<description>Java MCP SDK JSON Support API</description>
15+
<url>https://github.com/modelcontextprotocol/java-sdk</url>
16+
<scm>
17+
<url>https://github.com/modelcontextprotocol/java-sdk</url>
18+
<connection>git://github.com/modelcontextprotocol/java-sdk.git</connection>
19+
<developerConnection>[email protected]/modelcontextprotocol/java-sdk.git</developerConnection>
20+
</scm>
21+
<build>
22+
<plugins>
23+
<plugin>
24+
<groupId>org.apache.maven.plugins</groupId>
25+
<artifactId>maven-jar-plugin</artifactId>
26+
<configuration>
27+
<archive>
28+
<manifest>
29+
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
30+
</manifest>
31+
</archive>
32+
</configuration>
33+
</plugin>
34+
</plugins>
35+
</build>
36+
<dependencies>
37+
38+
</dependencies>
39+
</project>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json;
6+
7+
import java.util.ServiceLoader;
8+
import java.util.concurrent.atomic.AtomicReference;
9+
import java.util.stream.Stream;
10+
11+
/**
12+
* Utility class for creating a default {@link McpJsonMapper} instance. This class
13+
* provides a single method to create a default mapper using the {@link ServiceLoader}
14+
* mechanism.
15+
*/
16+
final class McpJsonInternal {
17+
18+
private static McpJsonMapper defaultJsonMapper = null;
19+
20+
/**
21+
* Returns the cached default {@link McpJsonMapper} instance. If the default mapper
22+
* has not been created yet, it will be initialized using the
23+
* {@link #createDefaultMapper()} method.
24+
* @return the default {@link McpJsonMapper} instance
25+
* @throws IllegalStateException if no default {@link McpJsonMapper} implementation is
26+
* found
27+
*/
28+
static McpJsonMapper getDefaultMapper() {
29+
if (defaultJsonMapper == null) {
30+
defaultJsonMapper = McpJsonInternal.createDefaultMapper();
31+
}
32+
return defaultJsonMapper;
33+
}
34+
35+
/**
36+
* Creates a default {@link McpJsonMapper} instance using the {@link ServiceLoader}
37+
* mechanism. The default mapper is resolved by loading the first available
38+
* {@link McpJsonMapperSupplier} implementation on the classpath.
39+
* @return the default {@link McpJsonMapper} instance
40+
* @throws IllegalStateException if no default {@link McpJsonMapper} implementation is
41+
* found
42+
*/
43+
static McpJsonMapper createDefaultMapper() {
44+
AtomicReference<IllegalStateException> ex = new AtomicReference<>();
45+
return ServiceLoader.load(McpJsonMapperSupplier.class).stream().flatMap(p -> {
46+
try {
47+
McpJsonMapperSupplier supplier = p.get();
48+
return Stream.ofNullable(supplier);
49+
}
50+
catch (Exception e) {
51+
addException(ex, e);
52+
return Stream.empty();
53+
}
54+
}).flatMap(jsonMapperSupplier -> {
55+
try {
56+
return Stream.ofNullable(jsonMapperSupplier.get());
57+
}
58+
catch (Exception e) {
59+
addException(ex, e);
60+
return Stream.empty();
61+
}
62+
}).findFirst().orElseThrow(() -> {
63+
if (ex.get() != null) {
64+
return ex.get();
65+
}
66+
else {
67+
return new IllegalStateException("No default McpJsonMapper implementation found");
68+
}
69+
});
70+
}
71+
72+
private static void addException(AtomicReference<IllegalStateException> ref, Exception toAdd) {
73+
ref.updateAndGet(existing -> {
74+
if (existing == null) {
75+
return new IllegalStateException("Failed to initialize default McpJsonMapper", toAdd);
76+
}
77+
else {
78+
existing.addSuppressed(toAdd);
79+
return existing;
80+
}
81+
});
82+
}
83+
84+
}

0 commit comments

Comments
 (0)