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>
1887 lines
64 KiB
Java
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();
|
|
}
|
|
|
|
}
|