diff --git a/README.md b/README.md
index b5aa640..454e7ab 100644
--- a/README.md
+++ b/README.md
@@ -216,15 +216,19 @@ public class App {
For a detailed example, check the ExampleUsage.java file in the example folder of this repo.
> [!NOTE]
->
-> The SDK is Async-First, using Java's `CompletableFuture` to bridge both patterns naturally.
-> - Asynchronous: Chain methods using `.thenCompose()`, `.thenAccept()`, and `.exceptionally()` for non-blocking execution.
-> - If you prefer synchronous execution, simply call `.join()` on the result to block until completion.
+> The SDK is async-first, but provides a pure synchronous blocking experience using `McpToolboxSyncClient`.
+> - **Asynchronous Client (`McpToolboxClient`):** Chain methods using `.thenCompose()`, `.thenAccept()`, and `.exceptionally()`.
+> - **Synchronous Client (`McpToolboxSyncClient`):** Call blocking methods directly. Under the hood, it manages threads safely and translates checked/execution exceptions into unchecked `McpToolboxException`s.
+
```java
-// Async (Non-blocking)
-client.invokeTool("tool-name", args).thenAccept(result -> ...);
-// Sync (Blocking)
-ToolResult result = client.invokeTool("tool-name", args).join();
+// Create a Synchronous Client
+McpToolboxSyncClient client = McpToolboxClient.builder()
+ .baseUrl("https://my-toolbox-service.a.run.app/mcp")
+ .buildSync();
+
+// Invoke a tool synchronously
+ToolResult result = client.invokeTool("get-toy-price", Map.of("description", "plush dinosaur"));
+System.out.println("Output: " + result.content().get(0).text());
```
## Authentication
diff --git a/demo-applications/cymbal-transit/pom.xml b/demo-applications/cymbal-transit/pom.xml
index 7c420d4..0897eaa 100644
--- a/demo-applications/cymbal-transit/pom.xml
+++ b/demo-applications/cymbal-transit/pom.xml
@@ -69,7 +69,7 @@
com.google.cloud.mcp
mcp-toolbox-sdk-java
- 0.2.0
+ 0.2.1-SNAPSHOT
diff --git a/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/CymbalTransitApplication.java b/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/CymbalTransitApplication.java
index 070c86e..a65ad96 100644
--- a/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/CymbalTransitApplication.java
+++ b/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/CymbalTransitApplication.java
@@ -40,6 +40,8 @@ public static void main(final String[] args) throws Exception {
// Start the Spring Boot application.
app.run(args);
logger.info(
- "Hello from Cloud Run! The container started successfully and is listening for HTTP requests on " + port);
+ "Hello from Cloud Run! The container started successfully and is listening for HTTP"
+ + " requests on "
+ + port);
}
}
diff --git a/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/web/CymbalTransitController.java b/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/web/CymbalTransitController.java
index 6cea16f..4ef044d 100644
--- a/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/web/CymbalTransitController.java
+++ b/demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/web/CymbalTransitController.java
@@ -16,250 +16,272 @@
package cloudcode.cymbal.web;
-import com.google.cloud.mcp.McpToolboxClient;
import cloudcode.cymbal.CymbalTransitApplication;
-import com.google.cloud.mcp.AuthTokenGetter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenProvider;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.beans.factory.annotation.Value;
-
-import javax.annotation.PostConstruct;
-import javax.servlet.http.HttpSession;
-import java.io.FileInputStream;
-import java.util.Collections;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-
-// LangChain4j Imports for Agentic Routing & Gemini 3 Flash
+import com.google.cloud.mcp.McpToolboxClient;
+import com.google.cloud.mcp.auth.AuthTokenGetter;
+import dev.langchain4j.agent.tool.Tool;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
-import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
-import dev.langchain4j.agent.tool.Tool;
-import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpSession;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.bind.annotation.*;
@SpringBootApplication
public class CymbalTransitController {
- public static void main(String[] args) {
- SpringApplication.run(CymbalTransitApplication.class, args);
- }
+ public static void main(String[] args) {
+ SpringApplication.run(CymbalTransitApplication.class, args);
+ }
}
/**
- * 1. AI AGENT CONFIGURATION
- * Configures Gemini 3 Flash and binds it to our LangChain4j Agent Interface.
+ * 1. AI AGENT CONFIGURATION Configures Gemini 3 Flash and binds it to our LangChain4j Agent
+ * Interface.
*/
@Configuration
class AgentConfiguration {
- @Value("${GCP_PROJECT_ID:fallback_project_id}")
- private String projectId;
-
- @Value("${GCP_REGION:fallback_region}")
- private String region;
-
- @Value("${GEMINI_MODEL_NAME:fallback_model}")
- private String modelName;
-
- @Bean
- ChatLanguageModel geminiChatModel() {
- return VertexAiGeminiChatModel.builder()
- .project(projectId)
- .location(region)
- .modelName(modelName) // Utilizing externalized parameters
- .build();
- }
-
- @Bean
- TransitAgent transitAgent(ChatLanguageModel chatLanguageModel, TransitAgentTools tools) {
- return AiServices.builder(TransitAgent.class)
- .chatLanguageModel(chatLanguageModel)
- .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20))
- .tools(tools) // Exposes our MCP tools to Gemini
- .build();
- }
+ @Value("${GCP_PROJECT_ID:fallback_project_id}")
+ private String projectId;
+
+ @Value("${GCP_REGION:fallback_region}")
+ private String region;
+
+ @Value("${GEMINI_MODEL_NAME:fallback_model}")
+ private String modelName;
+
+ @Bean
+ ChatLanguageModel geminiChatModel() {
+ return VertexAiGeminiChatModel.builder()
+ .project(projectId)
+ .location(region)
+ .modelName(modelName) // Utilizing externalized parameters
+ .build();
+ }
+
+ @Bean
+ TransitAgent transitAgent(ChatLanguageModel chatLanguageModel, TransitAgentTools tools) {
+ return AiServices.builder(TransitAgent.class)
+ .chatLanguageModel(chatLanguageModel)
+ .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20))
+ .tools(tools) // Exposes our MCP tools to Gemini
+ .build();
+ }
}
-/**
- * 2. THE AI AGENT INTERFACE
- * Declarative AI service handling routing logic via System Prompt.
- */
+/** 2. THE AI AGENT INTERFACE Declarative AI service handling routing logic via System Prompt. */
interface TransitAgent {
- @SystemMessage({
- "You are the Cymbal Transit Concierge.",
- "CRITICAL INSTRUCTION: On your very first interaction, you MUST use the 'findAllSchedules' tool to fetch and memorize the broad bus routes.",
- "Keep this data handy in your context. Answer general routing questions using this stored data. ",
- "If you have to list the route details to the user, show it along with the full UUID and with other details that are meaningful. If the user chooses to book ticket as the next step, prompt them to copy the correct UUID nad paste so the transaction can be confirmed.",
- "ONLY if the user asks a specifically narrowed-down question, asks for precise times, or assigns a booking task, or asks about policies should you route to the specific tools like 'querySchedules', 'bookTicket', 'searchPolicies'.",
- "Remember the tool 'querySchedules' is for finding schedules between cities, 'bookTicket' is for booking ticket actionable between 2 cities, 'searchPolicies' is for finding matching policies for this company.",
- "Be intuitive and intelligent in finding the context even when user has typos. Do no hallucinate and make up stuff though. USe only data from the tools. ",
- "Don't show any asterisks while listing results. Keep it formatted and numbered or bulleted. asterisks distract."
- })
- String chat(@MemoryId String sessionId, @UserMessage String userMessage);
+ @SystemMessage({
+ "You are the Cymbal Transit Concierge.",
+ "CRITICAL INSTRUCTION: On your very first interaction, you MUST use the 'findAllSchedules' tool"
+ + " to fetch and memorize the broad bus routes.",
+ "Keep this data handy in your context. Answer general routing questions using this stored data."
+ + " ",
+ "If you have to list the route details to the user, show it along with the full UUID and with"
+ + " other details that are meaningful. If the user chooses to book ticket as the next step,"
+ + " prompt them to copy the correct UUID nad paste so the transaction can be confirmed.",
+ "ONLY if the user asks a specifically narrowed-down question, asks for precise times, or"
+ + " assigns a booking task, or asks about policies should you route to the specific tools"
+ + " like 'querySchedules', 'bookTicket', 'searchPolicies'.",
+ "Remember the tool 'querySchedules' is for finding schedules between cities, 'bookTicket' is"
+ + " for booking ticket actionable between 2 cities, 'searchPolicies' is for finding"
+ + " matching policies for this company.",
+ "Be intuitive and intelligent in finding the context even when user has typos. Do no"
+ + " hallucinate and make up stuff though. USe only data from the tools. ",
+ "Don't show any asterisks while listing results. Keep it formatted and numbered or bulleted."
+ + " asterisks distract."
+ })
+ String chat(@MemoryId String sessionId, @UserMessage String userMessage);
}
/**
- * 3. THE TOOLBOX BRIDGE
- * Wraps our asynchronous MCP Client calls into synchronous @Tools that LangChain4j (Gemini) can execute.
+ * 3. THE TOOLBOX BRIDGE Wraps our asynchronous MCP Client calls into synchronous @Tools that
+ * LangChain4j (Gemini) can execute.
*/
@Service
class TransitAgentTools {
-
- private final McpToolboxService mcpService;
-
- public TransitAgentTools(McpToolboxService mcpService) {
- this.mcpService = mcpService;
- }
- @Tool("Fetches the initial, broad dataset of all available bus schedules and routes. Use this to build your context.")
- public String findAllSchedules() {
- return mcpService.findAllSchedules().join();
- }
-
- @Tool("Query specific schedules between an origin and destination city. Use only when the user narrows down their request.")
- public String querySchedules(String origin, String destination) {
- return mcpService.querySchedules(origin, destination).join();
- }
-
- @Tool("Book a ticket for a passenger using a specific trip ID.")
- public String bookTicket(String tripId, String passengerName) {
- return mcpService.bookTicket(tripId, passengerName).join();
- }
-
- @Tool("Semantic search for transit policies regarding luggage, pets, refunds, and general rules.")
- public String searchPolicies(String searchQuery) {
- return mcpService.searchPolicies(searchQuery).join();
- }
+ private final McpToolboxService mcpService;
+
+ public TransitAgentTools(McpToolboxService mcpService) {
+ this.mcpService = mcpService;
+ }
+
+ @Tool(
+ "Fetches the initial, broad dataset of all available bus schedules and routes. Use this to"
+ + " build your context.")
+ public String findAllSchedules() {
+ return mcpService.findAllSchedules().join();
+ }
+
+ @Tool(
+ "Query specific schedules between an origin and destination city. Use only when the user"
+ + " narrows down their request.")
+ public String querySchedules(String origin, String destination) {
+ return mcpService.querySchedules(origin, destination).join();
+ }
+
+ @Tool("Book a ticket for a passenger using a specific trip ID.")
+ public String bookTicket(String tripId, String passengerName) {
+ return mcpService.bookTicket(tripId, passengerName).join();
+ }
+
+ @Tool("Semantic search for transit policies regarding luggage, pets, refunds, and general rules.")
+ public String searchPolicies(String searchQuery) {
+ return mcpService.searchPolicies(searchQuery).join();
+ }
}
/**
- * 4. THE MCP TOOLBOX SERVICE
- * Handles the actual connection and execution against the AlloyDB backend.
+ * 4. THE MCP TOOLBOX SERVICE Handles the actual connection and execution against the AlloyDB
+ * backend.
*/
@Service
class McpToolboxService {
-
- private McpToolboxClient mcpClient;
- private String idToken;
-
- @Value("${MCP_TOOLBOX_URL:fallback_toolbox_url}")
- private String targetUrl;
-
- @PostConstruct
- public void init() {
- try {
- String tokenAudience = targetUrl;
-
- System.out.println("--- Initializing MCP Toolbox Client ---");
-
- GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
- if (!(credentials instanceof IdTokenProvider)) {
- throw new RuntimeException("Loaded credentials do not support ID Tokens.");
- }
-
- this.idToken = ((IdTokenProvider) credentials)
- .idTokenWithAudience(tokenAudience, Collections.emptyList())
- .getTokenValue();
-
- this.mcpClient = McpToolboxClient.builder()
- .baseUrl(targetUrl)
- .apiKey(idToken)
- .build();
-
- mcpClient.listTools().thenAccept(tools -> {
- System.out.println("Successfully discovered " + tools.size() + " tools.");
- }).join();
- } catch (Exception e) {
- System.err.println("Failed to initialize MCP Toolbox Client:");
- e.printStackTrace();
- }
- }
+ private McpToolboxClient mcpClient;
+ private String idToken;
- public CompletableFuture findAllSchedules() {
- return mcpClient.invokeTool("find-bus-schedules", Collections.emptyMap()).thenApply(result -> {
- if (result.isError() || result.content() == null || result.content().isEmpty()) return "No schedules found.";
- //return result.content().get(0).text();
- //return result.text();
- return result.content().stream()
- .map(content -> content.text())
- .collect(Collectors.joining(", ", "[", "]"));
- });
- }
+ @Value("${MCP_TOOLBOX_URL:fallback_toolbox_url}")
+ private String targetUrl;
- public CompletableFuture querySchedules(String origin, String destination) {
- java.util.Map params = new java.util.HashMap<>();
- params.put("origin", origin);
- params.put("destination", destination);
- return mcpClient.invokeTool("query-schedules", params).thenApply(result -> {
- if (result.isError() || result.content() == null || result.content().isEmpty()) return "No specific schedules found.";
- System.out.println(result);
- return result.content().stream()
- .map(content -> content.text())
- .collect(Collectors.joining(", ", "[", "]"));
- });
- }
+ @PostConstruct
+ public void init() {
+ try {
+ String tokenAudience = targetUrl;
+
+ System.out.println("--- Initializing MCP Toolbox Client ---");
+
+ GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
+ if (!(credentials instanceof IdTokenProvider)) {
+ throw new RuntimeException("Loaded credentials do not support ID Tokens.");
+ }
+
+ this.idToken =
+ ((IdTokenProvider) credentials)
+ .idTokenWithAudience(tokenAudience, Collections.emptyList())
+ .getTokenValue();
- public CompletableFuture bookTicket(String tripId, String passengerName) {
- AuthTokenGetter toolAuthGetter = () -> CompletableFuture.completedFuture(idToken);
- return mcpClient.loadTool("book-ticket", Collections.singletonMap("google_auth", toolAuthGetter))
- .thenCompose(tool -> {
- tool.bindParam("passenger_name", passengerName);
- return tool.execute(Collections.singletonMap("trip_id", tripId));
+ this.mcpClient = McpToolboxClient.builder().baseUrl(targetUrl).apiKey(idToken).build();
+
+ mcpClient
+ .listTools()
+ .thenAccept(
+ tools -> {
+ System.out.println("Successfully discovered " + tools.size() + " tools.");
+ })
+ .join();
+
+ } catch (Exception e) {
+ System.err.println("Failed to initialize MCP Toolbox Client:");
+ e.printStackTrace();
+ }
+ }
+
+ public CompletableFuture findAllSchedules() {
+ return mcpClient
+ .invokeTool("find-bus-schedules", Collections.emptyMap())
+ .thenApply(
+ result -> {
+ if (result.isError() || result.content() == null || result.content().isEmpty())
+ return "No schedules found.";
+ // return result.content().get(0).text();
+ // return result.text();
+ return result.content().stream()
+ .map(content -> content.text())
+ .collect(Collectors.joining(", ", "[", "]"));
+ });
+ }
+
+ public CompletableFuture querySchedules(String origin, String destination) {
+ java.util.Map params = new java.util.HashMap<>();
+ params.put("origin", origin);
+ params.put("destination", destination);
+ return mcpClient
+ .invokeTool("query-schedules", params)
+ .thenApply(
+ result -> {
+ if (result.isError() || result.content() == null || result.content().isEmpty())
+ return "No specific schedules found.";
+ System.out.println(result);
+ return result.content().stream()
+ .map(content -> content.text())
+ .collect(Collectors.joining(", ", "[", "]"));
+ });
+ }
+
+ public CompletableFuture bookTicket(String tripId, String passengerName) {
+ AuthTokenGetter toolAuthGetter = () -> CompletableFuture.completedFuture(idToken);
+ return mcpClient
+ .loadTool("book-ticket", Collections.singletonMap("google_auth", toolAuthGetter))
+ .thenCompose(
+ tool -> {
+ tool.bindParam("passenger_name", passengerName);
+ return tool.execute(Collections.singletonMap("trip_id", tripId));
})
- .thenApply(result -> {
- if (result.isError() || result.content() == null || result.content().isEmpty()) {
- System.err.println("Tool execution failed: " + result.content().get(0).text());
- return "Transaction failed.";
- }
- return result.content().get(0).text();
+ .thenApply(
+ result -> {
+ if (result.isError() || result.content() == null || result.content().isEmpty()) {
+ System.err.println("Tool execution failed: " + result.content().get(0).text());
+ return "Transaction failed.";
+ }
+ return result.content().get(0).text();
});
- }
-
- public CompletableFuture searchPolicies(String searchQuery) {
- return mcpClient.invokeTool("search-policies", Map.of("search_query", searchQuery))
- .thenApply(result -> {
- if (result.isError() || result.content() == null || result.content().isEmpty()) return "No policy information found.";
- return result.content().stream()
- .map(content -> content.text())
- .collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ public CompletableFuture searchPolicies(String searchQuery) {
+ return mcpClient
+ .invokeTool("search-policies", Map.of("search_query", searchQuery))
+ .thenApply(
+ result -> {
+ if (result.isError() || result.content() == null || result.content().isEmpty())
+ return "No policy information found.";
+ return result.content().stream()
+ .map(content -> content.text())
+ .collect(Collectors.joining(", ", "[", "]"));
});
- }
+ }
}
/**
- * 5. THE REST CONTROLLER
- * Now radically simplified! No more manual if/else logic or JSON parsing.
+ * 5. THE REST CONTROLLER Now radically simplified! No more manual if/else logic or JSON parsing.
*/
@RestController
@RequestMapping("/api/agent")
class TransitAgentController {
- private final TransitAgent transitAgent;
+ private final TransitAgent transitAgent;
- public TransitAgentController(TransitAgent transitAgent) {
- this.transitAgent = transitAgent;
- }
+ public TransitAgentController(TransitAgent transitAgent) {
+ this.transitAgent = transitAgent;
+ }
- @PostMapping("/chat")
- public ResponseEntity handleUserChat(@RequestBody String userMessage, HttpSession session) {
- // We use the HTTP Session ID to tell LangChain4j which memory context to load
- String sessionId = session.getId();
-
- // Let Gemini 3 Flash handle the thinking, tool execution, and response generation!
- String agentResponse = transitAgent.chat(sessionId, userMessage);
-
- return ResponseEntity.ok(agentResponse);
- }
+ @PostMapping("/chat")
+ public ResponseEntity handleUserChat(
+ @RequestBody String userMessage, HttpSession session) {
+ // We use the HTTP Session ID to tell LangChain4j which memory context to load
+ String sessionId = session.getId();
+
+ // Let Gemini 3 Flash handle the thinking, tool execution, and response generation!
+ String agentResponse = transitAgent.chat(sessionId, userMessage);
+
+ return ResponseEntity.ok(agentResponse);
+ }
}
diff --git a/example/pom.xml b/example/pom.xml
index 53ca764..551141e 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -33,7 +33,7 @@
com.google.cloud.mcp
mcp-toolbox-sdk-java
- 0.2.0
+ 0.2.1-SNAPSHOT
diff --git a/example/src/main/java/cloudcode/helloworld/ExampleUsage.java b/example/src/main/java/cloudcode/helloworld/ExampleUsage.java
index 504886c..70e18e5 100644
--- a/example/src/main/java/cloudcode/helloworld/ExampleUsage.java
+++ b/example/src/main/java/cloudcode/helloworld/ExampleUsage.java
@@ -16,145 +16,166 @@
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;
+import com.google.cloud.mcp.McpToolboxClient;
+import com.google.cloud.mcp.auth.AuthTokenGetter;
+import java.io.FileInputStream;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
/**
- * Sample Application to demostrate the usage of the MCP Toolbox Java SDK.
- * Covers: Global Auth, Parameterized Auth, Discovery, Simple Tool, Authenticated Tool, Parameter Binding.
+ * 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;
+ 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"));
+ })
+ .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 ---");
+ })
+ .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
index b171ae7..4971e25 100644
--- a/example/src/main/java/cloudcode/helloworld/InputValidationTest.java
+++ b/example/src/main/java/cloudcode/helloworld/InputValidationTest.java
@@ -16,100 +16,101 @@
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 com.google.cloud.mcp.McpToolboxClient;
+import com.google.cloud.mcp.tool.Tool;
import java.io.FileInputStream;
import java.util.Collections;
-import java.util.Map;
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.
- // --------------------------------------------------------------------------------
+ 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";
+ String keyPath = "/YOUR_CREDENTIALS_JSON_FILE_PATH.json";
- System.out.println("--- Starting MCP Toolbox Input Validation Test ---");
+ 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();
+ 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();
+ // 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();
+ // 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));
+ // 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 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 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
- // --- 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());
- }
+ // 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 ---");
+ } 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/helloworld/StrictFlagTest.java
index ba32ff9..df01a8c 100644
--- a/example/src/main/java/cloudcode/helloworld/StrictFlagTest.java
+++ b/example/src/main/java/cloudcode/helloworld/StrictFlagTest.java
@@ -17,18 +17,16 @@
package cloudcode.helloworld;
import com.google.cloud.mcp.McpToolboxClient;
-import com.google.cloud.mcp.Tool;
-import java.util.Map;
+import com.google.cloud.mcp.tool.Tool;
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/integration.cloudbuild.yaml b/integration.cloudbuild.yaml
index 200332a..fe6174f 100644
--- a/integration.cloudbuild.yaml
+++ b/integration.cloudbuild.yaml
@@ -16,11 +16,11 @@ steps:
- id: Install library requirements
name: 'maven:3.9.6-eclipse-temurin-17'
entrypoint: 'mvn'
- args: ['clean', 'install', '-DskipTests']
+ args: ['-B', '-ntp', 'clean', 'install', '-DskipTests']
- id: Run integration tests
name: 'maven:3.9.6-eclipse-temurin-17'
entrypoint: 'mvn'
- args: ['test']
+ args: ['-B', '-ntp', 'test']
env:
- GOOGLE_CLOUD_PROJECT=$PROJECT_ID
- TOOLBOX_VERSION=${_TOOLBOX_VERSION}
diff --git a/src/main/java/com/google/cloud/mcp/JsonRpc.java b/src/main/java/com/google/cloud/mcp/JsonRpc.java
index 902ac2a..54b4b98 100644
--- a/src/main/java/com/google/cloud/mcp/JsonRpc.java
+++ b/src/main/java/com/google/cloud/mcp/JsonRpc.java
@@ -19,47 +19,98 @@
import java.util.Map;
import java.util.UUID;
-class JsonRpc {
- static class Request {
+/** Namespace for JSON-RPC 2.0 MC Protocol data structures. */
+public class JsonRpc {
+ private JsonRpc() {}
+
+ /** Represents a JSON-RPC request. */
+ public static class Request {
+ /** The JSON-RPC version. */
public String jsonrpc = "2.0";
+
+ /** The request ID. */
public String id;
+
+ /** The method name. */
public String method;
+
+ /** The parameters. */
public Object params;
- public Request(String method, Object params) {
+ /**
+ * Constructs a new Request.
+ *
+ * @param method The method name.
+ * @param params The parameters.
+ */
+ public Request(final String method, final Object params) {
this.id = UUID.randomUUID().toString();
this.method = method;
this.params = params;
}
}
- static class Notification {
+ /** Represents a JSON-RPC notification. */
+ public static class Notification {
+ /** The JSON-RPC version. */
public String jsonrpc = "2.0";
+
+ /** The method name. */
public String method;
+
+ /** The parameters. */
public Object params;
- public Notification(String method, Object params) {
+ /**
+ * Constructs a new Notification.
+ *
+ * @param method The method name.
+ * @param params The parameters.
+ */
+ public Notification(final String method, final Object params) {
this.method = method;
this.params = params;
}
}
- static class CallToolParams {
+ /** Parameters for calling a tool. */
+ public static class CallToolParams {
+ /** The name of the tool to call. */
public String name;
+
+ /** The arguments for the tool call. */
public Map arguments;
- public CallToolParams(String name, Map arguments) {
+ /**
+ * Constructs a new CallToolParams.
+ *
+ * @param name The name of the tool.
+ * @param arguments The arguments.
+ */
+ public CallToolParams(final String name, final Map arguments) {
this.name = name;
this.arguments = arguments;
}
}
- static class InitializeParams {
+ /** Parameters for initializing the connection. */
+ public static class InitializeParams {
+ /** The protocol version. */
public String protocolVersion;
+
+ /** The client capabilities. */
public Map capabilities;
+
+ /** The client info. */
public Map clientInfo;
- public InitializeParams(String version, String clientName) {
+ /**
+ * Constructs a new InitializeParams.
+ *
+ * @param version The protocol version.
+ * @param clientName The client name.
+ */
+ public InitializeParams(final String version, final String clientName) {
this.protocolVersion = version;
this.capabilities = Map.of();
this.clientInfo = Map.of("name", clientName, "version", "1.0.0");
diff --git a/src/main/java/com/google/cloud/mcp/McpToolboxClient.java b/src/main/java/com/google/cloud/mcp/McpToolboxClient.java
index 472a6f3..30f6acc 100644
--- a/src/main/java/com/google/cloud/mcp/McpToolboxClient.java
+++ b/src/main/java/com/google/cloud/mcp/McpToolboxClient.java
@@ -16,6 +16,14 @@
package com.google.cloud.mcp;
+import com.google.cloud.mcp.auth.AuthTokenGetter;
+import com.google.cloud.mcp.auth.CredentialsProvider;
+import com.google.cloud.mcp.client.McpToolboxClientBuilder;
+import com.google.cloud.mcp.tool.Tool;
+import com.google.cloud.mcp.tool.ToolDefinition;
+import com.google.cloud.mcp.tool.ToolPostProcessor;
+import com.google.cloud.mcp.tool.ToolPreProcessor;
+import com.google.cloud.mcp.tool.ToolResult;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -167,6 +175,22 @@ interface Builder {
*/
Builder protocolVersion(ProtocolVersion protocolVersion);
+ /**
+ * Sets the connect timeout for the underlying HttpClient.
+ *
+ * @param connectTimeout The connect timeout.
+ * @return The builder instance.
+ */
+ Builder connectTimeout(java.time.Duration connectTimeout);
+
+ /**
+ * Sets the request timeout for every HTTP request.
+ *
+ * @param requestTimeout The request timeout.
+ * @return The builder instance.
+ */
+ Builder requestTimeout(java.time.Duration requestTimeout);
+
/**
* Sets a custom {@link java.net.http.HttpClient} for connection management.
*
@@ -183,11 +207,26 @@ interface Builder {
*/
Builder executor(java.util.concurrent.Executor executor);
+ /**
+ * Sets a custom Logger for telemetry and logs.
+ *
+ * @param logger The custom Logger.
+ * @return The builder instance.
+ */
+ Builder logger(java.util.logging.Logger logger);
+
/**
* Builds and returns a new {@link McpToolboxClient} instance.
*
* @return The new client instance.
*/
McpToolboxClient build();
+
+ /**
+ * Builds and returns a new {@link McpToolboxSyncClient} instance.
+ *
+ * @return The new synchronous client instance.
+ */
+ McpToolboxSyncClient buildSync();
}
}
diff --git a/src/main/java/com/google/cloud/mcp/McpToolboxSyncClient.java b/src/main/java/com/google/cloud/mcp/McpToolboxSyncClient.java
new file mode 100644
index 0000000..f50f1f1
--- /dev/null
+++ b/src/main/java/com/google/cloud/mcp/McpToolboxSyncClient.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import com.google.cloud.mcp.auth.AuthTokenGetter;
+import com.google.cloud.mcp.exception.McpToolboxException;
+import com.google.cloud.mcp.tool.Tool;
+import com.google.cloud.mcp.tool.ToolDefinition;
+import com.google.cloud.mcp.tool.ToolResult;
+import java.util.Map;
+
+/** The synchronous blocking client for interacting with MCP Toolbox. */
+public interface McpToolboxSyncClient {
+
+ /**
+ * Connects to the MCP Server and retrieves the list of all available tools.
+ *
+ * @return The map of Tool definitions (Key: Tool Name).
+ * @throws McpToolboxException if any error occurs.
+ */
+ Map listTools();
+
+ /**
+ * Loads the toolset from the MCP Server. Alias for {@link #listTools()}.
+ *
+ * @return The map of Tool definitions (Key: Tool Name).
+ * @throws McpToolboxException if any error occurs.
+ */
+ default Map loadToolset() {
+ return listTools();
+ }
+
+ /**
+ * Loads a specific toolset by name (if supported by server).
+ *
+ * @param toolsetName The name of the toolset to load.
+ * @return The map of Tool definitions (Key: Tool Name).
+ * @throws McpToolboxException if any error occurs.
+ */
+ Map loadToolset(String toolsetName);
+
+ /**
+ * Loads a toolset (or all tools if toolsetName is null) and applies bindings.
+ *
+ * @param toolsetName Name of the toolset to load (or null for all).
+ * @param paramBinds Map of Tool Name -> (Parameter Name -> Value).
+ * @param authBinds Map of Tool Name -> (Service -> Token Getter).
+ * @param strict Throws exception if bindings refer to non-existent tools.
+ * @return A Map of ready-to-use Tool objects.
+ * @throws McpToolboxException if any error occurs.
+ */
+ Map loadToolset(
+ String toolsetName,
+ Map> paramBinds,
+ Map> authBinds,
+ boolean strict);
+
+ /**
+ * Loads a specific tool definition and returns a smart Tool object.
+ *
+ * @param toolName The name of the tool to load.
+ * @return The Tool object.
+ * @throws McpToolboxException if any error occurs.
+ */
+ Tool loadTool(String toolName);
+
+ /**
+ * Loads a specific tool and registers authentication getters immediately.
+ *
+ * @param toolName The name of the tool.
+ * @param authTokenGetters A map of Service Name -> Token Getter Function.
+ * @return The Tool object.
+ * @throws McpToolboxException if any error occurs.
+ */
+ Tool loadTool(String toolName, Map authTokenGetters);
+
+ /**
+ * Low-level invocation method.
+ *
+ * @param toolName The name of the tool to invoke.
+ * @param arguments The arguments to pass to the tool.
+ * @return The result of the tool invocation.
+ * @throws McpToolboxException if any error occurs.
+ */
+ ToolResult invokeTool(String toolName, Map arguments);
+
+ /**
+ * Low-level invocation method with explicit headers.
+ *
+ * @param toolName The name of the tool to invoke.
+ * @param arguments The arguments to pass to the tool.
+ * @param extraHeaders Additional HTTP headers to include in the request.
+ * @return The result of the tool invocation.
+ * @throws McpToolboxException if any error occurs.
+ */
+ ToolResult invokeTool(
+ String toolName, Map arguments, Map extraHeaders);
+}
diff --git a/src/main/java/com/google/cloud/mcp/AuthMethods.java b/src/main/java/com/google/cloud/mcp/auth/AuthMethods.java
similarity index 98%
rename from src/main/java/com/google/cloud/mcp/AuthMethods.java
rename to src/main/java/com/google/cloud/mcp/auth/AuthMethods.java
index cb3815f..a287009 100644
--- a/src/main/java/com/google/cloud/mcp/AuthMethods.java
+++ b/src/main/java/com/google/cloud/mcp/auth/AuthMethods.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.auth;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenProvider;
diff --git a/src/main/java/com/google/cloud/mcp/AuthResolver.java b/src/main/java/com/google/cloud/mcp/auth/AuthResolver.java
similarity index 98%
rename from src/main/java/com/google/cloud/mcp/AuthResolver.java
rename to src/main/java/com/google/cloud/mcp/auth/AuthResolver.java
index 109c6ba..0fccadd 100644
--- a/src/main/java/com/google/cloud/mcp/AuthResolver.java
+++ b/src/main/java/com/google/cloud/mcp/auth/AuthResolver.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.auth;
import java.util.HashMap;
import java.util.List;
diff --git a/src/main/java/com/google/cloud/mcp/AuthTokenGetter.java b/src/main/java/com/google/cloud/mcp/auth/AuthTokenGetter.java
similarity index 96%
rename from src/main/java/com/google/cloud/mcp/AuthTokenGetter.java
rename to src/main/java/com/google/cloud/mcp/auth/AuthTokenGetter.java
index 5d3f736..6067352 100644
--- a/src/main/java/com/google/cloud/mcp/AuthTokenGetter.java
+++ b/src/main/java/com/google/cloud/mcp/auth/AuthTokenGetter.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.auth;
import java.util.concurrent.CompletableFuture;
diff --git a/src/main/java/com/google/cloud/mcp/CredentialsProvider.java b/src/main/java/com/google/cloud/mcp/auth/CredentialsProvider.java
similarity index 96%
rename from src/main/java/com/google/cloud/mcp/CredentialsProvider.java
rename to src/main/java/com/google/cloud/mcp/auth/CredentialsProvider.java
index eb428a0..a9c7ca4 100644
--- a/src/main/java/com/google/cloud/mcp/CredentialsProvider.java
+++ b/src/main/java/com/google/cloud/mcp/auth/CredentialsProvider.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.auth;
import java.util.concurrent.CompletableFuture;
diff --git a/src/main/java/com/google/cloud/mcp/GoogleCredentialsProvider.java b/src/main/java/com/google/cloud/mcp/auth/GoogleCredentialsProvider.java
similarity index 98%
rename from src/main/java/com/google/cloud/mcp/GoogleCredentialsProvider.java
rename to src/main/java/com/google/cloud/mcp/auth/GoogleCredentialsProvider.java
index 11a7232..bb1490e 100644
--- a/src/main/java/com/google/cloud/mcp/GoogleCredentialsProvider.java
+++ b/src/main/java/com/google/cloud/mcp/auth/GoogleCredentialsProvider.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.auth;
import com.google.auth.oauth2.GoogleCredentials;
import java.io.IOException;
diff --git a/src/main/java/com/google/cloud/mcp/ResolvedAuth.java b/src/main/java/com/google/cloud/mcp/auth/ResolvedAuth.java
similarity index 97%
rename from src/main/java/com/google/cloud/mcp/ResolvedAuth.java
rename to src/main/java/com/google/cloud/mcp/auth/ResolvedAuth.java
index 017d7e7..e79ecfb 100644
--- a/src/main/java/com/google/cloud/mcp/ResolvedAuth.java
+++ b/src/main/java/com/google/cloud/mcp/auth/ResolvedAuth.java
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.auth;
+import com.google.cloud.mcp.tool.ToolDefinition;
import java.util.Map;
/** Represents a resolved set of authentication credentials for a tool execution. */
diff --git a/src/main/java/com/google/cloud/mcp/client/HttpMcpToolboxSyncClient.java b/src/main/java/com/google/cloud/mcp/client/HttpMcpToolboxSyncClient.java
new file mode 100644
index 0000000..49c95e7
--- /dev/null
+++ b/src/main/java/com/google/cloud/mcp/client/HttpMcpToolboxSyncClient.java
@@ -0,0 +1,133 @@
+/*
+ * 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.client;
+
+import com.google.cloud.mcp.McpToolboxClient;
+import com.google.cloud.mcp.McpToolboxSyncClient;
+import com.google.cloud.mcp.auth.AuthTokenGetter;
+import com.google.cloud.mcp.exception.McpToolboxException;
+import com.google.cloud.mcp.tool.Tool;
+import com.google.cloud.mcp.tool.ToolDefinition;
+import com.google.cloud.mcp.tool.ToolResult;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletionException;
+
+/**
+ * Synchronous client wrapper that delegates to an async {@link McpToolboxClient} and blocks the
+ * calling thread, unwrapping exceptions.
+ */
+public final class HttpMcpToolboxSyncClient implements McpToolboxSyncClient {
+ /** The underlying asynchronous client to delegate to. */
+ private final McpToolboxClient delegate;
+
+ /**
+ * Constructs a new HttpMcpToolboxSyncClient wrapping the given async client.
+ *
+ * @param client The async client to delegate to.
+ */
+ public HttpMcpToolboxSyncClient(final McpToolboxClient client) {
+ if (client == null) {
+ throw new IllegalArgumentException("Delegate client cannot be null");
+ }
+ this.delegate = client;
+ }
+
+ @Override
+ public Map listTools() {
+ try {
+ return delegate.listTools().join();
+ } catch (CompletionException | CancellationException e) {
+ throw unwrapException(e);
+ }
+ }
+
+ @Override
+ public Map loadToolset(final String toolsetName) {
+ try {
+ return delegate.loadToolset(toolsetName).join();
+ } catch (CompletionException | CancellationException e) {
+ throw unwrapException(e);
+ }
+ }
+
+ @Override
+ public Map loadToolset(
+ final String toolsetName,
+ final Map> paramBinds,
+ final Map> authBinds,
+ final boolean strict) {
+ try {
+ return delegate.loadToolset(toolsetName, paramBinds, authBinds, strict).join();
+ } catch (CompletionException | CancellationException e) {
+ throw unwrapException(e);
+ }
+ }
+
+ @Override
+ public Tool loadTool(final String toolName) {
+ try {
+ return delegate.loadTool(toolName).join();
+ } catch (CompletionException | CancellationException e) {
+ throw unwrapException(e);
+ }
+ }
+
+ @Override
+ public Tool loadTool(final String toolName, final Map authTokenGetters) {
+ try {
+ return delegate.loadTool(toolName, authTokenGetters).join();
+ } catch (CompletionException | CancellationException e) {
+ throw unwrapException(e);
+ }
+ }
+
+ @Override
+ public ToolResult invokeTool(final String toolName, final Map arguments) {
+ try {
+ return delegate.invokeTool(toolName, arguments).join();
+ } catch (CompletionException | CancellationException e) {
+ throw unwrapException(e);
+ }
+ }
+
+ @Override
+ public ToolResult invokeTool(
+ final String toolName,
+ final Map arguments,
+ final Map extraHeaders) {
+ try {
+ return delegate.invokeTool(toolName, arguments, extraHeaders).join();
+ } catch (CompletionException | CancellationException e) {
+ throw unwrapException(e);
+ }
+ }
+
+ private RuntimeException unwrapException(final Throwable e) {
+ final Throwable cause = e.getCause();
+ if (cause == null) {
+ return new McpToolboxException(e);
+ }
+ if (cause instanceof McpToolboxException) {
+ return (McpToolboxException) cause;
+ }
+ if (cause instanceof IllegalArgumentException) {
+ return (IllegalArgumentException) cause;
+ }
+ return new McpToolboxException(cause.getMessage(), cause);
+ }
+}
diff --git a/src/main/java/com/google/cloud/mcp/McpToolboxClientBuilder.java b/src/main/java/com/google/cloud/mcp/client/McpToolboxClientBuilder.java
similarity index 76%
rename from src/main/java/com/google/cloud/mcp/McpToolboxClientBuilder.java
rename to src/main/java/com/google/cloud/mcp/client/McpToolboxClientBuilder.java
index 7c2f765..a9ebaec 100644
--- a/src/main/java/com/google/cloud/mcp/McpToolboxClientBuilder.java
+++ b/src/main/java/com/google/cloud/mcp/client/McpToolboxClientBuilder.java
@@ -14,8 +14,16 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
-
+package com.google.cloud.mcp.client;
+
+import com.google.cloud.mcp.McpToolboxClient;
+import com.google.cloud.mcp.McpToolboxSyncClient;
+import com.google.cloud.mcp.ProtocolVersion;
+import com.google.cloud.mcp.auth.CredentialsProvider;
+import com.google.cloud.mcp.tool.ToolPostProcessor;
+import com.google.cloud.mcp.tool.ToolPreProcessor;
+import com.google.cloud.mcp.transport.HttpMcpTransport;
+import com.google.cloud.mcp.transport.Transport;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -31,8 +39,11 @@ public final class McpToolboxClientBuilder implements McpToolboxClient.Builder {
private final List preProcessors = new ArrayList<>();
private final List postProcessors = new ArrayList<>();
private ProtocolVersion protocolVersion;
+ private java.time.Duration connectTimeout;
+ private java.time.Duration requestTimeout;
private java.net.http.HttpClient httpClient;
private java.util.concurrent.Executor executor;
+ private java.util.logging.Logger logger;
/** Constructs a new McpToolboxClientBuilder. */
public McpToolboxClientBuilder() {}
@@ -85,6 +96,18 @@ public McpToolboxClient.Builder protocolVersion(ProtocolVersion protocolVersion)
return this;
}
+ @Override
+ public McpToolboxClient.Builder connectTimeout(java.time.Duration connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ @Override
+ public McpToolboxClient.Builder requestTimeout(java.time.Duration requestTimeout) {
+ this.requestTimeout = requestTimeout;
+ return this;
+ }
+
@Override
public McpToolboxClient.Builder httpClient(java.net.http.HttpClient httpClient) {
this.httpClient = httpClient;
@@ -97,6 +120,12 @@ public McpToolboxClient.Builder executor(java.util.concurrent.Executor executor)
return this;
}
+ @Override
+ public McpToolboxClient.Builder logger(java.util.logging.Logger logger) {
+ this.logger = logger;
+ return this;
+ }
+
@Override
public McpToolboxClient build() {
if (baseUrl == null || baseUrl.isEmpty()) {
@@ -130,8 +159,16 @@ public McpToolboxClient build() {
resolvedProvider,
this.protocolVersion,
this.httpClient,
- this.executor);
+ this.executor,
+ this.connectTimeout,
+ this.requestTimeout,
+ this.logger);
return new McpToolboxClientImpl(
transport, this.headers, resolvedProvider, preProcessors, postProcessors);
}
+
+ @Override
+ public McpToolboxSyncClient buildSync() {
+ return new HttpMcpToolboxSyncClient(build());
+ }
}
diff --git a/src/main/java/com/google/cloud/mcp/McpToolboxClientImpl.java b/src/main/java/com/google/cloud/mcp/client/McpToolboxClientImpl.java
similarity index 95%
rename from src/main/java/com/google/cloud/mcp/McpToolboxClientImpl.java
rename to src/main/java/com/google/cloud/mcp/client/McpToolboxClientImpl.java
index c5d254b..d8ebdd2 100644
--- a/src/main/java/com/google/cloud/mcp/McpToolboxClientImpl.java
+++ b/src/main/java/com/google/cloud/mcp/client/McpToolboxClientImpl.java
@@ -14,10 +14,22 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.client;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.cloud.mcp.McpToolboxClient;
+import com.google.cloud.mcp.auth.AuthTokenGetter;
+import com.google.cloud.mcp.auth.CredentialsProvider;
+import com.google.cloud.mcp.tool.Tool;
+import com.google.cloud.mcp.tool.ToolDefinition;
+import com.google.cloud.mcp.tool.ToolPostProcessor;
+import com.google.cloud.mcp.tool.ToolPreProcessor;
+import com.google.cloud.mcp.tool.ToolResult;
+import com.google.cloud.mcp.transport.HttpMcpTransport;
+import com.google.cloud.mcp.transport.Transport;
+import com.google.cloud.mcp.transport.TransportManifest;
+import com.google.cloud.mcp.transport.TransportResponse;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
diff --git a/src/main/java/com/google/cloud/mcp/McpException.java b/src/main/java/com/google/cloud/mcp/exception/McpException.java
similarity index 96%
rename from src/main/java/com/google/cloud/mcp/McpException.java
rename to src/main/java/com/google/cloud/mcp/exception/McpException.java
index c057016..9016674 100644
--- a/src/main/java/com/google/cloud/mcp/McpException.java
+++ b/src/main/java/com/google/cloud/mcp/exception/McpException.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.exception;
/** Unchecked exception thrown for MCP Toolbox Client operations and protocol failures. */
public class McpException extends RuntimeException {
diff --git a/src/main/java/com/google/cloud/mcp/exception/McpProtocolException.java b/src/main/java/com/google/cloud/mcp/exception/McpProtocolException.java
new file mode 100644
index 0000000..c944ecf
--- /dev/null
+++ b/src/main/java/com/google/cloud/mcp/exception/McpProtocolException.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 com.google.cloud.mcp.exception;
+
+/** Exception thrown when JSON-RPC or protocol level errors occur. */
+public class McpProtocolException extends McpToolboxException {
+
+ /**
+ * Constructs a new McpProtocolException with the specified detail message.
+ *
+ * @param message The detail message.
+ */
+ public McpProtocolException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new McpProtocolException with the specified message and cause.
+ *
+ * @param message The detail message.
+ * @param cause The cause of the exception.
+ */
+ public McpProtocolException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/google/cloud/mcp/exception/McpToolboxException.java b/src/main/java/com/google/cloud/mcp/exception/McpToolboxException.java
new file mode 100644
index 0000000..5d9242e
--- /dev/null
+++ b/src/main/java/com/google/cloud/mcp/exception/McpToolboxException.java
@@ -0,0 +1,49 @@
+/*
+ * 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.exception;
+
+/** Base exception class for all MCP Toolbox SDK errors. */
+public class McpToolboxException extends RuntimeException {
+
+ /**
+ * Constructs a new McpToolboxException with the specified detail message.
+ *
+ * @param message The detail message.
+ */
+ public McpToolboxException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new McpToolboxException with the specified message and cause.
+ *
+ * @param message The detail message.
+ * @param cause The cause of the exception.
+ */
+ public McpToolboxException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new McpToolboxException with the specified cause.
+ *
+ * @param cause The cause of the exception.
+ */
+ public McpToolboxException(final Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/google/cloud/mcp/exception/McpTransportException.java b/src/main/java/com/google/cloud/mcp/exception/McpTransportException.java
new file mode 100644
index 0000000..16a0270
--- /dev/null
+++ b/src/main/java/com/google/cloud/mcp/exception/McpTransportException.java
@@ -0,0 +1,76 @@
+/*
+ * 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.exception;
+
+/** Exception thrown when communication or transport level errors occur. */
+public class McpTransportException extends McpToolboxException {
+ /** The HTTP status code associated with this error, or -1 if not applicable. */
+ private final int statusCode;
+
+ /**
+ * Constructs a new McpTransportException with the specified detail message.
+ *
+ * @param message The detail message.
+ */
+ public McpTransportException(final String message) {
+ super(message);
+ this.statusCode = -1;
+ }
+
+ /**
+ * Constructs a new McpTransportException with message and status code.
+ *
+ * @param message The detail message.
+ * @param statusCode The HTTP status code.
+ */
+ public McpTransportException(final String message, final int statusCode) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+
+ /**
+ * Constructs a new McpTransportException with message and cause.
+ *
+ * @param message The detail message.
+ * @param cause The cause of the exception.
+ */
+ public McpTransportException(final String message, final Throwable cause) {
+ super(message, cause);
+ this.statusCode = -1;
+ }
+
+ /**
+ * Constructs a new McpTransportException with message, status code, and cause.
+ *
+ * @param message The detail message.
+ * @param statusCode The HTTP status code.
+ * @param cause The cause of the exception.
+ */
+ public McpTransportException(final String message, final int statusCode, final Throwable cause) {
+ super(message, cause);
+ this.statusCode = statusCode;
+ }
+
+ /**
+ * Returns the HTTP status code associated with this error, or -1 if not applicable.
+ *
+ * @return The HTTP status code.
+ */
+ public int getStatusCode() {
+ return statusCode;
+ }
+}
diff --git a/src/main/java/com/google/cloud/mcp/exception/ToolExecutionException.java b/src/main/java/com/google/cloud/mcp/exception/ToolExecutionException.java
new file mode 100644
index 0000000..e8525f2
--- /dev/null
+++ b/src/main/java/com/google/cloud/mcp/exception/ToolExecutionException.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 com.google.cloud.mcp.exception;
+
+/** Exception thrown when tool lookup, validation, or invocation fails. */
+public class ToolExecutionException extends McpToolboxException {
+
+ /**
+ * Constructs a new ToolExecutionException with the specified detail message.
+ *
+ * @param message The detail message.
+ */
+ public ToolExecutionException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new ToolExecutionException with the specified message/cause.
+ *
+ * @param message The detail message.
+ * @param cause The cause of the exception.
+ */
+ public ToolExecutionException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/google/cloud/mcp/Tool.java b/src/main/java/com/google/cloud/mcp/tool/Tool.java
similarity index 80%
rename from src/main/java/com/google/cloud/mcp/Tool.java
rename to src/main/java/com/google/cloud/mcp/tool/Tool.java
index 49cfe59..2230eea 100644
--- a/src/main/java/com/google/cloud/mcp/Tool.java
+++ b/src/main/java/com/google/cloud/mcp/tool/Tool.java
@@ -14,8 +14,12 @@
* limitations under the License.
*/
-package com.google.cloud.mcp;
+package com.google.cloud.mcp.tool;
+import com.google.cloud.mcp.McpToolboxClient;
+import com.google.cloud.mcp.auth.AuthResolver;
+import com.google.cloud.mcp.auth.AuthTokenGetter;
+import com.google.cloud.mcp.exception.McpToolboxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -29,13 +33,25 @@
* resolution, and input validation.
*/
public class Tool {
+ /** The name of the tool. */
private final String name;
+
+ /** The definition of the tool. */
private final ToolDefinition definition;
+
+ /** The client used to invoke the tool. */
private final McpToolboxClient client;
+ /** The bound parameters. */
private final Map boundParameters = new HashMap<>();
+
+ /** The auth token getters. */
private final Map authGetters = new HashMap<>();
+
+ /** The pre-processors. */
private final List preProcessors = new ArrayList<>();
+
+ /** The post-processors. */
private final List postProcessors = new ArrayList<>();
/**
@@ -45,7 +61,7 @@ public class Tool {
* @param definition The definition of the tool.
* @param client The client used to invoke the tool.
*/
- public Tool(String name, ToolDefinition definition, McpToolboxClient client) {
+ public Tool(final String name, final ToolDefinition definition, final McpToolboxClient client) {
this.name = name;
this.definition = definition;
this.client = client;
@@ -127,6 +143,32 @@ public Tool addPostProcessor(ToolPostProcessor processor) {
return this;
}
+ /**
+ * Synchronously executes the tool with the provided arguments.
+ *
+ * @param args The arguments for the tool invocation.
+ * @return The result of the tool execution.
+ * @throws McpToolboxException if execution fails.
+ */
+ public ToolResult executeSync(final Map args) {
+ try {
+ return execute(args).join();
+ } catch (java.util.concurrent.CompletionException
+ | java.util.concurrent.CancellationException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof McpToolboxException) {
+ throw (McpToolboxException) cause;
+ }
+ if (cause instanceof IllegalArgumentException) {
+ throw (IllegalArgumentException) cause;
+ }
+ if (cause != null) {
+ throw new McpToolboxException(cause.getMessage(), cause);
+ }
+ throw new McpToolboxException(e);
+ }
+ }
+
/**
* Executes the tool with the provided arguments, applying any bound parameters and resolving
* authentication tokens.
@@ -134,7 +176,7 @@ public Tool addPostProcessor(ToolPostProcessor processor) {
* @param args The arguments for the tool invocation.
* @return A CompletableFuture containing the result of the tool execution.
*/
- public CompletableFuture execute(Map args) {
+ public CompletableFuture execute(final Map args) {
CompletableFuture