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 or HTTP/SSE, tool schema format, capability negotiation) is specified by the MCP protocol document at modelcontextprotocol.io.
| 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 |
| A4 HTTP/SSE transport | ❌ Not started | Post-demo, deferred |
axis2-transport-mcp native | ❌ Not started | Track B — novel Apache contribution |
Build, deploy, and test instructions for each container are in the sample READMEs:
modules/samples/userguide/src/userguide/springbootdemo-tomcat11/README.mdmodules/samples/userguide/src/userguide/springbootdemo-wildfly/README.mdspringbootdemo-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, scenarioAnalysis) - BigDataH2Service (HTTP/2 streaming) Deployed and validated on WildFly 32.0.1 (2026-04-09)
BigDataH2Service request format (confirmed working via MCP bridge):
{"processBigDataSet":[{"request":{"datasetId":"test-dataset-001","datasetSize":1048576}}]}
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
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.
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.
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
/openapi-mcp.json endpoint ✅ DoneImplementation: 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" } ] }
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.
axis2-mcp-bridge stdio JAR ✅ DoneLocation: 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, --truststore args, builds SSLContext, starts registry + serverToolRegistry — GETs {baseUrl}/openapi-mcp.json at startup, builds List<McpTool> and Map<String,McpTool>McpStdioServer — blocking stdin read loop, JSON-RPC 2.0 dispatchMcpTool — data class: name, description, inputSchema (JsonNode), endpoint, pathBuild: maven-shade-plugin 3.6.0 produces axis2-mcp-bridge-2.0.1-SNAPSHOT-exe.jar (classifier: exe) with MainClass=McpBridgeMain.
Axis2 JSON-RPC envelope: tools/call wraps arguments as {toolName: [arguments]} before POSTing to the Axis2 endpoint, matching the existing JSON-RPC convention.
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"] } } }
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
Adds persistent server mode (multiple Claude sessions sharing one bridge). Required for production. Additive — no changes to Axis2 side or tool catalog format.
POST /mcp → JSON-RPC request GET /mcp/sse → SSE stream for server-initiated messages
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.
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)
Populated from axis2-openapi Phase 2 output. initialize response includes capabilities.tools derived from deployed services and their @McpTool annotations.
axis2.transport.mcp.enabled=true axis2.transport.mcp.transport=stdio # or http axis2.transport.mcp.path=/mcp # only for http transport
Claude Desktop / AI agent → MCP (axis2-transport-mcp, native)
↓
REST clients → REST (@RestMapping, Phase 3) → Axis2 Service
↑ (one Java class)
Existing JSON-RPC callers → JSON-RPC (unchanged)
Why stdio first for both tracks: Simplest MCP transport, zero port conflicts, works immediately with Claude Desktop and Cursor. Validates the translation layer before adding HTTP connection management complexity.
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.
Why not JAX-RS instead of @RestMapping: JAX-RS brings a second framework dependency and its own lifecycle. @RestMapping is a thin annotation processed by Axis2's existing REST dispatcher — no container dependency, backwards compatible, opt-in per-operation.
| Step | Work | Notes |
|---|---|---|
mcpInputSchema in services.xml | ✅ Done | All financial benchmark tools + login have full parameter schemas |
| A4 HTTP/SSE | Persistent bridge server mode | Required for production, additive |
modules/transport-mcp/ — new module scaffoldingMCP 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 |
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:
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.
The MCP bridge currently supports stdio transport only (Claude Desktop subprocess model). HTTP/SSE transport (A4) — which would enable Claude API tool use, multi-user bridge sharing, and remote MCP clients — is deferred. Contributions welcome.
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:
ServiceClass parameter — the class is loaded directly from the classpath. Works immediately on the first catalog request.SpringBeanName parameter — the bean is resolved from the Spring WebApplicationContext via 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.
Track A (axis2-mcp-bridge) requires:
axis2-openapi module (for /openapi-mcp.json)com.fasterxml.jackson.core:jackson-databind:2.21.1 (Apache 2.0)Track B (axis2-transport-mcp) requires:
axis2-core / axis2-kernel (TransportListener interface)axis2-openapi (tool schema generation)