From 862022a77961e02317e7b15d49a6caa5e858ad06 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sat, 1 Mar 2025 12:57:26 +0100 Subject: [PATCH 1/3] refactor(mcp): remove redundant type field from Content implementations - The type field and associated methods were redundant in Content implementations (TextContent, ImageContent, EmbeddedResource) as the type information is already handled by Jackson's polymorphic type handling via @JsonSubTypes annotation. - Added comprehensive unit tests for McpSchema to validate serialization/deserialization behavior of all schema components. Resolves #26 Resolve https://github.com/spring-projects/spring-ai/issues/2350 Signed-off-by: Christian Tzolov --- .../modelcontextprotocol/spec/McpSchema.java | 32 +- .../spec/McpSchemaTests.java | 458 ++++++++++++++++++ 2 files changed, 461 insertions(+), 29 deletions(-) create mode 100644 mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index e721468d..60ef5643 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -964,7 +965,7 @@ public record CompleteCompletion(// @formatter:off @JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") }) public sealed interface Content permits TextContent, ImageContent, EmbeddedResource { - String type(); + // String type(); } @@ -972,19 +973,10 @@ public sealed interface Content permits TextContent, ImageContent, EmbeddedResou public record TextContent( // @formatter:off @JsonProperty("audience") List audience, @JsonProperty("priority") Double priority, - @JsonProperty("type") String type, @JsonProperty("text") String text) implements Content { // @formatter:on - public TextContent { - type = "text"; - } - - public String type() { - return type; - } - public TextContent(String content) { - this(null, null, "text", content); + this(null, null, content); } } @@ -992,33 +984,15 @@ public TextContent(String content) { public record ImageContent( // @formatter:off @JsonProperty("audience") List audience, @JsonProperty("priority") Double priority, - @JsonProperty("type") String type, @JsonProperty("data") String data, @JsonProperty("mimeType") String mimeType) implements Content { // @formatter:on - - public ImageContent { - type = "image"; - } - - public String type() { - return type; - } } @JsonInclude(JsonInclude.Include.NON_ABSENT) public record EmbeddedResource( // @formatter:off @JsonProperty("audience") List audience, @JsonProperty("priority") Double priority, - @JsonProperty("type") String type, @JsonProperty("resource") ResourceContents resource) implements Content { // @formatter:on - - public EmbeddedResource { - type = "resource"; - } - - public String type() { - return type; - } } // --------------------------- diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java new file mode 100644 index 00000000..51e20eab --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -0,0 +1,458 @@ +/* +* Copyright 2025 - 2025 the original author or authors. +*/ +package io.modelcontextprotocol.spec; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Christian Tzolov + */ +public class McpSchemaTests { + + ObjectMapper mapper = new ObjectMapper(); + + // Content Types Tests + + @Test + void testTextContent() throws Exception { + McpSchema.TextContent test = new McpSchema.TextContent("XXX"); + String value = mapper.writeValueAsString(test); + assertEquals(""" + {"type":"text","text":"XXX"}""", value); + } + + @Test + void testImageContent() throws Exception { + McpSchema.ImageContent test = new McpSchema.ImageContent(null, null, "base64encodeddata", "image/png"); + String value = mapper.writeValueAsString(test); + assertEquals(""" + {"type":"image","data":"base64encodeddata","mimeType":"image/png"}""", value); + } + + @Test + void testEmbeddedResource() throws Exception { + McpSchema.TextResourceContents resourceContents = new McpSchema.TextResourceContents("resource://test", + "text/plain", "Sample resource content"); + + McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); + + String value = mapper.writeValueAsString(test); + assertEquals( + """ + {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}""", + value); + } + + @Test + void testEmbeddedResourceWithBlobContents() throws Exception { + McpSchema.BlobResourceContents resourceContents = new McpSchema.BlobResourceContents("resource://test", + "application/octet-stream", "base64encodedblob"); + + McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); + + String value = mapper.writeValueAsString(test); + assertEquals( + """ + {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}""", + value); + } + + // JSON-RPC Message Types Tests + + @Test + void testJSONRPCRequest() throws Exception { + Map params = new HashMap<>(); + params.put("key", "value"); + + McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "method_name", 1, + params); + + String value = mapper.writeValueAsString(request); + assertEquals(""" + {"jsonrpc":"2.0","method":"method_name","id":1,"params":{"key":"value"}}""", value); + } + + @Test + void testJSONRPCNotification() throws Exception { + Map params = new HashMap<>(); + params.put("key", "value"); + + McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, + "notification_method", params); + + String value = mapper.writeValueAsString(notification); + assertEquals(""" + {"jsonrpc":"2.0","method":"notification_method","params":{"key":"value"}}""", value); + } + + @Test + void testJSONRPCResponse() throws Exception { + Map result = new HashMap<>(); + result.put("result_key", "result_value"); + + McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, result, null); + + String value = mapper.writeValueAsString(response); + assertEquals(""" + {"jsonrpc":"2.0","id":1,"result":{"result_key":"result_value"}}""", value); + } + + @Test + void testJSONRPCResponseWithError() throws Exception { + McpSchema.JSONRPCResponse.JSONRPCError error = new McpSchema.JSONRPCResponse.JSONRPCError( + McpSchema.ErrorCodes.INVALID_REQUEST, "Invalid request", null); + + McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, null, error); + + String value = mapper.writeValueAsString(response); + assertEquals(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}""", value); + } + + // Initialization Tests + + @Test + void testInitializeRequest() throws Exception { + McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder() + .roots(true) + .sampling() + .build(); + + McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); + + McpSchema.InitializeRequest request = new McpSchema.InitializeRequest(McpSchema.LATEST_PROTOCOL_VERSION, + capabilities, clientInfo); + + String value = mapper.writeValueAsString(request); + assertEquals( + """ + {"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}""", + value); + } + + @Test + void testInitializeResult() throws Exception { + McpSchema.ServerCapabilities capabilities = McpSchema.ServerCapabilities.builder() + .logging() + .prompts(true) + .resources(true, true) + .tools(true) + .build(); + + McpSchema.Implementation serverInfo = new McpSchema.Implementation("test-server", "1.0.0"); + + McpSchema.InitializeResult result = new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, + capabilities, serverInfo, "Server initialized successfully"); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"test-server","version":"1.0.0"},"instructions":"Server initialized successfully"}""", + value); + } + + // Resource Tests + + @Test + void testResource() throws Exception { + McpSchema.Annotations annotations = new McpSchema.Annotations( + Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), 0.8); + + McpSchema.Resource resource = new McpSchema.Resource("resource://test", "Test Resource", "A test resource", + "text/plain", annotations); + + String value = mapper.writeValueAsString(resource); + assertEquals( + """ + {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}""", + value); + } + + @Test + void testResourceTemplate() throws Exception { + McpSchema.Annotations annotations = new McpSchema.Annotations(Arrays.asList(McpSchema.Role.USER), 0.5); + + McpSchema.ResourceTemplate template = new McpSchema.ResourceTemplate("resource://{param}/test", "Test Template", + "A test resource template", "text/plain", annotations); + + String value = mapper.writeValueAsString(template); + assertEquals( + """ + {"uriTemplate":"resource://{param}/test","name":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}""", + value); + } + + @Test + void testListResourcesResult() throws Exception { + McpSchema.Resource resource1 = new McpSchema.Resource("resource://test1", "Test Resource 1", + "First test resource", "text/plain", null); + + McpSchema.Resource resource2 = new McpSchema.Resource("resource://test2", "Test Resource 2", + "Second test resource", "application/json", null); + + McpSchema.ListResourcesResult result = new McpSchema.ListResourcesResult(Arrays.asList(resource1, resource2), + "next-cursor"); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"resources":[{"uri":"resource://test1","name":"Test Resource 1","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}""", + value); + } + + @Test + void testListResourceTemplatesResult() throws Exception { + McpSchema.ResourceTemplate template1 = new McpSchema.ResourceTemplate("resource://{param}/test1", + "Test Template 1", "First test template", "text/plain", null); + + McpSchema.ResourceTemplate template2 = new McpSchema.ResourceTemplate("resource://{param}/test2", + "Test Template 2", "Second test template", "application/json", null); + + McpSchema.ListResourceTemplatesResult result = new McpSchema.ListResourceTemplatesResult( + Arrays.asList(template1, template2), "next-cursor"); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"resourceTemplates":[{"uriTemplate":"resource://{param}/test1","name":"Test Template 1","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}""", + value); + } + + @Test + void testReadResourceRequest() throws Exception { + McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test"); + + String value = mapper.writeValueAsString(request); + assertEquals(""" + {"uri":"resource://test"}""", value); + } + + @Test + void testReadResourceResult() throws Exception { + McpSchema.TextResourceContents contents1 = new McpSchema.TextResourceContents("resource://test1", "text/plain", + "Sample text content"); + + McpSchema.BlobResourceContents contents2 = new McpSchema.BlobResourceContents("resource://test2", + "application/octet-stream", "base64encodedblob"); + + McpSchema.ReadResourceResult result = new McpSchema.ReadResourceResult(Arrays.asList(contents1, contents2)); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"contents":[{"uri":"resource://test1","mimeType":"text/plain","text":"Sample text content"},{"uri":"resource://test2","mimeType":"application/octet-stream","blob":"base64encodedblob"}]}""", + value); + } + + // Prompt Tests + + @Test + void testPrompt() throws Exception { + McpSchema.PromptArgument arg1 = new McpSchema.PromptArgument("arg1", "First argument", true); + + McpSchema.PromptArgument arg2 = new McpSchema.PromptArgument("arg2", "Second argument", false); + + McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "A test prompt", Arrays.asList(arg1, arg2)); + + String value = mapper.writeValueAsString(prompt); + assertEquals( + """ + {"name":"test-prompt","description":"A test prompt","arguments":[{"name":"arg1","description":"First argument","required":true},{"name":"arg2","description":"Second argument","required":false}]}""", + value); + } + + @Test + void testPromptMessage() throws Exception { + McpSchema.TextContent content = new McpSchema.TextContent("Hello, world!"); + + McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content); + + String value = mapper.writeValueAsString(message); + assertEquals(""" + {"role":"user","content":{"type":"text","text":"Hello, world!"}}""", value); + } + + @Test + void testListPromptsResult() throws Exception { + McpSchema.PromptArgument arg = new McpSchema.PromptArgument("arg", "An argument", true); + + McpSchema.Prompt prompt1 = new McpSchema.Prompt("prompt1", "First prompt", Collections.singletonList(arg)); + + McpSchema.Prompt prompt2 = new McpSchema.Prompt("prompt2", "Second prompt", Collections.emptyList()); + + McpSchema.ListPromptsResult result = new McpSchema.ListPromptsResult(Arrays.asList(prompt1, prompt2), + "next-cursor"); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"prompts":[{"name":"prompt1","description":"First prompt","arguments":[{"name":"arg","description":"An argument","required":true}]},{"name":"prompt2","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}""", + value); + } + + @Test + void testGetPromptRequest() throws Exception { + Map arguments = new HashMap<>(); + arguments.put("arg1", "value1"); + arguments.put("arg2", 42); + + McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("test-prompt", arguments); + + String value = mapper.writeValueAsString(request); + + // Use Jackson to parse both the expected and actual JSON to compare them as + // objects + // This ignores the order of properties in JSON objects + assertEquals(mapper.readTree(""" + {"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}"""), mapper.readTree(value)); + } + + @Test + void testGetPromptResult() throws Exception { + McpSchema.TextContent content1 = new McpSchema.TextContent("System message"); + McpSchema.TextContent content2 = new McpSchema.TextContent("User message"); + + McpSchema.PromptMessage message1 = new McpSchema.PromptMessage(McpSchema.Role.ASSISTANT, content1); + + McpSchema.PromptMessage message2 = new McpSchema.PromptMessage(McpSchema.Role.USER, content2); + + McpSchema.GetPromptResult result = new McpSchema.GetPromptResult("A test prompt result", + Arrays.asList(message1, message2)); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"description":"A test prompt result","messages":[{"role":"assistant","content":{"type":"text","text":"System message"}},{"role":"user","content":{"type":"text","text":"User message"}}]}""", + value); + } + + // Tool Tests + + @Test + void testTool() throws Exception { + String schemaJson = """ + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "number" + } + }, + "required": ["name"] + } + """; + + McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson); + + String value = mapper.writeValueAsString(tool); + assertEquals( + """ + {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}""", + value); + } + + @Test + void testCallToolRequest() throws Exception { + Map arguments = new HashMap<>(); + arguments.put("name", "test"); + arguments.put("value", 42); + + McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", arguments); + + String value = mapper.writeValueAsString(request); + assertEquals(""" + {"name":"test-tool","arguments":{"name":"test","value":42}}""", value); + } + + @Test + void testCallToolResult() throws Exception { + McpSchema.TextContent content = new McpSchema.TextContent("Tool execution result"); + + McpSchema.CallToolResult result = new McpSchema.CallToolResult(Collections.singletonList(content), false); + + String value = mapper.writeValueAsString(result); + assertEquals(""" + {"content":[{"type":"text","text":"Tool execution result"}],"isError":false}""", value); + } + + // Sampling Tests + + @Test + void testCreateMessageRequest() throws Exception { + McpSchema.TextContent content = new McpSchema.TextContent("User message"); + + McpSchema.SamplingMessage message = new McpSchema.SamplingMessage(McpSchema.Role.USER, content); + + McpSchema.ModelHint hint = new McpSchema.ModelHint("gpt-4"); + + McpSchema.ModelPreferences preferences = new McpSchema.ModelPreferences(Collections.singletonList(hint), 0.3, + 0.7, 0.9); + + Map metadata = new HashMap<>(); + metadata.put("session", "test-session"); + + McpSchema.CreateMessageRequest request = new McpSchema.CreateMessageRequest(Collections.singletonList(message), + preferences, "You are a helpful assistant", + McpSchema.CreateMessageRequest.ContextInclusionStrategy.THIS_SERVER, 0.7, 1000, + Arrays.asList("STOP", "END"), metadata); + + String value = mapper.writeValueAsString(request); + assertEquals( + """ + {"messages":[{"role":"user","content":{"type":"text","text":"User message"}}],"modelPreferences":{"hints":[{"name":"gpt-4"}],"costPriority":0.3,"speedPriority":0.7,"intelligencePriority":0.9},"systemPrompt":"You are a helpful assistant","includeContext":"this_server","temperature":0.7,"maxTokens":1000,"stopSequences":["STOP","END"],"metadata":{"session":"test-session"}}""", + value); + } + + @Test + void testCreateMessageResult() throws Exception { + McpSchema.TextContent content = new McpSchema.TextContent("Assistant response"); + + McpSchema.CreateMessageResult result = new McpSchema.CreateMessageResult(McpSchema.Role.ASSISTANT, content, + "gpt-4", McpSchema.CreateMessageResult.StopReason.END_TURN); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"end_turn"}""", + value); + } + + // Roots Tests + + @Test + void testRoot() throws Exception { + McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root"); + + String value = mapper.writeValueAsString(root); + assertEquals(""" + {"uri":"file:///path/to/root","name":"Test Root"}""", value); + } + + @Test + void testListRootsResult() throws Exception { + McpSchema.Root root1 = new McpSchema.Root("file:///path/to/root1", "First Root"); + + McpSchema.Root root2 = new McpSchema.Root("file:///path/to/root2", "Second Root"); + + McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(Arrays.asList(root1, root2)); + + String value = mapper.writeValueAsString(result); + assertEquals( + """ + {"roots":[{"uri":"file:///path/to/root1","name":"First Root"},{"uri":"file:///path/to/root2","name":"Second Root"}]}""", + value); + } + +} From c395e7c4a1cee2c9c6c442f9a5204e4191c62a4d Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Mon, 3 Mar 2025 11:17:42 +0100 Subject: [PATCH 2/3] refactor: improve MCP schema and test assertions - Enhance Content interface with type() implementation: - Add default implementation that returns the appropriate type string based on the implementing class (text, image, or resource) - Remove unused JsonIgnore import - Improve test assertions and coverage: - Replace JUnit Jupiter assertions with AssertJ for more readable assertions - Add comprehensive deserialization tests for all content types - Add negative test case for invalid content type deserialization Signed-off-by: Christian Tzolov --- .../modelcontextprotocol/spec/McpSchema.java | 14 +- .../spec/McpSchemaTests.java | 198 +++++++++++------- 2 files changed, 132 insertions(+), 80 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 60ef5643..3ce2068b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -9,7 +9,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -965,7 +964,18 @@ public record CompleteCompletion(// @formatter:off @JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") }) public sealed interface Content permits TextContent, ImageContent, EmbeddedResource { - // String type(); + default String type() { + if (this instanceof TextContent) { + return "text"; + } + else if (this instanceof ImageContent) { + return "image"; + } + else if (this instanceof EmbeddedResource) { + return "resource"; + } + throw new IllegalArgumentException("Unknown content type: " + this); + } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 51e20eab..e380ea96 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -9,9 +9,12 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Christian Tzolov @@ -26,16 +29,46 @@ public class McpSchemaTests { void testTextContent() throws Exception { McpSchema.TextContent test = new McpSchema.TextContent("XXX"); String value = mapper.writeValueAsString(test); - assertEquals(""" - {"type":"text","text":"XXX"}""", value); + assertThat(value).isEqualTo(""" + {"type":"text","text":"XXX"}"""); + } + + @Test + void testTextContentDeserialization() throws Exception { + McpSchema.TextContent textContent = mapper.readValue(""" + {"type":"text","text":"XXX"}""", McpSchema.TextContent.class); + + assertThat(textContent).isNotNull(); + assertThat(textContent.type()).isEqualTo("text"); + assertThat(textContent.text()).isEqualTo("XXX"); + } + + @Test + void testContentDeserializationWrongType() throws Exception { + + assertThatThrownBy(() -> mapper.readValue(""" + {"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class)) + .isInstanceOf(InvalidTypeIdException.class) + .hasMessageContaining( + "Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [image, resource, text]"); } @Test void testImageContent() throws Exception { McpSchema.ImageContent test = new McpSchema.ImageContent(null, null, "base64encodeddata", "image/png"); String value = mapper.writeValueAsString(test); - assertEquals(""" - {"type":"image","data":"base64encodeddata","mimeType":"image/png"}""", value); + assertThat(value).isEqualTo(""" + {"type":"image","data":"base64encodeddata","mimeType":"image/png"}"""); + } + + @Test + void testImageContentDeserialization() throws Exception { + McpSchema.ImageContent imageContent = mapper.readValue(""" + {"type":"image","data":"base64encodeddata","mimeType":"image/png"}""", McpSchema.ImageContent.class); + assertThat(imageContent).isNotNull(); + assertThat(imageContent.type()).isEqualTo("image"); + assertThat(imageContent.data()).isEqualTo("base64encodeddata"); + assertThat(imageContent.mimeType()).isEqualTo("image/png"); } @Test @@ -46,10 +79,23 @@ void testEmbeddedResource() throws Exception { McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); String value = mapper.writeValueAsString(test); - assertEquals( + assertThat(value).isEqualTo( + """ + {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}"""); + } + + @Test + void testEmbeddedResourceDeserialization() throws Exception { + McpSchema.EmbeddedResource embeddedResource = mapper.readValue( """ {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}""", - value); + McpSchema.EmbeddedResource.class); + assertThat(embeddedResource).isNotNull(); + assertThat(embeddedResource.type()).isEqualTo("resource"); + assertThat(embeddedResource.resource()).isNotNull(); + assertThat(embeddedResource.resource().uri()).isEqualTo("resource://test"); + assertThat(embeddedResource.resource().mimeType()).isEqualTo("text/plain"); + assertThat(((TextResourceContents) embeddedResource.resource()).text()).isEqualTo("Sample resource content"); } @Test @@ -60,10 +106,24 @@ void testEmbeddedResourceWithBlobContents() throws Exception { McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); String value = mapper.writeValueAsString(test); - assertEquals( + assertThat(value).isEqualTo( + """ + {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}"""); + } + + @Test + void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { + McpSchema.EmbeddedResource embeddedResource = mapper.readValue( """ {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}""", - value); + McpSchema.EmbeddedResource.class); + assertThat(embeddedResource).isNotNull(); + assertThat(embeddedResource.type()).isEqualTo("resource"); + assertThat(embeddedResource.resource()).isNotNull(); + assertThat(embeddedResource.resource().uri()).isEqualTo("resource://test"); + assertThat(embeddedResource.resource().mimeType()).isEqualTo("application/octet-stream"); + assertThat(((McpSchema.BlobResourceContents) embeddedResource.resource()).blob()) + .isEqualTo("base64encodedblob"); } // JSON-RPC Message Types Tests @@ -77,8 +137,8 @@ void testJSONRPCRequest() throws Exception { params); String value = mapper.writeValueAsString(request); - assertEquals(""" - {"jsonrpc":"2.0","method":"method_name","id":1,"params":{"key":"value"}}""", value); + assertThat(value).isEqualTo(""" + {"jsonrpc":"2.0","method":"method_name","id":1,"params":{"key":"value"}}"""); } @Test @@ -90,8 +150,8 @@ void testJSONRPCNotification() throws Exception { "notification_method", params); String value = mapper.writeValueAsString(notification); - assertEquals(""" - {"jsonrpc":"2.0","method":"notification_method","params":{"key":"value"}}""", value); + assertThat(value).isEqualTo(""" + {"jsonrpc":"2.0","method":"notification_method","params":{"key":"value"}}"""); } @Test @@ -102,8 +162,8 @@ void testJSONRPCResponse() throws Exception { McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, result, null); String value = mapper.writeValueAsString(response); - assertEquals(""" - {"jsonrpc":"2.0","id":1,"result":{"result_key":"result_value"}}""", value); + assertThat(value).isEqualTo(""" + {"jsonrpc":"2.0","id":1,"result":{"result_key":"result_value"}}"""); } @Test @@ -114,8 +174,8 @@ void testJSONRPCResponseWithError() throws Exception { McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, null, error); String value = mapper.writeValueAsString(response); - assertEquals(""" - {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}""", value); + assertThat(value).isEqualTo(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}"""); } // Initialization Tests @@ -129,14 +189,12 @@ void testInitializeRequest() throws Exception { McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); - McpSchema.InitializeRequest request = new McpSchema.InitializeRequest(McpSchema.LATEST_PROTOCOL_VERSION, - capabilities, clientInfo); + McpSchema.InitializeRequest request = new McpSchema.InitializeRequest("2024-11-05", capabilities, clientInfo); String value = mapper.writeValueAsString(request); - assertEquals( + assertThat(value).isEqualTo( """ - {"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}""", - value); + {"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}"""); } @Test @@ -150,14 +208,13 @@ void testInitializeResult() throws Exception { McpSchema.Implementation serverInfo = new McpSchema.Implementation("test-server", "1.0.0"); - McpSchema.InitializeResult result = new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, - capabilities, serverInfo, "Server initialized successfully"); + McpSchema.InitializeResult result = new McpSchema.InitializeResult("2024-11-05", capabilities, serverInfo, + "Server initialized successfully"); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"test-server","version":"1.0.0"},"instructions":"Server initialized successfully"}""", - value); + {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"test-server","version":"1.0.0"},"instructions":"Server initialized successfully"}"""); } // Resource Tests @@ -171,10 +228,9 @@ void testResource() throws Exception { "text/plain", annotations); String value = mapper.writeValueAsString(resource); - assertEquals( + assertThat(value).isEqualTo( """ - {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}""", - value); + {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}"""); } @Test @@ -185,10 +241,9 @@ void testResourceTemplate() throws Exception { "A test resource template", "text/plain", annotations); String value = mapper.writeValueAsString(template); - assertEquals( + assertThat(value).isEqualTo( """ - {"uriTemplate":"resource://{param}/test","name":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}""", - value); + {"uriTemplate":"resource://{param}/test","name":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}"""); } @Test @@ -203,10 +258,9 @@ void testListResourcesResult() throws Exception { "next-cursor"); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"resources":[{"uri":"resource://test1","name":"Test Resource 1","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}""", - value); + {"resources":[{"uri":"resource://test1","name":"Test Resource 1","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}"""); } @Test @@ -221,10 +275,9 @@ void testListResourceTemplatesResult() throws Exception { Arrays.asList(template1, template2), "next-cursor"); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"resourceTemplates":[{"uriTemplate":"resource://{param}/test1","name":"Test Template 1","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}""", - value); + {"resourceTemplates":[{"uriTemplate":"resource://{param}/test1","name":"Test Template 1","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}"""); } @Test @@ -232,8 +285,8 @@ void testReadResourceRequest() throws Exception { McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test"); String value = mapper.writeValueAsString(request); - assertEquals(""" - {"uri":"resource://test"}""", value); + assertThat(value).isEqualTo(""" + {"uri":"resource://test"}"""); } @Test @@ -247,10 +300,9 @@ void testReadResourceResult() throws Exception { McpSchema.ReadResourceResult result = new McpSchema.ReadResourceResult(Arrays.asList(contents1, contents2)); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"contents":[{"uri":"resource://test1","mimeType":"text/plain","text":"Sample text content"},{"uri":"resource://test2","mimeType":"application/octet-stream","blob":"base64encodedblob"}]}""", - value); + {"contents":[{"uri":"resource://test1","mimeType":"text/plain","text":"Sample text content"},{"uri":"resource://test2","mimeType":"application/octet-stream","blob":"base64encodedblob"}]}"""); } // Prompt Tests @@ -264,10 +316,9 @@ void testPrompt() throws Exception { McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "A test prompt", Arrays.asList(arg1, arg2)); String value = mapper.writeValueAsString(prompt); - assertEquals( + assertThat(value).isEqualTo( """ - {"name":"test-prompt","description":"A test prompt","arguments":[{"name":"arg1","description":"First argument","required":true},{"name":"arg2","description":"Second argument","required":false}]}""", - value); + {"name":"test-prompt","description":"A test prompt","arguments":[{"name":"arg1","description":"First argument","required":true},{"name":"arg2","description":"Second argument","required":false}]}"""); } @Test @@ -277,8 +328,8 @@ void testPromptMessage() throws Exception { McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content); String value = mapper.writeValueAsString(message); - assertEquals(""" - {"role":"user","content":{"type":"text","text":"Hello, world!"}}""", value); + assertThat(value).isEqualTo(""" + {"role":"user","content":{"type":"text","text":"Hello, world!"}}"""); } @Test @@ -293,10 +344,9 @@ void testListPromptsResult() throws Exception { "next-cursor"); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"prompts":[{"name":"prompt1","description":"First prompt","arguments":[{"name":"arg","description":"An argument","required":true}]},{"name":"prompt2","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}""", - value); + {"prompts":[{"name":"prompt1","description":"First prompt","arguments":[{"name":"arg","description":"An argument","required":true}]},{"name":"prompt2","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}"""); } @Test @@ -309,11 +359,8 @@ void testGetPromptRequest() throws Exception { String value = mapper.writeValueAsString(request); - // Use Jackson to parse both the expected and actual JSON to compare them as - // objects - // This ignores the order of properties in JSON objects - assertEquals(mapper.readTree(""" - {"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}"""), mapper.readTree(value)); + assertThat(mapper.readTree(value)).isEqualTo(mapper.readTree(""" + {"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}""")); } @Test @@ -329,10 +376,9 @@ void testGetPromptResult() throws Exception { Arrays.asList(message1, message2)); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"description":"A test prompt result","messages":[{"role":"assistant","content":{"type":"text","text":"System message"}},{"role":"user","content":{"type":"text","text":"User message"}}]}""", - value); + {"description":"A test prompt result","messages":[{"role":"assistant","content":{"type":"text","text":"System message"}},{"role":"user","content":{"type":"text","text":"User message"}}]}"""); } // Tool Tests @@ -357,10 +403,9 @@ void testTool() throws Exception { McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson); String value = mapper.writeValueAsString(tool); - assertEquals( + assertThat(value).isEqualTo( """ - {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}""", - value); + {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}"""); } @Test @@ -372,8 +417,8 @@ void testCallToolRequest() throws Exception { McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", arguments); String value = mapper.writeValueAsString(request); - assertEquals(""" - {"name":"test-tool","arguments":{"name":"test","value":42}}""", value); + assertThat(value).isEqualTo(""" + {"name":"test-tool","arguments":{"name":"test","value":42}}"""); } @Test @@ -383,8 +428,8 @@ void testCallToolResult() throws Exception { McpSchema.CallToolResult result = new McpSchema.CallToolResult(Collections.singletonList(content), false); String value = mapper.writeValueAsString(result); - assertEquals(""" - {"content":[{"type":"text","text":"Tool execution result"}],"isError":false}""", value); + assertThat(value).isEqualTo(""" + {"content":[{"type":"text","text":"Tool execution result"}],"isError":false}"""); } // Sampling Tests @@ -409,10 +454,9 @@ void testCreateMessageRequest() throws Exception { Arrays.asList("STOP", "END"), metadata); String value = mapper.writeValueAsString(request); - assertEquals( + assertThat(value).isEqualTo( """ - {"messages":[{"role":"user","content":{"type":"text","text":"User message"}}],"modelPreferences":{"hints":[{"name":"gpt-4"}],"costPriority":0.3,"speedPriority":0.7,"intelligencePriority":0.9},"systemPrompt":"You are a helpful assistant","includeContext":"this_server","temperature":0.7,"maxTokens":1000,"stopSequences":["STOP","END"],"metadata":{"session":"test-session"}}""", - value); + {"messages":[{"role":"user","content":{"type":"text","text":"User message"}}],"modelPreferences":{"hints":[{"name":"gpt-4"}],"costPriority":0.3,"speedPriority":0.7,"intelligencePriority":0.9},"systemPrompt":"You are a helpful assistant","includeContext":"this_server","temperature":0.7,"maxTokens":1000,"stopSequences":["STOP","END"],"metadata":{"session":"test-session"}}"""); } @Test @@ -423,10 +467,9 @@ void testCreateMessageResult() throws Exception { "gpt-4", McpSchema.CreateMessageResult.StopReason.END_TURN); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"end_turn"}""", - value); + {"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"end_turn"}"""); } // Roots Tests @@ -436,8 +479,8 @@ void testRoot() throws Exception { McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root"); String value = mapper.writeValueAsString(root); - assertEquals(""" - {"uri":"file:///path/to/root","name":"Test Root"}""", value); + assertThat(value).isEqualTo(""" + {"uri":"file:///path/to/root","name":"Test Root"}"""); } @Test @@ -449,10 +492,9 @@ void testListRootsResult() throws Exception { McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(Arrays.asList(root1, root2)); String value = mapper.writeValueAsString(result); - assertEquals( + assertThat(value).isEqualTo( """ - {"roots":[{"uri":"file:///path/to/root1","name":"First Root"},{"uri":"file:///path/to/root2","name":"Second Root"}]}""", - value); + {"roots":[{"uri":"file:///path/to/root1","name":"First Root"},{"uri":"file:///path/to/root2","name":"Second Root"}]}"""); } } From fbf7ca9840972e34e9971c41089721bd127d2e78 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Mon, 3 Mar 2025 12:30:14 +0100 Subject: [PATCH 3/3] feat(test): add json-unit-assertj for flexible JSON testing Replace direct string assertions with json-unit-assertj in McpSchemaTests to make tests more robust against non-functional changes in JSON formatting. The new approach ignores array order and extra array items, reducing test brittleness while maintaining functional validation. Signed-off-by: Christian Tzolov --- mcp/pom.xml | 16 +- .../spec/McpSchemaTests.java | 240 ++++++++++++------ pom.xml | 2 + 3 files changed, 180 insertions(+), 78 deletions(-) diff --git a/mcp/pom.xml b/mcp/pom.xml index 2bd3e9d2..2170ffef 100644 --- a/mcp/pom.xml +++ b/mcp/pom.xml @@ -11,7 +11,7 @@ mcp jar Java MCP SDK - Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools + Java SDK implementation of the Model Context Protocol, enabling seamless integration with language models and AI tools https://github.com/modelcontextprotocol/java-sdk @@ -51,7 +51,7 @@ - + org.apache.maven.plugins maven-jar-plugin @@ -158,12 +158,20 @@ test + + net.javacrumbs.json-unit + json-unit-assertj + ${json-unit-assertj.version} + test + + + jakarta.servlet jakarta.servlet-api ${jakarta.servlet.version} - provided + provided @@ -183,4 +191,4 @@ - + \ No newline at end of file diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index e380ea96..05e2ce28 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -11,8 +11,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; +import net.javacrumbs.jsonunit.core.Option; import org.junit.jupiter.api.Test; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -29,8 +32,12 @@ public class McpSchemaTests { void testTextContent() throws Exception { McpSchema.TextContent test = new McpSchema.TextContent("XXX"); String value = mapper.writeValueAsString(test); - assertThat(value).isEqualTo(""" - {"type":"text","text":"XXX"}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"type":"text","text":"XXX"}""")); } @Test @@ -57,8 +64,12 @@ void testContentDeserializationWrongType() throws Exception { void testImageContent() throws Exception { McpSchema.ImageContent test = new McpSchema.ImageContent(null, null, "base64encodeddata", "image/png"); String value = mapper.writeValueAsString(test); - assertThat(value).isEqualTo(""" - {"type":"image","data":"base64encodeddata","mimeType":"image/png"}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"type":"image","data":"base64encodeddata","mimeType":"image/png"}""")); } @Test @@ -79,9 +90,12 @@ void testEmbeddedResource() throws Exception { McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); String value = mapper.writeValueAsString(test); - assertThat(value).isEqualTo( - """ - {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"}}""")); } @Test @@ -106,9 +120,12 @@ void testEmbeddedResourceWithBlobContents() throws Exception { McpSchema.EmbeddedResource test = new McpSchema.EmbeddedResource(null, null, resourceContents); String value = mapper.writeValueAsString(test); - assertThat(value).isEqualTo( - """ - {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob"}}""")); } @Test @@ -137,8 +154,11 @@ void testJSONRPCRequest() throws Exception { params); String value = mapper.writeValueAsString(request); - assertThat(value).isEqualTo(""" - {"jsonrpc":"2.0","method":"method_name","id":1,"params":{"key":"value"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"jsonrpc":"2.0","method":"method_name","id":1,"params":{"key":"value"}}""")); } @Test @@ -150,8 +170,11 @@ void testJSONRPCNotification() throws Exception { "notification_method", params); String value = mapper.writeValueAsString(notification); - assertThat(value).isEqualTo(""" - {"jsonrpc":"2.0","method":"notification_method","params":{"key":"value"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"jsonrpc":"2.0","method":"notification_method","params":{"key":"value"}}""")); } @Test @@ -162,8 +185,11 @@ void testJSONRPCResponse() throws Exception { McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, result, null); String value = mapper.writeValueAsString(response); - assertThat(value).isEqualTo(""" - {"jsonrpc":"2.0","id":1,"result":{"result_key":"result_value"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"jsonrpc":"2.0","id":1,"result":{"result_key":"result_value"}}""")); } @Test @@ -174,8 +200,11 @@ void testJSONRPCResponseWithError() throws Exception { McpSchema.JSONRPCResponse response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, 1, null, error); String value = mapper.writeValueAsString(response); - assertThat(value).isEqualTo(""" - {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid request"}}""")); } // Initialization Tests @@ -192,9 +221,12 @@ void testInitializeRequest() throws Exception { McpSchema.InitializeRequest request = new McpSchema.InitializeRequest("2024-11-05", capabilities, clientInfo); String value = mapper.writeValueAsString(request); - assertThat(value).isEqualTo( - """ - {"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}""")); } @Test @@ -212,9 +244,12 @@ void testInitializeResult() throws Exception { "Server initialized successfully"); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"test-server","version":"1.0.0"},"instructions":"Server initialized successfully"}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"test-server","version":"1.0.0"},"instructions":"Server initialized successfully"}""")); } // Resource Tests @@ -228,9 +263,12 @@ void testResource() throws Exception { "text/plain", annotations); String value = mapper.writeValueAsString(resource); - assertThat(value).isEqualTo( - """ - {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}""")); } @Test @@ -241,9 +279,12 @@ void testResourceTemplate() throws Exception { "A test resource template", "text/plain", annotations); String value = mapper.writeValueAsString(template); - assertThat(value).isEqualTo( - """ - {"uriTemplate":"resource://{param}/test","name":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"uriTemplate":"resource://{param}/test","name":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5}}""")); } @Test @@ -258,9 +299,12 @@ void testListResourcesResult() throws Exception { "next-cursor"); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"resources":[{"uri":"resource://test1","name":"Test Resource 1","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"resources":[{"uri":"resource://test1","name":"Test Resource 1","description":"First test resource","mimeType":"text/plain"},{"uri":"resource://test2","name":"Test Resource 2","description":"Second test resource","mimeType":"application/json"}],"nextCursor":"next-cursor"}""")); } @Test @@ -275,9 +319,12 @@ void testListResourceTemplatesResult() throws Exception { Arrays.asList(template1, template2), "next-cursor"); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"resourceTemplates":[{"uriTemplate":"resource://{param}/test1","name":"Test Template 1","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"resourceTemplates":[{"uriTemplate":"resource://{param}/test1","name":"Test Template 1","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}""")); } @Test @@ -285,8 +332,11 @@ void testReadResourceRequest() throws Exception { McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("resource://test"); String value = mapper.writeValueAsString(request); - assertThat(value).isEqualTo(""" - {"uri":"resource://test"}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"uri":"resource://test"}""")); } @Test @@ -300,9 +350,12 @@ void testReadResourceResult() throws Exception { McpSchema.ReadResourceResult result = new McpSchema.ReadResourceResult(Arrays.asList(contents1, contents2)); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"contents":[{"uri":"resource://test1","mimeType":"text/plain","text":"Sample text content"},{"uri":"resource://test2","mimeType":"application/octet-stream","blob":"base64encodedblob"}]}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"contents":[{"uri":"resource://test1","mimeType":"text/plain","text":"Sample text content"},{"uri":"resource://test2","mimeType":"application/octet-stream","blob":"base64encodedblob"}]}""")); } // Prompt Tests @@ -316,9 +369,12 @@ void testPrompt() throws Exception { McpSchema.Prompt prompt = new McpSchema.Prompt("test-prompt", "A test prompt", Arrays.asList(arg1, arg2)); String value = mapper.writeValueAsString(prompt); - assertThat(value).isEqualTo( - """ - {"name":"test-prompt","description":"A test prompt","arguments":[{"name":"arg1","description":"First argument","required":true},{"name":"arg2","description":"Second argument","required":false}]}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"name":"test-prompt","description":"A test prompt","arguments":[{"name":"arg1","description":"First argument","required":true},{"name":"arg2","description":"Second argument","required":false}]}""")); } @Test @@ -328,8 +384,11 @@ void testPromptMessage() throws Exception { McpSchema.PromptMessage message = new McpSchema.PromptMessage(McpSchema.Role.USER, content); String value = mapper.writeValueAsString(message); - assertThat(value).isEqualTo(""" - {"role":"user","content":{"type":"text","text":"Hello, world!"}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"role":"user","content":{"type":"text","text":"Hello, world!"}}""")); } @Test @@ -344,9 +403,12 @@ void testListPromptsResult() throws Exception { "next-cursor"); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"prompts":[{"name":"prompt1","description":"First prompt","arguments":[{"name":"arg","description":"An argument","required":true}]},{"name":"prompt2","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"prompts":[{"name":"prompt1","description":"First prompt","arguments":[{"name":"arg","description":"An argument","required":true}]},{"name":"prompt2","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}""")); } @Test @@ -357,10 +419,9 @@ void testGetPromptRequest() throws Exception { McpSchema.GetPromptRequest request = new McpSchema.GetPromptRequest("test-prompt", arguments); - String value = mapper.writeValueAsString(request); - - assertThat(mapper.readTree(value)).isEqualTo(mapper.readTree(""" - {"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}""")); + assertThat(mapper.readValue(""" + {"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}""", McpSchema.GetPromptRequest.class)) + .isEqualTo(request); } @Test @@ -376,9 +437,13 @@ void testGetPromptResult() throws Exception { Arrays.asList(message1, message2)); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"description":"A test prompt result","messages":[{"role":"assistant","content":{"type":"text","text":"System message"}},{"role":"user","content":{"type":"text","text":"User message"}}]}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"description":"A test prompt result","messages":[{"role":"assistant","content":{"type":"text","text":"System message"}},{"role":"user","content":{"type":"text","text":"User message"}}]}""")); } // Tool Tests @@ -403,9 +468,12 @@ void testTool() throws Exception { McpSchema.Tool tool = new McpSchema.Tool("test-tool", "A test tool", schemaJson); String value = mapper.writeValueAsString(tool); - assertThat(value).isEqualTo( - """ - {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}""")); } @Test @@ -417,8 +485,12 @@ void testCallToolRequest() throws Exception { McpSchema.CallToolRequest request = new McpSchema.CallToolRequest("test-tool", arguments); String value = mapper.writeValueAsString(request); - assertThat(value).isEqualTo(""" - {"name":"test-tool","arguments":{"name":"test","value":42}}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"name":"test-tool","arguments":{"name":"test","value":42}}""")); } @Test @@ -428,8 +500,12 @@ void testCallToolResult() throws Exception { McpSchema.CallToolResult result = new McpSchema.CallToolResult(Collections.singletonList(content), false); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo(""" - {"content":[{"type":"text","text":"Tool execution result"}],"isError":false}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"content":[{"type":"text","text":"Tool execution result"}],"isError":false}""")); } // Sampling Tests @@ -454,9 +530,13 @@ void testCreateMessageRequest() throws Exception { Arrays.asList("STOP", "END"), metadata); String value = mapper.writeValueAsString(request); - assertThat(value).isEqualTo( - """ - {"messages":[{"role":"user","content":{"type":"text","text":"User message"}}],"modelPreferences":{"hints":[{"name":"gpt-4"}],"costPriority":0.3,"speedPriority":0.7,"intelligencePriority":0.9},"systemPrompt":"You are a helpful assistant","includeContext":"this_server","temperature":0.7,"maxTokens":1000,"stopSequences":["STOP","END"],"metadata":{"session":"test-session"}}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"messages":[{"role":"user","content":{"type":"text","text":"User message"}}],"modelPreferences":{"hints":[{"name":"gpt-4"}],"costPriority":0.3,"speedPriority":0.7,"intelligencePriority":0.9},"systemPrompt":"You are a helpful assistant","includeContext":"this_server","temperature":0.7,"maxTokens":1000,"stopSequences":["STOP","END"],"metadata":{"session":"test-session"}}""")); } @Test @@ -467,9 +547,13 @@ void testCreateMessageResult() throws Exception { "gpt-4", McpSchema.CreateMessageResult.StopReason.END_TURN); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"end_turn"}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"end_turn"}""")); } // Roots Tests @@ -479,8 +563,11 @@ void testRoot() throws Exception { McpSchema.Root root = new McpSchema.Root("file:///path/to/root", "Test Root"); String value = mapper.writeValueAsString(root); - assertThat(value).isEqualTo(""" - {"uri":"file:///path/to/root","name":"Test Root"}"""); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo(json(""" + {"uri":"file:///path/to/root","name":"Test Root"}""")); } @Test @@ -492,9 +579,14 @@ void testListRootsResult() throws Exception { McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(Arrays.asList(root1, root2)); String value = mapper.writeValueAsString(result); - assertThat(value).isEqualTo( - """ - {"roots":[{"uri":"file:///path/to/root1","name":"First Root"},{"uri":"file:///path/to/root2","name":"Second Root"}]}"""); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"roots":[{"uri":"file:///path/to/root1","name":"First Root"},{"uri":"file:///path/to/root2","name":"Second Root"}]}""")); + } } diff --git a/pom.xml b/pom.xml index 9551c4db..893e5eb9 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,8 @@ 6.1.0 4.2.0 7.1.0 + 4.1.0 +