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:
parent
fcdc0d4c14
commit
95203233c8
13 changed files with 72 additions and 45 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue