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..4c9312ed6c 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 @@ -30,6 +30,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.util.Utils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; @@ -89,11 +90,13 @@ public List getResourceSpecifications() { } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResource = McpSchema.Resource.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) @@ -142,11 +145,13 @@ public List getResourceTemplateSpecification } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) 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..d882c8546e 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 @@ -29,6 +29,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.util.Utils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; @@ -89,11 +90,13 @@ public List getResourceSpecifications() { } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResource = McpSchema.Resource.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) @@ -142,11 +145,13 @@ public List getResourceTemplateSpecification } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) 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..87eafbdfeb 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 @@ -25,6 +25,7 @@ import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.util.Utils; import org.springframework.ai.mcp.annotation.McpResource; import org.springframework.ai.mcp.annotation.common.McpPredicates; @@ -63,11 +64,13 @@ public List getResourceSpecifications() { } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResource = McpSchema.Resource.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) @@ -106,11 +109,13 @@ public List getResourceTemplateSpecifications } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) 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..8f5ba8013e 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 @@ -29,6 +29,7 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.util.Assert; +import io.modelcontextprotocol.util.Utils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -88,11 +89,13 @@ public List getResourceSpecifications() { } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResource = McpSchema.Resource.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) @@ -141,11 +144,13 @@ public List getResourceTemplateSpecifications } var name = getName(mcpResourceMethod, resourceAnnotation); + var title = resourceAnnotation.title(); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); var meta = MetaUtils.getMeta(resourceAnnotation.metaProvider()); var mcpResourceTemplate = McpSchema.ResourceTemplate.builder(uri, name) + .title(Utils.hasText(title) ? title : null) .description(description) .mimeType(mimeType) .meta(meta) 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..8aa1825e68 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 @@ -495,4 +495,61 @@ public Mono syncMethodReturningMono() { }).verifyComplete(); } + @Test + void testResourceTitleIsPropagated() { + class TitledResource { + + @McpResource(uri = "test://titled", name = "titled-resource", title = "My Resource Title", + description = "A titled resource") + public Mono titledResource() { + return Mono.just("content"); + } + + } + + List resourceSpecs = new AsyncMcpResourceProvider(List.of(new TitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isEqualTo("My Resource Title"); + } + + @Test + void testResourceTemplateTitleIsPropagated() { + class TitledResourceTemplate { + + @McpResource(uri = "test://titled/{id}", name = "titled-template", title = "My Template Title", + description = "A titled resource template") + public Mono titledTemplate(String id) { + return Mono.just("content"); + } + + } + + List templateSpecs = new AsyncMcpResourceProvider( + List.of(new TitledResourceTemplate())) + .getResourceTemplateSpecifications(); + + assertThat(templateSpecs).hasSize(1); + assertThat(templateSpecs.get(0).resourceTemplate().title()).isEqualTo("My Template Title"); + } + + @Test + void testResourceTitleIsAbsentWhenNotSet() { + class UntitledResource { + + @McpResource(uri = "test://untitled", name = "untitled-resource", description = "No title") + public Mono untitledResource() { + return Mono.just("content"); + } + + } + + List resourceSpecs = new AsyncMcpResourceProvider(List.of(new UntitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isNull(); + } + } 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..b360c63a9e 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 @@ -496,4 +496,63 @@ public Mono syncMethodReturningMono() { }).verifyComplete(); } + @Test + void testResourceTitleIsPropagated() { + class TitledResource { + + @McpResource(uri = "test://titled", name = "titled-resource", title = "My Resource Title", + description = "A titled resource") + public Mono titledResource() { + return Mono.just("content"); + } + + } + + List resourceSpecs = new AsyncStatelessMcpResourceProvider( + List.of(new TitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isEqualTo("My Resource Title"); + } + + @Test + void testResourceTemplateTitleIsPropagated() { + class TitledResourceTemplate { + + @McpResource(uri = "test://titled/{id}", name = "titled-template", title = "My Template Title", + description = "A titled resource template") + public Mono titledTemplate(String id) { + return Mono.just("content"); + } + + } + + List templateSpecs = new AsyncStatelessMcpResourceProvider( + List.of(new TitledResourceTemplate())) + .getResourceTemplateSpecifications(); + + assertThat(templateSpecs).hasSize(1); + assertThat(templateSpecs.get(0).resourceTemplate().title()).isEqualTo("My Template Title"); + } + + @Test + void testResourceTitleIsAbsentWhenNotSet() { + class UntitledResource { + + @McpResource(uri = "test://untitled", name = "untitled-resource", description = "No title") + public Mono untitledResource() { + return Mono.just("content"); + } + + } + + List resourceSpecs = new AsyncStatelessMcpResourceProvider( + List.of(new UntitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isNull(); + } + } 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..02421e1117 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 @@ -531,6 +531,63 @@ public String noParamResource() { assertThat(((TextResourceContents) content).text()).isEqualTo("No parameters needed"); } + @Test + void testResourceTitleIsPropagated() { + class TitledResource { + + @McpResource(uri = "test://titled", name = "titled-resource", title = "My Resource Title", + description = "A titled resource") + public String titledResource() { + return "content"; + } + + } + + List resourceSpecs = new SyncMcpResourceProvider(List.of(new TitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isEqualTo("My Resource Title"); + } + + @Test + void testResourceTemplateTitleIsPropagated() { + class TitledResourceTemplate { + + @McpResource(uri = "test://titled/{id}", name = "titled-template", title = "My Template Title", + description = "A titled resource template") + public String titledTemplate(String id) { + return "content"; + } + + } + + List templateSpecs = new SyncMcpResourceProvider( + List.of(new TitledResourceTemplate())) + .getResourceTemplateSpecifications(); + + assertThat(templateSpecs).hasSize(1); + assertThat(templateSpecs.get(0).resourceTemplate().title()).isEqualTo("My Template Title"); + } + + @Test + void testResourceTitleIsAbsentWhenNotSet() { + class UntitledResource { + + @McpResource(uri = "test://untitled", name = "untitled-resource", description = "No title") + public String untitledResource() { + return "content"; + } + + } + + List resourceSpecs = new SyncMcpResourceProvider(List.of(new UntitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isNull(); + } + 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..0a82649958 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 @@ -458,4 +458,63 @@ public String requestResource(ReadResourceRequest request) { assertThat(((TextResourceContents) content).text()).isEqualTo("Resource for URI: request://resource"); } + @Test + void testResourceTitleIsPropagated() { + class TitledResource { + + @McpResource(uri = "test://titled", name = "titled-resource", title = "My Resource Title", + description = "A titled resource") + public String titledResource() { + return "content"; + } + + } + + List resourceSpecs = new SyncStatelessMcpResourceProvider( + List.of(new TitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isEqualTo("My Resource Title"); + } + + @Test + void testResourceTemplateTitleIsPropagated() { + class TitledResourceTemplate { + + @McpResource(uri = "test://titled/{id}", name = "titled-template", title = "My Template Title", + description = "A titled resource template") + public String titledTemplate(String id) { + return "content"; + } + + } + + List templateSpecs = new SyncStatelessMcpResourceProvider( + List.of(new TitledResourceTemplate())) + .getResourceTemplateSpecifications(); + + assertThat(templateSpecs).hasSize(1); + assertThat(templateSpecs.get(0).resourceTemplate().title()).isEqualTo("My Template Title"); + } + + @Test + void testResourceTitleIsAbsentWhenNotSet() { + class UntitledResource { + + @McpResource(uri = "test://untitled", name = "untitled-resource", description = "No title") + public String untitledResource() { + return "content"; + } + + } + + List resourceSpecs = new SyncStatelessMcpResourceProvider( + List.of(new UntitledResource())) + .getResourceSpecifications(); + + assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs.get(0).resource().title()).isNull(); + } + }