Skip to content

Commit 2f55dd0

Browse files
bzsurbhitzolov
authored andcommitted
feat: add ResourceLink content type and ResourceContent interface (#341)
- Add ResourceContent interface with common resource metadata methods - Implement ResourceContent in existing Resource class - Add new ResourceLink record class implementing Content and ResourceContent - Update Content interface to support ResourceLink with "resource_link" type (breaking!) - Add tests for ResourceLink serialization/deserialization - Update test expectations to include new resource_link type Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent ebb9764 commit 2f55dd0

File tree

2 files changed

+146
-9
lines changed

2 files changed

+146
-9
lines changed

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

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,28 @@ public record Annotations( // @formatter:off
479479
@JsonProperty("priority") Double priority) {
480480
} // @formatter:on
481481

482+
/**
483+
* A common interface for resource content, which includes metadata about the resource
484+
* such as its URI, name, description, MIME type, size, and annotations. This
485+
* interface is implemented by both {@link Resource} and {@link ResourceLink} to
486+
* provide a consistent way to access resource metadata.
487+
*/
488+
public interface ResourceContent {
489+
490+
String uri();
491+
492+
String name();
493+
494+
String description();
495+
496+
String mimeType();
497+
498+
Long size();
499+
500+
Annotations annotations();
501+
502+
}
503+
482504
/**
483505
* A known resource that the server is capable of reading.
484506
*
@@ -503,7 +525,7 @@ public record Resource( // @formatter:off
503525
@JsonProperty("description") String description,
504526
@JsonProperty("mimeType") String mimeType,
505527
@JsonProperty("size") Long size,
506-
@JsonProperty("annotations") Annotations annotations) implements Annotated {
528+
@JsonProperty("annotations") Annotations annotations) implements Annotated, ResourceContent {
507529

508530
/**
509531
* @deprecated Only exists for backwards-compatibility purposes. Use
@@ -1473,8 +1495,9 @@ public record CompleteCompletion(
14731495
@JsonSubTypes({ @JsonSubTypes.Type(value = TextContent.class, name = "text"),
14741496
@JsonSubTypes.Type(value = ImageContent.class, name = "image"),
14751497
@JsonSubTypes.Type(value = AudioContent.class, name = "audio"),
1476-
@JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") })
1477-
public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource {
1498+
@JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource"),
1499+
@JsonSubTypes.Type(value = ResourceLink.class, name = "resource_link") })
1500+
public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource, ResourceLink {
14781501

14791502
default String type() {
14801503
if (this instanceof TextContent) {
@@ -1489,6 +1512,9 @@ else if (this instanceof AudioContent) {
14891512
else if (this instanceof EmbeddedResource) {
14901513
return "resource";
14911514
}
1515+
else if (this instanceof ResourceLink) {
1516+
return "resource_link";
1517+
}
14921518
throw new IllegalArgumentException("Unknown content type: " + this);
14931519
}
14941520

@@ -1601,6 +1627,90 @@ public Double priority() {
16011627
}
16021628
}
16031629

1630+
/**
1631+
* A known resource that the server is capable of reading.
1632+
*
1633+
* @param uri the URI of the resource.
1634+
* @param name A human-readable name for this resource. This can be used by clients to
1635+
* populate UI elements.
1636+
* @param description A description of what this resource represents. This can be used
1637+
* by clients to improve the LLM's understanding of available resources. It can be
1638+
* thought of like a "hint" to the model.
1639+
* @param mimeType The MIME type of this resource, if known.
1640+
* @param size The size of the raw resource content, in bytes (i.e., before base64
1641+
* encoding or any tokenization), if known. This can be used by Hosts to display file
1642+
* sizes and estimate context window usage.
1643+
* @param annotations Optional annotations for the client. The client can use
1644+
* annotations to inform how objects are used or displayed.
1645+
*/
1646+
@JsonInclude(JsonInclude.Include.NON_ABSENT)
1647+
@JsonIgnoreProperties(ignoreUnknown = true)
1648+
public record ResourceLink( // @formatter:off
1649+
@JsonProperty("name") String name,
1650+
@JsonProperty("uri") String uri,
1651+
@JsonProperty("description") String description,
1652+
@JsonProperty("mimeType") String mimeType,
1653+
@JsonProperty("size") Long size,
1654+
@JsonProperty("annotations") Annotations annotations) implements Annotated, Content, ResourceContent { // @formatter:on
1655+
1656+
public static Builder builder() {
1657+
return new Builder();
1658+
}
1659+
1660+
public static class Builder {
1661+
1662+
private String name;
1663+
1664+
private String uri;
1665+
1666+
private String description;
1667+
1668+
private String mimeType;
1669+
1670+
private Annotations annotations;
1671+
1672+
private Long size;
1673+
1674+
public Builder name(String name) {
1675+
this.name = name;
1676+
return this;
1677+
}
1678+
1679+
public Builder uri(String uri) {
1680+
this.uri = uri;
1681+
return this;
1682+
}
1683+
1684+
public Builder description(String description) {
1685+
this.description = description;
1686+
return this;
1687+
}
1688+
1689+
public Builder mimeType(String mimeType) {
1690+
this.mimeType = mimeType;
1691+
return this;
1692+
}
1693+
1694+
public Builder annotations(Annotations annotations) {
1695+
this.annotations = annotations;
1696+
return this;
1697+
}
1698+
1699+
public Builder size(Long size) {
1700+
this.size = size;
1701+
return this;
1702+
}
1703+
1704+
public ResourceLink build() {
1705+
Assert.hasText(uri, "uri must not be empty");
1706+
Assert.hasText(name, "name must not be empty");
1707+
1708+
return new ResourceLink(name, uri, description, mimeType, size, annotations);
1709+
}
1710+
1711+
}
1712+
}
1713+
16041714
// ---------------------------
16051715
// Roots
16061716
// ---------------------------

mcp/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,22 @@
33
*/
44
package io.modelcontextprotocol.spec;
55

6-
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
7-
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
8-
import static org.assertj.core.api.Assertions.assertThat;
9-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10-
116
import java.util.Arrays;
127
import java.util.Collections;
138
import java.util.HashMap;
149
import java.util.List;
1510
import java.util.Map;
1611

12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1714
import org.junit.jupiter.api.Test;
1815

1916
import com.fasterxml.jackson.databind.ObjectMapper;
2017
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
2118

2219
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
20+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
21+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
2322
import net.javacrumbs.jsonunit.core.Option;
2423

2524
/**
@@ -60,7 +59,7 @@ void testContentDeserializationWrongType() throws Exception {
6059
{"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class))
6160
.isInstanceOf(InvalidTypeIdException.class)
6261
.hasMessageContaining(
63-
"Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [audio, image, resource, text]");
62+
"Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [audio, image, resource, resource_link, text]");
6463
}
6564

6665
@Test
@@ -168,6 +167,34 @@ void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception {
168167
.isEqualTo("base64encodedblob");
169168
}
170169

170+
@Test
171+
void testResourceLink() throws Exception {
172+
McpSchema.ResourceLink resourceLink = new McpSchema.ResourceLink("main.rs", "file:///project/src/main.rs",
173+
"Primary application entry point", "text/x-rust", null, null);
174+
String value = mapper.writeValueAsString(resourceLink);
175+
176+
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
177+
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
178+
.isObject()
179+
.isEqualTo(
180+
json("""
181+
{"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}"""));
182+
}
183+
184+
@Test
185+
void testResourceLinkDeserialization() throws Exception {
186+
McpSchema.ResourceLink resourceLink = mapper.readValue(
187+
"""
188+
{"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","description":"Primary application entry point","mimeType":"text/x-rust"}""",
189+
McpSchema.ResourceLink.class);
190+
assertThat(resourceLink).isNotNull();
191+
assertThat(resourceLink.type()).isEqualTo("resource_link");
192+
assertThat(resourceLink.name()).isEqualTo("main.rs");
193+
assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs");
194+
assertThat(resourceLink.description()).isEqualTo("Primary application entry point");
195+
assertThat(resourceLink.mimeType()).isEqualTo("text/x-rust");
196+
}
197+
171198
// JSON-RPC Message Types Tests
172199

173200
@Test

0 commit comments

Comments
 (0)