Apache Axis2 MCP Integration Guide

Who should read this: Developers building MCP servers or clients that target Axis2 JSON-RPC services and need to understand the auto-generated MCP tool catalog, the required envelope format, authentication flow, and current limitations.

Quick start: For step-by-step build, deploy, and test instructions (including curl commands for every endpoint), see the sample READMEs: springbootdemo-tomcat11 README (Tomcat 11) and springbootdemo-wildfly README (WildFly 32/39).

In one sentence: Axis2 auto-generates an MCP tool catalog from its deployed services, accessible at /openapi-mcp.json, that tells MCP clients the exact JSON-RPC envelope format, auth requirements, and endpoint URL for every deployed operation — no out-of-band documentation required.

What is MCP?

MCP (Model Context Protocol) is an open standard published at modelcontextprotocol.io that defines how AI assistants (Claude, ChatGPT, Cursor, etc.) discover and call external tools.

Relationship to OpenAPI: OpenAPI describes REST APIs for human developers and code generators. MCP describes the same services as tools for AI assistants. Axis2 generates both from the same deployed services — /openapi.json produces the OpenAPI spec, /openapi-mcp.json produces the MCP tool catalog. They are complementary: OpenAPI tells a developer how to call your API; MCP tells an AI assistant how to call it.

The core idea: An MCP server advertises a catalog of tools — each with a name, a natural-language description, and a JSON Schema describing its parameters. An AI assistant reads this catalog, decides which tool to call based on the user's request, fills in the parameters as a JSON object, and sends a tools/call request. The server executes the tool and returns the result as JSON. The entire exchange is JSON — requests, responses, parameter schemas, error messages. There is no XML anywhere in the protocol.

Why MCP is JSON-only: MCP is built on JSON-RPC 2.0, the same lightweight RPC protocol used by language servers (LSP), cryptocurrency nodes (Ethereum), and many other modern tools. AI assistants produce and consume JSON natively — their training data is overwhelmingly JSON, their function-calling APIs use JSON, and their tool-use formats are JSON Schema. XML/SOAP was never considered for MCP because the entire AI tooling ecosystem is JSON-native.

What this means for SOAP services: MCP cannot call SOAP endpoints directly. A SOAP service returns XML envelopes with namespaces, and MCP clients cannot parse them. To expose a SOAP service to AI agents, convert it to JSON-RPC first — this is a configuration change in services.xml (swap message receivers), not a code change. The service Java class is unchanged. See the Spring Boot Starter Guide for the axis2.mode=json setting.

MCP is to AI tool use what OpenAPI is to REST API discovery: a machine-readable contract that eliminates guesswork. The protocol specification is at modelcontextprotocol.io.

Axis2: Three Protocols from One Service

A single Axis2 service deployment simultaneously speaks three protocols from the same Java class, with no code duplication and no wrapper layers:

Protocol What it serves Who calls it
JSON-RPC Axis2's native wire format — {"op":[{"arg0":{...}}]} envelope over HTTP POST Existing enterprise callers, Node.js bridges, legacy integrations
REST + OpenAPI Auto-generated OpenAPI 3.0 spec at /openapi.json, interactive Swagger UI at /swagger-ui React frontends, data API consumers, API gateways, developers exploring the service
MCP Auto-generated MCP tool catalog at /openapi-mcp.json with full inputSchema, auth hints, and payload templates AI assistants (Claude Desktop, Claude API, Cursor), any MCP-compatible agent

This is unique to Axis2. No other Java framework serves all three protocols from the same service class in the same deployment:

  • Spring Boot — excellent REST + OpenAPI via springdoc. MCP is available via Spring AI, but as a separate server component with its own tool definitions — not auto-generated from existing service classes. No native JSON-RPC support.
  • Apache CXF — SOAP + REST, but no JSON-RPC transport and no MCP support.
  • JAX-RS (Jersey, RESTEasy) — REST + OpenAPI only. No JSON-RPC, no MCP.
  • gRPC — its own binary protocol with REST bridging via grpc-gateway. No JSON-RPC, no MCP.

With Axis2, adding MCP to an existing JSON-RPC service is a configuration change in services.xml — add mcpDescription and mcpInputSchema parameters to each operation, and the MCP catalog appears automatically at /openapi-mcp.json. The service Java class is unchanged.

