diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java new file mode 100644 index 00000000000..e551f57b837 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025-2025 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.client.autoconfigure.aot; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; + +import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage; + +/** + * @author Josh Long + * @author Soby Chacko + */ +public class McpClientAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("**.json"); + + var mcs = MemberCategory.values(); + for (var tr : findJsonAnnotatedClassesInPackage("org.springframework.ai.mcp.client.autoconfigure")) { + hints.reflection().registerType(tr, mcs); + } + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/aot.factories b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..306551e0d4e --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ + org.springframework.ai.mcp.client.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java new file mode 100644 index 00000000000..b5acb04af41 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025-2025 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.client.autoconfigure; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.mcp.client.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints; +import org.springframework.ai.mcp.client.autoconfigure.properties.McpStdioClientProperties; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage; + +/** + * @author Soby Chacko + */ +public class McpClientAutoConfigurationRuntimeHintsTests { + + @Test + void registerHints() throws IOException { + + RuntimeHints runtimeHints = new RuntimeHints(); + + McpClientAutoConfigurationRuntimeHints mcpRuntimeHints = new McpClientAutoConfigurationRuntimeHints(); + mcpRuntimeHints.registerHints(runtimeHints, null); + + boolean hasJsonPattern = runtimeHints.resources() + .resourcePatternHints() + .anyMatch(resourceHints -> resourceHints.getIncludes() + .stream() + .anyMatch(pattern -> "**.json".equals(pattern.getPattern()))); + + assertThat(hasJsonPattern).as("The **.json resource pattern should be registered").isTrue(); + + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("classpath*:**/*.json"); + + assertThat(resources.length).isGreaterThan(1); + + boolean foundRootJson = false; + boolean foundSubfolderJson = false; + + for (Resource resource : resources) { + try { + String path = resource.getURL().getPath(); + if (path.endsWith("/test-config.json")) { + foundRootJson = true; + } + else if (path.endsWith("/nested/nested-config.json")) { + foundSubfolderJson = true; + } + } + catch (IOException e) { + // nothing to do + } + } + + assertThat(foundRootJson).as("test-config.json should exist in the root test resources directory").isTrue(); + + assertThat(foundSubfolderJson).as("nested-config.json should exist in the nested subfolder").isTrue(); + + Set jsonAnnotatedClasses = findJsonAnnotatedClassesInPackage( + "org.springframework.ai.mcp.client.autoconfigure"); + + Set registeredTypes = new HashSet<>(); + runtimeHints.reflection().typeHints().forEach(typeHint -> registeredTypes.add(typeHint.getType())); + + for (TypeReference jsonAnnotatedClass : jsonAnnotatedClasses) { + assertThat(registeredTypes.contains(jsonAnnotatedClass)) + .as("JSON-annotated class %s should be registered for reflection", jsonAnnotatedClass.getName()) + .isTrue(); + } + + assertThat(registeredTypes.contains(TypeReference.of(McpStdioClientProperties.Parameters.class))) + .as("McpStdioClientProperties.Parameters class should be registered") + .isTrue(); + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/nested/nested-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/nested/nested-config.json new file mode 100644 index 00000000000..7cd51d6d490 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/nested/nested-config.json @@ -0,0 +1,8 @@ +{ + "name": "nested-config", + "description": "Test JSON file in nested subfolder of test resources", + "version": "1.0.0", + "nestedProperties": { + "nestedProperty1": "nestedValue1" + } +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/test-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/test-config.json new file mode 100644 index 00000000000..57e2a46f20e --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/test-config.json @@ -0,0 +1,8 @@ +{ + "name": "test-config", + "description": "Test JSON file in root test resources folder", + "version": "1.0.0", + "properties": { + "testProperty1": "value1" + } +}