From 9a63538840e2c73324e00f24676c5d58ea898087 Mon Sep 17 00:00:00 2001 From: Luca Chang Date: Fri, 16 May 2025 15:53:33 -0700 Subject: [PATCH] feat: add AudioContent support to MCP schema (#243) - Add AudioContent record class implementing Annotated and Content interfaces - Update Content sealed interface to permit AudioContent - Add audio type handling in Content.type() method - Include AudioContent in JsonSubTypes annotation for polymorphic deserialization - Add comprehensive tests for AudioContent serialization and deserialization - Update error message test to include audio in known type ids https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c87a0da6d8c2436d56a6398023c80b0562224454/schema/2025-03-26/schema.ts#L987-L1009 --- .../modelcontextprotocol/spec/McpSchema.java | 14 ++++++++++- .../spec/McpSchemaTests.java | 24 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index e21d53c..5792aad 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -1359,8 +1359,9 @@ public final class McpSchema { @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = TextContent.class, name = "text"), @JsonSubTypes.Type(value = ImageContent.class, name = "image"), + @JsonSubTypes.Type(value = AudioContent.class, name = "audio"), @JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") }) - public sealed interface Content permits TextContent, ImageContent, EmbeddedResource { + public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource { default String type() { if (this instanceof TextContent) { @@ -1369,6 +1370,9 @@ public final class McpSchema { else if (this instanceof ImageContent) { return "image"; } + else if (this instanceof AudioContent) { + return "audio"; + } else if (this instanceof EmbeddedResource) { return "resource"; } @@ -1398,6 +1402,14 @@ public final class McpSchema { @JsonProperty("mimeType") String mimeType) implements Content { // @formatter:on } + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record AudioContent( // @formatter:off + @JsonProperty("annotations") Annotations annotations, + @JsonProperty("data") String data, + @JsonProperty("mimeType") String mimeType) implements Annotated, Content { // @formatter:on + } + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record EmbeddedResource( // @formatter:off diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 99015d8..6d027b5 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -59,7 +59,7 @@ public class McpSchemaTests { {"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class)) .isInstanceOf(InvalidTypeIdException.class) .hasMessageContaining( - "Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [image, resource, text]"); + "Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [audio, image, resource, text]"); } @Test @@ -84,6 +84,28 @@ public class McpSchemaTests { assertThat(imageContent.mimeType()).isEqualTo("image/png"); } + @Test + void testAudioContent() throws Exception { + McpSchema.AudioContent audioContent = new McpSchema.AudioContent(null, "base64encodeddata", "audio/wav"); + String value = 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 = mapper.readValue(""" + {"type":"audio","data":"base64encodeddata","mimeType":"audio/wav"}""", McpSchema.AudioContent.class); + assertThat(audioContent).isNotNull(); + assertThat(audioContent.type()).isEqualTo("audio"); + assertThat(audioContent.data()).isEqualTo("base64encodeddata"); + assertThat(audioContent.mimeType()).isEqualTo("audio/wav"); + } + @Test void testEmbeddedResource() throws Exception { McpSchema.TextResourceContents resourceContents = new McpSchema.TextResourceContents("resource://test",