Skip to content

Commit d55d404

Browse files
committed
fix: model annotations as nested objects
Updates all Content types to represent annotations as nested objects instead of directly on the content itself. The existing ctors have been moved to deprecated shim ctors for backwards-compat. This brings the implementation in line with the content types as modeled by the specification. An integration test has been added to ensure this doesn't drift in the future. https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2025-03-26/schema.json#L1970-L1973
1 parent 07e7b8f commit d55d404

File tree

2 files changed

+92
-10
lines changed

2 files changed

+92
-10
lines changed

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

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,30 +1277,52 @@ else if (this instanceof EmbeddedResource) {
12771277
@JsonInclude(JsonInclude.Include.NON_ABSENT)
12781278
@JsonIgnoreProperties(ignoreUnknown = true)
12791279
public record TextContent( // @formatter:off
1280-
@JsonProperty("audience") List<Role> audience,
1281-
@JsonProperty("priority") Double priority,
1282-
@JsonProperty("text") String text) implements Content { // @formatter:on
1280+
@JsonProperty("annotations") Annotations annotations,
1281+
@JsonProperty("text") String text) implements Annotated, Content { // @formatter:on
12831282

12841283
public TextContent(String content) {
1285-
this(null, null, content);
1284+
this(null, content);
1285+
}
1286+
1287+
/**
1288+
* @deprecated Only exists for backwards-compatibility purposes. Use
1289+
* {@link TextContent#TextContent(Annotations, String)} instead.
1290+
*/
1291+
public TextContent(List<Role> audience, Double priority, String content) {
1292+
this(audience != null || priority != null ? new Annotations(audience, priority) : null, content);
12861293
}
12871294
}
12881295

12891296
@JsonInclude(JsonInclude.Include.NON_ABSENT)
12901297
@JsonIgnoreProperties(ignoreUnknown = true)
12911298
public record ImageContent( // @formatter:off
1292-
@JsonProperty("audience") List<Role> audience,
1293-
@JsonProperty("priority") Double priority,
1299+
@JsonProperty("annotations") Annotations annotations,
12941300
@JsonProperty("data") String data,
1295-
@JsonProperty("mimeType") String mimeType) implements Content { // @formatter:on
1301+
@JsonProperty("mimeType") String mimeType) implements Annotated, Content { // @formatter:on
1302+
1303+
/**
1304+
* @deprecated Only exists for backwards-compatibility purposes. Use
1305+
* {@link ImageContent#ImageContent(Annotations, String, String)} instead.
1306+
*/
1307+
public ImageContent(List<Role> audience, Double priority, String data, String mimeType) {
1308+
this(audience != null || priority != null ? new Annotations(audience, priority) : null, data, mimeType);
1309+
}
12961310
}
12971311

12981312
@JsonInclude(JsonInclude.Include.NON_ABSENT)
12991313
@JsonIgnoreProperties(ignoreUnknown = true)
13001314
public record EmbeddedResource( // @formatter:off
1301-
@JsonProperty("audience") List<Role> audience,
1302-
@JsonProperty("priority") Double priority,
1303-
@JsonProperty("resource") ResourceContents resource) implements Content { // @formatter:on
1315+
@JsonProperty("annotations") Annotations annotations,
1316+
@JsonProperty("resource") ResourceContents resource) implements Annotated, Content { // @formatter:on
1317+
1318+
/**
1319+
* @deprecated Only exists for backwards-compatibility purposes. Use
1320+
* {@link EmbeddedResource#EmbeddedResource(Annotations, ResourceContents)}
1321+
* instead.
1322+
*/
1323+
public EmbeddedResource(List<Role> audience, Double priority, ResourceContents resource) {
1324+
this(audience != null || priority != null ? new Annotations(audience, priority) : null, resource);
1325+
}
13041326
}
13051327

13061328
// ---------------------------

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
package io.modelcontextprotocol.client;
66

77
import java.time.Duration;
8+
import java.util.Map;
89
import java.util.concurrent.CountDownLatch;
910
import java.util.concurrent.TimeUnit;
1011
import java.util.concurrent.atomic.AtomicReference;
1112

1213
import io.modelcontextprotocol.client.transport.ServerParameters;
1314
import io.modelcontextprotocol.client.transport.StdioClientTransport;
1415
import io.modelcontextprotocol.spec.McpClientTransport;
16+
import io.modelcontextprotocol.spec.McpSchema;
1517
import org.junit.jupiter.api.Test;
1618
import org.junit.jupiter.api.Timeout;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.ValueSource;
1721
import reactor.core.publisher.Sinks;
1822
import reactor.test.StepVerifier;
1923

2024
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.assertj.core.api.Assertions.fail;
26+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
2127

2228
/**
2329
* Tests for the {@link McpSyncClient} with {@link StdioClientTransport}.
@@ -67,6 +73,60 @@ void customErrorHandlerShouldReceiveErrors() throws InterruptedException {
6773
StepVerifier.create(transport.closeGracefully()).expectComplete().verify(Duration.ofSeconds(5));
6874
}
6975

76+
@ParameterizedTest
77+
@ValueSource(strings = { "success", "error", "debug" })
78+
void testMessageAnnotations(String messageType) {
79+
McpClientTransport transport = createMcpTransport();
80+
81+
withClient(transport, client -> {
82+
client.initialize();
83+
84+
McpSchema.CallToolResult result = client.callTool(new McpSchema.CallToolRequest("annotatedMessage",
85+
Map.of("messageType", messageType, "includeImage", true)));
86+
87+
assertThat(result).isNotNull();
88+
assertThat(result.isError()).isNotEqualTo(true);
89+
assertThat(result.content()).isNotEmpty();
90+
assertThat(result.content()).allSatisfy(content -> {
91+
switch (content.type()) {
92+
case "text":
93+
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class, content);
94+
assertThat(textContent.text()).isNotEmpty();
95+
assertThat(textContent.annotations()).isNotNull();
96+
97+
switch (messageType) {
98+
case "error":
99+
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
100+
assertThat(textContent.annotations().audience()).containsOnly(McpSchema.Role.USER,
101+
McpSchema.Role.ASSISTANT);
102+
break;
103+
case "success":
104+
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
105+
assertThat(textContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
106+
break;
107+
case "debug":
108+
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
109+
assertThat(textContent.annotations().audience())
110+
.containsExactly(McpSchema.Role.ASSISTANT);
111+
break;
112+
default:
113+
throw new IllegalStateException("Unexpected value: " + content.type());
114+
}
115+
break;
116+
case "image":
117+
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class, content);
118+
assertThat(imageContent.data()).isNotEmpty();
119+
assertThat(imageContent.annotations()).isNotNull();
120+
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
121+
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
122+
break;
123+
default:
124+
fail("Unexpected content type: " + content.type());
125+
}
126+
});
127+
});
128+
}
129+
70130
protected Duration getInitializationTimeout() {
71131
return Duration.ofSeconds(6);
72132
}

0 commit comments

Comments
 (0)