1. The MCP Catalog Endpoint

Every Axis2 deployment exposes a machine-readable MCP tool catalog:

GET /axis2/openapi-mcp.json
Content-Type: application/json
Cache-Control: no-cache, no-store

The catalog is served by SwaggerUIHandler.handleMcpCatalogRequest() alongside the existing /swagger-ui and /openapi.json endpoints. Cache-Control: no-cache, no-store is intentional — the service list changes on every deployment and a stale catalog causes MCP clients to call operations that no longer exist.

HTTP headers set on the catalog response:

Header Value Purpose
Content-Type application/json; charset=UTF-8 MCP client parsing
Cache-Control no-cache, no-store Prevent stale tool lists
Access-Control-Allow-Origin * MCP clients from any origin
Access-Control-Allow-Methods GET, OPTIONS Catalog is GET-only (POST is on service endpoints)
Access-Control-Allow-Headers Content-Type, Authorization Bearer token in pre-flight
X-Content-Type-Options nosniff Security hardening
X-Frame-Options SAMEORIGIN Clickjacking protection

2. Catalog Schema Reference

The catalog JSON has two top-level keys: _meta (transport contract, constant across all services) and tools (one entry per deployed operation).

2.1 _meta Block

{
  "_meta": {
    "axis2JsonRpcFormat": "{\"<operationName>\":[{\"arg0\":{<params>}}]}",
    "contentType":        "application/json",
    "authHeader":         "Authorization: Bearer <token>",
    "tokenEndpoint":      "POST /services/loginService/doLogin",
    "errorContract": {
      "schemaRef": "#/components/schemas/ErrorResponse",
      "fields": {
        "error":      "Error code: VALIDATION_ERROR | RATE_LIMITED | SERVICE_UNAVAILABLE | BAD_REQUEST | INTERNAL_ERROR",
        "message":    "Human-readable error message",
        "errorRef":   "UUID correlation ID — quote in support requests",
        "timestamp":  "ISO 8601 when the error occurred",
        "retryAfter": "Seconds to wait before retrying (429/503 only, null otherwise)"
      },
      "httpStatusMapping": {
        "400": "BAD_REQUEST — malformed JSON or missing required fields",
        "422": "VALIDATION_ERROR — valid JSON but fails business validation",
        "429": "RATE_LIMITED — too many requests, check retryAfter",
        "500": "INTERNAL_ERROR — server fault, errorRef logged server-side",
        "503": "SERVICE_UNAVAILABLE — downstream dependency or overload"
      }
    }
  },
  ...
}

The _meta block answers the three questions every MCP client must answer before calling any tool:

  • What body structure does the server expect? See axis2JsonRpcFormat — the Axis2 JSON-RPC envelope with operation name as the top-level key.
  • How do I authenticate? See authHeader and tokenEndpoint — call loginService/doLogin first, then pass the returned token as a Bearer header.
  • What Content-Type? Always application/json.
  • What do errors look like? See errorContract — structured JSON with error code, message, correlation ID (errorRef), timestamp, and HTTP status mapping. Services throw JsonRpcFaultException to produce these responses.

2.2 Per-Tool Fields

{
  "tools": [
    {
      "name":        "doLogin",
      "description": "loginService: doLogin",
      "inputSchema": {
        "type":       "object",
        "properties": {},
        "required":   []
      },
      "endpoint":                "POST /services/loginService/doLogin",
      "x-axis2-payloadTemplate": "{\"doLogin\":[{\"arg0\":{}}]}",
      "x-requiresAuth":          false,
      "annotations": {
        "readOnlyHint":    false,
        "destructiveHint": false,
        "idempotentHint":  false,
        "openWorldHint":   false
      }
    },
    {
      "name":        "portfolioVariance",
      "description": "Calculate portfolio variance using O(n^2) covariance matrix multiplication",
      "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",
      "x-axis2-payloadTemplate": "{\"portfolioVariance\":[{\"arg0\":{}}]}",
      "x-requiresAuth":          true,
      "annotations": {
        "readOnlyHint":    true,
        "destructiveHint": false,
        "idempotentHint":  true,
        "openWorldHint":   false
      }
    }
  ]
}

Field semantics:

