fix: Remove JsonSchema and use a Map for inputSchema to support json schemas dialect (#749)

* feat: remove JsonSchema an use a Map for inputSchema

- Fixes #886

Co-authored-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Bilal Oumehdi 2026-04-10 10:32:22 +01:00 • committed by GitHub
commit 95203233c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 72 additions and 45 deletions

View file

@ -20,7 +20,6 @@ import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
import io.modelcontextprotocol.spec.McpSchema.EmbeddedResource;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.ImageContent;
import io.modelcontextprotocol.spec.McpSchema.JsonSchema;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.ProgressNotification;
@ -51,8 +50,8 @@ public class ConformanceServlet {
private static final String MCP_ENDPOINT = "/mcp";
private static final JsonSchema EMPTY_JSON_SCHEMA = new JsonSchema("object", Collections.emptyMap(), null, null,
null, null);
private static final Map<String, Object> EMPTY_JSON_SCHEMA = Map.of("type", "object", "properties",
Collections.emptyMap());
// Minimal 1x1 red pixel PNG (base64 encoded)
private static final String RED_PIXEL_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
@ -326,10 +325,10 @@ public class ConformanceServlet {
.tool(Tool.builder()
.name("test_sampling")
.description("Tool that requests LLM sampling from client")
.inputSchema(new JsonSchema("object",
.inputSchema(Map.of("type", "object", "properties",
Map.of("prompt",
Map.of("type", "string", "description", "The prompt to send to the LLM")),
List.of("prompt"), null, null, null))
"required", List.of("prompt")))
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_sampling' called");
@ -355,10 +354,10 @@ public class ConformanceServlet {
.tool(Tool.builder()
.name("test_elicitation")
.description("Tool that requests user input from client")
.inputSchema(new JsonSchema("object",
.inputSchema(Map.of("type", "object", "properties",
Map.of("message",
Map.of("type", "string", "description", "The message to show the user")),
List.of("message"), null, null, null))
"required", List.of("message")))
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation' called");

View file

@ -1307,7 +1307,9 @@ public final class McpSchema {
* @param additionalProperties Whether additional properties are allowed
* @param defs Schema definitions using the newer $defs keyword
* @param definitions Schema definitions using the legacy definitions keyword
* @deprecated use {@link Map} instead.
*/
@Deprecated
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record JsonSchema( // @formatter:off
@ -1363,7 +1365,7 @@ public final class McpSchema {
@JsonProperty("name") String name,
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("inputSchema") JsonSchema inputSchema,
@JsonProperty("inputSchema") Map<String, Object> inputSchema,
@JsonProperty("outputSchema") Map<String, Object> outputSchema,
@JsonProperty("annotations") ToolAnnotations annotations,
@JsonProperty("_meta") Map<String, Object> meta) { // @formatter:on
@ -1380,7 +1382,7 @@ public final class McpSchema {
private String description;
private JsonSchema inputSchema;
private Map<String, Object> inputSchema;
private Map<String, Object> outputSchema;
@ -1403,13 +1405,34 @@ public final class McpSchema {
return this;
}
/**
* @deprecated use {@link #inputSchema(Map)} instead.
*/
@Deprecated
public Builder inputSchema(JsonSchema inputSchema) {
Map<String, Object> schema = new HashMap<>();
if (inputSchema.type() != null)
schema.put("type", inputSchema.type());
if (inputSchema.properties() != null)
schema.put("properties", inputSchema.properties());
if (inputSchema.required() != null)
schema.put("required", inputSchema.required());
if (inputSchema.additionalProperties() != null)
schema.put("additionalProperties", inputSchema.additionalProperties());
if (inputSchema.defs() != null)
schema.put("$defs", inputSchema.defs());
if (inputSchema.definitions() != null)
schema.put("definitions", inputSchema.definitions());
return inputSchema(schema);
}
public Builder inputSchema(Map<String, Object> inputSchema) {
this.inputSchema = inputSchema;
return this;
}
public Builder inputSchema(McpJsonMapper jsonMapper, String inputSchema) {
this.inputSchema = parseSchema(jsonMapper, inputSchema);
this.inputSchema = schemaToMap(jsonMapper, inputSchema);
return this;
}
@ -1450,15 +1473,6 @@ public final class McpSchema {
}
}
private static JsonSchema parseSchema(McpJsonMapper jsonMapper, String schema) {
try {
return jsonMapper.readValue(schema, JsonSchema.class);
}
catch (IOException e) {
throw new IllegalArgumentException("Invalid schema: " + schema, e);
}
}
/**
* Used by the client to call a tool provided by the server.
*

View file

@ -4,6 +4,8 @@
package io.modelcontextprotocol.server;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.util.List;
import java.util.Map;
@ -25,7 +27,6 @@ import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

View file

@ -4,6 +4,8 @@
package io.modelcontextprotocol.server;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.util.List;
import java.util.Map;
@ -22,7 +24,6 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

View file

@ -1,15 +1,14 @@
package io.modelcontextprotocol.util;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.Collections;
import java.util.Map;
public final class ToolsUtils {
private ToolsUtils() {
}
public static final McpSchema.JsonSchema EMPTY_JSON_SCHEMA = new McpSchema.JsonSchema("object",
Collections.emptyMap(), null, null, null, null);
public static final Map<String, Object> EMPTY_JSON_SCHEMA = Map.of("type", "object", "properties",
Collections.emptyMap());
}

View file

@ -4,6 +4,8 @@
package io.modelcontextprotocol;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@ -56,7 +58,6 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;

View file

@ -4,6 +4,8 @@
package io.modelcontextprotocol;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@ -32,7 +34,6 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;

View file

@ -4,8 +4,11 @@
package io.modelcontextprotocol.server;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
@ -25,7 +28,6 @@ import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

View file

@ -4,7 +4,10 @@
package io.modelcontextprotocol.server;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
@ -20,7 +23,6 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

View file

@ -1,15 +1,14 @@
package io.modelcontextprotocol.util;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.Collections;
import java.util.Map;
public final class ToolsUtils {
private ToolsUtils() {
}
public static final McpSchema.JsonSchema EMPTY_JSON_SCHEMA = new McpSchema.JsonSchema("object",
Collections.emptyMap(), null, null, null, null);
public static final Map<String, Object> EMPTY_JSON_SCHEMA = Map.of("type", "object", "properties",
Collections.emptyMap());
}

View file

@ -44,11 +44,10 @@ class McpAsyncClientTests {
Map<String, Object> inputSchemaMap = Map.of("type", "object", "properties",
Map.of("expression", Map.of("type", "string")), "required", List.of("expression"));
McpSchema.JsonSchema inputSchema = new McpSchema.JsonSchema("object", inputSchemaMap, null, null, null, null);
McpSchema.Tool.Builder toolBuilder = McpSchema.Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.inputSchema(inputSchema);
.inputSchema(inputSchemaMap);
if (hasOutputSchema) {
Map<String, Object> outputSchema = Map.of("type", "object", "properties",

View file

@ -4,6 +4,8 @@
package io.modelcontextprotocol.server;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import java.time.Duration;
import java.util.List;
import java.util.Map;
@ -48,7 +50,6 @@ import org.springframework.web.client.RestClient;
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;

View file

@ -21,6 +21,7 @@ import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import io.modelcontextprotocol.json.TypeRef;
import net.javacrumbs.jsonunit.core.Option;
/**
@ -713,13 +714,15 @@ public class McpSchemaTests {
""";
// Deserialize the original string to a JsonSchema object
McpSchema.JsonSchema schema = JSON_MAPPER.readValue(schemaJson, McpSchema.JsonSchema.class);
Map<String, Object> schema = JSON_MAPPER.readValue(schemaJson, new TypeRef<HashMap<String, Object>>() {
});
// Serialize the object back to a string
String serialized = JSON_MAPPER.writeValueAsString(schema);
// Deserialize again
McpSchema.JsonSchema deserialized = JSON_MAPPER.readValue(serialized, McpSchema.JsonSchema.class);
Map<String, Object> deserialized = JSON_MAPPER.readValue(serialized, new TypeRef<HashMap<String, Object>>() {
});
// Serialize one more time and compare with the first serialization
String serializedAgain = JSON_MAPPER.writeValueAsString(deserialized);
@ -756,13 +759,15 @@ public class McpSchemaTests {
""";
// Deserialize the original string to a JsonSchema object
McpSchema.JsonSchema schema = JSON_MAPPER.readValue(schemaJson, McpSchema.JsonSchema.class);
Map<String, Object> schema = JSON_MAPPER.readValue(schemaJson, new TypeRef<HashMap<String, Object>>() {
});
// Serialize the object back to a string
String serialized = JSON_MAPPER.writeValueAsString(schema);
// Deserialize again
McpSchema.JsonSchema deserialized = JSON_MAPPER.readValue(serialized, McpSchema.JsonSchema.class);
Map<String, Object> deserialized = JSON_MAPPER.readValue(serialized, new TypeRef<HashMap<String, Object>>() {
});
// Serialize one more time and compare with the first serialization
String serializedAgain = JSON_MAPPER.writeValueAsString(deserialized);
@ -845,8 +850,11 @@ public class McpSchemaTests {
assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
// Just verify the basic structure was preserved
assertThat(deserializedTool.inputSchema().defs()).isNotNull();
assertThat(deserializedTool.inputSchema().defs()).containsKey("Address");
assertThat(deserializedTool.inputSchema()).containsKey("$defs")
.extractingByKey("$defs")
.isNotNull()
.asInstanceOf(InstanceOfAssertFactories.MAP)
.containsKey("Address");
}
@Test
@ -866,14 +874,14 @@ public class McpSchemaTests {
}
""";
McpSchema.JsonSchema schema = JSON_MAPPER.readValue(schemaJson, McpSchema.JsonSchema.class);
Map<String, Object> inputSchema = Map.of("inputSchema", schemaJson);
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.Tool tool = McpSchema.Tool.builder()
.name("addressTool")
.title("addressTool")
.description("Handles addresses")
.inputSchema(schema)
.inputSchema(inputSchema)
.meta(meta)
.build();
@ -1114,7 +1122,7 @@ public class McpSchemaTests {
assertThat(tool.name()).isEqualTo("test-tool");
assertThat(tool.description()).isEqualTo("A test tool");
assertThat(tool.inputSchema()).isNotNull();
assertThat(tool.inputSchema().type()).isEqualTo("object");
assertThat(tool.inputSchema().get("type")).isEqualTo("object");
assertThat(tool.outputSchema()).isNotNull();
assertThat(tool.outputSchema()).containsKey("type");
assertThat(tool.outputSchema().get("type")).isEqualTo("object");