Skip to content

Commit fb40047

Browse files
LucaButBoringtzolov
authored andcommitted
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: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2025-03-26/schema.json#L1970-L1973
1 parent b5ece76 commit fb40047

File tree

6 files changed

+321
-10
lines changed

6 files changed

+321
-10
lines changed

mcp-test/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
<artifactId>junit-jupiter-api</artifactId>
5555
<version>${junit.version}</version>
5656
</dependency>
57+
<dependency>
58+
<groupId>org.junit.jupiter</groupId>
59+
<artifactId>junit-jupiter-params</artifactId>
60+
<version>${junit.version}</version>
61+
</dependency>
5762
<dependency>
5863
<groupId>org.mockito</groupId>
5964
<artifactId>mockito-core</artifactId>

mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@
3434
import org.junit.jupiter.api.BeforeEach;
3535
import org.junit.jupiter.api.Disabled;
3636
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.ValueSource;
3739
import reactor.core.publisher.Flux;
3840
import reactor.core.publisher.Mono;
3941
import reactor.test.StepVerifier;
4042

4143
import static org.assertj.core.api.Assertions.assertThat;
4244
import static org.assertj.core.api.Assertions.assertThatCode;
4345
import static org.assertj.core.api.Assertions.assertThatThrownBy;
46+
import static org.assertj.core.api.Assertions.fail;
4447
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4548

4649
/**
@@ -208,6 +211,64 @@ void testCallToolWithInvalidTool() {
208211
});
209212
}
210213

214+
@ParameterizedTest
215+
@ValueSource(strings = { "success", "error", "debug" })
216+
void testCallToolWithMessageAnnotations(String messageType) {
217+
McpClientTransport transport = createMcpTransport();
218+
219+
withClient(transport, mcpAsyncClient -> {
220+
StepVerifier.create(mcpAsyncClient.initialize()
221+
.then(mcpAsyncClient.callTool(new McpSchema.CallToolRequest("annotatedMessage",
222+
Map.of("messageType", messageType, "includeImage", true)))))
223+
.consumeNextWith(result -> {
224+
assertThat(result).isNotNull();
225+
assertThat(result.isError()).isNotEqualTo(true);
226+
assertThat(result.content()).isNotEmpty();
227+
assertThat(result.content()).allSatisfy(content -> {
228+
switch (content.type()) {
229+
case "text":
230+
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class,
231+
content);
232+
assertThat(textContent.text()).isNotEmpty();
233+
assertThat(textContent.annotations()).isNotNull();
234+
235+
switch (messageType) {
236+
case "error":
237+
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
238+
assertThat(textContent.annotations().audience())
239+
.containsOnly(McpSchema.Role.USER, McpSchema.Role.ASSISTANT);
240+
break;
241+
case "success":
242+
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
243+
assertThat(textContent.annotations().audience())
244+
.containsExactly(McpSchema.Role.USER);
245+
break;
246+
case "debug":
247+
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
248+
assertThat(textContent.annotations().audience())
249+
.containsExactly(McpSchema.Role.ASSISTANT);
250+
break;
251+
default:
252+
throw new IllegalStateException("Unexpected value: " + content.type());
253+
}
254+
break;
255+
case "image":
256+
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class,
257+
content);
258+
assertThat(imageContent.data()).isNotEmpty();
259+
assertThat(imageContent.annotations()).isNotNull();
260+
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
261+
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
262+
break;
263+
default:
264+
fail("Unexpected content type: " + content.type());
265+
}
266+
});
267+
})
268+
.verifyComplete();
269+
});
270+
}
271+
211272
@Test
212273
void testListResourcesWithoutInitialization() {
213274
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");

mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@
3131
import org.junit.jupiter.api.AfterEach;
3232
import org.junit.jupiter.api.BeforeEach;
3333
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.ValueSource;
3436
import reactor.core.publisher.Mono;
3537
import reactor.test.StepVerifier;
3638

3739
import static org.assertj.core.api.Assertions.assertThat;
3840
import static org.assertj.core.api.Assertions.assertThatCode;
3941
import static org.assertj.core.api.Assertions.assertThatThrownBy;
42+
import static org.assertj.core.api.Assertions.fail;
4043
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4144

4245
/**
@@ -183,6 +186,60 @@ void testCallTools() {
183186
});
184187
}
185188

189+
@ParameterizedTest
190+
@ValueSource(strings = { "success", "error", "debug" })
191+
void testCallToolWithMessageAnnotations(String messageType) {
192+
McpClientTransport transport = createMcpTransport();
193+
194+
withClient(transport, client -> {
195+
client.initialize();
196+
197+
McpSchema.CallToolResult result = client.callTool(new McpSchema.CallToolRequest("annotatedMessage",
198+
Map.of("messageType", messageType, "includeImage", true)));
199+
200+
assertThat(result).isNotNull();
201+
assertThat(result.isError()).isNotEqualTo(true);
202+
assertThat(result.content()).isNotEmpty();
203+
assertThat(result.content()).allSatisfy(content -> {
204+
switch (content.type()) {
205+
case "text":
206+
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class, content);
207+
assertThat(textContent.text()).isNotEmpty();
208+
assertThat(textContent.annotations()).isNotNull();
209+
210+
switch (messageType) {
211+
case "error":
212+
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
213+
assertThat(textContent.annotations().audience()).containsOnly(McpSchema.Role.USER,
214+
McpSchema.Role.ASSISTANT);
215+
break;
216+
case "success":
217+
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
218+
assertThat(textContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
219+
break;
220+
case "debug":
221+
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
222+
assertThat(textContent.annotations().audience())
223+
.containsExactly(McpSchema.Role.ASSISTANT);
224+
break;
225+
default:
226+
throw new IllegalStateException("Unexpected value: " + content.type());
227+
}
228+
break;
229+
case "image":
230+
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class, content);
231+
assertThat(imageContent.data()).isNotEmpty();
232+
assertThat(imageContent.annotations()).isNotNull();
233+
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
234+
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
235+
break;
236+
default:
237+
fail("Unexpected content type: " + content.type());
238+
}
239+
});
240+
});
241+
}
242+
186243
@Test
187244
void testPingWithoutInitialization() {
188245
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");

mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,22 +1384,68 @@ else if (this instanceof EmbeddedResource) {
13841384
@JsonInclude(JsonInclude.Include.NON_ABSENT)
13851385
@JsonIgnoreProperties(ignoreUnknown = true)
13861386
public record TextContent( // @formatter:off
1387-
@JsonProperty("audience") List<Role> audience,
1388-
@JsonProperty("priority") Double priority,
1389-
@JsonProperty("text") String text) implements Content { // @formatter:on
1387+
@JsonProperty("annotations") Annotations annotations,
1388+
@JsonProperty("text") String text) implements Annotated, Content { // @formatter:on
13901389

13911390
public TextContent(String content) {
1392-
this(null, null, content);
1391+
this(null, content);
1392+
}
1393+
1394+
/**
1395+
* @deprecated Only exists for backwards-compatibility purposes. Use
1396+
* {@link TextContent#TextContent(Annotations, String)} instead.
1397+
*/
1398+
public TextContent(List<Role> audience, Double priority, String content) {
1399+
this(audience != null || priority != null ? new Annotations(audience, priority) : null, content);
1400+
}
1401+
1402+
/**
1403+
* @deprecated Only exists for backwards-compatibility purposes. Use
1404+
* {@link TextContent#annotations()} instead.
1405+
*/
1406+
public List<Role> audience() {
1407+
return annotations == null ? null : annotations.audience();
1408+
}
1409+
1410+
/**
1411+
* @deprecated Only exists for backwards-compatibility purposes. Use
1412+
* {@link TextContent#annotations()} instead.
1413+
*/
1414+
public Double priority() {
1415+
return annotations == null ? null : annotations.priority();
13931416
}
13941417
}
13951418

