Skip to content

Commit bf3a403

Browse files
MCP catalog B1: mcpInputSchema parameter support + build-time code-gen script
- OpenApiSpecGenerator: generateMcpCatalogJson() now reads mcpInputSchema parameter (operation-level overrides service-level via existing getMcpStringParam). Parses value with Jackson to validate JSON; falls back to empty schema with WARN log on parse failure. Backward compatible — no param = existing empty schema. - McpCatalogGeneratorTest: 6 new B1 tests covering parameter override, required array preservation, service-level fallback, precedence, invalid JSON fallback, and backward-compat empty schema baseline. - tools/gen_mcp_schema.py: Option 3 build-time code-gen. Parses typedef struct{} blocks from Axis2/C .h files, maps C types to JSON Schema (integer/number/string/ boolean/array/object), and writes mcpInputSchema parameters into services.xml. Run: python3 tools/gen_mcp_schema.py --header service.h --services services.xml - AXIS2_MODERNIZATION_PLAN.md: new Immediate Track section covering B1/B2/B3/C3 (Java), D1/D2/D3 (Axis2/C), and E (Penguin deployment) with sprint sequence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d140291 commit bf3a403

4 files changed

Lines changed: 669 additions & 7 deletions

File tree

AXIS2_MODERNIZATION_PLAN.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,189 @@ entirely. No other Java framework can do all three from the same service deploym
2323

2424
---
2525

