diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 7dfb8e1..78415d0 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -27,3 +27,9 @@ jobs: run: | cd example mvn compile + + - name: Build Spring Boot Example with Maven + run: | + cd example-spring-boot + mvn compile + diff --git a/example-spring-boot/README.md b/example-spring-boot/README.md new file mode 100644 index 0000000..1ae0a93 --- /dev/null +++ b/example-spring-boot/README.md @@ -0,0 +1,58 @@ +# Spring Boot Auto-Configuration Example ☕ + +This is a sample Spring Boot 3.x application demonstrating how to integrate the MCP Toolbox SDK using Spring's auto-configuration capabilities. + +With the auto-configuration module enabled, you do not need to write boilerplate code to instantiate, configure, or manage the lifecycle of the `McpToolboxClient`. The SDK client is automatically configured and registered as a Spring bean. + +--- + +## Prerequisites + +- **Java Development Kit (JDK):** Version 17 or higher. +- **Apache Maven:** Version 3.6 or higher. +- **MCP Toolbox Server:** A running instance of the Toolbox server (e.g. running locally or on Cloud Run). + +--- + +## How It Works + +1. **Auto-Detection:** The SDK's auto-configuration checks for the presence of the property `google.cloud.mcp.toolbox.base-url`. +2. **Bean Instantiation:** If the property is present, a singleton `McpToolboxClient` bean is instantiated and registered in the application context. +3. **Fallback Safe:** If a user registers their own custom `McpToolboxClient` bean, the SDK's auto-configuration backs off. + +--- + +## Getting Started + +### 1. Build and Install the Core SDK +Since the example depends on the local `0.3.0-SNAPSHOT` version of the SDK, you must first build and install the SDK to your local Maven cache (`~/.m2`): + +```bash +# In the repository root directory (mcp-toolbox-sdk-java) +mvn install -DskipTests -Dfmt.skip=true -Djacoco.skip=true +``` + +### 2. Configure the Toolbox Connection +Open `src/main/resources/application.properties` and configure the base URL of your running MCP Toolbox instance: + +```properties +google.cloud.mcp.toolbox.base-url=http://localhost:5000/mcp +``` + +### 3. Run the Application +Navigate to the example directory and run the Spring Boot app: + +```bash +cd example-spring-boot +mvn spring-boot:run +``` + +Upon a successful connection and tool execution, you will see output in the logs similar to: + +```text +=== Spring Boot Auto-Configuration Success === +Connecting to MCP server: McpToolboxClientImpl{baseUrl=http://localhost:5000/mcp} +Tool execution result content: + - Result value 1 + - Result value 2 +``` diff --git a/example-spring-boot/pom.xml b/example-spring-boot/pom.xml new file mode 100644 index 0000000..1fda8f5 --- /dev/null +++ b/example-spring-boot/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.example + mcp-toolbox-spring-boot-example + 1.0.0-SNAPSHOT + mcp-toolbox-spring-boot-example + Demo project for MCP Toolbox Spring Boot integration + + + 17 + + + + + + org.springframework.boot + spring-boot-starter + + + + + com.google.cloud.mcp + mcp-toolbox-sdk-java + 0.3.0-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/example-spring-boot/src/main/java/com/example/demo/DemoApplication.java b/example-spring-boot/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..8f53882 --- /dev/null +++ b/example-spring-boot/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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 com.example.demo; + +import com.google.cloud.mcp.McpToolboxClient; +import java.util.Map; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +/** Demo application demonstrating the autowired McpToolboxClient. */ +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + + /** + * Run a simple tool invocation after startup to verify auto-configuration is loaded. + * + * @param client Autowired McpToolboxClient bean from the SDK. + * @return CommandLineRunner bean. + */ + @Bean + public CommandLineRunner run(McpToolboxClient client) { + return args -> { + System.out.println("=== Spring Boot Auto-Configuration Success ==="); + System.out.println("Connecting to MCP server: " + client.toString()); + + // Attempt to invoke a toy price tool asynchronously + client + .invokeTool("get-toy-price", Map.of("description", "plush dinosaur")) + .thenAccept( + result -> { + System.out.println("Tool execution result content:"); + result + .content() + .forEach(content -> System.out.println(" - " + content.text())); + }) + .exceptionally( + ex -> { + System.err.println("Failed to invoke tool: " + ex.getMessage()); + return null; + }) + .join(); // Wait for demo execution completion + }; + } +} diff --git a/example-spring-boot/src/main/resources/application.properties b/example-spring-boot/src/main/resources/application.properties new file mode 100644 index 0000000..95e7b14 --- /dev/null +++ b/example-spring-boot/src/main/resources/application.properties @@ -0,0 +1,7 @@ +# Spring application properties for MCP Toolbox configuration + +# Configure the base URL of the MCP Toolbox server +google.cloud.mcp.toolbox.base-url=http://localhost:5000/mcp + +# Optional: Configure API key if your server requires it +# google.cloud.mcp.toolbox.api-key=YOUR_API_KEY diff --git a/pom.xml b/pom.xml index ef1b548..f560728 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,39 @@ ${google.auth.version} + + + org.springframework.boot + spring-boot-autoconfigure + 3.2.5 + true + + + org.springframework.boot + spring-boot-configuration-processor + 3.2.5 + true + + + + org.springframework.boot + spring-boot-test + 3.2.5 + test + + + org.springframework + spring-test + 6.1.6 + test + + + org.assertj + assertj-core + 3.24.2 + test + org.junit.jupiter junit-jupiter diff --git a/src/main/java/com/google/cloud/mcp/autoconfigure/McpToolboxAutoConfiguration.java b/src/main/java/com/google/cloud/mcp/autoconfigure/McpToolboxAutoConfiguration.java new file mode 100644 index 0000000..0589109 --- /dev/null +++ b/src/main/java/com/google/cloud/mcp/autoconfigure/McpToolboxAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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 com.google.cloud.mcp.autoconfigure; + +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.client.McpToolboxClientBuilder; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** Auto-configuration for the MCP Toolbox SDK Client. */ +@AutoConfiguration +@ConditionalOnClass(McpToolboxClient.class) +@ConditionalOnProperty(name = "google.cloud.mcp.toolbox.base-url") +@EnableConfigurationProperties(McpToolboxProperties.class) +public class McpToolboxAutoConfiguration { + + private final McpToolboxProperties properties; + + public McpToolboxAutoConfiguration(McpToolboxProperties properties) { + this.properties = properties; + } + + /** + * Registers a default {@link McpToolboxClient} bean if none is already defined. + * + * @return A configured {@link McpToolboxClient} instance. + */ + @Bean + @ConditionalOnMissingBean + public McpToolboxClient mcpToolboxClient() { + McpToolboxClient.Builder builder = + new McpToolboxClientBuilder().baseUrl(properties.getBaseUrl()); + if (properties.getApiKey() != null && !properties.getApiKey().isEmpty()) { + builder.apiKey(properties.getApiKey()); + } + return builder.build(); + } +} diff --git a/src/main/java/com/google/cloud/mcp/autoconfigure/McpToolboxProperties.java b/src/main/java/com/google/cloud/mcp/autoconfigure/McpToolboxProperties.java new file mode 100644 index 0000000..e1cc6d2 --- /dev/null +++ b/src/main/java/com/google/cloud/mcp/autoconfigure/McpToolboxProperties.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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 com.google.cloud.mcp.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Configuration properties for the MCP Toolbox SDK. */ +@ConfigurationProperties(prefix = "google.cloud.mcp.toolbox") +public class McpToolboxProperties { + /** Base URL for the MCP Toolbox service. */ + private String baseUrl; + + /** API Key or token for the MCP Toolbox service (optional). */ + private String apiKey; + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } +} diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f33311b --- /dev/null +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.google.cloud.mcp.autoconfigure.McpToolboxAutoConfiguration diff --git a/src/test/java/com/google/cloud/mcp/autoconfigure/McpToolboxAutoConfigurationTest.java b/src/test/java/com/google/cloud/mcp/autoconfigure/McpToolboxAutoConfigurationTest.java new file mode 100644 index 0000000..b4e1bd0 --- /dev/null +++ b/src/test/java/com/google/cloud/mcp/autoconfigure/McpToolboxAutoConfigurationTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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 com.google.cloud.mcp.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import com.google.cloud.mcp.McpToolboxClient; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +class McpToolboxAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(McpToolboxAutoConfiguration.class)); + + @Test + void testAutoConfigurationBacksOffWithoutBaseUrlProperty() { + this.contextRunner.run( + context -> { + assertThat(context).doesNotHaveBean(McpToolboxClient.class); + }); + } + + @Test + void testAutoConfigurationCreatesClientWithBaseUrlProperty() { + this.contextRunner + .withPropertyValues("google.cloud.mcp.toolbox.base-url=http://localhost:8080") + .run( + context -> { + assertThat(context).hasSingleBean(McpToolboxClient.class); + McpToolboxClient client = context.getBean(McpToolboxClient.class); + assertThat(client).isNotNull(); + }); + } + + @Test + void testAutoConfigurationRespectsUserDefinedClientBean() { + this.contextRunner + .withUserConfiguration(UserConfiguration.class) + .withPropertyValues("google.cloud.mcp.toolbox.base-url=http://localhost:8080") + .run( + context -> { + assertThat(context).hasSingleBean(McpToolboxClient.class); + McpToolboxClient client = context.getBean(McpToolboxClient.class); + McpToolboxClient mockClient = context.getBean(UserConfiguration.class).mockClient(); + assertThat(client).isSameAs(mockClient); + }); + } + + @Test + void testPropertiesGettersAndSetters() { + McpToolboxProperties props = new McpToolboxProperties(); + props.setBaseUrl("http://localhost:8080"); + props.setApiKey("my-key"); + assertThat(props.getBaseUrl()).isEqualTo("http://localhost:8080"); + assertThat(props.getApiKey()).isEqualTo("my-key"); + } + + @Test + void testDirectAutoConfigurationInstantiation() { + McpToolboxProperties props = new McpToolboxProperties(); + props.setBaseUrl("http://localhost:8080"); + McpToolboxAutoConfiguration config = new McpToolboxAutoConfiguration(props); + McpToolboxClient client = config.mcpToolboxClient(); + assertThat(client).isNotNull(); + + // Test with apiKey + props.setApiKey("some-key"); + config = new McpToolboxAutoConfiguration(props); + client = config.mcpToolboxClient(); + assertThat(client).isNotNull(); + } + + @Configuration(proxyBeanMethods = false) + static class UserConfiguration { + + private final McpToolboxClient mockClient = mock(McpToolboxClient.class); + + McpToolboxClient mockClient() { + return mockClient; + } + + @Bean + McpToolboxClient customClient() { + return mockClient; + } + } +}