You are viewing a plain text version of this content. The canonical link for it is here.
Posted to java-commits@axis.apache.org by ro...@apache.org on 2026/04/10 19:05:35 UTC
(axis-axis2-java-core) 01/02: Switch MCP bridge from java.net.http to Apache HttpComponents 5
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 <ro...@gmail.com>
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) <no...@anthropic.com>
---
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()) {