26+
## Immediate Track — MCP inputSchema + Axis2/C + Penguin Demo
27+
28+
**Goal**: Complete the MCP catalog to production quality, port the catalog handler to
29+
Axis2/C, and run a live demo on penguin via Apache httpd. This track runs ahead of
30+
Phases 1–6 because it validates the MCP story end-to-end on real hardware.
31+
32+
### Step B1 — `mcpInputSchema` static parameter support (Java + C)
33+
34+
**Problem**: Every tool in `/openapi-mcp.json` emits `"inputSchema": {}`. Claude has to
35+
guess parameters. This kills usability for financial benchmark tools with 6+ fields.
36+
37+
**Approach (dual strategy)**:
38+
39+
1. **Option 1 — Static declaration in services.xml** (ships first, zero risk):
40+
Each `<operation>` carries a `mcpInputSchema` parameter whose value is a literal
41+
JSON Schema string. `OpenApiSpecGenerator.generateMcpCatalogJson()` reads it with
42+
`getMcpStringParam()` and embeds it verbatim, parsing with Jackson to validate.
43+
Falls back to `{}` on parse failure with a WARN log.
44+
45+
```xml
46+
<operation name="portfolioVariance">
47+
<parameter name="mcpInputSchema">{
48+
"type": "object",
49+
"required": ["n_assets", "weights", "covariance_matrix"],
50+
"properties": {
51+
"n_assets": {"type": "integer", "minimum": 2, "maximum": 2000},
52+
"weights": {"type": "array", "items": {"type": "number"}},
53+
"covariance_matrix": {"type": "array", "items": {"type": "number"}},
54+
"request_id": {"type": "string"}
55+
}
56+
}</parameter>
57+
</operation>
58+
```
59+
60+
2. **Option 3 — Build-time code generation from C headers** (ships second):
61+
A Python script (`tools/gen_mcp_schema.py`) reads Axis2/C service header files,
62+
maps C struct fields to JSON Schema types, and writes `mcpInputSchema` parameters
63+
directly back into `services.xml`. The C type mapping table:
64+
65+
| C type | JSON Schema type |
66+
|--------|-----------------|
67+
| `int`, `long`, `axis2_int32_t` | `"integer"` |
68+
| `double`, `float` | `"number"` |
69+
| `axis2_char_t *`, `char *` | `"string"` |
70+
| `axis2_bool_t` | `"boolean"` |
71+
| pointer-to-struct | `"object"` |
72+
| array pointer + count field | `"array"` |
73+
74+
The script detects `_request_t` structs, infers which fields are required vs
75+
optional (required = no default value set in initialiser), and outputs a
76+
standards-compliant JSON Schema. Services.xml is updated in-place.
77+
78+
Run: `python3 tools/gen_mcp_schema.py --header financial_benchmark_service.h \
79+
--services services.xml`
80+
81+
**Java implementation**: `OpenApiSpecGenerator.generateMcpCatalogJson()` — check
82+
`mcpInputSchema` param before falling back to empty schema. Single method change.
83+
84+
**Tests**: `McpCatalogGeneratorTest` — add tests for schema embedding, invalid JSON
85+
graceful fallback, and missing param fallback.
86+
87+
### Step B2 — `mcpAuthScope` per-operation parameter
88+
89+
Operation-level auth scope string embedded in catalog for MCP clients that support
90+
scope-based auth (e.g. `"mcpAuthScope": "read:portfolio"`). Reads via
91+
`getMcpStringParam()`. Omitted from tool node when absent.
92+
93+
### Step B3 — `mcpStreaming` hint
94+
95+
Boolean `mcpStreaming` parameter marks operations that can stream chunked responses
96+
(e.g. large Monte Carlo results). Adds `"x-streaming": true` to the tool node.
97+
Reads via `getMcpBoolParam()`.
98+
99+
### Step C3 — MCP Resources endpoint
100+
101+
New servlet path `GET /mcp-resources` returns a JSON array of `resource://` URIs:
102+
103+
```json
104+
{
105+
"resources": [
106+
{"uri": "resource://axis2/openapi", "name": "OpenAPI Spec", "mimeType": "application/json"},
107+
{"uri": "resource://axis2/field-catalog", "name": "Field Catalog", "mimeType": "application/json"}
108+
]
109+
}
110+
```
111+
112+
Individual resource content served at `GET /mcp-resource?uri=resource://axis2/openapi`.
113+
Wired in `OpenApiServlet` as a new path case.
114+
115+
---
116+
117+
### Step D1 — Axis2/C MCP catalog handler
118+
119+
New file: `modules/mcp/mcp_catalog_handler.c`
120+
121+
Walks `axis2_conf_t` service map at request time — same traversal as Java's
122+
`axisConfig.getServices()`. Emits the identical JSON catalog format. Key functions:
123+
124+
```c
125+
// Entry point registered on GET /_mcp/openapi-mcp.json
126+
axis2_status_t mcp_catalog_handler_invoke(
127+
axis2_handler_t *handler,
128+
const axutil_env_t *env,
129+
struct axis2_msg_ctx *msg_ctx);
130+
131+
// Reads axis2_op_t parameter, falls back to axis2_svc_t parameter
132+
static const axis2_char_t *get_mcp_param(
133+
axis2_op_t *op, axis2_svc_t *svc,
134+
const axutil_env_t *env,
135+
const axis2_char_t *param_name,
136+
const axis2_char_t *default_val);
137+
```
138+
139+
Parameter reading uses `axis2_op_get_param()` / `axis2_svc_get_param()` — the same
140+
two-level lookup as Java. `mcpDescription`, `mcpReadOnly`, `mcpDestructive`,
141+
`mcpIdempotent`, `mcpInputSchema` all supported.
142+
143+
JSON output built with `json_object_new_object()` (json-c) — no string concatenation.
144+
145+
### Step D2 — Axis2/C correlation ID error hardening
146+
147+
New helper: `axis2_json_secure_fault.c`
148+
149+
```c
150+
axis2_char_t *axis2_json_make_secure_fault_message(
151+
const axutil_env_t *env,
152+
int is_parse_error);
153+
// Returns "Bad Request [errorRef=<uuid>]" or "Internal Server Error [errorRef=<uuid>]"
154+
// UUID generated from /dev/urandom (16 bytes → hex with hyphens)
155+
// Full context logged to axutil_log before sanitized message returned
156+
```
157+
158+
Applied to `financial_benchmark_service_handler.c` JSON parse error paths and any
159+
`axis2_json_rpc_msg_recv` equivalent in Axis2/C.
160+
161+
### Step D3 — Populate `mcpInputSchema` in all 5 financial benchmark operations
162+
163+
Using Option 1 (hand-authored) immediately; Option 3 code-gen script validates against
164+
it. The 5 operations:
165+
166+
| Operation | Required fields |
167+
|-----------|----------------|
168+
| `portfolioVariance` | `n_assets`, `weights`, `covariance_matrix` |
169+
| `monteCarlo` | `n_simulations`, `n_periods`, `initial_value`, `expected_return`, `volatility` |
170+
| `scenarioAnalysis` | `n_assets`, `assets` |
171+
| `generateTestData` | `n_assets` |
172+
| `metadata` | *(none — GET operation)* |
173+
174+
### Step E — Penguin deployment
175+
176+
1. Build `mod_axis2.so` from `axis-axis2-c-core` targeting penguin's Apache httpd
177+
2. `httpd.conf` fragment:
178+
```apache
179+
LoadModule axis2_module modules/mod_axis2.so
180+
Axis2RepoPath /opt/axis2c/repository
181+
<Location /axis2>
182+
SetHandler axis2_module
183+
</Location>
184+
```
185+
3. Deploy `FinancialBenchmarkService` to repository
186+
4. Verify:
187+
```bash
188+
curl https://penguin/axis2/_mcp/openapi-mcp.json
189+
curl -X POST https://penguin/axis2/services/FinancialBenchmarkService/monteCarlo \
190+
-H 'Content-Type: application/json' \
191+
-d '{"monteCarlo":[{"arg0":{"n_simulations":10000,"n_periods":252,...}}]}'
192+
```
193+
5. Demo: MCP-aware client resolves tools from catalog, calls financial operations
194+
195+
### Immediate Sprint Sequence
196+
197+
```
198+
B1 (Java) → B1 tests → B2/B3 (Java, config-only) → C3 (Java, new servlet path)
199+
200+
D1 (Axis2/C catalog handler) → D2 (error hardening) → D3 (services.xml schemas)
201+
202+
Option 3 code-gen script (tools/gen_mcp_schema.py)
203+
204+
E (Penguin deployment + demo)
205+
```
206+
207+
---
208+
26209
## Phase 1 — Spring Boot Starter
27210

