From ed9d822548e9db31ff6514ce0308aa79f031907e Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sun, 21 Sep 2025 11:05:03 +0200 Subject: [PATCH] feat: enhance McpResource annotation with title and annotations support - Add title field to McpResource annotation for display purposes - Add annotations support with audience, lastModified, and priority fields - Update ResourceAdapter to handle new title and conditional annotations - Rename parameter in ResourceAdapter for better clarity - Update related tests - Add nested McpAnnotations annotation with Role-based audience support Signed-off-by: Christian Tzolov --- .../mcp/adapter/ResourceAdapter.java | 30 ++++++--- .../mcp/annotation/McpResource.java | 44 ++++++++++++- .../AsyncMcpResourceMethodCallbackTests.java | 59 ++++++++++++++++++ ...atelessMcpResourceMethodCallbackTests.java | 59 ++++++++++++++++++ .../McpResourceUriValidationTest.java | 60 ++++++++++++++++++ .../SyncMcpResourceMethodCallbackTests.java | 61 +++++++++++++++++++ ...atelessMcpResourceMethodCallbackTests.java | 61 +++++++++++++++++++ 7 files changed, 365 insertions(+), 9 deletions(-) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/ResourceAdapter.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/ResourceAdapter.java index d6b8d46..5ebdb04 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/ResourceAdapter.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/adapter/ResourceAdapter.java @@ -3,6 +3,8 @@ */ package org.springaicommunity.mcp.adapter; +import java.util.List; + import io.modelcontextprotocol.spec.McpSchema; import org.springaicommunity.mcp.annotation.McpResource; @@ -14,17 +16,31 @@ public class ResourceAdapter { private ResourceAdapter() { } - public static McpSchema.Resource asResource(McpResource mcpResource) { - String name = mcpResource.name(); + public static McpSchema.Resource asResource(McpResource mcpResourceAnnotation) { + String name = mcpResourceAnnotation.name(); if (name == null || name.isEmpty()) { name = "resource"; // Default name when not specified } - return McpSchema.Resource.builder() - .uri(mcpResource.uri()) + + var resourceBuilder = McpSchema.Resource.builder() + .uri(mcpResourceAnnotation.uri()) .name(name) - .description(mcpResource.description()) - .mimeType(mcpResource.mimeType()) - .build(); + .title(mcpResourceAnnotation.title()) + .description(mcpResourceAnnotation.description()) + .mimeType(mcpResourceAnnotation.mimeType()); + + // Only set annotations if not default value is provided + // This is a workaround since Java annotations do not support null default values + // and we want to avoid setting empty annotations. + // The default annotations value is ignored. + // The user must explicitly set the annotations to get them included. + var annotations = mcpResourceAnnotation.annotations(); + if (annotations != null && annotations.lastModified() != null && !annotations.lastModified().isEmpty()) { + resourceBuilder + .annotations(new McpSchema.Annotations(List.of(annotations.audience()), annotations.priority())); + } + + return resourceBuilder.build(); } public static McpSchema.ResourceTemplate asResourceTemplate(McpResource mcpResource) { diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java index 1e44ab1..40e3e0d 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpResource.java @@ -10,6 +10,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.modelcontextprotocol.spec.McpSchema.Role; + /** * Marks a method as a MCP Resource. * @@ -21,11 +23,16 @@ public @interface McpResource { /** - * A human-readable name for this resource. This can be used by clients to populate UI - * elements. + * Intended for programmatic or logical use, but used as a display name in past specs + * or fallback (if title isn’t present). */ String name() default ""; + /** + * Optional human-readable name of the prompt for display purposes. + */ + String title() default ""; + /** * the URI of the resource. */ @@ -43,4 +50,37 @@ */ String mimeType() default "text/plain"; + /** + * Optional annotations for the client. Note: The default annotations value is + * ignored. + */ + McpAnnotations annotations() default @McpAnnotations(audience = { Role.USER }, lastModified = "", priority = 0.5); + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + public @interface McpAnnotations { + + /** + * Describes who the intended customer of this object or data is. It can include + * multiple entries to indicate content useful for multiple audiences (e.g., + * [“user”, “assistant”]). + */ + Role[] audience(); + + /** + * The date and time (in ISO 8601 format) when the resource was last modified. + */ + String lastModified() default ""; + + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means “most important,” and indicates that the data is effectively + * required, while 0 means “least important,” and indicates that the data is + * entirely optional. + */ + double priority(); + + } + } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java index 9118bed..447a990 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java @@ -14,6 +14,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.ResourceContents; +import io.modelcontextprotocol.spec.McpSchema.Role; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import io.modelcontextprotocol.util.McpUriTemplateManager; import io.modelcontextprotocol.util.McpUriTemplateManagerFactory; @@ -252,6 +253,11 @@ public String name() { return ""; } + @Override + public String title() { + return ""; + } + @Override public String description() { return ""; @@ -262,6 +268,30 @@ public String mimeType() { return "text/plain"; } + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; } @@ -562,6 +592,11 @@ public String name() { return ""; } + @Override + public String title() { + return ""; + } + @Override public String description() { return ""; @@ -572,6 +607,30 @@ public String mimeType() { return ""; } + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; assertThatThrownBy(() -> AsyncMcpResourceMethodCallback.builder() diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java index 3a404e3..73bdb70 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java @@ -14,6 +14,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.ResourceContents; +import io.modelcontextprotocol.spec.McpSchema.Role; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import io.modelcontextprotocol.util.McpUriTemplateManager; import io.modelcontextprotocol.util.McpUriTemplateManagerFactory; @@ -207,6 +208,11 @@ public String name() { return ""; } + @Override + public String title() { + return ""; + } + @Override public String description() { return ""; @@ -217,6 +223,30 @@ public String mimeType() { return "text/plain"; } + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; } @@ -523,6 +553,11 @@ public String name() { return ""; } + @Override + public String title() { + return ""; + } + @Override public String description() { return ""; @@ -533,6 +568,30 @@ public String mimeType() { return ""; } + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; assertThatThrownBy(() -> AsyncStatelessMcpResourceMethodCallback.builder() diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java index b9d30ee..5cb4ad4 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/McpResourceUriValidationTest.java @@ -9,6 +9,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; +import io.modelcontextprotocol.spec.McpSchema.Role; import org.springaicommunity.mcp.adapter.ResourceAdapter; import org.springaicommunity.mcp.annotation.McpResource; @@ -50,6 +51,11 @@ public String name() { return ""; } + @Override + public String title() { + return ""; + } + @Override public String description() { return ""; @@ -59,6 +65,31 @@ public String description() { public String mimeType() { return ""; } + + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; } @@ -80,6 +111,11 @@ public String name() { return ""; } + @Override + public String title() { + return ""; + } + @Override public String description() { return ""; @@ -90,6 +126,30 @@ public String mimeType() { return ""; } + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java index 50294dc..476d7bc 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java @@ -13,6 +13,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.ResourceContents; +import io.modelcontextprotocol.spec.McpSchema.Role; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import org.junit.jupiter.api.Test; import org.springaicommunity.mcp.adapter.ResourceAdapter; @@ -223,6 +224,11 @@ public String name() { return "testResource"; } + @Override + public String title() { + return ""; + } + @Override public String description() { return "Test resource description"; @@ -232,6 +238,31 @@ public String description() { public String mimeType() { return "text/plain"; } + + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; } @@ -493,6 +524,11 @@ public String name() { return "testResourceWithExtraVariables"; } + @Override + public String title() { + return ""; + } + @Override public String description() { return "Test resource with extra URI variables"; @@ -502,6 +538,31 @@ public String description() { public String mimeType() { return "text/plain"; } + + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; assertThatThrownBy(() -> SyncMcpResourceMethodCallback.builder() diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java index 62efde8..5348e78 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java @@ -14,6 +14,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.ResourceContents; +import io.modelcontextprotocol.spec.McpSchema.Role; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; import org.junit.jupiter.api.Test; import org.springaicommunity.mcp.adapter.ResourceAdapter; @@ -189,6 +190,11 @@ public String name() { return "testResource"; } + @Override + public String title() { + return ""; + } + @Override public String description() { return "Test resource description"; @@ -198,6 +204,31 @@ public String description() { public String mimeType() { return "text/plain"; } + + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; } @@ -571,6 +602,11 @@ public String name() { return "testResourceWithExtraVariables"; } + @Override + public String title() { + return ""; + } + @Override public String description() { return "Test resource with extra URI variables"; @@ -580,6 +616,31 @@ public String description() { public String mimeType() { return "text/plain"; } + + @Override + public McpAnnotations annotations() { + return new McpAnnotations() { + @Override + public Class annotationType() { + return McpAnnotations.class; + } + + @Override + public Role[] audience() { + return new Role[] { Role.USER }; + } + + @Override + public String lastModified() { + return ""; + } + + @Override + public double priority() { + return 0.5; + } + }; + } }; assertThatThrownBy(() -> SyncStatelessMcpResourceMethodCallback.builder()