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
2 changed files with 321 additions and 10 deletions
|
|
@ -34,6 +34,8 @@ import org.junit.jupiter.api.AfterEach;
|
|||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
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.Mono;
|
||||
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.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
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
|
||||
void testListResourcesWithoutInitialization() {
|
||||
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.BeforeEach;
|
||||
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.test.StepVerifier;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
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
|
||||
void testPingWithoutInitialization() {
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
|
||||
|
|
|
|||
Loading…
Reference in a new issue