28211
**Goal**: Reduce Axis2 + Spring Boot integration from a multi-day configuration project

modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -754,13 +754,50 @@ public String generateMcpCatalogJson(HttpServletRequest request) {
754754
service.getName() + ": " + opName);
755755
toolNode.put("description", description);
756756

757-
// inputSchema: minimal MCP-compliant structure. Richer schemas are
758-
// produced when services carry @McpTool annotations (future work).
759-
com.fasterxml.jackson.databind.node.ObjectNode schema =
760-
toolNode.putObject("inputSchema");
761-
schema.put("type", "object");
762-
schema.putObject("properties");
763-
schema.putArray("required");
757+
// inputSchema: prefer mcpInputSchema parameter (literal JSON Schema
758+
// string set in services.xml at operation or service level).
759+
// Falls back to an empty schema when absent or malformed.
760+
//
761+
// Option 1 usage (services.xml):
762+
// <operation name="portfolioVariance">
763+
// <parameter name="mcpInputSchema">{
764+
// "type": "object",
765+
// "required": ["n_assets", "weights"],
766+
// "properties": {
767+
// "n_assets": {"type": "integer"},
768+
// "weights": {"type": "array", "items": {"type": "number"}}
769+
// }
770+
// }</parameter>
771+
// </operation>
772+
//
773+
// Option 3: schemas can also be written by the build-time code-gen
774+
// script (tools/gen_mcp_schema.py) which reads C header structs and
775+
// emits mcpInputSchema parameters into services.xml automatically.
776+
String mcpInputSchemaStr = getMcpStringParam(operation, service,
777+
"mcpInputSchema", null);
778+
if (mcpInputSchemaStr != null) {
779+
try {
780+
com.fasterxml.jackson.databind.JsonNode parsedSchema =
781+
jackson.readTree(mcpInputSchemaStr);
782+
toolNode.set("inputSchema", parsedSchema);
783+
} catch (Exception parseEx) {
784+
log.warn("[MCP] Invalid mcpInputSchema JSON for operation '"
785+
+ opName + "' in service '" + service.getName()
786+
+ "' — falling back to empty schema: "
787+
+ parseEx.getMessage());
788+
com.fasterxml.jackson.databind.node.ObjectNode schema =
789+
toolNode.putObject("inputSchema");
790+
schema.put("type", "object");
791+
schema.putObject("properties");
792+
schema.putArray("required");
793+
}
794+
} else {
795+
com.fasterxml.jackson.databind.node.ObjectNode schema =
796+
toolNode.putObject("inputSchema");
797+
schema.put("type", "object");
798+
schema.putObject("properties");
799+
schema.putArray("required");
800+
}
764801