Field Type Notes
name string Axis2 operation name (local part of QName); use as tool name in MCP
description string Auto-generated "ServiceName: operationName" — not a rich natural language description
inputSchema object MCP-compliant JSON Schema; populated from mcpInputSchema parameter in services.xml when set, otherwise empty {}
endpoint string Full POST path for this operation
x-axis2-payloadTemplate string (JSON) The exact body to send, with the operation's wrapper already filled in
x-requiresAuth boolean false only for loginService (case-insensitive); true for everything else
annotations object MCP 2025-03-26 safety hints; all default to false (conservative)

MCP 2025-03-26 annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are present for spec compliance but are all false. They are not tuned per service. If your MCP host requires accurate hints, you must set them in a catalog post-processor or in your Python MCP server layer.

3. The Axis2 JSON-RPC Envelope (Critical)

Axis2's JSON-RPC layer requires every call to use a specific three-layer envelope. This is the single biggest difference from conventional REST APIs. Every MCP tool that calls an Axis2 service must use this format.

3.1 Required Envelope Structure

POST /services/{ServiceName}/{operationName}
Content-Type: application/json
Authorization: Bearer {token}

{
  "{operationName}": [
    {
      "arg0": {
        ... your parameters here ...
      }
    }
  ]
}

The parsing sequence in JsonUtils.invokeServiceClass() is strict:

  1. beginObject() — outer {}
  2. nextName() — must equal the operation name
  3. beginArray() — the [...] wrapper
  4. beginObject() — the parameter object
  5. nextName()fromJson() — reads each parameter
  6. endObject(), endArray(), endObject()

Any deviation — bare JSON object, missing array, wrong operation name — results in Bad Request [errorRef=<uuid>]. There is no partial match or helpful field-level error (see Section 5).

3.2 Login Payload Example

The exact payload for loginService/doLogin:

{
  "doLogin": [
    {
      "arg0": {
        "email":       "user@example.com",
        "credentials": "password"
      }
    }
  ]
}

This pattern applies to every Axis2 service. The x-axis2-payloadTemplate field in the catalog pre-fills the operation name wrapper so MCP clients only need to substitute parameters into arg0.

3.3 enableJSONOnly Mode

When a service is deployed with enableJSONOnly=true, the outer operation name wrapper is optional — the server dispatches by URL path alone. The payload reduces to:

[{ "arg0": { ... parameters ... } }]

Most production deployments use enableJSONOnly=false (the default), which requires the full envelope. Check the catalog's x-axis2-payloadTemplate to see which format a specific service expects.

4. Authentication: Two-Phase Bearer Token Flow

Note: Axis2 is a web services framework — it does not impose any specific authentication mechanism. The two-phase Bearer token flow described below is implemented by the sample application's Spring Security configuration, not by Axis2 itself. Your application can use any auth mechanism (OAuth2, API keys, mTLS, etc.) by configuring Spring Security or your servlet container accordingly.

The sample application uses the following two-phase flow:

Phase 1 — Obtain Token (no auth required)

POST /services/loginService/doLogin
Content-Type: application/json

{
  "doLogin": [
    {
      "arg0": {
        "email":       "user@example.com",
        "credentials": "password"
      }
    }
  ]
}

Response:
{
  "response": {
    "token": "eyJhbGciOiJIUzI1NiJ9...",
    "user": { ... }
  }
}

Token storage is implementation-specific. A common pattern is to persist the token as a JSON file with mode 0600 (user-only read), or pass it via an environment variable.

Phase 2 — Call Protected Services

POST /services/{ServiceName}/{operationName}
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

{
  "{operationName}": [{ "arg0": { ... } }]
}

All services except loginService require the Bearer header. The catalog's x-requiresAuth field encodes this per-tool. The _meta.tokenEndpoint tells MCP clients where to obtain the token without hardcoding the service path.

5. Error Handling

Axis2 JSON-RPC services support two error formats depending on where the error originates:

5.1 Structured JSON Errors (service-level validation)

Services that throw JsonRpcFaultException produce structured JSON error responses with proper HTTP status codes:

HTTP 422
Content-Type: application/json

