diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java
index 47f5452950..b7bd906a0f 100644
--- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java
+++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/adapter/ResourceAdapter.java
@@ -16,12 +16,11 @@
package org.springframework.ai.mcp.annotation.adapter;
-import java.util.List;
-
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.common.MetaUtils;
+import org.springframework.ai.mcp.annotation.common.ResourceAnnotationsUtils;
/**
* Utility class that converts {@link McpResource} annotations into MCP schema objects.
@@ -45,27 +44,15 @@ public static McpSchema.Resource asResource(McpResource mcpResourceAnnotation) {
name = "resource"; // Default name when not specified
}
var meta = MetaUtils.getMeta(mcpResourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(mcpResourceAnnotation.annotations());
- var resourceBuilder = McpSchema.Resource.builder(mcpResourceAnnotation.uri(), name)
+ return McpSchema.Resource.builder(mcpResourceAnnotation.uri(), name)
.title(mcpResourceAnnotation.title())
.description(mcpResourceAnnotation.description())
.mimeType(mcpResourceAnnotation.mimeType())
- .meta(meta);
-
- // 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(McpSchema.Annotations.builder()
- .audience(List.of(annotations.audience()))
- .priority(annotations.priority())
- .build());
- }
-
- return resourceBuilder.build();
+ .annotations(annotations)
+ .meta(meta)
+ .build();
}
public static McpSchema.ResourceTemplate asResourceTemplate(McpResource mcpResource) {
@@ -74,10 +61,12 @@ public static McpSchema.ResourceTemplate asResourceTemplate(McpResource mcpResou
name = "resource"; // Default name when not specified
}
var meta = MetaUtils.getMeta(mcpResource.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(mcpResource.annotations());
return McpSchema.ResourceTemplate.builder(mcpResource.uri(), name)
.description(mcpResource.description())
.mimeType(mcpResource.mimeType())
+ .annotations(annotations)
.meta(meta)
.build();
}
diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/common/ResourceAnnotationsUtils.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/common/ResourceAnnotationsUtils.java
new file mode 100644
index 0000000000..58e4f6cbec
--- /dev/null
+++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/common/ResourceAnnotationsUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.mcp.annotation.common;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import io.modelcontextprotocol.spec.McpSchema;
+
+import org.springframework.ai.mcp.annotation.McpResource;
+import org.springframework.ai.mcp.annotation.McpResource.McpAnnotations;
+
+/**
+ * Utility for converting the nested {@link McpAnnotations} value on a {@link McpResource}
+ * declaration into the wire-level {@link McpSchema.Annotations} record.
+ *
+ *
+ * Java annotation elements can never be {@code null}, so the compiler always materializes
+ * the declared default. This means every {@code @McpResource} instance appears to carry
+ * an "annotations" value regardless of whether the user actually set one. To avoid
+ * publishing that spurious default to MCP clients, this helper first compares the runtime
+ * value against the declared default of {@code McpResource#annotations()} — obtained via
+ * reflection so no default constants have to be duplicated — and returns {@code null}
+ * whenever they are equal (annotations use structural equality). Any user-supplied value
+ * is copied verbatim, including {@code audience}, {@code priority} and
+ * {@code lastModified}.
+ *
+ *
+ * @author Shiyang Chen
+ */
+public final class ResourceAnnotationsUtils {
+
+ private static final McpAnnotations DEFAULT_ANNOTATIONS = resolveDefaultAnnotations();
+
+ private ResourceAnnotationsUtils() {
+ }
+
+ /**
+ * Convert a {@link McpAnnotations} value declared on a {@link McpResource} into the
+ * corresponding {@link McpSchema.Annotations} payload.
+ * @param annotations the annotation value as returned by
+ * {@link McpResource#annotations()}. May be {@code null} for defensive use, in which
+ * case {@code null} is returned.
+ * @return the converted schema annotations, or {@code null} if {@code annotations} is
+ * {@code null} or equal to the declared default.
+ */
+ public static McpSchema.Annotations toSchemaAnnotations(McpAnnotations annotations) {
+ if (annotations == null || annotations.equals(DEFAULT_ANNOTATIONS)) {
+ return null;
+ }
+ return McpSchema.Annotations.builder()
+ .audience(List.of(annotations.audience()))
+ .priority(annotations.priority())
+ .lastModified(annotations.lastModified())
+ .build();
+ }
+
+ private static McpAnnotations resolveDefaultAnnotations() {
+ try {
+ Method annotationsMethod = McpResource.class.getDeclaredMethod("annotations");
+ Object defaultValue = annotationsMethod.getDefaultValue();
+ if (defaultValue instanceof McpAnnotations mcpAnnotations) {
+ return mcpAnnotations;
+ }
+ return null;
+ }
+ catch (NoSuchMethodException ex) {
+ throw new IllegalStateException("McpResource#annotations() is expected to be defined", ex);
+ }
+ }
+
+}
diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java
index 1d87b3f5d5..3dacae1c3e 100644
--- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java
+++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProvider.java
@@ -37,6 +37,7 @@
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.common.McpPredicates;
import org.springframework.ai.mcp.annotation.common.MetaUtils;
+import org.springframework.ai.mcp.annotation.common.ResourceAnnotationsUtils;
import org.springframework.ai.mcp.annotation.method.resource.AsyncMcpResourceMethodCallback;
/**
@@ -92,10 +93,12 @@ public List getResourceSpecifications() {
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResource = McpSchema.Resource.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
@@ -145,10 +148,12 @@ public List getResourceTemplateSpecification
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java
index 2861a67d28..864290c277 100644
--- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java
+++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProvider.java
@@ -36,6 +36,7 @@
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.common.McpPredicates;
import org.springframework.ai.mcp.annotation.common.MetaUtils;
+import org.springframework.ai.mcp.annotation.common.ResourceAnnotationsUtils;
import org.springframework.ai.mcp.annotation.method.resource.AsyncStatelessMcpResourceMethodCallback;
/**
@@ -92,10 +93,12 @@ public List getResourceSpecifications() {
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResource = McpSchema.Resource.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
@@ -145,10 +148,12 @@ public List getResourceTemplateSpecification
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java
index c152c22ac0..84266cc224 100644
--- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java
+++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProvider.java
@@ -29,6 +29,7 @@
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.common.McpPredicates;
import org.springframework.ai.mcp.annotation.common.MetaUtils;
+import org.springframework.ai.mcp.annotation.common.ResourceAnnotationsUtils;
import org.springframework.ai.mcp.annotation.method.resource.SyncMcpResourceMethodCallback;
/**
@@ -66,10 +67,12 @@ public List getResourceSpecifications() {
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResource = McpSchema.Resource.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
@@ -109,10 +112,12 @@ public List getResourceTemplateSpecifications
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java
index d257ba3979..cf62bf8a4d 100644
--- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java
+++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProvider.java
@@ -35,6 +35,7 @@
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.common.McpPredicates;
import org.springframework.ai.mcp.annotation.common.MetaUtils;
+import org.springframework.ai.mcp.annotation.common.ResourceAnnotationsUtils;
import org.springframework.ai.mcp.annotation.method.resource.SyncStatelessMcpResourceMethodCallback;
/**
@@ -91,10 +92,12 @@ public List getResourceSpecifications() {
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResource = McpSchema.Resource.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
@@ -144,10 +147,12 @@ public List getResourceTemplateSpecifications
var description = resourceAnnotation.description();
var mimeType = resourceAnnotation.mimeType();
var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider());
+ var annotations = ResourceAnnotationsUtils.toSchemaAnnotations(resourceAnnotation.annotations());
var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name)
.description(description)
.mimeType(mimeType)
+ .annotations(annotations)
.meta(meta)
.build();
diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProviderTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProviderTests.java
index 36507fcb5b..240efb6a54 100644
--- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProviderTests.java
+++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncMcpResourceProviderTests.java
@@ -24,12 +24,14 @@
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 reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.ai.mcp.annotation.McpResource;
+import org.springframework.ai.mcp.annotation.McpResource.McpAnnotations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -495,4 +497,71 @@ public Mono syncMethodReturningMono() {
}).verifyComplete();
}
+ @Test
+ void testDefaultAnnotationsAreNotPropagated() {
+ class NoAnnotationsResource {
+
+ @McpResource(uri = "no-ann://resource", name = "no-ann")
+ public Mono noAnnotations() {
+ return Mono.just("");
+ }
+
+ }
+
+ AsyncMcpResourceProvider provider = new AsyncMcpResourceProvider(List.of(new NoAnnotationsResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ assertThat(resourceSpecs.get(0).resource().annotations()).isNull();
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResource() {
+ class AnnotatedResource {
+
+ @McpResource(uri = "ann://resource", name = "ann",
+ annotations = @McpAnnotations(audience = { Role.ASSISTANT }, priority = 1.0,
+ lastModified = "2026-08-16T00:00:00Z"))
+ public Mono annotated() {
+ return Mono.just("");
+ }
+
+ }
+
+ AsyncMcpResourceProvider provider = new AsyncMcpResourceProvider(List.of(new AnnotatedResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ var annotations = resourceSpecs.get(0).resource().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(1.0);
+ assertThat(annotations.lastModified()).isEqualTo("2026-08-16T00:00:00Z");
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResourceTemplate() {
+ class AnnotatedTemplate {
+
+ @McpResource(uri = "ann://template/{id}", name = "ann-template",
+ annotations = @McpAnnotations(audience = { Role.USER, Role.ASSISTANT }, priority = 0.25))
+ public Mono annotatedTemplate(String id) {
+ return Mono.just(id);
+ }
+
+ }
+
+ AsyncMcpResourceProvider provider = new AsyncMcpResourceProvider(List.of(new AnnotatedTemplate()));
+
+ List resourceTemplateSpecs = provider.getResourceTemplateSpecifications();
+
+ assertThat(resourceTemplateSpecs).hasSize(1);
+ var annotations = resourceTemplateSpecs.get(0).resourceTemplate().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.USER, Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(0.25);
+ }
+
}
diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProviderTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProviderTests.java
index 007a6821b5..f174b90a7f 100644
--- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProviderTests.java
+++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/AsyncStatelessMcpResourceProviderTests.java
@@ -24,12 +24,14 @@
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 reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.ai.mcp.annotation.McpResource;
+import org.springframework.ai.mcp.annotation.McpResource.McpAnnotations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -496,4 +498,74 @@ public Mono syncMethodReturningMono() {
}).verifyComplete();
}
+ @Test
+ void testDefaultAnnotationsAreNotPropagated() {
+ class NoAnnotationsResource {
+
+ @McpResource(uri = "no-ann://resource", name = "no-ann")
+ public Mono noAnnotations() {
+ return Mono.just("");
+ }
+
+ }
+
+ AsyncStatelessMcpResourceProvider provider = new AsyncStatelessMcpResourceProvider(
+ List.of(new NoAnnotationsResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ assertThat(resourceSpecs.get(0).resource().annotations()).isNull();
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResource() {
+ class AnnotatedResource {
+
+ @McpResource(uri = "ann://resource", name = "ann",
+ annotations = @McpAnnotations(audience = { Role.ASSISTANT }, priority = 1.0,
+ lastModified = "2026-08-16T00:00:00Z"))
+ public Mono annotated() {
+ return Mono.just("");
+ }
+
+ }
+
+ AsyncStatelessMcpResourceProvider provider = new AsyncStatelessMcpResourceProvider(
+ List.of(new AnnotatedResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ var annotations = resourceSpecs.get(0).resource().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(1.0);
+ assertThat(annotations.lastModified()).isEqualTo("2026-08-16T00:00:00Z");
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResourceTemplate() {
+ class AnnotatedTemplate {
+
+ @McpResource(uri = "ann://template/{id}", name = "ann-template",
+ annotations = @McpAnnotations(audience = { Role.USER, Role.ASSISTANT }, priority = 0.25))
+ public Mono annotatedTemplate(String id) {
+ return Mono.just(id);
+ }
+
+ }
+
+ AsyncStatelessMcpResourceProvider provider = new AsyncStatelessMcpResourceProvider(
+ List.of(new AnnotatedTemplate()));
+
+ List resourceTemplateSpecs = provider.getResourceTemplateSpecifications();
+
+ assertThat(resourceTemplateSpecs).hasSize(1);
+ var annotations = resourceTemplateSpecs.get(0).resourceTemplate().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.USER, Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(0.25);
+ }
+
}
diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProviderTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProviderTests.java
index ced6075d6e..66566d9fe8 100644
--- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProviderTests.java
+++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncMcpResourceProviderTests.java
@@ -25,11 +25,13 @@
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 reactor.core.publisher.Mono;
import org.springframework.ai.mcp.annotation.McpResource;
+import org.springframework.ai.mcp.annotation.McpResource.McpAnnotations;
import org.springframework.ai.mcp.annotation.context.MetaProvider;
import static org.assertj.core.api.Assertions.assertThat;
@@ -531,6 +533,97 @@ public String noParamResource() {
assertThat(((TextResourceContents) content).text()).isEqualTo("No parameters needed");
}
+ @Test
+ void testDefaultAnnotationsAreNotPropagated() {
+ class NoAnnotationsResource {
+
+ @McpResource(uri = "no-ann://resource", name = "no-ann", description = "no annotations")
+ public String noAnnotations() {
+ return "";
+ }
+
+ }
+
+ SyncMcpResourceProvider provider = new SyncMcpResourceProvider(List.of(new NoAnnotationsResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ assertThat(resourceSpecs.get(0).resource().annotations()).isNull();
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResource() {
+ class AnnotatedResource {
+
+ @McpResource(uri = "ann://resource", name = "ann", description = "explicit annotations",
+ annotations = @McpAnnotations(audience = { Role.ASSISTANT }, priority = 1.0,
+ lastModified = "2026-08-16T00:00:00Z"))
+ public String annotated() {
+ return "";
+ }
+
+ }
+
+ SyncMcpResourceProvider provider = new SyncMcpResourceProvider(List.of(new AnnotatedResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ var annotations = resourceSpecs.get(0).resource().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(1.0);
+ assertThat(annotations.lastModified()).isEqualTo("2026-08-16T00:00:00Z");
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResourceTemplate() {
+ class AnnotatedTemplate {
+
+ @McpResource(uri = "ann://template/{id}", name = "ann-template", description = "annotated template",
+ annotations = @McpAnnotations(audience = { Role.USER, Role.ASSISTANT }, priority = 0.25))
+ public String annotatedTemplate(String id) {
+ return id;
+ }
+
+ }
+
+ SyncMcpResourceProvider provider = new SyncMcpResourceProvider(List.of(new AnnotatedTemplate()));
+
+ List resourceTemplateSpecs = provider.getResourceTemplateSpecifications();
+
+ assertThat(resourceTemplateSpecs).hasSize(1);
+ var annotations = resourceTemplateSpecs.get(0).resourceTemplate().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.USER, Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(0.25);
+ }
+
+ @Test
+ void testAudienceOnlyChangeIsPropagated() {
+ class AudienceOnly {
+
+ @McpResource(uri = "audience-only://resource", name = "audience-only",
+ annotations = @McpAnnotations(audience = { Role.ASSISTANT }, priority = 0.5))
+ public String audienceOnly() {
+ return "";
+ }
+
+ }
+
+ SyncMcpResourceProvider provider = new SyncMcpResourceProvider(List.of(new AudienceOnly()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ var annotations = resourceSpecs.get(0).resource().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(0.5);
+ assertThat(annotations.lastModified()).isNullOrEmpty();
+ }
+
public static class ResourceMetaProvider implements MetaProvider {
@Override
diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProviderTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProviderTests.java
index 2bf25e55b0..f57e7a0708 100644
--- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProviderTests.java
+++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/provider/resource/SyncStatelessMcpResourceProviderTests.java
@@ -24,11 +24,13 @@
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 reactor.core.publisher.Mono;
import org.springframework.ai.mcp.annotation.McpResource;
+import org.springframework.ai.mcp.annotation.McpResource.McpAnnotations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -458,4 +460,74 @@ public String requestResource(ReadResourceRequest request) {
assertThat(((TextResourceContents) content).text()).isEqualTo("Resource for URI: request://resource");
}
+ @Test
+ void testDefaultAnnotationsAreNotPropagated() {
+ class NoAnnotationsResource {
+
+ @McpResource(uri = "no-ann://resource", name = "no-ann")
+ public String noAnnotations() {
+ return "";
+ }
+
+ }
+
+ SyncStatelessMcpResourceProvider provider = new SyncStatelessMcpResourceProvider(
+ List.of(new NoAnnotationsResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ assertThat(resourceSpecs.get(0).resource().annotations()).isNull();
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResource() {
+ class AnnotatedResource {
+
+ @McpResource(uri = "ann://resource", name = "ann",
+ annotations = @McpAnnotations(audience = { Role.ASSISTANT }, priority = 1.0,
+ lastModified = "2026-08-16T00:00:00Z"))
+ public String annotated() {
+ return "";
+ }
+
+ }
+
+ SyncStatelessMcpResourceProvider provider = new SyncStatelessMcpResourceProvider(
+ List.of(new AnnotatedResource()));
+
+ List resourceSpecs = provider.getResourceSpecifications();
+
+ assertThat(resourceSpecs).hasSize(1);
+ var annotations = resourceSpecs.get(0).resource().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(1.0);
+ assertThat(annotations.lastModified()).isEqualTo("2026-08-16T00:00:00Z");
+ }
+
+ @Test
+ void testExplicitAnnotationsArePropagatedToResourceTemplate() {
+ class AnnotatedTemplate {
+
+ @McpResource(uri = "ann://template/{id}", name = "ann-template",
+ annotations = @McpAnnotations(audience = { Role.USER, Role.ASSISTANT }, priority = 0.25))
+ public String annotatedTemplate(String id) {
+ return id;
+ }
+
+ }
+
+ SyncStatelessMcpResourceProvider provider = new SyncStatelessMcpResourceProvider(
+ List.of(new AnnotatedTemplate()));
+
+ List resourceTemplateSpecs = provider.getResourceTemplateSpecifications();
+
+ assertThat(resourceTemplateSpecs).hasSize(1);
+ var annotations = resourceTemplateSpecs.get(0).resourceTemplate().annotations();
+ assertThat(annotations).isNotNull();
+ assertThat(annotations.audience()).containsExactly(Role.USER, Role.ASSISTANT);
+ assertThat(annotations.priority()).isEqualTo(0.25);
+ }
+
}