diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index d883af252..15bc1fdf7 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -5019,6 +5019,40 @@ public record ResourceLink( // @formatter:off @JsonProperty("annotations") Annotations annotations, @JsonProperty("_meta") Map meta) implements Content, ResourceContent { // @formatter:on + public ResourceLink { + Assert.notNull(uri, "uri must not be null"); + Assert.notNull(name, "name must not be null"); + } + + @JsonCreator + static ResourceLink fromJson(@JsonProperty("name") String name, @JsonProperty("title") String title, + @JsonProperty("uri") String uri, @JsonProperty("description") String description, + @JsonProperty("mimeType") String mimeType, @JsonProperty("size") Long size, + @JsonProperty("annotations") Annotations annotations, @JsonProperty("_meta") Map meta) { + if (name == null || uri == null) { + List missing = new ArrayList<>(); + if (name == null) { + missing.add("name -> ''"); + name = ""; + } + if (uri == null) { + missing.add("uri -> ''"); + uri = ""; + } + logger.warn("ResourceLink: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new ResourceLink(name, title, uri, description, mimeType, size, annotations, meta); + } + + public static Builder builder(String uri, String name) { + return new Builder(uri, name); + } + + /** + * @deprecated Use {@link #builder(String, String)} instead. + */ + @Deprecated public static Builder builder() { return new Builder(); } @@ -5041,6 +5075,24 @@ public static class Builder { private Map meta; + /** + * @deprecated Use {@link ResourceLink#builder(String, String)} instead. + */ + @Deprecated + public Builder() { + } + + private Builder(String uri, String name) { + Assert.hasText(uri, "uri must not be empty"); + Assert.hasText(name, "name must not be empty"); + this.uri = uri; + this.name = name; + } + + /** + * @deprecated Use {@link ResourceLink#builder(String, String)} instead. + */ + @Deprecated public Builder name(String name) { this.name = name; return this; @@ -5051,6 +5103,10 @@ public Builder title(String title) { return this; } + /** + * @deprecated Use {@link ResourceLink#builder(String, String)} instead. + */ + @Deprecated public Builder uri(String uri) { this.uri = uri; return this; diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 31265ec6c..a7fa6c0dc 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -228,10 +228,8 @@ void testEmbeddedResourceWithBlobContentsDeserialization() throws Exception { @Test void testResourceLink() throws Exception { - McpSchema.ResourceLink resourceLink = McpSchema.ResourceLink.builder() - .name("main.rs") + McpSchema.ResourceLink resourceLink = McpSchema.ResourceLink.builder("file:///project/src/main.rs", "main.rs") .title("Main file") - .uri("file:///project/src/main.rs") .description("Primary application entry point") .mimeType("text/x-rust") .meta(Map.of("metaKey", "metaValue")) @@ -261,6 +259,44 @@ void testResourceLinkDeserialization() throws Exception { assertThat(resourceLink.meta()).containsEntry("metaKey", "metaValue"); } + @Test + void testResourceLinkRejectsNullName() { + assertThatThrownBy(() -> new McpSchema.ResourceLink(null, null, "file:///project/src/main.rs", null, null, null, + null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("name must not be null"); + } + + @Test + void testResourceLinkRejectsNullUri() { + assertThatThrownBy(() -> new McpSchema.ResourceLink("main.rs", null, null, null, null, null, null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("uri must not be null"); + } + + @Test + void testResourceLinkDeserializationWithMissingRequiredFields() throws Exception { + McpSchema.ResourceLink resourceLink = JSON_MAPPER.readValue(""" + {"type":"resource_link","description":"Primary application entry point"}""", + McpSchema.ResourceLink.class); + + assertThat(resourceLink).isNotNull(); + assertThat(resourceLink.name()).isEmpty(); + assertThat(resourceLink.uri()).isEmpty(); + assertThat(resourceLink.description()).isEqualTo("Primary application entry point"); + } + + @Test + void testResourceLinkUnknownFieldsIgnored() throws Exception { + McpSchema.ResourceLink resourceLink = JSON_MAPPER.readValue( + """ + {"type":"resource_link","name":"main.rs","uri":"file:///project/src/main.rs","futureField":"ignored"}""", + McpSchema.ResourceLink.class); + + assertThat(resourceLink.name()).isEqualTo("main.rs"); + assertThat(resourceLink.uri()).isEqualTo("file:///project/src/main.rs"); + } + // JSON-RPC Message Types Tests @Test