diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AbstractMcpResourceMethodCallback.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AbstractMcpResourceMethodCallback.java index 55f537ff90..686c8f9b65 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AbstractMcpResourceMethodCallback.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AbstractMcpResourceMethodCallback.java @@ -368,23 +368,6 @@ else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + " in " + method.getDeclaringClass().getName() + ". URI variables: " + this.uriVariables); } - // Check that all non-special parameters are String type (for URI variables) - for (Parameter param : parameters) { - // Skip @McpProgressToken annotated parameters - if (param.isAnnotationPresent(McpProgressToken.class)) { - continue; - } - - Class paramType = param.getType(); - if (!McpSyncRequestContext.class.isAssignableFrom(paramType) - && !McpAsyncRequestContext.class.isAssignableFrom(paramType) && !isExchangeOrContextType(paramType) - && !ReadResourceRequest.class.isAssignableFrom(paramType) - && !McpMeta.class.isAssignableFrom(paramType) && !String.class.isAssignableFrom(paramType)) { - throw new IllegalArgumentException("URI variable parameters must be of type String: " + method.getName() - + " in " + method.getDeclaringClass().getName() + ", parameter of type " + paramType.getName() - + " is not valid"); - } - } } protected abstract Object assignExchangeType(Class paramType, Object exchange); @@ -492,7 +475,8 @@ protected void buildArgsWithUriVariables(Parameter[] parameters, Object[] args, // Assign the next URI variable if (variableIndex < this.uriVariables.size()) { String variableName = this.uriVariables.get(variableIndex); - args[i] = uriVariableValues.get(variableName); + args[i] = McpUriVariableValueConverter.convert(uriVariableValues.get(variableName), + parameters[i].getType()); assignedVariables.add(variableName); variableIndex++; } diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/McpUriVariableValueConverter.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/McpUriVariableValueConverter.java new file mode 100644 index 0000000000..5f0668abf6 --- /dev/null +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/McpUriVariableValueConverter.java @@ -0,0 +1,61 @@ +/* + * 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.method.resource; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.Assert; + +/** + * Converts URI template variable values to method parameter types. + * + * @author yubishen + */ +final class McpUriVariableValueConverter { + + private static final ConversionService CONVERSION_SERVICE = new DefaultConversionService(); + + private McpUriVariableValueConverter() { + } + + static Object convert(@Nullable String value, Class targetType) { + Assert.notNull(targetType, "targetType cannot be null"); + + if (String.class.equals(targetType)) { + return value; + } + + try { + Object converted = CONVERSION_SERVICE.convert(value, targetType); + if (converted == null && value != null) { + throw new ConversionFailedException(TypeDescriptor.valueOf(String.class), + TypeDescriptor.valueOf(targetType), value, + new IllegalArgumentException("Conversion returned null")); + } + return converted; + } + catch (ConversionFailedException ex) { + throw new IllegalArgumentException( + "Failed to convert URI variable value '%s' to %s".formatted(value, targetType.getName()), ex); + } + } + +} diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallbackTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallbackTests.java index 53c42c0d54..6dd418a13f 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallbackTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallbackTests.java @@ -220,6 +220,50 @@ public void testCallbackWithUriVariables() throws Exception { assertThat(textContent.text()).isEqualTo("User: 123, Post: 456"); } + @Test + public void testCallbackWithIntegerUriVariable() throws Exception { + TestResourceProvider provider = new TestResourceProvider(); + Method method = TestResourceProvider.class.getMethod("getUserByIntId", int.class); + McpResource resourceAnnotation = method.getAnnotation(McpResource.class); + + BiFunction callback = SyncMcpResourceMethodCallback + .builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(resourceAnnotation)) + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + ReadResourceRequest request = ReadResourceRequest.builder("example.com/users/42").build(); + + ReadResourceResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.contents()).hasSize(1); + assertThat(result.contents().get(0)).isInstanceOf(TextResourceContents.class); + TextResourceContents textContent = (TextResourceContents) result.contents().get(0); + assertThat(textContent.text()).isEqualTo("User id: 42"); + } + + @Test + public void testCallbackWithInvalidIntegerUriVariable() throws Exception { + TestResourceProvider provider = new TestResourceProvider(); + Method method = TestResourceProvider.class.getMethod("getUserByIntId", int.class); + McpResource resourceAnnotation = method.getAnnotation(McpResource.class); + + BiFunction callback = SyncMcpResourceMethodCallback + .builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(resourceAnnotation)) + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + ReadResourceRequest request = ReadResourceRequest.builder("example.com/users/not-a-number").build(); + + assertThatThrownBy(() -> callback.apply(exchange, request)).isInstanceOf(McpError.class); + } + @Test public void testCallbackWithExchangeAndUriVariable() throws Exception { TestResourceProvider provider = new TestResourceProvider(); @@ -1272,6 +1316,15 @@ public ReadResourceResult getResourceWithUriVariables(String userId, String post .build())).build(); } + @McpResource(uri = "example.com/users/{id}") + public ReadResourceResult getUserByIntId(int id) { + return ReadResourceResult + .builder(List.of(TextResourceContents.builder("example.com/users/" + id, "User id: " + id) + .mimeType("text/plain") + .build())) + .build(); + } + @McpResource(uri = "users/{userId}/profile") public ReadResourceResult getResourceWithExchangeAndUriVariable(McpSyncServerExchange exchange, String userId) { return ReadResourceResult.builder(