java-sdk/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java
Dariusz Jędrzejczyk 22a0efa577 feat!: enforce required MCP spec fields in McpSchema; lenient wire deserialization (#928)
Every wire-serialized record in McpSchema now validates spec-required
fields at construction time. Wire deserialization is intentionally
lenient: a missing required field is replaced with a documented default
and a WARN is logged, instead of failing the parse. A required-first
builder convention is introduced across the schema so it is no longer
possible to obtain a builder that is missing required state.

Required-field guards
---------------------
Every wire record's compact constructor asserts non-null (and non-empty
for String identifiers like name, uri, uriTemplate, version) on its
spec-required components. Passing null now throws
IllegalArgumentException at construction time instead of silently
producing invalid JSON via @JsonInclude(NON_ABSENT). This applies to
the JSON-RPC envelopes, lifecycle types, resource/prompt/tool requests
and results, sampling and elicitation, content records, root, complete,
logging and progress notifications, and the two CompleteReference
implementations. See MIGRATION-2.0.md for the full list.

Lenient wire deserialization
----------------------------
For each of those records (except JSONRPCResponse.JSONRPCError, which
still fails fast) a @JsonCreator static `fromJson` factory substitutes
a documented default for any absent required field — "" for strings,
[] for collections, {} for maps, 0 / 0.0 for numerics, INFO for
LoggingLevel, USER for SamplingMessage.role, ASSISTANT for
CreateMessageResult.role, CANCEL for ElicitResult.action, {values=[]}
for CompleteResult.completion — and logs a WARN naming the field and
the value used. Application code can still observe a malformed message
and react, but the SDK no longer halts the conversation.

Builder convention
------------------
Records that have a builder gain a required-first factory method
`builder(req1, req2, …)`; setters for required fields are removed from
the builder so it cannot be left in an invalid state. Existing no-arg
`builder()` factories and required-field setters are kept where source
compatibility demands it but are marked @Deprecated. New builders are
also added for several records that previously had none (ProgressNotification,
JSONRPCError, CompleteRequest, list/result types, content records, ...).

JSON-RPC envelope ergonomics
----------------------------
Previously every JSON-RPC envelope had to be constructed via the
canonical record constructor and the literal "2.0" string had to be
threaded through every call site, e.g.

    new JSONRPCRequest("2.0", "tools/call", id, params)
    new JSONRPCResponse("2.0", id, result, null)
    new JSONRPCResponse("2.0", id, null, new JSONRPCError(code, message, null))

Now:

    new JSONRPCRequest("tools/call", id, params)              // jsonrpc defaulted
    new JSONRPCNotification("notifications/initialized")       // params optional
    JSONRPCResponse.result(id, result)                        // factory
    JSONRPCResponse.error(id, new JSONRPCError(code, message)) // 2-arg error

JSONRPCResponse's compact constructor additionally enforces the
JSON-RPC invariant that exactly one of `result` / `error` is set —
previously the SDK could build envelopes that violated the protocol.

CompleteReference changes
-------------------------
- PromptReference.equals/hashCode now key on `name` only (previously
  derived from identifier()+type()). Two refs with the same name but
  different titles now compare equal — code using PromptReference as a
  map/set key should be audited.
- PromptReference's compact constructor pins `type` to "ref/prompt"
  and logs a WARN if the caller supplies a different non-null value.
  The legacy two-arg `PromptReference(String type, String name)`
  constructor remains @Deprecated.
- ResourceReference's record components are reduced from (type, uri)
  to (uri) — positional construction breaks. The legacy
  `ResourceReference(String type, String uri)` constructor stays
  @Deprecated and ignores `type`.
- CompleteReference.identifier() is @Deprecated and now returns null
  via a default method on the interface.

Tests / refactor
----------------
Conformance harness, integration tests, sample apps, and internal
callers were migrated to the new builder factories and convenience
constructors. No semantic changes to client/server runtime behaviour
beyond the McpSchema changes above.

Docs
----
- MIGRATION-2.0.md: required-field section now covers the broader
  record set and the lenient-deserialization behaviour; PromptReference
  WARN behaviour and ResourceReference component reduction are
  documented; the JSON-RPC envelope section is rewritten to compare the
  pre-2.0 surface with the new one.
- CONTRIBUTING.md: the "Evolving wire-serialized records" recipe is
  split into two cases — adding a new optional field (existing rules)
  and adding/maintaining a spec-required field (new rules covering
  Assert in compact constructor, @JsonCreator fromJson with defaults
  and WARN, required-first builder factory).

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
2026-05-04 21:01:46 +02:00

1887 lines
64 KiB
Java