13961419
@JsonInclude(JsonInclude.Include.NON_ABSENT)
13971420
@JsonIgnoreProperties(ignoreUnknown = true)
13981421
public record ImageContent( // @formatter:off
1399-
@JsonProperty("audience") List<Role> audience,
1400-
@JsonProperty("priority") Double priority,
1422+
@JsonProperty("annotations") Annotations annotations,
14011423
@JsonProperty("data") String data,
1402-
@JsonProperty("mimeType") String mimeType) implements Content { // @formatter:on
1424+
@JsonProperty("mimeType") String mimeType) implements Annotated, Content { // @formatter:on
1425+
1426+
/**
1427+
* @deprecated Only exists for backwards-compatibility purposes. Use
1428+
* {@link ImageContent#ImageContent(Annotations, String, String)} instead.
1429+
*/
1430+
public ImageContent(List<Role> audience, Double priority, String data, String mimeType) {
1431+
this(audience != null || priority != null ? new Annotations(audience, priority) : null, data, mimeType);
1432+
}
1433+
1434+
/**
1435+
* @deprecated Only exists for backwards-compatibility purposes. Use
1436+
* {@link ImageContent#annotations()} instead.
1437+
*/
1438+
public List<Role> audience() {
1439+
return annotations == null ? null : annotations.audience();
1440+
}
1441+
1442+
/**
1443+
* @deprecated Only exists for backwards-compatibility purposes. Use
1444+
* {@link ImageContent#annotations()} instead.
1445+
*/
1446+
public Double priority() {
1447+
return annotations == null ? null : annotations.priority();
1448+
}
14031449
}
14041450

