You are viewing a plain text version of this content. The canonical link for it is here.
Posted to c-commits@axis.apache.org by ro...@apache.org on 2026/04/07 20:44:10 UTC
(axis-axis2-c-core) branch master updated (dd7ebbd8a -> 697530355)
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
from dd7ebbd8a Release: document core-staging workflow matching Axis2/Java pattern
new 32a5a5ca6 Implement scenarioAnalysis and fix benchmark docs
new 2e93124f2 Fix axutil_hash key aliasing bug in scenarioAnalysis
new 822e34b7d Add probability-sum validation to scenarioAnalysis
new ecc9b4751 Add array dimension validation to portfolioVariance
new a507465ab Address Gemini code review findings
new 99d9b9e0b Add MCP development plan for Axis2/C
new 908d2101b financial-benchmark: improve API for Python quant developers
new aa16a21e8 docs: update MCP.md — Java A1/A2/A3 complete, C1 unblocked
new b4f4beb47 financial-benchmark: apply Gemini 2.5 Pro code review fixes
new 7ad47e26b financial-benchmark: C1 — MCP stdio transport (JSON-RPC 2.0)
new 8e22f33b9 docs: update MCP.md — C1 stdio complete
new 050a4b15b financial-benchmark: apply Gemini review fixes to C1 MCP transport
new 96391b602 financial-benchmark: B1 — mcpInputSchema + MCP annotations for all 5 operations
new 5f799e551 financial-benchmark: D1 — MCP catalog HTTP handler (mcp_catalog_handler.c)
new 322173557 financial-benchmark: address Gemini review findings on services.xml schemas
new 98b59588a D1 MCP catalog intercept wired into financial_benchmark_service_handler
new 0ca7d8d03 D2 secure JSON fault responses with /dev/urandom correlation IDs
new dd7438516 D2 fix: eliminate static-string cast in axis2_json_secure_fault
new 3915982cc Apply Gemini review findings — C mcp_catalog_handler + D1 handler wiring
new 9a9f53001 Apply Gemini D2 review findings
new 697530355 Fix mod_h2 vs mod_http2 naming in Ubuntu HTTP/2 diagnostic + build script
The 21 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
docs/MCP.md | 356 +++++++++
docs/userguide/json-httpd-h2-userguide.md | 51 +-
.../financial-benchmark-service/Makefile.am | 172 +++--
.../financial-benchmark-service/README.md | 35 +-
.../build_financial_service.sh | 82 ++-
.../financial-benchmark-service/services.xml | 90 +++
.../src/axis2_json_secure_fault.c | 167 +++++
.../src/axis2_json_secure_fault.h | 110 +++
.../src/financial_benchmark_service.c | 798 ++++++++++++++++++++-
.../src/financial_benchmark_service.h | 72 +-
.../src/financial_benchmark_service_handler.c | 72 +-
.../financial-benchmark-service/src/finbench_mcp.c | 554 ++++++++++++++
.../financial-benchmark-service/src/finbench_mcp.h | 78 ++
.../src/finbench_mcp_main.c | 83 +++
.../src/mcp_catalog_handler.c | 473 ++++++++++++
.../src/mcp_catalog_handler.h | 139 ++++
16 files changed, 3178 insertions(+), 154 deletions(-)
create mode 100644 docs/MCP.md
create mode 100644 samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
create mode 100644 samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h
create mode 100644 samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
create mode 100644 samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
create mode 100644 samples/user_guide/financial-benchmark-service/src/finbench_mcp_main.c
create mode 100644 samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
create mode 100644 samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.h
(axis-axis2-c-core) 12/21: financial-benchmark: apply Gemini review fixes to C1 MCP transport
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 050a4b15b58d4da73ed86a4e4e0629e4c64ba2e9
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 14:27:37 2026 -1000
financial-benchmark: apply Gemini review fixes to C1 MCP transport
CRITICAL — Mixed allocator usage:
Replace getline()/free() with a custom mcp_read_line() helper that uses
AXIS2_MALLOC/AXIS2_FREE exclusively. getline() uses stdlib malloc which
is undefined behavior when Axis2/C uses a custom pool or debug allocator.
Removed now-unused <stdlib.h> include.
HIGH — Unbounded memory / DoS:
mcp_read_line() enforces MAX_MCP_REQUEST_BYTES (16 MB) per line. Oversized
input is drained and returns a JSON-RPC error instead of exhausting RAM.
(16 MB accommodates the largest portfolioVariance requests: 500-asset
covariance matrix ~ 2 MB, with headroom for future growth.)
MEDIUM — Fragile tool catalog loop:
mcp_handle_tools_list() now iterates to the NULL sentinel instead of
using the FINBENCH_MCP_N_TOOLS macro. Adding a tool to the array is
sufficient — no separate count to keep in sync.
LOW — Fragile json_object_to_json_string usage:
mcp_handle_tools_call() now axutil_strdup()s the args JSON string before
passing it to finbench_*_json_only(). json_object_to_json_string() returns
a pointer to json-c's internal buffer, valid only until the next call on
any object. The stable copy prevents silent corruption if a future log
statement calls the same function. AXIS2_FREE()d on all exit paths.
LOW — Remove FINBENCH_MCP_N_TOOLS magic number:
Replaced with MAX_MCP_REQUEST_BYTES and MCP_LINE_INITIAL_CAP constants
(both needed for the CRITICAL fix). The loop no longer needs the count.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/src/finbench_mcp.c | 124 +++++++++++++++++----
1 file changed, 103 insertions(+), 21 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
index 6044b2a75..486c35b1a 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -37,7 +37,6 @@
#include <json-c/json.h>
#include <axutil_string.h>
#include <stdio.h>
-#include <stdlib.h>
#include <string.h>
/* ============================================================================
@@ -189,7 +188,9 @@ static const finbench_mcp_tool_t finbench_mcp_tools[] = {
{ NULL, NULL, NULL } /* sentinel */
};
-#define FINBENCH_MCP_N_TOOLS 3
+/* Maximum request size — prevents DoS via unbounded memory allocation */
+#define MAX_MCP_REQUEST_BYTES (16 * 1024 * 1024) /* 16 MB per line */
+#define MCP_LINE_INITIAL_CAP 4096
/* ============================================================================
* JSON-RPC 2.0 output helpers
@@ -282,8 +283,9 @@ static json_object *mcp_handle_tools_list(void)
{
json_object *tools_array = json_object_new_array();
- for (int i = 0; i < FINBENCH_MCP_N_TOOLS; i++) {
- const finbench_mcp_tool_t *tool = &finbench_mcp_tools[i];
+ /* Iterate to NULL sentinel — no hardcoded count needed */
+ for (const finbench_mcp_tool_t *tool = finbench_mcp_tools;
+ tool->name != NULL; tool++) {
json_object *tool_obj = json_object_new_object();
json_object_object_add(tool_obj, "name",
@@ -342,12 +344,21 @@ static json_object *mcp_handle_tools_call(
}
const char *tool_name = json_object_get_string(name_obj);
- /* Extract arguments — optional; default to empty object */
+ /* Extract arguments — optional; default to empty object.
+ * axutil_strdup gives a stable copy independent of json-c's internal buffer
+ * (json_object_to_json_string returns a pointer that is only valid until
+ * the next call to that function on *any* object). */
json_object *args_obj = NULL;
json_object_object_get_ex(params, "arguments", &args_obj);
- const char *args_json = args_obj
- ? json_object_to_json_string(args_obj) /* owned by args_obj, valid while args_obj lives */
+ const char *args_json_tmp = args_obj
+ ? json_object_to_json_string(args_obj)
: "{}";
+ axis2_char_t *args_json = axutil_strdup(env, args_json_tmp);
+ if (!args_json) {
+ *out_code = MCP_ERR_INTERNAL_ERROR;
+ *out_msg = "Failed to allocate memory for arguments";
+ return NULL;
+ }
/* Dispatch to the matching finbench_*_json_only() function.
* Each returns an axutil_strdup()'d string — free with AXIS2_FREE(). */
@@ -360,11 +371,14 @@ static json_object *mcp_handle_tools_call(
} else if (strcmp(tool_name, "scenarioAnalysis") == 0) {
result_json = finbench_scenario_json_only(env, args_json);
} else {
+ AXIS2_FREE(env->allocator, args_json);
*out_code = MCP_ERR_METHOD_NOT_FOUND;
*out_msg = "Unknown tool name. Available: portfolioVariance, monteCarlo, scenarioAnalysis";
return NULL;
}
+ AXIS2_FREE(env->allocator, args_json);
+
if (!result_json) {
*out_code = MCP_ERR_INTERNAL_ERROR;
*out_msg = "Operation returned a null response";
@@ -392,27 +406,95 @@ static json_object *mcp_handle_tools_call(
* ============================================================================
*/
-void finbench_run_mcp_stdio(const axutil_env_t *env)
+/*
+ * Read one newline-terminated line from stdin into a buffer managed by the
+ * Axis2/C allocator. Grows exponentially up to MAX_MCP_REQUEST_BYTES.
+ *
+ * Returns AXIS2_TRUE and sets *out_buf / *out_len on a complete line.
+ * Returns AXIS2_FALSE on EOF or allocation failure.
+ * On oversized input (> MAX_MCP_REQUEST_BYTES), drains the line, writes a
+ * JSON-RPC error to stdout, and returns AXIS2_FALSE with *out_len == 0.
+ */
+static axis2_bool_t
+mcp_read_line(const axutil_env_t *env,
+ axis2_char_t **buf_inout,
+ size_t *cap_inout,
+ size_t *out_len)
{
- char *line = NULL;
- size_t line_cap = 0;
- ssize_t line_len;
+ axis2_char_t *buf = *buf_inout;
+ size_t cap = *cap_inout;
+ size_t len = 0;
+ int c;
+
+ while ((c = getchar()) != EOF) {
+ if (c == '\n') {
+ /* Strip trailing CR */
+ if (len > 0 && buf[len - 1] == '\r') len--;
+ buf[len] = '\0';
+ *out_len = len;
+ return AXIS2_TRUE;
+ }
+
+ /* Size guard — prevents DoS via unbounded allocation */
+ if (len >= MAX_MCP_REQUEST_BYTES) {
+ /* Drain remainder of the oversized line */
+ while ((c = getchar()) != EOF && c != '\n') {}
+ mcp_write_error(NULL, MCP_ERR_INVALID_REQUEST,
+ "Request exceeds maximum size (16 MB)");
+ *out_len = 0;
+ return AXIS2_FALSE;
+ }
+ /* Grow buffer if needed (doubles each time) */
+ if (len + 1 >= cap) {
+ size_t new_cap = cap * 2;
+ axis2_char_t *new_buf = AXIS2_MALLOC(env->allocator, new_cap);
+ if (!new_buf) {
+ mcp_write_error(NULL, MCP_ERR_INTERNAL_ERROR,
+ "Failed to grow line buffer");
+ while ((c = getchar()) != EOF && c != '\n') {}
+ *out_len = 0;
+ return AXIS2_FALSE;
+ }
+ memcpy(new_buf, buf, len);
+ AXIS2_FREE(env->allocator, buf);
+ buf = new_buf;
+ cap = new_cap;
+ *buf_inout = buf;
+ *cap_inout = cap;
+ }
+
+ buf[len++] = (axis2_char_t)c;
+ }
+
+ /* EOF without newline — treat as end of session */
+ *out_len = 0;
+ return AXIS2_FALSE;
+}
+
+void finbench_run_mcp_stdio(const axutil_env_t *env)
+{
/*
- * getline() allocates dynamically — handles large portfolioVariance requests
- * (e.g. 500-asset covariance matrix ~ 2 MB) without a fixed buffer limit.
+ * Line buffer managed by the Axis2/C allocator — avoids mixing stdlib
+ * malloc/free with any custom allocator Axis2/C may be configured to use.
+ * Initial capacity 4 KB; grows up to MAX_MCP_REQUEST_BYTES (16 MB) to
+ * handle large portfolioVariance requests (500-asset matrix ~ 2 MB).
*/
- while ((line_len = getline(&line, &line_cap, stdin)) > 0) {
+ size_t line_cap = MCP_LINE_INITIAL_CAP;
+ axis2_char_t *line_buf = AXIS2_MALLOC(env->allocator, line_cap);
+ size_t line_len = 0;
+
+ if (!line_buf) {
+ mcp_write_error(NULL, MCP_ERR_INTERNAL_ERROR,
+ "Failed to allocate initial line buffer");
+ return;
+ }
- /* Strip trailing CR/LF */
- while (line_len > 0
- && (line[line_len - 1] == '\n' || line[line_len - 1] == '\r')) {
- line[--line_len] = '\0';
- }
+ while (mcp_read_line(env, &line_buf, &line_cap, &line_len)) {
if (line_len == 0) continue;
/* ── Parse JSON-RPC 2.0 request ─────────────────────────────── */
- json_object *req = json_tokener_parse(line);
+ json_object *req = json_tokener_parse(line_buf);
if (!req) {
/* id is unknown on parse failure — use null per spec */
mcp_write_error(NULL, MCP_ERR_PARSE_ERROR, "Parse error: invalid JSON");
@@ -468,5 +550,5 @@ void finbench_run_mcp_stdio(const axutil_env_t *env)
json_object_put(req); /* release request + all its members */
}
- free(line);
+ AXIS2_FREE(env->allocator, line_buf);
}
(axis-axis2-c-core) 15/21: financial-benchmark: address Gemini review findings on services.xml schemas
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 3221735572074efb8d05444d6e15fac50997205c
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 06:15:19 2026 -1000
financial-benchmark: address Gemini review findings on services.xml schemas
F1 (Major): generateTestData mcpIdempotent corrected to true — pure
deterministic function, same input always produces same output
F2 (Major): scenarioAnalysis assets array gains minItems:1, maxItems:2000;
scenario_prices and probabilities gain minItems:2
F3 (Major): scenario_prices and probabilities descriptions now explicitly
state they must have equal length (cross-property constraint)
F4 (Major): weights description corrected — states exact n_assets length
required and clarifies normalize_weights=false means caller ensures sum=1
F5 (Major): normalize_weights description fixed — previously implied
unconditional normalisation contradicting the boolean flag semantics
F6 (Minor): metadata mcpInputSchema gains additionalProperties:false since
the operation accepts no body fields
F7 (Minor): monteCarlo mcpIdempotent changed to false — PRNG seeded from
time() when random_seed absent, so unseeded calls are non-deterministic;
comment documents when idempotency holds
F10 (Nit): scenarioAnalysis position_size type changed integer→number with
description noting fractional positions unsupported by C implementation
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/services.xml | 33 +++++++++++++---------
1 file changed, 20 insertions(+), 13 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/services.xml b/samples/user_guide/financial-benchmark-service/services.xml
index 30d671b01..78cebc191 100644
--- a/samples/user_guide/financial-benchmark-service/services.xml
+++ b/samples/user_guide/financial-benchmark-service/services.xml
@@ -88,11 +88,11 @@
"n_assets": {"type": "integer", "minimum": 2, "maximum": 2000,
"description": "Number of assets in the portfolio"},
"weights": {"type": "array", "items": {"type": "number"},
- "description": "Portfolio weights, one per asset (will be normalised to sum=1)"},
+ "description": "Exactly n_assets values. If normalize_weights=true they are scaled to sum=1 before calculation; otherwise caller must ensure they already sum to 1."},
"covariance_matrix": {"type": "array", "items": {"type": "number"},
- "description": "Flattened n×n covariance matrix (row-major)"},
+ "description": "Exactly n_assets×n_assets values, row-major order (element [i*n+j] is cov(i,j))"},
"normalize_weights": {"type": "boolean", "default": false,
- "description": "Normalise weights before calculation"},
+ "description": "Scale weights to sum=1 before calculation. Default: false — caller must supply weights that already sum to 1."},
"request_id": {"type": "string", "description": "Optional correlation ID echoed in the response"}
}
}</parameter>
@@ -146,7 +146,10 @@
<operation name="monteCarlo">
<parameter name="mcpDescription">Run a Monte Carlo VaR simulation using Geometric Brownian Motion. Compute-intensive: 10 000 sims × 252 periods = 2.52M iterations. Returns VaR 95/99, CVaR, max drawdown, and probability of profit.</parameter>
<parameter name="mcpReadOnly">true</parameter>
- <parameter name="mcpIdempotent">true</parameter>
+ <!-- mcpIdempotent=false: without random_seed the PRNG is seeded from
+ time(), so repeated identical requests produce different VaR results.
+ Idempotency holds only when random_seed is explicitly supplied. -->
+ <parameter name="mcpIdempotent">false</parameter>
<parameter name="mcpInputSchema">{
"type": "object",
"required": ["n_simulations", "n_periods", "initial_value", "expected_return", "volatility"],
@@ -158,7 +161,7 @@
"initial_value": {"type": "number", "description": "Initial portfolio value"},
"expected_return": {"type": "number", "description": "Annualised expected return (e.g. 0.08 for 8%)"},
"volatility": {"type": "number", "description": "Annualised volatility (e.g. 0.20 for 20%)"},
- "random_seed": {"type": "integer", "description": "Optional seed for reproducible results"},
+ "random_seed": {"type": "integer", "description": "Seed for reproducible results. When omitted, seeded from time() — results differ across calls."},
"request_id": {"type": "string", "description": "Optional correlation ID"}
}
}</parameter>
@@ -223,18 +226,19 @@
"n_assets": {"type": "integer", "minimum": 1, "maximum": 2000,
"description": "Number of assets"},
"assets": {"type": "array",
- "description": "Array of asset scenario objects",
+ "minItems": 1, "maxItems": 2000,
+ "description": "Array of asset scenario objects. Length must equal n_assets.",
"items": {
"type": "object",
"required": ["asset_id", "current_price", "scenario_prices", "probabilities", "position_size"],
"properties": {
"asset_id": {"type": "integer"},
"current_price": {"type": "number"},
- "scenario_prices":{"type": "array", "items": {"type": "number"},
- "description": "Price in each scenario"},
- "probabilities": {"type": "array", "items": {"type": "number"},
- "description": "Probability weights (must sum to 1.0)"},
- "position_size": {"type": "integer", "description": "Number of shares held"}
+ "scenario_prices":{"type": "array", "minItems": 2, "items": {"type": "number"},
+ "description": "Price in each scenario. Must have equal length to probabilities."},
+ "probabilities": {"type": "array", "minItems": 2, "items": {"type": "number"},
+ "description": "Probability weight per scenario (must sum to 1.0). Must have equal length to scenario_prices."},
+ "position_size": {"type": "number", "description": "Shares held (integer positions; fractional positions not supported by C implementation)"}
}
}},
"use_hash_lookup": {"type": "boolean", "default": true,
@@ -284,7 +288,8 @@
<parameter name="mcpIdempotent">true</parameter>
<parameter name="mcpInputSchema">{
"type": "object",
- "properties": {}
+ "properties": {},
+ "additionalProperties": false
}</parameter>
<description>
Get service metadata, capabilities, and current device statistics.
@@ -324,7 +329,9 @@
<operation name="generateTestData">
<parameter name="mcpDescription">Generate a synthetic equal-weighted portfolio with a positive-semi-definite covariance matrix. Returns a ready-to-use portfolioVariance request body.</parameter>
<parameter name="mcpReadOnly">true</parameter>
- <parameter name="mcpIdempotent">false</parameter>
+ <!-- mcpIdempotent=true: deterministic — same n_assets always produces
+ the same equal-weighted PSD covariance matrix, no random state. -->
+ <parameter name="mcpIdempotent">true</parameter>
<parameter name="mcpInputSchema">{
"type": "object",
"required": ["n_assets"],
(axis-axis2-c-core) 05/21: Address Gemini code review findings
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit a507465ab6447b8aa1c07feff12f7094f365868a
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 07:03:20 2026 -1000
Address Gemini code review findings
Fix MEDIUM bugs:
- portfolioVariance: weights_provided == 0 (missing "weights" key) was
bypassing dimension validation via the "> 0" guard, silently returning
variance=0 as SUCCESS. Now explicitly rejects missing weights and
missing covariance_matrix with descriptive errors — same guard applied
to both required fields.
Fix LOW issues:
- Add explicit #include <inttypes.h> for PRId64; previously worked via
transitive inclusion on glibc/NDK but non-conforming per C99 standard.
- scenarioAnalysis: log AXIS2_LOG_ERROR when hash_keys allocation fails
so silent benchmark degradation is visible in the service log rather
than producing unexplained zero hash timing in the response.
- Fix comment direction error: tolerance 1e-4 is "looser than machine
epsilon" not "tighter than financial rounding error (~1e-6)".
Compiles clean under -Wall -Wextra.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/financial_benchmark_service.c | 46 +++++++++++++++++-----
1 file changed, 36 insertions(+), 10 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index f07a08487..d5cdaa9dd 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -31,6 +31,7 @@
#include "financial_benchmark_service.h"
#include <axutil_string.h>
#include <axutil_utils.h>
+#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -380,12 +381,22 @@ finbench_calculate_portfolio_variance(
/* -----------------------------------------------------------------------
* Dimension validation — the kind of check a Python quant will probe
- * immediately. Mismatched arrays silently produce wrong variance if not
- * caught here: n_assets=500 with 100 weights zero-fills the rest, giving
- * a mathematically meaningless result with no indication of bad input.
+ * immediately. Mismatched or absent arrays silently produce wrong variance
+ * if not caught here: n_assets=500 with 100 weights zero-fills the rest,
+ * giving a mathematically meaningless result with no indication of bad
+ * input. weights_provided == 0 means the "weights" key was absent — that
+ * is also an error, not a license to compute variance with all-zero weights.
* ----------------------------------------------------------------------- */
- if (request->weights_provided > 0 &&
- request->weights_provided != n) {
+ if (request->weights_provided == 0) {
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env,
+ "Missing required field: \"weights\" array (n_assets elements summing to 1.0).");
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench portfolioVariance: weights array missing from request");
+ return response;
+ }
+
+ if (request->weights_provided != n) {
char err_buf[256];
snprintf(err_buf, sizeof(err_buf),
"weights array length %d != n_assets %d. "
@@ -398,8 +409,17 @@ finbench_calculate_portfolio_variance(
return response;
}
- if (request->matrix_elements_provided > 0 &&
- request->matrix_elements_provided != n * n) {
+ if (request->matrix_elements_provided == 0) {
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env,
+ "Missing required field: \"covariance_matrix\" "
+ "(n_assets*n_assets elements as flat array or n_assets x n_assets 2D array).");
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench portfolioVariance: covariance_matrix missing from request");
+ return response;
+ }
+
+ if (request->matrix_elements_provided != n * n) {
char err_buf[256];
snprintf(err_buf, sizeof(err_buf),
"covariance_matrix element count %d != n_assets² (%d×%d=%d). "
@@ -1108,9 +1128,10 @@ finbench_calculate_scenarios(
* downstream portfolio decisions without any indication that the input
* data was bad.
*
- * Tolerance: 1e-4 (0.01%). Tighter than typical financial rounding error
- * (~1e-6) but looser than machine epsilon to accommodate legitimate
- * floating-point representation differences in JSON-encoded probabilities.
+ * Tolerance: 1e-4 (0.01%). Looser than machine epsilon and typical
+ * floating-point rounding (~1e-6) to accommodate legitimate representation
+ * differences in JSON-encoded probabilities, but tight enough to catch
+ * missing or miscounted scenarios (e.g., three scenarios summing to 0.85).
* ----------------------------------------------------------------------- */
#define FINBENCH_PROB_SUM_TOLERANCE 1e-4
for (i = 0; i < request->n_assets; i++) {
@@ -1211,6 +1232,11 @@ finbench_calculate_scenarios(
request->n_assets * sizeof(char *));
if (hash_keys) {
for (i = 0; i < request->n_assets; i++) hash_keys[i] = NULL;
+ } else {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench scenarioAnalysis: hash_keys alloc failed (%d assets) — "
+ "hash benchmark will be skipped; only linear search timing reported",
+ request->n_assets);
}
hash_build_start = get_time_us();
(axis-axis2-c-core) 08/21: docs: update MCP.md — Java A1/A2/A3 complete, C1 unblocked
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit aa16a21e87db21bdd48810cd1dbca5fd4763d01c
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 13:26:37 2026 -1000
docs: update MCP.md — Java A1/A2/A3 complete, C1 unblocked
Reflects completed Java milestones:
- A1 (/openapi-mcp.json endpoint) ✅
- A2 (axis2-mcp-bridge stdio JAR + mTLS port 8443) ✅
- A3 (end-to-end: Claude Desktop → bridge → mTLS → BigDataH2Service) ✅
C1 is now unblocked. The JSON-RPC 2.0 handshake is validated against
a real client; the C port translates proven behavior. Updated the C1
"When" from "after A2 is validated" to "Now — A2 is validated."
Added note that neither implementation needs an MCP SDK — just
json-c (C) or Jackson + stdlib HttpClient (Java).
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
docs/MCP.md | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/docs/MCP.md b/docs/MCP.md
index 447927a81..98b18951b 100644
--- a/docs/MCP.md
+++ b/docs/MCP.md
@@ -1,7 +1,8 @@
---
type: architecture
created: 2026-04-06
-status: Active — Java implementation first, C port follows
+last-verified: 2026-04-06
+status: Active — Java A1/A2/A3 complete, C1 now unblocked
---
# MCP Support for Apache Axis2/C
@@ -86,17 +87,26 @@ Android phone.
|----------|--------|
| `axis2-openapi` module | ✅ Serves `/openapi.json`, `/openapi.yaml`, `/swagger-ui` |
| `springbootdemo-tomcat11` | ✅ Reference implementation, Java 25 + Tomcat 11 |
-| `/openapi-mcp.json` endpoint | 🔄 In progress (A1) |
-| `axis2-mcp-bridge` stdio JAR | ❌ Not started (A2) |
+| `/openapi-mcp.json` endpoint | ✅ Done (A1) |
+| `axis2-mcp-bridge` stdio JAR | ✅ Done (A2) — with mTLS (port 8443, IoT CA) |
+| A3 end-to-end validation | ✅ Done — Claude Desktop → bridge → mTLS → BigDataH2Service confirmed |
| `axis2-transport-mcp` Java native | ❌ Not started (B1) |
+**C1 is now unblocked.** Java A2 was the gate: the JSON-RPC 2.0 handshake
+(`initialize` / `tools/list` / `tools/call`) is validated against Claude Desktop.
+The C port translates proven behavior rather than discovering it.
+
+The Java bridge uses no MCP SDK — just Jackson + Java stdlib HttpClient. The C port
+similarly needs no library beyond json-c, which is already in use throughout
+`financial_benchmark_service.c`.
+
---
## Track C — C Implementation Plan
### C1: stdio MCP Mode in `financial_benchmark_service.c`
-**When**: After Java A2 (`axis2-mcp-bridge` stdio JAR) is validated against Claude Desktop.
+**When**: Now — Java A2 is validated. C1 is unblocked.
**How it works**: The binary detects whether stdin is a pipe (MCP client attached) or
a tty (interactive/httpd mode) and switches behavior accordingly:
@@ -294,13 +304,13 @@ service registry already uses for WSDL.
## Relationship to Java Reference Implementation
```
-Java A1 (/openapi-mcp.json) ← defines MCP tool schema format
+Java A1 (/openapi-mcp.json) ✅ done — defines MCP tool schema format
↓
-Java A2 (axis2-mcp-bridge stdio) ← validates JSON-RPC 2.0 handshake
+Java A2 (axis2-mcp-bridge stdio) ✅ done — validates JSON-RPC 2.0 handshake
↓
Java B1 (axis2-transport-mcp) ← proves native transport pattern
↓
-C1 (stdio in financial-benchmark) ← port: same protocol, same schemas, C dispatch
+C1 (stdio in financial-benchmark) ← NEXT — port: same protocol, same schemas, C dispatch
↓
C2 (HTTP/SSE on Penguin) ← port: same as Java A4/B2
↓
(axis-axis2-c-core) 11/21: docs: update MCP.md — C1 stdio complete
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 8e22f33b924d13960d1a918e0686aefdf22ecd8f
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 14:11:00 2026 -1000
docs: update MCP.md — C1 stdio complete
Mark C1 as done: finbench_mcp.c + finbench_mcp_main.c implemented.
Update next-steps to point at C2 (HTTP/SSE) and Java B1 (transport-mcp).
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
docs/MCP.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/MCP.md b/docs/MCP.md
index 98b18951b..ea69747c8 100644
--- a/docs/MCP.md
+++ b/docs/MCP.md
@@ -2,7 +2,7 @@
type: architecture
created: 2026-04-06
last-verified: 2026-04-06
-status: Active — Java A1/A2/A3 complete, C1 now unblocked
+status: Active — Java A1/A2/A3 complete, C1 complete
---
# MCP Support for Apache Axis2/C
@@ -78,7 +78,7 @@ Android phone.
| Service dispatch table (`route_operation()`) | ✅ Reusable for MCP `tools/call` |
| Apache httpd handler | ✅ Running, mod_axis2 loaded |
| `axis2-transport-mcp` C module | ❌ Not started |
-| stdio MCP mode | ❌ Not started |
+| stdio MCP mode | ✅ Done (C1) — finbench_mcp.c + finbench_mcp_main.c |
| HTTP/SSE MCP endpoint | ❌ Not started |
### What exists in Axis2/Java (the reference)
@@ -106,7 +106,7 @@ similarly needs no library beyond json-c, which is already in use throughout
### C1: stdio MCP Mode in `financial_benchmark_service.c`
-**When**: Now — Java A2 is validated. C1 is unblocked.
+**When**: ✅ Done.
**How it works**: The binary detects whether stdin is a pipe (MCP client attached) or
a tty (interactive/httpd mode) and switches behavior accordingly:
@@ -310,7 +310,7 @@ Java A2 (axis2-mcp-bridge stdio) ✅ done — validates JSON-RPC 2.0 handshake
↓
Java B1 (axis2-transport-mcp) ← proves native transport pattern
↓
-C1 (stdio in financial-benchmark) ← NEXT — port: same protocol, same schemas, C dispatch
+C1 (stdio in financial-benchmark) ✅ done — finbench_mcp.c, same protocol, static tool catalog
↓
C2 (HTTP/SSE on Penguin) ← port: same as Java A4/B2
↓
(axis-axis2-c-core) 21/21: Fix mod_h2 vs mod_http2 naming in Ubuntu HTTP/2 diagnostic + build script
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 697530355a72c5d35c5e3e665d4c26a7c9ed69dc
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 09:54:26 2026 -1000
Fix mod_h2 vs mod_http2 naming in Ubuntu HTTP/2 diagnostic + build script
docs/userguide/json-httpd-h2-userguide.md:
- Diagnostic script checked for mod_h2.so but Ubuntu 22.04+ ships
mod_http2.so — caused false "compile from source" recommendation on
systems that already have HTTP/2 available
- Fix Tests 2/3/recommendation to check both mod_http2.so and mod_h2.so
- a2enmod call updated: try 'http2' first (Ubuntu 22.04 name), fall back
to 'h2' (older distros)
- IfModule guard changed from mod_h2.c to mod_http2.c in site config
- Verified platform updated: Ubuntu 22.04 LTS / Apache 2.4.52
- "When to use source compilation" note no longer cites Ubuntu 25.10
build_financial_service.sh:
- Add missing source files to gcc invocation:
financial_benchmark_service_handler.c (D1 HTTP entry points)
mcp_catalog_handler.c (D1 MCP catalog endpoint)
axis2_json_secure_fault.c (D2 secure error responses)
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
docs/userguide/json-httpd-h2-userguide.md | 50 +++++++++++++++-------
.../build_financial_service.sh | 9 ++++
2 files changed, 43 insertions(+), 16 deletions(-)
diff --git a/docs/userguide/json-httpd-h2-userguide.md b/docs/userguide/json-httpd-h2-userguide.md
index 423ce0cef..bed96e136 100644
--- a/docs/userguide/json-httpd-h2-userguide.md
+++ b/docs/userguide/json-httpd-h2-userguide.md
@@ -8,7 +8,7 @@ This guide will help you get started with Axis2/C HTTP/2 transport and JSON via
**New in Axis2/C 2.0**: Complete HTTP/2 transport implementation with enterprise capabilities, streaming optimization for large JSON payloads, authentication services, XSS protection demonstrations, and production-ready performance enhancements using Apache httpd as the application server.
-**✅ Verified Testing**: This guide has been tested and verified to work on **Ubuntu 25.10** with Apache httpd and mod_h2 support.
+**✅ Verified Testing**: This guide has been tested and verified to work on **Ubuntu 22.04 LTS** with system Apache 2.4.52 (`mod_http2.so`).
**🔧 Critical Configuration**: HTTPS Virtual Hosts **MUST** include `<Location /services>` directive inside `<VirtualHost *:443>` blocks. Without this, Axis2/C services will fail with routing errors and no debug messages will appear in logs.
@@ -295,11 +295,22 @@ echo "🔍 Testing Apache HTTP/2 support on your system..."
# Test 1: Check if Apache is installed
{ command -v apache2 >/dev/null 2>&1 || command -v httpd >/dev/null 2>&1; } && echo "✅ Apache installation found" || { echo "❌ No Apache installation found"; echo " Install Apache first: sudo apt install apache2"; return 1 2>/dev/null || echo "Please install Apache before continuing"; }
-# Test 2: Check if mod_h2 module file exists
-{ ls /usr/lib/apache2/modules/mod_h2.so 2>/dev/null || ls /usr/lib*/httpd/modules/mod_h2.so 2>/dev/null; } && { echo "✅ mod_h2.so found - HTTP/2 module is available!"; echo " You can use system Apache (skip source compilation)"; } || echo "❌ mod_h2.so not found - HTTP/2 module missing"
-
-# Test 3: Try enabling mod_h2 (safe test)
-command -v a2enmod >/dev/null 2>&1 && { sudo a2enmod h2 2>/dev/null && { echo "✅ mod_h2 enabled successfully!"; echo " Your system Apache supports HTTP/2"; sudo a2dismod h2 2>/dev/null; } || echo "❌ Cannot enable mod_h2 - module not available"; } || echo "⚠️ a2enmod not available - cannot test module enabling"
+# Test 2: Check if HTTP/2 module file exists
+# Ubuntu 22.04+ ships mod_http2.so (not mod_h2.so — that was the old name)
+{ ls /usr/lib/apache2/modules/mod_http2.so 2>/dev/null || \
+ ls /usr/lib*/apache2/modules/mod_h2.so 2>/dev/null || \
+ ls /usr/lib*/httpd/modules/mod_http2.so 2>/dev/null || \
+ ls /usr/lib*/httpd/modules/mod_h2.so 2>/dev/null; } \
+ && { echo "✅ HTTP/2 module found - HTTP/2 is available!"; echo " You can use system Apache (skip source compilation)"; } \
+ || echo "❌ HTTP/2 module not found - module missing"
+
+# Test 3: Try enabling mod_http2 (safe test)
+# Ubuntu 22.04: module is named http2 (not h2)
+command -v a2enmod >/dev/null 2>&1 && {
+ ( sudo a2enmod http2 2>/dev/null && echo "✅ mod_http2 enabled successfully!" && sudo a2dismod http2 2>/dev/null ) \
+ || ( sudo a2enmod h2 2>/dev/null && echo "✅ mod_h2 enabled successfully!" && sudo a2dismod h2 2>/dev/null ) \
+ || echo "❌ Cannot enable HTTP/2 module - not available"
+} || echo "⚠️ a2enmod not available - cannot test module enabling"
# Test 4: Check what HTTP/2 related modules exist
echo ""
@@ -308,7 +319,12 @@ find /usr/lib*/apache2/modules /usr/lib*/httpd/modules -name "*h2*" -o -name "*h
echo ""
echo "📋 RECOMMENDATION:"
-ls /usr/lib*/apache2/modules/mod_h2.so /usr/lib*/httpd/modules/mod_h2.so 2>/dev/null >/dev/null && { echo "✅ Use system Apache - HTTP/2 support is available"; echo " Continue with Step 1 Option A"; } || { echo "❌ Compile Apache from source - no HTTP/2 support in system package"; echo " Use Step 0 below, then Step 1 Option B"; }
+{ ls /usr/lib*/apache2/modules/mod_http2.so \
+ /usr/lib*/apache2/modules/mod_h2.so \
+ /usr/lib*/httpd/modules/mod_http2.so \
+ /usr/lib*/httpd/modules/mod_h2.so 2>/dev/null | grep -q .; } \
+ && { echo "✅ Use system Apache - HTTP/2 support is available"; echo " Continue with Step 1 Option A"; } \
+ || { echo "❌ Compile Apache from source - no HTTP/2 support in system package"; echo " Use Step 0 below, then Step 1 Option B"; }
```
**Run this test first to save time!**
@@ -318,7 +334,7 @@ ls /usr/lib*/apache2/modules/mod_h2.so /usr/lib*/httpd/modules/mod_h2.so 2>/dev/
## Compiling Apache httpd from Source with HTTP/2 Support
**When to use this approach:**
-- Your distribution lacks mod_h2 support (like Ubuntu 25.10)
+- Your distribution genuinely lacks HTTP/2 module support (check with the diagnostic above first)
- You need the latest Apache features
- You want full control over Apache compilation options
- You're working with Apache projects and expect to overcome such hurdles
@@ -637,10 +653,7 @@ curl -k -I --http2 https://localhost/
```bash
# Create the HTTPS-only configuration file
sudo tee /etc/apache2/sites-available/axis2-services.conf << 'EOF'
-# Load required modules for HTTP/2 + HTTPS
-LoadModule h2_module /usr/lib/apache2/modules/mod_h2.so
-LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
-LoadModule headers_module /usr/lib/apache2/modules/mod_headers.so
+# Load Axis2/C module (mod_http2 + mod_ssl are enabled via a2enmod)
LoadModule axis2_module /usr/local/axis2c/lib/libmod_axis2.so
# Axis2/C Configuration (standard Unix paths)
@@ -649,7 +662,8 @@ Axis2LogFile /var/log/axis2c/axis2.log
Axis2LogLevel info
# HTTP/2 Configuration (requires HTTPS)
-<IfModule mod_h2.c>
+# Ubuntu 22.04 ships mod_http2.so (not mod_h2.so — that was the pre-2.4.x name)
+<IfModule mod_http2.c>
# HTTP/2 performance settings for enterprise big data
H2WindowSize 65536
H2MaxWorkers 256
@@ -719,13 +733,17 @@ EOF
# Enable the site and required modules
sudo a2ensite axis2-services
-# Enable SSL and headers modules (standard modules)
+# Enable SSL and headers modules (standard modules, always available)
sudo a2enmod ssl
sudo a2enmod headers
# Enable HTTP/2 module (REQUIRED for this guide)
-# Try system Apache first, then custom Apache
-command -v a2enmod >/dev/null 2>&1 && { sudo a2enmod h2 2>/dev/null && echo "✅ HTTP/2 module enabled successfully (system Apache)" || { echo "❌ System Apache mod_h2 module not available"; echo ""; echo "🔧 SOLUTIONS:"; echo "1. Use custom-compiled Apache (Step 0 above)"; echo "2. Install from Ondrej's PPA: sudo add-apt-repository ppa:ondrej/apache2"; echo "3. Use Ubuntu 24.04 LTS (has proper mod_h2 support)"; echo ""; echo "❌ SETUP FAILED: Cannot continue without mod_h2"; return 1 2>/dev/ [...]
+# Ubuntu 22.04 uses 'http2' as the a2enmod name (module file is mod_http2.so)
+sudo a2enmod http2 && echo "✅ HTTP/2 module enabled (mod_http2)" \
+ || { echo "❌ Cannot enable HTTP/2 module."; \
+ echo " On Ubuntu 22.04 mod_http2 ships in apache2 package — check:"; \
+ echo " ls /usr/lib/apache2/modules/mod_http2.so"; \
+ echo " If missing, install: sudo apt install apache2"; }
# Create SSL certificates for HTTP/2 testing
# HTTP/2 requires valid TLS certificates - self-signed for development only
diff --git a/samples/user_guide/financial-benchmark-service/build_financial_service.sh b/samples/user_guide/financial-benchmark-service/build_financial_service.sh
index 098f2d3b9..f8e868bba 100755
--- a/samples/user_guide/financial-benchmark-service/build_financial_service.sh
+++ b/samples/user_guide/financial-benchmark-service/build_financial_service.sh
@@ -26,6 +26,12 @@ echo " • financial-benchmark-mcp — JSON-RPC 2.0 stdio, runs as Claude Desk
echo ""
# ── 1. Build the httpd shared library ─────────────────────────────────────────
+#
+# Includes:
+# financial_benchmark_service.c — core computation + JSON I/O
+# financial_benchmark_service_handler.c — Axis2/C HTTP entry points (D1)
+# mcp_catalog_handler.c — /_mcp/openapi-mcp.json endpoint (D1)
+# axis2_json_secure_fault.c — correlation-ID error responses (D2)
gcc -shared -fPIC \
-DAXIS2_JSON_ENABLED -DWITH_NGHTTP2 -O2 \
-I/usr/local/axis2c/include/axis2-2.0.0 \
@@ -34,6 +40,9 @@ gcc -shared -fPIC \
-I../../src/core/transport/http/util \
-I/usr/include/json-c \
src/financial_benchmark_service.c \
+ src/financial_benchmark_service_handler.c \
+ src/mcp_catalog_handler.c \
+ src/axis2_json_secure_fault.c \
-o libfinancial_benchmark_service.so \
-L/usr/local/axis2c/lib \
-laxutil -laxis2_engine \
(axis-axis2-c-core) 10/21: financial-benchmark: C1 — MCP stdio transport (JSON-RPC 2.0)
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 7ad47e26b0a4d10fa50a58b44bc196c464a26f7e
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 14:03:00 2026 -1000
financial-benchmark: C1 — MCP stdio transport (JSON-RPC 2.0)
Implements C1 from docs/MCP.md: stdio MCP mode for the financial benchmark
service. Claude Desktop launches the binary as a subprocess; it reads
JSON-RPC 2.0 requests from stdin and writes responses to stdout.
New files:
src/finbench_mcp.h — Public API: finbench_run_mcp_stdio(env)
src/finbench_mcp.c — JSON-RPC 2.0 loop, tool catalog, dispatch
src/finbench_mcp_main.c — Standalone entry point (axutil_env_create_all)
MCP protocol:
initialize → protocolVersion, capabilities, serverInfo
tools/list → static catalog: portfolioVariance, monteCarlo, scenarioAnalysis
tools/call → thin wrapper around finbench_*_json_only(); result in content[0].text
notifications (no "id") → silently consumed, no response (JSON-RPC 2.0 rule)
Design notes:
- getline() for dynamic line buffer (large portfolioVariance requests can be 2MB+)
- Axis2/C log output written to FINBENCH_LOG_FILE (default: /tmp/finbench_mcp.log)
so stdout is clean for JSON-RPC responses
- Tool input schemas use snake_case field names matching the C service
- static const tool catalog — no reflection, no overhead (C1/C2 pattern from MCP.md)
Claude Desktop config (~/.config/claude/claude_desktop_config.json):
{"mcpServers":{"axis2c-financial":
{"command":"/usr/local/axis2c/bin/financial-benchmark-mcp"}}}
Also:
Makefile.am: add financial-benchmark-mcp bin_PROGRAMS target (links .c directly)
build_financial_service.sh: add MCP binary build step + install + smoke-test instructions
financial_benchmark_service_handler.c: route scenarioAnalysis to
finbench_scenario_json_only() (was a NOT_IMPLEMENTED stub — implementation
has been complete in financial_benchmark_service.c since the previous commit)
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/Makefile.am | 164 ++++---
.../build_financial_service.sh | 73 +++-
.../src/financial_benchmark_service_handler.c | 5 +-
.../financial-benchmark-service/src/finbench_mcp.c | 472 +++++++++++++++++++++
.../financial-benchmark-service/src/finbench_mcp.h | 78 ++++
.../src/finbench_mcp_main.c | 83 ++++
6 files changed, 790 insertions(+), 85 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/Makefile.am b/samples/user_guide/financial-benchmark-service/Makefile.am
index 8e499e6c7..db4341ffd 100644
--- a/samples/user_guide/financial-benchmark-service/Makefile.am
+++ b/samples/user_guide/financial-benchmark-service/Makefile.am
@@ -1,64 +1,100 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Financial Benchmark Service - Makefile.am
-#
-# Builds the HTTP/2 JSON Financial Benchmark Service for Axis2/C
-# Target: Resource-constrained devices (old Android phones, embedded systems)
-
-# Only build if JSON is enabled (required for json-c library)
-if AXIS2_JSON_ENABLED
-
-# Service library
-servicesdir = $(prefix)/services/FinancialBenchmarkService
-
-lib_LTLIBRARIES = libfinancial_benchmark_service.la
-
-libfinancial_benchmark_service_la_SOURCES = \
- src/financial_benchmark_service.c
-
-libfinancial_benchmark_service_la_CPPFLAGS = \
- -I$(top_srcdir)/include \
- -I$(top_srcdir)/util/include \
- -I$(top_srcdir)/axiom/include \
- -I$(top_srcdir)/neethi/include \
- $(JSON_CFLAGS)
-
-libfinancial_benchmark_service_la_LIBADD = \
- $(top_builddir)/src/core/engine/libaxis2_engine.la \
- $(top_builddir)/axiom/src/om/libaxis2_axiom.la \
- $(top_builddir)/util/src/libaxutil.la \
- $(JSON_LIBS) \
- -lm
-
-libfinancial_benchmark_service_la_LDFLAGS = \
- -module \
- -avoid-version \
- -no-undefined \
- -shared
-
-# Install services.xml to the services directory
-services_DATA = services.xml
-
-# Install header for potential extensions
-include_HEADERS = src/financial_benchmark_service.h
-
-# Extra files to distribute
-EXTRA_DIST = \
- services.xml \
- src/financial_benchmark_service.h \
- README.md
-
-endif # AXIS2_JSON_ENABLED
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Financial Benchmark Service - Makefile.am
+#
+# Builds the HTTP/2 JSON Financial Benchmark Service for Axis2/C
+# Target: Resource-constrained devices (old Android phones, embedded systems)
+
+# Only build if JSON is enabled (required for json-c library)
+if AXIS2_JSON_ENABLED
+
+# ─── Httpd shared library (loaded by mod_axis2) ───────────────────────────────
+
+servicesdir = $(prefix)/services/FinancialBenchmarkService
+
+lib_LTLIBRARIES = libfinancial_benchmark_service.la
+
+libfinancial_benchmark_service_la_SOURCES = \
+ src/financial_benchmark_service.c
+
+libfinancial_benchmark_service_la_CPPFLAGS = \
+ -I$(top_srcdir)/include \
+ -I$(top_srcdir)/util/include \
+ -I$(top_srcdir)/axiom/include \
+ -I$(top_srcdir)/neethi/include \
+ $(JSON_CFLAGS)
+
+libfinancial_benchmark_service_la_LIBADD = \
+ $(top_builddir)/src/core/engine/libaxis2_engine.la \
+ $(top_builddir)/axiom/src/om/libaxis2_axiom.la \
+ $(top_builddir)/util/src/libaxutil.la \
+ $(JSON_LIBS) \
+ -lm
+
+libfinancial_benchmark_service_la_LDFLAGS = \
+ -module \
+ -avoid-version \
+ -no-undefined \
+ -shared
+
+# ─── MCP stdio binary (C1) ────────────────────────────────────────────────────
+#
+# Standalone binary launched by Claude Desktop (and Cursor) as an MCP subprocess.
+# Links financial_benchmark_service.c directly (no shared library dependency).
+#
+# Claude Desktop config (~/.config/claude/claude_desktop_config.json):
+# { "mcpServers": { "axis2c-financial": {
+# "command": "/usr/local/axis2c/bin/financial-benchmark-mcp" } } }
+
+bin_PROGRAMS = financial-benchmark-mcp
+
+financial_benchmark_mcp_SOURCES = \
+ src/financial_benchmark_service.c \
+ src/finbench_mcp.c \
+ src/finbench_mcp_main.c
+
+financial_benchmark_mcp_CPPFLAGS = \
+ -I$(top_srcdir)/include \
+ -I$(top_srcdir)/util/include \
+ -I$(top_srcdir)/axiom/include \
+ -I$(top_srcdir)/neethi/include \
+ $(JSON_CFLAGS)
+
+financial_benchmark_mcp_LDADD = \
+ $(top_builddir)/src/core/engine/libaxis2_engine.la \
+ $(top_builddir)/axiom/src/om/libaxis2_axiom.la \
+ $(top_builddir)/util/src/libaxutil.la \
+ $(JSON_LIBS) \
+ -lm
+
+# ─── Install ──────────────────────────────────────────────────────────────────
+
+# Install services.xml to the services directory
+services_DATA = services.xml
+
+# Install headers for potential extensions
+include_HEADERS = \
+ src/financial_benchmark_service.h \
+ src/finbench_mcp.h
+
+# Extra files to distribute
+EXTRA_DIST = \
+ services.xml \
+ src/financial_benchmark_service.h \
+ src/finbench_mcp.h \
+ README.md
+
+endif # AXIS2_JSON_ENABLED
diff --git a/samples/user_guide/financial-benchmark-service/build_financial_service.sh b/samples/user_guide/financial-benchmark-service/build_financial_service.sh
index 49bd331c3..098f2d3b9 100755
--- a/samples/user_guide/financial-benchmark-service/build_financial_service.sh
+++ b/samples/user_guide/financial-benchmark-service/build_financial_service.sh
@@ -19,9 +19,13 @@ echo ""
echo "📈 ALGORITHMS INCLUDED:"
echo " • Portfolio Variance - O(n²) covariance matrix calculation"
echo " • Monte Carlo VaR - Compute-intensive simulation"
-echo " • Scenario Analysis - Hash table lookups (coming soon)"
+echo " • Scenario Analysis - Hash table lookups"
+echo ""
+echo "🤖 MCP (Model Context Protocol) binary also built:"
+echo " • financial-benchmark-mcp — JSON-RPC 2.0 stdio, runs as Claude Desktop subprocess"
echo ""
+# ── 1. Build the httpd shared library ─────────────────────────────────────────
gcc -shared -fPIC \
-DAXIS2_JSON_ENABLED -DWITH_NGHTTP2 -O2 \
-I/usr/local/axis2c/include/axis2-2.0.0 \
@@ -36,43 +40,65 @@ gcc -shared -fPIC \
-ljson-c \
-lm
+# ── 2. Build the MCP stdio binary (C1) ────────────────────────────────────────
+gcc -O2 \
+ -DAXIS2_JSON_ENABLED -DWITH_NGHTTP2 \
+ -I/usr/local/axis2c/include/axis2-2.0.0 \
+ -I../../include \
+ -I../../util/include \
+ -I../../src/core/transport/http/util \
+ -I/usr/include/json-c \
+ src/financial_benchmark_service.c \
+ src/finbench_mcp.c \
+ src/finbench_mcp_main.c \
+ -o financial-benchmark-mcp \
+ -L/usr/local/axis2c/lib \
+ -Wl,-rpath,/usr/local/axis2c/lib \
+ -laxutil -laxis2_engine \
+ -ljson-c \
+ -lm
+
echo "Build exit code: $?"
-if [ -f "libfinancial_benchmark_service.so" ]; then
+if [ -f "libfinancial_benchmark_service.so" ] && [ -f "financial-benchmark-mcp" ]; then
echo ""
- echo "🎉 SUCCESS: Financial Benchmark JSON Service built with SOAP-FREE architecture!"
+ echo "🎉 SUCCESS: Financial Benchmark Service built (httpd .so + MCP binary)"
# Show file info
- ls -la libfinancial_benchmark_service.so
- file libfinancial_benchmark_service.so
+ ls -la libfinancial_benchmark_service.so financial-benchmark-mcp
+ file libfinancial_benchmark_service.so financial-benchmark-mcp
# Check dependencies
echo ""
echo "🔍 Checking runtime dependencies..."
- echo "📋 Service-level: No direct AXIOM usage (pure JSON processing)"
- echo "🔧 Engine-level: AXIOM present as transitive dependency (expected in Axis2/C)"
- # Show axis2 related dependencies only
- ldd libfinancial_benchmark_service.so | grep -E '(axis2|json|libax|libm)' || echo " (Dependencies shown above in full ldd output)"
+ ldd libfinancial_benchmark_service.so | grep -E '(axis2|json|libax|libm)' || true
+ ldd financial-benchmark-mcp | grep -E '(axis2|json|libax|libm)' || true
- # Install the service
+ # ── Install httpd service ──
echo ""
- echo "🚀 Installing SOAP-free JSON Financial Benchmark service..."
+ echo "🚀 Installing httpd JSON service (mod_axis2)..."
sudo cp libfinancial_benchmark_service.so /usr/local/axis2c/services/FinancialBenchmarkService/
sudo cp services.xml /usr/local/axis2c/services/FinancialBenchmarkService/
-
- # Fix ownership and permissions for Apache (runs as www-data)
sudo chown -R www-data:www-data /usr/local/axis2c/services/FinancialBenchmarkService
sudo chmod 755 /usr/local/axis2c/services/FinancialBenchmarkService
sudo chmod 644 /usr/local/axis2c/services/FinancialBenchmarkService/services.xml
sudo chmod 755 /usr/local/axis2c/services/FinancialBenchmarkService/libfinancial_benchmark_service.so
+ # ── Install MCP binary ──
+ echo "🤖 Installing MCP stdio binary..."
+ sudo mkdir -p /usr/local/axis2c/bin
+ sudo cp financial-benchmark-mcp /usr/local/axis2c/bin/
+ sudo chmod 755 /usr/local/axis2c/bin/financial-benchmark-mcp
+
echo ""
- echo "✅ Installation complete! Financial Benchmark Service ready for HTTP/2 JSON requests"
+ echo "✅ Installation complete!"
echo ""
echo "============================================================"
echo "🧪 TEST THE SERVICE:"
echo "============================================================"
echo ""
+ echo "── HTTP/2 JSON (via Apache httpd) ──"
+ echo ""
echo "1. Start Apache httpd with mod_axis2:"
echo " sudo systemctl start apache2 # or httpd"
echo ""
@@ -84,16 +110,27 @@ if [ -f "libfinancial_benchmark_service.so" ]; then
echo ' curl -k --http2 -H "Content-Type: application/json" -d @- \'
echo ' https://localhost:443/services/FinancialBenchmarkService/portfolioVariance'
echo ""
- echo "4. Test Portfolio Variance (500 assets - benchmark):"
- echo ' python3 test/generate_test_data.py --assets 500 -o /tmp/portfolio_500.json'
- echo ' curl -k --http2 -H "Content-Type: application/json" -d @/tmp/portfolio_500.json \'
- echo ' https://localhost:443/services/FinancialBenchmarkService/portfolioVariance'
+ echo "4. Test Scenario Analysis:"
+ echo ' curl -k --http2 -H "Content-Type: application/json" \'
+ echo ' -d '"'"'{"assets":[{"asset_id":1,"current_price":100,"position_size":10,"scenario_prices":[110,100,90],"probabilities":[0.4,0.35,0.25]}]}'"'"' \'
+ echo ' https://localhost:443/services/FinancialBenchmarkService/scenarioAnalysis'
echo ""
echo "5. Test Monte Carlo VaR (10,000 simulations):"
echo ' curl -k --http2 -H "Content-Type: application/json" \'
echo ' -d '\''{"n_simulations":10000,"n_periods":252,"initial_value":1000000,"expected_return":0.08,"volatility":0.20}'\'' \'
echo ' https://localhost:443/services/FinancialBenchmarkService/monteCarlo'
echo ""
+ echo "── MCP stdio (Claude Desktop) ──"
+ echo ""
+ echo "6. Add to ~/.config/claude/claude_desktop_config.json:"
+ echo ' {"mcpServers":{"axis2c-financial":{"command":"/usr/local/axis2c/bin/financial-benchmark-mcp"}}}'
+ echo ""
+ echo "7. Smoke-test MCP stdio directly:"
+ echo ' echo '"'"'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'"'"' \'
+ echo ' | /usr/local/axis2c/bin/financial-benchmark-mcp'
+ echo ""
+ echo " Expected: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"protocolVersion\":\"2024-11-05\",...}}"
+ echo ""
echo "============================================================"
echo "📊 EXPECTED BENCHMARK RESULTS (on modest hardware):"
echo "============================================================"
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
index b1b48e195..482288e7b 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
@@ -93,11 +93,10 @@ route_operation(
return finbench_generate_test_portfolio_json(env, n_assets);
}
- /* scenarioAnalysis operation (stub for now) */
+ /* scenarioAnalysis operation */
if (axutil_strcmp(operation_name, "scenarioAnalysis") == 0)
{
- return axutil_strdup(env,
- "{\"status\":\"NOT_IMPLEMENTED\",\"message\":\"Scenario analysis coming soon\"}");
+ return finbench_scenario_json_only(env, json_request);
}
/* Unknown operation */
diff --git a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
new file mode 100644
index 000000000..6044b2a75
--- /dev/null
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -0,0 +1,472 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file finbench_mcp.c
+ * @brief MCP stdio transport — JSON-RPC 2.0 loop for the Financial Benchmark Service
+ *
+ * Three required MCP methods:
+ * initialize — protocol version + server info + capabilities
+ * tools/list — static tool catalog (portfolioVariance, monteCarlo, scenarioAnalysis)
+ * tools/call — thin wrapper around finbench_*_json_only()
+ *
+ * JSON-RPC 2.0 rules honored:
+ * - Requests without "id" are notifications → silently consumed, no response
+ * - Parse errors produce id=null error responses
+ * - All responses include "jsonrpc": "2.0"
+ * - MCP tools/call result wraps the operation JSON in content[0].text
+ */
+
+#include "financial_benchmark_service.h"
+#include "finbench_mcp.h"
+
+#include <json-c/json.h>
+#include <axutil_string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* ============================================================================
+ * Protocol constants
+ * ============================================================================
+ */
+
+#define FINBENCH_MCP_PROTOCOL_VERSION "2024-11-05"
+#define FINBENCH_MCP_SERVER_NAME "axis2c-financial-benchmark"
+#define FINBENCH_MCP_SERVER_VERSION "2.0.1"
+
+/* JSON-RPC 2.0 error codes */
+#define MCP_ERR_PARSE_ERROR -32700
+#define MCP_ERR_INVALID_REQUEST -32600
+#define MCP_ERR_METHOD_NOT_FOUND -32601
+#define MCP_ERR_INVALID_PARAMS -32602
+#define MCP_ERR_INTERNAL_ERROR -32603
+
+/* ============================================================================
+ * Tool catalog — input schemas (snake_case, matching C service field names)
+ *
+ * These are the same JSON field names accepted by the HTTP/2 JSON endpoints.
+ * MCP clients (Claude Desktop, Cursor) use these schemas to populate requests.
+ * ============================================================================
+ */
+
+static const char SCHEMA_PORTFOLIO_VARIANCE[] =
+ "{"
+ "\"type\":\"object\","
+ "\"properties\":{"
+ "\"n_assets\":{\"type\":\"integer\","
+ "\"description\":\"Number of assets in the portfolio (max 2000)\"},"
+ "\"weights\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
+ "\"description\":\"Portfolio weights. Must sum to 1.0 unless normalize_weights=true\"},"
+ "\"covariance_matrix\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
+ "\"description\":\"Flattened n_assets x n_assets covariance matrix (row-major order)\"},"
+ "\"normalize_weights\":{\"type\":\"boolean\","
+ "\"description\":\"Rescale weights to sum to 1.0 before computing variance. Default: false\"},"
+ "\"n_periods_per_year\":{\"type\":\"integer\","
+ "\"description\":\"Trading periods per year for annualizing volatility. "
+ "Default 252 (equity). Use 260 (some fixed-income), 365 (crypto), 12 (monthly)\"},"
+ "\"request_id\":{\"type\":\"string\","
+ "\"description\":\"Optional identifier echoed in the response for request tracing\"}"
+ "},"
+ "\"required\":[\"n_assets\",\"weights\",\"covariance_matrix\"]"
+ "}";
+
+static const char SCHEMA_MONTE_CARLO[] =
+ "{"
+ "\"type\":\"object\","
+ "\"properties\":{"
+ "\"n_simulations\":{\"type\":\"integer\","
+ "\"description\":\"Number of simulation paths (max 1,000,000). Default: 10,000\"},"
+ "\"n_periods\":{\"type\":\"integer\","
+ "\"description\":\"Number of time steps per path (e.g. 252 = one trading year). Default: 252\"},"
+ "\"initial_value\":{\"type\":\"number\","
+ "\"description\":\"Initial portfolio value in currency units. Default: 1,000,000\"},"
+ "\"expected_return\":{\"type\":\"number\","
+ "\"description\":\"Expected annualized return (e.g. 0.08 for 8%). Default: 0.08\"},"
+ "\"volatility\":{\"type\":\"number\","
+ "\"description\":\"Annualized volatility (e.g. 0.20 for 20%). Default: 0.20\"},"
+ "\"random_seed\":{\"type\":\"integer\","
+ "\"description\":\"xorshift128+ seed for reproducibility. 0 = non-deterministic. Default: 0\"},"
+ "\"n_periods_per_year\":{\"type\":\"integer\","
+ "\"description\":\"Trading periods per year — controls GBM time step dt=1/n_periods_per_year. "
+ "Default: 252. Must match the annualization basis of expected_return and volatility\"},"
+ "\"percentiles\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
+ "\"description\":\"Percentile tail levels for VaR, e.g. [0.01, 0.05] for 99%% and 95%% VaR. "
+ "Max 8 values. Default: [0.01, 0.05]\"},"
+ "\"request_id\":{\"type\":\"string\"}"
+ "},"
+ "\"required\":[],"
+ "\"description\":\"All fields have defaults — an empty {} request body is valid\""
+ "}";
+
+static const char SCHEMA_SCENARIO_ANALYSIS[] =
+ "{"
+ "\"type\":\"object\","
+ "\"properties\":{"
+ "\"assets\":{"
+ "\"type\":\"array\","
+ "\"description\":\"Portfolio assets with scenario prices and probabilities\","
+ "\"items\":{"
+ "\"type\":\"object\","
+ "\"properties\":{"
+ "\"asset_id\":{\"type\":\"integer\","
+ "\"description\":\"Unique asset identifier\"},"
+ "\"current_price\":{\"type\":\"number\","
+ "\"description\":\"Current market price in currency units\"},"
+ "\"position_size\":{\"type\":\"number\","
+ "\"description\":\"Position size in shares/units. Scales upside/downside to dollar terms\"},"
+ "\"scenario_prices\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
+ "\"description\":\"Target price for each scenario\"},"
+ "\"probabilities\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
+ "\"description\":\"Probability weight for each scenario. Must sum to 1.0\"}"
+ "},"
+ "\"required\":[\"asset_id\",\"current_price\",\"scenario_prices\",\"probabilities\"]"
+ "}"
+ "},"
+ "\"use_hash_lookup\":{\"type\":\"boolean\","
+ "\"description\":\"Benchmark O(1) HashMap lookup against O(n) linear scan. Default: true\"},"
+ "\"prob_tolerance\":{\"type\":\"number\","
+ "\"description\":\"Tolerance for probability sum validation per asset. "
+ "Default: 1e-4 (0.01%%). Clamped to [1e-10, 0.1]\"},"
+ "\"request_id\":{\"type\":\"string\"}"
+ "},"
+ "\"required\":[\"assets\"]"
+ "}";
+
+/* ============================================================================
+ * Tool catalog struct
+ * ============================================================================
+ */
+
+typedef struct {
+ const char *name;
+ const char *description;
+ const char *input_schema_json;
+} finbench_mcp_tool_t;
+
+static const finbench_mcp_tool_t finbench_mcp_tools[] = {
+ {
+ "portfolioVariance",
+ "Calculate portfolio variance using O(n^2) covariance matrix multiplication: "
+ "sigma_p^2 = sum_i sum_j w_i * w_j * sigma_ij. "
+ "Returns variance, volatility, annualized volatility (sigma * sqrt(n_periods_per_year)), "
+ "matrix operation count, and microsecond timing. "
+ "Target: 500 assets in ~5ms on resource-constrained hardware.",
+ SCHEMA_PORTFOLIO_VARIANCE
+ },
+ {
+ "monteCarlo",
+ "Monte Carlo Value at Risk (VaR) simulation using Geometric Brownian Motion: "
+ "S(t+dt) = S(t) * exp((mu - sigma^2/2)*dt + sigma*sqrt(dt)*Z), where Z ~ N(0,1). "
+ "Returns VaR at caller-specified percentiles, CVaR (Expected Shortfall at 95%%), "
+ "max drawdown, probability of profit, and simulations-per-second throughput. "
+ "Uses xorshift128+ PRNG + Box-Muller transform for high-throughput random number generation.",
+ SCHEMA_MONTE_CARLO
+ },
+ {
+ "scenarioAnalysis",
+ "Probability-weighted scenario analysis for a multi-asset portfolio. "
+ "Computes expected return E[r_i] = sum_j (p_j * (price_j/current_price - 1)), "
+ "upside potential, downside risk, and upside/downside ratio. "
+ "Also benchmarks O(1) axutil_hash_t lookup against O(n) array scan — "
+ "the same HashMap optimization used in production systems for 500+ asset portfolios.",
+ SCHEMA_SCENARIO_ANALYSIS
+ },
+ { NULL, NULL, NULL } /* sentinel */
+};
+
+#define FINBENCH_MCP_N_TOOLS 3
+
+/* ============================================================================
+ * JSON-RPC 2.0 output helpers
+ *
+ * All output goes to stdout (the MCP transport channel).
+ * Caller must ensure the Axis2/C logger does NOT write to stdout.
+ * ============================================================================
+ */
+
+/**
+ * Write a JSON-RPC 2.0 success response.
+ * Takes ownership of result_obj (calls json_object_put on it).
+ * id_obj must be non-NULL; borrowed from the request object.
+ */
+static void mcp_write_result(json_object *id_obj, json_object *result_obj)
+{
+ json_object *response = json_object_new_object();
+ json_object_object_add(response, "jsonrpc",
+ json_object_new_string("2.0"));
+
+ /* Bump refcount: response takes a share; request still owns one */
+ json_object_get(id_obj);
+ json_object_object_add(response, "id", id_obj);
+
+ json_object_object_add(response, "result", result_obj); /* transfers ownership */
+
+ printf("%s\n",
+ json_object_to_json_string_ext(response, JSON_C_TO_STRING_PLAIN));
+ fflush(stdout);
+
+ json_object_put(response); /* releases response + result; id refcount back to 1 */
+}
+
+/**
+ * Write a JSON-RPC 2.0 error response.
+ * id_obj may be NULL (id=null per spec, used for parse errors before id is known).
+ */
+static void mcp_write_error(json_object *id_obj, int code, const char *message)
+{
+ json_object *error_obj = json_object_new_object();
+ json_object_object_add(error_obj, "code", json_object_new_int(code));
+ json_object_object_add(error_obj, "message", json_object_new_string(message));
+
+ json_object *response = json_object_new_object();
+ json_object_object_add(response, "jsonrpc",
+ json_object_new_string("2.0"));
+
+ if (id_obj) {
+ json_object_get(id_obj);
+ json_object_object_add(response, "id", id_obj);
+ } else {
+ json_object_object_add(response, "id", json_object_new_null());
+ }
+
+ json_object_object_add(response, "error", error_obj);
+
+ printf("%s\n",
+ json_object_to_json_string_ext(response, JSON_C_TO_STRING_PLAIN));
+ fflush(stdout);
+
+ json_object_put(response);
+}
+
+/* ============================================================================
+ * MCP method handlers
+ * ============================================================================
+ */
+
+static json_object *mcp_handle_initialize(void)
+{
+ json_object *result = json_object_new_object();
+ json_object_object_add(result, "protocolVersion",
+ json_object_new_string(FINBENCH_MCP_PROTOCOL_VERSION));
+
+ json_object *capabilities = json_object_new_object();
+ json_object_object_add(capabilities, "tools", json_object_new_object());
+ json_object_object_add(result, "capabilities", capabilities);
+
+ json_object *server_info = json_object_new_object();
+ json_object_object_add(server_info, "name",
+ json_object_new_string(FINBENCH_MCP_SERVER_NAME));
+ json_object_object_add(server_info, "version",
+ json_object_new_string(FINBENCH_MCP_SERVER_VERSION));
+ json_object_object_add(result, "serverInfo", server_info);
+
+ return result;
+}
+
+static json_object *mcp_handle_tools_list(void)
+{
+ json_object *tools_array = json_object_new_array();
+
+ for (int i = 0; i < FINBENCH_MCP_N_TOOLS; i++) {
+ const finbench_mcp_tool_t *tool = &finbench_mcp_tools[i];
+ json_object *tool_obj = json_object_new_object();
+
+ json_object_object_add(tool_obj, "name",
+ json_object_new_string(tool->name));
+ json_object_object_add(tool_obj, "description",
+ json_object_new_string(tool->description));
+
+ /* Parse the inlined schema string into a json_object */
+ json_object *schema = json_tokener_parse(tool->input_schema_json);
+ if (!schema) {
+ /* Fallback: minimal valid schema (should never happen) */
+ schema = json_object_new_object();
+ json_object_object_add(schema, "type", json_object_new_string("object"));
+ }
+ json_object_object_add(tool_obj, "inputSchema", schema);
+
+ json_object_array_add(tools_array, tool_obj);
+ }
+
+ json_object *result = json_object_new_object();
+ json_object_object_add(result, "tools", tools_array);
+ return result;
+}
+
+/**
+ * Dispatch tools/call to the appropriate financial operation.
+ *
+ * @param env Axis2/C environment for the *_json_only() calls.
+ * @param params The JSON-RPC "params" object (may be NULL).
+ * @param out_code Set to a JSON-RPC error code on failure (0 on success).
+ * @param out_msg Set to a human-readable error message on failure.
+ * @return MCP result object on success, NULL on failure.
+ */
+static json_object *mcp_handle_tools_call(
+ const axutil_env_t *env,
+ json_object *params,
+ int *out_code,
+ const char **out_msg)
+{
+ *out_code = 0;
+ *out_msg = NULL;
+
+ if (!params || !json_object_is_type(params, json_type_object)) {
+ *out_code = MCP_ERR_INVALID_PARAMS;
+ *out_msg = "params must be an object with name and arguments fields";
+ return NULL;
+ }
+
+ /* Extract tool name */
+ json_object *name_obj = NULL;
+ if (!json_object_object_get_ex(params, "name", &name_obj)
+ || !json_object_is_type(name_obj, json_type_string)) {
+ *out_code = MCP_ERR_INVALID_PARAMS;
+ *out_msg = "params.name is required and must be a string";
+ return NULL;
+ }
+ const char *tool_name = json_object_get_string(name_obj);
+
+ /* Extract arguments — optional; default to empty object */
+ json_object *args_obj = NULL;
+ json_object_object_get_ex(params, "arguments", &args_obj);
+ const char *args_json = args_obj
+ ? json_object_to_json_string(args_obj) /* owned by args_obj, valid while args_obj lives */
+ : "{}";
+
+ /* Dispatch to the matching finbench_*_json_only() function.
+ * Each returns an axutil_strdup()'d string — free with AXIS2_FREE(). */
+ axis2_char_t *result_json = NULL;
+
+ if (strcmp(tool_name, "portfolioVariance") == 0) {
+ result_json = finbench_portfolio_variance_json_only(env, args_json);
+ } else if (strcmp(tool_name, "monteCarlo") == 0) {
+ result_json = finbench_monte_carlo_json_only(env, args_json);
+ } else if (strcmp(tool_name, "scenarioAnalysis") == 0) {
+ result_json = finbench_scenario_json_only(env, args_json);
+ } else {
+ *out_code = MCP_ERR_METHOD_NOT_FOUND;
+ *out_msg = "Unknown tool name. Available: portfolioVariance, monteCarlo, scenarioAnalysis";
+ return NULL;
+ }
+
+ if (!result_json) {
+ *out_code = MCP_ERR_INTERNAL_ERROR;
+ *out_msg = "Operation returned a null response";
+ return NULL;
+ }
+
+ /* Wrap in MCP content envelope: { "content": [{ "type": "text", "text": "..." }] } */
+ json_object *content_item = json_object_new_object();
+ json_object_object_add(content_item, "type", json_object_new_string("text"));
+ json_object_object_add(content_item, "text", json_object_new_string(result_json));
+
+ json_object *content_array = json_object_new_array();
+ json_object_array_add(content_array, content_item);
+
+ json_object *result = json_object_new_object();
+ json_object_object_add(result, "content", content_array);
+
+ AXIS2_FREE(env->allocator, result_json);
+
+ return result;
+}
+
+/* ============================================================================
+ * Public API: stdio loop
+ * ============================================================================
+ */
+
+void finbench_run_mcp_stdio(const axutil_env_t *env)
+{
+ char *line = NULL;
+ size_t line_cap = 0;
+ ssize_t line_len;
+
+ /*
+ * getline() allocates dynamically — handles large portfolioVariance requests
+ * (e.g. 500-asset covariance matrix ~ 2 MB) without a fixed buffer limit.
+ */
+ while ((line_len = getline(&line, &line_cap, stdin)) > 0) {
+
+ /* Strip trailing CR/LF */
+ while (line_len > 0
+ && (line[line_len - 1] == '\n' || line[line_len - 1] == '\r')) {
+ line[--line_len] = '\0';
+ }
+ if (line_len == 0) continue;
+
+ /* ── Parse JSON-RPC 2.0 request ─────────────────────────────── */
+ json_object *req = json_tokener_parse(line);
+ if (!req) {
+ /* id is unknown on parse failure — use null per spec */
+ mcp_write_error(NULL, MCP_ERR_PARSE_ERROR, "Parse error: invalid JSON");
+ continue;
+ }
+
+ /* ── Check for "id" — absent means notification, no response ── */
+ json_object *id_obj = NULL;
+ if (!json_object_object_get_ex(req, "id", &id_obj)) {
+ /* MCP notifications/initialized and similar — silently consume */
+ json_object_put(req);
+ continue;
+ }
+
+ /* ── Extract "method" ────────────────────────────────────────── */
+ json_object *method_obj = NULL;
+ if (!json_object_object_get_ex(req, "method", &method_obj)
+ || !json_object_is_type(method_obj, json_type_string)) {
+ mcp_write_error(id_obj, MCP_ERR_INVALID_REQUEST,
+ "method is required and must be a string");
+ json_object_put(req);
+ continue;
+ }
+ const char *method = json_object_get_string(method_obj);
+
+ /* ── Extract optional "params" ───────────────────────────────── */
+ json_object *params_obj = NULL;
+ json_object_object_get_ex(req, "params", ¶ms_obj);
+
+ /* ── Dispatch ────────────────────────────────────────────────── */
+ if (strcmp(method, "initialize") == 0) {
+ mcp_write_result(id_obj, mcp_handle_initialize());
+
+ } else if (strcmp(method, "tools/list") == 0) {
+ mcp_write_result(id_obj, mcp_handle_tools_list());
+
+ } else if (strcmp(method, "tools/call") == 0) {
+ int err_code = 0;
+ const char *err_msg = NULL;
+ json_object *result = mcp_handle_tools_call(env, params_obj,
+ &err_code, &err_msg);
+ if (result) {
+ mcp_write_result(id_obj, result);
+ } else {
+ mcp_write_error(id_obj, err_code,
+ err_msg ? err_msg : "Internal error");
+ }
+
+ } else {
+ mcp_write_error(id_obj, MCP_ERR_METHOD_NOT_FOUND, "Method not found");
+ }
+
+ json_object_put(req); /* release request + all its members */
+ }
+
+ free(line);
+}
diff --git a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
new file mode 100644
index 000000000..b21b44c2c
--- /dev/null
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FINBENCH_MCP_H
+#define FINBENCH_MCP_H
+
+/**
+ * @file finbench_mcp.h
+ * @brief MCP stdio transport for the Financial Benchmark Service (C1)
+ *
+ * Implements JSON-RPC 2.0 over stdin/stdout for the three financial benchmark
+ * operations: portfolioVariance, monteCarlo, scenarioAnalysis.
+ *
+ * This is a thin wrapper around the existing finbench_*_json_only() functions.
+ * The same JSON request/response schemas are reused — MCP adds only the
+ * JSON-RPC 2.0 framing (method, id, params.arguments, result.content).
+ *
+ * Usage (Claude Desktop claude_desktop_config.json):
+ * @code
+ * {
+ * "mcpServers": {
+ * "axis2c-financial": {
+ * "command": "/usr/local/axis2c/bin/financial-benchmark-mcp",
+ * "args": []
+ * }
+ * }
+ * }
+ * @endcode
+ *
+ * IMPORTANT: stdout is reserved for JSON-RPC 2.0 responses.
+ * Set FINBENCH_LOG_FILE to redirect Axis2/C log output away from stdout.
+ * Default log file: /tmp/finbench_mcp.log
+ *
+ * Protocol version: 2024-11-05
+ *
+ * Tools exposed:
+ * - portfolioVariance (O(n^2) covariance matrix multiplication)
+ * - monteCarlo (GBM Value-at-Risk simulation)
+ * - scenarioAnalysis (expected return + HashMap vs ArrayList benchmark)
+ */
+
+#include <axutil_env.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Run the MCP JSON-RPC 2.0 stdio loop.
+ *
+ * Reads newline-delimited JSON requests from stdin, dispatches to the
+ * financial benchmark operations, and writes JSON-RPC 2.0 responses to stdout.
+ * Returns when stdin reaches EOF (Claude Desktop terminates the process).
+ *
+ * @param env Axis2/C environment. The associated logger MUST write to a file,
+ * not stdout. Use axutil_env_create_all("/tmp/finbench_mcp.log", ...).
+ */
+void finbench_run_mcp_stdio(const axutil_env_t *env);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FINBENCH_MCP_H */
diff --git a/samples/user_guide/financial-benchmark-service/src/finbench_mcp_main.c b/samples/user_guide/financial-benchmark-service/src/finbench_mcp_main.c
new file mode 100644
index 000000000..43f84cbab
--- /dev/null
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp_main.c
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file finbench_mcp_main.c
+ * @brief Entry point for the financial-benchmark-mcp standalone binary (C1)
+ *
+ * Claude Desktop config (~/.config/claude/claude_desktop_config.json):
+ * @code
+ * {
+ * "mcpServers": {
+ * "axis2c-financial": {
+ * "command": "/usr/local/axis2c/bin/financial-benchmark-mcp",
+ * "args": []
+ * }
+ * }
+ * }
+ * @endcode
+ *
+ * Environment variables:
+ * FINBENCH_LOG_FILE Path for Axis2/C log output.
+ * Default: /tmp/finbench_mcp.log
+ * MUST NOT point to stdout — that channel is reserved
+ * for JSON-RPC 2.0 responses.
+ *
+ * This binary is separate from the httpd shared library
+ * (libfinancial_benchmark_service.so). Both are built from the same
+ * financial_benchmark_service.c source — the MCP binary links it statically
+ * rather than as a loadable module.
+ */
+
+#include "financial_benchmark_service.h"
+#include "finbench_mcp.h"
+
+#include <axutil_env.h>
+#include <axutil_log.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+ /* Resolve log file — must NOT be stdout */
+ const char *log_file = getenv("FINBENCH_LOG_FILE");
+ if (!log_file || log_file[0] == '\0') {
+ log_file = "/tmp/finbench_mcp.log";
+ }
+
+ /*
+ * Create Axis2/C environment.
+ * AXIS2_LOG_LEVEL_WARNING suppresses INFO/DEBUG noise to keep the
+ * log file small during normal operation.
+ */
+ const axutil_env_t *env = axutil_env_create_all(
+ log_file, AXIS2_LOG_LEVEL_WARNING);
+
+ if (!env) {
+ /* Write to stderr — stdout is reserved for JSON-RPC */
+ fprintf(stderr,
+ "finbench_mcp: failed to create Axis2/C environment "
+ "(log_file=%s)\n", log_file);
+ return 1;
+ }
+
+ /* Run the MCP JSON-RPC 2.0 stdio loop until stdin closes */
+ finbench_run_mcp_stdio(env);
+
+ axutil_env_free((axutil_env_t *)env);
+ return 0;
+}
(axis-axis2-c-core) 01/21: Implement scenarioAnalysis and fix benchmark docs
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 32a5a5ca6e10be390f91cdfc8a4a8f436cc2bde2
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 06:19:30 2026 -1000
Implement scenarioAnalysis and fix benchmark docs
- financial_benchmark_service.c: Implement scenarioAnalysis operation
with dual O(n)/O(1) benchmark: linear scan vs axutil_hash lookup,
financial metrics (expected return, upside/downside, U/D ratio)
- financial_benchmark_service.c: Update comparison_note strings to
reflect real enterprise production numbers (16-32GB/server)
- README.md: Replace straw-man Java comparison with accurate enterprise
production context (70+ WildFly servers, 16-32GB minimum); remove
company-specific references per Apache project conventions
- json-httpd-h2-userguide.md: Fix markdown code fence at line 508
(unclosed block caused heading to render inside code block)
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
docs/userguide/json-httpd-h2-userguide.md | 1 +
.../financial-benchmark-service/README.md | 35 +-
.../src/financial_benchmark_service.c | 428 ++++++++++++++++++++-
3 files changed, 446 insertions(+), 18 deletions(-)
diff --git a/docs/userguide/json-httpd-h2-userguide.md b/docs/userguide/json-httpd-h2-userguide.md
index 828c84dd7..423ce0cef 100644
--- a/docs/userguide/json-httpd-h2-userguide.md
+++ b/docs/userguide/json-httpd-h2-userguide.md
@@ -504,6 +504,7 @@ journalctl -xeu apache2-custom.service
# Test configuration syntax
sudo /usr/local/apache2/bin/httpd -t
+```
**Verify Apache is working correctly:**
```bash
diff --git a/samples/user_guide/financial-benchmark-service/README.md b/samples/user_guide/financial-benchmark-service/README.md
index 7de9df096..96d224f5b 100644
--- a/samples/user_guide/financial-benchmark-service/README.md
+++ b/samples/user_guide/financial-benchmark-service/README.md
@@ -8,13 +8,15 @@ This service demonstrates Axis2/C performance on **resource-constrained hardware
### The "Single Camera" Use Case
-Just as the Kanaha Camera Control system provides value with a single old Android phone, this benchmark service demonstrates that **one modest device** can run production-grade financial calculations:
+Just as the Kanaha Camera Control system provides value with a single old Android phone, this benchmark service demonstrates that **one modest device** can run production-grade financial calculations.
-| Platform | Minimum RAM | Old Android (2GB) |
-|----------|-------------|-------------------|
-| **Java/WildFly 32** | 4-8 GB | ❌ Cannot start |
-| **Node.js/V8** | 1-2 GB | ⚠️ OOM risk |
-| **Axis2/C 2.0** | ~30 MB | ✅ Runs comfortably |
+The Java comparison here is not a straw man — it reflects real enterprise production deployments. Large financial services organizations routinely run **70+ WildFly servers**, all configured at **16 GB RAM minimum**, with the majority at **32 GB or higher**, running portfolio management, securities master, and data processing services under heavy load. These are the real numbers:
+
+| Platform | Edge Device (2 GB) | Enterprise Production Reality |
+|----------|--------------------|-----------------------|
+| **Java/WildFly 32** | ❌ Cannot start | 16–32+ GB per server (70+ servers) |
+| **Node.js/V8** | ⚠️ OOM risk | 4–8 GB |
+| **Axis2/C 2.0** | ✅ Runs comfortably | ~30 MB (unchanged at any scale) |
## Operations
@@ -98,7 +100,7 @@ Runs Value at Risk (VaR) simulation using Geometric Brownian Motion.
### 3. Scenario Analysis (`/scenarioAnalysis`)
-Demonstrates O(1) hash table lookups vs O(n) linear search (mirrors DPT's Array→Map optimization).
+Demonstrates O(1) hash table lookups vs O(n) linear search — a common optimization in enterprise portfolio systems migrating from array scans to hash-based asset lookups.
### 4. Service Metadata (`/metadata`)
@@ -195,20 +197,21 @@ On a **$20 used Android phone from 2014** (Galaxy S5, 2GB RAM):
│ Result: 250,000 matrix ops in 4.5ms │
│ Memory: 28 MB peak │
│ ──────────────────────────────────────────────────────── │
-│ Java/WildFly: Cannot run (requires 4-8GB RAM) │
-│ Hardware cost: $20 vs $2000+ │
+│ Java/WildFly: Cannot run on this device. │
+│ Enterprise minimum: 16 GB RAM per WildFly server │
+│ Hardware cost: $20 device vs $5,000–$20,000+ server │
└────────────────────────────────────────────────────────────┘
```
### Benchmark Results Table
-| Test | Axis2/C (2GB device) | Java/WildFly |
-|------|---------------------|--------------|
-| Portfolio 500 assets | ~5ms | N/A (cannot start) |
-| Monte Carlo 10K sims | ~100ms | N/A (cannot start) |
-| Memory usage | ~30MB | 4-8GB minimum |
-| Startup time | <1s | 30-60s |
-| Device cost | $20 | $2000+ |
+| Test | Axis2/C (2GB device) | Java/WildFly 32 (Enterprise production) |
+|------|---------------------|----------------------------------------|
+| Portfolio 500 assets | ~5ms | N/A — cannot start on device |
+| Monte Carlo 10K sims | ~100ms | N/A — cannot start on device |
+| Memory footprint | ~30 MB | 16–32 GB (enterprise: 70+ servers configured this way) |
+| Startup time | <1s | 30–60s |
+| Hardware cost | $20 used phone | Enterprise server ($5,000–$20,000+) |
## Use Cases
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index 62cbb3eac..8136f7fc8 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -468,7 +468,7 @@ finbench_portfolio_variance_response_to_json(
/* Add comparison note for demo */
json_object_object_add(json_resp, "comparison_note",
json_object_new_string(
- "Java/WildFly requires 4-8GB RAM minimum. "
+ "Enterprise Java/WildFly deployments require 16-32GB RAM per server. "
"This device runs Axis2/C in ~30MB."));
json_str = json_object_to_json_string_ext(json_resp, JSON_C_TO_STRING_PLAIN);
@@ -851,6 +851,430 @@ finbench_monte_carlo_json_only(
return json_response;
}
+/* ============================================================================
+ * Scenario Analysis Implementation
+ *
+ * Demonstrates O(1) hash table vs O(n) linear search for asset lookups.
+ * Mirrors DPT v2's Array→Map optimization for 500+ asset portfolios.
+ *
+ * Financial calculation per asset:
+ * expected_return = Σ( probability_i × (scenario_price_i / current_price - 1) )
+ * upside = Σ( probability_i × max(0, price_i - current) × position_size ) [gains only]
+ * downside = Σ( probability_i × max(0, current - price_i) × position_size ) [losses only]
+ *
+ * Benchmark: N_LOOKUPS = n_assets × 10 lookups measured for both methods.
+ * ============================================================================
+ */
+
+AXIS2_EXTERN finbench_scenario_request_t* AXIS2_CALL
+finbench_scenario_request_create_from_json(
+ const axutil_env_t *env,
+ const axis2_char_t *json_string)
+{
+ finbench_scenario_request_t *request = NULL;
+ json_object *json_obj = NULL;
+ json_object *assets_arr = NULL;
+ json_object *value_obj = NULL;
+ int i, j, n_assets;
+
+ if (!env || !json_string) return NULL;
+
+ json_obj = json_tokener_parse(json_string);
+ if (!json_obj) {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "ScenarioAnalysis: Failed to parse JSON request");
+ return NULL;
+ }
+
+ request = AXIS2_MALLOC(env->allocator, sizeof(finbench_scenario_request_t));
+ if (!request) {
+ json_object_put(json_obj);
+ return NULL;
+ }
+ memset(request, 0, sizeof(finbench_scenario_request_t));
+
+ /* use_hash_lookup flag (default: run both, report both) */
+ if (json_object_object_get_ex(json_obj, "use_hash_lookup", &value_obj)) {
+ request->use_hash_lookup = json_object_get_boolean(value_obj)
+ ? AXIS2_TRUE : AXIS2_FALSE;
+ } else {
+ request->use_hash_lookup = AXIS2_TRUE;
+ }
+
+ /* request_id */
+ if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
+ const char *rid = json_object_get_string(value_obj);
+ if (rid) request->request_id = axutil_strdup(env, rid);
+ }
+
+ /* assets array */
+ if (!json_object_object_get_ex(json_obj, "assets", &assets_arr) ||
+ !json_object_is_type(assets_arr, json_type_array)) {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "ScenarioAnalysis: Missing or invalid 'assets' array");
+ if (request->request_id) AXIS2_FREE(env->allocator, request->request_id);
+ AXIS2_FREE(env->allocator, request);
+ json_object_put(json_obj);
+ return NULL;
+ }
+
+ n_assets = json_object_array_length(assets_arr);
+ if (n_assets <= 0 || n_assets > FINBENCH_MAX_ASSETS) {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "ScenarioAnalysis: n_assets=%d out of range (max %d)",
+ n_assets, FINBENCH_MAX_ASSETS);
+ if (request->request_id) AXIS2_FREE(env->allocator, request->request_id);
+ AXIS2_FREE(env->allocator, request);
+ json_object_put(json_obj);
+ return NULL;
+ }
+
+ request->n_assets = n_assets;
+ request->assets = AXIS2_MALLOC(env->allocator,
+ n_assets * sizeof(finbench_asset_scenario_t));
+ if (!request->assets) {
+ if (request->request_id) AXIS2_FREE(env->allocator, request->request_id);
+ AXIS2_FREE(env->allocator, request);
+ json_object_put(json_obj);
+ return NULL;
+ }
+ memset(request->assets, 0, n_assets * sizeof(finbench_asset_scenario_t));
+
+ for (i = 0; i < n_assets; i++) {
+ json_object *asset_obj = json_object_array_get_idx(assets_arr, i);
+ json_object *field_obj = NULL;
+ finbench_asset_scenario_t *a = &request->assets[i];
+
+ if (!asset_obj) continue;
+
+ if (json_object_object_get_ex(asset_obj, "asset_id", &field_obj))
+ a->asset_id = (int64_t)json_object_get_int64(field_obj);
+ else
+ a->asset_id = (int64_t)(i + 1); /* auto-assign if missing */
+
+ if (json_object_object_get_ex(asset_obj, "current_price", &field_obj))
+ a->current_price = json_object_get_double(field_obj);
+
+ if (json_object_object_get_ex(asset_obj, "position_size", &field_obj))
+ a->position_size = json_object_get_double(field_obj);
+
+ /* scenarios array: [{price, probability}, ...] */
+ json_object *sc_arr = NULL;
+ if (json_object_object_get_ex(asset_obj, "scenarios", &sc_arr) &&
+ json_object_is_type(sc_arr, json_type_array)) {
+ int n_sc = json_object_array_length(sc_arr);
+ if (n_sc > FINBENCH_MAX_SCENARIOS) n_sc = FINBENCH_MAX_SCENARIOS;
+ a->n_scenarios = n_sc;
+ for (j = 0; j < n_sc; j++) {
+ json_object *sc = json_object_array_get_idx(sc_arr, j);
+ json_object *sc_field = NULL;
+ if (!sc) continue;
+ if (json_object_object_get_ex(sc, "price", &sc_field))
+ a->scenario_prices[j] = json_object_get_double(sc_field);
+ if (json_object_object_get_ex(sc, "probability", &sc_field))
+ a->probabilities[j] = json_object_get_double(sc_field);
+ }
+ }
+ }
+
+ json_object_put(json_obj);
+ AXIS2_LOG_INFO(env->log, "ScenarioAnalysis: Parsed %d assets", n_assets);
+ return request;
+}
+
+AXIS2_EXTERN void AXIS2_CALL
+finbench_scenario_request_free(
+ finbench_scenario_request_t *request,
+ const axutil_env_t *env)
+{
+ if (!request || !env) return;
+ if (request->assets) AXIS2_FREE(env->allocator, request->assets);
+ if (request->request_id) AXIS2_FREE(env->allocator, request->request_id);
+ AXIS2_FREE(env->allocator, request);
+}
+
+AXIS2_EXTERN finbench_scenario_response_t* AXIS2_CALL
+finbench_scenario_response_create(const axutil_env_t *env)
+{
+ finbench_scenario_response_t *response;
+ response = AXIS2_MALLOC(env->allocator, sizeof(finbench_scenario_response_t));
+ if (response) memset(response, 0, sizeof(finbench_scenario_response_t));
+ return response;
+}
+
+AXIS2_EXTERN void AXIS2_CALL
+finbench_scenario_response_free(
+ finbench_scenario_response_t *response,
+ const axutil_env_t *env)
+{
+ if (!response || !env) return;
+ if (response->status) AXIS2_FREE(env->allocator, response->status);
+ if (response->error_message) AXIS2_FREE(env->allocator, response->error_message);
+ if (response->lookup_method) AXIS2_FREE(env->allocator, response->lookup_method);
+ if (response->request_id) AXIS2_FREE(env->allocator, response->request_id);
+ AXIS2_FREE(env->allocator, response);
+}
+
+/**
+ * Core Scenario Analysis Calculation
+ *
+ * Step 1 — Financial computation: expected return, upside, downside per asset.
+ * Step 2 — O(n) linear search benchmark: scan array for each lookup.
+ * Step 3 — O(1) hash table benchmark: axutil_hash lookup for each query.
+ *
+ * Both timings always reported so the comparison is always visible.
+ */
+AXIS2_EXTERN finbench_scenario_response_t* AXIS2_CALL
+finbench_calculate_scenarios(
+ const axutil_env_t *env,
+ finbench_scenario_request_t *request)
+{
+ finbench_scenario_response_t *response = NULL;
+ axutil_hash_t *hash = NULL;
+ long linear_start, linear_end, hash_build_start, hash_build_end;
+ long hash_lookup_start, hash_lookup_end;
+ int i, j, q;
+ double total_upside = 0.0, total_downside = 0.0;
+ double portfolio_weighted_value = 0.0;
+ double portfolio_expected_return = 0.0;
+ double total_position_value = 0.0;
+ int n_lookups;
+ long linear_found = 0, hash_found = 0;
+ char key_buf[32];
+
+ response = finbench_scenario_response_create(env);
+ if (!response) return NULL;
+
+ if (!request || !request->assets || request->n_assets <= 0) {
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env,
+ "Invalid request: no assets provided");
+ return response;
+ }
+
+ /* -----------------------------------------------------------------------
+ * Step 1: Financial computation — expected return, upside, downside
+ * ----------------------------------------------------------------------- */
+ for (i = 0; i < request->n_assets; i++) {
+ finbench_asset_scenario_t *a = &request->assets[i];
+ double asset_expected_return = 0.0;
+ double asset_upside = 0.0, asset_downside = 0.0;
+ double asset_weighted_value = 0.0;
+
+ if (a->current_price <= 0.0 || a->n_scenarios <= 0) continue;
+
+ for (j = 0; j < a->n_scenarios; j++) {
+ double prob = a->probabilities[j];
+ double price = a->scenario_prices[j];
+ double ret = (price / a->current_price) - 1.0;
+
+ asset_expected_return += prob * ret;
+ asset_weighted_value += prob * price * a->position_size;
+
+ if (price > a->current_price)
+ asset_upside += prob * (price - a->current_price) * a->position_size;
+ else if (price < a->current_price)
+ asset_downside += prob * (a->current_price - price) * a->position_size;
+ }
+
+ double position_value = a->current_price * a->position_size;
+ portfolio_expected_return += asset_expected_return * position_value;
+ total_position_value += position_value;
+ portfolio_weighted_value += asset_weighted_value;
+ total_upside += asset_upside;
+ total_downside += asset_downside;
+ }
+
+ if (total_position_value > 0.0)
+ portfolio_expected_return /= total_position_value;
+
+ /* -----------------------------------------------------------------------
+ * Step 2: O(n) linear search benchmark
+ * Each query scans the array from index 0 until asset_id matches.
+ * n_lookups = n_assets × 10 to amplify the timing difference.
+ * ----------------------------------------------------------------------- */
+ n_lookups = request->n_assets * 10;
+ linear_start = get_time_us();
+
+ for (q = 0; q < n_lookups; q++) {
+ int64_t target_id = request->assets[q % request->n_assets].asset_id;
+ for (i = 0; i < request->n_assets; i++) {
+ if (request->assets[i].asset_id == target_id) {
+ linear_found++;
+ break;
+ }
+ }
+ }
+
+ linear_end = get_time_us();
+
+ /* -----------------------------------------------------------------------
+ * Step 3: O(1) hash table benchmark
+ * Build axutil_hash keyed by asset_id string, then perform same lookups.
+ * ----------------------------------------------------------------------- */
+ hash_build_start = get_time_us();
+ hash = axutil_hash_make(env);
+ if (hash) {
+ for (i = 0; i < request->n_assets; i++) {
+ snprintf(key_buf, sizeof(key_buf), "%" PRId64,
+ request->assets[i].asset_id);
+ axutil_hash_set(hash, key_buf, AXIS2_HASH_KEY_STRING,
+ &request->assets[i]);
+ }
+ }
+ hash_build_end = get_time_us();
+
+ hash_lookup_start = get_time_us();
+ if (hash) {
+ for (q = 0; q < n_lookups; q++) {
+ snprintf(key_buf, sizeof(key_buf), "%" PRId64,
+ request->assets[q % request->n_assets].asset_id);
+ finbench_asset_scenario_t *found =
+ axutil_hash_get(hash, key_buf, AXIS2_HASH_KEY_STRING);
+ if (found) hash_found++;
+ }
+ }
+ hash_lookup_end = get_time_us();
+
+ if (hash) axutil_hash_free(hash, env);
+
+ /* -----------------------------------------------------------------------
+ * Populate response
+ * ----------------------------------------------------------------------- */
+ long linear_us = linear_end - linear_start;
+ long hash_total_us = (hash_build_end - hash_build_start)
+ + (hash_lookup_end - hash_lookup_start);
+
+ response->status = axutil_strdup(env, FINBENCH_STATUS_SUCCESS);
+ response->expected_return = portfolio_expected_return;
+ response->weighted_value = portfolio_weighted_value;
+ response->upside_potential = total_upside;
+ response->downside_risk = total_downside;
+ response->upside_downside_ratio =
+ (total_downside > 0.0) ? (total_upside / total_downside) : 0.0;
+ response->calc_time_us = linear_us; /* primary timing: linear */
+ response->lookups_performed = n_lookups;
+ response->memory_used_kb = finbench_get_memory_usage_kb();
+
+ if (linear_us > 0)
+ response->lookups_per_second =
+ (double)n_lookups / (linear_us / 1000000.0);
+
+ /* Store hash timing in lookup_method string for demo output */
+ char method_buf[256];
+ snprintf(method_buf, sizeof(method_buf),
+ "linear_search_us=%ld hash_total_us=%ld speedup=%.1fx "
+ "(linear=%ld found, hash=%ld found, n_assets=%d, n_lookups=%d)",
+ linear_us, hash_total_us,
+ (hash_total_us > 0) ? (double)linear_us / hash_total_us : 0.0,
+ linear_found, hash_found,
+ request->n_assets, n_lookups);
+ response->lookup_method = axutil_strdup(env, method_buf);
+
+ if (request->request_id)
+ response->request_id = axutil_strdup(env, request->request_id);
+
+ AXIS2_LOG_INFO(env->log,
+ "ScenarioAnalysis: %d assets, %d lookups — linear=%ldus hash=%ldus "
+ "speedup=%.1fx E[r]=%.4f U/D=%.2f",
+ request->n_assets, n_lookups, linear_us, hash_total_us,
+ (hash_total_us > 0) ? (double)linear_us / hash_total_us : 0.0,
+ portfolio_expected_return,
+ response->upside_downside_ratio);
+
+ return response;
+}
+
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_scenario_response_to_json(
+ const finbench_scenario_response_t *response,
+ const axutil_env_t *env)
+{
+ json_object *json_resp;
+ const char *json_str;
+ axis2_char_t *result;
+
+ if (!response || !env) return NULL;
+
+ json_resp = json_object_new_object();
+
+ json_object_object_add(json_resp, "status",
+ json_object_new_string(response->status ? response->status : "UNKNOWN"));
+
+ json_object_object_add(json_resp, "expected_return",
+ json_object_new_double(response->expected_return));
+
+ json_object_object_add(json_resp, "weighted_value",
+ json_object_new_double(response->weighted_value));
+
+ json_object_object_add(json_resp, "upside_potential",
+ json_object_new_double(response->upside_potential));
+
+ json_object_object_add(json_resp, "downside_risk",
+ json_object_new_double(response->downside_risk));
+
+ json_object_object_add(json_resp, "upside_downside_ratio",
+ json_object_new_double(response->upside_downside_ratio));
+
+ json_object_object_add(json_resp, "calc_time_us",
+ json_object_new_int64(response->calc_time_us));
+
+ json_object_object_add(json_resp, "lookups_performed",
+ json_object_new_int64(response->lookups_performed));
+
+ json_object_object_add(json_resp, "lookups_per_second",
+ json_object_new_double(response->lookups_per_second));
+
+ json_object_object_add(json_resp, "memory_used_kb",
+ json_object_new_int(response->memory_used_kb));
+
+ if (response->lookup_method) {
+ json_object_object_add(json_resp, "lookup_benchmark",
+ json_object_new_string(response->lookup_method));
+ }
+
+ if (response->request_id) {
+ json_object_object_add(json_resp, "request_id",
+ json_object_new_string(response->request_id));
+ }
+
+ if (response->error_message) {
+ json_object_object_add(json_resp, "error_message",
+ json_object_new_string(response->error_message));
+ }
+
+ json_str = json_object_to_json_string_ext(json_resp, JSON_C_TO_STRING_PLAIN);
+ result = axutil_strdup(env, json_str);
+ json_object_put(json_resp);
+
+ return result;
+}
+
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_scenario_json_only(
+ const axutil_env_t *env,
+ const axis2_char_t *json_request)
+{
+ finbench_scenario_request_t *request;
+ finbench_scenario_response_t *response;
+ axis2_char_t *json_response;
+
+ request = finbench_scenario_request_create_from_json(env, json_request);
+ if (!request) {
+ return axutil_strdup(env,
+ "{\"status\":\"FAILED\","
+ "\"error_message\":\"Failed to parse scenario request\"}");
+ }
+
+ response = finbench_calculate_scenarios(env, request);
+ json_response = finbench_scenario_response_to_json(response, env);
+
+ finbench_scenario_request_free(request, env);
+ finbench_scenario_response_free(response, env);
+
+ return json_response;
+}
+
/* ============================================================================
* Service Metadata
* ============================================================================
@@ -913,7 +1337,7 @@ finbench_get_metadata_json(const axutil_env_t *env)
json_object_object_add(json_resp, "comparison_note",
json_object_new_string(
"This service runs on hardware with 1-2GB RAM where "
- "Java/WildFly (4-8GB minimum) cannot operate. "
+ "enterprise Java/WildFly (16-32GB per server minimum) cannot operate. "
"Demonstrates Axis2/C efficiency for edge computing."));
json_str = json_object_to_json_string_ext(json_resp, JSON_C_TO_STRING_PRETTY);
(axis-axis2-c-core) 04/21: Add array dimension validation to portfolioVariance
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit ecc9b4751f7b07897a778c146de6da39c4d35cc5
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 06:55:28 2026 -1000
Add array dimension validation to portfolioVariance
pyRAPI (the Python RAPI client) makes clear that quants will probe edge
cases: sending n_assets=500 with a 100-element weights array or a
100-element covariance matrix previously silently zero-filled the rest,
producing a mathematically meaningless variance with no error signal.
Changes:
- Add weights_provided and matrix_elements_provided tracking fields to
finbench_portfolio_variance_request_t (populated during JSON parse)
- Validate dimensions in finbench_calculate_portfolio_variance() before
any computation: weights.length must equal n_assets; matrix element
count must equal n_assets² (checked for both flat and 2D input formats)
- Error response names the mismatch explicitly: actual count, expected
count, and the correct format to use
- Header updated accordingly; compiles clean under -Wall -Wextra
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/financial_benchmark_service.c | 50 +++++++++++++++++++++-
.../src/financial_benchmark_service.h | 4 ++
2 files changed, 52 insertions(+), 2 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index 0ae11c4e5..f07a08487 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -215,10 +215,11 @@ finbench_portfolio_variance_request_create_from_json(
return NULL;
}
- /* Extract weights */
+ /* Extract weights — record actual length for dimension validation */
if (json_object_object_get_ex(json_obj, "weights", &array_obj) &&
json_object_is_type(array_obj, json_type_array)) {
int len = json_object_array_length(array_obj);
+ request->weights_provided = len;
for (i = 0; i < request->n_assets && i < len; i++) {
json_object *elem = json_object_array_get_idx(array_obj, i);
request->weights[i] = json_object_get_double(elem);
@@ -245,13 +246,15 @@ finbench_portfolio_variance_request_create_from_json(
json_object *first_elem = json_object_array_get_idx(array_obj, 0);
if (first_elem && json_object_is_type(first_elem, json_type_array)) {
- /* 2D array format: [[row0], [row1], ...] */
+ /* 2D array format: [[row0], [row1], ...] — record as flat element count */
AXIS2_LOG_INFO(env->log,
"FinBench: Parsing 2D covariance matrix format");
+ int total_elements = 0;
for (i = 0; i < request->n_assets && i < outer_len; i++) {
json_object *row = json_object_array_get_idx(array_obj, i);
if (row && json_object_is_type(row, json_type_array)) {
int row_len = json_object_array_length(row);
+ total_elements += row_len;
for (j = 0; j < request->n_assets && j < row_len; j++) {
json_object *cell = json_object_array_get_idx(row, j);
request->covariance_matrix[i * request->n_assets + j] =
@@ -259,10 +262,18 @@ finbench_portfolio_variance_request_create_from_json(
}
}
}
+ /* Account for any rows beyond n_assets (for mismatch detection) */
+ for (i = request->n_assets; i < outer_len; i++) {
+ json_object *row = json_object_array_get_idx(array_obj, i);
+ if (row && json_object_is_type(row, json_type_array))
+ total_elements += json_object_array_length(row);
+ }
+ request->matrix_elements_provided = total_elements;
} else {
/* Flat array format: [row0_col0, row0_col1, ..., row1_col0, ...] */
AXIS2_LOG_INFO(env->log,
"FinBench: Parsing flat covariance matrix format");
+ request->matrix_elements_provided = outer_len;
for (i = 0; i < (int)matrix_size && i < outer_len; i++) {
json_object *elem = json_object_array_get_idx(array_obj, i);
request->covariance_matrix[i] = json_object_get_double(elem);
@@ -366,6 +377,41 @@ finbench_calculate_portfolio_variance(
}
n = request->n_assets;
+
+ /* -----------------------------------------------------------------------
+ * Dimension validation — the kind of check a Python quant will probe
+ * immediately. Mismatched arrays silently produce wrong variance if not
+ * caught here: n_assets=500 with 100 weights zero-fills the rest, giving
+ * a mathematically meaningless result with no indication of bad input.
+ * ----------------------------------------------------------------------- */
+ if (request->weights_provided > 0 &&
+ request->weights_provided != n) {
+ char err_buf[256];
+ snprintf(err_buf, sizeof(err_buf),
+ "weights array length %d != n_assets %d. "
+ "Provide exactly n_assets weights summing to 1.0.",
+ request->weights_provided, n);
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, err_buf);
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench portfolioVariance: %s", err_buf);
+ return response;
+ }
+
+ if (request->matrix_elements_provided > 0 &&
+ request->matrix_elements_provided != n * n) {
+ char err_buf[256];
+ snprintf(err_buf, sizeof(err_buf),
+ "covariance_matrix element count %d != n_assets² (%d×%d=%d). "
+ "Provide a flat array of n_assets² elements or an n_assets×n_assets 2D array.",
+ request->matrix_elements_provided, n, n, n * n);
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, err_buf);
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench portfolioVariance: %s", err_buf);
+ return response;
+ }
+
start_time = get_time_us();
/*
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
index 8d2c74484..a67a213a9 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
@@ -91,6 +91,10 @@ typedef struct finbench_portfolio_variance_request
/** Request identifier for tracing */
char *request_id;
+ /** Actual array lengths as parsed (for dimension validation before computation) */
+ int weights_provided; /* length of weights[] in JSON */
+ int matrix_elements_provided; /* length of covariance_matrix[] in JSON (flat count) */
+
} finbench_portfolio_variance_request_t;
/**
(axis-axis2-c-core) 07/21: financial-benchmark: improve API for Python quant developers
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 908d2101bd7aae605d2b7b57739f13c26039b3dd
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 13:25:11 2026 -1000
financial-benchmark: improve API for Python quant developers
Five targeted improvements addressing the expectations of Python
quantitative developers who probe APIs with bad inputs immediately:
normalize_weights (portfolioVariance):
New bool field. When true, rescales weights in-place to sum to 1.0,
enabling callers to pass unnormalized exposures (e.g., notional
position values) without a client-side preprocessing step. When false
(default), weights that deviate from 1.0 by more than 1e-4 return a
descriptive error that names the actual sum and hints at the option.
Response adds weight_sum and weights_normalized fields for auditability.
n_periods_per_year (portfolioVariance + monteCarlo):
New int field (default 252) controlling annualization.
portfolioVariance: annualized_volatility = σ × sqrt(n_periods_per_year)
monteCarlo: GBM time step dt = 1/n_periods_per_year.
Previously hardcoded to 252; now callers can pass 260 (some fixed-
income conventions), 365 (crypto), 12 (monthly factor models), etc.
percentiles array (monteCarlo):
New float[] field (default [0.01, 0.05]). Each value p in (0,1)
produces a VaR entry: initial_value - sorted_final_values[p × n_sims].
Response adds percentile_vars: [{percentile, var}, ...] array.
Fixed var_95/var_99 fields are still always populated for compatibility.
Up to 8 percentile levels supported.
prob_tolerance (scenarioAnalysis):
New float field (default 1e-4, clamped to [1e-10, 0.1]).
Previously hardcoded as a preprocessor constant. Now callers with
externally-sourced probability data can loosen the tolerance without
modifying the service. Error message now prints the actual tolerance
used, making the validation contract explicit.
Better parse error messages (all three json_only entry points):
Previously returned generic "Failed to parse request". Now each
operation returns a field-level description of required and optional
parameters with their types and defaults.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/financial_benchmark_service.c | 213 +++++++++++++++++++--
.../src/financial_benchmark_service.h | 65 ++++++-
2 files changed, 256 insertions(+), 22 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index d5cdaa9dd..853c4a8ed 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -290,6 +290,22 @@ finbench_portfolio_variance_request_create_from_json(
}
}
+ /* normalize_weights — default false; set true to rescale weights to sum 1.0 */
+ if (json_object_object_get_ex(json_obj, "normalize_weights", &value_obj)) {
+ request->normalize_weights = json_object_get_boolean(value_obj)
+ ? AXIS2_TRUE : AXIS2_FALSE;
+ } else {
+ request->normalize_weights = AXIS2_FALSE;
+ }
+
+ /* n_periods_per_year — default 252 (equity trading days) */
+ if (json_object_object_get_ex(json_obj, "n_periods_per_year", &value_obj)) {
+ int npy = json_object_get_int(value_obj);
+ request->n_periods_per_year = (npy > 0) ? npy : 252;
+ } else {
+ request->n_periods_per_year = 252;
+ }
+
json_object_put(json_obj);
return request;
}
@@ -400,7 +416,7 @@ finbench_calculate_portfolio_variance(
char err_buf[256];
snprintf(err_buf, sizeof(err_buf),
"weights array length %d != n_assets %d. "
- "Provide exactly n_assets weights summing to 1.0.",
+ "Provide exactly n_assets weights (or set normalize_weights=true to rescale).",
request->weights_provided, n);
response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
response->error_message = axutil_strdup(env, err_buf);
@@ -432,6 +448,52 @@ finbench_calculate_portfolio_variance(
return response;
}
+ /* -----------------------------------------------------------------------
+ * Optional weight normalization.
+ *
+ * Compute weight sum first; if normalize_weights=true and sum != 1.0,
+ * rescale all weights in-place. This lets callers pass unnormalized
+ * exposures (e.g., notional values) without a preprocessing step.
+ * ----------------------------------------------------------------------- */
+ double weight_sum = 0.0;
+ for (i = 0; i < n; i++) {
+ weight_sum += request->weights[i];
+ }
+ response->weight_sum = weight_sum;
+ response->weights_normalized = AXIS2_FALSE;
+
+ if (request->normalize_weights) {
+ if (weight_sum <= 0.0) {
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env,
+ "normalize_weights=true but weights sum to zero or negative. "
+ "Cannot normalize a zero-weight portfolio.");
+ return response;
+ }
+ if (fabs(weight_sum - 1.0) > 1e-10) {
+ for (i = 0; i < n; i++) {
+ request->weights[i] /= weight_sum;
+ }
+ response->weights_normalized = AXIS2_TRUE;
+ AXIS2_LOG_INFO(env->log,
+ "FinBench portfolioVariance: normalized weights (sum was %.8f)", weight_sum);
+ }
+ } else {
+ /* Reject clearly non-unit weights (tolerance 1e-6 — tighter than prob check) */
+ if (fabs(weight_sum - 1.0) > 1e-4) {
+ char err_buf[256];
+ snprintf(err_buf, sizeof(err_buf),
+ "weights sum to %.8f, expected 1.0 (tolerance 1e-4). "
+ "Pass normalize_weights=true to rescale automatically.",
+ weight_sum);
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, err_buf);
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench portfolioVariance: %s", err_buf);
+ return response;
+ }
+ }
+
start_time = get_time_us();
/*
@@ -452,11 +514,13 @@ finbench_calculate_portfolio_variance(
end_time = get_time_us();
+ int npy = (request->n_periods_per_year > 0) ? request->n_periods_per_year : 252;
+
/* Populate response */
response->status = axutil_strdup(env, FINBENCH_STATUS_SUCCESS);
response->portfolio_variance = variance;
response->portfolio_volatility = sqrt(variance);
- response->annualized_volatility = sqrt(variance) * sqrt(252.0);
+ response->annualized_volatility = sqrt(variance) * sqrt((double)npy);
response->calc_time_us = end_time - start_time;
response->memory_used_kb = finbench_get_memory_usage_kb();
response->matrix_operations = ops;
@@ -516,6 +580,12 @@ finbench_portfolio_variance_response_to_json(
json_object_object_add(json_resp, "ops_per_second",
json_object_new_double(response->ops_per_second));
+ json_object_object_add(json_resp, "weight_sum",
+ json_object_new_double(response->weight_sum));
+
+ json_object_object_add(json_resp, "weights_normalized",
+ json_object_new_boolean(response->weights_normalized));
+
if (response->request_id) {
json_object_object_add(json_resp, "request_id",
json_object_new_string(response->request_id));
@@ -559,7 +629,12 @@ finbench_portfolio_variance_json_only(
request = finbench_portfolio_variance_request_create_from_json(env, json_request);
if (!request) {
return axutil_strdup(env,
- "{\"status\":\"FAILED\",\"error_message\":\"Failed to parse request\"}");
+ "{\"status\":\"FAILED\",\"error_message\":"
+ "\"Failed to parse portfolioVariance request. "
+ "Required fields: n_assets (int), weights (float[]), "
+ "covariance_matrix (float[n²] flat or float[n][n] 2D). "
+ "Optional: normalize_weights (bool), n_periods_per_year (int, default 252), "
+ "request_id (string).\"}");
}
response = finbench_calculate_portfolio_variance(env, request);
@@ -632,6 +707,38 @@ finbench_monte_carlo_request_create_from_json(
request->random_seed = (uint32_t)json_object_get_int(value_obj);
}
+ /* n_periods_per_year — controls GBM time step dt = 1/n_periods_per_year */
+ if (json_object_object_get_ex(json_obj, "n_periods_per_year", &value_obj)) {
+ int npy = json_object_get_int(value_obj);
+ request->n_periods_per_year = (npy > 0) ? npy : 252;
+ } else {
+ request->n_periods_per_year = 252;
+ }
+
+ /* percentiles — array of tail levels in (0,1); default {0.01, 0.05} */
+ {
+ json_object *pct_arr = NULL;
+ if (json_object_object_get_ex(json_obj, "percentiles", &pct_arr) &&
+ json_object_is_type(pct_arr, json_type_array)) {
+ int n_pct = json_object_array_length(pct_arr);
+ if (n_pct > 8) n_pct = 8;
+ request->n_percentiles = 0;
+ int pi;
+ for (pi = 0; pi < n_pct; pi++) {
+ json_object *pct_elem = json_object_array_get_idx(pct_arr, pi);
+ double p = json_object_get_double(pct_elem);
+ if (p > 0.0 && p < 1.0) {
+ request->percentiles[request->n_percentiles++] = p;
+ }
+ }
+ } else {
+ /* Default: 1% and 5% VaR levels */
+ request->percentiles[0] = 0.01;
+ request->percentiles[1] = 0.05;
+ request->n_percentiles = 2;
+ }
+ }
+
if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
const char *rid = json_object_get_string(value_obj);
if (rid) request->request_id = axutil_strdup(env, rid);
@@ -730,7 +837,10 @@ finbench_run_monte_carlo(
}
/* Pre-calculate constants */
- dt = 1.0 / 252.0; /* Daily time step */
+ {
+ int npy = (request->n_periods_per_year > 0) ? request->n_periods_per_year : 252;
+ dt = 1.0 / (double)npy;
+ }
drift = (request->expected_return - 0.5 * request->volatility * request->volatility) * dt;
vol_sqrt_dt = request->volatility * sqrt(dt);
@@ -782,17 +892,38 @@ finbench_run_monte_carlo(
/* Sort for percentiles */
qsort(final_values, request->n_simulations, sizeof(double), compare_doubles);
- int idx_5 = (int)(0.05 * request->n_simulations);
- int idx_1 = (int)(0.01 * request->n_simulations);
- int idx_50 = request->n_simulations / 2;
+ int n_sims = request->n_simulations;
+ int idx_5 = (int)(0.05 * n_sims);
+ int idx_1 = (int)(0.01 * n_sims);
+ int idx_50 = n_sims / 2;
/* Calculate CVaR (Expected Shortfall) - average of worst 5% */
double cvar_sum = 0.0;
- for (int i = 0; i < idx_5; i++) {
- cvar_sum += final_values[i];
+ {
+ int ci;
+ for (ci = 0; ci < idx_5; ci++) {
+ cvar_sum += final_values[ci];
+ }
}
double cvar_95 = (idx_5 > 0) ? (cvar_sum / idx_5) : final_values[0];
+ /* Compute caller-requested percentile VaR values */
+ response->n_percentiles = 0;
+ {
+ int pi;
+ int n_pct = (request->n_percentiles > 8) ? 8 : request->n_percentiles;
+ for (pi = 0; pi < n_pct; pi++) {
+ double p = request->percentiles[pi];
+ if (p <= 0.0 || p >= 1.0) continue;
+ int idx = (int)(p * n_sims);
+ if (idx >= n_sims) idx = n_sims - 1;
+ response->percentile_levels[response->n_percentiles] = p;
+ response->var_at_percentile[response->n_percentiles] =
+ request->initial_value - final_values[idx];
+ response->n_percentiles++;
+ }
+ }
+
/* Populate response */
response->status = axutil_strdup(env, FINBENCH_STATUS_SUCCESS);
response->mean_final_value = mean;
@@ -876,6 +1007,21 @@ finbench_monte_carlo_response_to_json(
json_object_object_add(json_resp, "memory_used_kb",
json_object_new_int(response->memory_used_kb));
+ /* Emit caller-specified percentile VaR values as a structured array */
+ if (response->n_percentiles > 0) {
+ json_object *pct_array = json_object_new_array();
+ int pi;
+ for (pi = 0; pi < response->n_percentiles; pi++) {
+ json_object *entry = json_object_new_object();
+ json_object_object_add(entry, "percentile",
+ json_object_new_double(response->percentile_levels[pi]));
+ json_object_object_add(entry, "var",
+ json_object_new_double(response->var_at_percentile[pi]));
+ json_object_array_add(pct_array, entry);
+ }
+ json_object_object_add(json_resp, "percentile_vars", pct_array);
+ }
+
if (response->request_id) {
json_object_object_add(json_resp, "request_id",
json_object_new_string(response->request_id));
@@ -905,7 +1051,17 @@ finbench_monte_carlo_json_only(
request = finbench_monte_carlo_request_create_from_json(env, json_request);
if (!request) {
return axutil_strdup(env,
- "{\"status\":\"FAILED\",\"error_message\":\"Failed to parse request\"}");
+ "{\"status\":\"FAILED\",\"error_message\":"
+ "\"Failed to parse monteCarlo request. "
+ "All fields optional with defaults: "
+ "n_simulations (int, default 10000, max 1000000), "
+ "n_periods (int, default 252), "
+ "initial_value (float, default 1000000.0), "
+ "expected_return (float, default 0.08), "
+ "volatility (float, default 0.20), "
+ "random_seed (int, default 0=random), "
+ "n_periods_per_year (int, default 252), "
+ "percentiles (float[], default [0.01, 0.05]).\"}");
}
response = finbench_run_monte_carlo(env, request);
@@ -967,6 +1123,19 @@ finbench_scenario_request_create_from_json(
request->use_hash_lookup = AXIS2_TRUE;
}
+ /*
+ * prob_tolerance — probability sum tolerance per asset.
+ * Default 1e-4; 0.0 → default; clamped to [1e-10, 0.1].
+ */
+ if (json_object_object_get_ex(json_obj, "prob_tolerance", &value_obj)) {
+ double tol = json_object_get_double(value_obj);
+ if (tol <= 0.0) tol = 1e-4;
+ if (tol > 0.1) tol = 0.1;
+ request->prob_tolerance = tol;
+ } else {
+ request->prob_tolerance = 1e-4;
+ }
+
/* request_id */
if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
const char *rid = json_object_get_string(value_obj);
@@ -1133,7 +1302,9 @@ finbench_calculate_scenarios(
* differences in JSON-encoded probabilities, but tight enough to catch
* missing or miscounted scenarios (e.g., three scenarios summing to 0.85).
* ----------------------------------------------------------------------- */
-#define FINBENCH_PROB_SUM_TOLERANCE 1e-4
+ double prob_tolerance = (request->prob_tolerance > 0.0)
+ ? request->prob_tolerance : 1e-4;
+
for (i = 0; i < request->n_assets; i++) {
finbench_asset_scenario_t *a = &request->assets[i];
double prob_sum = 0.0;
@@ -1145,15 +1316,16 @@ finbench_calculate_scenarios(
prob_sum += a->probabilities[k];
}
- if (prob_sum < (1.0 - FINBENCH_PROB_SUM_TOLERANCE) ||
- prob_sum > (1.0 + FINBENCH_PROB_SUM_TOLERANCE)) {
+ if (prob_sum < (1.0 - prob_tolerance) ||
+ prob_sum > (1.0 + prob_tolerance)) {
char err_buf[256];
snprintf(err_buf, sizeof(err_buf),
"Asset index %d (id=%" PRId64 "): scenario probabilities sum to "
- "%.8f, expected 1.0 (tolerance %.0e). "
- "All %d scenario probabilities must sum to exactly 1.0.",
- i, a->asset_id, prob_sum, FINBENCH_PROB_SUM_TOLERANCE,
+ "%.8f, expected 1.0 (tolerance %.2g). "
+ "All %d scenario probabilities must sum to exactly 1.0. "
+ "Pass prob_tolerance to adjust validation strictness.",
+ i, a->asset_id, prob_sum, prob_tolerance,
a->n_scenarios);
response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
response->error_message = axutil_strdup(env, err_buf);
@@ -1404,8 +1576,13 @@ finbench_scenario_json_only(
request = finbench_scenario_request_create_from_json(env, json_request);
if (!request) {
return axutil_strdup(env,
- "{\"status\":\"FAILED\","
- "\"error_message\":\"Failed to parse scenario request\"}");
+ "{\"status\":\"FAILED\",\"error_message\":"
+ "\"Failed to parse scenarioAnalysis request. "
+ "Required: assets (array of {asset_id, current_price, position_size, "
+ "scenarios: [{price, probability}]}). "
+ "Optional: use_hash_lookup (bool, default true), "
+ "prob_tolerance (float, default 0.0001), "
+ "request_id (string).\"}");
}
response = finbench_calculate_scenarios(env, request);
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
index a67a213a9..6ac07b6cd 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
@@ -95,6 +95,21 @@ typedef struct finbench_portfolio_variance_request
int weights_provided; /* length of weights[] in JSON */
int matrix_elements_provided; /* length of covariance_matrix[] in JSON (flat count) */
+ /**
+ * Normalize weights to sum to 1.0 before computing variance.
+ * When AXIS2_TRUE, weights are rescaled in-place if their sum != 1.0.
+ * When AXIS2_FALSE (default), weights that do not sum to 1.0 return an error.
+ * Quants often work with unnormalized exposures; this avoids a pre-processing step.
+ */
+ axis2_bool_t normalize_weights;
+
+ /**
+ * Trading periods per year used for annualizing volatility.
+ * Default: 252 (equity trading days). Use 260 for some fixed-income conventions,
+ * 365 for crypto, or 12 for monthly data.
+ */
+ int n_periods_per_year;
+
} finbench_portfolio_variance_request_t;
/**
@@ -111,9 +126,15 @@ typedef struct finbench_portfolio_variance_response
/** Portfolio volatility (σ = sqrt(variance)) */
double portfolio_volatility;
- /** Annualized volatility (σ × sqrt(252)) */
+ /** Annualized volatility (σ × sqrt(n_periods_per_year)) */
double annualized_volatility;
+ /** Actual sum of weights as provided (before any normalization) */
+ double weight_sum;
+
+ /** Whether weights were renormalized to 1.0 (only when normalize_weights=true) */
+ axis2_bool_t weights_normalized;
+
/** Processing time in microseconds */
long calc_time_us;
@@ -168,6 +189,24 @@ typedef struct finbench_monte_carlo_request
/** Random seed for reproducibility (0 = use time) */
uint32_t random_seed;
+ /**
+ * Trading periods per year — controls the GBM time step (dt = 1/n_periods_per_year).
+ * Default: 252 (equity trading days). Must match the frequency of expected_return
+ * and volatility inputs (both must be annualized).
+ */
+ int n_periods_per_year;
+
+ /**
+ * Percentile levels for VaR reporting (values in [0,1], e.g. 0.01 for 1%).
+ * Defaults: {0.01, 0.05} — the 99% and 95% VaR levels.
+ * Up to FINBENCH_MAX_PERCENTILES values; extras are ignored.
+ * Each percentile p produces: VaR_p = initial_value - sorted_final_values[p * n_sims].
+ */
+ double percentiles[8];
+
+ /** Number of entries in percentiles[]. 0 → use defaults {0.01, 0.05}. */
+ int n_percentiles;
+
/** Request identifier */
char *request_id;
@@ -190,15 +229,24 @@ typedef struct finbench_monte_carlo_response
/** Standard deviation of final values */
double std_dev_final_value;
- /** Value at Risk 95% (5th percentile loss) */
+ /** Value at Risk 95% (5th percentile loss) — always populated */
double var_95;
- /** Value at Risk 99% (1st percentile loss) */
+ /** Value at Risk 99% (1st percentile loss) — always populated */
double var_99;
- /** Conditional VaR 95% (Expected Shortfall) */
+ /** Conditional VaR 95% (Expected Shortfall) — always populated */
double cvar_95;
+ /**
+ * Per-request percentile VaR values (parallel to request->percentiles[]).
+ * var_at_percentile[i] = initial_value - sorted_final_values[percentiles[i] * n_sims]
+ * Count is stored in n_percentiles; up to 8 entries.
+ */
+ double var_at_percentile[8];
+ double percentile_levels[8];
+ int n_percentiles;
+
/** Maximum drawdown observed */
double max_drawdown;
@@ -268,6 +316,15 @@ typedef struct finbench_scenario_request
/** Enable hash table optimization (for comparison) */
axis2_bool_t use_hash_lookup;
+ /**
+ * Tolerance for probability sum validation.
+ * Each asset's scenario probabilities must sum to 1.0 within this tolerance.
+ * Default: 1e-4 (0.01%) — accommodates JSON representation rounding while
+ * catching genuinely miscounted scenarios (e.g., three scenarios summing to 0.85).
+ * Pass 0.0 to use the default. Values > 0.1 are clamped to 0.1.
+ */
+ double prob_tolerance;
+
/** Request identifier */
char *request_id;
(axis-axis2-c-core) 17/21: D2 secure JSON fault responses with /dev/urandom correlation IDs
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 0ca7d8d030cc0c046ce1b9e08c4b237f266b34cc
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 06:51:32 2026 -1000
D2 secure JSON fault responses with /dev/urandom correlation IDs
New module: axis2_json_secure_fault.{h,c}
- axis2_json_corr_id_generate(): reads 8 bytes from /dev/urandom,
encodes as 16-char lowercase hex (e.g. "e1a2b3c4d5e6f7a8").
Falls back to XOR-mixed clock/time/pid when /dev/urandom is
unavailable (Android SELinux, embedded targets).
- axis2_json_secure_fault(): returns heap-allocated JSON:
{"status":"ERROR","correlation_id":"<id>","error":"<safe_msg>"}
All values go through json_object_new_string() — no injection risk.
Returns a static fallback string (never NULL) on OOM.
mcp_catalog_handler.c: conf==NULL, json_object OOM, axutil_strdup OOM,
and serialisation failure paths all emit a correlation ID to the log
and return axis2_json_secure_fault() instead of NULL / bare {"tools":[]}.
financial_benchmark_service_handler.c: route_operation() "no operation"
and "unknown operation" paths use axis2_json_secure_fault(); the
operation name is no longer echoed in the response body (prevents
client-side operation enumeration).
Makefile.am: axis2_json_secure_fault.c added to SOURCES,
axis2_json_secure_fault.h to include_HEADERS and EXTRA_DIST.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/Makefile.am | 7 +-
.../src/axis2_json_secure_fault.c | 153 +++++++++++++++++++++
.../src/axis2_json_secure_fault.h | 109 +++++++++++++++
.../src/financial_benchmark_service_handler.c | 22 +--
.../src/mcp_catalog_handler.c | 44 +++++-
5 files changed, 320 insertions(+), 15 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/Makefile.am b/samples/user_guide/financial-benchmark-service/Makefile.am
index 4d831be25..0f10b9102 100644
--- a/samples/user_guide/financial-benchmark-service/Makefile.am
+++ b/samples/user_guide/financial-benchmark-service/Makefile.am
@@ -30,7 +30,8 @@ lib_LTLIBRARIES = libfinancial_benchmark_service.la
libfinancial_benchmark_service_la_SOURCES = \
src/financial_benchmark_service.c \
src/financial_benchmark_service_handler.c \
- src/mcp_catalog_handler.c
+ src/mcp_catalog_handler.c \
+ src/axis2_json_secure_fault.c
libfinancial_benchmark_service_la_CPPFLAGS = \
-I$(top_srcdir)/include \
@@ -91,7 +92,8 @@ services_DATA = services.xml
include_HEADERS = \
src/financial_benchmark_service.h \
src/finbench_mcp.h \
- src/mcp_catalog_handler.h
+ src/mcp_catalog_handler.h \
+ src/axis2_json_secure_fault.h
# Extra files to distribute
EXTRA_DIST = \
@@ -100,6 +102,7 @@ EXTRA_DIST = \
src/financial_benchmark_service_handler.c \
src/finbench_mcp.h \
src/mcp_catalog_handler.h \
+ src/axis2_json_secure_fault.h \
README.md
endif # AXIS2_JSON_ENABLED
diff --git a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
new file mode 100644
index 000000000..568099c29
--- /dev/null
+++ b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file axis2_json_secure_fault.c
+ * @brief Secure JSON error responses with /dev/urandom correlation IDs (D2)
+ *
+ * Design notes:
+ *
+ * /dev/urandom vs /dev/random:
+ * We use /dev/urandom (non-blocking) intentionally. /dev/random can block
+ * on low-entropy systems (headless servers, embedded devices). Correlation
+ * IDs need uniqueness, not cryptographic-grade entropy.
+ *
+ * Fallback PRNG:
+ * On Android (and other restricted environments) /dev/urandom may be
+ * unavailable due to SELinux policy. The fallback mixes clock(), time(),
+ * and getpid() with XOR shifts to produce a usable 64-bit value. It is
+ * not cryptographically random but is adequate for log correlation.
+ *
+ * JSON safety:
+ * Both corr_id and safe_message are inserted via json_object_new_string()
+ * which handles all JSON escaping internally. No manual string building.
+ */
+
+#include "axis2_json_secure_fault.h"
+
+#include <axutil_string.h>
+#include <json-c/json.h>
+
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <time.h>
+
+#ifdef __linux__
+#include <unistd.h> /* getpid() */
+#endif
+
+/* ============================================================================
+ * Correlation ID generation
+ * ============================================================================
+ */
+
+AXIS2_EXTERN void AXIS2_CALL
+axis2_json_corr_id_generate(char *buf, size_t len)
+{
+ if (!buf || len < AXIS2_JSON_CORR_ID_LEN) {
+ if (buf && len > 0) buf[0] = '\0';
+ return;
+ }
+
+ uint8_t rand_bytes[8];
+ int got_random = 0;
+
+ /* Primary source: /dev/urandom (non-blocking kernel CSPRNG) */
+ FILE *urandom = fopen("/dev/urandom", "rb");
+ if (urandom) {
+ size_t nread = fread(rand_bytes, 1, sizeof(rand_bytes), urandom);
+ fclose(urandom);
+ if (nread == sizeof(rand_bytes)) {
+ got_random = 1;
+ }
+ }
+
+ if (!got_random) {
+ /* Fallback: mix time, clock, and pid with XOR shifts.
+ * Not cryptographically random — adequate for log correlation only. */
+ uint64_t v = (uint64_t)time(NULL);
+ v ^= (uint64_t)clock() << 17;
+#ifdef __linux__
+ v ^= (uint64_t)getpid() << 32;
+#endif
+ /* XOR-shift to spread bits */
+ v ^= v >> 13;
+ v ^= v << 7;
+ v ^= v >> 17;
+ memcpy(rand_bytes, &v, sizeof(rand_bytes));
+ }
+
+ /* Encode as 16 lowercase hex digits */
+ static const char hex[] = "0123456789abcdef";
+ for (int i = 0; i < 8; i++) {
+ buf[i * 2] = hex[(rand_bytes[i] >> 4) & 0x0f];
+ buf[i * 2 + 1] = hex[ rand_bytes[i] & 0x0f];
+ }
+ buf[16] = '\0';
+}
+
+
+/* ============================================================================
+ * Secure JSON fault response
+ * ============================================================================
+ */
+
+/* Returned verbatim when json-c allocation fails (static, never freed) */
+static const char FAULT_ALLOC_FALLBACK[] =
+ "{\"status\":\"ERROR\",\"error\":\"internal error\"}";
+
+AXIS2_EXTERN axis2_char_t *AXIS2_CALL
+axis2_json_secure_fault(
+ const axutil_env_t *env,
+ const char *corr_id,
+ const char *safe_message)
+{
+ const char *safe_corr = corr_id ? corr_id : "unknown";
+ const char *safe_msg = safe_message ? safe_message
+ : "an internal error occurred";
+
+ json_object *root = json_object_new_object();
+ if (!root) {
+ /* json-c allocation failed — return static fallback (never NULL) */
+ return (axis2_char_t *)(uintptr_t)FAULT_ALLOC_FALLBACK;
+ }
+
+ json_object_object_add(root, "status",
+ json_object_new_string("ERROR"));
+ json_object_object_add(root, "correlation_id",
+ json_object_new_string(safe_corr));
+ json_object_object_add(root, "error",
+ json_object_new_string(safe_msg));
+
+ const char *json_cstr = json_object_to_json_string_ext(
+ root, JSON_C_TO_STRING_PLAIN);
+
+ axis2_char_t *result = NULL;
+ if (json_cstr && env) {
+ result = axutil_strdup(env, json_cstr);
+ }
+
+ json_object_put(root); /* free entire tree */
+
+ if (!result) {
+ /* axutil_strdup failed or env was NULL */
+ return (axis2_char_t *)(uintptr_t)FAULT_ALLOC_FALLBACK;
+ }
+
+ return result;
+}
diff --git a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h
new file mode 100644
index 000000000..dd21293fd
--- /dev/null
+++ b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef AXIS2_JSON_SECURE_FAULT_H
+#define AXIS2_JSON_SECURE_FAULT_H
+
+/**
+ * @file axis2_json_secure_fault.h
+ * @brief Secure JSON error responses with /dev/urandom correlation IDs (D2)
+ *
+ * Provides two building blocks:
+ *
+ * 1. axis2_json_corr_id_generate()
+ * Reads 8 random bytes from /dev/urandom and encodes them as a 16-char
+ * lowercase hex string (e.g. "e1a2b3c4d5e6f7a8"). Falls back to a
+ * timestamp-seeded value if /dev/urandom is unavailable (embedded targets).
+ *
+ * 2. axis2_json_secure_fault()
+ * Returns a heap-allocated JSON error string:
+ * {"status":"ERROR","correlation_id":"<id>","error":"<safe_msg>"}
+ * The correlation_id should be logged alongside internal diagnostic
+ * details that must not appear in the HTTP response body.
+ *
+ * Usage pattern:
+ * @code
+ * char corr_id[17];
+ * axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
+ * AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ * "[%s] catalog generation failed: %s", corr_id, internal_detail);
+ * return axis2_json_secure_fault(env, corr_id,
+ * "catalog generation failed — see server log");
+ * @endcode
+ *
+ * Caller owns the returned string and must free it with AXIS2_FREE.
+ */
+
+#include <axutil_env.h>
+#include <axis2_defines.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Minimum buffer size for axis2_json_corr_id_generate() output:
+ * 16 hex digits + NUL terminator.
+ */
+#define AXIS2_JSON_CORR_ID_LEN 17
+
+/**
+ * Generate a random correlation ID string.
+ *
+ * Reads 8 bytes from /dev/urandom, formats them as 16 lowercase hex digits,
+ * and writes the NUL-terminated result to @p buf.
+ *
+ * Falls back to a XOR mix of clock() and time() when /dev/urandom cannot be
+ * opened (e.g. Android devices with restricted /dev access). The fallback is
+ * NOT cryptographically random but provides reasonable uniqueness for log
+ * correlation purposes.
+ *
+ * @param buf Output buffer; must be at least AXIS2_JSON_CORR_ID_LEN bytes.
+ * @param len Size of @p buf in bytes.
+ */
+AXIS2_EXTERN void AXIS2_CALL
+axis2_json_corr_id_generate(char *buf, size_t len);
+
+/**
+ * Build a secure JSON fault response.
+ *
+ * Returns a heap-allocated string of the form:
+ * {"status":"ERROR","correlation_id":"<corr_id>","error":"<safe_message>"}
+ *
+ * Both @p corr_id and @p safe_message are JSON-escaped by json-c so they
+ * cannot inject content into the response. @p safe_message MUST NOT contain
+ * internal detail (stack traces, file paths, SQL, etc.) — only a human-
+ * readable description suitable for API consumers.
+ *
+ * @param env Axis2/C environment (allocator used for the result).
+ * @param corr_id 16-char hex correlation ID from axis2_json_corr_id_generate().
+ * NULL produces "unknown" in the response.
+ * @param safe_message Public-safe error description. NULL uses a generic message.
+ * @return Heap-allocated JSON string; caller frees with AXIS2_FREE.
+ * Returns a minimal static fallback on allocation failure (never NULL).
+ */
+AXIS2_EXTERN axis2_char_t *AXIS2_CALL
+axis2_json_secure_fault(
+ const axutil_env_t *env,
+ const char *corr_id,
+ const char *safe_message);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* AXIS2_JSON_SECURE_FAULT_H */
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
index 3a205329b..ceaf7f84b 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
@@ -29,6 +29,7 @@
#include "financial_benchmark_service.h"
#include "mcp_catalog_handler.h"
+#include "axis2_json_secure_fault.h"
#include <axis2_svc.h>
#include <axis2_msg_ctx.h>
#include <axis2_conf_ctx.h>
@@ -54,7 +55,12 @@ route_operation(
if (!operation_name)
{
- return axutil_strdup(env, "{\"error\":\"No operation specified\"}");
+ char corr_id[AXIS2_JSON_CORR_ID_LEN];
+ axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
+ AXIS2_LOG_WARNING(env->log, AXIS2_LOG_SI,
+ "[FinancialBenchmark][%s] route_operation: no operation name",
+ corr_id);
+ return axis2_json_secure_fault(env, corr_id, "no operation specified");
}
/* portfolioVariance operation */
@@ -102,14 +108,14 @@ route_operation(
return finbench_scenario_json_only(env, json_request);
}
- /* Unknown operation */
+ /* Unknown operation — log the name internally; do not echo it in the
+ * response body (prevents operation-name enumeration by clients). */
+ char corr_id[AXIS2_JSON_CORR_ID_LEN];
+ axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
AXIS2_LOG_WARNING(env->log, AXIS2_LOG_SI,
- "FinancialBenchmarkService: Unknown operation '%s'", operation_name);
-
- char error_msg[256];
- snprintf(error_msg, sizeof(error_msg),
- "{\"error\":\"Unknown operation: %s\"}", operation_name);
- return axutil_strdup(env, error_msg);
+ "[FinancialBenchmark][%s] unknown operation '%s'",
+ corr_id, operation_name);
+ return axis2_json_secure_fault(env, corr_id, "unknown operation");
}
/**
diff --git a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
index 2181df49c..b1720fa83 100644
--- a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
@@ -44,6 +44,7 @@
*/
#include "mcp_catalog_handler.h"
+#include "axis2_json_secure_fault.h"
#include <axis2_svc.h>
#include <axis2_op.h>
@@ -225,11 +226,28 @@ mcp_catalog_generate_json(
axis2_conf_t *conf,
const axutil_env_t *env)
{
- if (!conf || !env) return NULL;
+ if (!env) return NULL;
+ if (!conf) {
+ char corr_id[AXIS2_JSON_CORR_ID_LEN];
+ axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "[MCP][%s] mcp_catalog_generate_json: conf is NULL — "
+ "cannot walk services", corr_id);
+ return axis2_json_secure_fault(env, corr_id,
+ "catalog generation failed — see server log");
+ }
/* ── Root object ─────────────────────────────────────────────────────── */
json_object *root = json_object_new_object();
- if (!root) return NULL;
+ if (!root) {
+ char corr_id[AXIS2_JSON_CORR_ID_LEN];
+ axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "[MCP][%s] json_object_new_object() returned NULL — "
+ "out of memory", corr_id);
+ return axis2_json_secure_fault(env, corr_id,
+ "catalog generation failed — see server log");
+ }
/* ── _meta block ─────────────────────────────────────────────────────── */
json_object *meta = json_object_new_object();
@@ -382,11 +400,27 @@ mcp_catalog_generate_json(
axis2_char_t *result = NULL;
if (json_cstr) {
result = axutil_strdup(env, json_cstr);
+ if (!result) {
+ /* axutil_strdup OOM — log with correlation ID and return fault */
+ char corr_id[AXIS2_JSON_CORR_ID_LEN];
+ axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "[MCP][%s] axutil_strdup failed serialising catalog "
+ "(%zu bytes) — out of memory",
+ corr_id, strlen(json_cstr));
+ json_object_put(root);
+ return axis2_json_secure_fault(env, corr_id,
+ "catalog generation failed — see server log");
+ }
} else {
+ char corr_id[AXIS2_JSON_CORR_ID_LEN];
+ axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
- "[MCP] json_object_to_json_string_ext returned NULL — "
- "returning fallback catalog");
- result = axutil_strdup(env, "{\"tools\":[]}");
+ "[MCP][%s] json_object_to_json_string_ext returned NULL",
+ corr_id);
+ json_object_put(root);
+ return axis2_json_secure_fault(env, corr_id,
+ "catalog generation failed — see server log");
}
/* json_object_put frees the entire tree including all children */
(axis-axis2-c-core) 16/21: D1 MCP catalog intercept wired into financial_benchmark_service_handler
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 98b59588a4bc35b3c393ddc6dc5786b2e1698dc2
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 06:23:44 2026 -1000
D1 MCP catalog intercept wired into financial_benchmark_service_handler
* financial_benchmark_service_handler.c: at the top of the invoke function,
check the incoming request URI (via axis2_msg_ctx_get_to) for
MCP_CATALOG_PATH ("/_mcp/openapi-mcp.json"). On match, fetch
axis2_conf_t via conf_ctx and return mcp_catalog_generate_json()
output directly (caller frees via AXIS2_FREE).
* financial_benchmark_service.c: renamed the json_object* invoke
function to finbench_dispatch_json_obj() (file-local static) to
eliminate the duplicate symbol that would have prevented the
handler.c entry point from linking.
* Makefile.am: add financial_benchmark_service_handler.c to
libfinancial_benchmark_service_la_SOURCES so the handler entry
points (axis2_get_instance, financial_benchmark_service_invoke_json,
axis2_remove_instance) are compiled into the service library.
Accessible at: GET /services/FinancialBenchmarkService/_mcp/openapi-mcp.json
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/Makefile.am | 2 ++
.../src/financial_benchmark_service.c | 17 +++++-----
.../src/financial_benchmark_service_handler.c | 36 ++++++++++++++++++++++
3 files changed, 47 insertions(+), 8 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/Makefile.am b/samples/user_guide/financial-benchmark-service/Makefile.am
index f7bd83322..4d831be25 100644
--- a/samples/user_guide/financial-benchmark-service/Makefile.am
+++ b/samples/user_guide/financial-benchmark-service/Makefile.am
@@ -29,6 +29,7 @@ lib_LTLIBRARIES = libfinancial_benchmark_service.la
libfinancial_benchmark_service_la_SOURCES = \
src/financial_benchmark_service.c \
+ src/financial_benchmark_service_handler.c \
src/mcp_catalog_handler.c
libfinancial_benchmark_service_la_CPPFLAGS = \
@@ -96,6 +97,7 @@ include_HEADERS = \
EXTRA_DIST = \
services.xml \
src/financial_benchmark_service.h \
+ src/financial_benchmark_service_handler.c \
src/finbench_mcp.h \
src/mcp_catalog_handler.h \
README.md
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index d4a7e4db6..535bc463c 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -1737,21 +1737,22 @@ finbench_generate_test_portfolio_json(
}
/* ============================================================================
- * JSON Service Entry Point - Matches Camera Service Pattern
+ * JSON Object Dispatcher - json_object-based calling convention
*
- * This is the entry point that axis2_json_rpc_msg_recv calls for JSON requests.
- * Function naming convention: {service_name}_invoke_json
+ * This is used by axis2_json_rpc_msg_recv when it passes a parsed json_object
+ * directly. The exported entry point for string-based callers and for
+ * the Axis2/C dynamic loader is in financial_benchmark_service_handler.c
* ============================================================================
*/
/**
- * @brief JSON Service Entry Point - Routes requests to appropriate operation
+ * @brief JSON object dispatcher — routes a pre-parsed request to the right op.
*
- * This function is called by the Axis2/C JSON RPC message receiver.
- * It parses the incoming JSON request and routes to the appropriate operation.
+ * Internal function; the public axis2_json_rpc_msg_recv entry point lives in
+ * financial_benchmark_service_handler.c where msg_ctx is available.
*/
-AXIS2_EXTERN json_object* AXIS2_CALL
-financial_benchmark_service_invoke_json(
+static json_object *
+finbench_dispatch_json_obj(
const axutil_env_t *env,
json_object *json_request)
{
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
index 482288e7b..3a205329b 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
@@ -28,8 +28,11 @@
*/
#include "financial_benchmark_service.h"
+#include "mcp_catalog_handler.h"
#include <axis2_svc.h>
#include <axis2_msg_ctx.h>
+#include <axis2_conf_ctx.h>
+#include <axis2_endpoint_ref.h>
#include <axis2_http_transport.h>
#include <json-c/json.h>
#include <axutil_string.h>
@@ -128,6 +131,39 @@ financial_benchmark_service_invoke_json(
AXIS2_LOG_INFO(env->log,
"FinancialBenchmarkService: Processing HTTP/2 JSON request");
+ /* ── MCP catalog intercept ──────────────────────────────────────────────
+ * GET /services/FinancialBenchmarkService/_mcp/openapi-mcp.json
+ *
+ * Must run before operation dispatch so it works even with no JSON body.
+ * The catalog is generated by walking all deployed services via conf,
+ * which mirrors the Java OpenApiSpecGenerator.generateMcpCatalogJson().
+ * ────────────────────────────────────────────────────────────────────── */
+ if (msg_ctx)
+ {
+ const axis2_char_t *request_uri = NULL;
+ axis2_endpoint_ref_t *to = axis2_msg_ctx_get_to(msg_ctx, env);
+ if (to)
+ request_uri = axis2_endpoint_ref_get_address(to, env);
+
+ if (request_uri && strstr(request_uri, MCP_CATALOG_PATH))
+ {
+ axis2_conf_ctx_t *conf_ctx = axis2_msg_ctx_get_conf_ctx(msg_ctx, env);
+ axis2_conf_t *conf = conf_ctx
+ ? axis2_conf_ctx_get_conf(conf_ctx, env)
+ : NULL;
+ axis2_char_t *catalog_json = NULL;
+ if (mcp_catalog_check_and_respond(env, conf, request_uri,
+ &catalog_json)
+ && catalog_json)
+ {
+ AXIS2_LOG_INFO(env->log,
+ "FinancialBenchmarkService: Returning MCP catalog (%zu bytes)",
+ strlen(catalog_json));
+ return catalog_json; /* caller (msg_recv) frees via AXIS2_FREE */
+ }
+ }
+ }
+
/* Try to get operation from message context */
if (msg_ctx)
{
(axis-axis2-c-core) 19/21: Apply Gemini review findings — C mcp_catalog_handler + D1 handler wiring
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 3915982cca5c13941a88bd138e18e8121c43df6a
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 06:54:21 2026 -1000
Apply Gemini review findings — C mcp_catalog_handler + D1 handler wiring
Critical:
- mcp_catalog_check_and_respond: return AXIS2_FALSE (not AXIS2_TRUE) when
mcp_catalog_generate_json returns NULL; previously told caller a response
was ready but *out_json was NULL.
Major:
- mcp_catalog_check_and_respond: add !env guard before AXIS2_LOG_DEBUG
dereference; change strstr path match to suffix match (strcmp on tail
of path) to prevent path-confusion info-disclosure via embedded subpath.
- financial_benchmark_service_handler.c: remove redundant outer strstr
(mcp_catalog_check_and_respond is now the sole path gatekeeper); use
catalog_json ? strlen(catalog_json) : 0u in log call for null safety.
Minor:
- mcp_catalog_handler.c: add #include <stdio.h> (snprintf guarantee);
drop unnecessary (axutil_qname_t *) non-const casts on svc_qname and
op_qname (axutil_qname_get_localpart accepts const).
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/financial_benchmark_service_handler.c | 12 ++++----
.../src/mcp_catalog_handler.c | 35 ++++++++++++++++------
2 files changed, 33 insertions(+), 14 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
index ceaf7f84b..429746f57 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
@@ -151,20 +151,22 @@ financial_benchmark_service_invoke_json(
if (to)
request_uri = axis2_endpoint_ref_get_address(to, env);
- if (request_uri && strstr(request_uri, MCP_CATALOG_PATH))
+ if (request_uri)
{
axis2_conf_ctx_t *conf_ctx = axis2_msg_ctx_get_conf_ctx(msg_ctx, env);
axis2_conf_t *conf = conf_ctx
? axis2_conf_ctx_get_conf(conf_ctx, env)
: NULL;
+ /* mcp_catalog_check_and_respond does suffix-based path matching
+ * internally (no redundant strstr here). Returns AXIS2_FALSE when
+ * path does not match OR when catalog generation fails (NULL output),
+ * so catalog_json is guaranteed non-NULL on AXIS2_TRUE. */
axis2_char_t *catalog_json = NULL;
- if (mcp_catalog_check_and_respond(env, conf, request_uri,
- &catalog_json)
- && catalog_json)
+ if (mcp_catalog_check_and_respond(env, conf, request_uri, &catalog_json))
{
AXIS2_LOG_INFO(env->log,
"FinancialBenchmarkService: Returning MCP catalog (%zu bytes)",
- strlen(catalog_json));
+ catalog_json ? strlen(catalog_json) : 0u);
return catalog_json; /* caller (msg_recv) frees via AXIS2_FREE */
}
}
diff --git a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
index b1720fa83..ff4866457 100644
--- a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
@@ -56,6 +56,7 @@
#include <axutil_log.h>
#include <json-c/json.h>
+#include <stdio.h> /* snprintf */
#include <string.h>
#include <ctype.h>
@@ -297,8 +298,7 @@ mcp_catalog_generate_json(
const axutil_qname_t *svc_qname = axis2_svc_get_qname(svc, env);
if (!svc_qname) continue;
const axis2_char_t *svc_name =
- axutil_qname_get_localpart(
- (axutil_qname_t *)svc_qname, env);
+ axutil_qname_get_localpart(svc_qname, env);
if (!svc_name) continue;
/* Skip system services */
@@ -322,8 +322,7 @@ mcp_catalog_generate_json(
const axutil_qname_t *op_qname = axis2_op_get_qname(op, env);
if (!op_qname) continue;
const axis2_char_t *op_name =
- axutil_qname_get_localpart(
- (axutil_qname_t *)op_qname, env);
+ axutil_qname_get_localpart(op_qname, env);
if (!op_name) continue;
/* Build endpoint path: POST /services/SvcName/opName */
@@ -436,15 +435,33 @@ mcp_catalog_check_and_respond(
const axis2_char_t *request_path,
axis2_char_t **out_json)
{
- if (!request_path || !out_json) return AXIS2_FALSE;
-
- /* Match suffix: path may be "/axis2/_mcp/openapi-mcp.json" or just the
- * bare catalog path — check strstr so context-path prefixes are handled. */
- if (!strstr(request_path, MCP_CATALOG_PATH)) return AXIS2_FALSE;
+ if (!env || !request_path || !out_json) return AXIS2_FALSE;
+
+ /* Suffix match: the path must END with MCP_CATALOG_PATH so that a
+ * path like /evil/_mcp/openapi-mcp.json/../../admin (which contains
+ * the catalog path as a substring but not as a suffix) does not match.
+ * strstr-anywhere matching would allow path-confusion info disclosure. */
+ size_t path_len = strlen(request_path);
+ size_t catalog_len = strlen(MCP_CATALOG_PATH);
+ if (path_len < catalog_len) return AXIS2_FALSE;
+ if (strcmp(request_path + path_len - catalog_len, MCP_CATALOG_PATH) != 0)
+ return AXIS2_FALSE;
AXIS2_LOG_DEBUG(env->log, AXIS2_LOG_SI,
"[MCP] Catalog request matched path '%s'", request_path);
*out_json = mcp_catalog_generate_json(conf, env);
+
+ /* mcp_catalog_generate_json returns NULL on conf==NULL, env==NULL, or OOM.
+ * Return AXIS2_FALSE rather than AXIS2_TRUE with a NULL response pointer
+ * so the caller falls through to normal dispatch instead of sending NULL. */
+ if (!*out_json) {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "[MCP] mcp_catalog_generate_json returned NULL for path '%s' "
+ "(conf=%p) — falling through to normal dispatch",
+ request_path, (void *)conf);
+ return AXIS2_FALSE;
+ }
+
return AXIS2_TRUE;
}
(axis-axis2-c-core) 02/21: Fix axutil_hash key aliasing bug in scenarioAnalysis
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 2e93124f2cc6d25366f1ff7780d4a46290ec8949
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 06:28:09 2026 -1000
Fix axutil_hash key aliasing bug in scenarioAnalysis
axutil_hash_set with AXIS2_HASH_KEY_STRING stores the key pointer — it
does not copy the string. The build loop was reusing a single 32-byte
stack buffer (key_buf) for all n_assets entries, causing every hash
entry to point to the same buffer holding only the last asset_id.
All lookups except the last entry would fail, making hash_found always
<= 1 regardless of n_assets.
Fix: allocate a per-entry key string (axutil_strdup) into a hash_keys[]
array before passing to axutil_hash_set. Free all keys after
axutil_hash_free.
Also:
- Split hash timing into hash_build_us + hash_lookup_us so the reported
speedup compares lookup-only cost (the amortized steady-state case),
not build+lookup combined
- Remove unused weight_sum variable in finbench_generate_test_portfolio_json
- Build now clean under -Wall -Wextra
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/financial_benchmark_service.c | 60 ++++++++++++++++------
1 file changed, 43 insertions(+), 17 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index 8136f7fc8..32db4c207 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -1041,6 +1041,7 @@ finbench_calculate_scenarios(
int n_lookups;
long linear_found = 0, hash_found = 0;
char key_buf[32];
+ char **hash_keys = NULL; /* per-entry allocated keys (axutil_hash stores ptr, not copy) */
response = finbench_scenario_response_create(env);
if (!response) return NULL;
@@ -1112,14 +1113,28 @@ finbench_calculate_scenarios(
* Step 3: O(1) hash table benchmark
* Build axutil_hash keyed by asset_id string, then perform same lookups.
* ----------------------------------------------------------------------- */
+ /*
+ * axutil_hash_set with AXIS2_HASH_KEY_STRING stores the key pointer — it does
+ * NOT copy the string. Each entry needs its own allocated key so that all
+ * n_assets entries in the table have distinct, stable key pointers.
+ */
+ hash_keys = (char **)AXIS2_MALLOC(env->allocator,
+ request->n_assets * sizeof(char *));
+ if (hash_keys) {
+ for (i = 0; i < request->n_assets; i++) hash_keys[i] = NULL;
+ }
+
hash_build_start = get_time_us();
hash = axutil_hash_make(env);
- if (hash) {
+ if (hash && hash_keys) {
for (i = 0; i < request->n_assets; i++) {
snprintf(key_buf, sizeof(key_buf), "%" PRId64,
request->assets[i].asset_id);
- axutil_hash_set(hash, key_buf, AXIS2_HASH_KEY_STRING,
- &request->assets[i]);
+ hash_keys[i] = axutil_strdup(env, key_buf); /* stable per-entry copy */
+ if (hash_keys[i]) {
+ axutil_hash_set(hash, hash_keys[i], AXIS2_HASH_KEY_STRING,
+ &request->assets[i]);
+ }
}
}
hash_build_end = get_time_us();
@@ -1137,13 +1152,18 @@ finbench_calculate_scenarios(
hash_lookup_end = get_time_us();
if (hash) axutil_hash_free(hash, env);
+ if (hash_keys) {
+ for (i = 0; i < request->n_assets; i++) {
+ if (hash_keys[i]) AXIS2_FREE(env->allocator, hash_keys[i]);
+ }
+ AXIS2_FREE(env->allocator, hash_keys);
+ hash_keys = NULL;
+ }
/* -----------------------------------------------------------------------
* Populate response
* ----------------------------------------------------------------------- */
- long linear_us = linear_end - linear_start;
- long hash_total_us = (hash_build_end - hash_build_start)
- + (hash_lookup_end - hash_lookup_start);
+ long linear_us = linear_end - linear_start;
response->status = axutil_strdup(env, FINBENCH_STATUS_SUCCESS);
response->expected_return = portfolio_expected_return;
@@ -1160,13 +1180,20 @@ finbench_calculate_scenarios(
response->lookups_per_second =
(double)n_lookups / (linear_us / 1000000.0);
- /* Store hash timing in lookup_method string for demo output */
- char method_buf[256];
+ /*
+ * Report lookup-only speedup (build cost excluded from comparison — amortized
+ * in real workloads where the hash is built once and queried many times).
+ */
+ long hash_build_us = hash_build_end - hash_build_start;
+ long hash_lookup_us = hash_lookup_end - hash_lookup_start;
+ char method_buf[320];
snprintf(method_buf, sizeof(method_buf),
- "linear_search_us=%ld hash_total_us=%ld speedup=%.1fx "
+ "linear_search_us=%ld hash_lookup_us=%ld lookup_speedup=%.1fx "
+ "hash_build_us=%ld "
"(linear=%ld found, hash=%ld found, n_assets=%d, n_lookups=%d)",
- linear_us, hash_total_us,
- (hash_total_us > 0) ? (double)linear_us / hash_total_us : 0.0,
+ linear_us, hash_lookup_us,
+ (hash_lookup_us > 0) ? (double)linear_us / hash_lookup_us : 0.0,
+ hash_build_us,
linear_found, hash_found,
request->n_assets, n_lookups);
response->lookup_method = axutil_strdup(env, method_buf);
@@ -1175,11 +1202,11 @@ finbench_calculate_scenarios(
response->request_id = axutil_strdup(env, request->request_id);
AXIS2_LOG_INFO(env->log,
- "ScenarioAnalysis: %d assets, %d lookups — linear=%ldus hash=%ldus "
- "speedup=%.1fx E[r]=%.4f U/D=%.2f",
- request->n_assets, n_lookups, linear_us, hash_total_us,
- (hash_total_us > 0) ? (double)linear_us / hash_total_us : 0.0,
- portfolio_expected_return,
+ "ScenarioAnalysis: %d assets, %d lookups — linear=%ldus "
+ "hash_lookup=%ldus speedup=%.1fx build=%ldus E[r]=%.4f U/D=%.2f",
+ request->n_assets, n_lookups, linear_us, hash_lookup_us,
+ (hash_lookup_us > 0) ? (double)linear_us / hash_lookup_us : 0.0,
+ hash_build_us, portfolio_expected_return,
response->upside_downside_ratio);
return response;
@@ -1363,7 +1390,6 @@ finbench_generate_test_portfolio_json(
const char *json_str;
axis2_char_t *result;
int i, j;
- double weight_sum = 0.0;
if (n_assets <= 0 || n_assets > FINBENCH_MAX_ASSETS) {
n_assets = 100; /* Default */
(axis-axis2-c-core) 03/21: Add probability-sum validation to scenarioAnalysis
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 822e34b7dec6460fd092c6071147dcd893fbc009
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 06:35:02 2026 -1000
Add probability-sum validation to scenarioAnalysis
Without this check, scenario probabilities that do not sum to 1.0
silently produce incorrect expected-return figures. A hostile or
skeptical audience (Python programmers, financial quants) will test
malformed input immediately — returning wrong numbers without any
error is the kind of silent failure that damages credibility.
Validation runs before any computation (Step 1). Each asset's
probability array is summed and checked against 1.0 within a 1e-4
tolerance (tight enough to catch bad data, loose enough to accommodate
legitimate floating-point representation differences in JSON-encoded
probabilities). On failure: returns HTTP 200 with status=FAILED, a
descriptive error naming the asset index, asset_id, actual probability
sum, and expected count — plus an AXIS2_LOG_ERROR entry.
Tolerance rationale documented in code: 1e-4 is tighter than typical
financial rounding error (~1e-6) but looser than machine epsilon.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/financial_benchmark_service.c | 49 ++++++++++++++++++++--
1 file changed, 46 insertions(+), 3 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index 32db4c207..0ae11c4e5 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -1054,7 +1054,50 @@ finbench_calculate_scenarios(
}
/* -----------------------------------------------------------------------
- * Step 1: Financial computation — expected return, upside, downside
+ * Step 1: Validate probability distributions before any computation.
+ *
+ * Each asset's scenario probabilities must sum to 1.0 (within floating-
+ * point tolerance). Without this check, malformed input silently produces
+ * wrong expected-return figures — the kind of silent error that corrupts
+ * downstream portfolio decisions without any indication that the input
+ * data was bad.
+ *
+ * Tolerance: 1e-4 (0.01%). Tighter than typical financial rounding error
+ * (~1e-6) but looser than machine epsilon to accommodate legitimate
+ * floating-point representation differences in JSON-encoded probabilities.
+ * ----------------------------------------------------------------------- */
+#define FINBENCH_PROB_SUM_TOLERANCE 1e-4
+ for (i = 0; i < request->n_assets; i++) {
+ finbench_asset_scenario_t *a = &request->assets[i];
+ double prob_sum = 0.0;
+ int k;
+
+ if (a->n_scenarios <= 0) continue;
+
+ for (k = 0; k < a->n_scenarios; k++) {
+ prob_sum += a->probabilities[k];
+ }
+
+ if (prob_sum < (1.0 - FINBENCH_PROB_SUM_TOLERANCE) ||
+ prob_sum > (1.0 + FINBENCH_PROB_SUM_TOLERANCE)) {
+
+ char err_buf[256];
+ snprintf(err_buf, sizeof(err_buf),
+ "Asset index %d (id=%" PRId64 "): scenario probabilities sum to "
+ "%.8f, expected 1.0 (tolerance %.0e). "
+ "All %d scenario probabilities must sum to exactly 1.0.",
+ i, a->asset_id, prob_sum, FINBENCH_PROB_SUM_TOLERANCE,
+ a->n_scenarios);
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, err_buf);
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "ScenarioAnalysis: probability validation failed - %s", err_buf);
+ return response;
+ }
+ }
+
+ /* -----------------------------------------------------------------------
+ * Step 2: Financial computation — expected return, upside, downside
* ----------------------------------------------------------------------- */
for (i = 0; i < request->n_assets; i++) {
finbench_asset_scenario_t *a = &request->assets[i];
@@ -1090,7 +1133,7 @@ finbench_calculate_scenarios(
portfolio_expected_return /= total_position_value;
/* -----------------------------------------------------------------------
- * Step 2: O(n) linear search benchmark
+ * Step 3: O(n) linear search benchmark
* Each query scans the array from index 0 until asset_id matches.
* n_lookups = n_assets × 10 to amplify the timing difference.
* ----------------------------------------------------------------------- */
@@ -1110,7 +1153,7 @@ finbench_calculate_scenarios(
linear_end = get_time_us();
/* -----------------------------------------------------------------------
- * Step 3: O(1) hash table benchmark
+ * Step 4: O(1) hash table benchmark
* Build axutil_hash keyed by asset_id string, then perform same lookups.
* ----------------------------------------------------------------------- */
/*
(axis-axis2-c-core) 09/21: financial-benchmark: apply Gemini 2.5 Pro code review fixes
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit b4f4beb47ab77a238959e88447b0dbfc247317d8
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 13:32:55 2026 -1000
financial-benchmark: apply Gemini 2.5 Pro code review fixes
Three issues found and fixed:
MEDIUM — NaN propagation from floating-point cancellation in portfolio
variance O(n²) sum. For numerically near-zero portfolios, the
accumulation Σ w_i*w_j*σ_ij can yield a small negative result
(-1e-17) due to cancellation, causing sqrt() to return NaN. Clamp
variance to 0.0 before sqrt. Also store sqrt(variance) in a local
'volatility' variable to avoid calling sqrt() twice.
LOW — Magic number 8 used in three places for the percentile array
bounds. Added FINBENCH_MAX_PERCENTILES = 8 to the header and replaced
all three occurrences in the struct definitions and parsing code.
LOW — Double computation of sqrt(variance) in the portfolio variance
response population. Stored result in 'volatility' local variable and
used it for both portfolio_volatility and annualized_volatility.
Note: Gemini flagged var_95/var_99 as unpopulated (HIGH severity) but
this was a false positive — both fields are still assigned at their
original location in the populate-response block.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/financial_benchmark_service.c | 17 +++++++++++++----
.../src/financial_benchmark_service.h | 9 ++++++---
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index 853c4a8ed..d4a7e4db6 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -516,11 +516,19 @@ finbench_calculate_portfolio_variance(
int npy = (request->n_periods_per_year > 0) ? request->n_periods_per_year : 252;
+ /*
+ * Clamp variance to zero before sqrt. Floating-point cancellation in the
+ * O(n²) sum can yield a tiny negative result (e.g., -1e-17) for a
+ * numerically near-zero portfolio variance, producing NaN from sqrt().
+ */
+ if (variance < 0.0) variance = 0.0;
+ double volatility = sqrt(variance);
+
/* Populate response */
response->status = axutil_strdup(env, FINBENCH_STATUS_SUCCESS);
response->portfolio_variance = variance;
- response->portfolio_volatility = sqrt(variance);
- response->annualized_volatility = sqrt(variance) * sqrt((double)npy);
+ response->portfolio_volatility = volatility;
+ response->annualized_volatility = volatility * sqrt((double)npy);
response->calc_time_us = end_time - start_time;
response->memory_used_kb = finbench_get_memory_usage_kb();
response->matrix_operations = ops;
@@ -721,7 +729,7 @@ finbench_monte_carlo_request_create_from_json(
if (json_object_object_get_ex(json_obj, "percentiles", &pct_arr) &&
json_object_is_type(pct_arr, json_type_array)) {
int n_pct = json_object_array_length(pct_arr);
- if (n_pct > 8) n_pct = 8;
+ if (n_pct > FINBENCH_MAX_PERCENTILES) n_pct = FINBENCH_MAX_PERCENTILES;
request->n_percentiles = 0;
int pi;
for (pi = 0; pi < n_pct; pi++) {
@@ -911,7 +919,8 @@ finbench_run_monte_carlo(
response->n_percentiles = 0;
{
int pi;
- int n_pct = (request->n_percentiles > 8) ? 8 : request->n_percentiles;
+ int n_pct = (request->n_percentiles > FINBENCH_MAX_PERCENTILES)
+ ? FINBENCH_MAX_PERCENTILES : request->n_percentiles;
for (pi = 0; pi < n_pct; pi++) {
double p = request->percentiles[pi];
if (p <= 0.0 || p >= 1.0) continue;
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
index 6ac07b6cd..b77d406f2 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
@@ -53,6 +53,9 @@ extern "C"
#define FINBENCH_MAX_SCENARIOS 10
#define FINBENCH_MAX_SIMULATIONS 1000000
+/** Maximum number of percentile levels accepted in a monteCarlo request */
+#define FINBENCH_MAX_PERCENTILES 8
+
/* Memory constraint for Android devices */
#define FINBENCH_MEMORY_CONSTRAINT_MB 512
@@ -202,7 +205,7 @@ typedef struct finbench_monte_carlo_request
* Up to FINBENCH_MAX_PERCENTILES values; extras are ignored.
* Each percentile p produces: VaR_p = initial_value - sorted_final_values[p * n_sims].
*/
- double percentiles[8];
+ double percentiles[FINBENCH_MAX_PERCENTILES];
/** Number of entries in percentiles[]. 0 → use defaults {0.01, 0.05}. */
int n_percentiles;
@@ -243,8 +246,8 @@ typedef struct finbench_monte_carlo_response
* var_at_percentile[i] = initial_value - sorted_final_values[percentiles[i] * n_sims]
* Count is stored in n_percentiles; up to 8 entries.
*/
- double var_at_percentile[8];
- double percentile_levels[8];
+ double var_at_percentile[FINBENCH_MAX_PERCENTILES];
+ double percentile_levels[FINBENCH_MAX_PERCENTILES];
int n_percentiles;
/** Maximum drawdown observed */
(axis-axis2-c-core) 14/21: financial-benchmark: D1 — MCP catalog HTTP handler (mcp_catalog_handler.c)
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 5f799e551dd927e1256fdd308cdcb9f084ec59d0
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 03:11:32 2026 -1000
financial-benchmark: D1 — MCP catalog HTTP handler (mcp_catalog_handler.c)
New files:
- src/mcp_catalog_handler.h — public API with MCP_CATALOG_PATH constant,
mcp_catalog_generate_json(), mcp_catalog_check_and_respond()
- src/mcp_catalog_handler.c — dynamic service introspection via Axis2/C
configuration APIs; matches Java OpenApiSpecGenerator.generateMcpCatalogJson()
Behaviour:
- Walks axis2_conf_get_all_svcs() at request time (same as Java)
- Two-level parameter lookup: axis2_op_get_param() > axis2_svc_get_param()
for mcpDescription, mcpInputSchema, mcpReadOnly, mcpDestructive,
mcpIdempotent, mcpOpenWorld
- mcpInputSchema: json_tokener_parse() validates the services.xml value;
logs WARN and uses empty baseline on parse failure (matches Java fallback)
- System service filter: Version, AdminService, __ prefix, *AdminService*
- Auth flag: services with "login" in name or "adminconsole" → requiresAuth=false
- tickerResolveEndpoint: reads global axis2_conf_get_param("mcpTickerResolveService")
- JSON safety: all string values through json_object_new_string() — no
manual string concatenation for JSON values
- mcp_catalog_check_and_respond(): strstr match on /_mcp/openapi-mcp.json,
suitable for integration at the top of any service handler's invoke()
Makefile.am: adds mcp_catalog_handler.c to libfinancial_benchmark_service_la
and mcp_catalog_handler.h to include_HEADERS / EXTRA_DIST.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/Makefile.am | 7 +-
.../src/mcp_catalog_handler.c | 416 +++++++++++++++++++++
.../src/mcp_catalog_handler.h | 139 +++++++
3 files changed, 560 insertions(+), 2 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/Makefile.am b/samples/user_guide/financial-benchmark-service/Makefile.am
index db4341ffd..f7bd83322 100644
--- a/samples/user_guide/financial-benchmark-service/Makefile.am
+++ b/samples/user_guide/financial-benchmark-service/Makefile.am
@@ -28,7 +28,8 @@ servicesdir = $(prefix)/services/FinancialBenchmarkService
lib_LTLIBRARIES = libfinancial_benchmark_service.la
libfinancial_benchmark_service_la_SOURCES = \
- src/financial_benchmark_service.c
+ src/financial_benchmark_service.c \
+ src/mcp_catalog_handler.c
libfinancial_benchmark_service_la_CPPFLAGS = \
-I$(top_srcdir)/include \
@@ -88,13 +89,15 @@ services_DATA = services.xml
# Install headers for potential extensions
include_HEADERS = \
src/financial_benchmark_service.h \
- src/finbench_mcp.h
+ src/finbench_mcp.h \
+ src/mcp_catalog_handler.h
# Extra files to distribute
EXTRA_DIST = \
services.xml \
src/financial_benchmark_service.h \
src/finbench_mcp.h \
+ src/mcp_catalog_handler.h \
README.md
endif # AXIS2_JSON_ENABLED
diff --git a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
new file mode 100644
index 000000000..2181df49c
--- /dev/null
+++ b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
@@ -0,0 +1,416 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file mcp_catalog_handler.c
+ * @brief MCP catalog HTTP handler for Axis2/C — dynamic service introspection
+ *
+ * Mirrors Java's OpenApiSpecGenerator.generateMcpCatalogJson() exactly.
+ *
+ * Design notes:
+ *
+ * Parameter lookup order — matches the Java getMcpStringParam() / getMcpBoolParam():
+ * 1. axis2_op_get_param(op, env, name) — operation level
+ * 2. axis2_svc_get_param(svc, env, name) — service level
+ * 3. default value
+ *
+ * System service filter — excludes "Version", "AdminService", names starting
+ * with "__", and names containing "AdminService" (same list as Java).
+ *
+ * Auth flag — services whose lowercased name contains "login" or equals
+ * "adminconsole" set x-requiresAuth: false; all others: true.
+ *
+ * mcpInputSchema — if the parameter value is valid JSON it is embedded as a
+ * parsed json_object tree. If json_tokener_parse() fails, a WARN is logged
+ * and the fallback empty schema is used — matching Java's behaviour.
+ *
+ * JSON safety — all string values passed to json-c go through
+ * json_object_new_string(), which handles escaping internally. No manual
+ * string concatenation is used for JSON values.
+ */
+
+#include "mcp_catalog_handler.h"
+
+#include <axis2_svc.h>
+#include <axis2_op.h>
+#include <axis2_conf.h>
+#include <axutil_param.h>
+#include <axutil_hash.h>
+#include <axutil_qname.h>
+#include <axutil_string.h>
+#include <axutil_log.h>
+
+#include <json-c/json.h>
+#include <string.h>
+#include <ctype.h>
+
+/* ============================================================================
+ * Internal helpers
+ * ============================================================================
+ */
+
+/**
+ * Retrieve a string-valued parameter, checking the operation first then the
+ * service. Returns default_val (which may be NULL) when neither has the param.
+ *
+ * Never returns a pointer to freed memory: the return value points into the
+ * axutil_param_t's own storage (owned by the Axis2 config, not this function).
+ */
+static const axis2_char_t *
+get_mcp_string_param(
+ axis2_op_t *op,
+ axis2_svc_t *svc,
+ const axutil_env_t *env,
+ const axis2_char_t *param_name,
+ const axis2_char_t *default_val)
+{
+ axutil_param_t *p = NULL;
+
+ /* Operation level first */
+ p = axis2_op_get_param(op, env, param_name);
+ if (!p)
+ p = axis2_svc_get_param(svc, env, param_name);
+
+ if (p) {
+ void *val = axutil_param_get_value(p, env);
+ if (val && ((axis2_char_t *)val)[0] != '\0')
+ return (const axis2_char_t *)val;
+ }
+ return default_val;
+}
+
+/**
+ * Retrieve a boolean-valued MCP parameter.
+ * Accepts "true" / "false" (case-insensitive); anything else returns default_val.
+ */
+static axis2_bool_t
+get_mcp_bool_param(
+ axis2_op_t *op,
+ axis2_svc_t *svc,
+ const axutil_env_t *env,
+ const axis2_char_t *param_name,
+ axis2_bool_t default_val)
+{
+ axutil_param_t *p = NULL;
+
+ p = axis2_op_get_param(op, env, param_name);
+ if (!p)
+ p = axis2_svc_get_param(svc, env, param_name);
+
+ if (p) {
+ void *val = axutil_param_get_value(p, env);
+ if (val) {
+ const axis2_char_t *s = (const axis2_char_t *)val;
+ if (axutil_strcasecmp(s, "true") == 0) return AXIS2_TRUE;
+ if (axutil_strcasecmp(s, "false") == 0) return AXIS2_FALSE;
+ }
+ }
+ return default_val;
+}
+
+/**
+ * Return AXIS2_TRUE if the service is an Axis2 internal service that should
+ * be excluded from the MCP catalog. Mirrors Java's isSystemService().
+ */
+static axis2_bool_t
+is_system_service(const axis2_char_t *svc_name)
+{
+ if (!svc_name) return AXIS2_TRUE;
+ if (axutil_strcmp(svc_name, "Version") == 0) return AXIS2_TRUE;
+ if (axutil_strcmp(svc_name, "AdminService") == 0) return AXIS2_TRUE;
+ if (strncmp(svc_name, "__", 2) == 0) return AXIS2_TRUE;
+ if (strstr(svc_name, "AdminService") != NULL) return AXIS2_TRUE;
+ return AXIS2_FALSE;
+}
+
+/**
+ * Return AXIS2_TRUE if the service requires a Bearer auth token.
+ * loginService and adminconsole do not; everything else does.
+ * Case-insensitive comparison — mirrors Java's requiresAuth logic.
+ */
+static axis2_bool_t
+service_requires_auth(const axis2_char_t *svc_name)
+{
+ if (!svc_name) return AXIS2_TRUE;
+
+ /* Build a lowercase copy for comparison */
+ char lower[256];
+ size_t i;
+ size_t len = strlen(svc_name);
+ if (len >= sizeof(lower))
+ len = sizeof(lower) - 1;
+ for (i = 0; i < len; i++)
+ lower[i] = (char)tolower((unsigned char)svc_name[i]);
+ lower[len] = '\0';
+
+ if (strstr(lower, "login") != NULL) return AXIS2_FALSE;
+ if (strcmp(lower, "adminconsole") == 0) return AXIS2_FALSE;
+ return AXIS2_TRUE;
+}
+
+/**
+ * Build an empty baseline inputSchema json_object:
+ * { "type": "object", "properties": {}, "required": [] }
+ *
+ * The caller owns the returned object; it must be added to the parent with
+ * json_object_object_add() (which takes ownership) or freed with json_object_put().
+ */
+static json_object *
+make_empty_input_schema(void)
+{
+ json_object *schema = json_object_new_object();
+ if (!schema) return NULL;
+ json_object_object_add(schema, "type", json_object_new_string("object"));
+ json_object_object_add(schema, "properties", json_object_new_object());
+ json_object_object_add(schema, "required", json_object_new_array());
+ return schema;
+}
+
+/**
+ * Build the inputSchema json_object for an operation.
+ *
+ * If the mcpInputSchema parameter is present and parses as valid JSON it is
+ * returned directly (json_tokener_parse creates a new object tree).
+ * On parse failure a WARN is logged and the empty baseline schema is returned.
+ * When the parameter is absent the empty baseline is returned.
+ */
+static json_object *
+build_input_schema(
+ axis2_op_t *op,
+ axis2_svc_t *svc,
+ const axutil_env_t *env,
+ const axis2_char_t *op_name,
+ const axis2_char_t *svc_name)
+{
+ const axis2_char_t *schema_str = get_mcp_string_param(
+ op, svc, env, "mcpInputSchema", NULL);
+
+ if (schema_str) {
+ json_object *parsed = json_tokener_parse(schema_str);
+ if (parsed) {
+ return parsed; /* json_tokener_parse allocates a new tree */
+ }
+ /* Invalid JSON — log and fall through to empty schema */
+ AXIS2_LOG_WARNING(env->log, AXIS2_LOG_SI,
+ "[MCP] Invalid mcpInputSchema JSON for operation '%s' in service '%s'"
+ " — falling back to empty schema",
+ op_name ? op_name : "<unknown>",
+ svc_name ? svc_name : "<unknown>");
+ }
+
+ return make_empty_input_schema();
+}
+
+/* ============================================================================
+ * Public API
+ * ============================================================================
+ */
+
+AXIS2_EXTERN axis2_char_t *AXIS2_CALL
+mcp_catalog_generate_json(
+ axis2_conf_t *conf,
+ const axutil_env_t *env)
+{
+ if (!conf || !env) return NULL;
+
+ /* ── Root object ─────────────────────────────────────────────────────── */
+ json_object *root = json_object_new_object();
+ if (!root) return NULL;
+
+ /* ── _meta block ─────────────────────────────────────────────────────── */
+ json_object *meta = json_object_new_object();
+ json_object_object_add(meta, "axis2JsonRpcFormat",
+ json_object_new_string(
+ "{\"<operationName>\":[{\"arg0\":{<params>}}]}"));
+ json_object_object_add(meta, "contentType",
+ json_object_new_string("application/json"));
+ json_object_object_add(meta, "authHeader",
+ json_object_new_string("Authorization: Bearer <token>"));
+ json_object_object_add(meta, "tokenEndpoint",
+ json_object_new_string("POST /services/loginService/doLogin"));
+
+ /* Optional global ticker resolve endpoint (axis2.xml parameter) */
+ axutil_param_t *ticker_param =
+ axis2_conf_get_param(conf, env, "mcpTickerResolveService");
+ if (ticker_param) {
+ void *ticker_val = axutil_param_get_value(ticker_param, env);
+ if (ticker_val && ((axis2_char_t *)ticker_val)[0] != '\0') {
+ /* Build "POST /services/<value>" */
+ const axis2_char_t *svc_op = (const axis2_char_t *)ticker_val;
+ char endpoint_buf[512];
+ snprintf(endpoint_buf, sizeof(endpoint_buf),
+ "POST " MCP_SERVICES_PREFIX "%s", svc_op);
+ json_object_object_add(meta, "tickerResolveEndpoint",
+ json_object_new_string(endpoint_buf));
+ }
+ }
+
+ json_object_object_add(root, "_meta", meta);
+
+ /* ── tools array ─────────────────────────────────────────────────────── */
+ json_object *tools = json_object_new_array();
+
+ axutil_hash_t *svc_map = axis2_conf_get_all_svcs(conf, env);
+ if (svc_map) {
+ axutil_hash_index_t *svc_hi = axutil_hash_first(svc_map, env);
+ while (svc_hi) {
+ void *svc_void = NULL;
+ axutil_hash_this(svc_hi, NULL, NULL, &svc_void);
+ svc_hi = axutil_hash_next(env, svc_hi);
+
+ if (!svc_void) continue;
+ axis2_svc_t *svc = (axis2_svc_t *)svc_void;
+
+ /* Service name */
+ const axutil_qname_t *svc_qname = axis2_svc_get_qname(svc, env);
+ if (!svc_qname) continue;
+ const axis2_char_t *svc_name =
+ axutil_qname_get_localpart(
+ (axutil_qname_t *)svc_qname, env);
+ if (!svc_name) continue;
+
+ /* Skip system services */
+ if (is_system_service(svc_name)) continue;
+
+ axis2_bool_t requires_auth = service_requires_auth(svc_name);
+
+ /* Walk operations */
+ axutil_hash_t *ops_hash = axis2_svc_get_all_ops(svc, env);
+ if (!ops_hash) continue;
+
+ axutil_hash_index_t *op_hi = axutil_hash_first(ops_hash, env);
+ while (op_hi) {
+ void *op_void = NULL;
+ axutil_hash_this(op_hi, NULL, NULL, &op_void);
+ op_hi = axutil_hash_next(env, op_hi);
+
+ if (!op_void) continue;
+ axis2_op_t *op = (axis2_op_t *)op_void;
+
+ const axutil_qname_t *op_qname = axis2_op_get_qname(op, env);
+ if (!op_qname) continue;
+ const axis2_char_t *op_name =
+ axutil_qname_get_localpart(
+ (axutil_qname_t *)op_qname, env);
+ if (!op_name) continue;
+
+ /* Build endpoint path: POST /services/SvcName/opName */
+ char endpoint_buf[512];
+ snprintf(endpoint_buf, sizeof(endpoint_buf),
+ "POST " MCP_SERVICES_PREFIX "%s/%s",
+ svc_name, op_name);
+
+ /* Build payload template:
+ * {"opName":[{"arg0":{}}]}
+ * json-c handles escaping of op_name and svc_name strings. */
+ char template_buf[512];
+ snprintf(template_buf, sizeof(template_buf),
+ "{\"%s\":[{\"arg0\":{}}]}", op_name);
+
+ /* Description: operation > service > auto-generated fallback */
+ char auto_desc[512];
+ snprintf(auto_desc, sizeof(auto_desc), "%s: %s",
+ svc_name, op_name);
+ const axis2_char_t *description = get_mcp_string_param(
+ op, svc, env, "mcpDescription", auto_desc);
+
+ /* MCP 2025-03-26 annotation hints */
+ axis2_bool_t read_only = get_mcp_bool_param(op, svc, env,
+ "mcpReadOnly", AXIS2_FALSE);
+ axis2_bool_t destructive = get_mcp_bool_param(op, svc, env,
+ "mcpDestructive", AXIS2_FALSE);
+ axis2_bool_t idempotent = get_mcp_bool_param(op, svc, env,
+ "mcpIdempotent", AXIS2_FALSE);
+ axis2_bool_t open_world = get_mcp_bool_param(op, svc, env,
+ "mcpOpenWorld", AXIS2_FALSE);
+
+ /* inputSchema — static param or empty baseline */
+ json_object *input_schema =
+ build_input_schema(op, svc, env, op_name, svc_name);
+
+ /* Build tool node */
+ json_object *tool = json_object_new_object();
+ json_object_object_add(tool, "name",
+ json_object_new_string(op_name));
+ json_object_object_add(tool, "description",
+ json_object_new_string(description));
+ json_object_object_add(tool, "inputSchema", input_schema);
+ json_object_object_add(tool, "endpoint",
+ json_object_new_string(endpoint_buf));
+ json_object_object_add(tool, "x-axis2-payloadTemplate",
+ json_object_new_string(template_buf));
+ json_object_object_add(tool, "x-requiresAuth",
+ json_object_new_boolean(requires_auth));
+
+ /* annotations sub-object */
+ json_object *annotations = json_object_new_object();
+ json_object_object_add(annotations, "readOnlyHint",
+ json_object_new_boolean(read_only));
+ json_object_object_add(annotations, "destructiveHint",
+ json_object_new_boolean(destructive));
+ json_object_object_add(annotations, "idempotentHint",
+ json_object_new_boolean(idempotent));
+ json_object_object_add(annotations, "openWorldHint",
+ json_object_new_boolean(open_world));
+ json_object_object_add(tool, "annotations", annotations);
+
+ json_object_array_add(tools, tool);
+ } /* end op loop */
+ } /* end svc loop */
+ }
+
+ json_object_object_add(root, "tools", tools);
+
+ /* ── Serialise ───────────────────────────────────────────────────────── */
+ const char *json_cstr =
+ json_object_to_json_string_ext(root, JSON_C_TO_STRING_PLAIN);
+
+ axis2_char_t *result = NULL;
+ if (json_cstr) {
+ result = axutil_strdup(env, json_cstr);
+ } else {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "[MCP] json_object_to_json_string_ext returned NULL — "
+ "returning fallback catalog");
+ result = axutil_strdup(env, "{\"tools\":[]}");
+ }
+
+ /* json_object_put frees the entire tree including all children */
+ json_object_put(root);
+
+ return result;
+}
+
+AXIS2_EXTERN axis2_bool_t AXIS2_CALL
+mcp_catalog_check_and_respond(
+ const axutil_env_t *env,
+ axis2_conf_t *conf,
+ const axis2_char_t *request_path,
+ axis2_char_t **out_json)
+{
+ if (!request_path || !out_json) return AXIS2_FALSE;
+
+ /* Match suffix: path may be "/axis2/_mcp/openapi-mcp.json" or just the
+ * bare catalog path — check strstr so context-path prefixes are handled. */
+ if (!strstr(request_path, MCP_CATALOG_PATH)) return AXIS2_FALSE;
+
+ AXIS2_LOG_DEBUG(env->log, AXIS2_LOG_SI,
+ "[MCP] Catalog request matched path '%s'", request_path);
+
+ *out_json = mcp_catalog_generate_json(conf, env);
+ return AXIS2_TRUE;
+}
diff --git a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.h b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.h
new file mode 100644
index 000000000..9821b246f
--- /dev/null
+++ b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.h
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file mcp_catalog_handler.h
+ * @brief MCP catalog HTTP handler for Axis2/C — GET /_mcp/openapi-mcp.json
+ *
+ * Generates an MCP 2025-03-26-compatible tool catalog by walking all deployed
+ * Axis2/C services at request time. Reads per-operation parameters from
+ * services.xml:
+ *
+ * mcpDescription — human-readable tool description (operation > service)
+ * mcpInputSchema — literal JSON Schema string (operation > service)
+ * mcpReadOnly — sets readOnlyHint (default: false)
+ * mcpIdempotent — sets idempotentHint (default: false)
+ * mcpDestructive — sets destructiveHint (default: false)
+ * mcpOpenWorld — sets openWorldHint (default: false)
+ *
+ * The catalog format matches the Java OpenApiSpecGenerator output exactly,
+ * so the same MCP client (axis2-mcp-bridge, Claude Desktop, rapi-mcp) can
+ * point at either a Java or C deployment and receive the same schema shape.
+ *
+ * Output format:
+ * @code
+ * {
+ * "_meta": {
+ * "axis2JsonRpcFormat": "{\"<operationName>\":[{\"arg0\":{<params>}}]}",
+ * "contentType": "application/json",
+ * "authHeader": "Authorization: Bearer <token>",
+ * "tokenEndpoint": "POST /services/loginService/doLogin"
+ * },
+ * "tools": [
+ * {
+ * "name": "portfolioVariance",
+ * "description": "...",
+ * "inputSchema": { "type": "object", "properties": {...}, "required": [...] },
+ * "endpoint": "POST /services/FinancialBenchmarkService/portfolioVariance",
+ * "x-axis2-payloadTemplate": "{\"portfolioVariance\":[{\"arg0\":{}}]}",
+ * "x-requiresAuth": true,
+ * "annotations": {
+ * "readOnlyHint": true, "destructiveHint": false,
+ * "idempotentHint": true, "openWorldHint": false
+ * }
+ * }
+ * ]
+ * }
+ * @endcode
+ */
+
+#ifndef MCP_CATALOG_HANDLER_H
+#define MCP_CATALOG_HANDLER_H
+
+#include <axutil_env.h>
+#include <axis2_conf.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * URL path that triggers the MCP catalog response.
+ * Checked by mcp_catalog_check_and_respond() against the incoming request URI.
+ */
+#define MCP_CATALOG_PATH "/_mcp/openapi-mcp.json"
+
+/**
+ * HTTP path prefix that identifies all Axis2 services.
+ * Combined with service and operation names to build endpoint strings.
+ */
+#define MCP_SERVICES_PREFIX "/services/"
+
+/**
+ * Generate the MCP tool catalog JSON string.
+ *
+ * Walks all services registered in @p conf, reads MCP metadata parameters,
+ * and builds the catalog JSON using json-c. Services named "Version",
+ * "AdminService", or starting with "__" are filtered out (system services).
+ *
+ * @param conf Axis2 configuration object (from axis2_conf_ctx_get_conf)
+ * @param env Axis2 environment
+ * @return Heap-allocated JSON string (caller must free with AXIS2_FREE),
+ * or NULL on allocation failure. On json-c failure returns a
+ * minimal fallback: @c {"tools":[]}.
+ */
+AXIS2_EXTERN axis2_char_t *AXIS2_CALL
+mcp_catalog_generate_json(
+ axis2_conf_t *conf,
+ const axutil_env_t *env);
+
+/**
+ * Check whether @p request_path matches MCP_CATALOG_PATH and, if so,
+ * write the catalog JSON to @p response_buf / @p response_len.
+ *
+ * Intended to be called near the top of a service handler's invoke()
+ * before normal operation dispatch:
+ *
+ * @code
+ * axis2_char_t *resp = NULL;
+ * if (mcp_catalog_check_and_respond(env, conf, request_path, &resp)) {
+ * // write resp to HTTP response and return AXIS2_SUCCESS
+ * AXIS2_FREE(env->allocator, resp);
+ * return AXIS2_SUCCESS;
+ * }
+ * // ... normal dispatch
+ * @endcode
+ *
+ * @param env Axis2 environment
+ * @param conf Axis2 configuration
+ * @param request_path URI path from the incoming request (may be NULL)
+ * @param out_json Output: heap-allocated JSON string on match; unchanged on no-match
+ * @return AXIS2_TRUE if the path matched and *out_json was set;
+ * AXIS2_FALSE otherwise.
+ */
+AXIS2_EXTERN axis2_bool_t AXIS2_CALL
+mcp_catalog_check_and_respond(
+ const axutil_env_t *env,
+ axis2_conf_t *conf,
+ const axis2_char_t *request_path,
+ axis2_char_t **out_json);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MCP_CATALOG_HANDLER_H */
(axis-axis2-c-core) 06/21: Add MCP development plan for Axis2/C
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 99d9b9e0b6abb66f4d10292d2adc5adbd937bdc5
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Apr 6 07:12:41 2026 -1000
Add MCP development plan for Axis2/C
Documents the Java-first-then-C-port strategy for MCP support:
- Java reference implementation proves the protocol (A1→A2→B1)
- C port follows the same sequence used for HTTP/2
- C1: stdio MCP mode in financial-benchmark-service (~500 lines)
- C2: HTTP/SSE endpoint via Apache httpd for Penguin demo (~800 lines)
- C3: axis2-transport-mcp C module (Apache graduation milestone)
Includes static tool catalog design (mcp_tools[] struct array),
isatty() mode detection rationale, relationship to Java schemas,
and the dual-runtime demo deliverable: Claude Desktop connecting
to a $20 Android phone and an enterprise Java server via the same
MCP protocol.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
docs/MCP.md | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 346 insertions(+)
diff --git a/docs/MCP.md b/docs/MCP.md
new file mode 100644
index 000000000..447927a81
--- /dev/null
+++ b/docs/MCP.md
@@ -0,0 +1,346 @@
+---
+type: architecture
+created: 2026-04-06
+status: Active — Java implementation first, C port follows
+---
+
+# MCP Support for Apache Axis2/C
+
+**BLUF**: Axis2/C gains MCP (Model Context Protocol) support by porting from the
+Axis2/Java reference implementation — the same pattern used successfully for HTTP/2.
+The C implementation targets hardware where Java cannot run: edge devices, embedded
+systems, Android phones. One MCP client (Claude Desktop, Claude API) connects to
+both runtimes via identical protocol. One AI protocol, the full hardware spectrum.
+
+MCP is JSON-RPC 2.0. Three required methods: `initialize`, `tools/list`, `tools/call`.
+Two transports: stdio (simple, process-per-client) and HTTP/SSE (persistent server).
+Existing Axis2/C infrastructure — json-c, Apache httpd, the service dispatch table —
+covers everything needed. No new dependencies.
+
+---
+
+## Why C Port Comes After Java
+
+The HTTP/2 feature followed this sequence: Java reference implementation → C port.
+It worked because the wire protocol is well-specified and both runtimes share the
+same transport abstraction (TransportListener/TransportSender in Java;
+`axis2_transport_receiver_t`/`axis2_transport_sender_t` in C).
+
+MCP is a better candidate than HTTP/2 was:
+
+- **Simpler wire protocol**: JSON lines over stdio or HTTP. No binary framing,
+ no HPACK compression, no stream multiplexing.
+- **C already has the dispatch layer**: `route_operation()` in
+ `financial_benchmark_service.c` already does what `tools/call` needs — parse
+ action name, dispatch to handler, return JSON. The JSON-RPC 2.0 wrapper is
+ the only addition.
+- **stdio in C is trivial**: `fgets(stdin)` / `fprintf(stdout)`. In Java this
+ requires thread management and stream handling. In C it is a 30-line loop.
+- **No JVM startup**: Claude Desktop starts MCP servers as subprocesses. JVM
+ cold-start is 1–3 seconds. A C binary starts in under 100ms. For stdio MCP
+ (process-per-client), this is visible every time Claude Desktop opens.
+- **Port is mechanical once Java proves the protocol**: After Axis2/Java validates
+ the `initialize`/`tools/list`/`tools/call` handshake against Claude Desktop,
+ the C port translates proven behavior rather than discovering it.
+
+**Estimated new C code**: ~500 lines for stdio, ~800 for HTTP/SSE. The HTTP/2 port
+was ~5,000 lines. MCP is lighter because 80% of the dispatch work already exists.
+
+---
+
+## What the Two Runtimes Provide
+
+| | Axis2/Java MCP | Axis2/C MCP |
+|---|---|---|
+| **Target** | Enterprise service bus | Edge / embedded / mobile |
+| **Hardware** | WildFly, Tomcat, cloud | Raspberry Pi, Android, IoT |
+| **JVM startup** | 1–3 seconds | N/A — native binary |
+| **Memory** | 512MB–2GB JVM heap | ~30 MB total process |
+| **Tool catalog** | `axis2-openapi` annotation reflection | Static `mcp_tools[]` struct array |
+| **Demo** | Claude calling springbootdemo-tomcat11 | Claude calling financial calculations on a $20 phone |
+| **Apache contribution** | First Java framework with native MCP transport | First C middleware with MCP — nothing else exists |
+
+Same MCP client. Same protocol. Full hardware spectrum: enterprise server to $20
+Android phone.
+
+---
+
+## Current State (2026-04-06)
+
+### What exists in Axis2/C today
+
+| Artifact | Status |
+|----------|--------|
+| json-c integration | ✅ In use throughout financial benchmark service |
+| HTTP/2 transport (`modules/transport-h2`) | ✅ Working, tested |
+| `financial_benchmark_service.c` | ✅ portfolioVariance, monteCarlo, scenarioAnalysis — all validated |
+| Service dispatch table (`route_operation()`) | ✅ Reusable for MCP `tools/call` |
+| Apache httpd handler | ✅ Running, mod_axis2 loaded |
+| `axis2-transport-mcp` C module | ❌ Not started |
+| stdio MCP mode | ❌ Not started |
+| HTTP/SSE MCP endpoint | ❌ Not started |
+
+### What exists in Axis2/Java (the reference)
+
+| Artifact | Status |
+|----------|--------|
+| `axis2-openapi` module | ✅ Serves `/openapi.json`, `/openapi.yaml`, `/swagger-ui` |
+| `springbootdemo-tomcat11` | ✅ Reference implementation, Java 25 + Tomcat 11 |
+| `/openapi-mcp.json` endpoint | 🔄 In progress (A1) |
+| `axis2-mcp-bridge` stdio JAR | ❌ Not started (A2) |
+| `axis2-transport-mcp` Java native | ❌ Not started (B1) |
+
+---
+
+## Track C — C Implementation Plan
+
+### C1: stdio MCP Mode in `financial_benchmark_service.c`
+
+**When**: After Java A2 (`axis2-mcp-bridge` stdio JAR) is validated against Claude Desktop.
+
+**How it works**: The binary detects whether stdin is a pipe (MCP client attached) or
+a tty (interactive/httpd mode) and switches behavior accordingly:
+
+```c
+/* In main entry point or service init */
+if (!isatty(fileno(stdin))) {
+ finbench_run_mcp_stdio(env);
+ return AXIS2_SUCCESS;
+}
+```
+
+The stdio loop is JSON-RPC 2.0 framing around the existing dispatch:
+
+```c
+static void finbench_run_mcp_stdio(const axutil_env_t *env) {
+ char line_buf[65536];
+ while (fgets(line_buf, sizeof(line_buf), stdin)) {
+ json_object *req = json_tokener_parse(line_buf);
+ if (!req) continue;
+
+ const char *method = /* json_object_get_string(req["method"]) */;
+ json_object *id_obj = /* req["id"] */;
+
+ if (strcmp(method, "initialize") == 0)
+ finbench_mcp_write_result(id_obj, finbench_mcp_initialize());
+ else if (strcmp(method, "tools/list") == 0)
+ finbench_mcp_write_result(id_obj, finbench_mcp_tools_list());
+ else if (strcmp(method, "tools/call") == 0)
+ finbench_mcp_write_result(id_obj, finbench_mcp_dispatch(env, req));
+ else
+ finbench_mcp_write_error(id_obj, -32601, "Method not found");
+
+ json_object_put(req);
+ }
+}
+```
+
+**Tool catalog**: Static struct array — no reflection, no overhead. Derived from the
+same operation names as the HTTP/2 JSON dispatch:
+
+```c
+typedef axis2_char_t* (*finbench_mcp_handler_t)(
+ const axutil_env_t *env,
+ json_object *arguments);
+
+typedef struct {
+ const char *name;
+ const char *description;
+ const char *input_schema_json; /* JSON string, inlined or from services.xml */
+ finbench_mcp_handler_t handler;
+} finbench_mcp_tool_t;
+
+static const finbench_mcp_tool_t finbench_mcp_tools[] = {
+ {
+ "portfolioVariance",
+ "Calculate portfolio variance using O(n^2) covariance matrix "
+ "multiplication. Returns variance, volatility, annualized volatility, "
+ "and timing in microseconds.",
+ "{\"type\":\"object\",\"properties\":{"
+ "\"n_assets\":{\"type\":\"integer\"},"
+ "\"weights\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}},"
+ "\"covariance_matrix\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}"
+ "},\"required\":[\"n_assets\",\"weights\",\"covariance_matrix\"]}",
+ finbench_portfolio_variance_mcp_handler
+ },
+ {
+ "monteCarlo",
+ "Monte Carlo Value at Risk simulation using Geometric Brownian Motion. "
+ "Returns VaR at 95%% and 99%% confidence, max drawdown, probability of "
+ "profit, and simulations-per-second throughput.",
+ "{\"type\":\"object\",\"properties\":{"
+ "\"n_simulations\":{\"type\":\"integer\"},"
+ "\"n_periods\":{\"type\":\"integer\"},"
+ "\"initial_value\":{\"type\":\"number\"},"
+ "\"expected_return\":{\"type\":\"number\"},"
+ "\"volatility\":{\"type\":\"number\"}"
+ "},\"required\":[\"n_simulations\",\"n_periods\",\"initial_value\","
+ "\"expected_return\",\"volatility\"]}",
+ finbench_monte_carlo_mcp_handler
+ },
+ {
+ "scenarioAnalysis",
+ "Scenario analysis with probability-weighted expected return, "
+ "upside/downside calculation, and O(n) vs O(1) hash lookup benchmark. "
+ "Validates that scenario probabilities sum to 1.0.",
+ "{\"type\":\"object\",\"properties\":{"
+ "\"assets\":{\"type\":\"array\"}"
+ "},\"required\":[\"assets\"]}",
+ finbench_scenario_mcp_handler
+ },
+ { NULL, NULL, NULL, NULL } /* sentinel */
+};
+```
+
+Each `_mcp_handler` is a thin wrapper: extract arguments from `json_object *`,
+call the existing `finbench_*_json_only()` function, return the result string.
+
+**Claude Desktop config** (the demo deliverable):
+```json
+{
+ "mcpServers": {
+ "axis2c-financial": {
+ "command": "/usr/local/axis2c/bin/financial-benchmark-mcp",
+ "args": []
+ }
+ }
+}
+```
+
+**Effort**: ~500 lines. Two to three days after Java A2 is validated.
+
+---
+
+### C2: HTTP/SSE MCP Endpoint (Penguin Demo)
+
+**When**: After C1 stdio works. Enables multiple Claude sessions sharing one
+persistent C server over HTTP/2.
+
+**Transport**: Reuses the existing Apache httpd handler. New routes:
+- `POST /mcp` — JSON-RPC 2.0 request (same dispatch as stdio, different framing)
+- `GET /mcp/sse` — `Content-Type: text/event-stream`; server pushes
+ `data: {...}\n\n` per response
+
+SSE in C via Apache httpd:
+```c
+/* Handler for GET /mcp/sse */
+ap_set_content_type(r, "text/event-stream");
+apr_table_set(r->headers_out, "Cache-Control", "no-cache");
+apr_table_set(r->headers_out, "Connection", "keep-alive");
+ap_rflush(r);
+
+/* Response loop — write MCP responses as SSE events */
+while (session_active) {
+ json_object *mcp_response = /* next result */;
+ const char *json_str = json_object_to_json_string(mcp_response);
+ ap_rprintf(r, "data: %s\n\n", json_str);
+ ap_rflush(r);
+ json_object_put(mcp_response);
+}
+```
+
+**Effort**: ~800 lines including session management. One week after C1.
+
+---
+
+### C3: `axis2-transport-mcp` C Module (Long Term — Apache Graduation)
+
+Full port of the Java `axis2-transport-mcp` module to C's transport interfaces:
+- Implements `axis2_transport_receiver_t` (TransportListener equivalent)
+- Implements `axis2_transport_sender_t` (TransportSender equivalent)
+- Translates MCP JSON-RPC 2.0 messages ↔ `axis2_msg_ctx_t` (MessageContext)
+- Deployable as a proper Axis2/C module (`.mar` file), not embedded in a service
+
+This is the Apache-grade contribution — the same architectural position in C as
+`axis2-transport-mcp` in Java. Targets the Axis2/C release after the Java transport
+is accepted by the Apache community.
+
+---
+
+## Key Design Decisions for C Port
+
+**Why static tool catalog, not services.xml reflection**
+
+C has no runtime reflection. Options are:
+1. Static struct array (chosen for C1/C2) — zero overhead, explicit, easily audited
+2. Parse `services.xml` at startup — more Axis2-idiomatic, allows adding tools
+ without recompiling, required for C3 as a proper Axis2 module
+
+C1 and C2 use option 1 because the financial benchmark service is a self-contained
+demo. C3 uses option 2 because a general-purpose transport module must discover
+services dynamically.
+
+**Why `isatty()` for mode detection**
+
+The same binary serves both HTTP/2 JSON (via Apache httpd as a shared library) and
+MCP stdio (as a subprocess launched by Claude Desktop). The `isatty()` check is the
+POSIX-standard way to detect whether stdin is connected to an interactive terminal or
+a pipe. When Claude Desktop launches the binary, stdin is a pipe — MCP mode. When
+httpd loads the shared library, stdin is not opened at all.
+
+In production the binary and the shared library would be separate build targets from
+the same source. The `isatty()` check handles the development/demo case where one
+binary does both.
+
+**Why input_schema_json is inlined as a string**
+
+Avoids a JSON-within-JSON parse at startup and keeps the tool definition co-located
+with its handler. For C3, the schema would be loaded from `services.xml` or a
+companion `.json` file in the service directory, following the pattern the Axis2/C
+service registry already uses for WSDL.
+
+---
+
+## Relationship to Java Reference Implementation
+
+```
+Java A1 (/openapi-mcp.json) ← defines MCP tool schema format
+ ↓
+Java A2 (axis2-mcp-bridge stdio) ← validates JSON-RPC 2.0 handshake
+ ↓
+Java B1 (axis2-transport-mcp) ← proves native transport pattern
+ ↓
+C1 (stdio in financial-benchmark) ← port: same protocol, same schemas, C dispatch
+ ↓
+C2 (HTTP/SSE on Penguin) ← port: same as Java A4/B2
+ ↓
+C3 (axis2-transport-mcp C module) ← port: same transport abstraction in C
+```
+
+The Java schemas defined in `axis2-openapi` (`/openapi-mcp.json`) are the canonical
+`inputSchema` for each tool. The C static struct array uses the same JSON schema
+strings, ensuring Claude sees identical tool definitions regardless of which runtime
+is serving them.
+
+---
+
+## Demo Deliverable
+
+When C1 and Java B1 are both complete, the demonstration is:
+
+```
+Claude Desktop
+ ├── axis2c-financial (stdio, $20 Android phone)
+ │ portfolioVariance — 500 assets in 5ms, 30MB memory
+ │ monteCarlo — 10,000 simulations in 100ms
+ │ scenarioAnalysis — O(n) vs O(1) hash, 2,000 assets
+ │
+ └── axis2-java-services (HTTP/SSE, enterprise server)
+ AssetCalculations — same calculations, JVM runtime
+ BigDataH2Service — HTTP/2 streaming, large payloads
+```
+
+Same Claude session. Same MCP protocol. One C binary on a $20 phone. One Java
+service on an enterprise server. The protocol unifies them.
+
+---
+
+## Files to Create (C Port)
+
+| File | Purpose |
+|------|---------|
+| `samples/user_guide/financial-benchmark-service/src/finbench_mcp.c` | stdio loop, tools/list, initialize, dispatch |
+| `samples/user_guide/financial-benchmark-service/src/finbench_mcp.h` | Tool catalog struct, handler type, public API |
+| `samples/user_guide/financial-benchmark-service/src/finbench_mcp_sse.c` | HTTP/SSE transport (C2) |
+| `modules/transport-mcp/` | Full Axis2/C transport module (C3) |
+| `docs/userguide/mcp-userguide.md` | User-facing setup guide |
(axis-axis2-c-core) 13/21: financial-benchmark: B1 — mcpInputSchema + MCP annotations for all 5 operations
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 96391b602c9a4f9a52d694322010d52c224118c9
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 03:03:57 2026 -1000
financial-benchmark: B1 — mcpInputSchema + MCP annotations for all 5 operations
Populates Option 1 (static declaration) mcpInputSchema parameters for all
five FinancialBenchmarkService operations in services.xml:
- portfolioVariance: required [n_assets, weights, covariance_matrix]; typed
integer/array properties; normalize_weights boolean; request_id string
- monteCarlo: required [n_simulations, n_periods, initial_value,
expected_return, volatility]; range constraints; optional random_seed
- scenarioAnalysis: nested asset object schema with required sub-fields
(asset_id, current_price, scenario_prices, probabilities, position_size);
use_hash_lookup boolean
- metadata: empty properties (GET operation, no request body)
- generateTestData: single required n_assets integer
Each operation also gains mcpDescription (natural language), mcpReadOnly,
and mcpIdempotent parameters for MCP 2025-03-26 annotation hints.
These parameters are consumed by the D1 Axis2/C MCP catalog handler
(mcp_catalog_handler.c, forthcoming) via axis2_op_get_param().
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/services.xml | 83 ++++++++++++++++++++++
1 file changed, 83 insertions(+)
diff --git a/samples/user_guide/financial-benchmark-service/services.xml b/samples/user_guide/financial-benchmark-service/services.xml
index 52202af02..30d671b01 100644
--- a/samples/user_guide/financial-benchmark-service/services.xml
+++ b/samples/user_guide/financial-benchmark-service/services.xml
@@ -78,6 +78,24 @@
Target: <10ms on old Android phone
================================================================ -->
<operation name="portfolioVariance">
+ <parameter name="mcpDescription">Calculate portfolio variance using an O(n²) covariance matrix. Returns portfolio volatility and annualised volatility. Benchmark: 500 assets in <10ms on constrained hardware.</parameter>
+ <parameter name="mcpReadOnly">true</parameter>
+ <parameter name="mcpIdempotent">true</parameter>
+ <parameter name="mcpInputSchema">{
+ "type": "object",
+ "required": ["n_assets", "weights", "covariance_matrix"],
+ "properties": {
+ "n_assets": {"type": "integer", "minimum": 2, "maximum": 2000,
+ "description": "Number of assets in the portfolio"},
+ "weights": {"type": "array", "items": {"type": "number"},
+ "description": "Portfolio weights, one per asset (will be normalised to sum=1)"},
+ "covariance_matrix": {"type": "array", "items": {"type": "number"},
+ "description": "Flattened n×n covariance matrix (row-major)"},
+ "normalize_weights": {"type": "boolean", "default": false,
+ "description": "Normalise weights before calculation"},
+ "request_id": {"type": "string", "description": "Optional correlation ID echoed in the response"}
+ }
+ }</parameter>
<description>
Calculate portfolio variance using covariance matrix multiplication.
@@ -126,6 +144,24 @@
Target: <500ms on old Android phone
================================================================ -->
<operation name="monteCarlo">
+ <parameter name="mcpDescription">Run a Monte Carlo VaR simulation using Geometric Brownian Motion. Compute-intensive: 10 000 sims × 252 periods = 2.52M iterations. Returns VaR 95/99, CVaR, max drawdown, and probability of profit.</parameter>
+ <parameter name="mcpReadOnly">true</parameter>
+ <parameter name="mcpIdempotent">true</parameter>
+ <parameter name="mcpInputSchema">{
+ "type": "object",
+ "required": ["n_simulations", "n_periods", "initial_value", "expected_return", "volatility"],
+ "properties": {
+ "n_simulations": {"type": "integer", "minimum": 100, "maximum": 1000000,
+ "description": "Number of simulation paths"},
+ "n_periods": {"type": "integer", "minimum": 1, "maximum": 2520,
+ "description": "Number of time steps (252 = 1 trading year)"},
+ "initial_value": {"type": "number", "description": "Initial portfolio value"},
+ "expected_return": {"type": "number", "description": "Annualised expected return (e.g. 0.08 for 8%)"},
+ "volatility": {"type": "number", "description": "Annualised volatility (e.g. 0.20 for 20%)"},
+ "random_seed": {"type": "integer", "description": "Optional seed for reproducible results"},
+ "request_id": {"type": "string", "description": "Optional correlation ID"}
+ }
+ }</parameter>
<description>
Run Monte Carlo simulation for Value at Risk calculation.
@@ -177,6 +213,35 @@
Mirrors DPT v2's fundAssetList.find() pattern.
================================================================ -->
<operation name="scenarioAnalysis">
+ <parameter name="mcpDescription">Scenario-weighted portfolio analysis using O(1) hash lookups vs O(n) linear search. Returns expected portfolio value and volatility across probability-weighted price scenarios.</parameter>
+ <parameter name="mcpReadOnly">true</parameter>
+ <parameter name="mcpIdempotent">true</parameter>
+ <parameter name="mcpInputSchema">{
+ "type": "object",
+ "required": ["n_assets", "assets"],
+ "properties": {
+ "n_assets": {"type": "integer", "minimum": 1, "maximum": 2000,
+ "description": "Number of assets"},
+ "assets": {"type": "array",
+ "description": "Array of asset scenario objects",
+ "items": {
+ "type": "object",
+ "required": ["asset_id", "current_price", "scenario_prices", "probabilities", "position_size"],
+ "properties": {
+ "asset_id": {"type": "integer"},
+ "current_price": {"type": "number"},
+ "scenario_prices":{"type": "array", "items": {"type": "number"},
+ "description": "Price in each scenario"},
+ "probabilities": {"type": "array", "items": {"type": "number"},
+ "description": "Probability weights (must sum to 1.0)"},
+ "position_size": {"type": "integer", "description": "Number of shares held"}
+ }
+ }},
+ "use_hash_lookup": {"type": "boolean", "default": true,
+ "description": "Use O(1) hash lookup instead of O(n) linear scan"},
+ "request_id": {"type": "string"}
+ }
+ }</parameter>
<description>
Calculate scenario-weighted portfolio analysis.
@@ -214,6 +279,13 @@
Get service information, capabilities, and device stats.
================================================================ -->
<operation name="metadata">
+ <parameter name="mcpDescription">Get FinancialBenchmarkService metadata: supported operations, capacity limits, current memory usage, and device platform information. No request body required.</parameter>
+ <parameter name="mcpReadOnly">true</parameter>
+ <parameter name="mcpIdempotent">true</parameter>
+ <parameter name="mcpInputSchema">{
+ "type": "object",
+ "properties": {}
+ }</parameter>
<description>
Get service metadata, capabilities, and current device statistics.
@@ -250,6 +322,17 @@
Generate synthetic test portfolio data for benchmarking.
================================================================ -->
<operation name="generateTestData">
+ <parameter name="mcpDescription">Generate a synthetic equal-weighted portfolio with a positive-semi-definite covariance matrix. Returns a ready-to-use portfolioVariance request body.</parameter>
+ <parameter name="mcpReadOnly">true</parameter>
+ <parameter name="mcpIdempotent">false</parameter>
+ <parameter name="mcpInputSchema">{
+ "type": "object",
+ "required": ["n_assets"],
+ "properties": {
+ "n_assets": {"type": "integer", "minimum": 2, "maximum": 2000,
+ "description": "Number of assets to generate"}
+ }
+ }</parameter>
<description>
Generate synthetic portfolio test data for benchmarking.
(axis-axis2-c-core) 18/21: D2 fix: eliminate static-string cast in axis2_json_secure_fault
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit dd7438516a9c5ad5c22cbde98b9cdeadf71700e0
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 06:52:27 2026 -1000
D2 fix: eliminate static-string cast in axis2_json_secure_fault
The original code returned (axis2_char_t*)(uintptr_t)FAULT_ALLOC_FALLBACK
on OOM — a static string that the caller would free via AXIS2_FREE,
causing undefined behaviour.
Fix: on json-c OOM or axutil_strdup failure, build a minimal JSON
error via snprintf into a stack buffer and axutil_strdup that instead.
The returned pointer is always heap-allocated and safe to AXIS2_FREE.
Returns NULL only when env itself is NULL or on total memory exhaustion
(documented in the header).
Also updated the header doc to reflect NULL-on-NULL-env contract.
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../src/axis2_json_secure_fault.c | 31 ++++++++++++++--------
.../src/axis2_json_secure_fault.h | 7 ++---
2 files changed, 24 insertions(+), 14 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
index 568099c29..bce2a3c23 100644
--- a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
+++ b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
@@ -107,9 +107,11 @@ axis2_json_corr_id_generate(char *buf, size_t len)
* ============================================================================
*/
-/* Returned verbatim when json-c allocation fails (static, never freed) */
-static const char FAULT_ALLOC_FALLBACK[] =
- "{\"status\":\"ERROR\",\"error\":\"internal error\"}";
+/* Minimal fallback written directly via snprintf when json-c itself fails.
+ * Unlike the static string approach, this is always heap-allocated so the
+ * caller can safely free it with AXIS2_FREE in all code paths. */
+#define FAULT_FALLBACK_TEMPLATE \
+ "{\"status\":\"ERROR\",\"correlation_id\":\"%s\",\"error\":\"internal error\"}"
AXIS2_EXTERN axis2_char_t *AXIS2_CALL
axis2_json_secure_fault(
@@ -117,14 +119,19 @@ axis2_json_secure_fault(
const char *corr_id,
const char *safe_message)
{
- const char *safe_corr = corr_id ? corr_id : "unknown";
- const char *safe_msg = safe_message ? safe_message
+ if (!env) return NULL; /* cannot allocate without env */
+
+ const char *safe_corr = (corr_id && corr_id[0]) ? corr_id : "unknown";
+ const char *safe_msg = (safe_message && safe_message[0])
+ ? safe_message
: "an internal error occurred";
json_object *root = json_object_new_object();
if (!root) {
- /* json-c allocation failed — return static fallback (never NULL) */
- return (axis2_char_t *)(uintptr_t)FAULT_ALLOC_FALLBACK;
+ /* json-c OOM — build fallback string via snprintf (heap-allocated) */
+ char buf[128];
+ snprintf(buf, sizeof(buf), FAULT_FALLBACK_TEMPLATE, safe_corr);
+ return axutil_strdup(env, buf); /* NULL on total OOM — documented */
}
json_object_object_add(root, "status",
@@ -138,16 +145,18 @@ axis2_json_secure_fault(
root, JSON_C_TO_STRING_PLAIN);
axis2_char_t *result = NULL;
- if (json_cstr && env) {
+ if (json_cstr) {
result = axutil_strdup(env, json_cstr);
}
json_object_put(root); /* free entire tree */
if (!result) {
- /* axutil_strdup failed or env was NULL */
- return (axis2_char_t *)(uintptr_t)FAULT_ALLOC_FALLBACK;
+ /* axutil_strdup OOM after successful serialisation — same snprintf fallback */
+ char buf[128];
+ snprintf(buf, sizeof(buf), FAULT_FALLBACK_TEMPLATE, safe_corr);
+ result = axutil_strdup(env, buf);
}
- return result;
+ return result; /* NULL only on total memory exhaustion */
}
diff --git a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h
index dd21293fd..cbf5ef7df 100644
--- a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h
+++ b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.h
@@ -90,11 +90,12 @@ axis2_json_corr_id_generate(char *buf, size_t len);
* readable description suitable for API consumers.
*
* @param env Axis2/C environment (allocator used for the result).
+ * NULL causes an immediate NULL return.
* @param corr_id 16-char hex correlation ID from axis2_json_corr_id_generate().
- * NULL produces "unknown" in the response.
+ * NULL or empty produces "unknown" in the response.
* @param safe_message Public-safe error description. NULL uses a generic message.
- * @return Heap-allocated JSON string; caller frees with AXIS2_FREE.
- * Returns a minimal static fallback on allocation failure (never NULL).
+ * @return Heap-allocated JSON string; caller MUST free with AXIS2_FREE.
+ * Returns NULL only when @p env is NULL or on total memory exhaustion.
*/
AXIS2_EXTERN axis2_char_t *AXIS2_CALL
axis2_json_secure_fault(
(axis-axis2-c-core) 20/21: Apply Gemini D2 review findings
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 9a9f53001fe8fe5ce663934bc8ce091660a615c3
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Apr 7 06:56:36 2026 -1000
Apply Gemini D2 review findings
Major:
- mcp_catalog_handler.c: capture json_cstr length eagerly into json_len
before axutil_strdup/json_object_put so the log message does not use
a pointer into freed json-c memory; add (axis2_char_t *) cast on the
axutil_strdup void* return value.
Minor:
- financial_benchmark_service_handler.c: replace raw axutil_strdup error
string in process_json_only() with axis2_json_secure_fault() for
consistent correlation-ID error surface.
- axis2_json_secure_fault.c: add #include <stdlib.h> explicitly; update
fallback PRNG comment to note clock() returns (clock_t)-1 (not 0) on
POSIX failure, and document the non-thread-safe characteristic.
(Finding 6 — AXIS2_LOG_INFO missing AXIS2_LOG_SI — confirmed false
positive: AXIS2_LOG_INFO maps to axutil_log_impl_log_info which takes
(log, format, ...) with no file/line args, unlike AXIS2_LOG_WARNING/ERROR.)
Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
.../financial-benchmark-service/src/axis2_json_secure_fault.c | 7 ++++++-
.../src/financial_benchmark_service_handler.c | 7 +++++--
.../financial-benchmark-service/src/mcp_catalog_handler.c | 10 ++++++++--
3 files changed, 19 insertions(+), 5 deletions(-)
diff --git a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
index bce2a3c23..4e166a93e 100644
--- a/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
+++ b/samples/user_guide/financial-benchmark-service/src/axis2_json_secure_fault.c
@@ -44,6 +44,7 @@
#include <stdio.h>
#include <stdint.h>
+#include <stdlib.h> /* size_t */
#include <string.h>
#include <time.h>
@@ -79,7 +80,11 @@ axis2_json_corr_id_generate(char *buf, size_t len)
if (!got_random) {
/* Fallback: mix time, clock, and pid with XOR shifts.
- * Not cryptographically random — adequate for log correlation only. */
+ * Not cryptographically random — adequate for log correlation only.
+ * Note: clock() returns (clock_t)-1 on POSIX when unavailable, not 0;
+ * that sentinel still contributes non-zero bits after the XOR mix.
+ * Not thread-safe: concurrent fallback calls at the same instant may
+ * produce duplicate IDs; this is acceptable for log correlation. */
uint64_t v = (uint64_t)time(NULL);
v ^= (uint64_t)clock() << 17;
#ifdef __linux__
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
index 429746f57..41d742942 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
@@ -229,9 +229,12 @@ financial_benchmark_service_process_json_only(
if (!json_request)
{
+ char corr_id[AXIS2_JSON_CORR_ID_LEN];
+ axis2_json_corr_id_generate(corr_id, sizeof(corr_id));
AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
- "FinancialBenchmarkService: No JSON request provided");
- return axutil_strdup(env, "{\"error\": \"No JSON request provided\"}");
+ "[FinancialBenchmark][%s] process_json_only: no JSON request",
+ corr_id);
+ return axis2_json_secure_fault(env, corr_id, "no request body");
}
/* Try to extract operation from JSON */
diff --git a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
index ff4866457..669607556 100644
--- a/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/mcp_catalog_handler.c
@@ -396,9 +396,15 @@ mcp_catalog_generate_json(
const char *json_cstr =
json_object_to_json_string_ext(root, JSON_C_TO_STRING_PLAIN);
+ /* Record length before json_object_put() invalidates json_cstr.
+ * json_object_to_json_string_ext() returns a pointer into root's internal
+ * buffer; any call to json_object_put(root) after this point makes it
+ * a dangling pointer. Capture size eagerly so the log message is safe. */
+ size_t json_len = json_cstr ? strlen(json_cstr) : 0;
+
axis2_char_t *result = NULL;
if (json_cstr) {
- result = axutil_strdup(env, json_cstr);
+ result = (axis2_char_t *)axutil_strdup(env, json_cstr);
if (!result) {
/* axutil_strdup OOM — log with correlation ID and return fault */
char corr_id[AXIS2_JSON_CORR_ID_LEN];
@@ -406,7 +412,7 @@ mcp_catalog_generate_json(
AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
"[MCP][%s] axutil_strdup failed serialising catalog "
"(%zu bytes) — out of memory",
- corr_id, strlen(json_cstr));
+ corr_id, json_len);
json_object_put(root);
return axis2_json_secure_fault(env, corr_id,
"catalog generation failed — see server log");