From cd0ac9cee7968ff1a6754d7b9ba197a97337d575 Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Wed, 9 Apr 2025 19:24:16 -0400 Subject: [PATCH] GH-2633: MCP client autoconfiguration runtime hints (#2683) Fixes: https://github.com/spring-projects/spring-ai/issues/2633 - Create McpClientAutoConfigurationRuntimeHints class - Implement registerHints method to support JSON resources - Add test class with comprehensive assertions - Update aot.factories to register the new runtime hints - Add sample JSON configuration files for testing Signed-off-by: Soby Chacko --- ...cpClientAutoConfigurationRuntimeHints.java | 41 +++++++ .../resources/META-INF/spring/aot.factories | 2 + ...entAutoConfigurationRuntimeHintsTests.java | 100 ++++++++++++++++++ .../test/resources/nested/nested-config.json | 8 ++ .../src/test/resources/test-config.json | 8 ++ 5 files changed, 159 insertions(+) create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/aot.factories create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/nested/nested-config.json create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/test-config.json 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 000000000..e551f57b8 --- /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 000000000..306551e0d --- /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 000000000..b5acb04af --- /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 000000000..7cd51d6d4 --- /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 000000000..57e2a46f2 --- /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" + } +}