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/longinteger, double/floatnumber, booleanboolean, Stringstring, 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