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>
This commit is contained in:
parent
ebb9764c1b
commit
2f55dd0015
2 changed files with 146 additions and 9 deletions
|
|
@ -479,6 +479,28 @@ public final class McpSchema {
|
||||||
@JsonProperty("priority") Double priority) {
|
@JsonProperty("priority") Double priority) {
|
||||||
} // @formatter:on
|
} // @formatter:on
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common interface for resource content, which includes metadata about the resource
|
||||||
|
* such as its URI, name, description, MIME type, size, and annotations. This
|
||||||
|
* interface is implemented by both {@link Resource} and {@link ResourceLink} to
|
||||||
|
* provide a consistent way to access resource metadata.
|
||||||
|
*/
|
||||||
|
public interface ResourceContent {
|
||||||
|
|
||||||
|
String uri();
|
||||||
|
|
||||||
|
String name();
|
||||||
|
|
||||||
|
String description();
|
||||||
|
|
||||||
|
String mimeType();
|
||||||
|
|
||||||
|
Long size();
|
||||||
|
|
||||||
|
Annotations annotations();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A known resource that the server is capable of reading.
|
* A known resource that the server is capable of reading.
|
||||||
*
|
*
|
||||||
|
|
@ -503,7 +525,7 @@ public final class McpSchema {
|
||||||
@JsonProperty("description") String description,
|
@JsonProperty("description") String description,
|
||||||
@JsonProperty("mimeType") String mimeType,
|
@JsonProperty("mimeType") String mimeType,
|
||||||
@JsonProperty("size") Long size,
|
@JsonProperty("size") Long size,
|
||||||
@JsonProperty("annotations") Annotations annotations) implements Annotated {
|
@JsonProperty("annotations") Annotations annotations) implements Annotated, ResourceContent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Only exists for backwards-compatibility purposes. Use
|
* @deprecated Only exists for backwards-compatibility purposes. Use
|
||||||
|
|
@ -1473,8 +1495,9 @@ public final class McpSchema {
|
||||||
@JsonSubTypes({ @JsonSubTypes.Type(value = TextContent.class, name = "text"),
|
@JsonSubTypes({ @JsonSubTypes.Type(value = TextContent.class, name = "text"),
|
||||||
@JsonSubTypes.Type(value = ImageContent.class, name = "image"),
|
@JsonSubTypes.Type(value = ImageContent.class, name = "image"),
|
||||||
@JsonSubTypes.Type(value = AudioContent.class, name = "audio"),
|
@JsonSubTypes.Type(value = AudioContent.class, name = "audio"),
|
||||||
@JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") })
|
@JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource"),
|
||||||
public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource {
|
@JsonSubTypes.Type(value = ResourceLink.class, name = "resource_link") })
|
||||||
|
public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource, ResourceLink {
|
||||||
|
|
||||||
default String type() {
|
default String type() {
|
||||||
if (this instanceof TextContent) {
|
if (this instanceof TextContent) {
|
||||||
|
|
@ -1489,6 +1512,9 @@ public final class McpSchema {
|
||||||
else if (this instanceof EmbeddedResource) {
|
else if (this instanceof EmbeddedResource) {
|
||||||
return "resource";
|
return "resource";
|
||||||
}
|
}
|
||||||
|
else if (this instanceof ResourceLink) {
|
||||||
|
return "resource_link";
|
||||||
|
}
|
||||||
throw new IllegalArgumentException("Unknown content type: " + this);
|
throw new IllegalArgumentException("Unknown content type: " + this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1601,6 +1627,90 @@ public final class McpSchema {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A known resource that the server is capable of reading.
|
||||||
|
*
|
||||||
|
* @param uri the URI of the resource.
|
||||||
|
* @param name A human-readable name for this resource. This can be used by clients to
|
||||||
|
* populate UI elements.
|
||||||
|
* @param description A description of what this resource represents. This can be used
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ResourceLink( // @formatter:off
|
||||||
|
@JsonProperty("name") String name,
|
||||||
|
@JsonProperty("uri") String uri,
|
||||||
|
@JsonProperty("description") String description,
|
||||||
|
@JsonProperty("mimeType") String mimeType,
|
||||||
|
@JsonProperty("size") Long size,
|
||||||
|
@JsonProperty("annotations") Annotations annotations) implements Annotated, Content, ResourceContent { // @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, size, annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Roots
|
// Roots
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,22 @@
|
||||||
*/
|
*/
|
||||||
package io.modelcontextprotocol.spec;
|
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.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
|
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
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;
|
import net.javacrumbs.jsonunit.core.Option;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,7 +59,7 @@ public class McpSchemaTests {
|
||||||
{"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class))
|
{"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class))
|
||||||
.isInstanceOf(InvalidTypeIdException.class)
|
.isInstanceOf(InvalidTypeIdException.class)
|
||||||
.hasMessageContaining(
|
.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
|
@Test
|
||||||
|
|
@ -168,6 +167,34 @@ public class McpSchemaTests {
|
||||||
.isEqualTo("base64encodedblob");
|
.isEqualTo("base64encodedblob");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testResourceLink() throws Exception {
|
||||||
|
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)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isObject()
|
||||||
|
.isEqualTo(
|
||||||
|
json("""
|
||||||
|
{"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","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.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
|
// JSON-RPC Message Types Tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue