blob: 4ab3b98ee9faee07b9fc5f9ed77856e8f431c509 [file] [view]
# 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 or HTTP/SSE, tool schema format,
capability negotiation) is specified by the MCP protocol document at
modelcontextprotocol.io.
---
## Current State (2026-04-09)
### 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 |
| A4 HTTP/SSE transport | Not started | Post-demo, deferred |
| `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, scenarioAnalysis)
- BigDataH2Service (HTTP/2 streaming)
Deployed and validated on WildFly 32.0.1 (2026-04-09)
```
`BigDataH2Service` request format (confirmed working via MCP bridge):
```json
{"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`:
```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:
```json
{
"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.
### 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-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`):
```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
```
### A4 — HTTP/SSE transport (deferred)
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
```
---
## 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)
```
### Sequencing within Track B
1. **stdio first** — simpler, no connection management, validates the
JSON-RPC 2.0 ↔ MessageContext translation layer end-to-end
2. **HTTP/SSE second** — reuses Axis2's existing HTTP infrastructure, adds
SSE for progress notifications on long-running service operations
### 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
```properties
axis2.transport.mcp.enabled=true
axis2.transport.mcp.transport=stdio # or http
axis2.transport.mcp.path=/mcp # only for http transport
```
### End state
```
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)
```
---
## Key Design Decisions
**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.
---
## Next Steps
### Track A remaining
| 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 |
### Track B
1. `modules/transport-mcp/` — new module scaffolding
2. stdio transport first (B1) — validates JSON-RPC 2.0 ↔ MessageContext translation
3. HTTP/SSE transport (B2) — reuses Axis2 HTTP infrastructure
### 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](https://axis.apache.org/axis2/c/core/), 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](mcp-examples.md#full-performance-summary)).
Both implementations expose identical MCP tool schemas — an AI assistant
configured with either backend gets the same financial capabilities.
### Stdio transport only (HTTP/SSE deferred)
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.
### 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