This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 9d86d49aee50a85e3acfd8dd49ee2562b3ba90f6 Author: Robert Lazarski <[email protected]> AuthorDate: Fri Apr 10 08:30:45 2026 -1000 Switch MCP bridge from java.net.http to Apache HttpComponents 5 Replace JDK HttpClient with HC5 (httpclient5 5.6) to unify with the rest of Axis2/Java which uses HC5 for all HTTP transports. Benefits: - One HTTP stack across all Axis2/Java modules - Built-in connection pooling (PoolingHttpClientConnectionManager) - ToolRegistry: 10 connections (catalog fetch, startup-only) - McpStdioServer: 20 connections (tool calls, reuses connections) - Fine-grained timeout control (connect + response separately) - Same debugging/wire logging patterns as HTTPSenderImpl - mTLS: SSLContext handling unchanged (both accept javax.net.ssl) Files changed: - pom.xml: add httpclient5 + httpcore5 dependencies - ToolRegistry.java: HttpGet + execute() with response handler - McpStdioServer.java: HttpPost + StringEntity + execute() Removed InterruptedException from callAxis2/buildToolsCallResult signatures (HC5 classic API doesn't throw it). Tested: mvn clean package -DskipTests succeeds. Exe JAR: 4.6 MB. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- modules/mcp-bridge/pom.xml | 10 +++ .../apache/axis2/mcp/bridge/McpStdioServer.java | 75 ++++++++++++++-------- .../org/apache/axis2/mcp/bridge/ToolRegistry.java | 72 ++++++++++++++------- 3 files changed, 107 insertions(+), 50 deletions(-) diff --git a/modules/mcp-bridge/pom.xml b/modules/mcp-bridge/pom.xml index ef19382743..666def4f1f 100644 --- a/modules/mcp-bridge/pom.xml +++ b/modules/mcp-bridge/pom.xml @@ -54,6 +54,16 @@ <version>${jackson.version}</version> </dependency> + <!-- HTTP client — same stack as all Axis2/Java transports --> + <dependency> + <groupId>org.apache.httpcomponents.client5</groupId> + <artifactId>httpclient5</artifactId> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents.core5</groupId> + <artifactId>httpcore5</artifactId> + </dependency> + <!-- Test --> <dependency> <groupId>junit</groupId> diff --git a/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/McpStdioServer.java b/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/McpStdioServer.java index 715f0f469d..2d2723d7de 100644 --- a/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/McpStdioServer.java +++ b/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/McpStdioServer.java @@ -23,17 +23,24 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.util.Timeout; + import javax.net.ssl.SSLContext; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.time.Duration; /** * MCP stdio server: reads JSON-RPC 2.0 requests from stdin, writes responses @@ -59,23 +66,44 @@ public class McpStdioServer { private final String baseUrl; private final ToolRegistry registry; private final ObjectMapper mapper; - private final HttpClient httpClient; + private final CloseableHttpClient httpClient; private final PrintStream out; public McpStdioServer(String baseUrl, ToolRegistry registry, ObjectMapper mapper, SSLContext sslContext) { this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; this.registry = registry; this.mapper = mapper; - HttpClient.Builder builder = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)); - if (sslContext != null) { - builder.sslContext(sslContext); - } - this.httpClient = builder.build(); + this.httpClient = buildHttpClient(sslContext); // stdout must be raw bytes in UTF-8; replace the default PrintStream this.out = new PrintStream(System.out, true, StandardCharsets.UTF_8); } + private static CloseableHttpClient buildHttpClient(SSLContext sslContext) { + HttpClientConnectionManager connManager; + if (sslContext != null) { + connManager = PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() + .setSslContext(sslContext) + .build()) + .setMaxConnTotal(20) + .setMaxConnPerRoute(20) + .build(); + } else { + connManager = PoolingHttpClientConnectionManagerBuilder.create() + .setMaxConnTotal(20) + .setMaxConnPerRoute(20) + .build(); + } + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(Timeout.ofSeconds(10)) + .setResponseTimeout(Timeout.ofSeconds(60)) + .build(); + return HttpClients.custom() + .setConnectionManager(connManager) + .setDefaultRequestConfig(requestConfig) + .build(); + } + /** * Blocking read loop. Returns when stdin is closed (client disconnected). */ @@ -136,9 +164,6 @@ public class McpStdioServer { } catch (IllegalArgumentException e) { // Invalid params: unknown tool name, missing required param, etc. writeError(id, -32602, "Invalid params: " + e.getMessage()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - writeError(id, -32000, "Server error during tool call: " + e.getMessage()); } catch (IOException e) { writeError(id, -32000, "Server error during tool call: " + e.getMessage()); } catch (Exception e) { @@ -183,7 +208,7 @@ public class McpStdioServer { // ── tools/call ────────────────────────────────────────────────────────── - private ObjectNode buildToolsCallResult(JsonNode params) throws IOException, InterruptedException { + private ObjectNode buildToolsCallResult(JsonNode params) throws IOException { String toolName = params.path("name").asText(null); if (toolName == null || toolName.isEmpty()) { throw new IllegalArgumentException("tools/call missing required param 'name'"); @@ -210,7 +235,7 @@ public class McpStdioServer { * POST to the Axis2 endpoint. Wraps MCP arguments in the Axis2 JSON-RPC * request envelope: {@code {operationName: [arguments]}}. */ - private String callAxis2(McpTool tool, JsonNode arguments) throws IOException, InterruptedException { + private String callAxis2(McpTool tool, JsonNode arguments) throws IOException { String url = baseUrl + tool.getPath(); // Axis2 JSON-RPC envelope: {"operationName": [arguments]} @@ -222,16 +247,14 @@ public class McpStdioServer { System.err.println("[axis2-mcp-bridge] Calling: POST " + url); System.err.println("[axis2-mcp-bridge] Body: " + requestBody); - HttpRequest httpRequest = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .timeout(Duration.ofSeconds(60)) - .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8)) - .build(); + HttpPost httpPost = new HttpPost(url); + httpPost.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); - HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - System.err.println("[axis2-mcp-bridge] Response: HTTP " + response.statusCode()); - return response.body(); + return httpClient.execute(httpPost, response -> { + int status = response.getCode(); + System.err.println("[axis2-mcp-bridge] Response: HTTP " + status); + return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + }); } // ── JSON-RPC 2.0 response helpers ─────────────────────────────────────── diff --git a/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/ToolRegistry.java b/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/ToolRegistry.java index 4f2203a0b0..796116be13 100644 --- a/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/ToolRegistry.java +++ b/modules/mcp-bridge/src/main/java/org/apache/axis2/mcp/bridge/ToolRegistry.java @@ -21,13 +21,18 @@ package org.apache.axis2.mcp.bridge; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.util.Timeout; + import javax.net.ssl.SSLContext; import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -46,7 +51,7 @@ public class ToolRegistry { private final String baseUrl; private final ObjectMapper mapper; - private final HttpClient httpClient; + private final CloseableHttpClient httpClient; private List<McpTool> tools = Collections.emptyList(); private Map<String, McpTool> toolMap = Collections.emptyMap(); @@ -54,37 +59,56 @@ public class ToolRegistry { public ToolRegistry(String baseUrl, ObjectMapper mapper, SSLContext sslContext) { this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; this.mapper = mapper; - HttpClient.Builder builder = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)); + this.httpClient = buildHttpClient(sslContext); + } + + private static CloseableHttpClient buildHttpClient(SSLContext sslContext) { + HttpClientConnectionManager connManager; if (sslContext != null) { - builder.sslContext(sslContext); + connManager = PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() + .setSslContext(sslContext) + .build()) + .setMaxConnTotal(10) + .setMaxConnPerRoute(10) + .build(); + } else { + connManager = PoolingHttpClientConnectionManagerBuilder.create() + .setMaxConnTotal(10) + .setMaxConnPerRoute(10) + .build(); } - this.httpClient = builder.build(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(Timeout.ofSeconds(10)) + .setResponseTimeout(Timeout.ofSeconds(15)) + .build(); + return HttpClients.custom() + .setConnectionManager(connManager) + .setDefaultRequestConfig(requestConfig) + .build(); } /** * Fetches {@code /openapi-mcp.json} and builds the tool registry. * Logs to stderr; does not throw on partial failure (empty catalog is valid). */ - public void load() throws IOException, InterruptedException { + public void load() throws IOException { String catalogUrl = baseUrl + "/openapi-mcp.json"; System.err.println("[axis2-mcp-bridge] Loading tool catalog from: " + catalogUrl); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(catalogUrl)) - .header("Accept", "application/json") - .timeout(Duration.ofSeconds(15)) - .GET() - .build(); - - HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpGet httpGet = new HttpGet(catalogUrl); + httpGet.setHeader("Accept", "application/json"); - if (response.statusCode() != 200) { - throw new IOException("Tool catalog fetch failed: HTTP " + response.statusCode() - + " from " + catalogUrl); - } + String responseBody = httpClient.execute(httpGet, response -> { + int status = response.getCode(); + if (status != 200) { + throw new IOException("Tool catalog fetch failed: HTTP " + status + + " from " + catalogUrl); + } + return EntityUtils.toString(response.getEntity()); + }); - JsonNode root = mapper.readTree(response.body()); + JsonNode root = mapper.readTree(responseBody); JsonNode toolsNode = root.path("tools"); if (!toolsNode.isArray()) {