14051451
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@@ -1413,9 +1459,33 @@ public record AudioContent( // @formatter:off
14131459
@JsonInclude(JsonInclude.Include.NON_ABSENT)
14141460
@JsonIgnoreProperties(ignoreUnknown = true)
14151461
public record EmbeddedResource( // @formatter:off
1416-
@JsonProperty("audience") List<Role> audience,
1417-
@JsonProperty("priority") Double priority,
1418-
@JsonProperty("resource") ResourceContents resource) implements Content { // @formatter:on
1462+
@JsonProperty("annotations") Annotations annotations,
1463+
@JsonProperty("resource") ResourceContents resource) implements Annotated, Content { // @formatter:on
1464+
1465+
/**
1466+
* @deprecated Only exists for backwards-compatibility purposes. Use
1467+
* {@link EmbeddedResource#EmbeddedResource(Annotations, ResourceContents)}
1468+
* instead.
1469+
*/
1470+
public EmbeddedResource(List<Role> audience, Double priority, ResourceContents resource) {
1471+
this(audience != null || priority != null ? new Annotations(audience, priority) : null, resource);
1472+
}
1473+
1474+
/**
1475+
* @deprecated Only exists for backwards-compatibility purposes. Use
1476+
* {@link EmbeddedResource#annotations()} instead.
1477+
*/
1478+
public List<Role> audience() {
1479+
return annotations == null ? null : annotations.audience();
1480+
}
1481+
1482+
/**
1483+
* @deprecated Only exists for backwards-compatibility purposes. Use
1484+
* {@link EmbeddedResource#annotations()} instead.
1485+
*/
1486+
public Double priority() {
1487+
return annotations == null ? null : annotations.priority();
1488+
}
14191489
}
14201490

14211491
// ---------------------------

mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@
3434
import org.junit.jupiter.api.BeforeEach;
3535
import org.junit.jupiter.api.Disabled;
3636
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.ValueSource;
3739
import reactor.core.publisher.Flux;
3840
import reactor.core.publisher.Mono;
3941
import reactor.test.StepVerifier;
4042

4143
import static org.assertj.core.api.Assertions.assertThat;
4244
import static org.assertj.core.api.Assertions.assertThatCode;
4345
import static org.assertj.core.api.Assertions.assertThatThrownBy;
46+
import static org.assertj.core.api.Assertions.fail;
4447
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4548

4649
/**
@@ -209,6 +212,64 @@ void testCallToolWithInvalidTool() {
209212
});
210213
}
211214

215+
@ParameterizedTest
216+
@ValueSource(strings = { "success", "error", "debug" })
217+
void testCallToolWithMessageAnnotations(String messageType) {
218+
McpClientTransport transport = createMcpTransport();
219+
220+
withClient(transport, mcpAsyncClient -> {
221+
StepVerifier.create(mcpAsyncClient.initialize()
222+
.then(mcpAsyncClient.callTool(new McpSchema.CallToolRequest("annotatedMessage",
223+
Map.of("messageType", messageType, "includeImage", true)))))
224+
.consumeNextWith(result -> {
225+
assertThat(result).isNotNull();
226+
assertThat(result.isError()).isNotEqualTo(true);
227+
assertThat(result.content()).isNotEmpty();
228+
assertThat(result.content()).allSatisfy(content -> {
229+
switch (content.type()) {
230+
case "text":
231+
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class,
232+
content);
233+
assertThat(textContent.text()).isNotEmpty();
234+
assertThat(textContent.annotations()).isNotNull();
235+
236+
switch (messageType) {
237+
case "error":
238+
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
239+
assertThat(textContent.annotations().audience())
240+
.containsOnly(McpSchema.Role.USER, McpSchema.Role.ASSISTANT);
241+
break;
242+
case "success":
243+
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
244+
assertThat(textContent.annotations().audience())
245+
.containsExactly(McpSchema.Role.USER);
246+
break;
247+
case "debug":
248+
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
249+
assertThat(textContent.annotations().audience())
250+
.containsExactly(McpSchema.Role.ASSISTANT);
251+
break;
252+
default:
253+
throw new IllegalStateException("Unexpected value: " + content.type());
254+
}
255+
break;
256+
case "image":
257+
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class,
258+
content);
259+
assertThat(imageContent.data()).isNotEmpty();
260+
assertThat(imageContent.annotations()).isNotNull();
261+
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
262+
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
263+
break;
264+
default:
265+
fail("Unexpected content type: " + content.type());
266+
}
267+
});
268+
})
269+
.verifyComplete();
270+
});
271+
}
272+
212273
@Test
213274
void testListResourcesWithoutInitialization() {
214275
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");

0 commit comments

Comments
 (0)