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:
parent
d8959ef854
commit
80d0ad82a6
8 changed files with 2043 additions and 1421 deletions
39
mcp-json/pom.xml
Normal file
39
mcp-json/pom.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp-parent</artifactId>
|
||||
<version>0.13.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>mcp-json</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Java MCP SDK JSON Support</name>
|
||||
<description>Java MCP SDK JSON Support API</description>
|
||||
<url>https://github.com/modelcontextprotocol/java-sdk</url>
|
||||
<scm>
|
||||
<url>https://github.com/modelcontextprotocol/java-sdk</url>
|
||||
<connection>git://github.com/modelcontextprotocol/java-sdk.git</connection>
|
||||
<developerConnection>git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
|
||||
</scm>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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<IllegalStateException> 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<IllegalStateException> ref, Exception toAdd) {
|
||||
ref.updateAndGet(existing -> {
|
||||
if (existing == null) {
|
||||
return new IllegalStateException("Failed to initialize default McpJsonMapper", toAdd);
|
||||
}
|
||||
else {
|
||||
existing.addSuppressed(toAdd);
|
||||
return existing;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 <T> generic type
|
||||
* @throws IOException on parse errors
|
||||
*/
|
||||
<T> T readValue(String content, Class<T> type) throws IOException;
|
||||
|
||||
/**
|
||||
* Deserialize JSON bytes into a target type.
|
||||
* @param content JSON as bytes
|
||||
* @param type target class
|
||||
* @return deserialized instance
|
||||
* @param <T> generic type
|
||||
* @throws IOException on parse errors
|
||||
*/
|
||||
<T> T readValue(byte[] content, Class<T> 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 <T> generic type
|
||||
* @throws IOException on parse errors
|
||||
*/
|
||||
<T> T readValue(String content, TypeRef<T> 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 <T> generic type
|
||||
* @throws IOException on parse errors
|
||||
*/
|
||||
<T> T readValue(byte[] content, TypeRef<T> 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 <T> generic type
|
||||
*/
|
||||
<T> T convertValue(Object fromValue, Class<T> type);
|
||||
|
||||
/**
|
||||
* Convert a value to a given parameterized type.
|
||||
* @param fromValue source value
|
||||
* @param type target type reference
|
||||
* @return converted value
|
||||
* @param <T> generic type
|
||||
*/
|
||||
<T> T convertValue(Object fromValue, TypeRef<T> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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<McpJsonMapper> {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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<List<Foo>> ref = new TypeRef<>(){};
|
||||
*/
|
||||
public abstract class TypeRef<T> {
|
||||
|
||||
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: <pre>
|
||||
* TypeRef<List<Foo>> ref = new TypeRef<>(){};
|
||||
* </pre>
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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<IllegalStateException> 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<IllegalStateException> ref, Exception toAdd) {
|
||||
ref.updateAndGet(existing -> {
|
||||
if (existing == null) {
|
||||
return new IllegalStateException("Failed to initialize default JsonSchemaValidatorSupplier", toAdd);
|
||||
}
|
||||
else {
|
||||
existing.addSuppressed(toAdd);
|
||||
return existing;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*/
|
||||
package io.modelcontextprotocol.json.schema;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface for validating structured content against a JSON schema. This interface
|
||||
* defines a method to validate structured content based on the provided output schema.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public interface JsonSchemaValidator {
|
||||
|
||||
/**
|
||||
* Represents the result of a validation operation.
|
||||
*
|
||||
* @param valid Indicates whether the validation was successful.
|
||||
* @param errorMessage An error message if the validation failed, otherwise null.
|
||||
* @param jsonStructuredOutput The text structured content in JSON format if the
|
||||
* validation was successful, otherwise null.
|
||||
*/
|
||||
record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) {
|
||||
|
||||
public static ValidationResponse asValid(String jsonStructuredOutput) {
|
||||
return new ValidationResponse(true, null, jsonStructuredOutput);
|
||||
}
|
||||
|
||||
public static ValidationResponse asInvalid(String message) {
|
||||
return new ValidationResponse(false, message, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the structured content against the provided JSON schema.
|
||||
* @param schema The JSON schema to validate against.
|
||||
* @param structuredContent The structured content to validate.
|
||||
* @return A ValidationResponse indicating whether the validation was successful or
|
||||
* not.
|
||||
*/
|
||||
ValidationResponse validate(Map<String, Object> schema, Object 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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<JsonSchemaValidator> {
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue