Skip to content

Commit d518303

Browse files
committed
Initial refactor of json/json schema api. Moved classes in mcp-json
into mcp-core.
1 parent 86991c1 commit d518303

File tree

34 files changed

+393
-67
lines changed

34 files changed

+393
-67
lines changed

mcp-core/pom.xml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,6 @@
6565
</build>
6666

6767
<dependencies>
68-
<dependency>
69-
<groupId>io.modelcontextprotocol.sdk</groupId>
70-
<artifactId>mcp-json</artifactId>
71-
<version>0.17.0-SNAPSHOT</version>
72-
</dependency>
73-
7468
<dependency>
7569
<groupId>org.slf4j</groupId>
7670
<artifactId>slf4j-api</artifactId>
@@ -90,6 +84,11 @@
9084

9185

9286
<!-- Used by the HttpServletSseServerTransport -->
87+
<dependency>
88+
<groupId>com.fasterxml.jackson.core</groupId>
89+
<artifactId>jackson-databind</artifactId>
90+
<version>${jackson.version}</version>
91+
</dependency>
9392
<dependency>
9493
<groupId>jakarta.servlet</groupId>
9594
<artifactId>jakarta.servlet-api</artifactId>
@@ -104,7 +103,6 @@
104103
<version>0.17.0-SNAPSHOT</version>
105104
<scope>test</scope>
106105
</dependency>
107-
108106
<dependency>
109107
<groupId>org.springframework</groupId>
110108
<artifactId>spring-webmvc</artifactId>

mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
import java.util.function.Function;
1414
import java.util.function.Supplier;
1515

16-
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
1716
import io.modelcontextprotocol.common.McpTransportContext;
17+
import io.modelcontextprotocol.json.internal.DefaultMcpJsonSchemaValidatorSupplier;
18+
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
1819
import io.modelcontextprotocol.spec.McpClientTransport;
1920
import io.modelcontextprotocol.spec.McpSchema;
2021
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
@@ -475,7 +476,8 @@ public McpSyncClient build() {
475476
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
476477

477478
return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
478-
jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault(),
479+
jsonSchemaValidator != null ? jsonSchemaValidator
480+
: DefaultMcpJsonSchemaValidatorSupplier.getDefaultJsonSchemaValidator(),
479481
asyncFeatures), this.contextProvider);
480482
}
481483

@@ -809,7 +811,7 @@ public AsyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching
809811
*/
810812
public McpAsyncClient build() {
811813
var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
812-
: JsonSchemaValidator.getDefault();
814+
: DefaultMcpJsonSchemaValidatorSupplier.getDefaultJsonSchemaValidator();
813815
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
814816
jsonSchemaValidator,
815817
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.modelcontextprotocol.common.McpTransportContext;
2525
import io.modelcontextprotocol.json.McpJsonMapper;
2626
import io.modelcontextprotocol.json.TypeRef;
27+
import io.modelcontextprotocol.json.internal.DefaultMcpJsonMapperSupplier;
2728
import io.modelcontextprotocol.spec.HttpHeaders;
2829
import io.modelcontextprotocol.spec.McpClientTransport;
2930
import io.modelcontextprotocol.spec.McpSchema;
@@ -327,7 +328,8 @@ public Builder connectTimeout(Duration connectTimeout) {
327328
public HttpClientSseClientTransport build() {
328329
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
329330
return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint,
330-
jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, httpRequestCustomizer);
331+
jsonMapper == null ? DefaultMcpJsonMapperSupplier.getDefaultMcpJsonMapper() : jsonMapper,
332+
httpRequestCustomizer);
331333
}
332334

