From 10d1c1e59b6d69dd1770f163c47a151c36297991 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Fri, 11 Apr 2025 13:56:12 -0400 Subject: [PATCH 1/2] feat(schema): add support for JSON Schema $defs and definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added support for $defs and definitions properties in JsonSchema record to handle JSON Schema references properly. Added tests to verify both formats work correctly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../modelcontextprotocol/spec/McpSchema.java | 4 +- .../spec/McpSchemaTests.java | 145 ++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index e621ac19..1f9e2df9 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -691,7 +691,9 @@ public record JsonSchema( // @formatter:off @JsonProperty("type") String type, @JsonProperty("properties") Map properties, @JsonProperty("required") List required, - @JsonProperty("additionalProperties") Boolean additionalProperties) { + @JsonProperty("additionalProperties") Boolean additionalProperties, + @JsonProperty("$defs") Map defs, + @JsonProperty("definitions") Map definitions) { } // @formatter:on /** diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index a41fc095..7c22a356 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; @@ -449,6 +450,106 @@ void testGetPromptResult() throws Exception { // Tool Tests + @Test + void testJsonSchema() throws Exception { + String schemaJson = """ + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address": { + "$ref": "#/$defs/Address" + } + }, + "required": ["name"], + "$defs": { + "Address": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"} + }, + "required": ["street", "city"] + } + } + } + """; + + McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class); + + assertThat(schema.type()).isEqualTo("object"); + assertThat(schema.properties()).containsKeys("name", "address"); + assertThat(schema.required()).containsExactly("name"); + assertThat(schema.defs()).isNotNull(); + assertThat(schema.defs()).containsKey("Address"); + + String value = mapper.writeValueAsString(schema); + + // Convert to map for easier assertions + Map jsonMap = mapper.readValue(value, new TypeReference>() { + }); + Map defs = (Map) jsonMap.get("$defs"); + Map address = (Map) defs.get("Address"); + + assertThat(address).containsEntry("type", "object"); + assertThat(((Map) ((Map) address.get("properties")).get("street")).get("type")) + .isEqualTo("string"); + assertThat(((Map) ((Map) address.get("properties")).get("city")).get("type")) + .isEqualTo("string"); + } + + @Test + void testJsonSchemaWithDefinitions() throws Exception { + String schemaJson = """ + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address": { + "$ref": "#/definitions/Address" + } + }, + "required": ["name"], + "definitions": { + "Address": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"} + }, + "required": ["street", "city"] + } + } + } + """; + + McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class); + + assertThat(schema.type()).isEqualTo("object"); + assertThat(schema.properties()).containsKeys("name", "address"); + assertThat(schema.required()).containsExactly("name"); + assertThat(schema.definitions()).isNotNull(); + assertThat(schema.definitions()).containsKey("Address"); + + String value = mapper.writeValueAsString(schema); + + // Convert to map for easier assertions + Map jsonMap = mapper.readValue(value, new TypeReference>() { + }); + Map definitions = (Map) jsonMap.get("definitions"); + Map address = (Map) definitions.get("Address"); + + assertThat(address).containsEntry("type", "object"); + assertThat(((Map) ((Map) address.get("properties")).get("street")).get("type")) + .isEqualTo("string"); + assertThat(((Map) ((Map) address.get("properties")).get("city")).get("type")) + .isEqualTo("string"); + } + @Test void testTool() throws Exception { String schemaJson = """ @@ -477,6 +578,50 @@ void testTool() throws Exception { {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}""")); } + @Test + void testToolWithComplexSchema() throws Exception { + String complexSchemaJson = """ + { + "type": "object", + "$defs": { + "Address": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"} + }, + "required": ["street", "city"] + } + }, + "properties": { + "name": {"type": "string"}, + "shippingAddress": {"$ref": "#/$defs/Address"} + }, + "required": ["name", "shippingAddress"] + } + """; + + McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", complexSchemaJson); + + // Verify the schema was properly parsed and stored with $defs + assertThat(tool.inputSchema().defs()).isNotNull(); + assertThat(tool.inputSchema().defs()).containsKey("Address"); + + String value = mapper.writeValueAsString(tool); + + // Convert to map for easier assertions + Map jsonMap = mapper.readValue(value, new TypeReference>() { + }); + Map inputSchema = (Map) jsonMap.get("inputSchema"); + Map defs = (Map) inputSchema.get("$defs"); + Map address = (Map) defs.get("Address"); + Map properties = (Map) inputSchema.get("properties"); + Map shippingAddress = (Map) properties.get("shippingAddress"); + + assertThat(address).containsEntry("type", "object"); + assertThat(shippingAddress).containsEntry("$ref", "#/$defs/Address"); + } + @Test void testCallToolRequest() throws Exception { Map arguments = new HashMap<>(); From c7f96c26539c3712c6c6c7ff3ec6a8a23845bf50 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Tue, 15 Apr 2025 21:55:18 -0400 Subject: [PATCH 2/2] refactor(tests): simplify JsonSchema test assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed the JsonSchema test approach to use serialization/deserialization round-trip validation instead of property-by-property assertions. This makes tests more maintainable and less likely to break when new properties are added. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../spec/McpSchemaTests.java | 78 ++++++++----------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 7c22a356..ff78c1bf 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -477,27 +477,20 @@ void testJsonSchema() throws Exception { } """; + // Deserialize the original string to a JsonSchema object McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class); - assertThat(schema.type()).isEqualTo("object"); - assertThat(schema.properties()).containsKeys("name", "address"); - assertThat(schema.required()).containsExactly("name"); - assertThat(schema.defs()).isNotNull(); - assertThat(schema.defs()).containsKey("Address"); + // Serialize the object back to a string + String serialized = mapper.writeValueAsString(schema); - String value = mapper.writeValueAsString(schema); + // Deserialize again + McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class); - // Convert to map for easier assertions - Map jsonMap = mapper.readValue(value, new TypeReference>() { - }); - Map defs = (Map) jsonMap.get("$defs"); - Map address = (Map) defs.get("Address"); + // Serialize one more time and compare with the first serialization + String serializedAgain = mapper.writeValueAsString(deserialized); - assertThat(address).containsEntry("type", "object"); - assertThat(((Map) ((Map) address.get("properties")).get("street")).get("type")) - .isEqualTo("string"); - assertThat(((Map) ((Map) address.get("properties")).get("city")).get("type")) - .isEqualTo("string"); + // The two serialized strings should be the same + assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized)); } @Test @@ -527,27 +520,20 @@ void testJsonSchemaWithDefinitions() throws Exception { } """; + // Deserialize the original string to a JsonSchema object McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class); - assertThat(schema.type()).isEqualTo("object"); - assertThat(schema.properties()).containsKeys("name", "address"); - assertThat(schema.required()).containsExactly("name"); - assertThat(schema.definitions()).isNotNull(); - assertThat(schema.definitions()).containsKey("Address"); + // Serialize the object back to a string + String serialized = mapper.writeValueAsString(schema); - String value = mapper.writeValueAsString(schema); + // Deserialize again + McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class); - // Convert to map for easier assertions - Map jsonMap = mapper.readValue(value, new TypeReference>() { - }); - Map definitions = (Map) jsonMap.get("definitions"); - Map address = (Map) definitions.get("Address"); + // Serialize one more time and compare with the first serialization + String serializedAgain = mapper.writeValueAsString(deserialized); - assertThat(address).containsEntry("type", "object"); - assertThat(((Map) ((Map) address.get("properties")).get("street")).get("type")) - .isEqualTo("string"); - assertThat(((Map) ((Map) address.get("properties")).get("city")).get("type")) - .isEqualTo("string"); + // The two serialized strings should be the same + assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized)); } @Test @@ -603,23 +589,21 @@ void testToolWithComplexSchema() throws Exception { McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", complexSchemaJson); - // Verify the schema was properly parsed and stored with $defs - assertThat(tool.inputSchema().defs()).isNotNull(); - assertThat(tool.inputSchema().defs()).containsKey("Address"); + // Serialize the tool to a string + String serialized = mapper.writeValueAsString(tool); - String value = mapper.writeValueAsString(tool); + // Deserialize back to a Tool object + McpSchema.Tool deserializedTool = mapper.readValue(serialized, McpSchema.Tool.class); + + // Serialize again and compare with first serialization + String serializedAgain = mapper.writeValueAsString(deserializedTool); + + // The two serialized strings should be the same + assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized)); - // Convert to map for easier assertions - Map jsonMap = mapper.readValue(value, new TypeReference>() { - }); - Map inputSchema = (Map) jsonMap.get("inputSchema"); - Map defs = (Map) inputSchema.get("$defs"); - Map address = (Map) defs.get("Address"); - Map properties = (Map) inputSchema.get("properties"); - Map shippingAddress = (Map) properties.get("shippingAddress"); - - assertThat(address).containsEntry("type", "object"); - assertThat(shippingAddress).containsEntry("$ref", "#/$defs/Address"); + // Just verify the basic structure was preserved + assertThat(deserializedTool.inputSchema().defs()).isNotNull(); + assertThat(deserializedTool.inputSchema().defs()).containsKey("Address"); } @Test