/*
* Copyright 2025 - 2025 the original author or authors.
*/
package io.modelcontextprotocol.spec;
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
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;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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;
/**
* @author Christian Tzolov
* @author Anurag Pant
*/
public class McpSchemaTests {
// Content Types Tests
@Test
void testTextContent() throws Exception {
McpSchema.TextContent test = McpSchema.TextContent.builder("XXX").build();
String value = JSON_MAPPER.writeValueAsString(test);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"type":"text","text":"XXX"}"""));
}
@Test
void testTextContentDeserialization() throws Exception {
McpSchema.TextContent textContent = JSON_MAPPER.readValue("""
{"type":"text","text":"XXX","_meta":{"metaKey":"metaValue"}}""", McpSchema.TextContent.class);
assertThat(textContent).isNotNull();
assertThat(textContent.type()).isEqualTo("text");
assertThat(textContent.text()).isEqualTo("XXX");
assertThat(textContent.meta()).containsKey("metaKey");
}
@Test
void testContentDeserializationWrongType() {
assertThatThrownBy(() -> JSON_MAPPER.readValue("""
{"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class)).isInstanceOf(IOException.class)
// Jackson 2 throws the InvalidTypeException directly, but Jackson 3 wraps it.
// Try to unwrap in case it's Jackson 3.
.extracting(throwable -> throwable.getCause() != null ? throwable.getCause() : throwable)
.asInstanceOf(InstanceOfAssertFactories.THROWABLE)
.hasMessageContaining(
"Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [audio, image, resource, resource_link, text]")
.extracting(Object::getClass)
.extracting(Class::getSimpleName)
// Class name is the same for both Jackson 2 and 3, only the package differs.
.isEqualTo("InvalidTypeIdException");
}
@Test
void testImageContent() throws Exception {
McpSchema.ImageContent test = McpSchema.ImageContent.builder("base64encodeddata", "image/png").build();
String value = JSON_MAPPER.writeValueAsString(test);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"type":"image","data":"base64encodeddata","mimeType":"image/png"}"""));
}
@Test
void testImageContentDeserialization() throws Exception {
McpSchema.ImageContent imageContent = JSON_MAPPER.readValue("""
{"type":"image","data":"base64encodeddata","mimeType":"image/png","_meta":{"metaKey":"metaValue"}}""",
McpSchema.ImageContent.class);
assertThat(imageContent).isNotNull();
assertThat(imageContent.type()).isEqualTo("image");
assertThat(imageContent.data()).isEqualTo("base64encodeddata");
assertThat(imageContent.mimeType()).isEqualTo("image/png");
assertThat(imageContent.meta()).containsKey("metaKey");
}
@Test
void testAudioContent() throws Exception {
McpSchema.AudioContent audioContent = McpSchema.AudioContent.builder("base64encodeddata", "audio/wav").build();
String value = JSON_MAPPER.writeValueAsString(audioContent);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"type":"audio","data":"base64encodeddata","mimeType":"audio/wav"}"""));
}
@Test
void testAudioContentDeserialization() throws Exception {
McpSchema.AudioContent audioContent = JSON_MAPPER.readValue("""
{"type":"audio","data":"base64encodeddata","mimeType":"audio/wav","_meta":{"metaKey":"metaValue"}}""",
McpSchema.AudioContent.class);
assertThat(audioContent).isNotNull();
assertThat(audioContent.type()).isEqualTo("audio");
assertThat(audioContent.data()).isEqualTo("base64encodeddata");
assertThat(audioContent.mimeType()).isEqualTo("audio/wav");
assertThat(audioContent.meta()).containsKey("metaKey");
}
@Test
void testCreateMessageRequestWithMeta() throws Exception {
McpSchema.TextContent content = McpSchema.TextContent.builder("User message").build();
McpSchema.SamplingMessage message = McpSchema.SamplingMessage.builder(McpSchema.Role.USER, content).build();
McpSchema.ModelHint hint = McpSchema.ModelHint.of("gpt-4");
McpSchema.ModelPreferences preferences = McpSchema.ModelPreferences.builder()
.hints(Collections.singletonList(hint))
.costPriority(0.3)
.speedPriority(0.7)
.intelligencePriority(0.9)
.build();
Map<String, Object> metadata = new HashMap<>();
metadata.put("session", "test-session");
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "create-message-token-456");
McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest
.builder(Collections.singletonList(message), 1000)
.modelPreferences(preferences)
.systemPrompt("You are a helpful assistant")
.includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.THIS_SERVER)
.temperature(0.7)
.stopSequences(Arrays.asList("STOP", "END"))
.metadata(metadata)
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.containsEntry("_meta", Map.of("progressToken", "create-message-token-456"));
// Test Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("create-message-token-456");
}
@Test
void testEmbeddedResource() throws Exception {
McpSchema.TextResourceContents resourceContents = McpSchema.TextResourceContents
.builder("resource://test", "Sample resource content")
.mimeType("text/plain")
.build();
McpSchema.EmbeddedResource test = McpSchema.EmbeddedResource.builder(resourceContents).build();
String value = JSON_MAPPER.writeValueAsString(test);
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
void testEmbeddedResourceDeserialization() throws Exception {
McpSchema.EmbeddedResource embeddedResource = JSON_MAPPER.readValue(
"""
{"type":"resource","resource":{"uri":"resource://test","mimeType":"text/plain","text":"Sample resource content"},"_meta":{"metaKey":"metaValue"}}""",
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");
assertThat(embeddedResource.meta()).containsKey("metaKey");
}
@Test
void testEmbeddedResourceWithBlobContents() throws Exception {
McpSchema.BlobResourceContents resourceContents = McpSchema.BlobResourceContents
.builder("resource://test", "base64encodedblob")
.mimeType("application/octet-stream")
.build();
McpSchema.EmbeddedResource test = McpSchema.EmbeddedResource.builder(resourceContents).build();
String value = JSON_MAPPER.writeValueAsString(test);
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
void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception {
McpSchema.EmbeddedResource embeddedResource = JSON_MAPPER.readValue(
"""
{"type":"resource","resource":{"uri":"resource://test","mimeType":"application/octet-stream","blob":"base64encodedblob","_meta":{"metaKey":"metaValue"}}}""",
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");
assertThat(((McpSchema.BlobResourceContents) embeddedResource.resource()).meta()).containsKey("metaKey");
}
@Test
void testResourceLink() throws Exception {
McpSchema.ResourceLink resourceLink = McpSchema.ResourceLink.builder()
.name("main.rs")
.title("Main file")
.uri("file:///project/src/main.rs")
.description("Primary application entry point")
.mimeType("text/x-rust")
.meta(Map.of("metaKey", "metaValue"))
.build();
String value = JSON_MAPPER.writeValueAsString(resourceLink);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"type":"resource_link","name":"main.rs","title":"Main file","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust","_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testResourceLinkDeserialization() throws Exception {
McpSchema.ResourceLink resourceLink = JSON_MAPPER.readValue(
"""
{"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust","_meta":{"metaKey":"metaValue"}}""",
McpSchema.ResourceLink.class);
assertThat(resourceLink).isNotNull();
assertThat(resourceLink.type()).isEqualTo("resource_link");
assertThat(resourceLink.name()).isEqualTo("main.rs");
assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs");
assertThat(resourceLink.description()).isEqualTo("Primary application entry point");
assertThat(resourceLink.mimeType()).isEqualTo("text/x-rust");
assertThat(resourceLink.meta()).containsEntry("metaKey", "metaValue");
}
// JSON-RPC Message Types Tests
@Test
void testJSONRPCRequest() throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("key", "value");
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest("method_name", 1, params);
String value = JSON_MAPPER.writeValueAsString(request);
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
void testJSONRPCNotification() throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("key", "value");
McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification("notification_method", params);
String value = JSON_MAPPER.writeValueAsString(notification);
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
void testJSONRPCResponse() throws Exception {
Map<String, Object> result = new HashMap<>();
result.put("result_key", "result_value");
McpSchema.JSONRPCResponse response = McpSchema.JSONRPCResponse.result(1, result);
String value = JSON_MAPPER.writeValueAsString(response);
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
void testJSONRPCResponseWithError() throws Exception {
McpSchema.JSONRPCResponse.JSONRPCError error = new McpSchema.JSONRPCResponse.JSONRPCError(
McpSchema.ErrorCodes.INVALID_REQUEST, "Invalid request");
McpSchema.JSONRPCResponse response = McpSchema.JSONRPCResponse.error(1, error);
String value = JSON_MAPPER.writeValueAsString(response);
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
@Test
void testInitializeRequest() throws Exception {
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
.roots(true)
.sampling()
.build();
McpSchema.Implementation clientInfo = McpSchema.Implementation.builder("test-client", "1.0.0").build();
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.InitializeRequest request = McpSchema.InitializeRequest
.builder(ProtocolVersions.MCP_2024_11_05, capabilities, clientInfo)
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(request);
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"},"_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testInitializeResult() throws Exception {
McpSchema.ServerCapabilities capabilities = McpSchema.ServerCapabilities.builder()
.logging()
.prompts(true)
.resources(true, true)
.tools(true)
.build();
McpSchema.Implementation serverInfo = McpSchema.Implementation.builder("test-server", "1.0.0").build();
McpSchema.InitializeResult result = McpSchema.InitializeResult
.builder(ProtocolVersions.MCP_2024_11_05, capabilities, serverInfo)
.instructions("Server initialized successfully")
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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
@Test
void testResource() throws Exception {
McpSchema.Annotations annotations = McpSchema.Annotations.builder()
.audience(Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT))
.priority(0.8)
.build();
McpSchema.Resource resource = McpSchema.Resource.builder("resource://test", "Test Resource")
.description("A test resource")
.mimeType("text/plain")
.annotations(annotations)
.build();
String value = JSON_MAPPER.writeValueAsString(resource);
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
void testResourceBuilder() throws Exception {
McpSchema.Annotations annotations = McpSchema.Annotations.builder()
.audience(Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT))
.priority(0.8)
.build();
McpSchema.Resource resource = McpSchema.Resource.builder("resource://test", "Test Resource")
.description("A test resource")
.mimeType("text/plain")
.size(256L)
.annotations(annotations)
.meta(Map.of("metaKey", "metaValue"))
.build();
String value = JSON_MAPPER.writeValueAsString(resource);
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","size":256,"annotations":{"audience":["user","assistant"],"priority":0.8},"_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testResourceBuilderUriRequired() {
assertThatThrownBy(() -> McpSchema.Resource.builder(null, "Test Resource"))
.isInstanceOf(java.lang.IllegalArgumentException.class);
}
@Test
void testResourceBuilderNameRequired() {
assertThatThrownBy(() -> McpSchema.Resource.builder("resource://test", null))
.isInstanceOf(java.lang.IllegalArgumentException.class);
}
@Test
void testResourceTemplate() throws Exception {
McpSchema.Annotations annotations = McpSchema.Annotations.builder()
.audience(Arrays.asList(McpSchema.Role.USER))
.priority(0.5)
.build();
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate
.builder("resource://{param}/test", "Test Template")
.title("Test Template")
.description("A test resource template")
.mimeType("text/plain")
.annotations(annotations)
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(template);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"uriTemplate":"resource://{param}/test","name":"Test Template","title":"Test Template","description":"A test resource template","mimeType":"text/plain","annotations":{"audience":["user"],"priority":0.5},"_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testListResourcesResult() throws Exception {
McpSchema.Resource resource1 = McpSchema.Resource.builder("resource://test1", "Test Resource 1")
.description("First test resource")
.mimeType("text/plain")
.build();
McpSchema.Resource resource2 = McpSchema.Resource.builder("resource://test2", "Test Resource 2")
.description("Second test resource")
.mimeType("application/json")
.build();
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.ListResourcesResult result = McpSchema.ListResourcesResult
.builder(Arrays.asList(resource1, resource2))
.nextCursor("next-cursor")
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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","_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testListResourceTemplatesResult() throws Exception {
McpSchema.ResourceTemplate template1 = McpSchema.ResourceTemplate
.builder("resource://{param}/test1", "Test Template 1")
.title("Test Template 1")
.description("First test template")
.mimeType("text/plain")
.build();
McpSchema.ResourceTemplate template2 = McpSchema.ResourceTemplate
.builder("resource://{param}/test2", "Test Template 2")
.title("Test Template 2")
.description("Second test template")
.mimeType("application/json")
.build();
McpSchema.ListResourceTemplatesResult result = McpSchema.ListResourceTemplatesResult
.builder(Arrays.asList(template1, template2))
.nextCursor("next-cursor")
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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","title":"Test Template 1","description":"First test template","mimeType":"text/plain"},{"uriTemplate":"resource://{param}/test2","name":"Test Template 2","title":"Test Template 2","description":"Second test template","mimeType":"application/json"}],"nextCursor":"next-cursor"}"""));
}
@Test
void testReadResourceRequest() throws Exception {
McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder("resource://test")
.meta(Map.of("metaKey", "metaValue"))
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"uri":"resource://test","_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testReadResourceRequestWithMeta() throws Exception {
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "read-resource-token-123");
McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder("resource://test")
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"uri":"resource://test","_meta":{"progressToken":"read-resource-token-123"}}"""));
// Test Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("read-resource-token-123");
}
@Test
void testReadResourceRequestDeserialization() throws Exception {
McpSchema.ReadResourceRequest request = JSON_MAPPER.readValue("""
{"uri":"resource://test","_meta":{"progressToken":"test-token"}}""",
McpSchema.ReadResourceRequest.class);
assertThat(request.uri()).isEqualTo("resource://test");
assertThat(request.meta()).containsEntry("progressToken", "test-token");
assertThat(request.progressToken()).isEqualTo("test-token");
}
@Test
void testReadResourceResult() throws Exception {
McpSchema.TextResourceContents contents1 = McpSchema.TextResourceContents
.builder("resource://test1", "Sample text content")
.mimeType("text/plain")
.build();
McpSchema.BlobResourceContents contents2 = McpSchema.BlobResourceContents
.builder("resource://test2", "base64encodedblob")
.mimeType("application/octet-stream")
.build();
McpSchema.ReadResourceResult result = McpSchema.ReadResourceResult.builder(Arrays.asList(contents1, contents2))
.meta(Map.of("metaKey", "metaValue"))
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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"}],"_meta":{"metaKey":"metaValue"}}"""));
}
// Prompt Tests
@Test
void testPrompt() throws Exception {
McpSchema.PromptArgument arg1 = McpSchema.PromptArgument.builder("arg1")
.title("First argument")
.description("First argument")
.required(true)
.build();
McpSchema.PromptArgument arg2 = McpSchema.PromptArgument.builder("arg2")
.title("Second argument")
.description("Second argument")
.required(false)
.build();
McpSchema.Prompt prompt = McpSchema.Prompt.builder("test-prompt")
.title("Test Prompt")
.description("A test prompt")
.arguments(Arrays.asList(arg1, arg2))
.meta(Map.of("metaKey", "metaValue"))
.build();
String value = JSON_MAPPER.writeValueAsString(prompt);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"name":"test-prompt","title":"Test Prompt","description":"A test prompt","arguments":[{"name":"arg1","title":"First argument","description":"First argument","required":true},{"name":"arg2","title":"Second argument","description":"Second argument","required":false}],"_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testPromptMessage() throws Exception {
McpSchema.TextContent content = McpSchema.TextContent.builder("Hello, world!").build();
McpSchema.PromptMessage message = McpSchema.PromptMessage.builder(McpSchema.Role.USER, content).build();
String value = JSON_MAPPER.writeValueAsString(message);
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
void testListPromptsResult() throws Exception {
McpSchema.PromptArgument arg = McpSchema.PromptArgument.builder("arg")
.title("Argument")
.description("An argument")
.required(true)
.build();
McpSchema.Prompt prompt1 = McpSchema.Prompt.builder("prompt1")
.title("First prompt")
.description("First prompt")
.arguments(Collections.singletonList(arg))
.build();
McpSchema.Prompt prompt2 = McpSchema.Prompt.builder("prompt2")
.title("Second prompt")
.description("Second prompt")
.arguments(Collections.emptyList())
.build();
McpSchema.ListPromptsResult result = McpSchema.ListPromptsResult.builder(Arrays.asList(prompt1, prompt2))
.nextCursor("next-cursor")
.build();
String value = JSON_MAPPER.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"prompts":[{"name":"prompt1","title":"First prompt","description":"First prompt","arguments":[{"name":"arg","title":"Argument","description":"An argument","required":true}]},{"name":"prompt2","title":"Second prompt","description":"Second prompt","arguments":[]}],"nextCursor":"next-cursor"}"""));
}
@Test
void testGetPromptRequest() throws Exception {
Map<String, Object> arguments = new HashMap<>();
arguments.put("arg1", "value1");
arguments.put("arg2", 42);
McpSchema.GetPromptRequest request = McpSchema.GetPromptRequest.builder("test-prompt")
.arguments(arguments)
.build();
assertThat(JSON_MAPPER.readValue("""
{"name":"test-prompt","arguments":{"arg1":"value1","arg2":42}}""", McpSchema.GetPromptRequest.class))
.isEqualTo(request);
}
@Test
void testGetPromptRequestWithMeta() throws Exception {
Map<String, Object> arguments = new HashMap<>();
arguments.put("arg1", "value1");
arguments.put("arg2", 42);
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "token123");
McpSchema.GetPromptRequest request = McpSchema.GetPromptRequest.builder("test-prompt")
.arguments(arguments)
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"name":"test-prompt","arguments":{"arg1":"value1","arg2":42},"_meta":{"progressToken":"token123"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("token123");
}
@Test
void testGetPromptResult() throws Exception {
McpSchema.TextContent content1 = McpSchema.TextContent.builder("System message").build();
McpSchema.TextContent content2 = McpSchema.TextContent.builder("User message").build();
McpSchema.PromptMessage message1 = McpSchema.PromptMessage.builder(McpSchema.Role.ASSISTANT, content1).build();
McpSchema.PromptMessage message2 = McpSchema.PromptMessage.builder(McpSchema.Role.USER, content2).build();
McpSchema.GetPromptResult result = McpSchema.GetPromptResult.builder(Arrays.asList(message1, message2))
.description("A test prompt result")
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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
@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"]
}
}
}
""";
// Deserialize the original string to a JsonSchema object
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
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);
// The two serialized strings should be the same
assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
}
@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"]
}
}
}
""";
// Deserialize the original string to a JsonSchema object
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
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);
// The two serialized strings should be the same
assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
}
@Test
void testTool() throws Exception {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "number"
}
},
"required": ["name"]
}
""";
McpSchema.Tool tool = McpSchema.Tool.builder("test-tool", JSON_MAPPER, schemaJson)
.description("A test tool")
.build();
String value = JSON_MAPPER.writeValueAsString(tool);
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
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 = McpSchema.Tool.builder("addressTool", JSON_MAPPER, complexSchemaJson)
.title("Handles addresses")
.build();
// Serialize the tool to a string
String serialized = JSON_MAPPER.writeValueAsString(tool);
// Deserialize back to a Tool object
McpSchema.Tool deserializedTool = JSON_MAPPER.readValue(serialized, McpSchema.Tool.class);
// Serialize again and compare with first serialization
String serializedAgain = JSON_MAPPER.writeValueAsString(deserializedTool);
// The two serialized strings should be the same
assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
// Just verify the basic structure was preserved
assertThat(deserializedTool.inputSchema()).containsKey("$defs")
.extractingByKey("$defs")
.isNotNull()
.asInstanceOf(InstanceOfAssertFactories.MAP)
.containsKey("Address");
}
@Test
void testToolWithMeta() throws Exception {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "number"
}
},
"required": ["name"]
}
""";
Map<String, Object> inputSchema = Map.of("inputSchema", schemaJson);
Map<String, Object> meta = Map.of("metaKey", "metaValue");
McpSchema.Tool tool = McpSchema.Tool.builder("addressTool", inputSchema)
.title("addressTool")
.description("Handles addresses")
.meta(meta)
.build();
// Verify that meta value was preserved
assertThat(tool.meta()).isNotNull();
assertThat(tool.meta()).containsKey("metaKey");
}
@Test
void testToolWithAnnotations() throws Exception {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "number"
}
},
"required": ["name"]
}
""";
McpSchema.ToolAnnotations annotations = McpSchema.ToolAnnotations.builder()
.title("A test tool")
.readOnlyHint(false)
.destructiveHint(false)
.idempotentHint(false)
.openWorldHint(false)
.returnDirect(false)
.build();
McpSchema.Tool tool = McpSchema.Tool.builder("test-tool", JSON_MAPPER, schemaJson)
.description("A test tool")
.annotations(annotations)
.build();
String value = JSON_MAPPER.writeValueAsString(tool);
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"]
},
"annotations":{
"title":"A test tool",
"readOnlyHint":false,
"destructiveHint":false,
"idempotentHint":false,
"openWorldHint":false,
"returnDirect":false
}
}
"""));
}
@Test
void testToolWithOutputSchema() throws Exception {
String inputSchemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "number"
}
},
"required": ["name"]
}
""";
String outputSchemaJson = """
{
"type": "object",
"properties": {
"result": {
"type": "string"
},
"status": {
"type": "string",
"enum": ["success", "error"]
}
},
"required": ["result", "status"]
}
""";
McpSchema.Tool tool = McpSchema.Tool.builder("test-tool", JSON_MAPPER, inputSchemaJson)
.description("A test tool")
.outputSchema(JSON_MAPPER, outputSchemaJson)
.build();
String value = JSON_MAPPER.writeValueAsString(tool);
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"]
},
"outputSchema":{
"type":"object",
"properties":{
"result":{"type":"string"},
"status":{
"type":"string",
"enum":["success","error"]
}
},
"required":["result","status"]
}
}
"""));
}
@Test
void testToolWithOutputSchemaAndAnnotations() throws Exception {
String inputSchemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
""";
String outputSchemaJson = """
{
"type": "object",
"properties": {
"result": {
"type": "string"
}
},
"required": ["result"]
}
""";
McpSchema.ToolAnnotations annotations = McpSchema.ToolAnnotations.builder()
.title("A test tool with output")
.readOnlyHint(true)
.destructiveHint(false)
.idempotentHint(true)
.openWorldHint(false)
.returnDirect(true)
.build();
McpSchema.Tool tool = McpSchema.Tool.builder("test-tool", JSON_MAPPER, inputSchemaJson)
.description("A test tool")
.outputSchema(JSON_MAPPER, outputSchemaJson)
.annotations(annotations)
.build();
String value = JSON_MAPPER.writeValueAsString(tool);
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"}
},
"required":["name"]
},
"outputSchema":{
"type":"object",
"properties":{
"result":{"type":"string"}
},
"required":["result"]
},
"annotations":{
"title":"A test tool with output",
"readOnlyHint":true,
"destructiveHint":false,
"idempotentHint":true,
"openWorldHint":false,
"returnDirect":true
}
}"""));
}
@Test
void testToolDeserialization() throws Exception {
String toolJson = """
{
"name": "test-tool",
"description": "A test tool",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
},
"outputSchema": {
"type": "object",
"properties": {
"result": {"type": "string"}
},
"required": ["result"]
},
"annotations": {
"title": "Test Tool",
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false,
"returnDirect": false
}
}
""";
McpSchema.Tool tool = JSON_MAPPER.readValue(toolJson, McpSchema.Tool.class);
assertThat(tool).isNotNull();
assertThat(tool.name()).isEqualTo("test-tool");
assertThat(tool.description()).isEqualTo("A test tool");
assertThat(tool.inputSchema()).isNotNull();
assertThat(tool.inputSchema().get("type")).isEqualTo("object");
assertThat(tool.outputSchema()).isNotNull();
assertThat(tool.outputSchema()).containsKey("type");
assertThat(tool.outputSchema().get("type")).isEqualTo("object");
assertThat(tool.annotations()).isNotNull();
assertThat(tool.annotations().title()).isEqualTo("Test Tool");
assertThat(tool.annotations().readOnlyHint()).isTrue();
assertThat(tool.annotations().idempotentHint()).isTrue();
assertThat(tool.annotations().destructiveHint()).isFalse();
assertThat(tool.annotations().returnDirect()).isFalse();
}
@Test
void testToolDeserializationWithoutOutputSchema() throws Exception {
String toolJson = """
{
"name": "test-tool",
"description": "A test tool",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
}
}
""";
McpSchema.Tool tool = JSON_MAPPER.readValue(toolJson, McpSchema.Tool.class);
assertThat(tool).isNotNull();
assertThat(tool.name()).isEqualTo("test-tool");
assertThat(tool.description()).isEqualTo("A test tool");
assertThat(tool.inputSchema()).isNotNull();
assertThat(tool.outputSchema()).isNull();
assertThat(tool.annotations()).isNull();
}
@Test
void testCallToolRequest() throws Exception {
Map<String, Object> arguments = new HashMap<>();
arguments.put("name", "test");
arguments.put("value", 42);
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder("test-tool").arguments(arguments).build();
String value = JSON_MAPPER.writeValueAsString(request);
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
void testCallToolRequestJsonArguments() throws Exception {
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder("test-tool").arguments(JSON_MAPPER, """
{
"name": "test",
"value": 42
}
""").build();
String value = JSON_MAPPER.writeValueAsString(request);
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
void testCallToolRequestWithMeta() throws Exception {
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
.name("test-tool")
.arguments(Map.of("name", "test", "value", 42))
.progressToken("tool-progress-123")
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"name":"test-tool","arguments":{"name":"test","value":42},"_meta":{"progressToken":"tool-progress-123"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(Map.of("progressToken", "tool-progress-123"));
assertThat(request.progressToken()).isEqualTo("tool-progress-123");
}
@Test
void testCallToolRequestBuilderWithJsonArguments() throws Exception {
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "json-builder-789");
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
.name("test-tool")
.arguments(JSON_MAPPER, """
{
"name": "test",
"value": 42
}
""")
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"name":"test-tool","arguments":{"name":"test","value":42},"_meta":{"progressToken":"json-builder-789"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("json-builder-789");
}
@Test
void testCallToolRequestBuilderNameRequired() {
Map<String, Object> arguments = new HashMap<>();
arguments.put("name", "test");
McpSchema.CallToolRequest.Builder builder = McpSchema.CallToolRequest.builder().arguments(arguments);
assertThatThrownBy(builder::build).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("name must not be empty");
}
@Test
void testCallToolResult() throws Exception {
McpSchema.TextContent content = McpSchema.TextContent.builder("Tool execution result").build();
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
.content(Collections.singletonList(content))
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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}"""));
}
@Test
void testCallToolResultBuilder() throws Exception {
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
.addTextContent("Tool execution result")
.isError(false)
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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}"""));
}
@Test
void testCallToolResultBuilderWithMultipleContents() throws Exception {
McpSchema.TextContent textContent = McpSchema.TextContent.builder("Text result").build();
McpSchema.ImageContent imageContent = McpSchema.ImageContent.builder("base64data", "image/png").build();
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
.addContent(textContent)
.addContent(imageContent)
.isError(false)
.build();
String value = JSON_MAPPER.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"content":[{"type":"text","text":"Text result"},{"type":"image","data":"base64data","mimeType":"image/png"}],"isError":false}"""));
}
@Test
void testCallToolResultBuilderWithContentList() throws Exception {
McpSchema.TextContent textContent = McpSchema.TextContent.builder("Text result").build();
McpSchema.ImageContent imageContent = McpSchema.ImageContent.builder("base64data", "image/png").build();
List<McpSchema.Content> contents = Arrays.asList(textContent, imageContent);
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder().content(contents).isError(true).build();
String value = JSON_MAPPER.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"content":[{"type":"text","text":"Text result"},{"type":"image","data":"base64data","mimeType":"image/png"}],"isError":true}"""));
}
@Test
void testCallToolResultBuilderWithErrorResult() throws Exception {
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
.addTextContent("Error: Operation failed")
.isError(true)
.build();
String value = JSON_MAPPER.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"content":[{"type":"text","text":"Error: Operation failed"}],"isError":true}"""));
}
@Test
void testCallToolResultDeserializationWithMissingContent() throws Exception {
McpSchema.CallToolResult result = JSON_MAPPER.readValue("""
{"isError":false}""", McpSchema.CallToolResult.class);
assertThat(result).isNotNull();
assertThat(result.content()).isEmpty();
assertThat(result.isError()).isFalse();
}
// Sampling Tests
@Test
void testCreateMessageRequest() throws Exception {
McpSchema.TextContent content = McpSchema.TextContent.builder("User message").build();
McpSchema.SamplingMessage message = McpSchema.SamplingMessage.builder(McpSchema.Role.USER, content).build();
McpSchema.ModelHint hint = McpSchema.ModelHint.of("gpt-4");
McpSchema.ModelPreferences preferences = McpSchema.ModelPreferences.builder()
.hints(Collections.singletonList(hint))
.costPriority(0.3)
.speedPriority(0.7)
.intelligencePriority(0.9)
.build();
Map<String, Object> metadata = new HashMap<>();
metadata.put("session", "test-session");
McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest
.builder(Collections.singletonList(message), 1000)
.modelPreferences(preferences)
.systemPrompt("You are a helpful assistant")
.includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.THIS_SERVER)
.temperature(0.7)
.stopSequences(Arrays.asList("STOP", "END"))
.metadata(metadata)
.build();
String value = JSON_MAPPER.writeValueAsString(request);
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":"thisServer","temperature":0.7,"maxTokens":1000,"stopSequences":["STOP","END"],"metadata":{"session":"test-session"}}"""));
}
@Test
void testSamplingMessageDeserializationWithMissingFields() throws Exception {
McpSchema.SamplingMessage message = JSON_MAPPER.readValue("{}", McpSchema.SamplingMessage.class);
assertThat(message).isNotNull();
assertThat(message.role()).isEqualTo(McpSchema.Role.USER);
assertThat(message.content()).isInstanceOf(McpSchema.TextContent.class);
}
@Test
void testCreateMessageRequestDeserializationWithMissingRequiredFields() throws Exception {
McpSchema.CreateMessageRequest request = JSON_MAPPER.readValue("""
{"systemPrompt":"hello"}""", McpSchema.CreateMessageRequest.class);
assertThat(request).isNotNull();
assertThat(request.messages()).isEmpty();
assertThat(request.maxTokens()).isZero();
assertThat(request.systemPrompt()).isEqualTo("hello");
}
@Test
void testCreateMessageResult() throws Exception {
McpSchema.TextContent content = McpSchema.TextContent.builder("Assistant response").build();
McpSchema.CreateMessageResult result = McpSchema.CreateMessageResult
.builder(McpSchema.Role.ASSISTANT, content, "gpt-4")
.stopReason(McpSchema.CreateMessageResult.StopReason.END_TURN)
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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":"endTurn"}"""));
}
@Test
void testCreateMessageResultUnknownStopReason() throws Exception {
String input = """
{"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"arbitrary value"}""";
McpSchema.CreateMessageResult value = JSON_MAPPER.readValue(input, McpSchema.CreateMessageResult.class);
McpSchema.TextContent expectedContent = McpSchema.TextContent.builder("Assistant response").build();
McpSchema.CreateMessageResult expected = McpSchema.CreateMessageResult
.builder(McpSchema.Role.ASSISTANT, expectedContent, "gpt-4")
.stopReason(McpSchema.CreateMessageResult.StopReason.UNKNOWN)
.build();
assertThat(value).isEqualTo(expected);
}
// Elicitation Tests
@Test
void testCreateElicitationRequest() throws Exception {
McpSchema.ElicitRequest request = McpSchema.ElicitRequest
.builder("Please provide additional information", Map.of("type", "object", "required", List.of("a"),
"properties", Map.of("foo", Map.of("type", "string"))))
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"message":"Please provide additional information","requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}"""));
}
@Test
void testCreateElicitationResult() throws Exception {
McpSchema.ElicitResult result = McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT)
.content(Map.of("foo", "bar"))
.build();
String value = JSON_MAPPER.writeValueAsString(result);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"action":"accept","content":{"foo":"bar"}}"""));
}
@Test
void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception {
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{}", McpSchema.ElicitRequest.class);
assertThat(request).isNotNull();
assertThat(request.message()).isEmpty();
assertThat(request.requestedSchema()).isEmpty();
}
@Test
void testElicitRequestWithMeta() throws Exception {
Map<String, Object> requestedSchema = Map.of("type", "object", "required", List.of("name"), "properties",
Map.of("name", Map.of("type", "string")));
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "elicit-token-789");
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder("Please provide your name", requestedSchema)
.meta(meta)
.build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.containsEntry("_meta", Map.of("progressToken", "elicit-token-789"));
// Test Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("elicit-token-789");
}
// Pagination Tests
@Test
void testPaginatedRequestNoArgs() throws Exception {
McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isNull();
assertThat(request.progressToken()).isNull();
}
@Test
void testPaginatedRequestWithCursor() throws Exception {
McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest("cursor123");
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"cursor":"cursor123"}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isNull();
assertThat(request.progressToken()).isNull();
}
@Test
void testPaginatedRequestWithMeta() throws Exception {
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "pagination-progress-456");
McpSchema.PaginatedRequest request = new McpSchema.PaginatedRequest("cursor123", meta);
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"cursor":"cursor123","_meta":{"progressToken":"pagination-progress-456"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("pagination-progress-456");
}
@Test
void testPaginatedRequestDeserialization() throws Exception {
McpSchema.PaginatedRequest request = JSON_MAPPER.readValue("""
{"cursor":"test-cursor","_meta":{"progressToken":"test-token"}}""", McpSchema.PaginatedRequest.class);
assertThat(request.cursor()).isEqualTo("test-cursor");
assertThat(request.meta()).containsEntry("progressToken", "test-token");
assertThat(request.progressToken()).isEqualTo("test-token");
}
// Complete Request Tests
@Test
void testCompleteRequest() throws Exception {
McpSchema.PromptReference promptRef = new McpSchema.PromptReference("test-prompt");
McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument("arg1",
"partial-value");
McpSchema.CompleteRequest request = McpSchema.CompleteRequest.builder(promptRef, argument).build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"ref":{"type":"ref/prompt","name":"test-prompt"},"argument":{"name":"arg1","value":"partial-value"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isNull();
assertThat(request.progressToken()).isNull();
}
@Test
void testCompleteRequestWithMeta() throws Exception {
McpSchema.ResourceReference resourceRef = new McpSchema.ResourceReference("file:///test.txt");
McpSchema.CompleteRequest.CompleteArgument argument = new McpSchema.CompleteRequest.CompleteArgument("path",
"/partial/path");
Map<String, Object> meta = new HashMap<>();
meta.put("progressToken", "complete-progress-789");
McpSchema.CompleteRequest request = McpSchema.CompleteRequest.builder(resourceRef, argument).meta(meta).build();
String value = JSON_MAPPER.writeValueAsString(request);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"ref":{"type":"ref/resource","uri":"file:///test.txt"},"argument":{"name":"path","value":"/partial/path"},"_meta":{"progressToken":"complete-progress-789"}}"""));
// Test that it implements Request interface methods
assertThat(request.meta()).isEqualTo(meta);
assertThat(request.progressToken()).isEqualTo("complete-progress-789");
}
// Roots Tests
@Test
void testRoot() throws Exception {
McpSchema.Root root = McpSchema.Root.builder("file:///path/to/root")
.name("Test Root")
.meta(Map.of("metaKey", "metaValue"))
.build();
String value = JSON_MAPPER.writeValueAsString(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","_meta":{"metaKey":"metaValue"}}"""));
}
@Test
void testListRootsResult() throws Exception {
McpSchema.Root root1 = McpSchema.Root.builder("file:///path/to/root1").name("First Root").build();
McpSchema.Root root2 = McpSchema.Root.builder("file:///path/to/root2").name("Second Root").build();
McpSchema.ListRootsResult result = McpSchema.ListRootsResult.builder(Arrays.asList(root1, root2))
.nextCursor("next-cursor")
.build();
String value = JSON_MAPPER.writeValueAsString(result);
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"}],"nextCursor":"next-cursor"}"""));
}
// Elicitation Capability Tests (Issue #724)
@Test
void testElicitationCapabilityWithFormField() throws Exception {
// Test that elicitation with "form" field can be deserialized (2025-11-25 spec)
String json = """
{"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"}}
""";
McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class);
assertThat(request).isNotNull();
assertThat(request.capabilities()).isNotNull();
assertThat(request.capabilities().elicitation()).isNotNull();
}
@Test
void testElicitationCapabilityWithFormAndUrlFields() throws Exception {
// Test that elicitation with both "form" and "url" fields can be deserialized
String json = """
{"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{},"url":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"}}
""";
McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class);
assertThat(request).isNotNull();
assertThat(request.capabilities()).isNotNull();
assertThat(request.capabilities().elicitation()).isNotNull();
}
@Test
void testElicitationCapabilityBackwardCompatibilityEmptyObject() throws Exception {
// Test backward compatibility: empty elicitation {} should still work
String json = """
{"protocolVersion":"2024-11-05","capabilities":{"elicitation":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}
""";
McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class);
assertThat(request).isNotNull();
assertThat(request.capabilities()).isNotNull();
assertThat(request.capabilities().elicitation()).isNotNull();
}
@Test
void testElicitationCapabilityBuilderBackwardCompatibility() throws Exception {
// Test that the existing builder API still works and produces valid JSON
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
assertThat(capabilities.elicitation()).isNotNull();
// Serialize and verify it produces valid JSON (should be {} for backward compat)
String json = JSON_MAPPER.writeValueAsString(capabilities);
assertThat(json).contains("\"elicitation\"");
}
@Test
void testElicitationCapabilitySerializationRoundTrip() throws Exception {
// Test that serialization and deserialization round-trip works
McpSchema.ClientCapabilities original = McpSchema.ClientCapabilities.builder().elicitation().build();
String json = JSON_MAPPER.writeValueAsString(original);
McpSchema.ClientCapabilities deserialized = JSON_MAPPER.readValue(json, McpSchema.ClientCapabilities.class);
assertThat(deserialized.elicitation()).isNotNull();
}
@Test
void testElicitationCapabilityBuilderWithFormAndUrl() throws Exception {
// Test the new builder method that explicitly sets form and url support
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
.elicitation(true, true)
.build();
assertThat(capabilities.elicitation()).isNotNull();
assertThat(capabilities.elicitation().form()).isNotNull();
assertThat(capabilities.elicitation().url()).isNotNull();
// Verify serialization produces the expected JSON
String json = JSON_MAPPER.writeValueAsString(capabilities);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().containsKey("elicitation");
assertThat(json).contains("\"form\"");
assertThat(json).contains("\"url\"");
}
@Test
void testElicitationCapabilityBuilderFormOnly() throws Exception {
// Test builder with form only
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
.elicitation(true, false)
.build();
assertThat(capabilities.elicitation()).isNotNull();
assertThat(capabilities.elicitation().form()).isNotNull();
assertThat(capabilities.elicitation().url()).isNull();
String json = JSON_MAPPER.writeValueAsString(capabilities);
assertThat(json).contains("\"form\"");
assertThat(json).doesNotContain("\"url\"");
}
// Progress Notification Tests
@Test
void testProgressNotificationWithMessage() throws Exception {
McpSchema.ProgressNotification notification = McpSchema.ProgressNotification.builder("progress-token-123", 0.5)
.total(1.0)
.message("Processing file 1 of 2")
.meta(Map.of("key", "value"))
.build();
String value = JSON_MAPPER.writeValueAsString(notification);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(
json("""
{"progressToken":"progress-token-123","progress":0.5,"total":1.0,"message":"Processing file 1 of 2","_meta":{"key":"value"}}"""));
}
@Test
void testProgressNotificationDeserialization() throws Exception {
McpSchema.ProgressNotification notification = JSON_MAPPER.readValue(
"""
{"progressToken":"token-456","progress":0.75,"total":1.0,"message":"Almost done","_meta":{"key":"value"}}""",
McpSchema.ProgressNotification.class);
assertThat(notification.progressToken()).isEqualTo("token-456");
assertThat(notification.progress()).isEqualTo(0.75);
assertThat(notification.total()).isEqualTo(1.0);
assertThat(notification.message()).isEqualTo("Almost done");
assertThat(notification.meta()).containsEntry("key", "value");
}
@Test
void testProgressNotificationDeserializationWithMissingRequiredFields() throws Exception {
McpSchema.ProgressNotification notification = JSON_MAPPER.readValue("""
{"total":1.0}""", McpSchema.ProgressNotification.class);
assertThat(notification).isNotNull();
assertThat(notification.progressToken()).isEqualTo("");
assertThat(notification.progress()).isZero();
assertThat(notification.total()).isEqualTo(1.0);
}
@Test
void testProgressNotificationWithoutMessage() throws Exception {
McpSchema.ProgressNotification notification = McpSchema.ProgressNotification.builder("progress-token-789", 0.25)
.build();
String value = JSON_MAPPER.writeValueAsString(notification);
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"progressToken":"progress-token-789","progress":0.25}"""));
}
@Test
void testLoggingMessageNotificationDeserializationWithMissingRequiredFields() throws Exception {
McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue("""
{"logger":"my-logger"}""", McpSchema.LoggingMessageNotification.class);
assertThat(notification).isNotNull();
assertThat(notification.level()).isEqualTo(McpSchema.LoggingLevel.INFO);
assertThat(notification.logger()).isEqualTo("my-logger");
assertThat(notification.data()).isEmpty();
}
}