333335
}

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
2626
import io.modelcontextprotocol.common.McpTransportContext;
2727
import io.modelcontextprotocol.json.McpJsonMapper;
28+
import io.modelcontextprotocol.json.internal.DefaultMcpJsonMapperSupplier;
2829
import io.modelcontextprotocol.json.TypeRef;
2930
import io.modelcontextprotocol.spec.ClosedMcpTransportSession;
3031
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
@@ -813,7 +814,8 @@ public Builder supportedProtocolVersions(List<String> supportedProtocolVersions)
813814
*/
814815
public HttpClientStreamableHttpTransport build() {
815816
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
816-
return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
817+
return new HttpClientStreamableHttpTransport(
818+
jsonMapper == null ? DefaultMcpJsonMapperSupplier.getDefaultMcpJsonMapper() : jsonMapper,
817819
httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
818820
httpRequestCustomizer, supportedProtocolVersions);
819821
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json;
6+
7+
import java.io.IOException;
8+
9+
/**
10+
* Abstraction for JSON serialization/deserialization to decouple the SDK from any
11+
* specific JSON library. A default implementation backed by Jackson is provided in
12+
* io.modelcontextprotocol.spec.json.jackson.JacksonJsonMapper.
13+
*/
14+
public interface McpJsonMapper {
15+
16+
/**
17+
* Deserialize JSON string into a target type.
18+
* @param content JSON as String
19+
* @param type target class
20+
* @return deserialized instance
21+
* @param <T> generic type
22+
* @throws IOException on parse errors
23+
*/
24+
<T> T readValue(String content, Class<T> type) throws IOException;
25+
26+
/**
27+
* Deserialize JSON bytes into a target type.
28+
* @param content JSON as bytes
29+
* @param type target class
30+
* @return deserialized instance
31+
* @param <T> generic type
32+
* @throws IOException on parse errors
33+
*/
34+
<T> T readValue(byte[] content, Class<T> type) throws IOException;
35+
36+
/**
37+
* Deserialize JSON string into a parameterized target type.
38+
* @param content JSON as String
39+
* @param type parameterized type reference
40+
* @return deserialized instance
41+
* @param <T> generic type
42+
* @throws IOException on parse errors
43+
*/
44+
<T> T readValue(String content, TypeRef<T> type) throws IOException;
45+
46+
/**
47+
* Deserialize JSON bytes into a parameterized target type.
48+
* @param content JSON as bytes
49+
* @param type parameterized type reference
50+
* @return deserialized instance
51+
* @param <T> generic type
52+
* @throws IOException on parse errors
53+
*/
54+
<T> T readValue(byte[] content, TypeRef<T> type) throws IOException;
55+
56+
/**
57+
* Convert a value to a given type, useful for mapping nested JSON structures.
58+
* @param fromValue source value
59+
* @param type target class
60+
* @return converted value
61+
* @param <T> generic type
62+
*/
63+
<T> T convertValue(Object fromValue, Class<T> type);
64+
65+
/**
66+
* Convert a value to a given parameterized type.
67+
* @param fromValue source value
68+
* @param type target type reference
69+
* @return converted value
70+
* @param <T> generic type
71+
*/
72+
<T> T convertValue(Object fromValue, TypeRef<T> type);
73+
74+
/**
75+
* Serialize an object to JSON string.
76+
* @param value object to serialize
77+
* @return JSON as String
78+
* @throws IOException on serialization errors
79+
*/
80+
String writeValueAsString(Object value) throws IOException;
81+
82+
/**
83+
* Serialize an object to JSON bytes.
84+
* @param value object to serialize
85+
* @return JSON as bytes
86+
* @throws IOException on serialization errors
87+
*/
88+
byte[] writeValueAsBytes(Object value) throws IOException;
89+
90+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json;
6+
7+
import java.util.function.Supplier;
8+
9+
/**
10+
* Strategy interface for resolving a {@link McpJsonMapper}.
11+
*/
12+
public interface McpJsonMapperSupplier extends Supplier<McpJsonMapper> {
13+
14+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json;
6+
7+
import java.lang.reflect.ParameterizedType;
8+
import java.lang.reflect.Type;
9+
10+
/**
11+
* Captures generic type information at runtime for parameterized JSON (de)serialization.
12+
* Usage: TypeRef<List<Foo>> ref = new TypeRef<>(){};
13+
*/
14+
public abstract class TypeRef<T> {
15+
16+
private final Type type;
17+
18+
/**
19+
* Constructs a new TypeRef instance, capturing the generic type information of the
20+
* subclass. This constructor should be called from an anonymous subclass to capture
21+
* the actual type arguments. For example: <pre>
22+
* TypeRef&lt;List&lt;Foo&gt;&gt; ref = new TypeRef&lt;&gt;(){};
23+
* </pre>
24+
* @throws IllegalStateException if TypeRef is not subclassed with actual type
25+
* information
26+
*/
27+
protected TypeRef() {
28+
Type superClass = getClass().getGenericSuperclass();
29+
if (superClass instanceof Class) {
30+
throw new IllegalStateException("TypeRef constructed without actual type information");
31+
}
32+
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
33+
}
34+
35+
/**
36+
* Returns the captured type information.
37+
* @return the Type representing the actual type argument captured by this TypeRef
38+
* instance
39+
*/
40+
public Type getType() {
41+
return type;
42+
}
43+
44+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.modelcontextprotocol.json.internal;
2+
3+
import java.util.Optional;
4+
import java.util.ServiceConfigurationError;
5+
import java.util.ServiceLoader;
6+
7+
import io.modelcontextprotocol.json.McpJsonMapper;
8+
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
9+
10+
public class DefaultMcpJsonMapperSupplier {
11+
12+
private static McpJsonMapperSupplier jsonMapperSupplier;
13+
14+
private static McpJsonMapper defaultJsonMapper;
15+
16+
void setMcpJsonMapperSupplier(McpJsonMapperSupplier supplier) {
17+
jsonMapperSupplier = supplier;
18+
}
19+
20+
void unsetMcpJsonMapperSupplier(McpJsonMapperSupplier supplier) {
21+
jsonMapperSupplier = null;
22+
defaultJsonMapper = null;
23+
}
24+
25+
public synchronized static McpJsonMapper getDefaultMcpJsonMapper() {
26+
if (defaultJsonMapper == null) {
27+
if (jsonMapperSupplier == null) {
28+
// Use serviceloader
29+
Optional<McpJsonMapperSupplier> sl = ServiceLoader.load(McpJsonMapperSupplier.class).findFirst();
30+
if (sl.isEmpty()) {
31+
throw new ServiceConfigurationError("No JsonMapperSupplier available for creating McpJsonMapper");
32+
}
33+
jsonMapperSupplier = sl.get();
34+
}
35+
defaultJsonMapper = jsonMapperSupplier.get();
36+
}
37+
return defaultJsonMapper;
38+
}
39+
40+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.modelcontextprotocol.json.internal;
2+
3+
import java.util.Optional;
4+
import java.util.ServiceConfigurationError;
5+
import java.util.ServiceLoader;
6+
7+
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
8+
import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier;
9+
10+
public class DefaultMcpJsonSchemaValidatorSupplier {
11+
12+
private static JsonSchemaValidatorSupplier jsonSchemaValidatorSupplier;
13+
14+
private static JsonSchemaValidator defaultJsonSchemaValidator;
15+
16+
void setJsonSchemaValidatorSupplier(JsonSchemaValidatorSupplier supplier) {
17+
jsonSchemaValidatorSupplier = supplier;
18+
}
19+
20+
void unsetJsonSchemaValidatorSupplier(JsonSchemaValidatorSupplier supplier) {
21+
jsonSchemaValidatorSupplier = null;
22+
defaultJsonSchemaValidator = null;
23+
}
24+
25+
public synchronized static JsonSchemaValidator getDefaultJsonSchemaValidator() {
26+
if (defaultJsonSchemaValidator == null) {
27+
if (jsonSchemaValidatorSupplier == null) {
28+
// Use serviceloader
29+
Optional<JsonSchemaValidatorSupplier> sl = ServiceLoader.load(JsonSchemaValidatorSupplier.class)
30+
.findFirst();
31+
if (sl.isEmpty()) {
32+
throw new ServiceConfigurationError(
33+
"No JsonSchemaValidatorSupplier available for creating JsonSchemaValidator");
34+
}
35+
jsonSchemaValidatorSupplier = sl.get();
36+
}
37+
defaultJsonSchemaValidator = jsonSchemaValidatorSupplier.get();
38+
}
39+
return defaultJsonSchemaValidator;
40+
}
41+
42+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*/
4+
package io.modelcontextprotocol.json.schema;
5+
6+
import java.util.Map;
7+
8+
/**
9+
* Interface for validating structured content against a JSON schema. This interface
10+
* defines a method to validate structured content based on the provided output schema.
11+
*
12+
* @author Christian Tzolov
13+
*/
14+
public interface JsonSchemaValidator {
15+
16+
/**
17+
* Represents the result of a validation operation.
18+
*
19+
* @param valid Indicates whether the validation was successful.
20+
* @param errorMessage An error message if the validation failed, otherwise null.
21+
* @param jsonStructuredOutput The text structured content in JSON format if the
22+
* validation was successful, otherwise null.
23+
*/
24+
record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) {
25+
26+
public static ValidationResponse asValid(String jsonStructuredOutput) {
27+
return new ValidationResponse(true, null, jsonStructuredOutput);
28+
}
29+
30+
public static ValidationResponse asInvalid(String message) {
31+
return new ValidationResponse(false, message, null);
32+
}
33+
}
34+
35+
/**
36+
* Validates the structured content against the provided JSON schema.
37+
* @param schema The JSON schema to validate against.
38+
* @param structuredContent The structured content to validate.
39+
* @return A ValidationResponse indicating whether the validation was successful or
40+
* not.
41+
*/
42+
ValidationResponse validate(Map<String, Object> schema, Object structuredContent);
43+
44+
}

0 commit comments

Comments
 (0)