From 714a0cf080f095d72a74ecebb59161e756cfc6ce Mon Sep 17 00:00:00 2001 From: Stenal P Jolly Date: Thu, 25 Jun 2026 16:52:14 +0530 Subject: [PATCH 1/2] refactor: Restructure SDK into subpackages and implement versioned HTTP transport layers --- demo-applications/cymbal-transit/pom.xml | 2 +- .../cymbal/CymbalTransitApplication.java | 4 +- .../cymbal/web/CymbalTransitController.java | 410 +++++++++--------- example/pom.xml | 2 +- .../cloudcode/helloworld/ExampleUsage.java | 285 ++++++------ .../helloworld/InputValidationTest.java | 151 +++---- .../cloudcode/helloworld/StrictFlagTest.java | 13 +- .../java/com/google/cloud/mcp/JsonRpc.java | 18 +- .../google/cloud/mcp/McpToolboxClient.java | 8 + .../cloud/mcp/{ => auth}/AuthMethods.java | 2 +- .../cloud/mcp/{ => auth}/AuthResolver.java | 2 +- .../cloud/mcp/{ => auth}/AuthTokenGetter.java | 2 +- .../mcp/{ => auth}/CredentialsProvider.java | 2 +- .../{ => auth}/GoogleCredentialsProvider.java | 2 +- .../cloud/mcp/{ => auth}/ResolvedAuth.java | 3 +- .../{ => client}/McpToolboxClientBuilder.java | 11 +- .../{ => client}/McpToolboxClientImpl.java | 14 +- .../mcp/{ => exception}/McpException.java | 2 +- .../com/google/cloud/mcp/{ => tool}/Tool.java | 5 +- .../cloud/mcp/{ => tool}/ToolDefinition.java | 2 +- .../mcp/{ => tool}/ToolPostProcessor.java | 2 +- .../mcp/{ => tool}/ToolPreProcessor.java | 2 +- .../cloud/mcp/{ => tool}/ToolResult.java | 2 +- .../BaseMcpTransport.java} | 254 ++--------- .../cloud/mcp/transport/HttpMcpTransport.java | 175 ++++++++ .../cloud/mcp/{ => transport}/Transport.java | 2 +- .../{ => transport}/TransportManifest.java | 3 +- .../{ => transport}/TransportResponse.java | 2 +- .../v20241105/HttpMcpTransportV20241105.java | 130 ++++++ .../v20250326/HttpMcpTransportV20250326.java | 146 +++++++ .../v20250618/HttpMcpTransportV20250618.java | 132 ++++++ .../v20251125/HttpMcpTransportV20251125.java | 132 ++++++ .../cloud/mcp/{ => auth}/AuthMethodsTest.java | 2 +- .../HttpMcpToolboxClientTest.java | 4 +- .../McpToolboxClientBuilderTest.java | 9 +- .../McpToolboxClientImplErrorsTest.java | 6 +- .../McpToolboxClientImplHeadersTest.java | 27 +- .../McpToolboxClientImplJsonRpcTest.java | 7 +- .../McpToolboxClientImplTest.java | 50 ++- .../cloud/mcp/e2e/McpToolboxClientTest.java | 6 +- .../google/cloud/mcp/{ => tool}/ToolTest.java | 4 +- .../mcp/{ => tool}/ToolValidationTest.java | 3 +- .../{ => transport}/HttpMcpTransportTest.java | 16 +- 43 files changed, 1371 insertions(+), 685 deletions(-) rename src/main/java/com/google/cloud/mcp/{ => auth}/AuthMethods.java (98%) rename src/main/java/com/google/cloud/mcp/{ => auth}/AuthResolver.java (98%) rename src/main/java/com/google/cloud/mcp/{ => auth}/AuthTokenGetter.java (96%) rename src/main/java/com/google/cloud/mcp/{ => auth}/CredentialsProvider.java (96%) rename src/main/java/com/google/cloud/mcp/{ => auth}/GoogleCredentialsProvider.java (98%) rename src/main/java/com/google/cloud/mcp/{ => auth}/ResolvedAuth.java (97%) rename src/main/java/com/google/cloud/mcp/{ => client}/McpToolboxClientBuilder.java (91%) rename src/main/java/com/google/cloud/mcp/{ => client}/McpToolboxClientImpl.java (95%) rename src/main/java/com/google/cloud/mcp/{ => exception}/McpException.java (96%) rename src/main/java/com/google/cloud/mcp/{ => tool}/Tool.java (97%) rename src/main/java/com/google/cloud/mcp/{ => tool}/ToolDefinition.java (98%) rename src/main/java/com/google/cloud/mcp/{ => tool}/ToolPostProcessor.java (96%) rename src/main/java/com/google/cloud/mcp/{ => tool}/ToolPreProcessor.java (97%) rename src/main/java/com/google/cloud/mcp/{ => tool}/ToolResult.java (97%) rename src/main/java/com/google/cloud/mcp/{HttpMcpTransport.java => transport/BaseMcpTransport.java} (56%) create mode 100644 src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java rename src/main/java/com/google/cloud/mcp/{ => transport}/Transport.java (97%) rename src/main/java/com/google/cloud/mcp/{ => transport}/TransportManifest.java (92%) rename src/main/java/com/google/cloud/mcp/{ => transport}/TransportResponse.java (97%) create mode 100644 src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java create mode 100644 src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java create mode 100644 src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java create mode 100644 src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java rename src/test/java/com/google/cloud/mcp/{ => auth}/AuthMethodsTest.java (99%) rename src/test/java/com/google/cloud/mcp/{ => client}/HttpMcpToolboxClientTest.java (98%) rename src/test/java/com/google/cloud/mcp/{ => client}/McpToolboxClientBuilderTest.java (94%) rename src/test/java/com/google/cloud/mcp/{ => client}/McpToolboxClientImplErrorsTest.java (97%) rename src/test/java/com/google/cloud/mcp/{ => client}/McpToolboxClientImplHeadersTest.java (92%) rename src/test/java/com/google/cloud/mcp/{ => client}/McpToolboxClientImplJsonRpcTest.java (98%) rename src/test/java/com/google/cloud/mcp/{ => client}/McpToolboxClientImplTest.java (95%) rename src/test/java/com/google/cloud/mcp/{ => tool}/ToolTest.java (99%) rename src/test/java/com/google/cloud/mcp/{ => tool}/ToolValidationTest.java (99%) rename src/test/java/com/google/cloud/mcp/{ => transport}/HttpMcpTransportTest.java (97%) 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/src/main/java/com/google/cloud/mcp/JsonRpc.java b/src/main/java/com/google/cloud/mcp/JsonRpc.java index 902ac2a..7bc7501 100644 --- a/src/main/java/com/google/cloud/mcp/JsonRpc.java +++ b/src/main/java/com/google/cloud/mcp/JsonRpc.java @@ -19,47 +19,47 @@ import java.util.Map; import java.util.UUID; -class JsonRpc { - static class Request { +public class JsonRpc { + public static class Request { public String jsonrpc = "2.0"; public String id; public String method; public Object params; - public Request(String method, Object params) { + public Request(final String method, final Object params) { this.id = UUID.randomUUID().toString(); this.method = method; this.params = params; } } - static class Notification { + public static class Notification { public String jsonrpc = "2.0"; public String method; public Object params; - public Notification(String method, Object params) { + public Notification(final String method, final Object params) { this.method = method; this.params = params; } } - static class CallToolParams { + public static class CallToolParams { public String name; public Map arguments; - public CallToolParams(String name, Map arguments) { + public CallToolParams(final String name, final Map arguments) { this.name = name; this.arguments = arguments; } } - static class InitializeParams { + public static class InitializeParams { public String protocolVersion; public Map capabilities; public Map clientInfo; - public InitializeParams(String version, String clientName) { + 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..ff79e04 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; 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/McpToolboxClientBuilder.java b/src/main/java/com/google/cloud/mcp/client/McpToolboxClientBuilder.java similarity index 91% 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..0b66c13 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,15 @@ * 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.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; 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/Tool.java b/src/main/java/com/google/cloud/mcp/tool/Tool.java similarity index 97% 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..4fb0229 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,11 @@ * 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 java.util.ArrayList; import java.util.HashMap; import java.util.List; diff --git a/src/main/java/com/google/cloud/mcp/ToolDefinition.java b/src/main/java/com/google/cloud/mcp/tool/ToolDefinition.java similarity index 98% rename from src/main/java/com/google/cloud/mcp/ToolDefinition.java rename to src/main/java/com/google/cloud/mcp/tool/ToolDefinition.java index ac8e60b..bef492f 100644 --- a/src/main/java/com/google/cloud/mcp/ToolDefinition.java +++ b/src/main/java/com/google/cloud/mcp/tool/ToolDefinition.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.tool; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/google/cloud/mcp/ToolPostProcessor.java b/src/main/java/com/google/cloud/mcp/tool/ToolPostProcessor.java similarity index 96% rename from src/main/java/com/google/cloud/mcp/ToolPostProcessor.java rename to src/main/java/com/google/cloud/mcp/tool/ToolPostProcessor.java index 09000bb..61280ea 100644 --- a/src/main/java/com/google/cloud/mcp/ToolPostProcessor.java +++ b/src/main/java/com/google/cloud/mcp/tool/ToolPostProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.tool; import java.util.concurrent.CompletableFuture; diff --git a/src/main/java/com/google/cloud/mcp/ToolPreProcessor.java b/src/main/java/com/google/cloud/mcp/tool/ToolPreProcessor.java similarity index 97% rename from src/main/java/com/google/cloud/mcp/ToolPreProcessor.java rename to src/main/java/com/google/cloud/mcp/tool/ToolPreProcessor.java index a280a85..190e38d 100644 --- a/src/main/java/com/google/cloud/mcp/ToolPreProcessor.java +++ b/src/main/java/com/google/cloud/mcp/tool/ToolPreProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.tool; import java.util.Map; import java.util.concurrent.CompletableFuture; diff --git a/src/main/java/com/google/cloud/mcp/ToolResult.java b/src/main/java/com/google/cloud/mcp/tool/ToolResult.java similarity index 97% rename from src/main/java/com/google/cloud/mcp/ToolResult.java rename to src/main/java/com/google/cloud/mcp/tool/ToolResult.java index d447adc..29b6a61 100644 --- a/src/main/java/com/google/cloud/mcp/ToolResult.java +++ b/src/main/java/com/google/cloud/mcp/tool/ToolResult.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.tool; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/google/cloud/mcp/HttpMcpTransport.java b/src/main/java/com/google/cloud/mcp/transport/BaseMcpTransport.java similarity index 56% rename from src/main/java/com/google/cloud/mcp/HttpMcpTransport.java rename to src/main/java/com/google/cloud/mcp/transport/BaseMcpTransport.java index 6cfcc86..6a60297 100644 --- a/src/main/java/com/google/cloud/mcp/HttpMcpTransport.java +++ b/src/main/java/com/google/cloud/mcp/transport/BaseMcpTransport.java @@ -14,10 +14,14 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.transport; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.mcp.JsonRpc; +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.tool.ToolDefinition; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -33,91 +37,29 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; -/** Default HTTP transport implementation using Java 11 HttpClient. */ -public final class HttpMcpTransport implements Transport { +public abstract class BaseMcpTransport implements Transport { - private static final Logger logger = Logger.getLogger(HttpMcpTransport.class.getName()); - private static final String HTTP_WARNING = + protected static final Logger logger = Logger.getLogger(BaseMcpTransport.class.getName()); + protected static final String HTTP_WARNING = "This connection is using HTTP. To prevent credential exposure, please ensure all" + " communication is sent over HTTPS."; - private final String baseUrl; - private final Map clientHeaders; - private final CredentialsProvider credentialsProvider; - private final HttpClient httpClient; - private final ObjectMapper objectMapper; - private final ProtocolVersion preferredProtocolVersion; - private final Object initLock = new Object(); - private CompletableFuture initFuture; - private volatile ProtocolVersion negotiatedProtocolVersion; - private volatile String sessionId; - - /** - * Constructs a new HttpMcpTransport with a base URL. - * - * @param baseUrl The base URL of the remote service. - */ - public HttpMcpTransport(String baseUrl) { - this(baseUrl, Map.of(), (CredentialsProvider) null); - } - - /** - * Constructs a new HttpMcpTransport with base URL and default headers. - * - * @param baseUrl The base URL of the remote service. - * @param clientHeaders Default HTTP headers to include in every request. - */ - public HttpMcpTransport(String baseUrl, Map clientHeaders) { - this(baseUrl, clientHeaders, (CredentialsProvider) null); - } - - /** - * Constructs a new HttpMcpTransport with base URL, default headers and credentials provider. - * - * @param baseUrl The base URL of the remote service. - * @param clientHeaders Default HTTP headers to include in every request. - * @param credentialsProvider Provider for retrieving authorization credentials. - */ - public HttpMcpTransport( - String baseUrl, Map clientHeaders, CredentialsProvider credentialsProvider) { - this(baseUrl, clientHeaders, credentialsProvider, null, null, null); - } - - /** - * Constructs a HttpMcpTransport. - * - * @param baseUrl The base URL of the remote service. - * @param clientHeaders Default HTTP headers to include in every request. - * @param preferredProtocolVersion Preferred MCP protocol version. - * @param httpClient Custom HTTP Client. - * @param executor Optional Executor for handling async requests. - */ - public HttpMcpTransport( - String baseUrl, - Map clientHeaders, - ProtocolVersion preferredProtocolVersion, - HttpClient httpClient, - java.util.concurrent.Executor executor) { - this(baseUrl, clientHeaders, null, preferredProtocolVersion, httpClient, executor); - } - - /** - * Primary constructor for HttpMcpTransport. - * - * @param baseUrl The base URL of the remote service. - * @param clientHeaders Default HTTP headers to include in every request. - * @param credentialsProvider Provider for retrieving authorization credentials. - * @param preferredProtocolVersion Preferred MCP protocol version. - * @param httpClient Custom HTTP Client. - * @param executor Optional Executor for handling async requests. - */ - public HttpMcpTransport( - String baseUrl, - Map clientHeaders, - CredentialsProvider credentialsProvider, - ProtocolVersion preferredProtocolVersion, - HttpClient httpClient, - java.util.concurrent.Executor executor) { + protected final String baseUrl; + protected final Map clientHeaders; + protected final CredentialsProvider credentialsProvider; + protected final HttpClient httpClient; + protected final ObjectMapper objectMapper; + protected final ProtocolVersion preferredProtocolVersion; + protected final Object initLock = new Object(); + protected CompletableFuture initFuture; + + protected BaseMcpTransport( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final ProtocolVersion preferredProtocolVersion, + final HttpClient httpClient, + final java.util.concurrent.Executor executor) { if (baseUrl == null || baseUrl.isEmpty()) { throw new IllegalArgumentException("Base URL must be provided"); } @@ -146,28 +88,13 @@ public HttpMcpTransport( this.objectMapper = new ObjectMapper(); } - HttpMcpTransport(String baseUrl, HttpClient httpClient) { - this(baseUrl, Map.of(), null, null, httpClient, null); - } - - HttpMcpTransport(String baseUrl, Map clientHeaders, HttpClient httpClient) { - this(baseUrl, clientHeaders, null, null, httpClient, null); - } - - HttpMcpTransport( - String baseUrl, - Map clientHeaders, - CredentialsProvider credentialsProvider, - HttpClient httpClient) { - this(baseUrl, clientHeaders, credentialsProvider, null, httpClient, null); - } - @Override - public String getBaseUrl() { + public final String getBaseUrl() { return this.baseUrl; } - private CompletableFuture> mergeHeaders(Map extraMetadata) { + final CompletableFuture> mergeHeaders( + final Map extraMetadata) { CompletableFuture authFuture = this.credentialsProvider != null ? this.credentialsProvider.getAuthorizationHeader() @@ -234,7 +161,7 @@ private CompletableFuture> mergeHeaders(Map }); } - private CompletableFuture ensureInitialized(Map extraMetadata) { + final CompletableFuture ensureInitialized(final Map extraMetadata) { synchronized (initLock) { if (initFuture == null) { Map handshakeMetadata = new HashMap<>(); @@ -260,118 +187,14 @@ private CompletableFuture ensureInitialized(Map extraMetad } } - private CompletableFuture performInitialization( - String authHeader, Map handshakeHeaders) { - try { - if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") - && authHeader != null) { - logger.warning(HTTP_WARNING); - } - JsonRpc.Request initReq = - new JsonRpc.Request( - "initialize", - new JsonRpc.InitializeParams( - preferredProtocolVersion.getValue(), "mcp-toolbox-sdk-java")); - String body = objectMapper.writeValueAsString(initReq); - HttpRequest.Builder req = - HttpRequest.newBuilder() - .uri(URI.create(baseUrl)) - .POST(HttpRequest.BodyPublishers.ofString(body)); - - handshakeHeaders.forEach(req::setHeader); - applyProtocolHeaders(req); - - return httpClient - .sendAsync(req.build(), HttpResponse.BodyHandlers.ofString()) - .thenCompose( - res -> { - if (res.statusCode() != 200) { - return CompletableFuture.failedFuture( - new McpException("Init failed: " + res.statusCode() + " " + res.body())); - } - try { - JsonNode responseJson = objectMapper.readTree(res.body()); - if (responseJson.has("error")) { - return CompletableFuture.failedFuture( - new McpException("MCP Error: " + responseJson.get("error").toString())); - } - JsonNode result = responseJson.get("result"); - String serverVersion; - if (result != null && result.has("protocolVersion")) { - serverVersion = result.get("protocolVersion").asText(); - } else { - // Fallback to the client's preferred version for backward-compatible/mock - // servers - serverVersion = preferredProtocolVersion.getValue(); - } + protected abstract CompletableFuture performInitialization( + final String authHeader, final Map handshakeHeaders); - // Verify strict compliance with Python/Go behavior - if (!preferredProtocolVersion.getValue().equals(serverVersion)) { - return CompletableFuture.failedFuture( - new McpException( - "MCP version mismatch: client (" - + preferredProtocolVersion.getValue() - + ") != server (" - + serverVersion - + ")")); - } - - this.negotiatedProtocolVersion = ProtocolVersion.fromString(serverVersion); - - if (negotiatedProtocolVersion == ProtocolVersion.VERSION_2025_03_26) { - java.util.Optional sessionIdOpt = - res.headers().firstValue("Mcp-Session-Id"); - if (sessionIdOpt.isEmpty()) { - return CompletableFuture.failedFuture( - new McpException( - "Server did not return a Mcp-Session-Id header during" - + " initialization.")); - } - this.sessionId = sessionIdOpt.get(); - } - - JsonRpc.Notification notif = - new JsonRpc.Notification("notifications/initialized", Map.of()); - String notifBody = objectMapper.writeValueAsString(notif); - HttpRequest.Builder nReq = - HttpRequest.newBuilder() - .uri(URI.create(baseUrl)) - .POST(HttpRequest.BodyPublishers.ofString(notifBody)); - - handshakeHeaders.forEach(nReq::setHeader); - applyProtocolHeaders(nReq); - - return httpClient - .sendAsync(nReq.build(), HttpResponse.BodyHandlers.ofString()) - .thenAccept(nRes -> {}); - } catch (Exception e) { - return CompletableFuture.failedFuture(e); - } - }); - } catch (Exception e) { - return CompletableFuture.failedFuture(e); - } - } - - private void applyProtocolHeaders(HttpRequest.Builder builder) { - builder.header("Content-Type", "application/json"); - if (negotiatedProtocolVersion == null) { - return; - } - if (negotiatedProtocolVersion.requiresAcceptJson()) { - builder.header("Accept", "application/json"); - } - if (negotiatedProtocolVersion.requiresVersionHeader()) { - builder.header("MCP-Protocol-Version", negotiatedProtocolVersion.getValue()); - } - if (negotiatedProtocolVersion.requiresSessionIdHeader() && sessionId != null) { - builder.header("Mcp-Session-Id", sessionId); - } - } + protected abstract void applyProtocolHeaders(final HttpRequest.Builder builder); @Override - public CompletableFuture listTools( - String toolsetName, Map metadata) { + public final CompletableFuture listTools( + final String toolsetName, final Map metadata) { if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") && !metadata.isEmpty()) { logger.warning(HTTP_WARNING); @@ -402,8 +225,10 @@ public CompletableFuture listTools( } @Override - public CompletableFuture invokeTool( - String toolName, Map arguments, Map metadata) { + public final CompletableFuture invokeTool( + final String toolName, + final Map arguments, + final Map metadata) { if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") && !metadata.isEmpty()) { logger.warning(HTTP_WARNING); @@ -440,10 +265,11 @@ public void close() { // No-op for HttpClient in Java 11 } - private TransportManifest handleListToolsResponse(HttpResponse response) { - if (response.statusCode() != 200) + private TransportManifest handleListToolsResponse(final HttpResponse response) { + if (response.statusCode() != 200) { throw new RuntimeException( "Failed to list tools. Status: " + response.statusCode() + " " + response.body()); + } try { JsonNode root = objectMapper.readTree(response.body()); if (root.has("error")) { diff --git a/src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java b/src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java new file mode 100644 index 0000000..d5833fc --- /dev/null +++ b/src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java @@ -0,0 +1,175 @@ +/* + * 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.transport; + +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.transport.v20241105.HttpMcpTransportV20241105; +import com.google.cloud.mcp.transport.v20250326.HttpMcpTransportV20250326; +import com.google.cloud.mcp.transport.v20250618.HttpMcpTransportV20250618; +import com.google.cloud.mcp.transport.v20251125.HttpMcpTransportV20251125; +import java.net.http.HttpClient; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** Default HTTP transport implementation routing requests to version-specific handlers. */ +public final class HttpMcpTransport implements Transport { + + private final Transport delegate; + + /** + * Constructs a new HttpMcpTransport with a base URL. + * + * @param baseUrl The base URL of the remote service. + */ + public HttpMcpTransport(final String baseUrl) { + this(baseUrl, Map.of(), (CredentialsProvider) null); + } + + /** + * Constructs a new HttpMcpTransport with base URL and default headers. + * + * @param baseUrl The base URL of the remote service. + * @param clientHeaders Default HTTP headers to include in every request. + */ + public HttpMcpTransport(final String baseUrl, final Map clientHeaders) { + this(baseUrl, clientHeaders, (CredentialsProvider) null); + } + + /** + * Constructs a new HttpMcpTransport with base URL, default headers and credentials provider. + * + * @param baseUrl The base URL of the remote service. + * @param clientHeaders Default HTTP headers to include in every request. + * @param credentialsProvider Provider for retrieving authorization credentials. + */ + public HttpMcpTransport( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider) { + this(baseUrl, clientHeaders, credentialsProvider, null, null, null); + } + + /** + * Constructs a HttpMcpTransport. + * + * @param baseUrl The base URL of the remote service. + * @param clientHeaders Default HTTP headers to include in every request. + * @param preferredProtocolVersion Preferred MCP protocol version. + * @param httpClient Custom HTTP Client. + * @param executor Optional Executor for handling async requests. + */ + public HttpMcpTransport( + final String baseUrl, + final Map clientHeaders, + final ProtocolVersion preferredProtocolVersion, + final HttpClient httpClient, + final java.util.concurrent.Executor executor) { + this(baseUrl, clientHeaders, null, preferredProtocolVersion, httpClient, executor); + } + + /** + * Primary constructor for HttpMcpTransport. + * + * @param baseUrl The base URL of the remote service. + * @param clientHeaders Default HTTP headers to include in every request. + * @param credentialsProvider Provider for retrieving authorization credentials. + * @param preferredProtocolVersion Preferred MCP protocol version. + * @param httpClient Custom HTTP Client. + * @param executor Optional Executor for handling async requests. + */ + public HttpMcpTransport( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final ProtocolVersion preferredProtocolVersion, + final HttpClient httpClient, + final java.util.concurrent.Executor executor) { + final ProtocolVersion version = + preferredProtocolVersion != null + ? preferredProtocolVersion + : ProtocolVersion.VERSION_2025_11_25; + + switch (version) { + case VERSION_2025_11_25: + this.delegate = + new HttpMcpTransportV20251125( + baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + break; + case VERSION_2025_06_18: + this.delegate = + new HttpMcpTransportV20250618( + baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + break; + case VERSION_2025_03_26: + this.delegate = + new HttpMcpTransportV20250326( + baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + break; + case VERSION_2024_11_05: + this.delegate = + new HttpMcpTransportV20241105( + baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + break; + default: + throw new IllegalArgumentException("Unsupported protocol version: " + version); + } + } + + /** Internal constructor for testing purposes. */ + public HttpMcpTransport(final String baseUrl, final HttpClient httpClient) { + this(baseUrl, Map.of(), null, null, httpClient, null); + } + + /** Internal constructor for testing purposes. */ + public HttpMcpTransport( + final String baseUrl, final Map clientHeaders, final HttpClient httpClient) { + this(baseUrl, clientHeaders, null, null, httpClient, null); + } + + HttpMcpTransport( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient) { + this(baseUrl, clientHeaders, credentialsProvider, null, httpClient, null); + } + + @Override + public String getBaseUrl() { + return delegate.getBaseUrl(); + } + + @Override + public CompletableFuture listTools( + final String toolsetName, final Map metadata) { + return delegate.listTools(toolsetName, metadata); + } + + @Override + public CompletableFuture invokeTool( + final String toolName, + final Map arguments, + final Map metadata) { + return delegate.invokeTool(toolName, arguments, metadata); + } + + @Override + public void close() { + delegate.close(); + } +} diff --git a/src/main/java/com/google/cloud/mcp/Transport.java b/src/main/java/com/google/cloud/mcp/transport/Transport.java similarity index 97% rename from src/main/java/com/google/cloud/mcp/Transport.java rename to src/main/java/com/google/cloud/mcp/transport/Transport.java index 566eefe..37ac88b 100644 --- a/src/main/java/com/google/cloud/mcp/Transport.java +++ b/src/main/java/com/google/cloud/mcp/transport/Transport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.transport; import java.util.Map; import java.util.concurrent.CompletableFuture; diff --git a/src/main/java/com/google/cloud/mcp/TransportManifest.java b/src/main/java/com/google/cloud/mcp/transport/TransportManifest.java similarity index 92% rename from src/main/java/com/google/cloud/mcp/TransportManifest.java rename to src/main/java/com/google/cloud/mcp/transport/TransportManifest.java index e294afa..f8a8dac 100644 --- a/src/main/java/com/google/cloud/mcp/TransportManifest.java +++ b/src/main/java/com/google/cloud/mcp/transport/TransportManifest.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.transport; +import com.google.cloud.mcp.tool.ToolDefinition; import java.util.Map; /** Represents the raw tools manifest returned by the transport. */ diff --git a/src/main/java/com/google/cloud/mcp/TransportResponse.java b/src/main/java/com/google/cloud/mcp/transport/TransportResponse.java similarity index 97% rename from src/main/java/com/google/cloud/mcp/TransportResponse.java rename to src/main/java/com/google/cloud/mcp/transport/TransportResponse.java index 4532b69..5044af4 100644 --- a/src/main/java/com/google/cloud/mcp/TransportResponse.java +++ b/src/main/java/com/google/cloud/mcp/transport/TransportResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.transport; /** Represents a raw transport response containing status code and response body. */ public final class TransportResponse { diff --git a/src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java b/src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java new file mode 100644 index 0000000..dbbade0 --- /dev/null +++ b/src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java @@ -0,0 +1,130 @@ +/* + * 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.transport.v20241105; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.cloud.mcp.JsonRpc; +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.exception.McpException; +import com.google.cloud.mcp.transport.BaseMcpTransport; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class HttpMcpTransportV20241105 extends BaseMcpTransport { + + public HttpMcpTransportV20241105( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2024_11_05, + httpClient, + executor); + } + + @Override + protected CompletableFuture performInitialization( + final String authHeader, final Map handshakeHeaders) { + try { + if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") + && authHeader != null) { + logger.warning(HTTP_WARNING); + } + JsonRpc.Request initReq = + new JsonRpc.Request( + "initialize", + new JsonRpc.InitializeParams( + ProtocolVersion.VERSION_2024_11_05.getValue(), "mcp-toolbox-sdk-java")); + String body = objectMapper.writeValueAsString(initReq); + HttpRequest.Builder req = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(body)); + + handshakeHeaders.forEach(req::setHeader); + applyProtocolHeaders(req); + + return httpClient + .sendAsync(req.build(), HttpResponse.BodyHandlers.ofString()) + .thenCompose( + res -> { + if (res.statusCode() != 200) { + return CompletableFuture.failedFuture( + new McpException("Init failed: " + res.statusCode() + " " + res.body())); + } + try { + JsonNode responseJson = objectMapper.readTree(res.body()); + if (responseJson.has("error")) { + return CompletableFuture.failedFuture( + new McpException("MCP Error: " + responseJson.get("error").toString())); + } + JsonNode result = responseJson.get("result"); + String serverVersion; + if (result != null && result.has("protocolVersion")) { + serverVersion = result.get("protocolVersion").asText(); + } else { + serverVersion = ProtocolVersion.VERSION_2024_11_05.getValue(); + } + + if (!ProtocolVersion.VERSION_2024_11_05.getValue().equals(serverVersion)) { + return CompletableFuture.failedFuture( + new McpException( + "MCP version mismatch: client (" + + ProtocolVersion.VERSION_2024_11_05.getValue() + + ") != server (" + + serverVersion + + ")")); + } + + JsonRpc.Notification notif = + new JsonRpc.Notification("notifications/initialized", Map.of()); + String notifBody = objectMapper.writeValueAsString(notif); + HttpRequest.Builder nReq = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(notifBody)); + + handshakeHeaders.forEach(nReq::setHeader); + applyProtocolHeaders(nReq); + + return httpClient + .sendAsync(nReq.build(), HttpResponse.BodyHandlers.ofString()) + .thenAccept(nRes -> {}); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + }); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + + @Override + protected void applyProtocolHeaders(final HttpRequest.Builder builder) { + builder.header("Content-Type", "application/json"); + } +} diff --git a/src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java b/src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java new file mode 100644 index 0000000..37b518e --- /dev/null +++ b/src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java @@ -0,0 +1,146 @@ +/* + * 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.transport.v20250326; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.cloud.mcp.JsonRpc; +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.exception.McpException; +import com.google.cloud.mcp.transport.BaseMcpTransport; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public final class HttpMcpTransportV20250326 extends BaseMcpTransport { + + private volatile String sessionId; + + public HttpMcpTransportV20250326( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2025_03_26, + httpClient, + executor); + } + + @Override + protected CompletableFuture performInitialization( + final String authHeader, final Map handshakeHeaders) { + try { + if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") + && authHeader != null) { + logger.warning(HTTP_WARNING); + } + JsonRpc.Request initReq = + new JsonRpc.Request( + "initialize", + new JsonRpc.InitializeParams( + ProtocolVersion.VERSION_2025_03_26.getValue(), "mcp-toolbox-sdk-java")); + String body = objectMapper.writeValueAsString(initReq); + HttpRequest.Builder req = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(body)); + + handshakeHeaders.forEach(req::setHeader); + applyProtocolHeaders(req); + + return httpClient + .sendAsync(req.build(), HttpResponse.BodyHandlers.ofString()) + .thenCompose( + res -> { + if (res.statusCode() != 200) { + return CompletableFuture.failedFuture( + new McpException("Init failed: " + res.statusCode() + " " + res.body())); + } + try { + JsonNode responseJson = objectMapper.readTree(res.body()); + if (responseJson.has("error")) { + return CompletableFuture.failedFuture( + new McpException("MCP Error: " + responseJson.get("error").toString())); + } + JsonNode result = responseJson.get("result"); + String serverVersion; + if (result != null && result.has("protocolVersion")) { + serverVersion = result.get("protocolVersion").asText(); + } else { + serverVersion = ProtocolVersion.VERSION_2025_03_26.getValue(); + } + + if (!ProtocolVersion.VERSION_2025_03_26.getValue().equals(serverVersion)) { + return CompletableFuture.failedFuture( + new McpException( + "MCP version mismatch: client (" + + ProtocolVersion.VERSION_2025_03_26.getValue() + + ") != server (" + + serverVersion + + ")")); + } + + Optional sessionIdOpt = res.headers().firstValue("Mcp-Session-Id"); + if (sessionIdOpt.isEmpty()) { + return CompletableFuture.failedFuture( + new McpException( + "Server did not return a Mcp-Session-Id header during" + + " initialization.")); + } + this.sessionId = sessionIdOpt.get(); + + JsonRpc.Notification notif = + new JsonRpc.Notification("notifications/initialized", Map.of()); + String notifBody = objectMapper.writeValueAsString(notif); + HttpRequest.Builder nReq = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(notifBody)); + + handshakeHeaders.forEach(nReq::setHeader); + applyProtocolHeaders(nReq); + + return httpClient + .sendAsync(nReq.build(), HttpResponse.BodyHandlers.ofString()) + .thenAccept(nRes -> {}); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + }); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + + @Override + protected void applyProtocolHeaders(final HttpRequest.Builder builder) { + builder.header("Content-Type", "application/json"); + builder.header("Accept", "application/json"); + if (sessionId != null) { + builder.header("Mcp-Session-Id", sessionId); + } + } +} diff --git a/src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java b/src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java new file mode 100644 index 0000000..7ca28ad --- /dev/null +++ b/src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java @@ -0,0 +1,132 @@ +/* + * 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.transport.v20250618; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.cloud.mcp.JsonRpc; +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.exception.McpException; +import com.google.cloud.mcp.transport.BaseMcpTransport; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class HttpMcpTransportV20250618 extends BaseMcpTransport { + + public HttpMcpTransportV20250618( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2025_06_18, + httpClient, + executor); + } + + @Override + protected CompletableFuture performInitialization( + final String authHeader, final Map handshakeHeaders) { + try { + if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") + && authHeader != null) { + logger.warning(HTTP_WARNING); + } + JsonRpc.Request initReq = + new JsonRpc.Request( + "initialize", + new JsonRpc.InitializeParams( + ProtocolVersion.VERSION_2025_06_18.getValue(), "mcp-toolbox-sdk-java")); + String body = objectMapper.writeValueAsString(initReq); + HttpRequest.Builder req = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(body)); + + handshakeHeaders.forEach(req::setHeader); + applyProtocolHeaders(req); + + return httpClient + .sendAsync(req.build(), HttpResponse.BodyHandlers.ofString()) + .thenCompose( + res -> { + if (res.statusCode() != 200) { + return CompletableFuture.failedFuture( + new McpException("Init failed: " + res.statusCode() + " " + res.body())); + } + try { + JsonNode responseJson = objectMapper.readTree(res.body()); + if (responseJson.has("error")) { + return CompletableFuture.failedFuture( + new McpException("MCP Error: " + responseJson.get("error").toString())); + } + JsonNode result = responseJson.get("result"); + String serverVersion; + if (result != null && result.has("protocolVersion")) { + serverVersion = result.get("protocolVersion").asText(); + } else { + serverVersion = ProtocolVersion.VERSION_2025_06_18.getValue(); + } + + if (!ProtocolVersion.VERSION_2025_06_18.getValue().equals(serverVersion)) { + return CompletableFuture.failedFuture( + new McpException( + "MCP version mismatch: client (" + + ProtocolVersion.VERSION_2025_06_18.getValue() + + ") != server (" + + serverVersion + + ")")); + } + + JsonRpc.Notification notif = + new JsonRpc.Notification("notifications/initialized", Map.of()); + String notifBody = objectMapper.writeValueAsString(notif); + HttpRequest.Builder nReq = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(notifBody)); + + handshakeHeaders.forEach(nReq::setHeader); + applyProtocolHeaders(nReq); + + return httpClient + .sendAsync(nReq.build(), HttpResponse.BodyHandlers.ofString()) + .thenAccept(nRes -> {}); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + }); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + + @Override + protected void applyProtocolHeaders(final HttpRequest.Builder builder) { + builder.header("Content-Type", "application/json"); + builder.header("Accept", "application/json"); + builder.header("MCP-Protocol-Version", ProtocolVersion.VERSION_2025_06_18.getValue()); + } +} diff --git a/src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java b/src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java new file mode 100644 index 0000000..c7e5695 --- /dev/null +++ b/src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java @@ -0,0 +1,132 @@ +/* + * 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.transport.v20251125; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.cloud.mcp.JsonRpc; +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.exception.McpException; +import com.google.cloud.mcp.transport.BaseMcpTransport; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class HttpMcpTransportV20251125 extends BaseMcpTransport { + + public HttpMcpTransportV20251125( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2025_11_25, + httpClient, + executor); + } + + @Override + protected CompletableFuture performInitialization( + final String authHeader, final Map handshakeHeaders) { + try { + if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") + && authHeader != null) { + logger.warning(HTTP_WARNING); + } + JsonRpc.Request initReq = + new JsonRpc.Request( + "initialize", + new JsonRpc.InitializeParams( + ProtocolVersion.VERSION_2025_11_25.getValue(), "mcp-toolbox-sdk-java")); + String body = objectMapper.writeValueAsString(initReq); + HttpRequest.Builder req = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(body)); + + handshakeHeaders.forEach(req::setHeader); + applyProtocolHeaders(req); + + return httpClient + .sendAsync(req.build(), HttpResponse.BodyHandlers.ofString()) + .thenCompose( + res -> { + if (res.statusCode() != 200) { + return CompletableFuture.failedFuture( + new McpException("Init failed: " + res.statusCode() + " " + res.body())); + } + try { + JsonNode responseJson = objectMapper.readTree(res.body()); + if (responseJson.has("error")) { + return CompletableFuture.failedFuture( + new McpException("MCP Error: " + responseJson.get("error").toString())); + } + JsonNode result = responseJson.get("result"); + String serverVersion; + if (result != null && result.has("protocolVersion")) { + serverVersion = result.get("protocolVersion").asText(); + } else { + serverVersion = ProtocolVersion.VERSION_2025_11_25.getValue(); + } + + if (!ProtocolVersion.VERSION_2025_11_25.getValue().equals(serverVersion)) { + return CompletableFuture.failedFuture( + new McpException( + "MCP version mismatch: client (" + + ProtocolVersion.VERSION_2025_11_25.getValue() + + ") != server (" + + serverVersion + + ")")); + } + + JsonRpc.Notification notif = + new JsonRpc.Notification("notifications/initialized", Map.of()); + String notifBody = objectMapper.writeValueAsString(notif); + HttpRequest.Builder nReq = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl)) + .POST(HttpRequest.BodyPublishers.ofString(notifBody)); + + handshakeHeaders.forEach(nReq::setHeader); + applyProtocolHeaders(nReq); + + return httpClient + .sendAsync(nReq.build(), HttpResponse.BodyHandlers.ofString()) + .thenAccept(nRes -> {}); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + }); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + + @Override + protected void applyProtocolHeaders(final HttpRequest.Builder builder) { + builder.header("Content-Type", "application/json"); + builder.header("Accept", "application/json"); + builder.header("MCP-Protocol-Version", ProtocolVersion.VERSION_2025_11_25.getValue()); + } +} diff --git a/src/test/java/com/google/cloud/mcp/AuthMethodsTest.java b/src/test/java/com/google/cloud/mcp/auth/AuthMethodsTest.java similarity index 99% rename from src/test/java/com/google/cloud/mcp/AuthMethodsTest.java rename to src/test/java/com/google/cloud/mcp/auth/AuthMethodsTest.java index 3cd75ab..66a497f 100644 --- a/src/test/java/com/google/cloud/mcp/AuthMethodsTest.java +++ b/src/test/java/com/google/cloud/mcp/auth/AuthMethodsTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.auth; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/src/test/java/com/google/cloud/mcp/HttpMcpToolboxClientTest.java b/src/test/java/com/google/cloud/mcp/client/HttpMcpToolboxClientTest.java similarity index 98% rename from src/test/java/com/google/cloud/mcp/HttpMcpToolboxClientTest.java rename to src/test/java/com/google/cloud/mcp/client/HttpMcpToolboxClientTest.java index a558e6d..9ea1dad 100644 --- a/src/test/java/com/google/cloud/mcp/HttpMcpToolboxClientTest.java +++ b/src/test/java/com/google/cloud/mcp/client/HttpMcpToolboxClientTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.client; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -22,6 +22,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.ProtocolVersion; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; diff --git a/src/test/java/com/google/cloud/mcp/McpToolboxClientBuilderTest.java b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientBuilderTest.java similarity index 94% rename from src/test/java/com/google/cloud/mcp/McpToolboxClientBuilderTest.java rename to src/test/java/com/google/cloud/mcp/client/McpToolboxClientBuilderTest.java index 5de1e1f..b5b1168 100644 --- a/src/test/java/com/google/cloud/mcp/McpToolboxClientBuilderTest.java +++ b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientBuilderTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.client; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -23,6 +23,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.exception.McpException; +import com.google.cloud.mcp.tool.ToolPostProcessor; +import com.google.cloud.mcp.tool.ToolPreProcessor; +import com.google.cloud.mcp.transport.Transport; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; diff --git a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplErrorsTest.java b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplErrorsTest.java similarity index 97% rename from src/test/java/com/google/cloud/mcp/McpToolboxClientImplErrorsTest.java rename to src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplErrorsTest.java index 36d1686..49732a8 100644 --- a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplErrorsTest.java +++ b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplErrorsTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.client; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -24,6 +24,10 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.tool.ToolDefinition; +import com.google.cloud.mcp.tool.ToolResult; +import com.google.cloud.mcp.transport.HttpMcpTransport; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; diff --git a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplHeadersTest.java b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplHeadersTest.java similarity index 92% rename from src/test/java/com/google/cloud/mcp/McpToolboxClientImplHeadersTest.java rename to src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplHeadersTest.java index 72b7031..7dc2aab 100644 --- a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplHeadersTest.java +++ b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplHeadersTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.client; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -24,6 +24,10 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.transport.BaseMcpTransport; +import com.google.cloud.mcp.transport.HttpMcpTransport; import java.lang.reflect.Field; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -66,9 +70,12 @@ void testCustomHeadersPopulatedInAllRequests() throws Exception { Field transportField = McpToolboxClientImpl.class.getDeclaredField("transport"); transportField.setAccessible(true); HttpMcpTransport transport = (HttpMcpTransport) transportField.get(client); - Field httpClientField = HttpMcpTransport.class.getDeclaredField("httpClient"); + Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + Field httpClientField = BaseMcpTransport.class.getDeclaredField("httpClient"); httpClientField.setAccessible(true); - httpClientField.set(transport, mockHttpClient); + httpClientField.set(delegate, mockHttpClient); HttpResponse initResponse = mock(HttpResponse.class); when(initResponse.statusCode()).thenReturn(200); @@ -150,9 +157,12 @@ void testExtraHeadersOverrideAndAuthPriority() throws Exception { Field transportField = McpToolboxClientImpl.class.getDeclaredField("transport"); transportField.setAccessible(true); HttpMcpTransport transport = (HttpMcpTransport) transportField.get(client); - Field httpClientField = HttpMcpTransport.class.getDeclaredField("httpClient"); + Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + Field httpClientField = BaseMcpTransport.class.getDeclaredField("httpClient"); httpClientField.setAccessible(true); - httpClientField.set(transport, mockHttpClient); + httpClientField.set(delegate, mockHttpClient); HttpResponse initResponse = mock(HttpResponse.class); when(initResponse.statusCode()).thenReturn(200); @@ -223,9 +233,12 @@ void testNoDuplicateHeaders() throws Exception { Field transportField = McpToolboxClientImpl.class.getDeclaredField("transport"); transportField.setAccessible(true); HttpMcpTransport transport = (HttpMcpTransport) transportField.get(client); - Field httpClientField = HttpMcpTransport.class.getDeclaredField("httpClient"); + Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + Field httpClientField = BaseMcpTransport.class.getDeclaredField("httpClient"); httpClientField.setAccessible(true); - httpClientField.set(transport, mockHttpClient); + httpClientField.set(delegate, mockHttpClient); HttpResponse initResponse = mock(HttpResponse.class); when(initResponse.statusCode()).thenReturn(200); diff --git a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplJsonRpcTest.java b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplJsonRpcTest.java similarity index 98% rename from src/test/java/com/google/cloud/mcp/McpToolboxClientImplJsonRpcTest.java rename to src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplJsonRpcTest.java index 3a9a99e..2bb1db9 100644 --- a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplJsonRpcTest.java +++ b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplJsonRpcTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.client; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -26,6 +26,11 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.mcp.JsonRpc; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.tool.ToolDefinition; +import com.google.cloud.mcp.tool.ToolResult; +import com.google.cloud.mcp.transport.HttpMcpTransport; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; diff --git a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplTest.java b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplTest.java similarity index 95% rename from src/test/java/com/google/cloud/mcp/McpToolboxClientImplTest.java rename to src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplTest.java index 075fbf0..2ff4387 100644 --- a/src/test/java/com/google/cloud/mcp/McpToolboxClientImplTest.java +++ b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.client; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -31,6 +31,20 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.mcp.JsonRpc; +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.BaseMcpTransport; +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.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -514,10 +528,12 @@ void testLoadToolset_withInvalidUriThrowsException() { @Test void testInvokeTool_withInvalidUriThrowsException() throws Exception { HttpMcpTransport transport = new HttpMcpTransport("http://invalid uri", mockHttpClient); - Field initFutureField = HttpMcpTransport.class.getDeclaredField("initFuture"); + Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + Field initFutureField = BaseMcpTransport.class.getDeclaredField("initFuture"); initFutureField.setAccessible(true); - initFutureField.set( - transport, CompletableFuture.completedFuture(null)); // bypass initialization + initFutureField.set(delegate, CompletableFuture.completedFuture(null)); // bypass initialization McpToolboxClientImpl badClient = new McpToolboxClientImpl(transport, java.util.Collections.emptyMap(), null); @@ -594,11 +610,15 @@ void testEnsureInitialized_withNullAuthHeader() throws Exception { .thenReturn(CompletableFuture.completedFuture(initResponse)) .thenReturn(CompletableFuture.completedFuture(notifResponse)); - Method initMethod = HttpMcpTransport.class.getDeclaredMethod("ensureInitialized", Map.class); + Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + + Method initMethod = BaseMcpTransport.class.getDeclaredMethod("ensureInitialized", Map.class); initMethod.setAccessible(true); CompletableFuture future = - (CompletableFuture) initMethod.invoke(transport, java.util.Collections.emptyMap()); + (CompletableFuture) initMethod.invoke(delegate, java.util.Collections.emptyMap()); future.join(); // should complete and NOT set Authorization header ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); @@ -782,9 +802,12 @@ void testListTools_withInvalidToolsetNameThrows() throws Exception { HttpMcpTransport transport = new HttpMcpTransport("http://localhost:8080", mockHttpClient); // Force transport to be initialized first - Field initFutureField = HttpMcpTransport.class.getDeclaredField("initFuture"); + Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + Field initFutureField = BaseMcpTransport.class.getDeclaredField("initFuture"); initFutureField.setAccessible(true); - initFutureField.set(transport, CompletableFuture.completedFuture(null)); + initFutureField.set(delegate, CompletableFuture.completedFuture(null)); CompletableFuture future = transport.listTools("invalid path with spaces \\", java.util.Collections.emptyMap()); @@ -805,9 +828,12 @@ void testEnsureInitialized_withNotificationSerializationFailure() throws Excepti when(mockMapper.writeValueAsString(any(JsonRpc.Notification.class))) .thenThrow(new RuntimeException("Simulated notification serialization failure")); - Field mapperField = HttpMcpTransport.class.getDeclaredField("objectMapper"); + Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + Field mapperField = BaseMcpTransport.class.getDeclaredField("objectMapper"); mapperField.setAccessible(true); - mapperField.set(transport, mockMapper); + mapperField.set(delegate, mockMapper); HttpResponse initResponse = mock(HttpResponse.class); when(initResponse.statusCode()).thenReturn(200); @@ -816,11 +842,11 @@ void testEnsureInitialized_withNotificationSerializationFailure() throws Excepti when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(CompletableFuture.completedFuture(initResponse)); - Method initMethod = HttpMcpTransport.class.getDeclaredMethod("ensureInitialized", Map.class); + Method initMethod = BaseMcpTransport.class.getDeclaredMethod("ensureInitialized", Map.class); initMethod.setAccessible(true); CompletableFuture future = - (CompletableFuture) initMethod.invoke(transport, java.util.Collections.emptyMap()); + (CompletableFuture) initMethod.invoke(delegate, java.util.Collections.emptyMap()); java.util.concurrent.ExecutionException ex = org.junit.jupiter.api.Assertions.assertThrows( diff --git a/src/test/java/com/google/cloud/mcp/e2e/McpToolboxClientTest.java b/src/test/java/com/google/cloud/mcp/e2e/McpToolboxClientTest.java index c2b61e9..77f9823 100644 --- a/src/test/java/com/google/cloud/mcp/e2e/McpToolboxClientTest.java +++ b/src/test/java/com/google/cloud/mcp/e2e/McpToolboxClientTest.java @@ -21,9 +21,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.cloud.mcp.McpToolboxClient; -import com.google.cloud.mcp.Tool; -import com.google.cloud.mcp.ToolDefinition; -import com.google.cloud.mcp.ToolResult; +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.CompletableFuture; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/com/google/cloud/mcp/ToolTest.java b/src/test/java/com/google/cloud/mcp/tool/ToolTest.java similarity index 99% rename from src/test/java/com/google/cloud/mcp/ToolTest.java rename to src/test/java/com/google/cloud/mcp/tool/ToolTest.java index 4c3682a..9ba2c0d 100644 --- a/src/test/java/com/google/cloud/mcp/ToolTest.java +++ b/src/test/java/com/google/cloud/mcp/tool/ToolTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.tool; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; @@ -28,6 +28,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.cloud.mcp.McpToolboxClient; +import com.google.cloud.mcp.auth.ResolvedAuth; import java.util.ArrayList; import java.util.HashMap; import java.util.List; diff --git a/src/test/java/com/google/cloud/mcp/ToolValidationTest.java b/src/test/java/com/google/cloud/mcp/tool/ToolValidationTest.java similarity index 99% rename from src/test/java/com/google/cloud/mcp/ToolValidationTest.java rename to src/test/java/com/google/cloud/mcp/tool/ToolValidationTest.java index bef9d1a..e645a73 100644 --- a/src/test/java/com/google/cloud/mcp/ToolValidationTest.java +++ b/src/test/java/com/google/cloud/mcp/tool/ToolValidationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.tool; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.cloud.mcp.McpToolboxClient; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; diff --git a/src/test/java/com/google/cloud/mcp/HttpMcpTransportTest.java b/src/test/java/com/google/cloud/mcp/transport/HttpMcpTransportTest.java similarity index 97% rename from src/test/java/com/google/cloud/mcp/HttpMcpTransportTest.java rename to src/test/java/com/google/cloud/mcp/transport/HttpMcpTransportTest.java index 1787e89..d96aa76 100644 --- a/src/test/java/com/google/cloud/mcp/HttpMcpTransportTest.java +++ b/src/test/java/com/google/cloud/mcp/transport/HttpMcpTransportTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.mcp; +package com.google.cloud.mcp.transport; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -26,6 +26,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.cloud.mcp.ProtocolVersion; +import com.google.cloud.mcp.auth.CredentialsProvider; +import com.google.cloud.mcp.exception.McpException; +import com.google.cloud.mcp.tool.ToolDefinition; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -253,9 +257,13 @@ void testConstructor_WithCustomExecutorConfiguresHttpClient() throws Exception { null, customExecutor); - java.lang.reflect.Field httpClientField = HttpMcpTransport.class.getDeclaredField("httpClient"); + java.lang.reflect.Field delegateField = HttpMcpTransport.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + Object delegate = delegateField.get(transport); + + java.lang.reflect.Field httpClientField = BaseMcpTransport.class.getDeclaredField("httpClient"); httpClientField.setAccessible(true); - java.net.http.HttpClient httpClient = (java.net.http.HttpClient) httpClientField.get(transport); + java.net.http.HttpClient httpClient = (java.net.http.HttpClient) httpClientField.get(delegate); assertNotNull(httpClient); Object internalExecutor = null; @@ -319,7 +327,7 @@ void testListTools_WithHttpUrlAndMetadata_LogsWarning() throws Exception { .thenReturn(CompletableFuture.completedFuture(mockListResponse)); java.util.logging.Logger transportLogger = - java.util.logging.Logger.getLogger(HttpMcpTransport.class.getName()); + java.util.logging.Logger.getLogger(BaseMcpTransport.class.getName()); java.util.List logRecords = new java.util.ArrayList<>(); java.util.logging.Handler logHandler = new java.util.logging.Handler() { From 985b16126c1f76c0053b130855bd91528a5fc186 Mon Sep 17 00:00:00 2001 From: Stenal P Jolly Date: Thu, 25 Jun 2026 16:20:51 +0530 Subject: [PATCH 2/2] feat: align idiomatic sync client with latest upstream main --- README.md | 20 +- integration.cloudbuild.yaml | 4 +- .../java/com/google/cloud/mcp/JsonRpc.java | 51 +++ .../google/cloud/mcp/McpToolboxClient.java | 31 ++ .../cloud/mcp/McpToolboxSyncClient.java | 112 ++++++ .../mcp/client/HttpMcpToolboxSyncClient.java | 133 +++++++ .../mcp/client/McpToolboxClientBuilder.java | 32 +- .../mcp/exception/McpProtocolException.java | 40 ++ .../mcp/exception/McpToolboxException.java | 49 +++ .../mcp/exception/McpTransportException.java | 76 ++++ .../mcp/exception/ToolExecutionException.java | 40 ++ .../java/com/google/cloud/mcp/tool/Tool.java | 62 ++- .../cloud/mcp/transport/BaseMcpTransport.java | 99 ++++- .../cloud/mcp/transport/HttpMcpTransport.java | 88 ++++- .../v20241105/HttpMcpTransportV20241105.java | 45 +++ .../v20250326/HttpMcpTransportV20250326.java | 45 +++ .../v20250618/HttpMcpTransportV20250618.java | 45 +++ .../v20251125/HttpMcpTransportV20251125.java | 45 +++ .../cloud/mcp/McpToolboxSyncClientTest.java | 364 ++++++++++++++++++ .../client/McpToolboxClientBuilderTest.java | 23 ++ .../McpToolboxClientImplJsonRpcTest.java | 8 +- .../mcp/transport/HttpMcpTransportTest.java | 47 ++- 22 files changed, 1427 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/google/cloud/mcp/McpToolboxSyncClient.java create mode 100644 src/main/java/com/google/cloud/mcp/client/HttpMcpToolboxSyncClient.java create mode 100644 src/main/java/com/google/cloud/mcp/exception/McpProtocolException.java create mode 100644 src/main/java/com/google/cloud/mcp/exception/McpToolboxException.java create mode 100644 src/main/java/com/google/cloud/mcp/exception/McpTransportException.java create mode 100644 src/main/java/com/google/cloud/mcp/exception/ToolExecutionException.java create mode 100644 src/test/java/com/google/cloud/mcp/McpToolboxSyncClientTest.java 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/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 7bc7501..54b4b98 100644 --- a/src/main/java/com/google/cloud/mcp/JsonRpc.java +++ b/src/main/java/com/google/cloud/mcp/JsonRpc.java @@ -19,13 +19,30 @@ import java.util.Map; import java.util.UUID; +/** 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; + /** + * 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; @@ -33,32 +50,66 @@ public Request(final String method, final Object params) { } } + /** 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; + /** + * 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; } } + /** 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; + /** + * 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; } } + /** 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; + /** + * 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(); diff --git a/src/main/java/com/google/cloud/mcp/McpToolboxClient.java b/src/main/java/com/google/cloud/mcp/McpToolboxClient.java index ff79e04..30f6acc 100644 --- a/src/main/java/com/google/cloud/mcp/McpToolboxClient.java +++ b/src/main/java/com/google/cloud/mcp/McpToolboxClient.java @@ -175,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. * @@ -191,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/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/client/McpToolboxClientBuilder.java b/src/main/java/com/google/cloud/mcp/client/McpToolboxClientBuilder.java index 0b66c13..a9ebaec 100644 --- a/src/main/java/com/google/cloud/mcp/client/McpToolboxClientBuilder.java +++ b/src/main/java/com/google/cloud/mcp/client/McpToolboxClientBuilder.java @@ -17,6 +17,7 @@ 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; @@ -38,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() {} @@ -92,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; @@ -104,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()) { @@ -137,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/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/Tool.java b/src/main/java/com/google/cloud/mcp/tool/Tool.java index 4fb0229..2230eea 100644 --- a/src/main/java/com/google/cloud/mcp/tool/Tool.java +++ b/src/main/java/com/google/cloud/mcp/tool/Tool.java @@ -19,6 +19,7 @@ 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; @@ -32,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<>(); /** @@ -48,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; @@ -130,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. @@ -137,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> argsFuture = CompletableFuture.completedFuture(new HashMap<>(args)); @@ -168,11 +207,12 @@ public CompletableFuture execute(Map args) { .thenCompose( resolvedAuth -> { try { - // Apply credential parameter bindings and extra headers + // Apply credential parameter bindings and headers resolvedAuth.applyTo(finalArgs, extraHeaders, definition); // Validation & Cleanup validateAndSanitizeArgs(finalArgs); + return client.invokeTool(name, finalArgs, extraHeaders); } catch (Exception e) { return CompletableFuture.failedFuture(e); @@ -187,12 +227,18 @@ public CompletableFuture execute(Map args) { return resultFuture; } - /** Validates arguments against the tool definition and removes null values. */ - private void validateAndSanitizeArgs(Map args) { + /** + * Validates arguments against the tool definition and removes null values. + * + * @param args The arguments to validate. + */ + private void validateAndSanitizeArgs(final Map args) { // Remove nulls first (filtering none values) args.values().removeIf(Objects::isNull); - if (definition.parameters() == null) return; + if (definition.parameters() == null) { + return; + } for (ToolDefinition.Parameter param : definition.parameters()) { Object value = args.get(param.name()); @@ -221,7 +267,7 @@ private void validateAndSanitizeArgs(Map args) { } } - private Object deepCopy(Object value) { + private Object deepCopy(final Object value) { if (value instanceof Map) { Map map = (Map) value; Map copy = new HashMap<>(); @@ -240,7 +286,7 @@ private Object deepCopy(Object value) { return value; } - private boolean isTypeMatch(Object value, String type) { + private boolean isTypeMatch(final Object value, final String type) { switch (type.toLowerCase()) { case "string": return value instanceof String; diff --git a/src/main/java/com/google/cloud/mcp/transport/BaseMcpTransport.java b/src/main/java/com/google/cloud/mcp/transport/BaseMcpTransport.java index 6a60297..1bbcdda 100644 --- a/src/main/java/com/google/cloud/mcp/transport/BaseMcpTransport.java +++ b/src/main/java/com/google/cloud/mcp/transport/BaseMcpTransport.java @@ -37,22 +37,60 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; +/** + * Base class for HTTP-based MCP transports providing common functionality like session tracking, + * header merging, and credentials resolution. + */ public abstract class BaseMcpTransport implements Transport { + /** Default static logger for BaseMcpTransport. */ protected static final Logger logger = Logger.getLogger(BaseMcpTransport.class.getName()); + + /** Warning message displayed when using unencrypted HTTP connections. */ protected static final String HTTP_WARNING = "This connection is using HTTP. To prevent credential exposure, please ensure all" + " communication is sent over HTTPS."; + /** The base URL of the MCP service. */ protected final String baseUrl; + + /** Client headers configured for the transport. */ protected final Map clientHeaders; + + /** The credentials provider for dynamic authorization. */ protected final CredentialsProvider credentialsProvider; + + /** The HTTP client used for requests. */ protected final HttpClient httpClient; + + /** The ObjectMapper for JSON serialization. */ protected final ObjectMapper objectMapper; + + /** The preferred protocol version. */ protected final ProtocolVersion preferredProtocolVersion; + + /** Lock object to synchronize initialization. */ protected final Object initLock = new Object(); + + /** Future indicating the status of initialization. */ protected CompletableFuture initFuture; + /** The request timeout for HTTP requests. */ + protected final java.time.Duration requestTimeout; + + /** The logger used for active transport logging. */ + protected final Logger activeLogger; + + /** + * Constructs a new BaseMcpTransport. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param preferredProtocolVersion The preferred protocol version. + * @param httpClient The HTTP client. + * @param executor The executor. + */ protected BaseMcpTransport( final String baseUrl, final Map clientHeaders, @@ -60,6 +98,41 @@ protected BaseMcpTransport( final ProtocolVersion preferredProtocolVersion, final HttpClient httpClient, final java.util.concurrent.Executor executor) { + this( + baseUrl, + clientHeaders, + credentialsProvider, + preferredProtocolVersion, + httpClient, + executor, + null, + null, + null); + } + + /** + * Constructs a new BaseMcpTransport with timeouts and custom logger. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param preferredProtocolVersion The preferred protocol version. + * @param httpClient The HTTP client. + * @param executor The executor. + * @param connectTimeout The connection timeout. + * @param requestTimeout The request timeout. + * @param logger The custom logger. + */ + protected BaseMcpTransport( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final ProtocolVersion preferredProtocolVersion, + final HttpClient httpClient, + final java.util.concurrent.Executor executor, + final java.time.Duration connectTimeout, + final java.time.Duration requestTimeout, + final Logger logger) { if (baseUrl == null || baseUrl.isEmpty()) { throw new IllegalArgumentException("Base URL must be provided"); } @@ -79,12 +152,14 @@ protected BaseMcpTransport( HttpClient.Builder builder = HttpClient.newBuilder() .cookieHandler(new java.net.CookieManager()) - .connectTimeout(Duration.ofSeconds(10)); + .connectTimeout(connectTimeout != null ? connectTimeout : Duration.ofSeconds(10)); if (executor != null) { builder.executor(executor); } this.httpClient = builder.build(); } + this.requestTimeout = requestTimeout; + this.activeLogger = logger != null ? logger : BaseMcpTransport.logger; this.objectMapper = new ObjectMapper(); } @@ -187,9 +262,21 @@ final CompletableFuture ensureInitialized(final Map extraM } } + /** + * Performs the version-specific initialization handshake. + * + * @param authHeader The authorization header value, if present. + * @param handshakeHeaders The resolved headers for the handshake. + * @return A CompletableFuture that completes when initialization is done. + */ protected abstract CompletableFuture performInitialization( final String authHeader, final Map handshakeHeaders); + /** + * Applies protocol-specific headers to the request builder. + * + * @param builder The HTTP request builder. + */ protected abstract void applyProtocolHeaders(final HttpRequest.Builder builder); @Override @@ -197,7 +284,7 @@ public final CompletableFuture listTools( final String toolsetName, final Map metadata) { if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") && !metadata.isEmpty()) { - logger.warning(HTTP_WARNING); + activeLogger.warning(HTTP_WARNING); } return ensureInitialized(metadata) .thenCompose(v -> mergeHeaders(metadata)) @@ -212,6 +299,9 @@ public final CompletableFuture listTools( HttpRequest.newBuilder() .uri(URI.create(url)) .POST(HttpRequest.BodyPublishers.ofString(body)); + if (requestTimeout != null) { + req.timeout(requestTimeout); + } mergedHeaders.forEach(req::setHeader); applyProtocolHeaders(req); @@ -231,7 +321,7 @@ public final CompletableFuture invokeTool( final Map metadata) { if (this.baseUrl.toLowerCase(java.util.Locale.ROOT).startsWith("http://") && !metadata.isEmpty()) { - logger.warning(HTTP_WARNING); + activeLogger.warning(HTTP_WARNING); } return ensureInitialized(metadata) .thenCompose(v -> mergeHeaders(metadata)) @@ -248,6 +338,9 @@ public final CompletableFuture invokeTool( .uri(URI.create(baseUrl)) .POST(HttpRequest.BodyPublishers.ofString(requestBody)); + if (requestTimeout != null) { + requestBuilder.timeout(requestTimeout); + } mergedHeaders.forEach(requestBuilder::setHeader); applyProtocolHeaders(requestBuilder); diff --git a/src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java b/src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java index d5833fc..10e2540 100644 --- a/src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java +++ b/src/main/java/com/google/cloud/mcp/transport/HttpMcpTransport.java @@ -23,8 +23,10 @@ import com.google.cloud.mcp.transport.v20250618.HttpMcpTransportV20250618; import com.google.cloud.mcp.transport.v20251125.HttpMcpTransportV20251125; import java.net.http.HttpClient; +import java.time.Duration; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; /** Default HTTP transport implementation routing requests to version-specific handlers. */ public final class HttpMcpTransport implements Transport { @@ -99,6 +101,41 @@ public HttpMcpTransport( final ProtocolVersion preferredProtocolVersion, final HttpClient httpClient, final java.util.concurrent.Executor executor) { + this( + baseUrl, + clientHeaders, + credentialsProvider, + preferredProtocolVersion, + httpClient, + executor, + null, + null, + null); + } + + /** + * Constructs a new HttpMcpTransport with full configuration. + * + * @param baseUrl The base URL of the MCP service. + * @param clientHeaders Optional headers to include in every request. + * @param credentialsProvider Optional provider for auth credentials. + * @param preferredProtocolVersion Optional preferred protocol version. + * @param httpClient Optional HttpClient instance. + * @param executor Optional Executor for handling async requests. + * @param connectTimeout Optional connection timeout. + * @param requestTimeout Optional request timeout. + * @param logger Optional Logger instance. + */ + public HttpMcpTransport( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final ProtocolVersion preferredProtocolVersion, + final HttpClient httpClient, + final java.util.concurrent.Executor executor, + final Duration connectTimeout, + final Duration requestTimeout, + final Logger logger) { final ProtocolVersion version = preferredProtocolVersion != null ? preferredProtocolVersion @@ -108,34 +145,73 @@ public HttpMcpTransport( case VERSION_2025_11_25: this.delegate = new HttpMcpTransportV20251125( - baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + baseUrl, + clientHeaders, + credentialsProvider, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); break; case VERSION_2025_06_18: this.delegate = new HttpMcpTransportV20250618( - baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + baseUrl, + clientHeaders, + credentialsProvider, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); break; case VERSION_2025_03_26: this.delegate = new HttpMcpTransportV20250326( - baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + baseUrl, + clientHeaders, + credentialsProvider, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); break; case VERSION_2024_11_05: this.delegate = new HttpMcpTransportV20241105( - baseUrl, clientHeaders, credentialsProvider, httpClient, executor); + baseUrl, + clientHeaders, + credentialsProvider, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); break; default: throw new IllegalArgumentException("Unsupported protocol version: " + version); } } - /** Internal constructor for testing purposes. */ + /** + * Internal constructor for testing purposes. + * + * @param baseUrl The base URL. + * @param httpClient The mock HttpClient. + */ public HttpMcpTransport(final String baseUrl, final HttpClient httpClient) { this(baseUrl, Map.of(), null, null, httpClient, null); } - /** Internal constructor for testing purposes. */ + /** + * Internal constructor for testing purposes. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param httpClient The mock HttpClient. + */ public HttpMcpTransport( final String baseUrl, final Map clientHeaders, final HttpClient httpClient) { this(baseUrl, clientHeaders, null, null, httpClient, null); diff --git a/src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java b/src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java index dbbade0..46b7314 100644 --- a/src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java +++ b/src/main/java/com/google/cloud/mcp/transport/v20241105/HttpMcpTransportV20241105.java @@ -26,11 +26,23 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +/** HTTP transport implementation for protocol version 2024-11-05. */ public final class HttpMcpTransportV20241105 extends BaseMcpTransport { + /** + * Constructs a new HttpMcpTransportV20241105. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + */ public HttpMcpTransportV20241105( final String baseUrl, final Map clientHeaders, @@ -46,6 +58,39 @@ public HttpMcpTransportV20241105( executor); } + /** + * Constructs a new HttpMcpTransportV20241105 with timeouts and logger. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + * @param connectTimeout The connection timeout. + * @param requestTimeout The request timeout. + * @param logger The logger. + */ + public HttpMcpTransportV20241105( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor, + final Duration connectTimeout, + final Duration requestTimeout, + final Logger logger) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2024_11_05, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); + } + @Override protected CompletableFuture performInitialization( final String authHeader, final Map handshakeHeaders) { diff --git a/src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java b/src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java index 37b518e..fb8faf8 100644 --- a/src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java +++ b/src/main/java/com/google/cloud/mcp/transport/v20250326/HttpMcpTransportV20250326.java @@ -26,14 +26,26 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +/** HTTP transport implementation for protocol version 2025-03-26. */ public final class HttpMcpTransportV20250326 extends BaseMcpTransport { private volatile String sessionId; + /** + * Constructs a new HttpMcpTransportV20250326. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + */ public HttpMcpTransportV20250326( final String baseUrl, final Map clientHeaders, @@ -49,6 +61,39 @@ public HttpMcpTransportV20250326( executor); } + /** + * Constructs a new HttpMcpTransportV20250326 with timeouts and logger. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + * @param connectTimeout The connection timeout. + * @param requestTimeout The request timeout. + * @param logger The logger. + */ + public HttpMcpTransportV20250326( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor, + final Duration connectTimeout, + final Duration requestTimeout, + final Logger logger) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2025_03_26, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); + } + @Override protected CompletableFuture performInitialization( final String authHeader, final Map handshakeHeaders) { diff --git a/src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java b/src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java index 7ca28ad..28fd0f7 100644 --- a/src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java +++ b/src/main/java/com/google/cloud/mcp/transport/v20250618/HttpMcpTransportV20250618.java @@ -26,11 +26,23 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +/** HTTP transport implementation for protocol version 2025-06-18. */ public final class HttpMcpTransportV20250618 extends BaseMcpTransport { + /** + * Constructs a new HttpMcpTransportV20250618. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + */ public HttpMcpTransportV20250618( final String baseUrl, final Map clientHeaders, @@ -46,6 +58,39 @@ public HttpMcpTransportV20250618( executor); } + /** + * Constructs a new HttpMcpTransportV20250618 with timeouts and logger. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + * @param connectTimeout The connection timeout. + * @param requestTimeout The request timeout. + * @param logger The logger. + */ + public HttpMcpTransportV20250618( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor, + final Duration connectTimeout, + final Duration requestTimeout, + final Logger logger) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2025_06_18, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); + } + @Override protected CompletableFuture performInitialization( final String authHeader, final Map handshakeHeaders) { diff --git a/src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java b/src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java index c7e5695..d80c9eb 100644 --- a/src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java +++ b/src/main/java/com/google/cloud/mcp/transport/v20251125/HttpMcpTransportV20251125.java @@ -26,11 +26,23 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +/** HTTP transport implementation for protocol version 2025-11-25. */ public final class HttpMcpTransportV20251125 extends BaseMcpTransport { + /** + * Constructs a new HttpMcpTransportV20251125. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + */ public HttpMcpTransportV20251125( final String baseUrl, final Map clientHeaders, @@ -46,6 +58,39 @@ public HttpMcpTransportV20251125( executor); } + /** + * Constructs a new HttpMcpTransportV20251125 with timeouts and logger. + * + * @param baseUrl The base URL. + * @param clientHeaders The client headers. + * @param credentialsProvider The credentials provider. + * @param httpClient The HTTP client. + * @param executor The executor. + * @param connectTimeout The connection timeout. + * @param requestTimeout The request timeout. + * @param logger The logger. + */ + public HttpMcpTransportV20251125( + final String baseUrl, + final Map clientHeaders, + final CredentialsProvider credentialsProvider, + final HttpClient httpClient, + final java.util.concurrent.Executor executor, + final Duration connectTimeout, + final Duration requestTimeout, + final Logger logger) { + super( + baseUrl, + clientHeaders, + credentialsProvider, + ProtocolVersion.VERSION_2025_11_25, + httpClient, + executor, + connectTimeout, + requestTimeout, + logger); + } + @Override protected CompletableFuture performInitialization( final String authHeader, final Map handshakeHeaders) { diff --git a/src/test/java/com/google/cloud/mcp/McpToolboxSyncClientTest.java b/src/test/java/com/google/cloud/mcp/McpToolboxSyncClientTest.java new file mode 100644 index 0000000..992b926 --- /dev/null +++ b/src/test/java/com/google/cloud/mcp/McpToolboxSyncClientTest.java @@ -0,0 +1,364 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.cloud.mcp.auth.AuthTokenGetter; +import com.google.cloud.mcp.client.HttpMcpToolboxSyncClient; +import com.google.cloud.mcp.exception.McpProtocolException; +import com.google.cloud.mcp.exception.McpToolboxException; +import com.google.cloud.mcp.exception.McpTransportException; +import com.google.cloud.mcp.exception.ToolExecutionException; +import com.google.cloud.mcp.tool.Tool; +import com.google.cloud.mcp.tool.ToolDefinition; +import com.google.cloud.mcp.tool.ToolResult; +import com.google.cloud.mcp.transport.HttpMcpTransport; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +@Timeout(10) +class McpToolboxSyncClientTest { + + @Test + void testConstructorValidation() { + assertThrows(IllegalArgumentException.class, () -> new HttpMcpToolboxSyncClient(null)); + } + + @Test + void testBuilderConfigurationAndAdvancedTimeouts() { + HttpClient mockHttpClient = mock(HttpClient.class); + Logger mockLogger = Logger.getLogger("test"); + + McpToolboxClient asyncClient = + McpToolboxClient.builder() + .baseUrl("http://localhost:8080") + .apiKey("my-api-key") + .connectTimeout(Duration.ofSeconds(5)) + .requestTimeout(Duration.ofSeconds(3)) + .httpClient(mockHttpClient) + .logger(mockLogger) + .build(); + + assertNotNull(asyncClient); + + McpToolboxSyncClient syncClient = + McpToolboxClient.builder() + .baseUrl("http://localhost:8080") + .connectTimeout(Duration.ofSeconds(5)) + .requestTimeout(Duration.ofSeconds(3)) + .logger(mockLogger) + .buildSync(); + + assertNotNull(syncClient); + } + + @Test + void testSyncClientDelegationAllMethods() { + McpToolboxClient mockAsync = mock(McpToolboxClient.class); + McpToolboxSyncClient syncClient = new HttpMcpToolboxSyncClient(mockAsync); + + // 1. listTools() + Map expectedTools = Collections.emptyMap(); + when(mockAsync.listTools()).thenReturn(CompletableFuture.completedFuture(expectedTools)); + assertEquals(expectedTools, syncClient.listTools()); + verify(mockAsync).listTools(); + + // 2. loadToolset(String) + when(mockAsync.loadToolset("set1")) + .thenReturn(CompletableFuture.completedFuture(expectedTools)); + assertEquals(expectedTools, syncClient.loadToolset("set1")); + verify(mockAsync).loadToolset("set1"); + + // 3. loadToolset(String, Map, Map, boolean) + Map expectedLoadedTools = Collections.emptyMap(); + when(mockAsync.loadToolset("set1", Map.of(), Map.of(), true)) + .thenReturn(CompletableFuture.completedFuture(expectedLoadedTools)); + assertEquals(expectedLoadedTools, syncClient.loadToolset("set1", Map.of(), Map.of(), true)); + verify(mockAsync).loadToolset("set1", Map.of(), Map.of(), true); + + // 4. loadTool(String) + Tool mockTool = mock(Tool.class); + when(mockAsync.loadTool("tool1")).thenReturn(CompletableFuture.completedFuture(mockTool)); + assertEquals(mockTool, syncClient.loadTool("tool1")); + verify(mockAsync).loadTool("tool1"); + + // 5. loadTool(String, Map) + when(mockAsync.loadTool("tool1", Map.of())) + .thenReturn(CompletableFuture.completedFuture(mockTool)); + assertEquals(mockTool, syncClient.loadTool("tool1", Map.of())); + verify(mockAsync).loadTool("tool1", Map.of()); + + // 6. invokeTool(String, Map) + ToolResult mockResult = new ToolResult(Collections.emptyList(), false); + when(mockAsync.invokeTool("tool1", Map.of())) + .thenReturn(CompletableFuture.completedFuture(mockResult)); + assertEquals(mockResult, syncClient.invokeTool("tool1", Map.of())); + verify(mockAsync).invokeTool("tool1", Map.of()); + + // 7. invokeTool(String, Map, Map) + when(mockAsync.invokeTool("tool1", Map.of(), Map.of())) + .thenReturn(CompletableFuture.completedFuture(mockResult)); + assertEquals(mockResult, syncClient.invokeTool("tool1", Map.of(), Map.of())); + verify(mockAsync).invokeTool("tool1", Map.of(), Map.of()); + } + + @Test + void testExceptionTranslationDifferentTypes() { + McpToolboxClient mockAsync = mock(McpToolboxClient.class); + McpToolboxSyncClient syncClient = new HttpMcpToolboxSyncClient(mockAsync); + + // 1. McpToolboxException -> rethrown directly + CompletableFuture> f1 = new CompletableFuture<>(); + f1.completeExceptionally(new McpToolboxException("toolbox-error")); + when(mockAsync.listTools()).thenReturn(f1); + McpToolboxException ex1 = assertThrows(McpToolboxException.class, () -> syncClient.listTools()); + assertEquals("toolbox-error", ex1.getMessage()); + + // 2. IllegalArgumentException -> rethrown directly + CompletableFuture> f2 = new CompletableFuture<>(); + f2.completeExceptionally(new IllegalArgumentException("illegal-arg")); + when(mockAsync.listTools()).thenReturn(f2); + IllegalArgumentException ex2 = + assertThrows(IllegalArgumentException.class, () -> syncClient.listTools()); + assertEquals("illegal-arg", ex2.getMessage()); + + // 3. Other checked Exception -> wrapped in McpToolboxException + CompletableFuture> f3 = new CompletableFuture<>(); + f3.completeExceptionally(new Exception("generic-error")); + when(mockAsync.listTools()).thenReturn(f3); + McpToolboxException ex3 = assertThrows(McpToolboxException.class, () -> syncClient.listTools()); + assertEquals("generic-error", ex3.getMessage()); + + // 4. Null cause -> wrapped in McpToolboxException + CompletableFuture> f4 = new CompletableFuture<>(); + f4.completeExceptionally(new java.util.concurrent.CompletionException(null)); + when(mockAsync.listTools()).thenReturn(f4); + McpToolboxException ex4 = assertThrows(McpToolboxException.class, () -> syncClient.listTools()); + assertNotNull(ex4); + } + + @Test + void testToolExecuteSyncExceptionTranslation() { + McpToolboxClient mockAsync = mock(McpToolboxClient.class); + ToolDefinition def = + new ToolDefinition("test description", Collections.emptyList(), Collections.emptyList()); + Tool tool = new Tool("test-tool", def, mockAsync); + + // 1. Tool execution McpToolboxException -> rethrown + CompletableFuture f1 = new CompletableFuture<>(); + f1.completeExceptionally(new McpToolboxException("execution-failed")); + when(mockAsync.invokeTool(any(), any(), any())).thenReturn(f1); + McpToolboxException ex1 = + assertThrows(McpToolboxException.class, () -> tool.executeSync(Collections.emptyMap())); + assertEquals("execution-failed", ex1.getMessage()); + + // 2. Tool execution IllegalArgumentException -> rethrown + CompletableFuture f2 = new CompletableFuture<>(); + f2.completeExceptionally(new IllegalArgumentException("invalid-arg")); + when(mockAsync.invokeTool(any(), any(), any())).thenReturn(f2); + IllegalArgumentException ex2 = + assertThrows( + IllegalArgumentException.class, () -> tool.executeSync(Collections.emptyMap())); + assertEquals("invalid-arg", ex2.getMessage()); + + // 3. Tool execution other Exception -> wrapped in McpToolboxException + CompletableFuture f3 = new CompletableFuture<>(); + f3.completeExceptionally(new RuntimeException("general-fail")); + when(mockAsync.invokeTool(any(), any(), any())).thenReturn(f3); + McpToolboxException ex3 = + assertThrows(McpToolboxException.class, () -> tool.executeSync(Collections.emptyMap())); + assertEquals("general-fail", ex3.getMessage()); + + // 4. Tool execution Null cause -> wrapped in McpToolboxException + CompletableFuture f4 = new CompletableFuture<>(); + f4.completeExceptionally(new java.util.concurrent.CompletionException(null)); + when(mockAsync.invokeTool(any(), any(), any())).thenReturn(f4); + McpToolboxException ex4 = + assertThrows(McpToolboxException.class, () -> tool.executeSync(Collections.emptyMap())); + assertNotNull(ex4); + } + + @Test + void testToolDeepCopyListSupport() { + McpToolboxClient mockAsync = mock(McpToolboxClient.class); + // Set up a parameter with a default value that is a List + List defaultList = List.of("element1", List.of("subelement")); + ToolDefinition.Parameter param = + new ToolDefinition.Parameter( + "listParam", "array", false, "list param", Collections.emptyList(), defaultList); + ToolDefinition def = + new ToolDefinition("test description", List.of(param), Collections.emptyList()); + Tool tool = new Tool("test-tool", def, mockAsync); + + // Call invokeTool and verify the list is deep-copied and passed + ToolResult expectedResult = new ToolResult(Collections.emptyList(), false); + when(mockAsync.invokeTool(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(expectedResult)); + + // Execute with empty args so the default value (list) is used and deep-copied + ToolResult actualResult = tool.executeSync(Collections.emptyMap()); + assertEquals(expectedResult, actualResult); + } + + @Test + void testSyncClientDelegationExceptions() { + McpToolboxClient mockAsync = mock(McpToolboxClient.class); + McpToolboxSyncClient syncClient = new HttpMcpToolboxSyncClient(mockAsync); + + CompletableFuture failedFuture = + CompletableFuture.failedFuture(new McpToolboxException("error")); + + // 1. loadToolset(String) exception + when(mockAsync.loadToolset("set1")).thenAnswer(inv -> failedFuture); + assertThrows(McpToolboxException.class, () -> syncClient.loadToolset("set1")); + + // 2. loadToolset(String, Map, Map, boolean) exception + when(mockAsync.loadToolset("set1", Map.of(), Map.of(), true)).thenAnswer(inv -> failedFuture); + assertThrows( + McpToolboxException.class, () -> syncClient.loadToolset("set1", Map.of(), Map.of(), true)); + + // 3. loadTool(String) exception + when(mockAsync.loadTool("tool1")).thenAnswer(inv -> failedFuture); + assertThrows(McpToolboxException.class, () -> syncClient.loadTool("tool1")); + + // 4. loadTool(String, Map) exception + when(mockAsync.loadTool("tool1", Map.of())).thenAnswer(inv -> failedFuture); + assertThrows(McpToolboxException.class, () -> syncClient.loadTool("tool1", Map.of())); + + // 5. invokeTool(String, Map) exception + when(mockAsync.invokeTool("tool1", Map.of())).thenAnswer(inv -> failedFuture); + assertThrows(McpToolboxException.class, () -> syncClient.invokeTool("tool1", Map.of())); + + // 6. invokeTool(String, Map, Map) exception + when(mockAsync.invokeTool("tool1", Map.of(), Map.of())).thenAnswer(inv -> failedFuture); + assertThrows( + McpToolboxException.class, () -> syncClient.invokeTool("tool1", Map.of(), Map.of())); + } + + @Test + void testExceptionsAndInterfaceCoverage() { + // 1. Instantiate the exception classes + assertNotNull(new McpProtocolException("protocol error")); + assertNotNull(new McpProtocolException("protocol error", new Exception())); + assertNotNull(new ToolExecutionException("execution error")); + assertNotNull(new ToolExecutionException("execution error", new Exception())); + assertNotNull(new McpTransportException("transport error")); + assertNotNull(new McpTransportException("transport error", new Exception())); + + // 2. Interface default method call + McpToolboxSyncClient syncClient = + new McpToolboxSyncClient() { + @Override + public Map listTools() { + return Collections.emptyMap(); + } + + @Override + public Map loadToolset(String toolsetName) { + return Collections.emptyMap(); + } + + @Override + public Map loadToolset( + String toolsetName, + Map> paramBinds, + Map> authBinds, + boolean strict) { + return Collections.emptyMap(); + } + + @Override + public Tool loadTool(String toolName) { + return null; + } + + @Override + public Tool loadTool(String toolName, Map authTokenGetters) { + return null; + } + + @Override + public ToolResult invokeTool(String toolName, Map arguments) { + return null; + } + + @Override + public ToolResult invokeTool( + String toolName, Map arguments, Map extraHeaders) { + return null; + } + }; + assertEquals(Collections.emptyMap(), syncClient.loadToolset()); + } + + @Test + @SuppressWarnings("unchecked") + void testRequestTimeoutPropagation() throws Exception { + HttpClient mockHttpClient = mock(HttpClient.class); + HttpResponse mockInitResponse = mock(HttpResponse.class); + when(mockInitResponse.statusCode()).thenReturn(200); + when(mockInitResponse.body()) + .thenReturn( + "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"protocolVersion\":\"2025-11-25\"}}"); + + HttpResponse mockInitializedResponse = mock(HttpResponse.class); + when(mockInitializedResponse.statusCode()).thenReturn(200); + when(mockInitializedResponse.body()).thenReturn(""); + + HttpResponse mockListResponse = mock(HttpResponse.class); + when(mockListResponse.statusCode()).thenReturn(200); + when(mockListResponse.body()) + .thenReturn("{\"jsonrpc\":\"2.0\",\"id\":\"2\",\"result\":{\"tools\":[]}}"); + + when(mockHttpClient.sendAsync( + any(java.net.http.HttpRequest.class), + any(java.net.http.HttpResponse.BodyHandler.class))) + .thenReturn(CompletableFuture.completedFuture(mockInitResponse)) + .thenReturn(CompletableFuture.completedFuture(mockInitializedResponse)) + .thenReturn(CompletableFuture.completedFuture(mockListResponse)); + + HttpMcpTransport transport = + new HttpMcpTransport( + "http://localhost:8080", + Map.of(), + null, + ProtocolVersion.VERSION_2025_11_25, + mockHttpClient, + null, + Duration.ofSeconds(1), + Duration.ofSeconds(1), + null); + + transport.listTools("", Collections.emptyMap()).get(); + assertNotNull(transport); + } +} diff --git a/src/test/java/com/google/cloud/mcp/client/McpToolboxClientBuilderTest.java b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientBuilderTest.java index b5b1168..fcc2082 100644 --- a/src/test/java/com/google/cloud/mcp/client/McpToolboxClientBuilderTest.java +++ b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientBuilderTest.java @@ -27,6 +27,7 @@ import com.google.cloud.mcp.ProtocolVersion; import com.google.cloud.mcp.auth.CredentialsProvider; import com.google.cloud.mcp.exception.McpException; +import com.google.cloud.mcp.exception.McpTransportException; import com.google.cloud.mcp.tool.ToolPostProcessor; import com.google.cloud.mcp.tool.ToolPreProcessor; import com.google.cloud.mcp.transport.Transport; @@ -199,4 +200,26 @@ void testProtocolVersionFromString() { assertNull(ProtocolVersion.fromString("invalid-version")); assertEquals(ProtocolVersion.VERSION_2025_11_25, ProtocolVersion.fromString("2025-11-25")); } + + @Test + void testMcpTransportExceptionConstructors() { + McpTransportException ex1 = new McpTransportException("msg1"); + assertEquals("msg1", ex1.getMessage()); + assertEquals(-1, ex1.getStatusCode()); + + McpTransportException ex2 = new McpTransportException("msg2", 404); + assertEquals("msg2", ex2.getMessage()); + assertEquals(404, ex2.getStatusCode()); + + RuntimeException cause = new RuntimeException("root"); + McpTransportException ex3 = new McpTransportException("msg3", cause); + assertEquals("msg3", ex3.getMessage()); + assertSame(cause, ex3.getCause()); + assertEquals(-1, ex3.getStatusCode()); + + McpTransportException ex4 = new McpTransportException("msg4", 500, cause); + assertEquals("msg4", ex4.getMessage()); + assertSame(cause, ex4.getCause()); + assertEquals(500, ex4.getStatusCode()); + } } diff --git a/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplJsonRpcTest.java b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplJsonRpcTest.java index 2bb1db9..7c15073 100644 --- a/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplJsonRpcTest.java +++ b/src/test/java/com/google/cloud/mcp/client/McpToolboxClientImplJsonRpcTest.java @@ -492,9 +492,11 @@ void testListTools_withMissingInputSchemaOrProperties() throws Exception { } @Test - void testJsonRpcInstantiation() { - // Instantiate package-private JsonRpc namespace to cover its default constructor - JsonRpc rpc = new JsonRpc(); + void testJsonRpcInstantiation() throws Exception { + // Instantiate private JsonRpc namespace to cover its constructor + java.lang.reflect.Constructor constructor = JsonRpc.class.getDeclaredConstructor(); + constructor.setAccessible(true); + JsonRpc rpc = constructor.newInstance(); assertNotNull(rpc); } diff --git a/src/test/java/com/google/cloud/mcp/transport/HttpMcpTransportTest.java b/src/test/java/com/google/cloud/mcp/transport/HttpMcpTransportTest.java index d96aa76..7921c15 100644 --- a/src/test/java/com/google/cloud/mcp/transport/HttpMcpTransportTest.java +++ b/src/test/java/com/google/cloud/mcp/transport/HttpMcpTransportTest.java @@ -33,6 +33,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; @@ -280,7 +281,6 @@ void testConstructor_WithCustomExecutorConfiguresHttpClient() throws Exception { } } - @Test @SuppressWarnings("unchecked") void testInitialize_ServerReturnsErrorJsonRpcResponse() throws Exception { HttpResponse mockInitResponse = mock(HttpResponse.class); @@ -474,4 +474,49 @@ void testListTools_ParsesComplexToolsCorrectly() throws Exception { assertFalse(p2.required()); assertEquals("string", p2.type()); } + + @Test + @SuppressWarnings("unchecked") + void testInvokeTool_WithRequestTimeout() throws Exception { + HttpMcpTransport transportWithTimeout = + new HttpMcpTransport( + "https://test-mcp-service.com", + Map.of(), + null, + ProtocolVersion.VERSION_2025_11_25, + mockClient, + null, + Duration.ofSeconds(5), + Duration.ofSeconds(3), + null); + + HttpResponse mockInitResponse = mock(HttpResponse.class); + when(mockInitResponse.statusCode()).thenReturn(200); + when(mockInitResponse.body()) + .thenReturn( + "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"protocolVersion\":\"2025-11-25\"}}"); + + HttpResponse mockInitializedResponse = mock(HttpResponse.class); + when(mockInitializedResponse.statusCode()).thenReturn(200); + when(mockInitializedResponse.body()).thenReturn(""); + + HttpResponse mockInvokeResponse = mock(HttpResponse.class); + when(mockInvokeResponse.statusCode()).thenReturn(200); + when(mockInvokeResponse.body()) + .thenReturn( + "{\"jsonrpc\":\"2.0\",\"id\":\"3\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"success\"}]}}"); + + when(mockClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(CompletableFuture.completedFuture(mockInitResponse)) + .thenReturn(CompletableFuture.completedFuture(mockInitializedResponse)) + .thenReturn(CompletableFuture.completedFuture(mockInvokeResponse)); + + TransportResponse response = + transportWithTimeout + .invokeTool("test-tool", Map.of("param1", "value1"), Collections.emptyMap()) + .get(); + + assertNotNull(response); + assertEquals(200, response.getStatusCode()); + } }