From a9651e1d12d25cd54f09db99795c79bdbc826a1f Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 25 Sep 2025 20:21:42 +0200 Subject: [PATCH 1/2] feat: add support for MCP resource templates - Add getResourceTemplateSpecifications() methods to all resource providers - Separate regular resources from resource templates based on URI template detection - Rename ProvidrerUtils to McpProviderUtils (fix typo) - Filter resources with URI templates into separate template specifications - Update tests to verify resource template handling - Upgrade MCP Java SDK from 0.13.1 to 0.14.0-SNAPSHOT This change enables proper handling of MCP resource templates (URIs with variables like {id}) while maintaining backward compatibility for regular resources. Signed-off-by: Christian Tzolov --- ...vidrerUtils.java => McpProviderUtils.java} | 9 ++- .../resource/AsyncMcpResourceProvider.java | 73 +++++++++++++++++-- .../AsyncStatelessMcpResourceProvider.java | 63 +++++++++++++++- .../resource/SyncMcpResourceProvider.java | 53 +++++++++++++- .../SyncStatelessMcpResourceProvider.java | 61 +++++++++++++++- .../provider/tool/AsyncMcpToolProvider.java | 4 +- .../tool/AsyncStatelessMcpToolProvider.java | 4 +- .../provider/tool/SyncMcpToolProvider.java | 4 +- .../tool/SyncStatelessMcpToolProvider.java | 4 +- .../AsyncMcpResourceProviderTests.java | 38 ++++++---- ...syncStatelessMcpResourceProviderTests.java | 24 +++--- .../SyncMcpResourceProviderTests.java | 31 +++++--- ...SyncStatelessMcpResourceProviderTests.java | 32 +++++--- pom.xml | 2 +- 14 files changed, 336 insertions(+), 66 deletions(-) rename mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/{ProvidrerUtils.java => McpProviderUtils.java} (83%) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/ProvidrerUtils.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/McpProviderUtils.java similarity index 83% rename from mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/ProvidrerUtils.java rename to mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/McpProviderUtils.java index 65f7c8c..0daaec5 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/ProvidrerUtils.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/McpProviderUtils.java @@ -18,12 +18,19 @@ import java.lang.reflect.Method; import java.util.function.Predicate; +import java.util.regex.Pattern; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public class ProvidrerUtils { +public class McpProviderUtils { + + private static final Pattern URI_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); + + public static boolean isUriTemplate(String uri) { + return URI_VARIABLE_PATTERN.matcher(uri).find(); + } public final static Predicate isReactiveReturnType = method -> Mono.class .isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java index 1f7d91b..5fd4bef 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java @@ -18,21 +18,23 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Objects; import java.util.function.BiFunction; import java.util.stream.Stream; -import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springaicommunity.mcp.annotation.McpResource; -import org.springaicommunity.mcp.method.resource.AsyncMcpResourceMethodCallback; - import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.util.Assert; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.annotation.McpResource; +import org.springaicommunity.mcp.method.resource.AsyncMcpResourceMethodCallback; +import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -79,6 +81,11 @@ public List getResourceSpecifications() { var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); var uri = resourceAnnotation.uri(); + + if (McpProviderUtils.isUriTemplate(uri)) { + return null; + } + var name = getName(mcpResourceMethod, resourceAnnotation); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); @@ -101,6 +108,60 @@ public List getResourceSpecifications() { return resourceSpec; }) + .filter(Objects::nonNull) + .toList()) + .flatMap(List::stream) + .toList(); + + if (resourceSpecs.isEmpty()) { + logger.warn("No resource methods found in the provided resource objects: {}", this.resourceObjects); + } + + return resourceSpecs; + } + + public List getResourceTemplateSpecifications() { + + List resourceSpecs = this.resourceObjects.stream() + .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) + .filter(method -> method.isAnnotationPresent(McpResource.class)) + .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) + || Flux.class.isAssignableFrom(method.getReturnType()) + || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) + .map(mcpResourceMethod -> { + + var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); + + var uri = resourceAnnotation.uri(); + + if (!McpProviderUtils.isUriTemplate(uri)) { + return null; + } + + var name = getName(mcpResourceMethod, resourceAnnotation); + var description = resourceAnnotation.description(); + var mimeType = resourceAnnotation.mimeType(); + + var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() + .uri(uri) + .name(name) + .description(description) + .mimeType(mimeType) + .build(); + + BiFunction> methodCallback = AsyncMcpResourceMethodCallback + .builder() + .method(mcpResourceMethod) + .bean(resourceObject) + .resource(mcpResourceTemplate) + .build(); + + var resourceSpec = new AsyncResourceTemplateSpecification(mcpResourceTemplate, methodCallback); + + return resourceSpec; + }) + .filter(Objects::nonNull) .toList()) .flatMap(List::stream) .toList(); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java index 249a4ec..ba714d9 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Objects; import java.util.function.BiFunction; import java.util.stream.Stream; @@ -26,8 +27,9 @@ import org.slf4j.LoggerFactory; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.method.resource.AsyncStatelessMcpResourceMethodCallback; - +import org.springaicommunity.mcp.provider.McpProviderUtils; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; @@ -79,6 +81,11 @@ public List getResourceSpecifications() { var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); var uri = resourceAnnotation.uri(); + + if (McpProviderUtils.isUriTemplate(uri)) { + return null; + } + var name = getName(mcpResourceMethod, resourceAnnotation); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); @@ -101,6 +108,60 @@ public List getResourceSpecifications() { return resourceSpec; }) + .filter(Objects::nonNull) + .toList()) + .flatMap(List::stream) + .toList(); + + if (resourceSpecs.isEmpty()) { + logger.warn("No resource methods found in the provided resource objects: {}", this.resourceObjects); + } + + return resourceSpecs; + } + + public List getResourceTemplateSpecifications() { + + List resourceSpecs = this.resourceObjects.stream() + .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) + .filter(method -> method.isAnnotationPresent(McpResource.class)) + .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) + || Flux.class.isAssignableFrom(method.getReturnType()) + || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) + .map(mcpResourceMethod -> { + + var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); + + var uri = resourceAnnotation.uri(); + + if (!McpProviderUtils.isUriTemplate(uri)) { + return null; + } + + var name = getName(mcpResourceMethod, resourceAnnotation); + var description = resourceAnnotation.description(); + var mimeType = resourceAnnotation.mimeType(); + + var mcpResource = McpSchema.ResourceTemplate.builder() + .uri(uri) + .name(name) + .description(description) + .mimeType(mimeType) + .build(); + + BiFunction> methodCallback = AsyncStatelessMcpResourceMethodCallback + .builder() + .method(mcpResourceMethod) + .bean(resourceObject) + .resource(mcpResource) + .build(); + + var resourceSpec = new AsyncResourceTemplateSpecification(mcpResource, methodCallback); + + return resourceSpec; + }) + .filter(Objects::nonNull) .toList()) .flatMap(List::stream) .toList(); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java index 5e72a0a..7396de4 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java @@ -18,12 +18,14 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Objects; import java.util.stream.Stream; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.method.resource.SyncMcpResourceMethodCallback; - +import org.springaicommunity.mcp.provider.McpProviderUtils; import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; @@ -50,6 +52,11 @@ public List getResourceSpecifications() { var resourceAnnotation = mcpResourceMethod.getAnnotation(McpResource.class); var uri = resourceAnnotation.uri(); + + if (McpProviderUtils.isUriTemplate(uri)) { + return null; + } + var name = getName(mcpResourceMethod, resourceAnnotation); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); @@ -69,6 +76,50 @@ public List getResourceSpecifications() { return new SyncResourceSpecification(mcpResource, methodCallback); }) + .filter(Objects::nonNull) + .toList()) + .flatMap(List::stream) + .toList(); + + return methodCallbacks; + } + + public List getResourceTemplateSpecifications() { + + List methodCallbacks = this.resourceObjects.stream() + .map(resourceObject -> Stream.of(this.doGetClassMethods(resourceObject)) + .filter(resourceMethod -> resourceMethod.isAnnotationPresent(McpResource.class)) + .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) + .map(mcpResourceMethod -> { + var resourceAnnotation = mcpResourceMethod.getAnnotation(McpResource.class); + + var uri = resourceAnnotation.uri(); + + if (!McpProviderUtils.isUriTemplate(uri)) { + return null; + } + + var name = getName(mcpResourceMethod, resourceAnnotation); + var description = resourceAnnotation.description(); + var mimeType = resourceAnnotation.mimeType(); + + var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() + .uri(uri) + .name(name) + .description(description) + .mimeType(mimeType) + .build(); + + var methodCallback = SyncMcpResourceMethodCallback.builder() + .method(mcpResourceMethod) + .bean(resourceObject) + .resource(mcpResourceTemplate) + .build(); + + return new SyncResourceTemplateSpecification(mcpResourceTemplate, methodCallback); + }) + .filter(Objects::nonNull) .toList()) .flatMap(List::stream) .toList(); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java index b9e9b68..099b5d3 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Objects; import java.util.function.BiFunction; import java.util.stream.Stream; @@ -25,7 +26,8 @@ import org.slf4j.LoggerFactory; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.method.resource.SyncStatelessMcpResourceMethodCallback; - +import org.springaicommunity.mcp.provider.McpProviderUtils; +import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpSchema; @@ -75,6 +77,11 @@ public List getResourceSpecifications() { var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); var uri = resourceAnnotation.uri(); + + if (McpProviderUtils.isUriTemplate(uri)) { + return null; + } + var name = getName(mcpResourceMethod, resourceAnnotation); var description = resourceAnnotation.description(); var mimeType = resourceAnnotation.mimeType(); @@ -97,6 +104,58 @@ public List getResourceSpecifications() { return resourceSpec; }) + .filter(Objects::nonNull) + .toList()) + .flatMap(List::stream) + .toList(); + + if (resourceSpecs.isEmpty()) { + logger.warn("No resource methods found in the provided resource objects: {}", this.resourceObjects); + } + + return resourceSpecs; + } + + public List getResourceTemplateSpecifications() { + + List resourceSpecs = this.resourceObjects.stream() + .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) + .filter(method -> method.isAnnotationPresent(McpResource.class)) + .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) + .map(mcpResourceMethod -> { + + var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); + + var uri = resourceAnnotation.uri(); + + if (!McpProviderUtils.isUriTemplate(uri)) { + return null; + } + + var name = getName(mcpResourceMethod, resourceAnnotation); + var description = resourceAnnotation.description(); + var mimeType = resourceAnnotation.mimeType(); + + var mcpResource = McpSchema.ResourceTemplate.builder() + .uri(uri) + .name(name) + .description(description) + .mimeType(mimeType) + .build(); + + BiFunction methodCallback = SyncStatelessMcpResourceMethodCallback + .builder() + .method(mcpResourceMethod) + .bean(resourceObject) + .resource(mcpResource) + .build(); + + var resourceSpec = new SyncResourceTemplateSpecification(mcpResource, methodCallback); + + return resourceSpec; + }) + .filter(Objects::nonNull) .toList()) .flatMap(List::stream) .toList(); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java index baf2966..15f749e 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java @@ -34,7 +34,7 @@ import org.springaicommunity.mcp.method.tool.ReturnMode; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.ProvidrerUtils; +import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -63,7 +63,7 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(this.doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(ProvidrerUtils.isReactiveReturnType) + .filter(McpProviderUtils.isReactiveReturnType) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java index 4841ca9..a1b5274 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java @@ -35,7 +35,7 @@ import org.springaicommunity.mcp.method.tool.ReturnMode; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.ProvidrerUtils; +import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -68,7 +68,7 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(ProvidrerUtils.isReactiveReturnType) + .filter(McpProviderUtils.isReactiveReturnType) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java index 9f3dda2..ba5ff86 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java @@ -33,7 +33,7 @@ import org.springaicommunity.mcp.method.tool.SyncMcpToolMethodCallback; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.ProvidrerUtils; +import org.springaicommunity.mcp.provider.McpProviderUtils; /** * @author Christian Tzolov @@ -61,7 +61,7 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(this.doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(ProvidrerUtils.isNotReactiveReturnType) + .filter(McpProviderUtils.isNotReactiveReturnType) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java index 86495ec..2ce6e63 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java @@ -34,7 +34,7 @@ import org.springaicommunity.mcp.method.tool.SyncStatelessMcpToolMethodCallback; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.ProvidrerUtils; +import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous stateless MCP tool methods. @@ -65,7 +65,7 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(this.doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(ProvidrerUtils.isNotReactiveReturnType) + .filter(McpProviderUtils.isNotReactiveReturnType) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProviderTests.java index 9e94880..44b46ff 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProviderTests.java @@ -16,24 +16,24 @@ package org.springaicommunity.mcp.provider.resource; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; - import java.util.List; -import org.junit.jupiter.api.Test; -import org.springaicommunity.mcp.annotation.McpResource; - import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import io.modelcontextprotocol.spec.McpSchema.ResourceContents; import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; +import org.junit.jupiter.api.Test; +import org.springaicommunity.mcp.annotation.McpResource; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + /** * Tests for {@link AsyncMcpResourceProvider}. * @@ -65,12 +65,14 @@ public Mono testResource(String id) { List resourceSpecs = provider.getResourceSpecifications(); assertThat(resourceSpecs).isNotNull(); - assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs).hasSize(0); - AsyncResourceSpecification resourceSpec = resourceSpecs.get(0); - assertThat(resourceSpec.resource().uri()).isEqualTo("test://resource/{id}"); - assertThat(resourceSpec.resource().name()).isEqualTo("test-resource"); - assertThat(resourceSpec.resource().description()).isEqualTo("A test resource"); + var resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); + + AsyncResourceTemplateSpecification resourceSpec = resourceTemplateSpecs.get(0); + assertThat(resourceSpec.resourceTemplate().uriTemplate()).isEqualTo("test://resource/{id}"); + assertThat(resourceSpec.resourceTemplate().name()).isEqualTo("test-resource"); + assertThat(resourceSpec.resourceTemplate().description()).isEqualTo("A test resource"); assertThat(resourceSpec.readHandler()).isNotNull(); // Test that the handler works @@ -280,15 +282,19 @@ public Mono variableResource(String id, String type) { AsyncMcpResourceProvider provider = new AsyncMcpResourceProvider(List.of(resourceObject)); List resourceSpecs = provider.getResourceSpecifications(); + assertThat(resourceSpecs).hasSize(0); - assertThat(resourceSpecs).hasSize(1); - assertThat(resourceSpecs.get(0).resource().uri()).isEqualTo("variable://resource/{id}/{type}"); - assertThat(resourceSpecs.get(0).resource().name()).isEqualTo("variable-resource"); + var resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); + + assertThat(resourceTemplateSpecs).hasSize(1); + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().uriTemplate()) + .isEqualTo("variable://resource/{id}/{type}"); + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().name()).isEqualTo("variable-resource"); // Test that the handler works with URI variables McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); ReadResourceRequest request = new ReadResourceRequest("variable://resource/123/document"); - Mono result = resourceSpecs.get(0).readHandler().apply(exchange, request); + Mono result = resourceTemplateSpecs.get(0).readHandler().apply(exchange, request); StepVerifier.create(result).assertNext(readResult -> { assertThat(readResult.contents()).hasSize(1); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProviderTests.java index 7156c31..dd651d0 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProviderTests.java @@ -26,6 +26,7 @@ import org.springaicommunity.mcp.annotation.McpResource; import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceSpecification; +import io.modelcontextprotocol.server.McpStatelessServerFeatures.AsyncResourceTemplateSpecification; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; @@ -66,12 +67,14 @@ public Mono testResource(String id) { List resourceSpecs = provider.getResourceSpecifications(); assertThat(resourceSpecs).isNotNull(); - assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs).hasSize(0); + + var resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); - AsyncResourceSpecification resourceSpec = resourceSpecs.get(0); - assertThat(resourceSpec.resource().uri()).isEqualTo("test://resource/{id}"); - assertThat(resourceSpec.resource().name()).isEqualTo("test-resource"); - assertThat(resourceSpec.resource().description()).isEqualTo("A test resource"); + AsyncResourceTemplateSpecification resourceSpec = resourceTemplateSpecs.get(0); + assertThat(resourceSpec.resourceTemplate().uriTemplate()).isEqualTo("test://resource/{id}"); + assertThat(resourceSpec.resourceTemplate().name()).isEqualTo("test-resource"); + assertThat(resourceSpec.resourceTemplate().description()).isEqualTo("A test resource"); assertThat(resourceSpec.readHandler()).isNotNull(); // Test that the handler works @@ -282,15 +285,18 @@ public Mono variableResource(String id, String type) { AsyncStatelessMcpResourceProvider provider = new AsyncStatelessMcpResourceProvider(List.of(resourceObject)); List resourceSpecs = provider.getResourceSpecifications(); + assertThat(resourceSpecs).hasSize(0); - assertThat(resourceSpecs).hasSize(1); - assertThat(resourceSpecs.get(0).resource().uri()).isEqualTo("variable://resource/{id}/{type}"); - assertThat(resourceSpecs.get(0).resource().name()).isEqualTo("variable-resource"); + var resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); + + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().uriTemplate()) + .isEqualTo("variable://resource/{id}/{type}"); + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().name()).isEqualTo("variable-resource"); // Test that the handler works with URI variables McpTransportContext context = mock(McpTransportContext.class); ReadResourceRequest request = new ReadResourceRequest("variable://resource/123/document"); - Mono result = resourceSpecs.get(0).readHandler().apply(context, request); + Mono result = resourceTemplateSpecs.get(0).readHandler().apply(context, request); StepVerifier.create(result).assertNext(readResult -> { assertThat(readResult.contents()).hasSize(1); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProviderTests.java index e2bc658..24ee9b5 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProviderTests.java @@ -26,6 +26,7 @@ import org.springaicommunity.mcp.annotation.McpResource; import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; @@ -64,18 +65,20 @@ public String testResource(String id) { List resourceSpecs = provider.getResourceSpecifications(); assertThat(resourceSpecs).isNotNull(); - assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs).hasSize(0); + + List resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); - SyncResourceSpecification resourceSpec = resourceSpecs.get(0); - assertThat(resourceSpec.resource().uri()).isEqualTo("test://resource/{id}"); - assertThat(resourceSpec.resource().name()).isEqualTo("test-resource"); - assertThat(resourceSpec.resource().description()).isEqualTo("A test resource"); - assertThat(resourceSpec.readHandler()).isNotNull(); + SyncResourceTemplateSpecification resourceTemplateSpec = resourceTemplateSpecs.get(0); + assertThat(resourceTemplateSpec.resourceTemplate().uriTemplate()).isEqualTo("test://resource/{id}"); + assertThat(resourceTemplateSpec.resourceTemplate().name()).isEqualTo("test-resource"); + assertThat(resourceTemplateSpec.resourceTemplate().description()).isEqualTo("A test resource"); + assertThat(resourceTemplateSpec.readHandler()).isNotNull(); // Test that the handler works McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); ReadResourceRequest request = new ReadResourceRequest("test://resource/123"); - ReadResourceResult result = resourceSpec.readHandler().apply(exchange, request); + ReadResourceResult result = resourceTemplateSpec.readHandler().apply(exchange, request); assertThat(result.contents()).hasSize(1); ResourceContents content = result.contents().get(0); @@ -278,14 +281,20 @@ public String variableResource(String id, String type) { List resourceSpecs = provider.getResourceSpecifications(); - assertThat(resourceSpecs).hasSize(1); - assertThat(resourceSpecs.get(0).resource().uri()).isEqualTo("variable://resource/{id}/{type}"); - assertThat(resourceSpecs.get(0).resource().name()).isEqualTo("variable-resource"); + assertThat(resourceSpecs).hasSize(0); + + List resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); + + assertThat(resourceTemplateSpecs).hasSize(1); + + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().uriTemplate()) + .isEqualTo("variable://resource/{id}/{type}"); + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().name()).isEqualTo("variable-resource"); // Test that the handler works with URI variables McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); ReadResourceRequest request = new ReadResourceRequest("variable://resource/123/document"); - ReadResourceResult result = resourceSpecs.get(0).readHandler().apply(exchange, request); + ReadResourceResult result = resourceTemplateSpecs.get(0).readHandler().apply(exchange, request); assertThat(result.contents()).hasSize(1); ResourceContents content = result.contents().get(0); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProviderTests.java index a6f94e4..33a78d8 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProviderTests.java @@ -26,6 +26,7 @@ import org.springaicommunity.mcp.annotation.McpResource; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification; +import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; @@ -65,18 +66,22 @@ public String testResource(String id) { List resourceSpecs = provider.getResourceSpecifications(); assertThat(resourceSpecs).isNotNull(); - assertThat(resourceSpecs).hasSize(1); + assertThat(resourceSpecs).hasSize(0); + + List resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); - SyncResourceSpecification resourceSpec = resourceSpecs.get(0); - assertThat(resourceSpec.resource().uri()).isEqualTo("test://resource/{id}"); - assertThat(resourceSpec.resource().name()).isEqualTo("test-resource"); - assertThat(resourceSpec.resource().description()).isEqualTo("A test resource"); - assertThat(resourceSpec.readHandler()).isNotNull(); + assertThat(resourceTemplateSpecs).hasSize(1); + + SyncResourceTemplateSpecification resourceTemplateSpec = resourceTemplateSpecs.get(0); + assertThat(resourceTemplateSpec.resourceTemplate().uriTemplate()).isEqualTo("test://resource/{id}"); + assertThat(resourceTemplateSpec.resourceTemplate().name()).isEqualTo("test-resource"); + assertThat(resourceTemplateSpec.resourceTemplate().description()).isEqualTo("A test resource"); + assertThat(resourceTemplateSpec.readHandler()).isNotNull(); // Test that the handler works McpTransportContext context = mock(McpTransportContext.class); ReadResourceRequest request = new ReadResourceRequest("test://resource/123"); - ReadResourceResult result = resourceSpec.readHandler().apply(context, request); + ReadResourceResult result = resourceTemplateSpec.readHandler().apply(context, request); assertThat(result).isNotNull(); assertThat(result.contents()).hasSize(1); @@ -281,14 +286,19 @@ public String variableResource(String id, String type) { List resourceSpecs = provider.getResourceSpecifications(); - assertThat(resourceSpecs).hasSize(1); - assertThat(resourceSpecs.get(0).resource().uri()).isEqualTo("variable://resource/{id}/{type}"); - assertThat(resourceSpecs.get(0).resource().name()).isEqualTo("variable-resource"); + assertThat(resourceSpecs).hasSize(0); + + var resourceTemplateSpecs = provider.getResourceTemplateSpecifications(); + + assertThat(resourceTemplateSpecs).hasSize(1); + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().uriTemplate()) + .isEqualTo("variable://resource/{id}/{type}"); + assertThat(resourceTemplateSpecs.get(0).resourceTemplate().name()).isEqualTo("variable-resource"); // Test that the handler works with URI variables McpTransportContext context = mock(McpTransportContext.class); ReadResourceRequest request = new ReadResourceRequest("variable://resource/123/document"); - ReadResourceResult result = resourceSpecs.get(0).readHandler().apply(context, request); + ReadResourceResult result = resourceTemplateSpecs.get(0).readHandler().apply(context, request); assertThat(result).isNotNull(); assertThat(result.contents()).hasSize(1); diff --git a/pom.xml b/pom.xml index ee41004..e7dc322 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ 17 17 - 0.13.1 + 0.14.0-SNAPSHOT 4.38.0 2.2.36 From 772ac983c6beab6a220020ccee9b4573aa934215 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sat, 27 Sep 2025 08:20:18 +0200 Subject: [PATCH 2/2] re-align with the java-sdk Signed-off-by: Christian Tzolov --- .../mcp/provider/resource/AsyncMcpResourceProvider.java | 2 +- .../provider/resource/AsyncStatelessMcpResourceProvider.java | 2 +- .../mcp/provider/resource/SyncMcpResourceProvider.java | 2 +- .../mcp/provider/resource/SyncStatelessMcpResourceProvider.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java index 5fd4bef..e7e515b 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java @@ -144,7 +144,7 @@ public List getResourceTemplateSpecification var mimeType = resourceAnnotation.mimeType(); var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() - .uri(uri) + .uriTemplate(uri) .name(name) .description(description) .mimeType(mimeType) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java index ba714d9..59bafe8 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java @@ -144,7 +144,7 @@ public List getResourceTemplateSpecification var mimeType = resourceAnnotation.mimeType(); var mcpResource = McpSchema.ResourceTemplate.builder() - .uri(uri) + .uriTemplate(uri) .name(name) .description(description) .mimeType(mimeType) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java index 7396de4..0c5d972 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java @@ -105,7 +105,7 @@ public List getResourceTemplateSpecifications var mimeType = resourceAnnotation.mimeType(); var mcpResourceTemplate = McpSchema.ResourceTemplate.builder() - .uri(uri) + .uriTemplate(uri) .name(name) .description(description) .mimeType(mimeType) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java index 099b5d3..cfd3cb9 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java @@ -138,7 +138,7 @@ public List getResourceTemplateSpecifications var mimeType = resourceAnnotation.mimeType(); var mcpResource = McpSchema.ResourceTemplate.builder() - .uri(uri) + .uriTemplate(uri) .name(name) .description(description) .mimeType(mimeType)