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, --truststore args, builds SSLContext, starts registry + server
  • ToolRegistry — GETs {baseUrl}/openapi-mcp.json at startup, builds List<McpTool> and Map<String,McpTool>
  • McpStdioServer — blocking stdin read loop, JSON-RPC 2.0 dispatch
  • McpTool — 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

  1. modules/transport-mcp/ — new module scaffolding
  2. 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:

  1. ServiceClass parameter — the class is loaded directly from the classpath. Works immediately on the first catalog request.
  2. 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.


Dependencies and Build

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)
  • 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