MCP Support for Apache Axis2/Java
Summary: Axis2/Java gains MCP (Model Context Protocol) support in two phases. Phase A
(practical, immediate) wraps an existing Axis2 deployment with a bridge that reads
/openapi-mcp.json and proxies MCP tools/call to Axis2 over HTTPS+mTLS. Phase B (native,
novel Apache contribution) implements axis2-transport-mcp so Axis2 speaks MCP
directly — no wrapper. One service deployment, three protocols: JSON-RPC, REST, MCP.
MCP is JSON-RPC 2.0. The three required methods are initialize, tools/list, and
tools/call. Everything else (transport: stdio, tool schema format,
capability negotiation) is specified by the MCP protocol document at
modelcontextprotocol.io.
Current State (2026-05-16)
What exists today
| Artifact | Status | Notes |
|---|---|---|
springbootdemo-tomcat11 |
✅ Working | Spring Boot 3.x + Axis2 + Tomcat 11 + Java 25 |
axis2-openapi module |
✅ Working | Serves /openapi.json, /openapi.yaml, /swagger-ui |
/openapi-mcp.json endpoint |
✅ Done | OpenApiSpecGenerator.generateMcpCatalogJson() + SwaggerUIHandler.handleMcpCatalogRequest() |
axis2-mcp-bridge stdio JAR |
✅ Done | modules/mcp-bridge/, produces *-exe.jar uber-jar |
| mTLS transport | ✅ Done | Tomcat 8443, certificateVerification="required", IoT CA pattern |
| X.509 Spring Security | ✅ Done | X509AuthenticationFilter at @Order(2), CN → ROLE_X509_CLIENT |
| A3 end-to-end validation | ✅ Done | Claude Desktop → bridge → mTLS 8443 → BigDataH2Service confirmed |
axis2-spring-boot-starter |
❌ Not started | Phase 1 of modernization plan |
axis2-transport-mcp native |
❌ Not started | Track B — novel Apache contribution |
Reference implementations
Build, deploy, and test instructions for each container are in the sample READMEs:
- Tomcat 11:
modules/samples/userguide/src/userguide/springbootdemo-tomcat11/README.md - WildFly 32/39:
modules/samples/userguide/src/userguide/springbootdemo-wildfly/README.md
springbootdemo-tomcat11 base URL: https://localhost:8443/axis2-json-api
- LoginService (auth, port 8080 only)
- BigDataH2Service (streaming/multiplexing demo, accessible via mTLS on 8443)
springbootdemo-wildfly base URL: https://localhost:8443/axis2-json-api
- LoginService (JWT auth)
- FinancialBenchmarkService (portfolioVariance, monteCarlo VaR with Merton jump-diffusion, scenarioAnalysis)
- BigDataH2Service (HTTP/2 streaming)
Deployed and validated on WildFly 32 and WildFly 39
BigDataH2Service request format (confirmed working via MCP bridge):
{"processBigDataSet":[{"request":{"datasetId":"test-dataset-001","datasetSize":1048576}}]}
Security Architecture
PKI (IoT CA Pattern)
Certificates live in ${project.basedir}/certs/. The CA follows
a standard IoT CA pattern — RSA 4096 CA with RSA 2048 leaf certs,
appropriate for IoT/embedded where certificate management is manual.
| File | Contents | Validity |
|---|---|---|
ca.key / ca.crt |
Root CA, CN=Axis2 CA, O=Apache Axis2, OU=IoT Services |
10 years |
server.key / server.crt |
Server cert, CN=localhost, SAN: DNS:localhost, IP:127.0.0.1 |
2 years |
server-keystore.p12 |
Tomcat server keystore (server cert + key + CA chain) | — |
ca-truststore.p12 |
Tomcat truststore (CA cert only) | — |
client.key / client.crt |
Client cert, CN=axis2-mcp-bridge, extendedKeyUsage=clientAuth |
2 years |
client-keystore.p12 |
Bridge client keystore (client cert + key + CA chain) | — |
Keystores are also copied to ${CATALINA_HOME}/conf/.
Password for all PKCS12 files: changeit
Tomcat mTLS Connector (port 8443)
server.xml connector in ${CATALINA_HOME}/conf/server.xml:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
<SSLHostConfig certificateVerification="required"
truststoreFile="conf/ca-truststore.p12"
truststorePassword="changeit"
truststoreType="PKCS12"
protocols="TLSv1.2+">
<Certificate certificateKeystoreFile="conf/server-keystore.p12"
certificateKeystorePassword="changeit"
certificateKeystoreType="PKCS12"
type="RSA" />
</SSLHostConfig>
</Connector>
Plain HTTP port 8081 is commented out. All traffic goes through 8443.
Spring Security Filter Chain
The filter chains in Axis2Application.java are ordered:
| Order | Chain | Matcher | Auth |
|---|---|---|---|
| 1 | springSecurityFilterChain (default) |
Everything | JWT |
| 2 | springSecurityFilterChainMtls |
Port 8443 (MtlsRequestMatcher) |
X.509 cert |
| 3 | springSecurityFilterChainOpenApi |
/openapi.json, /openapi.yaml, /swagger-ui, /openapi-mcp.json |
None |
| 4 | springSecurityFilterChainLogin |
/services/LoginService/** |
None |
The @Order(2) mTLS chain intercepts all 8443 requests before the JWT chain.
X509AuthenticationFilter reads jakarta.servlet.request.X509Certificate (set by
Tomcat after the TLS handshake), extracts the CN, and creates an
UsernamePasswordAuthenticationToken with ROLE_X509_CLIENT. The existing
GenericAccessDecisionManager.decide() is a no-op, so any authenticated principal
passes FilterSecurityInterceptor.
X.509 Authentication Flow
Client presents cert → Tomcat TLS handshake (certificateVerification=required)
→ Only CA-signed certs pass
→ Tomcat writes cert chain to jakarta.servlet.request.X509Certificate attribute
→ X509AuthenticationFilter.doFilter()
→ Extract CN (e.g., "axis2-mcp-bridge")
→ SecurityContextHolder.getContext().setAuthentication(token)
→ FilterSecurityInterceptor: authenticated → passes
→ Service handler executes
Track A — OpenAPI-Driven MCP Bridge
A1 — /openapi-mcp.json endpoint ✅ Done
Implementation: OpenApiSpecGenerator.generateMcpCatalogJson(HttpServletRequest) iterates
AxisConfiguration.getServices() using the same isSystemService() / shouldIncludeService() /
shouldIncludeOperation() filters as the existing OpenAPI path generation. Output:
{
"tools": [
{
"name": "portfolioVariance",
"description": "Calculate portfolio variance using O(n²) covariance matrix...",
"inputSchema": {
"type": "object",
"required": ["nAssets", "weights", "covarianceMatrix"],
"properties": {
"nAssets": {"type": "integer", "minimum": 2, "maximum": 2000},
"weights": {"type": "array", "items": {"type": "number"}},
"covarianceMatrix": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}},
"normalizeWeights": {"type": "boolean", "default": false},
"nPeriodsPerYear": {"type": "integer", "default": 252}
}
},
"endpoint": "POST /services/FinancialBenchmarkService/portfolioVariance"
},
{
"name": "monteCarlo",
"description": "Monte Carlo VaR simulation using Geometric Brownian Motion or Merton jump-diffusion...",
"inputSchema": {
"type": "object",
"required": [],
"properties": {
"nSimulations": {"type": "integer", "default": 10000, "maximum": 1000000},
"nPeriods": {"type": "integer", "default": 252},
"initialValue": {"type": "number", "default": 1000000},
"expectedReturn": {"type": "number", "default": 0.08},
"volatility": {"type": "number", "default": 0.20},
"model": {"type": "string", "default": "gbm", "enum": ["gbm", "merton"]},
"jumpIntensity": {"type": "number", "default": 1.0},
"jumpMean": {"type": "number", "default": -0.03},
"jumpVol": {"type": "number", "default": 0.05},
"nPeriodsPerYear": {"type": "integer", "default": 252},
"randomSeed": {"type": "integer", "default": 0},
"percentiles": {"type": "array", "default": [0.01, 0.05]}
}
},
"endpoint": "POST /services/FinancialBenchmarkService/monteCarlo"
}
]
}
Tool schemas are populated via mcpInputSchema parameters in
services.xml — parsed by generateMcpCatalogJson() at runtime.
Routing: OpenApiServlet.java dispatches uri.endsWith("/openapi-mcp.json") to
handler.handleMcpCatalogRequest(). Axis2WebAppInitializer.java maps the path.
Axis2Application.java OPENAPI_PATHS array includes /openapi-mcp.json so the
OpenAPI filter chain (@Order(3)) handles it without auth.
A2 — axis2-mcp-bridge stdio JAR ✅ Done
Location: modules/mcp-bridge/
Key decision: No MCP Java SDK (Apache 2.0 license constraint — SDK license
uncertain at implementation time). JSON-RPC 2.0 is implemented directly using
Jackson 2.21.1 (Apache 2.0) + Java stdlib HttpClient. The three-method
handshake is straightforward enough to hand-roll correctly.
Classes:
McpBridgeMain— entry point, parses--base-url,--keystore,--truststoreargs, buildsSSLContext, starts registry + serverToolRegistry— GETs{baseUrl}/openapi-mcp.jsonat startup, buildsList<McpTool>andMap<String,McpTool>McpStdioServer— blocking stdin read loop, JSON-RPC 2.0 dispatchMcpTool— data class: name, description, inputSchema (JsonNode), endpoint, path
Build: maven-shade-plugin 3.6.0 produces axis2-mcp-bridge-2.0.1-SNAPSHOT-exe.jar
(classifier: exe) with MainClass=McpBridgeMain.
Axis2 JSON envelope translation: The bridge translates between standard MCP
JSON-RPC 2.0 and Axis2's internal JSON convention. MCP sends clean named parameters
({"nAssets":5, "weights":[...]}); the bridge wraps them into the envelope that
Axis2's JsonRpcMessageReceiver expects: {"operationName":[{arguments}]}. The
array wrapper and operation-name-as-key pattern are artifacts of Axis2's SOAP/XML
heritage — the JSON formatter maps the request body to an Axiom OMElement tree
where the operation name is the root element and each array entry corresponds to
a Java method parameter. When the service method takes a single POJO argument,
callers see {"operationName":[{"arg0":{...}}]} where arg0 is the default WSDL
parameter name. The bridge hides this from AI clients so they see only standard
JSON-RPC 2.0.
Notifications: MCP notifications/initialized (no id field) is silently consumed
with no response, as required by JSON-RPC 2.0.
Protocol version: "2024-11-05"
Claude Desktop config (~/.config/claude/claude_desktop_config.json):
{
"mcpServers": {
"axis2-demo": {
"command": "java",
"args": ["-jar", "/path/to/axis2-mcp-bridge-2.0.1-SNAPSHOT-exe.jar",
"--base-url", "https://localhost:8443/axis2-json-api",
"--keystore", "${project.basedir}/certs/client-keystore.p12",
"--truststore", "${project.basedir}/certs/ca-truststore.p12"]
}
}
}
A3 — End-to-end validation ✅ Done
Full chain confirmed working:
Claude Desktop → axis2-mcp-bridge stdio → HTTPS+mTLS port 8443
→ Tomcat TLS handshake (client cert CN=axis2-mcp-bridge)
→ X509AuthenticationFilter (authenticated, ROLE_X509_CLIENT)
→ BigDataH2Service.processBigDataSet()
→ real response returned to Claude
Tomcat log confirmation:
X509AuthenticationFilter: authenticated CN=axis2-mcp-bridge on port 8443
Track B — Native MCP Transport (axis2-transport-mcp)
When: After Track A is demonstrated. This is the Apache contribution — no other Java framework has native MCP transport.
Module location: modules/transport-mcp/
Interface: Axis2's TransportListener + TransportSender.
Protocol translation
MCP tools/call (JSON-RPC 2.0)
↓
axis2-transport-mcp
↓
Axis2 MessageContext (service name + operation name + payload)
↓
Service implementation (same Java class as JSON-RPC and REST callers)
↓
Axis2 MessageContext (response payload)
↓
axis2-transport-mcp
↓
MCP tools/call result (JSON-RPC 2.0)
Tool schema generation
Populated from axis2-openapi Phase 2 output. initialize response includes
capabilities.tools derived from deployed services and their @McpTool annotations.
Starter integration
axis2.transport.mcp.enabled=true
axis2.transport.mcp.transport=stdio
End state
Claude Desktop / AI agent → MCP (axis2-transport-mcp, native)
↓
REST clients → REST (planned, Phase 3) → Axis2 Service
↑ (one Java class)
Existing JSON-RPC callers → JSON-RPC (unchanged)
Key Design Decisions
Why stdio transport: Simplest MCP transport, zero port conflicts, works immediately with Claude Desktop and Cursor. No market demand yet for HTTP/SSE transport — stdio covers all current use cases.
Why OpenAPI as the bridge, not direct Axis2 introspection: /openapi-mcp.json
decouples the bridge from Axis2 internals. The bridge works against any HTTP service
that serves this format — not just Axis2. This is useful for the Apache community
beyond the Axis2 user base.
Why no MCP Java SDK: Apache 2.0 license constraint. Jackson (Apache 2.0) + Java
stdlib HttpClient implement the three-method JSON-RPC 2.0 protocol without external
dependencies whose license compatibility is uncertain. The protocol is well-specified
enough to hand-roll correctly.
Why IoT CA pattern: RSA 4096 CA (10 years) + RSA 2048 leaf certs (2 years) matches a standard IoT CA pattern. Appropriate for environments where certificate management is manual and infrequent. The CA is only on one machine — this is a development/demo CA, not a production CA.
Why certificateVerification="required" at Tomcat, not Spring Security: Tomcat
enforces the TLS handshake before any HTTP processing. Invalid client certs are rejected
at the TCP layer — Spring Security never sees them. X509AuthenticationFilter only
needs to extract identity from an already-verified cert, not verify it.
Next Steps
Track A remaining
| Step | Work | Notes |
|---|---|---|
mcpInputSchema in services.xml |
✅ Done | All financial benchmark tools + login have full parameter schemas |
Track B
modules/transport-mcp/— new module scaffolding- stdio transport (B1) — validates JSON-RPC 2.0 ↔ MessageContext translation
Testing matrix
MCP and OpenAPI support needs validation across the full container/JDK matrix:
| Container | JDK | MCP | OpenAPI | Status |
|---|---|---|---|---|
| WildFly 32 | OpenJDK 21 | ✅ | ✅ | Validated |
| WildFly 39 | OpenJDK 25 | ✅ | ✅ | Validated |
| Tomcat 11 | OpenJDK 21 | ✅ | ✅ | Validated |
| Tomcat 11 | OpenJDK 25 | ✅ | ✅ | Validated |
Known Limitations
No progress notifications during long-running operations
The MCP spec supports progress notifications — JSON-RPC messages sent from the server to the client while a tool call is executing. This is useful for operations like Monte Carlo simulations (100K+ paths can take 1-14 seconds) where the AI assistant could display incremental status.
The limitation is architectural, not transport-related. The MCP stdio transport supports progress notifications natively (they are regular JSON-RPC notifications on stdout). The constraint is the bridge's HTTP proxy pattern:
Claude Desktop ←stdio→ axis2-mcp-bridge ←blocking HTTP POST→ Axis2 service
The bridge sends one HTTP POST to Axis2 and blocks until the full response arrives. During a long computation, the bridge has no way to obtain intermediate status from the service. Adding progress support would require one of:
- A polling side-channel (bridge polls a status endpoint while the main call runs)
- HTTP chunked/streaming responses from Axis2
- A callback mechanism from the service to the bridge
These are non-trivial changes to the Axis2 response pipeline and the bridge architecture.
Practical impact: The financial benchmark services complete well within interactive time budgets — portfolio variance in under 1 ms, Monte Carlo 100K paths in ~1.4 seconds on Java. For workloads where even this latency is a concern, the same financial benchmark operations are available on Axis2/C, which runs 2-3x faster: Monte Carlo 100K paths in ~0.7 seconds, 500-asset portfolio variance in 232 μs vs Java's 660 μs (see performance comparison). Both implementations expose identical MCP tool schemas — an AI assistant configured with either backend gets the same financial capabilities.
Auto-generated inputSchema from Java types
When mcpInputSchema is not set in services.xml, the MCP catalog
generator auto-generates a JSON Schema by introspecting the Java service
method's parameter type. Two resolution strategies are used:
ServiceClassparameter — the class is loaded directly from the classpath. Works immediately on the first catalog request.SpringBeanNameparameter — the bean is resolved from the SpringWebApplicationContextvia reflection (no compile-time Spring dependency in the OpenAPI module). Works after Spring initialization is complete.
Supported types: int/long → integer, double/float → number,
boolean → boolean, String → string, arrays (including nested
double[][]), List<T>, and POJOs → object.
Explicit mcpInputSchema in services.xml always takes precedence —
use it when you need required fields, minimum/maximum constraints,
default values, or description text that reflection cannot provide.
Dependencies and Build
Track A (axis2-mcp-bridge) requires:
axis2-openapimodule (for/openapi-mcp.json)com.fasterxml.jackson.core:jackson-databind:2.21.1(Apache 2.0)- Java 21+ (HttpClient is standard library)
- No Axis2 core dependency — bridge is a separate process
Track B (axis2-transport-mcp) requires:
axis2-core/axis2-kernel(TransportListener interface)axis2-openapi(tool schema generation)- No MCP SDK — same Jackson-only approach as A2
Apache Axis2