{
  "response": {
    "status":     "FAILED",
    "error":      "VALIDATION_ERROR",
    "message":    "initialValue must be > 0 (GBM is undefined for non-positive starting values).",
    "errorRef":   "a3f2c1d0-7b4e-4a2f-9c8d-1e6f3b5a2d7c",
    "timestamp":  "2026-05-15T14:30:00Z",
    "retryAfter": null
  }
}

The HTTP status codes map to error categories:

HTTP Status Error Code Meaning
400 BAD_REQUEST Malformed JSON or missing required fields
422 VALIDATION_ERROR Valid JSON but fails business validation
429 RATE_LIMITED Too many requests; check retryAfter
500 INTERNAL_ERROR Server fault; errorRef logged server-side
503 SERVICE_UNAVAILABLE Downstream dependency or overload

The errorRef UUID is logged server-side with full context (operation name, exception message, stack trace). Clients should surface the UUID in their error displays so users can quote it in support requests.

5.2 SOAP Fault Fallback (parse-level errors)

Requests that fail before reaching the service method (malformed JSON, wrong operation name, missing array wrapper) produce a legacy SOAP fault with a sanitized Bad Request message. This is a deliberate security feature — no structural information leakage:

HTTP 500
<soapenv:Fault>
  <faultcode>soapenv:Server</faultcode>
  <faultstring>Bad Request [errorRef=a3f2c1d0-7b4e-4a2f-9c8d-1e6f3b5a2d7c]</faultstring>
</soapenv:Fault>

5.2 What Triggers This

Bad Payload Result
Not valid JSON at all Bad Request [errorRef=...]
Valid JSON but missing outer array [...] Bad Request [errorRef=...]
Operation name in body does not match URL path Bad Request [errorRef=...]
Parameters wrong type for service method Bad Request [errorRef=...]
Service reflection error (wrong method signature) Internal Server Error [errorRef=...]

5.3 Python MCP Implications

MCP tools built against Axis2 services should surface the errorRef UUID in their error responses so users can correlate with server logs. A recommended pattern is to return a structured ErrorResponse with error_type and suggestions:

from dataclasses import dataclass

@dataclass
class ErrorResponse:
    error:      str        # "Bad Request [errorRef=a3f2c1d0...]"
    error_type: str        # "axis2_payload_error"
    suggestions: list[str] # ["Check x-axis2-payloadTemplate in catalog"]

6. Not Implemented / Limitations

