From 6c330db5ad93585df0a83d1bdd273b786ea521f0 Mon Sep 17 00:00:00 2001 From: Luca Chang Date: Thu, 19 Jun 2025 14:41:56 -0700 Subject: [PATCH] feat: add size field and builder pattern to Resource record (#331) The size field helps hosts display file sizes and estimate context window usage. - Add size parameter to Resource record for tracking raw content size in bytes - Implement Builder pattern for Resource construction with validation - Add deprecated constructor for backwards compatibility - Add tests for builder pattern and field validation Resolves #330 Signed-off-by: Christian Tzolov --- .../modelcontextprotocol/spec/McpSchema.java | 64 +++++++++++++++++ .../spec/ArgumentException.java | 5 ++ .../spec/McpSchemaTests.java | 72 ++++++++++++++++--- 3 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 mcp/src/test/java/io/modelcontextprotocol/spec/ArgumentException.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index a78600a..ab69af8 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; * Context Protocol Schema. * * @author Christian Tzolov + * @author Luca Chang */ public final class McpSchema { @@ -482,6 +483,9 @@ public final class McpSchema { * by clients to improve the LLM's understanding of available resources. It can be * thought of like a "hint" to the model. * @param mimeType The MIME type of this resource, if known. + * @param size The size of the raw resource content, in bytes (i.e., before base64 + * encoding or any tokenization), if known. This can be used by Hosts to display file + * sizes and estimate context window usage. * @param annotations Optional annotations for the client. The client can use * annotations to inform how objects are used or displayed. */ @@ -492,7 +496,67 @@ public final class McpSchema { @JsonProperty("name") String name, @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, + @JsonProperty("size") Long size, @JsonProperty("annotations") Annotations annotations) implements Annotated { + + /** + * @deprecated Only exists for backwards-compatibility purposes. Use + * {@link Resource#builder()} instead. + */ + @Deprecated + public Resource(String uri, String name, String description, String mimeType, Annotations annotations) { + this(uri, name, description, mimeType, null, annotations); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String uri; + private String name; + private String description; + private String mimeType; + private Long size; + private Annotations annotations; + + public Builder uri(String uri) { + this.uri = uri; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder mimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + public Builder size(Long size) { + this.size = size; + return this; + } + + public Builder annotations(Annotations annotations) { + this.annotations = annotations; + return this; + } + + public Resource build() { + Assert.hasText(uri, "uri must not be empty"); + Assert.hasText(name, "name must not be empty"); + + return new Resource(uri, name, description, mimeType, size, annotations); + } + } } // @formatter:on /** diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/ArgumentException.java b/mcp/src/test/java/io/modelcontextprotocol/spec/ArgumentException.java new file mode 100644 index 0000000..ba4e851 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/ArgumentException.java @@ -0,0 +1,5 @@ +package io.modelcontextprotocol.spec; + +public class ArgumentException { + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 6d027b5..f8cc08f 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -3,23 +3,24 @@ */ package io.modelcontextprotocol.spec; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; -import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; -import net.javacrumbs.jsonunit.core.Option; import org.junit.jupiter.api.Test; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; + +import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; +import net.javacrumbs.jsonunit.core.Option; /** * @author Christian Tzolov @@ -295,6 +296,59 @@ public class McpSchemaTests { {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","annotations":{"audience":["user","assistant"],"priority":0.8}}""")); } + @Test + void testResourceBuilder() throws Exception { + McpSchema.Annotations annotations = new McpSchema.Annotations( + Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), 0.8); + + McpSchema.Resource resource = McpSchema.Resource.builder() + .uri("resource://test") + .name("Test Resource") + .description("A test resource") + .mimeType("text/plain") + .size(256L) + .annotations(annotations) + .build(); + + String value = mapper.writeValueAsString(resource); + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"uri":"resource://test","name":"Test Resource","description":"A test resource","mimeType":"text/plain","size":256,"annotations":{"audience":["user","assistant"],"priority":0.8}}""")); + } + + @Test + void testResourceBuilderUriRequired() { + McpSchema.Annotations annotations = new McpSchema.Annotations( + Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), 0.8); + + McpSchema.Resource.Builder resourceBuilder = McpSchema.Resource.builder() + .name("Test Resource") + .description("A test resource") + .mimeType("text/plain") + .size(256L) + .annotations(annotations); + + assertThatThrownBy(resourceBuilder::build).isInstanceOf(java.lang.IllegalArgumentException.class); + } + + @Test + void testResourceBuilderNameRequired() { + McpSchema.Annotations annotations = new McpSchema.Annotations( + Arrays.asList(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), 0.8); + + McpSchema.Resource.Builder resourceBuilder = McpSchema.Resource.builder() + .uri("resource://test") + .description("A test resource") + .mimeType("text/plain") + .size(256L) + .annotations(annotations); + + assertThatThrownBy(resourceBuilder::build).isInstanceOf(java.lang.IllegalArgumentException.class); + } + @Test void testResourceTemplate() throws Exception { McpSchema.Annotations annotations = new McpSchema.Annotations(Arrays.asList(McpSchema.Role.USER), 0.5);