765802
toolNode.put("endpoint", "POST " + path);
766803

modules/openapi/src/test/java/org/apache/axis2/openapi/McpCatalogGeneratorTest.java

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,148 @@ public void testAnnotationDefaultsAreConservativeWhenNoParamsSet() throws Except
715715
assertFalse("openWorldHint default must be false", annotations.path("openWorldHint").asBoolean());
716716
}
717717

718+
// ── B1: mcpInputSchema static parameter ──────────────────────────────────
719+
720+
/**
721+
* When an operation has a {@code mcpInputSchema} parameter containing a valid
722+
* JSON Schema string, that schema is embedded verbatim in the catalog tool entry.
723+
* This is Option 1: explicit declaration in services.xml.
724+
*/
725+
public void testMcpInputSchemaParamOverridesEmptySchema() throws Exception {
726+
AxisService svc = new AxisService("FinancialBenchmarkService");
727+
AxisOperation op = new InOutAxisOperation();
728+
op.setName(QName.valueOf("portfolioVariance"));
729+
op.addParameter(new org.apache.axis2.description.Parameter(
730+
"mcpInputSchema",
731+
"{\"type\":\"object\",\"required\":[\"n_assets\",\"weights\"]," +
732+
"\"properties\":{\"n_assets\":{\"type\":\"integer\"}," +
733+
"\"weights\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}}}"));
734+
svc.addOperation(op);
735+
axisConfig.addService(svc);
736+
737+
JsonNode schema = getCatalogTools().get(0).path("inputSchema");
738+
assertEquals("type must be 'object'", "object", schema.path("type").asText());
739+
assertFalse("properties must be present from mcpInputSchema",
740+
schema.path("properties").isMissingNode());
741+
assertFalse("n_assets property must be present",
742+
schema.path("properties").path("n_assets").isMissingNode());
743+
assertEquals("n_assets must be integer type",
744+
"integer", schema.path("properties").path("n_assets").path("type").asText());
745+
}
746+
747+
/**
748+
* The required array from the mcpInputSchema parameter must be preserved
749+
* exactly — not replaced with an empty array.
750+
*/
751+
public void testMcpInputSchemaRequiredArrayPreserved() throws Exception {
752+
AxisService svc = new AxisService("FinancialBenchmarkService");
753+
AxisOperation op = new InOutAxisOperation();
754+
op.setName(QName.valueOf("monteCarlo"));
755+
op.addParameter(new org.apache.axis2.description.Parameter(
756+
"mcpInputSchema",
757+
"{\"type\":\"object\",\"required\":[\"n_simulations\",\"n_periods\"]," +
758+
"\"properties\":{\"n_simulations\":{\"type\":\"integer\"}," +
759+
"\"n_periods\":{\"type\":\"integer\"}}}"));
760+
svc.addOperation(op);
761+
axisConfig.addService(svc);
762+
763+
JsonNode required = getCatalogTools().get(0).path("inputSchema").path("required");
764+
assertTrue("required must be an array", required.isArray());
765+
assertEquals("required must have 2 entries", 2, required.size());
766+
// Collect required field names
767+
java.util.Set<String> reqFields = new java.util.HashSet<>();
768+
for (JsonNode r : required) reqFields.add(r.asText());
769+
assertTrue("n_simulations must be required", reqFields.contains("n_simulations"));
770+
assertTrue("n_periods must be required", reqFields.contains("n_periods"));
771+
}
772+
773+
/**
774+
* mcpInputSchema set at service level applies to all operations in the service
775+
* that do not have their own operation-level override.
776+
*/
777+
public void testServiceLevelMcpInputSchemaAppliesWhenNoOperationLevel() throws Exception {
778+
AxisService svc = new AxisService("MetadataService");
779+
svc.addParameter(new org.apache.axis2.description.Parameter(
780+
"mcpInputSchema",
781+
"{\"type\":\"object\",\"properties\":{\"request_id\":{\"type\":\"string\"}}}"));
782+
AxisOperation op = new InOutAxisOperation();
783+
op.setName(QName.valueOf("metadata"));
784+
svc.addOperation(op);
785+
axisConfig.addService(svc);
786+
787+
JsonNode schema = getCatalogTools().get(0).path("inputSchema");
788+
assertFalse("request_id property must come from service-level mcpInputSchema",
789+
schema.path("properties").path("request_id").isMissingNode());
790+
}
791+
792+
/**
793+
* Operation-level mcpInputSchema takes precedence over a service-level one.
794+
*/
795+
public void testOperationLevelMcpInputSchemaTakesPrecedenceOverServiceLevel() throws Exception {
796+
AxisService svc = new AxisService("SomeService");
797+
svc.addParameter(new org.apache.axis2.description.Parameter(
798+
"mcpInputSchema",
799+
"{\"type\":\"object\",\"properties\":{\"service_field\":{\"type\":\"string\"}}}"));
800+
AxisOperation op = new InOutAxisOperation();
801+
op.setName(QName.valueOf("specificOp"));
802+
op.addParameter(new org.apache.axis2.description.Parameter(
803+
"mcpInputSchema",
804+
"{\"type\":\"object\",\"properties\":{\"op_field\":{\"type\":\"integer\"}}}"));
805+
svc.addOperation(op);
806+
axisConfig.addService(svc);
807+
808+
JsonNode props = getCatalogTools().get(0).path("inputSchema").path("properties");
809+
assertFalse("op_field from operation-level schema must be present",
810+
props.path("op_field").isMissingNode());
811+
assertTrue("service_field must not be present when operation-level overrides",
812+
props.path("service_field").isMissingNode());
813+
}
814+
815+
/**
816+
* When mcpInputSchema contains invalid JSON, the generator must log a warning
817+
* and fall back to the empty schema — never throw or produce invalid JSON.
818+
*/
819+
public void testInvalidMcpInputSchemaFallsBackToEmptySchema() throws Exception {
820+
AxisService svc = new AxisService("BrokenService");
821+
AxisOperation op = new InOutAxisOperation();
822+
op.setName(QName.valueOf("brokenOp"));
823+
op.addParameter(new org.apache.axis2.description.Parameter(
824+
"mcpInputSchema", "NOT_VALID_JSON{{"));
825+
svc.addOperation(op);
826+
axisConfig.addService(svc);
827+
828+
// Must not throw — output must still be valid JSON
829+
String json = generator.generateMcpCatalogJson(mockRequest);
830+
JsonNode root = MAPPER.readTree(json);
831+
assertNotNull("Output must still be valid JSON after mcpInputSchema parse failure", root);
832+
833+
JsonNode schema = root.path("tools").get(0).path("inputSchema");
834+
assertEquals("Fallback schema must have type=object", "object",
835+
schema.path("type").asText());
836+
assertFalse("Fallback schema must still have properties",
837+
schema.path("properties").isMissingNode());
838+
}
839+
840+
/**
841+
* When no mcpInputSchema parameter is set, the catalog emits the baseline
842+
* empty schema — preserving backward compatibility for all existing services.
843+
*/
844+
public void testAbsentMcpInputSchemaProducesEmptyBaselineSchema() throws Exception {
845+
addService("LegacyService", "legacyOp");
846+
847+
JsonNode schema = getCatalogTools().get(0).path("inputSchema");
848+
assertEquals("Absent mcpInputSchema must produce type=object", "object",
849+
schema.path("type").asText());
850+
assertTrue("Baseline properties must be an empty object",
851+
schema.path("properties").isObject());
852+
assertEquals("Baseline properties must be empty", 0,
853+
schema.path("properties").size());
854+
assertTrue("Baseline required must be an empty array",
855+
schema.path("required").isArray());
856+
assertEquals("Baseline required must be empty", 0,
857+
schema.path("required").size());
858+
}
859+
718860
// ── tool list mirrors existing OpenAPI paths ──────────────────────────────
719861

720862
/**

0 commit comments

Comments
 (0)