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()) {

Reply via email to