The following capabilities are not present in the Axis2 MCP catalog. Each item notes whether the gap is architectural (won't be added to Axis2) or deferred (could be added).

Feature Status Notes
Rich inputSchema properties Implemented — set mcpInputSchema parameter on <operation> in services.xml When mcpInputSchema is set, the catalog embeds the full JSON Schema (types, constraints, required fields). Falls back to empty {} when not set. All financial benchmark operations have full schemas. Automatic introspection from Java types is not yet implemented — schemas are hand-authored in services.xml.
Natural language tool descriptions Implemented — set mcpDescription parameter on <operation> or <service> in services.xml Operation-level parameter takes precedence over service-level; falls back to auto-generated "ServiceName: operationName".
MCP Resources and Prompts Not implemented The catalog exposes only MCP "tools". The MCP protocol also defines "resources" (URIs for data blobs) and "prompts" (parameterized message templates). Axis2 services are operation-based and do not map to resources or prompts.
MCP 2025-03-26 annotation tuning Implemented — set mcpReadOnly, mcpIdempotent, mcpDestructive, mcpOpenWorld on <operation> or <service> in services.xml Conservative false defaults preserved when parameters absent. Operation-level overrides service-level.
Streaming / SSE responses Not implemented — architectural gap Axis2 JSON-RPC is request/response only. The MCP protocol supports server-sent event streams for long-running operations. There is no streaming path.
Batch tool calls Not implemented Each Axis2 operation is a separate HTTP POST. There is no batch envelope.
Semantic query layer (filter/sort/fields) Not applicable to Axis2 Axis2 JSON-RPC is operation-based — parameters are passed in arg0, not as query strings. A REST API layer could add uniform filter/sort/fields query semantics, but that is outside the scope of the Axis2 JSON-RPC transport.
Natural key resolution (ticker → assetId) Implemented — set global parameter mcpTickerResolveService in axis2.xml When set to ServiceName/operationName, _meta.tickerResolveEndpoint is added to the catalog. Omitted entirely when not configured so deployments without a ticker service are unaffected.
Pagination Offset/limit implemented — see Pagination Guide Axis2 provides PaginatedResponse<T> and PaginationRequest for offset/limit pagination with maxLimit clamping. Cursor-based pagination is not implemented (offset/limit maps directly to JPA/Hibernate DAO patterns).
Structured error responses Implemented — see Section 5 Services throw JsonRpcFaultException to produce structured JSON errors with HTTP status codes (422/429/503), error codes, correlation IDs, and timestamps. Parse-level errors still produce legacy SOAP faults. Not RFC 7807 format, but provides equivalent structured error information.
API key management Not applicable to Axis2 Axis2 uses email/password → Bearer token via loginService only. API key + secret issuance with scopes and rotation would need to be implemented in a separate layer.
Write operations (CRUD mutations) Axis2 has write services; catalog supports them Axis2 write operations exist as services and will appear in the catalog with x-requiresAuth: true. If your deployment also exposes a REST API layer for writes, determine which path is canonical for your use case.

7. Migration Path: JSON-RPC to REST

Organizations that deploy both Axis2 JSON-RPC services and a modern REST API layer will have two different MCP transport paths. Understanding which path a given MCP tool uses determines what limitations apply:

Aspect Axis2 JSON-RPC REST API Layer
Protocol Axis2 JSON-RPC envelope: {"op":[{"arg0":{}}]} Plain REST: GET /api/v1/resources/{id}?filter=...
Tool definitions Auto-generated from deployed Axis2 services Auto-generated from OpenAPI 3.1 (e.g., via springdoc-openapi)
Auth email + password → Bearer token (loginService) API key + secret → scoped JWT
Query semantics Per-operation parameters in arg0 Uniform filter/sort/fields on every resource
Error format Structured JSON (JsonRpcFaultException) with correlation ID, HTTP status codes RFC 7807 Problem Details (JSON, per-field)
Pagination Offset/limit (PaginatedResponse) Cursor-based
inputSchema Full JSON Schema via mcpInputSchema in services.xml (hand-authored) Full JSON Schema from OpenAPI annotations (auto-generated)

The Axis2 MCP catalog at /openapi-mcp.json provides MCP-ready tool discovery for existing Axis2 services. If your organization is building a REST API layer alongside Axis2, the catalog serves as a bridge — providing machine-readable tool discovery during the transition period.

8. Python MCP Client Example

The most common MCP integration path is a Python bridge — AI assistants like Claude and ChatGPT use Python-based MCP servers to call external tools. The template below shows how to authenticate, discover tools from the Axis2 MCP catalog, and call services using the correct JSON-RPC envelope.

import httpx
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

BASE_URL = "https://your-axis2-server/services"
server  = Server("axis2-mcp")
_token  = None

async def login(email: str, password: str) -> str:
    async with httpx.AsyncClient() as c:
        r = await c.post(
            f"{BASE_URL}/loginService/doLogin",
            json={"doLogin": [{"arg0": {"email": email, "credentials": password}}]}
        )
        return r.json()["response"]["token"]

async def call_service(service: str, op: str, params: dict) -> dict:
    async with httpx.AsyncClient() as c:
        r = await c.post(
            f"{BASE_URL}/{service}/{op}",
            json={op: [{"arg0": params}]},
            headers={"Authorization": f"Bearer {_token}"}
        )
        return r.json()

@server.list_tools()
async def list_tools() -> list[Tool]:
    # Fetch live from catalog — honors Cache-Control: no-cache
    async with httpx.AsyncClient() as c:
        catalog = (await c.get(f"{BASE_URL}/../openapi-mcp.json")).json()
    return [
        Tool(name=t["name"], description=t["description"],
             inputSchema=t["inputSchema"])
        for t in catalog["tools"]
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    # Resolve service name from catalog endpoint field
    # then delegate to call_service()
    ...

Key points:

  • The catalog at /openapi-mcp.json has Cache-Control: no-cache, so fetching it at list_tools() time is correct — the tool list stays current without restarting the MCP server.
  • When mcpInputSchema is set in services.xml, the catalog provides full JSON Schema for each tool. For services without it, define the schema in your Python MCP server.
  • The _meta.axis2JsonRpcFormat field documents the exact envelope format your HTTP client must send.