diff --git a/.gitignore b/.gitignore index afd0bdc..810cc18 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,7 @@ crash.*.log # Language Specific target/ +src/test/resources/toolbox +src/test/resources/verification.db + diff --git a/example/README.md b/example/README.md index 672a4d5..1a1347d 100644 --- a/example/README.md +++ b/example/README.md @@ -53,8 +53,39 @@ In any case remember to change the `YOUR_TOOLBOX_SERVICE_ENDPOINT` placeholder i ```bash mvn compile ``` - Now run the example class: + Now run any of the example classes: ```bash - mvn clean compile exec:java -Dexec.mainClass="cloudcode.helloworld.ExampleUsage" + # Simple Async SDK Client usage + mvn clean compile exec:java -Dexec.mainClass="cloudcode.simple.ExampleUsage" + + # Sequential parameter bindings and authentication token provider usage + mvn clean compile exec:java -Dexec.mainClass="cloudcode.simple.ParameterBindingUsage" + + # Advanced Bulk Toolset pre-binding usage + mvn clean compile exec:java -Dexec.mainClass="cloudcode.bulk.BulkToolsetUsage" + + # Input validation behavior testing + mvn clean compile exec:java -Dexec.mainClass="cloudcode.validation.InputValidationTest" + + # Strict bindings behavior testing + mvn clean compile exec:java -Dexec.mainClass="cloudcode.validation.StrictFlagTest" ``` +## Integration Verification Testing + +To ensure the example remains fully functional and correct as the SDK evolves, the repository includes an automated integration test: `ToolboxActualServerVerifyTest`. + +### Purpose of the Test +This test validates the `ExampleUsage` client flow against the actual `toolbox` server binary using a local SQLite database. It compiles `ExampleUsage.java` dynamically and executes its `main` method, asserting that: +- Server discovery works and returns correct tools schema. +- Parameters can be dynamically queried, validated, and bound. +- Executing SQL queries via MCP Toolbox against SQLite returns the exact expected rows and prices. + +### How to Run the Verification Test Locally +To run this verification test: +1. Ensure the `toolbox` server binary for your architecture is placed at: + `src/test/resources/toolbox` +2. Run: + ```bash + mvn test -Dtest=ToolboxActualServerVerifyTest -Dnet.bytebuddy.experimental=true + ``` diff --git a/example/src/main/java/cloudcode/bulk/BulkToolsetUsage.java b/example/src/main/java/cloudcode/bulk/BulkToolsetUsage.java new file mode 100644 index 0000000..33d2c84 --- /dev/null +++ b/example/src/main/java/cloudcode/bulk/BulkToolsetUsage.java @@ -0,0 +1,160 @@ +/* + * 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 cloudcode.bulk; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.IdTokenProvider; +import com.google.cloud.mcp.AuthTokenGetter; +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.Tool; +import java.io.FileInputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Example demonstrating how to use the advanced Bulk Toolset Loading API. This showcases how to + * pre-bind parameters and credentials to multiple tools at startup, retrieving a map of fully + * configured and ready-to-use Tool objects. + */ +public class BulkToolsetUsage { + public static void main(String[] args) { + // CONFIGURATION + String targetUrl = System.getProperty("toolbox.url", "YOUR_TOOLBOX_SERVICE_ENDPOINT"); + String tokenAudience = System.getProperty("toolbox.tokenAudience", targetUrl); + String keyPath = System.getProperty("toolbox.keyPath", "YOUR_CREDENTIALS_JSON_FILE_PATH.json"); + + System.out.println("--- Starting MCP Toolbox Bulk Toolset Example ---"); + System.out.println("Target Server: " + targetUrl); + + try { + System.out.println(" [Init] Fetching ID Token..."); + GoogleCredentials credentials; + if (keyPath != null + && !keyPath.isEmpty() + && !keyPath.contains("YOUR_CREDENTIALS_JSON_FILE_PATH")) { + System.out.println(" [Auth] Using Service Account Key File: " + keyPath); + credentials = GoogleCredentials.fromStream(new FileInputStream(keyPath)); + } else { + System.out.println(" [Auth] Using Application Default Credentials (ADC)"); + credentials = GoogleCredentials.getApplicationDefault(); + } + + if (!(credentials instanceof IdTokenProvider)) { + throw new RuntimeException("Loaded credentials do not support ID Tokens."); + } + + String idToken = + ((IdTokenProvider) credentials) + .idTokenWithAudience(tokenAudience, Collections.emptyList()) + .getTokenValue(); + + // 1. Initialize Client + McpToolboxClient client = + McpToolboxClient.builder().baseUrl(targetUrl).apiKey(idToken).build(); + + // 2. Prepare Bulk Parameter Bindings + // Map structure: Tool Name -> (Parameter Name -> Parameter Value) + Map> paramBinds = new HashMap<>(); + + Map toyParams = new HashMap<>(); + toyParams.put( + "description", + "teddy bear"); // Pre-bind "description" to "teddy bear" for "get-toy-price" + paramBinds.put("get-toy-price", toyParams); + + // 3. Prepare Bulk Auth Bindings + // Map structure: Tool Name -> (Service Name -> Auth Token Getter) + Map> authBinds = new HashMap<>(); + + Map toyAuth = new HashMap<>(); + AuthTokenGetter toolAuthGetter = + () -> CompletableFuture.completedFuture("dummy-auth-token-from-provider"); + toyAuth.put( + "google_auth", toolAuthGetter); // Pre-bind "google_auth" service for "get-toy-price" + authBinds.put("get-toy-price", toyAuth); + + // 4. Load the entire toolset and apply all bindings at once + System.out.println(" [Init] Loading and binding toolset in bulk..."); + client + .loadToolset(null, paramBinds, authBinds, true) + .thenAccept( + boundTools -> { + System.out.println( + "\n[1] Toolset loaded in bulk. Total tools bound: " + boundTools.size()); + + // Let's verify and execute "get-retail-facet-filters" (simple tool, no + // pre-bindings) + Tool filterTool = boundTools.get("get-retail-facet-filters"); + if (filterTool != null) { + System.out.println("\n[2] Executing 'get-retail-facet-filters'..."); + filterTool + .execute(Map.of()) + .thenAccept( + result -> { + System.out.println(" -> Result: " + result.content().get(0).text()); + }) + .join(); + } else { + System.err.println("Tool 'get-retail-facet-filters' not found in toolset!"); + } + + // Let's verify and execute "get-toy-price" (pre-bound to "teddy bear") + Tool priceTool = boundTools.get("get-toy-price"); + if (priceTool != null) { + System.out.println( + "\n" + + "[3] Executing 'get-toy-price' (using pre-bound parameter 'teddy" + + " bear')..."); + // We run execute with empty map, so it falls back to the bound 'teddy bear' + priceTool + .execute(Map.of()) + .thenAccept( + result -> { + System.out.println( + " -> Result (Teddy Bear): " + result.content().get(0).text()); + }) + .join(); + + System.out.println( + "\n" + + "[4] Attempting to override parameter at runtime to 'barbie' (should be" + + " ignored/overridden by bound value)..."); + // We pass description at runtime but it will be overridden by bound "teddy bear" + priceTool + .execute(Map.of("description", "barbie")) + .thenAccept( + result -> { + System.out.println( + " -> Result (Actual): " + + result.content().get(0).text() + + " (Expect price for teddy bear: 14.99)"); + }) + .join(); + } else { + System.err.println("Tool 'get-toy-price' not found in toolset!"); + } + }) + .join(); + + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("\n--- Bulk Toolset Example Complete ---"); + } +} diff --git a/example/src/main/java/cloudcode/helloworld/ExampleUsage.java b/example/src/main/java/cloudcode/helloworld/ExampleUsage.java deleted file mode 100644 index 504886c..0000000 --- a/example/src/main/java/cloudcode/helloworld/ExampleUsage.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 cloudcode.helloworld; - -import java.util.Map; -import java.util.Collections; -import java.util.concurrent.CompletableFuture; -import java.io.FileInputStream; -import com.google.cloud.mcp.McpToolboxClient; -import com.google.cloud.mcp.AuthTokenGetter; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.IdTokenProvider; - -/** - * Sample Application to demostrate the usage of the MCP Toolbox Java SDK. - * Covers: Global Auth, Parameterized Auth, Discovery, Simple Tool, Authenticated Tool, Parameter Binding. - */ -public class ExampleUsage { - public static void main(String[] args) { - // CONFIGURATION - String targetUrl = "YOUR_TOOLBOX_SERVICE_ENDPOINT"; - - // Match the Service URL if using Cloud Run OIDC - String tokenAudience = targetUrl; - - // -------------------------------------------------------------------------------- - // AUTHENTICATION SETUP - // -------------------------------------------------------------------------------- - // FOR LOCAL DEVELOPMENT: Use a Service Account Key JSON file. - // FOR PRODUCTION (Cloud Run): Comment out the 'keyPath' logic and use ADC directly. - // -------------------------------------------------------------------------------- - - String keyPath = "YOUR_CREDENTIALS_JSON_FILE_PATH.json"; - - System.out.println("--- Starting MCP Toolbox Integration Test ---"); - System.out.println("Target Server: " + targetUrl); - -try { - System.out.println(" [Init] Fetching ID Token..."); - - GoogleCredentials credentials; - - // --- OPTION A: LOCAL DEV (Explicit Key File) --- - if (keyPath != null && !keyPath.isEmpty()) { - System.out.println(" [Auth] Using Service Account Key File: " + keyPath); - credentials = GoogleCredentials.fromStream(new FileInputStream(keyPath)); - } - // --- OPTION B: PRODUCTION (ADC) --- - else { - System.out.println(" [Auth] Using Application Default Credentials (ADC)"); - credentials = GoogleCredentials.getApplicationDefault(); - } - - if (!(credentials instanceof IdTokenProvider)) { - throw new RuntimeException("Loaded credentials do not support ID Tokens."); - } - - // Generate Token for the specified Audience - String idToken = ((IdTokenProvider) credentials).idTokenWithAudience(tokenAudience, Collections.emptyList()).getTokenValue(); - System.out.println(" [Debug] Token Generated."); - - // Initialize Client with Global Auth (Applies to ALL calls - Gate 1) - McpToolboxClient client = McpToolboxClient.builder() - .baseUrl(targetUrl) - .apiKey(idToken) - .build(); - - // STEP 1: TEST DISCOVERY METHODS - client.listTools() - .thenCompose(tools -> { - System.out.println("\n[1] listTools(): Success. Found " + tools.size() + " tools."); - return client.loadToolset(); - }) - .thenCompose(tools -> { - System.out.println("[2] loadToolset() (Alias): Success."); - return client.loadToolset("retail") - .handle((res, ex) -> { - if (ex == null) System.out.println("[3] loadToolset('retail'): Found " + res.size() + " tools."); - else System.out.println("[3] loadToolset('retail'): Skipped (Not configured on server)."); - return null; - }); - }) - .thenCompose(ignore -> { - - // STEP 2: INVOKE TOOL WITHOUT EXTRA AUTH - System.out.println("\n[4] Testing Simple Tool: 'get-retail-facet-filters'..."); - return client.invokeTool("get-retail-facet-filters", Map.of()); - }) - .thenCompose(result -> { - System.out.println(" -> Result: " + (result.content() != null ? "Received Data" : "Empty")); - - // STEP 3: INVOKE TOOL WITH AUTHENTICATED PARAMETERS - System.out.println("\n[5] Testing Authenticated Tool: 'get-toy-price'..."); - - // Define the getter for the 'google_auth' service - AuthTokenGetter toolAuthGetter = () -> CompletableFuture.completedFuture(idToken); - - // Load using the sophisticated overload - return client.loadTool("get-toy-price", Map.of("google_auth", toolAuthGetter)); - }) - .thenCompose(tool -> { - System.out.println(" -> Loaded Tool: " + tool.definition().description()); - - // STEP 4: TEST BINDING PARAMETERS SEQUENTIALLY - System.out.println("\n[A] Executing UNBOUND (Runtime arg: 'barbie')..."); - - return tool.execute(Map.of("description", "barbie")) - .thenCompose(result1 -> { - if (result1.content() != null && !result1.content().isEmpty()) { - System.out - .println(" -> Result (Unbound): " + result1.content().get(0).text()); - } - - // NOW bind the parameter - System.out.println("\n[B] Binding 'description' to 'soft toy'..."); - tool.bindParam("description", "soft toy"); - - System.out.println(" -> Executing BOUND (Runtime arg: 'barbie' - should be IGNORED)..."); - // We pass 'barbie', but expecting 'soft toy' price because of binding override - return tool.execute(Map.of("description", "barbie")); - }); - }) - .thenAccept(result -> { - System.out.println("\n[6] Final Result (Bound):"); - if (result.isError()) { - System.err.println("Tool execution failed: " + result.content().get(0).text()); - } else if (result.content() != null && !result.content().isEmpty()) { - String output = result.content().get(0).text(); - System.out.println(" " + output.substring(0, Math.min(output.length(), 200)) + "..."); - } else { - System.out.println(" Empty Response"); - } - }) - .exceptionally(ex -> { - System.err.println("\n!!! TEST FAILED !!!"); - ex.printStackTrace(); - return null; - }) - .join(); - - } catch (Exception e) { - e.printStackTrace(); - } - System.out.println("\n--- Test Suite Complete ---"); - } -} diff --git a/example/src/main/java/cloudcode/helloworld/InputValidationTest.java b/example/src/main/java/cloudcode/helloworld/InputValidationTest.java deleted file mode 100644 index b171ae7..0000000 --- a/example/src/main/java/cloudcode/helloworld/InputValidationTest.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 cloudcode.helloworld; - -import com.google.cloud.mcp.McpToolboxClient; -import com.google.cloud.mcp.Tool; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.IdTokenProvider; -import java.io.FileInputStream; -import java.util.Collections; -import java.util.Map; -import java.util.HashMap; -import java.util.concurrent.CompletableFuture; - -public class InputValidationTest { - public static void main(String[] args) { - String targetUrl = "YOUR_TOOLBOX_SERVICE_ENDPOINT"; - String tokenAudience = targetUrl; - // -------------------------------------------------------------------------------- - // AUTHENTICATION SETUP - // -------------------------------------------------------------------------------- - // FOR LOCAL DEVELOPMENT: Use a Service Account Key JSON file. - // FOR PRODUCTION (Cloud Run): Comment out the 'keyPath' logic and use ADC directly. - // -------------------------------------------------------------------------------- - - String keyPath = "/YOUR_CREDENTIALS_JSON_FILE_PATH.json"; - - System.out.println("--- Starting MCP Toolbox Input Validation Test ---"); - - try { - // 1. Setup Auth (Same as before) - System.out.println(" [Init] Fetching ID Token..."); - GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(keyPath)); - if (!(credentials instanceof IdTokenProvider)) { - throw new RuntimeException("Loaded credentials do not support ID Tokens."); - } - String idToken = ((IdTokenProvider) credentials).idTokenWithAudience(tokenAudience, Collections.emptyList()).getTokenValue(); - - // 2. Initialize Client - McpToolboxClient client = McpToolboxClient.builder() - .baseUrl(targetUrl) - .build(); - - // 3. Load the Tool - // We MUST use loadTool() because validation relies on the ToolDefinition fetched from the server. - System.out.println(" [Init] Loading tool 'get-toy-price'..."); - Tool tool = client.loadTool("get-toy-price").join(); - - // 4. Register Auth - // We manually register the token getter so the Tool object can inject the header. - tool.addAuthTokenGetter("google_auth", () -> CompletableFuture.completedFuture(idToken)); - - - // --- Test Case A: Valid Input --- - System.out.println("\n[Test A] Sending VALID input (String)..."); - try { - Map validArgs = Map.of("description", "barbie"); - var result = tool.execute(validArgs).join(); - System.out.println(" ✅ Success! Output: " + (result.content().isEmpty() ? "Empty" : result.content().get(0).text())); - } catch (Exception e) { - System.err.println(" ❌ Unexpected failure: " + e.getMessage()); - e.printStackTrace(); - } - - - // --- Test Case B: Invalid Type (Int instead of String) --- - System.out.println("\n[Test B] Sending INVALID input (Integer instead of String)..."); - try { - // The 'description' parameter is defined as type: string. We pass an Integer. - Map invalidArgs = Map.of("description", 12345); - - tool.execute(invalidArgs).join(); - System.err.println(" ❌ FAILED: Validation did not catch the error!"); - } catch (Exception e) { - // We expect a RuntimeException wrapping IllegalArgumentException - Throwable cause = e.getCause(); - System.out.println(" ✅ Caught Expected Error: " + cause.getMessage()); - } - - - // --- Test Case C: Null Value (Filtering) --- - System.out.println("\n[Test C] Sending NULL value (should be filtered)..."); - try { - // We use a HashMap because Map.of doesn't allow nulls - Map nullArgs = new HashMap<>(); - nullArgs.put("description", "barbie"); // Valid param - nullArgs.put("some_optional_param", null); // Null param - - // If validation works, 'some_optional_param' will be removed before sending - var result = tool.execute(nullArgs).join(); - System.out.println(" ✅ Success! Null value was filtered and request succeeded."); - } catch (Exception e) { - System.out.println(" ❌ Result: " + e.getCause().getMessage()); - } - - } catch (Exception e) { - e.printStackTrace(); - } - System.out.println("\n--- Done ---"); - } -} diff --git a/example/src/main/java/cloudcode/simple/ExampleUsage.java b/example/src/main/java/cloudcode/simple/ExampleUsage.java new file mode 100644 index 0000000..8189c79 --- /dev/null +++ b/example/src/main/java/cloudcode/simple/ExampleUsage.java @@ -0,0 +1,40 @@ +/* + * 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 cloudcode.simple; + +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.ToolResult; +import java.util.Map; + +/** A minimal example demonstrating client initialization, tool discovery, and invocation. */ +public class ExampleUsage { + public static void main(String[] args) { + String targetUrl = System.getProperty("toolbox.url", "YOUR_TOOLBOX_SERVICE_ENDPOINT"); + String apiKey = System.getProperty("toolbox.apiKey", "YOUR_API_KEY"); + + // Initialize the client + McpToolboxClient client = McpToolboxClient.builder().baseUrl(targetUrl).apiKey(apiKey).build(); + + // 1. List available tools synchronously + Map tools = client.listTools().join(); + System.out.println("Available tools: " + tools.keySet()); + + // 2. Invoke a simple tool synchronously + ToolResult result = client.invokeTool("get-retail-facet-filters", Map.of()).join(); + System.out.println("Result: " + result.content().get(0).text()); + } +} diff --git a/example/src/main/java/cloudcode/simple/ParameterBindingUsage.java b/example/src/main/java/cloudcode/simple/ParameterBindingUsage.java new file mode 100644 index 0000000..e4f703e --- /dev/null +++ b/example/src/main/java/cloudcode/simple/ParameterBindingUsage.java @@ -0,0 +1,75 @@ +/* + * 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 cloudcode.simple; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.IdTokenProvider; +import com.google.cloud.mcp.AuthTokenGetter; +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.Tool; +import com.google.cloud.mcp.ToolResult; +import java.io.FileInputStream; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** Example demonstrating how to use parameter bindings and authenticated tool methods. */ +public class ParameterBindingUsage { + public static void main(String[] args) { + String targetUrl = System.getProperty("toolbox.url", "YOUR_TOOLBOX_SERVICE_ENDPOINT"); + String tokenAudience = System.getProperty("toolbox.tokenAudience", targetUrl); + String keyPath = System.getProperty("toolbox.keyPath", "YOUR_CREDENTIALS_JSON_FILE_PATH.json"); + + try { + GoogleCredentials credentials; + if (keyPath != null && !keyPath.isEmpty()) { + credentials = GoogleCredentials.fromStream(new FileInputStream(keyPath)); + } else { + credentials = GoogleCredentials.getApplicationDefault(); + } + + if (!(credentials instanceof IdTokenProvider)) { + throw new RuntimeException("Loaded credentials do not support ID Tokens."); + } + + String idToken = + ((IdTokenProvider) credentials) + .idTokenWithAudience(tokenAudience, Collections.emptyList()) + .getTokenValue(); + + // Initialize Client + McpToolboxClient client = + McpToolboxClient.builder().baseUrl(targetUrl).apiKey(idToken).build(); + + // 1. Load the tool with authentication providers + AuthTokenGetter toolAuthGetter = () -> CompletableFuture.completedFuture(idToken); + Tool tool = client.loadTool("get-toy-price", Map.of("google_auth", toolAuthGetter)).join(); + + // 2. Execute unbound (with explicit runtime argument) + ToolResult resultUnbound = tool.execute(Map.of("description", "barbie")).join(); + System.out.println("Result (unbound): " + resultUnbound.content().get(0).text()); + + // 3. Bind the parameter and execute bound (runtime arg will be overridden by binding) + tool.bindParam("description", "soft toy"); + ToolResult resultBound = tool.execute(Map.of("description", "barbie")).join(); + System.out.println("Result (bound): " + resultBound.content().get(0).text()); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/example/src/main/java/cloudcode/validation/InputValidationTest.java b/example/src/main/java/cloudcode/validation/InputValidationTest.java new file mode 100644 index 0000000..9db5e6f --- /dev/null +++ b/example/src/main/java/cloudcode/validation/InputValidationTest.java @@ -0,0 +1,116 @@ +/* + * 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 cloudcode.validation; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.IdTokenProvider; +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.Tool; +import java.io.FileInputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class InputValidationTest { + public static void main(String[] args) { + String targetUrl = "YOUR_TOOLBOX_SERVICE_ENDPOINT"; + String tokenAudience = targetUrl; + // -------------------------------------------------------------------------------- + // AUTHENTICATION SETUP + // -------------------------------------------------------------------------------- + // FOR LOCAL DEVELOPMENT: Use a Service Account Key JSON file. + // FOR PRODUCTION (Cloud Run): Comment out the 'keyPath' logic and use ADC directly. + // -------------------------------------------------------------------------------- + + String keyPath = "/YOUR_CREDENTIALS_JSON_FILE_PATH.json"; + + System.out.println("--- Starting MCP Toolbox Input Validation Test ---"); + + try { + // 1. Setup Auth (Same as before) + System.out.println(" [Init] Fetching ID Token..."); + GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(keyPath)); + if (!(credentials instanceof IdTokenProvider)) { + throw new RuntimeException("Loaded credentials do not support ID Tokens."); + } + String idToken = + ((IdTokenProvider) credentials) + .idTokenWithAudience(tokenAudience, Collections.emptyList()) + .getTokenValue(); + + // 2. Initialize Client + McpToolboxClient client = McpToolboxClient.builder().baseUrl(targetUrl).build(); + + // 3. Load the Tool + // We MUST use loadTool() because validation relies on the ToolDefinition fetched from the + // server. + System.out.println(" [Init] Loading tool 'get-toy-price'..."); + Tool tool = client.loadTool("get-toy-price").join(); + + // 4. Register Auth + // We manually register the token getter so the Tool object can inject the header. + tool.addAuthTokenGetter("google_auth", () -> CompletableFuture.completedFuture(idToken)); + + // --- Test Case A: Valid Input --- + System.out.println("\n[Test A] Sending VALID input (String)..."); + try { + Map validArgs = Map.of("description", "barbie"); + var result = tool.execute(validArgs).join(); + System.out.println( + " ✅ Success! Output: " + + (result.content().isEmpty() ? "Empty" : result.content().get(0).text())); + } catch (Exception e) { + System.err.println(" ❌ Unexpected failure: " + e.getMessage()); + e.printStackTrace(); + } + + // --- Test Case B: Invalid Type (Int instead of String) --- + System.out.println("\n[Test B] Sending INVALID input (Integer instead of String)..."); + try { + // The 'description' parameter is defined as type: string. We pass an Integer. + Map invalidArgs = Map.of("description", 12345); + + tool.execute(invalidArgs).join(); + System.err.println(" ❌ FAILED: Validation did not catch the error!"); + } catch (Exception e) { + // We expect a RuntimeException wrapping IllegalArgumentException + Throwable cause = e.getCause(); + System.out.println(" ✅ Caught Expected Error: " + cause.getMessage()); + } + + // --- Test Case C: Null Value (Filtering) --- + System.out.println("\n[Test C] Sending NULL value (should be filtered)..."); + try { + // We use a HashMap because Map.of doesn't allow nulls + Map nullArgs = new HashMap<>(); + nullArgs.put("description", "barbie"); // Valid param + nullArgs.put("some_optional_param", null); // Null param + + // If validation works, 'some_optional_param' will be removed before sending + var result = tool.execute(nullArgs).join(); + System.out.println(" ✅ Success! Null value was filtered and request succeeded."); + } catch (Exception e) { + System.out.println(" ❌ Result: " + e.getCause().getMessage()); + } + + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("\n--- Done ---"); + } +} diff --git a/example/src/main/java/cloudcode/helloworld/StrictFlagTest.java b/example/src/main/java/cloudcode/validation/StrictFlagTest.java similarity index 89% rename from example/src/main/java/cloudcode/helloworld/StrictFlagTest.java rename to example/src/main/java/cloudcode/validation/StrictFlagTest.java index ba32ff9..f502ef2 100644 --- a/example/src/main/java/cloudcode/helloworld/StrictFlagTest.java +++ b/example/src/main/java/cloudcode/validation/StrictFlagTest.java @@ -14,21 +14,19 @@ * limitations under the License. */ -package cloudcode.helloworld; +package cloudcode.validation; import com.google.cloud.mcp.McpToolboxClient; import com.google.cloud.mcp.Tool; -import java.util.Map; import java.util.HashMap; +import java.util.Map; public class StrictFlagTest { public static void main(String[] args) { String targetUrl = "YOUR_TOOLBOX_SERVICE_ENDPOINT"; System.out.println("--- Starting MCP Toolbox Strict Flag Test ---"); - McpToolboxClient client = McpToolboxClient.builder() - .baseUrl(targetUrl) - .build(); + McpToolboxClient client = McpToolboxClient.builder().baseUrl(targetUrl).build(); // Prepare bindings for a NON-EXISTENT tool Map> paramBinds = new HashMap<>(); @@ -38,7 +36,8 @@ public static void main(String[] args) { System.out.println("\n[Test 1] Loading with Strict = FALSE..."); try { Map tools = client.loadToolset(null, paramBinds, null, false).join(); - System.out.println(" ✅ Success! Loaded " + tools.size() + " tools. Unknown binding was ignored."); + System.out.println( + " ✅ Success! Loaded " + tools.size() + " tools. Unknown binding was ignored."); } catch (Exception e) { System.err.println(" ❌ Failed unexpectedly: " + e.getMessage()); } @@ -60,4 +59,4 @@ public static void main(String[] args) { System.out.println("\n--- Done ---"); } -} \ No newline at end of file +} diff --git a/src/test/java/com/google/cloud/mcp/e2e/ToolboxActualServerVerifyTest.java b/src/test/java/com/google/cloud/mcp/e2e/ToolboxActualServerVerifyTest.java new file mode 100644 index 0000000..d772fa6 --- /dev/null +++ b/src/test/java/com/google/cloud/mcp/e2e/ToolboxActualServerVerifyTest.java @@ -0,0 +1,274 @@ +/* + * 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.e2e; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.IdToken; +import com.google.auth.oauth2.IdTokenProvider; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class ToolboxActualServerVerifyTest { + private static Process serverProcess; + private static final int PORT = 8099; + private static final String DB_PATH = "src/test/resources/verification.db"; + + @BeforeAll + public static void startActualServer() throws Exception { + // 1. Clean up any previous DB file + Files.deleteIfExists(Paths.get(DB_PATH)); + + // 2. Initialize local SQLite database + System.out.println("Initializing SQLite database at " + DB_PATH + "..."); + ProcessBuilder pbDb = new ProcessBuilder("/usr/bin/sqlite3", DB_PATH); + pbDb.redirectInput(new File("src/test/resources/verify_schema.sql")); + Process pDb = pbDb.start(); + boolean finished = pDb.waitFor(10, TimeUnit.SECONDS); + if (!finished || pDb.exitValue() != 0) { + throw new RuntimeException( + "Failed to initialize SQLite database. Exit value: " + pDb.exitValue()); + } + System.out.println("Database initialized."); + + // 3. Start actual MCP Toolbox server + System.out.println("Starting actual MCP Toolbox server on port " + PORT + "..."); + ProcessBuilder pbServer = + new ProcessBuilder( + "src/test/resources/toolbox", + "--config", + "src/test/resources/verify_tools.yaml", + "--port", + String.valueOf(PORT)); + pbServer.inheritIO(); + serverProcess = pbServer.start(); + + // Wait a few seconds for server to start up + Thread.sleep(3000); + + if (!serverProcess.isAlive()) { + throw new RuntimeException("Toolbox server process died immediately."); + } + System.out.println("Actual Toolbox server started successfully."); + } + + @AfterAll + public static void stopActualServer() throws Exception { + System.out.println("Stopping actual MCP Toolbox server..."); + if (serverProcess != null) { + serverProcess.destroy(); + if (!serverProcess.waitFor(5, TimeUnit.SECONDS)) { + serverProcess.destroyForcibly(); + } + } + Files.deleteIfExists(Paths.get(DB_PATH)); + System.out.println("Server stopped and database cleaned."); + } + + @Test + public void testActualServerExampleFlow() throws Exception { + // Set System properties to configure ExampleUsage to hit our local port 8099 + System.setProperty("toolbox.url", "http://localhost:" + PORT + "/mcp"); + System.setProperty("toolbox.keyPath", ""); // Empty string to bypass file checking and use ADC + + // 1. Programmatically compile ExampleUsage.java on the fly using test classpath + File sourceFile = new File("example/src/main/java/cloudcode/simple/ExampleUsage.java"); + assertTrue( + sourceFile.exists(), "ExampleUsage.java does not exist at " + sourceFile.getAbsolutePath()); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull( + compiler, + "System Java compiler is not available. Please run tests using a JDK, not a JRE."); + + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + String classpath = System.getProperty("java.class.path"); + + List optionList = new ArrayList<>(); + optionList.add("-classpath"); + optionList.add(classpath); + optionList.add("-d"); + optionList.add("target/test-classes"); + + Iterable compilationUnits = + fileManager.getJavaFileObjectsFromFiles(Arrays.asList(sourceFile)); + boolean compileSuccess = + compiler.getTask(null, fileManager, null, optionList, null, compilationUnits).call(); + fileManager.close(); + + assertTrue(compileSuccess, "Compilation of ExampleUsage.java failed."); + System.out.println("ExampleUsage.java compiled successfully."); + + // 2. Load the compiled ExampleUsage class + Class clazz = Class.forName("cloudcode.simple.ExampleUsage"); + + // Mock GoogleCredentials statically to return a dummy IdTokenProvider + try (MockedStatic mockedCredentials = + Mockito.mockStatic(GoogleCredentials.class)) { + GoogleCredentials mockCreds = + Mockito.mock( + GoogleCredentials.class, + Mockito.withSettings().extraInterfaces(IdTokenProvider.class)); + IdToken mockToken = Mockito.mock(IdToken.class); + Mockito.when(mockToken.getTokenValue()).thenReturn("dummy-token"); + Mockito.when( + ((IdTokenProvider) mockCreds) + .idTokenWithAudience(Mockito.anyString(), Mockito.anyList())) + .thenReturn(mockToken); + + mockedCredentials.when(GoogleCredentials::getApplicationDefault).thenReturn(mockCreds); + + // Execute the actual example class's main method directly! + System.out.println("Executing ExampleUsage.main..."); + clazz.getMethod("main", String[].class).invoke(null, (Object) new String[0]); + System.out.println("ExampleUsage.main completed successfully."); + } finally { + System.clearProperty("toolbox.url"); + System.clearProperty("toolbox.keyPath"); + } + } + + @Test + public void testBulkToolsetExampleFlow() throws Exception { + // Set System properties to configure BulkToolsetUsage to hit our local port 8099 + System.setProperty("toolbox.url", "http://localhost:" + PORT + "/mcp"); + System.setProperty("toolbox.keyPath", ""); + + // 1. Programmatically compile BulkToolsetUsage.java on the fly + File sourceFile = new File("example/src/main/java/cloudcode/bulk/BulkToolsetUsage.java"); + assertTrue(sourceFile.exists(), "BulkToolsetUsage.java does not exist"); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + String classpath = System.getProperty("java.class.path"); + + List optionList = new ArrayList<>(); + optionList.add("-classpath"); + optionList.add(classpath); + optionList.add("-d"); + optionList.add("target/test-classes"); + + Iterable compilationUnits = + fileManager.getJavaFileObjectsFromFiles(Arrays.asList(sourceFile)); + boolean compileSuccess = + compiler.getTask(null, fileManager, null, optionList, null, compilationUnits).call(); + fileManager.close(); + + assertTrue(compileSuccess, "Compilation of BulkToolsetUsage.java failed."); + + // 2. Load the compiled BulkToolsetUsage class + Class clazz = Class.forName("cloudcode.bulk.BulkToolsetUsage"); + + // Mock GoogleCredentials statically to return a dummy IdTokenProvider + try (MockedStatic mockedCredentials = + Mockito.mockStatic(GoogleCredentials.class)) { + GoogleCredentials mockCreds = + Mockito.mock( + GoogleCredentials.class, + Mockito.withSettings().extraInterfaces(IdTokenProvider.class)); + IdToken mockToken = Mockito.mock(IdToken.class); + Mockito.when(mockToken.getTokenValue()).thenReturn("dummy-token"); + Mockito.when( + ((IdTokenProvider) mockCreds) + .idTokenWithAudience(Mockito.anyString(), Mockito.anyList())) + .thenReturn(mockToken); + + mockedCredentials.when(GoogleCredentials::getApplicationDefault).thenReturn(mockCreds); + + // Execute the actual example class's main method directly! + System.out.println("Executing BulkToolsetUsage.main..."); + clazz.getMethod("main", String[].class).invoke(null, (Object) new String[0]); + System.out.println("BulkToolsetUsage.main completed successfully."); + } finally { + System.clearProperty("toolbox.url"); + System.clearProperty("toolbox.keyPath"); + } + } + + @Test + public void testParameterBindingExampleFlow() throws Exception { + // Set System properties to configure ParameterBindingUsage to hit our local port 8099 + System.setProperty("toolbox.url", "http://localhost:" + PORT + "/mcp"); + System.setProperty("toolbox.keyPath", ""); + + // 1. Programmatically compile ParameterBindingUsage.java on the fly + File sourceFile = new File("example/src/main/java/cloudcode/simple/ParameterBindingUsage.java"); + assertTrue(sourceFile.exists(), "ParameterBindingUsage.java does not exist"); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + String classpath = System.getProperty("java.class.path"); + + List optionList = new ArrayList<>(); + optionList.add("-classpath"); + optionList.add(classpath); + optionList.add("-d"); + optionList.add("target/test-classes"); + + Iterable compilationUnits = + fileManager.getJavaFileObjectsFromFiles(Arrays.asList(sourceFile)); + boolean compileSuccess = + compiler.getTask(null, fileManager, null, optionList, null, compilationUnits).call(); + fileManager.close(); + + assertTrue(compileSuccess, "Compilation of ParameterBindingUsage.java failed."); + + // 2. Load the compiled ParameterBindingUsage class + Class clazz = Class.forName("cloudcode.simple.ParameterBindingUsage"); + + // Mock GoogleCredentials statically to return a dummy IdTokenProvider + try (MockedStatic mockedCredentials = + Mockito.mockStatic(GoogleCredentials.class)) { + GoogleCredentials mockCreds = + Mockito.mock( + GoogleCredentials.class, + Mockito.withSettings().extraInterfaces(IdTokenProvider.class)); + IdToken mockToken = Mockito.mock(IdToken.class); + Mockito.when(mockToken.getTokenValue()).thenReturn("dummy-token"); + Mockito.when( + ((IdTokenProvider) mockCreds) + .idTokenWithAudience(Mockito.anyString(), Mockito.anyList())) + .thenReturn(mockToken); + + mockedCredentials.when(GoogleCredentials::getApplicationDefault).thenReturn(mockCreds); + + // Execute the actual example class's main method directly! + System.out.println("Executing ParameterBindingUsage.main..."); + clazz.getMethod("main", String[].class).invoke(null, (Object) new String[0]); + System.out.println("ParameterBindingUsage.main completed successfully."); + } finally { + System.clearProperty("toolbox.url"); + System.clearProperty("toolbox.keyPath"); + } + } +} diff --git a/src/test/resources/verify_schema.sql b/src/test/resources/verify_schema.sql new file mode 100644 index 0000000..103a126 --- /dev/null +++ b/src/test/resources/verify_schema.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS apparels ( + id INTEGER PRIMARY KEY, + content TEXT, + uri TEXT, + category TEXT, + sub_category TEXT, + color TEXT, + gender TEXT +); + +INSERT INTO apparels (content, uri, category, sub_category, color, gender) VALUES +('Red T-Shirt', 'http://example.com/red-tshirt', 'apparel', 'tshirt', 'red', 'unisex'), +('Blue Jeans', 'http://example.com/blue-jeans', 'apparel', 'jeans', 'blue', 'unisex'); + +CREATE TABLE IF NOT EXISTS toys ( + id INTEGER PRIMARY KEY, + name TEXT, + description TEXT, + price REAL +); + +INSERT INTO toys (name, description, price) VALUES +('Barbie', 'barbie doll', 19.99), +('Teddy Bear', 'soft toy teddy bear', 14.99), +('Lego Set', 'construction blocks lego', 49.99); diff --git a/src/test/resources/verify_tools.yaml b/src/test/resources/verify_tools.yaml new file mode 100644 index 0000000..b9d18b7 --- /dev/null +++ b/src/test/resources/verify_tools.yaml @@ -0,0 +1,22 @@ +kind: source +name: local-sqlite +type: sqlite +database: "/Users/stenalpjolly/github/Google/opensource/mcp-toolbox-sdk-java/src/test/resources/verification.db" +--- +kind: tool +name: get-retail-facet-filters +type: sqlite-sql +source: local-sqlite +description: "Get the list of facet filter values from the retail dataset." +statement: "SELECT id, content, uri, category, sub_category, color, gender FROM apparels;" +--- +kind: tool +name: get-toy-price +type: sqlite-sql +source: local-sqlite +description: "Get the price of a toy based on a description." +parameters: + - name: description + type: string + description: "A description of the toy to search for." +statement: "SELECT price FROM toys WHERE lower(description) LIKE '%' || lower(?) || '%' LIMIT 1;"