From 92167080a3f4c214f49d7ef792ec22a013b6d6fa Mon Sep 17 00:00:00 2001 From: bzsurbhi Date: Wed, 25 Jun 2025 13:21:41 -0700 Subject: [PATCH 1/2] feat: add resource link --- .../modelcontextprotocol/spec/McpSchema.java | 20 ++++++++++-- .../spec/McpSchemaTests.java | 32 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 59713094..9c86ec7a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -1473,8 +1473,9 @@ public record CompleteCompletion( @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, AudioContent, EmbeddedResource { + @JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource"), + @JsonSubTypes.Type(value = ResourceLink.class, name = "resource_link") }) + public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource, ResourceLink { default String type() { if (this instanceof TextContent) { @@ -1489,6 +1490,9 @@ else if (this instanceof AudioContent) { else if (this instanceof EmbeddedResource) { return "resource"; } + else if (this instanceof ResourceLink) { + return "resource_link"; + } throw new IllegalArgumentException("Unknown content type: " + this); } @@ -1601,6 +1605,18 @@ public Double priority() { } } + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record ResourceLink( // @formatter:off + @JsonProperty("name") String name, + @JsonProperty("title") String title, + @JsonProperty("uri") String uri, + @JsonProperty("description") String description, + @JsonProperty("mimeType") String mimeType, + @JsonProperty("annotations") Annotations annotations, + @JsonProperty("size") Integer size) implements Annotated, Content { // @formatter:on + } + // --------------------------- // Roots // --------------------------- diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index df8176a4..72cc4a13 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -60,7 +60,7 @@ void testContentDeserializationWrongType() throws Exception { {"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 = [audio, image, resource, text]"); + "Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [audio, image, resource, resource_link, text]"); } @Test @@ -168,6 +168,36 @@ void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { .isEqualTo("base64encodedblob"); } + @Test + void testResourceLink() throws Exception { + McpSchema.ResourceLink resourceLink = new McpSchema.ResourceLink("main.rs", + "Rust Software Application Main File", "file:///project/src/main.rs", "Primary application entry point", + "text/x-rust", null, null); + String value = mapper.writeValueAsString(resourceLink); + + assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) + .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) + .isObject() + .isEqualTo( + json(""" + {"type":"resource_link","name":"main.rs","title":"Rust Software Application Main File","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""")); + } + + @Test + void testResourceLinkDeserialization() throws Exception { + McpSchema.ResourceLink resourceLink = mapper.readValue( + """ + {"type":"resource_link","name":"main.rs","title":"Rust Software Application Main File","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""", + McpSchema.ResourceLink.class); + assertThat(resourceLink).isNotNull(); + assertThat(resourceLink.type()).isEqualTo("resource_link"); + assertThat(resourceLink.name()).isEqualTo("main.rs"); + assertThat(resourceLink.title()).isEqualTo("Rust Software Application Main File"); + assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs"); + assertThat(resourceLink.description()).isEqualTo("Primary application entry point"); + assertThat(resourceLink.mimeType()).isEqualTo("text/x-rust"); + } + // JSON-RPC Message Types Tests @Test From 1bce2c7e8b7346193fa80791c5fda63453704df3 Mon Sep 17 00:00:00 2001 From: bzsurbhi Date: Thu, 26 Jun 2025 13:47:19 -0700 Subject: [PATCH 2/2] added a common interface for Resource and ResourceLink --- .../modelcontextprotocol/spec/McpSchema.java | 78 ++++++++++++++++++- .../spec/McpSchemaTests.java | 19 ++--- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 9c86ec7a..6ca6ea94 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -479,6 +479,22 @@ public record Annotations( // @formatter:off @JsonProperty("priority") Double priority) { } // @formatter:on + public interface ResourceType { + + String uri(); + + String name(); + + String description(); + + String mimeType(); + + Long size(); + + Annotations annotations(); + + } + /** * A known resource that the server is capable of reading. * @@ -503,7 +519,7 @@ public record Resource( // @formatter:off @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, @JsonProperty("size") Long size, - @JsonProperty("annotations") Annotations annotations) implements Annotated { + @JsonProperty("annotations") Annotations annotations) implements Annotated, ResourceType { /** * @deprecated Only exists for backwards-compatibility purposes. Use @@ -1609,12 +1625,68 @@ public Double priority() { @JsonIgnoreProperties(ignoreUnknown = true) public record ResourceLink( // @formatter:off @JsonProperty("name") String name, - @JsonProperty("title") String title, @JsonProperty("uri") String uri, @JsonProperty("description") String description, @JsonProperty("mimeType") String mimeType, @JsonProperty("annotations") Annotations annotations, - @JsonProperty("size") Integer size) implements Annotated, Content { // @formatter:on + @JsonProperty("size") Long size) implements Annotated, Content, ResourceType { // @formatter:on + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String name; + + private String uri; + + private String description; + + private String mimeType; + + private Annotations annotations; + + private Long size; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder uri(String uri) { + this.uri = uri; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder mimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + public Builder annotations(Annotations annotations) { + this.annotations = annotations; + return this; + } + + public Builder size(Long size) { + this.size = size; + return this; + } + + public ResourceLink build() { + Assert.hasText(uri, "uri must not be empty"); + Assert.hasText(name, "name must not be empty"); + + return new ResourceLink(name, uri, description, mimeType, annotations, size); + } + + } } // --------------------------- diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 72cc4a13..dcd302f9 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -3,23 +3,22 @@ */ 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import net.javacrumbs.jsonunit.core.Option; /** @@ -170,9 +169,8 @@ void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { @Test void testResourceLink() throws Exception { - McpSchema.ResourceLink resourceLink = new McpSchema.ResourceLink("main.rs", - "Rust Software Application Main File", "file:///project/src/main.rs", "Primary application entry point", - "text/x-rust", null, null); + McpSchema.ResourceLink resourceLink = new McpSchema.ResourceLink("main.rs", "file:///project/src/main.rs", + "Primary application entry point", "text/x-rust", null, null); String value = mapper.writeValueAsString(resourceLink); assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) @@ -180,19 +178,18 @@ void testResourceLink() throws Exception { .isObject() .isEqualTo( json(""" - {"type":"resource_link","name":"main.rs","title":"Rust Software Application Main File","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""")); + {"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""")); } @Test void testResourceLinkDeserialization() throws Exception { McpSchema.ResourceLink resourceLink = mapper.readValue( """ - {"type":"resource_link","name":"main.rs","title":"Rust Software Application Main File","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""", + {"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""", McpSchema.ResourceLink.class); assertThat(resourceLink).isNotNull(); assertThat(resourceLink.type()).isEqualTo("resource_link"); assertThat(resourceLink.name()).isEqualTo("main.rs"); - assertThat(resourceLink.title()).isEqualTo("Rust Software Application Main File"); assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs"); assertThat(resourceLink.description()).isEqualTo("Primary application entry point"); assertThat(resourceLink.mimeType()).isEqualTo("text/x-rust");