refactor: consolidate content annotations into Annotations (#242)
Updates all Content types to represent annotations as nested objects
instead of directly on the content itself.
- Refactor TextContent, ImageContent, and EmbeddedResource to use Annotations object instead of separate audience/priority fields
- Add backward-compatible deprecated methods for audience() and priority() accessors
- Add parameterized tests for message annotations across different message types (success, error, debug)
- Test both text and image content annotations with proper priority and audience validation
This brings the implementation in line with the content types as modeled by the specification:
c87a0da6d8/schema/2025-03-26/schema.json (L1970-L1973)
This commit is contained in:
parent
b5ece766e7
commit
fb40047c39
6 changed files with 321 additions and 10 deletions
|
|
@ -54,6 +54,11 @@
|
||||||
<artifactId>junit-jupiter-api</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
<version>${junit.version}</version>
|
<version>${junit.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
@ -41,6 +43,7 @@ import reactor.test.StepVerifier;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -208,6 +211,64 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "success", "error", "debug" })
|
||||||
|
void testCallToolWithMessageAnnotations(String messageType) {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
withClient(transport, mcpAsyncClient -> {
|
||||||
|
StepVerifier.create(mcpAsyncClient.initialize()
|
||||||
|
.then(mcpAsyncClient.callTool(new McpSchema.CallToolRequest("annotatedMessage",
|
||||||
|
Map.of("messageType", messageType, "includeImage", true)))))
|
||||||
|
.consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.isError()).isNotEqualTo(true);
|
||||||
|
assertThat(result.content()).isNotEmpty();
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
switch (content.type()) {
|
||||||
|
case "text":
|
||||||
|
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class,
|
||||||
|
content);
|
||||||
|
assertThat(textContent.text()).isNotEmpty();
|
||||||
|
assertThat(textContent.annotations()).isNotNull();
|
||||||
|
|
||||||
|
switch (messageType) {
|
||||||
|
case "error":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsOnly(McpSchema.Role.USER, McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
case "success":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
case "debug":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsExactly(McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unexpected value: " + content.type());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "image":
|
||||||
|
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class,
|
||||||
|
content);
|
||||||
|
assertThat(imageContent.data()).isNotEmpty();
|
||||||
|
assertThat(imageContent.annotations()).isNotNull();
|
||||||
|
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
|
||||||
|
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fail("Unexpected content type: " + content.type());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourcesWithoutInitialization() {
|
void testListResourcesWithoutInitialization() {
|
||||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
|
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,15 @@ import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,6 +186,60 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "success", "error", "debug" })
|
||||||
|
void testCallToolWithMessageAnnotations(String messageType) {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
withClient(transport, client -> {
|
||||||
|
client.initialize();
|
||||||
|
|
||||||
|
McpSchema.CallToolResult result = client.callTool(new McpSchema.CallToolRequest("annotatedMessage",
|
||||||
|
Map.of("messageType", messageType, "includeImage", true)));
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.isError()).isNotEqualTo(true);
|
||||||
|
assertThat(result.content()).isNotEmpty();
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
switch (content.type()) {
|
||||||
|
case "text":
|
||||||
|
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class, content);
|
||||||
|
assertThat(textContent.text()).isNotEmpty();
|
||||||
|
assertThat(textContent.annotations()).isNotNull();
|
||||||
|
|
||||||
|
switch (messageType) {
|
||||||
|
case "error":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
|
||||||
|
assertThat(textContent.annotations().audience()).containsOnly(McpSchema.Role.USER,
|
||||||
|
McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
case "success":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
|
||||||
|
assertThat(textContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
case "debug":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsExactly(McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unexpected value: " + content.type());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "image":
|
||||||
|
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class, content);
|
||||||
|
assertThat(imageContent.data()).isNotEmpty();
|
||||||
|
assertThat(imageContent.annotations()).isNotNull();
|
||||||
|
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
|
||||||
|
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fail("Unexpected content type: " + content.type());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPingWithoutInitialization() {
|
void testPingWithoutInitialization() {
|
||||||
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
|
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
|
||||||
|
|
|
||||||
|
|
@ -1384,22 +1384,68 @@ public final class McpSchema {
|
||||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record TextContent( // @formatter:off
|
public record TextContent( // @formatter:off
|
||||||
@JsonProperty("audience") List<Role> audience,
|
@JsonProperty("annotations") Annotations annotations,
|
||||||
@JsonProperty("priority") Double priority,
|
@JsonProperty("text") String text) implements Annotated, Content { // @formatter:on
|
||||||
@JsonProperty("text") String text) implements Content { // @formatter:on
|
|
||||||
|
|
||||||
public TextContent(String content) {
|
public TextContent(String content) {
|
||||||
this(null, null, content);
|
this(null, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link TextContent#TextContent(Annotations, String)} instead.
|
||||||
|
*/
|
||||||
|
public TextContent(List<Role> audience, Double priority, String content) {
|
||||||
|
this(audience != null || priority != null ? new Annotations(audience, priority) : null, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link TextContent#annotations()} instead.
|
||||||
|
*/
|
||||||
|
public List<Role> audience() {
|
||||||
|
return annotations == null ? null : annotations.audience();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link TextContent#annotations()} instead.
|
||||||
|
*/
|
||||||
|
public Double priority() {
|
||||||
|
return annotations == null ? null : annotations.priority();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record ImageContent( // @formatter:off
|
public record ImageContent( // @formatter:off
|
||||||
@JsonProperty("audience") List<Role> audience,
|
@JsonProperty("annotations") Annotations annotations,
|
||||||
@JsonProperty("priority") Double priority,
|
|
||||||
@JsonProperty("data") String data,
|
@JsonProperty("data") String data,
|
||||||
@JsonProperty("mimeType") String mimeType) implements Content { // @formatter:on
|
@JsonProperty("mimeType") String mimeType) implements Annotated, Content { // @formatter:on
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link ImageContent#ImageContent(Annotations, String, String)} instead.
|
||||||
|
*/
|
||||||
|
public ImageContent(List<Role> audience, Double priority, String data, String mimeType) {
|
||||||
|
this(audience != null || priority != null ? new Annotations(audience, priority) : null, data, mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link ImageContent#annotations()} instead.
|
||||||
|
*/
|
||||||
|
public List<Role> audience() {
|
||||||
|
return annotations == null ? null : annotations.audience();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link ImageContent#annotations()} instead.
|
||||||
|
*/
|
||||||
|
public Double priority() {
|
||||||
|
return annotations == null ? null : annotations.priority();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
|
@ -1413,9 +1459,33 @@ public final class McpSchema {
|
||||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record EmbeddedResource( // @formatter:off
|
public record EmbeddedResource( // @formatter:off
|
||||||
@JsonProperty("audience") List<Role> audience,
|
@JsonProperty("annotations") Annotations annotations,
|
||||||
@JsonProperty("priority") Double priority,
|
@JsonProperty("resource") ResourceContents resource) implements Annotated, Content { // @formatter:on
|
||||||
@JsonProperty("resource") ResourceContents resource) implements Content { // @formatter:on
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link EmbeddedResource#EmbeddedResource(Annotations, ResourceContents)}
|
||||||
|
* instead.
|
||||||
|
*/
|
||||||
|
public EmbeddedResource(List<Role> audience, Double priority, ResourceContents resource) {
|
||||||
|
this(audience != null || priority != null ? new Annotations(audience, priority) : null, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link EmbeddedResource#annotations()} instead.
|
||||||
|
*/
|
||||||
|
public List<Role> audience() {
|
||||||
|
return annotations == null ? null : annotations.audience();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
* {@link EmbeddedResource#annotations()} instead.
|
||||||
|
*/
|
||||||
|
public Double priority() {
|
||||||
|
return annotations == null ? null : annotations.priority();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
@ -41,6 +43,7 @@ import reactor.test.StepVerifier;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -209,6 +212,64 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "success", "error", "debug" })
|
||||||
|
void testCallToolWithMessageAnnotations(String messageType) {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
withClient(transport, mcpAsyncClient -> {
|
||||||
|
StepVerifier.create(mcpAsyncClient.initialize()
|
||||||
|
.then(mcpAsyncClient.callTool(new McpSchema.CallToolRequest("annotatedMessage",
|
||||||
|
Map.of("messageType", messageType, "includeImage", true)))))
|
||||||
|
.consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.isError()).isNotEqualTo(true);
|
||||||
|
assertThat(result.content()).isNotEmpty();
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
switch (content.type()) {
|
||||||
|
case "text":
|
||||||
|
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class,
|
||||||
|
content);
|
||||||
|
assertThat(textContent.text()).isNotEmpty();
|
||||||
|
assertThat(textContent.annotations()).isNotNull();
|
||||||
|
|
||||||
|
switch (messageType) {
|
||||||
|
case "error":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsOnly(McpSchema.Role.USER, McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
case "success":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
case "debug":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsExactly(McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unexpected value: " + content.type());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "image":
|
||||||
|
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class,
|
||||||
|
content);
|
||||||
|
assertThat(imageContent.data()).isNotEmpty();
|
||||||
|
assertThat(imageContent.annotations()).isNotNull();
|
||||||
|
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
|
||||||
|
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fail("Unexpected content type: " + content.type());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourcesWithoutInitialization() {
|
void testListResourcesWithoutInitialization() {
|
||||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
|
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Scheduler;
|
import reactor.core.scheduler.Scheduler;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
@ -39,6 +41,7 @@ import reactor.test.StepVerifier;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -229,6 +232,60 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "success", "error", "debug" })
|
||||||
|
void testCallToolWithMessageAnnotations(String messageType) {
|
||||||
|
McpClientTransport transport = createMcpTransport();
|
||||||
|
|
||||||
|
withClient(transport, client -> {
|
||||||
|
client.initialize();
|
||||||
|
|
||||||
|
McpSchema.CallToolResult result = client.callTool(new McpSchema.CallToolRequest("annotatedMessage",
|
||||||
|
Map.of("messageType", messageType, "includeImage", true)));
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.isError()).isNotEqualTo(true);
|
||||||
|
assertThat(result.content()).isNotEmpty();
|
||||||
|
assertThat(result.content()).allSatisfy(content -> {
|
||||||
|
switch (content.type()) {
|
||||||
|
case "text":
|
||||||
|
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class, content);
|
||||||
|
assertThat(textContent.text()).isNotEmpty();
|
||||||
|
assertThat(textContent.annotations()).isNotNull();
|
||||||
|
|
||||||
|
switch (messageType) {
|
||||||
|
case "error":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
|
||||||
|
assertThat(textContent.annotations().audience()).containsOnly(McpSchema.Role.USER,
|
||||||
|
McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
case "success":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
|
||||||
|
assertThat(textContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
case "debug":
|
||||||
|
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
|
||||||
|
assertThat(textContent.annotations().audience())
|
||||||
|
.containsExactly(McpSchema.Role.ASSISTANT);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unexpected value: " + content.type());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "image":
|
||||||
|
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class, content);
|
||||||
|
assertThat(imageContent.data()).isNotEmpty();
|
||||||
|
assertThat(imageContent.annotations()).isNotNull();
|
||||||
|
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
|
||||||
|
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fail("Unexpected content type: " + content.type());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChangedWithoutInitialization() {
|
void testRootsListChangedWithoutInitialization() {
|
||||||
verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(),
|
verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue