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/10/20 17:34:49 UTC

(axis-axis2-c-core) branch master updated: Refuse finbench inputs the service used to default or drop

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


The following commit(s) were added to refs/heads/master by this push:
     new 68b458146 Refuse finbench inputs the service used to default or drop
68b458146 is described below

commit 68b4581468e14804ef554a4154df31cfcc4e01fd
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Oct 19 16:30:46 2026 -1000

    Refuse finbench inputs the service used to default or drop
    
    model, percentiles, prob_tolerance, check_positive_definite, asset_ids,
    covariance_basis and scenarioAnalysis n_assets are now refused with a
    named error when wrong, instead of being replaced, truncated or dropped;
    use_hash_lookup, which never changed anything, is refused. The MCP and
    services.xml schemas now state the limits the code enforces.
    
    Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
 docs/MCP_EXAMPLES.md                               |   5 +-
 .../financial-benchmark-service/services.xml       |  69 ++---
 .../src/financial_benchmark_service.c              | 280 +++++++++++++++------
 .../src/financial_benchmark_service.h              |  52 +++-
 .../financial-benchmark-service/src/finbench_mcp.c |  73 +++---
 5 files changed, 318 insertions(+), 161 deletions(-)

diff --git a/docs/MCP_EXAMPLES.md b/docs/MCP_EXAMPLES.md
index 78d794ccd..c0395611a 100644
--- a/docs/MCP_EXAMPLES.md
+++ b/docs/MCP_EXAMPLES.md
@@ -512,8 +512,7 @@ curl -k --http2 -s \
               {"price": 180.0, "probability": 0.10}
           ]
         }
-      ],
-      "use_hash_lookup": true
+      ]
     }' \
     https://10.10.10.10/services/FinancialBenchmarkService/scenarioAnalysis
 ```
@@ -535,7 +534,7 @@ curl -k --http2 -s \
 
 **MCP stdio:**
 ```bash
-echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"scenarioAnalysis","arguments":{"n_assets":3,"assets":[{"asset_id":1,"current_price":150.0,"position_size":100,"scenarios":[{"price":165.0,"probability":0.15},{"price":157.5,"probability":0.25},{"price":150.0,"probability":0.30},{"price":142.5,"probability":0.20},{"price":135.0,"probability":0.10}]},{"asset_id":2,"current_price":75.0,"position_size":200,"scenarios":[{"price":82.5,"probability":0.15},{"price":78.75,"prob [...]
+echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"scenarioAnalysis","arguments":{"n_assets":3,"assets":[{"asset_id":1,"current_price":150.0,"position_size":100,"scenarios":[{"price":165.0,"probability":0.15},{"price":157.5,"probability":0.25},{"price":150.0,"probability":0.30},{"price":142.5,"probability":0.20},{"price":135.0,"probability":0.10}]},{"asset_id":2,"current_price":75.0,"position_size":200,"scenarios":[{"price":82.5,"probability":0.15},{"price":78.75,"prob [...]
     | /usr/local/axis2c/bin/financial-benchmark-mcp
 ```
 
diff --git a/samples/user_guide/financial-benchmark-service/services.xml b/samples/user_guide/financial-benchmark-service/services.xml
index a4536fcb1..abf895674 100644
--- a/samples/user_guide/financial-benchmark-service/services.xml
+++ b/samples/user_guide/financial-benchmark-service/services.xml
@@ -86,18 +86,20 @@
             <parameter name="mcpIdempotent">true</parameter>
             <parameter name="mcpInputSchema">{
                 "type": "object",
-                "required": ["n_assets", "weights", "covariance_matrix"],
+                "required": ["weights", "covariance_matrix"],
                 "properties": {
-                    "n_assets":          {"type": "integer",  "minimum": 2, "maximum": 2000,
-                                          "description": "Number of assets in the portfolio"},
+                    "n_assets":          {"type": "integer",  "minimum": 1, "maximum": 3000,
+                                          "description": "Number of assets in the portfolio. Inferred from weights if omitted; if given it must equal the weights length."},
                     "weights":           {"type": "array",    "items": {"type": "number"},
                                           "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": "Exactly n_assets×n_assets values, row-major order (element [i*n+j] is cov(i,j))"},
                     "normalize_weights": {"type": "boolean",  "default": false,
                                           "description": "Scale weights to sum=1 before calculation. Default: false — caller must supply weights that already sum to 1."},
-                    "n_periods_per_year": {"type": "integer", "default": 1,
+                    "n_periods_per_year": {"type": "integer", "minimum": 1, "maximum": 10000000, "default": 1,
                                           "description": "Periods per year of the INPUT matrix. Default 1 = already annualised (any matrix from composeCovariance or covarianceFromReturns). Only for a per-period matrix: 252 daily, 52 weekly, 12 monthly."},
+                    "covariance_basis":  {"type": "string",   "enum": ["annualized", "per_period"],
+                                          "description": "Optional label for the matrix's basis, as the covariance producers echo it. \"annualized\" with n_periods_per_year other than 1 is refused; the value applied is always n_periods_per_year."},
                     "request_id":        {"type": "string",   "description": "Optional correlation ID echoed in the response"}
                 }
             }</parameter>
@@ -169,7 +171,7 @@
                     "check_positive_definite": {"type": "boolean", "default": true,
                                                 "description": "Cholesky check (O(n³)). Refuses a matrix that is not positive definite."},
                     "asset_ids":               {"type": "array",   "items": {"type": "string"},
-                                                "description": "Optional labels, one per asset, echoed back."},
+                                                "description": "Optional labels, exactly one string per asset, echoed back."},
                     "request_id":              {"type": "string",  "description": "Optional correlation ID echoed in the response"}
                 }
             }</parameter>
@@ -231,12 +233,12 @@
                                                 "description": "Number of assets (max 50: the request carries n_assets×n_obs numbers). Inferred from a nested returns array."},
                     "n_obs":                   {"type": "integer", "minimum": 2, "maximum": 5000,
                                                 "description": "Observations per asset (max 5000, about twenty years of daily data). Inferred from the first row."},
-                    "n_periods_per_year":      {"type": "number",  "default": 252,
+                    "n_periods_per_year":      {"type": "number",  "exclusiveMinimum": 0, "default": 252,
                                                 "description": "Annualisation factor: Σ is multiplied by it and the means with it. 252 daily, 12 monthly, 1 to keep the input's own basis."},
                     "check_positive_definite": {"type": "boolean", "default": true,
                                                 "description": "Cholesky check. Refuses a rank-deficient sample (fewer complete observations than assets, or duplicated series)."},
                     "asset_ids":               {"type": "array",   "items": {"type": "string"},
-                                                "description": "Optional labels, one per asset, echoed back."},
+                                                "description": "Optional labels, exactly one string per asset, echoed back."},
                     "request_id":              {"type": "string",  "description": "Optional correlation ID echoed in the response"}
                 }
             }</parameter>
@@ -298,20 +300,21 @@
             <parameter name="mcpIdempotent">false</parameter>
             <parameter name="mcpInputSchema">{
                 "type": "object",
-                "required": ["n_simulations", "n_periods", "initial_value", "expected_return", "volatility"],
+                "required": [],
+                "description": "All fields have defaults; an empty {} request body is valid.",
                 "properties": {
-                    "n_simulations":   {"type": "integer", "minimum": 100,  "maximum": 1000000,
+                    "n_simulations":   {"type": "integer", "minimum": 1,    "maximum": 1000000, "default": 10000,
                                         "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%)"},
+                    "n_periods":       {"type": "integer", "minimum": 1,    "maximum": 100000, "default": 252,
+                                        "description": "Number of time steps (252 = 1 trading year). n_simulations × n_periods (× n_assets for a book) may not exceed 1e9."},
+                    "initial_value":   {"type": "number",  "exclusiveMinimum": 0, "default": 1000000, "description": "Initial portfolio value"},
+                    "expected_return": {"type": "number",  "default": 0.08, "description": "Annualised expected return (e.g. 0.08 for 8%)"},
+                    "volatility":      {"type": "number",  "minimum": 0, "default": 0.20, "description": "Annualised volatility (e.g. 0.20 for 20%)"},
                     "random_seed":     {"type": "integer", "description": "Seed for reproducible results. When omitted, seeded from time() — results differ across calls."},
-                    "n_periods_per_year": {"type": "integer", "default": 252,
+                    "n_periods_per_year": {"type": "integer", "minimum": 1, "maximum": 10000000, "default": 252,
                                         "description": "Trading periods per year; dt = 1/n_periods_per_year. Must match the basis of expected_return and volatility."},
-                    "percentiles":     {"type": "array",   "items": {"type": "number"}, "maxItems": 8,
-                                        "description": "Tail levels for VaR, e.g. [0.01, 0.05] for 99% and 95%. Default [0.01, 0.05]."},
+                    "percentiles":     {"type": "array",   "items": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1}, "maxItems": 8,
+                                        "description": "Tail levels for VaR, e.g. [0.01, 0.05] for 99% and 95%. Max 8 values, each strictly between 0 and 1. Default [0.01, 0.05]."},
                     "model":           {"type": "string",  "enum": ["gbm", "merton"], "default": "gbm"},
                     "jump_intensity":  {"type": "number",  "default": 1.0,  "description": "Merton: jumps per year (lambda*dt must be at most 0.1)."},
                     "jump_mean":       {"type": "number",  "default": -0.03, "description": "Merton: mean log-jump."},
@@ -320,7 +323,7 @@
                                         "description": "Correlated book: n_assets×n_assets annualized covariance matrix (flat row-major or nested), as composeCovariance returns it. Switches the run to a buy-and-hold multi-asset book; volatility is then ignored. Must be positive definite."},
                     "weights":         {"type": "array",   "items": {"type": "number"},
                                         "description": "Correlated book: n_assets weights ≥ 0 (long-only), summing to 1 unless normalize_weights."},
-                    "n_assets":        {"type": "integer", "maximum": 100, "description": "Correlated book: inferred from weights if omitted."},
+                    "n_assets":        {"type": "integer", "minimum": 1, "maximum": 100, "description": "Correlated book: inferred from weights if omitted."},
                     "expected_returns": {"type": "array",  "items": {"type": "number"},
                                         "description": "Correlated book: per-asset drifts. Default: expected_return for every asset."},
                     "normalize_weights": {"type": "boolean", "default": false},
@@ -391,20 +394,20 @@
             <parameter name="mcpIdempotent">true</parameter>
             <parameter name="mcpInputSchema">{
                 "type": "object",
-                "required": ["n_assets", "assets"],
+                "required": ["assets"],
                 "properties": {
-                    "n_assets":        {"type": "integer", "minimum": 1, "maximum": 2000,
-                                        "description": "Number of assets"},
+                    "n_assets":        {"type": "integer", "minimum": 1, "maximum": 3000,
+                                        "description": "Optional; if given it must equal the length of assets."},
                     "assets":          {"type": "array",
-                                        "minItems": 1, "maxItems": 2000,
-                                        "description": "Array of asset scenario objects. Length must equal n_assets.",
+                                        "minItems": 1, "maxItems": 3000,
+                                        "description": "Array of asset scenario objects.",
                                         "items": {
                                             "type": "object",
-                                            "required": ["asset_id", "current_price", "scenarios", "position_size"],
+                                            "required": ["current_price", "scenarios", "position_size"],
                                             "properties": {
-                                                "asset_id":       {"type": "integer"},
-                                                "current_price":  {"type": "number"},
-                                                "scenarios":      {"type": "array", "minItems": 2,
+                                                "asset_id":       {"type": "integer", "description": "Unique asset identifier. Default: 1-based position in assets."},
+                                                "current_price":  {"type": "number", "exclusiveMinimum": 0},
+                                                "scenarios":      {"type": "array", "minItems": 1, "maxItems": 10,
                                                                    "description": "One entry per scenario; probabilities must sum to 1.0.",
                                                                    "items": {"type": "object",
                                                                              "required": ["price", "probability"],
@@ -412,11 +415,11 @@
                                                                                  "price":       {"type": "number", "description": "Target price in this scenario"},
                                                                                  "probability": {"type": "number", "description": "Probability weight of this scenario"}
                                                                              }}},
-                                                "position_size":  {"type": "number", "description": "Shares held (integer positions; fractional positions not supported by C implementation)"}
+                                                "position_size":  {"type": "number", "exclusiveMinimum": 0, "description": "Units held, > 0 (long only)"}
                                             }
                                         }},
-                    "use_hash_lookup": {"type": "boolean", "default": true,
-                                        "description": "Use O(1) hash lookup instead of O(n) linear scan"},
+                    "prob_tolerance":  {"type": "number", "exclusiveMinimum": 0, "maximum": 0.1, "default": 0.0001,
+                                        "description": "Tolerance for probability sum validation per asset. Must be greater than 0 and at most 0.1."},
                     "request_id":      {"type": "string"}
                 }
             }</parameter>
@@ -445,7 +448,6 @@
                         },
                         ...
                     ],
-                    "use_hash_lookup": true,
                     "request_id": "scenario-001"
                 }
             </description>
@@ -486,7 +488,7 @@
                     "service_name": "FinancialBenchmarkService",
                     "version": "1.0.0",
                     "operations": ["portfolioVariance", "composeCovariance", "covarianceFromReturns", "monteCarlo", ...],
-                    "max_assets": 2000,
+                    "max_assets": 3000,
                     "max_simulations": 1000000,
                     "memory_constraint_mb": 512,
                     "current_memory_kb": 28456,
@@ -514,9 +516,8 @@
             <parameter name="mcpIdempotent">true</parameter>
             <parameter name="mcpInputSchema">{
                 "type": "object",
-                "required": ["n_assets"],
                 "properties": {
-                    "n_assets": {"type": "integer", "minimum": 2, "maximum": 2000,
+                    "n_assets": {"type": "integer", "minimum": 1, "maximum": 3000, "default": 100,
                                  "description": "Number of assets to generate"}
                 }
             }</parameter>
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 d6740c880..f67a3eead 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
@@ -200,6 +200,79 @@ finbench_read_periods_per_year(json_object *json_obj, double def,
 #define FINBENCH_PERIODS_INVALID_MSG \
     "n_periods_per_year must be a number > 0%s; omit it for the default."
 
+/* Record a refused field in a request's input_error buffer. The first
+ * refusal is kept, so the caller sees the earliest problem in the request.
+ * The calculation returns it as the error_message before doing any work. */
+static void
+finbench_input_error(char *buf, const char *fmt, ...)
+{
+    va_list ap;
+
+    if (buf[0])
+        return;
+    va_start(ap, fmt);
+    vsnprintf(buf, FINBENCH_INPUT_ERROR_LEN, fmt, ap);
+    va_end(ap);
+}
+
+/* check_positive_definite: absent means true; otherwise it must be a JSON
+ * boolean. json_object_get_boolean would read the string "false" as true. */
+static axis2_bool_t
+finbench_read_check_pd(json_object *json_obj, char *err)
+{
+    json_object *v = NULL;
+
+    if (!json_object_object_get_ex(json_obj, "check_positive_definite", &v))
+        return AXIS2_TRUE;
+    if (!json_object_is_type(v, json_type_boolean)) {
+        finbench_input_error(err, "check_positive_definite must be true or false.");
+        return AXIS2_TRUE;
+    }
+    return json_object_get_boolean(v) ? AXIS2_TRUE : AXIS2_FALSE;
+}
+
+/* asset_ids: absent, or an array of exactly n strings. A wrong length or a
+ * non-string entry is refused rather than dropped or echoed as text. */
+static char **
+finbench_read_asset_ids(const axutil_env_t *env, json_object *json_obj,
+                        int n, char *err)
+{
+    json_object *arr = NULL;
+    char **ids;
+    int i, len;
+
+    if (!json_object_object_get_ex(json_obj, "asset_ids", &arr))
+        return NULL;
+    if (!json_object_is_type(arr, json_type_array)) {
+        finbench_input_error(err,
+            "asset_ids must be an array of %d strings, one per asset.", n);
+        return NULL;
+    }
+    len = (int)json_object_array_length(arr);
+    if (len != n) {
+        finbench_input_error(err,
+            "asset_ids has %d entries but n_assets is %d; give one string per asset.",
+            len, n);
+        return NULL;
+    }
+    for (i = 0; i < n; i++) {
+        if (!json_object_is_type(json_object_array_get_idx(arr, i), json_type_string)) {
+            finbench_input_error(err, "asset_ids[%d] must be a string.", i);
+            return NULL;
+        }
+    }
+    ids = AXIS2_MALLOC(env->allocator, (size_t)n * sizeof(char *));
+    if (!ids) {
+        finbench_input_error(err, "Memory allocation failed for asset_ids.");
+        return NULL;
+    }
+    for (i = 0; i < n; i++) {
+        ids[i] = axutil_strdup(env,
+            json_object_get_string(json_object_array_get_idx(arr, i)));
+    }
+    return ids;
+}
+
 /* ============================================================================
  * Portfolio Variance Implementation
  *
@@ -373,11 +446,17 @@ finbench_portfolio_variance_request_create_from_json(
      * n_periods_per_year beside covariance_basis "annualized". Taking that
      * echo as this matrix's basis would annualize it twice, so the pair is
      * flagged and refused in the calculation. */
-    if (request->n_periods_per_year != 1 &&
-        json_object_object_get_ex(json_obj, "covariance_basis", &value_obj) &&
-        json_object_is_type(value_obj, json_type_string) &&
-        strcmp(json_object_get_string(value_obj), "annualized") == 0) {
-        request->basis_conflict = AXIS2_TRUE;
+    if (json_object_object_get_ex(json_obj, "covariance_basis", &value_obj)) {
+        const char *basis = json_object_is_type(value_obj, json_type_string)
+            ? json_object_get_string(value_obj) : NULL;
+        if (!basis || (strcmp(basis, "annualized") != 0 &&
+                       strcmp(basis, "per_period") != 0)) {
+            finbench_input_error(request->input_error,
+                "covariance_basis must be \"annualized\" or \"per_period\".");
+        } else if (request->n_periods_per_year != 1 &&
+                   strcmp(basis, "annualized") == 0) {
+            request->basis_conflict = AXIS2_TRUE;
+        }
     }
 
     json_object_put(json_obj);
@@ -500,6 +579,14 @@ finbench_calculate_portfolio_variance(
 
     n = request->n_assets;
 
+    if (request->input_error[0]) {
+        response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+        response->error_message = axutil_strdup(env, request->input_error);
+        AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench portfolioVariance: %s",
+            request->input_error);
+        return response;
+    }
+
     if (request->n_periods_per_year_invalid) {
         char err_buf[160];
         snprintf(err_buf, sizeof(err_buf), FINBENCH_PERIODS_INVALID_MSG,
@@ -1047,26 +1134,12 @@ finbench_compose_covariance_request_create_from_json(
     }
 
     /* check_positive_definite — default true */
-    if (json_object_object_get_ex(json_obj, "check_positive_definite", &value_obj)) {
-        request->check_positive_definite = json_object_get_boolean(value_obj)
-            ? AXIS2_TRUE : AXIS2_FALSE;
-    }
+    request->check_positive_definite =
+        finbench_read_check_pd(json_obj, request->input_error);
 
-    /* asset_ids (optional; only kept when the length matches) */
-    if (json_object_object_get_ex(json_obj, "asset_ids", &array_obj) &&
-        json_object_is_type(array_obj, json_type_array) &&
-        (int)json_object_array_length(array_obj) == request->n_assets) {
-        request->asset_ids = AXIS2_MALLOC(env->allocator,
-            (size_t)request->n_assets * sizeof(char *));
-        if (request->asset_ids) {
-            memset(request->asset_ids, 0, (size_t)request->n_assets * sizeof(char *));
-            for (i = 0; i < request->n_assets; i++) {
-                json_object *elem = json_object_array_get_idx(array_obj, i);
-                const char *id = elem ? json_object_get_string(elem) : NULL;
-                request->asset_ids[i] = id ? axutil_strdup(env, id) : NULL;
-            }
-        }
-    }
+    /* asset_ids (optional; exactly n_assets strings when present) */
+    request->asset_ids = finbench_read_asset_ids(env, json_obj,
+        request->n_assets, request->input_error);
 
     if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
         const char *rid = json_object_get_string(value_obj);
@@ -1204,6 +1277,9 @@ finbench_compose_covariance(
     if (!request || !request->volatilities) {
         return compose_fail(env, response, "Invalid request parameters");
     }
+    if (request->input_error[0]) {
+        return compose_fail(env, response, "%s", request->input_error);
+    }
 
     n = request->n_assets;
     response->n_assets = n;
@@ -1698,26 +1774,12 @@ finbench_covariance_from_returns_request_create_from_json(
                                         0, &request->n_periods_per_year))
         request->n_periods_per_year_invalid = AXIS2_TRUE;
 
-    if (json_object_object_get_ex(json_obj, "check_positive_definite", &value_obj)) {
-        request->check_positive_definite = json_object_get_boolean(value_obj)
-            ? AXIS2_TRUE : AXIS2_FALSE;
-    }
+    request->check_positive_definite =
+        finbench_read_check_pd(json_obj, request->input_error);
 
-    /* asset_ids (optional; only kept when the length matches) */
-    if (json_object_object_get_ex(json_obj, "asset_ids", &array_obj) &&
-        json_object_is_type(array_obj, json_type_array) &&
-        (int)json_object_array_length(array_obj) == request->n_assets) {
-        request->asset_ids = AXIS2_MALLOC(env->allocator,
-            (size_t)request->n_assets * sizeof(char *));
-        if (request->asset_ids) {
-            memset(request->asset_ids, 0, (size_t)request->n_assets * sizeof(char *));
-            for (i = 0; i < request->n_assets; i++) {
-                json_object *elem = json_object_array_get_idx(array_obj, i);
-                const char *id = elem ? json_object_get_string(elem) : NULL;
-                request->asset_ids[i] = id ? axutil_strdup(env, id) : NULL;
-            }
-        }
-    }
+    /* asset_ids (optional; exactly n_assets strings when present) */
+    request->asset_ids = finbench_read_asset_ids(env, json_obj,
+        request->n_assets, request->input_error);
 
     if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
         const char *rid = json_object_get_string(value_obj);
@@ -1822,6 +1884,9 @@ finbench_covariance_from_returns(
     if (!request || !request->returns) {
         return cfr_fail(env, response, "Invalid request parameters");
     }
+    if (request->input_error[0]) {
+        return cfr_fail(env, response, "%s", request->input_error);
+    }
 
     n = request->n_assets;
     obs = request->n_obs;
@@ -2256,19 +2321,40 @@ finbench_monte_carlo_request_create_from_json(
         request->n_periods_per_year = (int)npy;
     }
 
-    /* percentiles — array of tail levels in (0,1); default {0.01, 0.05} */
+    /* percentiles — array of up to FINBENCH_MAX_PERCENTILES numbers, each
+     * strictly inside (0,1); default {0.01, 0.05}. An extra or out-of-range
+     * entry is refused: dropping it returned SUCCESS without the VaR asked for. */
     {
         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 > FINBENCH_MAX_PERCENTILES) n_pct = FINBENCH_MAX_PERCENTILES;
+        if (json_object_object_get_ex(json_obj, "percentiles", &pct_arr)) {
+            int n_pct, pi;
             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) {
+            if (!json_object_is_type(pct_arr, json_type_array)) {
+                finbench_input_error(request->input_error,
+                    "percentiles must be an array of up to %d numbers in (0, 1).",
+                    FINBENCH_MAX_PERCENTILES);
+            } else if ((n_pct = (int)json_object_array_length(pct_arr)) >
+                       FINBENCH_MAX_PERCENTILES) {
+                finbench_input_error(request->input_error,
+                    "percentiles has %d entries; the maximum is %d.",
+                    n_pct, FINBENCH_MAX_PERCENTILES);
+            } else {
+                for (pi = 0; pi < n_pct; pi++) {
+                    json_object *pct_elem = json_object_array_get_idx(pct_arr, pi);
+                    double p;
+                    if (!json_object_is_type(pct_elem, json_type_double) &&
+                        !json_object_is_type(pct_elem, json_type_int)) {
+                        finbench_input_error(request->input_error,
+                            "percentiles[%d] must be a number in (0, 1).", pi);
+                        break;
+                    }
+                    p = json_object_get_double(pct_elem);
+                    if (!(p > 0.0 && p < 1.0)) {
+                        finbench_input_error(request->input_error,
+                            "percentiles[%d] = %g must be strictly between 0 and 1 "
+                            "(0.05 for a 95%% VaR).", pi, p);
+                        break;
+                    }
                     request->percentiles[request->n_percentiles++] = p;
                 }
             }
@@ -2280,17 +2366,18 @@ finbench_monte_carlo_request_create_from_json(
         }
     }
 
-    /* model — 0=GBM (default), 1=Merton jump-diffusion */
+    /* model — "gbm" (default) or "merton", exactly the schema's enum. Any
+     * other value is refused; it used to run GBM without saying so. */
+    request->model = FINBENCH_MODEL_GBM;
     if (json_object_object_get_ex(json_obj, "model", &value_obj)) {
-        const char *model_str = json_object_get_string(value_obj);
-        if (model_str && (strcmp(model_str, "merton") == 0 ||
-                          strcmp(model_str, "MERTON") == 0)) {
+        const char *model_str = json_object_is_type(value_obj, json_type_string)
+            ? json_object_get_string(value_obj) : NULL;
+        if (model_str && strcmp(model_str, "merton") == 0) {
             request->model = FINBENCH_MODEL_MERTON;
-        } else {
-            request->model = FINBENCH_MODEL_GBM;
+        } else if (!model_str || strcmp(model_str, "gbm") != 0) {
+            finbench_input_error(request->input_error,
+                "model must be \"gbm\" or \"merton\".");
         }
-    } else {
-        request->model = FINBENCH_MODEL_GBM;
     }
 
     /* Jump-diffusion parameters (Merton only; harmless defaults for GBM) */
@@ -2413,11 +2500,8 @@ finbench_monte_carlo_request_create_from_json(
         }
     }
 
-    /* Validate limits */
-    if (request->n_simulations > FINBENCH_MAX_SIMULATIONS) {
-        request->n_simulations = FINBENCH_MAX_SIMULATIONS;
-    }
-
+    /* n_simulations above FINBENCH_MAX_SIMULATIONS is refused by
+     * finbench_run_monte_carlo, not clamped here. */
     json_object_put(json_obj);
     return request;
 }
@@ -2954,6 +3038,12 @@ finbench_run_monte_carlo(
         return response;
     }
 
+    if (request->input_error[0]) {
+        response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+        response->error_message = axutil_strdup(env, request->input_error);
+        return response;
+    }
+
     if (request->n_periods_per_year_invalid) {
         char err_buf[160];
         snprintf(err_buf, sizeof(err_buf), FINBENCH_PERIODS_INVALID_MSG,
@@ -3328,7 +3418,7 @@ finbench_monte_carlo_json_only(
             "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]), "
+            "percentiles (up to 8 floats in (0, 1), default [0.01, 0.05]), "
             "model (string, 'gbm' or 'merton', default 'gbm'), "
             "correlated book: covariance_matrix (float[n*n] or float[n][n]) + weights (float[n] >= 0), "
             "optional expected_returns (float[n]), normalize_weights (bool); "
@@ -3389,25 +3479,33 @@ finbench_scenario_request_create_from_json(
     }
     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;
+    /* use_hash_lookup is refused: the operation always times both the
+     * linear scan and the hash lookup and reports both, so the flag could
+     * only be ignored. */
+    if (json_object_object_get_ex(json_obj, "use_hash_lookup", NULL)) {
+        finbench_input_error(request->input_error,
+            "use_hash_lookup is not supported: scenarioAnalysis always times "
+            "both the linear scan and the hash lookup. Remove the field.");
     }
 
     /*
-     * prob_tolerance — probability sum tolerance per asset.
-     * Default 1e-4; 0.0 → default; clamped to [1e-10, 0.1].
+     * prob_tolerance — probability sum tolerance per asset. Default 1e-4;
+     * otherwise a number in (0, 0.1], refused outside it rather than
+     * replaced, so the tolerance applied is always the one asked for.
      */
+    request->prob_tolerance = 1e-4;
     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;
+        double tol;
+        if (!json_object_is_type(value_obj, json_type_double) &&
+            !json_object_is_type(value_obj, json_type_int)) {
+            finbench_input_error(request->input_error,
+                "prob_tolerance must be a number in (0, 0.1].");
+        } else if (!((tol = json_object_get_double(value_obj)) > 0.0 && tol <= 0.1)) {
+            finbench_input_error(request->input_error,
+                "prob_tolerance = %g must be > 0 and <= 0.1 (default 0.0001).", tol);
+        } else {
+            request->prob_tolerance = tol;
+        }
     }
 
     /* request_id */
@@ -3439,6 +3537,16 @@ finbench_scenario_request_create_from_json(
     }
 
     request->n_assets = n_assets;
+
+    /* n_assets is optional; when given it must match the assets array. */
+    if (json_object_object_get_ex(json_obj, "n_assets", &value_obj) &&
+        (!json_object_is_type(value_obj, json_type_int) ||
+         json_object_get_int64(value_obj) != n_assets)) {
+        finbench_input_error(request->input_error,
+            "n_assets must equal the number of entries in assets (%d); "
+            "omit it to use that count.", n_assets);
+    }
+
     request->assets = AXIS2_MALLOC(env->allocator,
         n_assets * sizeof(finbench_asset_scenario_t));
     if (!request->assets) {
@@ -3591,6 +3699,12 @@ finbench_calculate_scenarios(
         return response;
     }
 
+    if (request->input_error[0]) {
+        response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+        response->error_message = axutil_strdup(env, request->input_error);
+        return response;
+    }
+
     /* -----------------------------------------------------------------------
      * Step 1: Validate probability distributions before any computation.
      *
@@ -3981,8 +4095,8 @@ finbench_scenario_json_only(
             "\"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), "
+            "Optional: n_assets (int, must equal the assets count), "
+            "prob_tolerance (float in (0, 0.1], default 0.0001), "
             "request_id (string).\"}");
     }
 
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 e61d8a2d8..32cd574b9 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
@@ -119,6 +119,9 @@ extern "C"
 #define FINBENCH_STATUS_SUCCESS     "SUCCESS"
 #define FINBENCH_STATUS_FAILED      "FAILED"
 
+/** Size of the input_error buffer each request struct carries. */
+#define FINBENCH_INPUT_ERROR_LEN    256
+
 /* ============================================================================
  * Portfolio Variance Calculation
  *
@@ -236,6 +239,13 @@ typedef struct finbench_portfolio_variance_request
     /** n_periods_per_year was present but not a positive whole number; refused. */
     axis2_bool_t n_periods_per_year_invalid;
 
+    /**
+     * First refused optional field, as the error_message to return; empty
+     * when the request parsed cleanly. A field present with the wrong type
+     * or an out-of-range value is refused rather than defaulted.
+     */
+    char input_error[FINBENCH_INPUT_ERROR_LEN];
+
 } finbench_portfolio_variance_request_t;
 
 /**
@@ -374,6 +384,13 @@ typedef struct finbench_compose_covariance_request
     /** Request identifier for tracing */
     char *request_id;
 
+    /**
+     * First refused optional field, as the error_message to return; empty
+     * when the request parsed cleanly. A field present with the wrong type
+     * or an out-of-range value is refused rather than defaulted.
+     */
+    char input_error[FINBENCH_INPUT_ERROR_LEN];
+
 } finbench_compose_covariance_request_t;
 
 /**
@@ -493,6 +510,13 @@ typedef struct finbench_covariance_from_returns_request
     /** Request identifier for tracing */
     char *request_id;
 
+    /**
+     * First refused optional field, as the error_message to return; empty
+     * when the request parsed cleanly. A field present with the wrong type
+     * or an out-of-range value is refused rather than defaulted.
+     */
+    char input_error[FINBENCH_INPUT_ERROR_LEN];
+
 } finbench_covariance_from_returns_request_t;
 
 /**
@@ -741,7 +765,8 @@ typedef struct finbench_monte_carlo_request
      * Percentile levels for VaR reporting, values strictly in (0, 1). The
      * service interprets p as a LOSS PROBABILITY, so 0.05 means "5% chance
      * of losing at least VaR_p" (i.e., a 95% VaR). Defaults: {0.01, 0.05}.
-     * Up to FINBENCH_MAX_PERCENTILES values; any beyond that are ignored.
+     * Up to FINBENCH_MAX_PERCENTILES values; the parser refuses more, and
+     * refuses a value that is not a number strictly inside (0, 1).
      *
      * Estimator: floor(p * n_sims). After ascending sort,
      *   VaR_p = initial_value - sorted_final_values[floor(p * n_sims)]
@@ -856,6 +881,13 @@ typedef struct finbench_monte_carlo_request
     /** Request identifier */
     char *request_id;
 
+    /**
+     * First refused optional field, as the error_message to return; empty
+     * when the request parsed cleanly. A field present with the wrong type
+     * or an out-of-range value is refused rather than defaulted.
+     */
+    char input_error[FINBENCH_INPUT_ERROR_LEN];
+
 } finbench_monte_carlo_request_t;
 
 /**
@@ -1026,23 +1058,27 @@ typedef struct finbench_scenario_request
     /** Array of asset scenarios */
     finbench_asset_scenario_t *assets;
 
-    /** 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; the service rejects the whole request otherwise.
-     * Default: 1e-4 (0.01%). Passing 0.0 or any negative value falls back
-     * to this default. Values > 0.1 are capped at 0.1 to prevent an
-     * overly-loose tolerance from hiding real probability-sum bugs
-     * (e.g., three scenarios at 0.3 each summing to 0.9).
+     * Default: 1e-4 (0.01%). The parser refuses a value outside (0, 0.1]:
+     * a looser tolerance would hide real probability-sum bugs (e.g., three
+     * scenarios at 0.3 each summing to 0.9). A direct C caller that leaves
+     * it 0 gets the default.
      */
     double prob_tolerance;
 
     /** Request identifier */
     char *request_id;
 
+    /**
+     * First refused optional field, as the error_message to return; empty
+     * when the request parsed cleanly. A field present with the wrong type
+     * or an out-of-range value is refused rather than defaulted.
+     */
+    char input_error[FINBENCH_INPUT_ERROR_LEN];
+
 } finbench_scenario_request_t;
 
 /**
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 c98e8550e..65ad758da 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -67,8 +67,9 @@ static const char SCHEMA_PORTFOLIO_VARIANCE[] =
     "{"
     "\"type\":\"object\","
     "\"properties\":{"
-        "\"n_assets\":{\"type\":\"integer\","
-            "\"description\":\"Number of assets in the portfolio (max 3000)\"},"
+        "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":3000,"
+            "\"description\":\"Number of assets in the portfolio (max 3000). Inferred from weights if "
+                "omitted; if given it must equal the weights length\"},"
         "\"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\"},"
@@ -78,26 +79,30 @@ static const char SCHEMA_PORTFOLIO_VARIANCE[] =
                 "their covariance_matrix straight through\"},"
         "\"normalize_weights\":{\"type\":\"boolean\","
             "\"description\":\"Rescale weights to sum to 1.0 before computing variance. Default: false\"},"
-        "\"n_periods_per_year\":{\"type\":\"integer\",\"default\":1,"
+        "\"n_periods_per_year\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":10000000,\"default\":1,"
             "\"description\":\"Periods per year of the INPUT matrix. Default 1 = the matrix is already "
                 "annualized (leave it for any matrix produced by this service). Only for a per-period "
                 "matrix: 252 daily, 52 weekly, 12 monthly. The response echoes covariance_basis. "
                 "Refused if the request also says covariance_basis \\\"annualized\\\"\"},"
+        "\"covariance_basis\":{\"type\":\"string\",\"enum\":[\"annualized\",\"per_period\"],"
+            "\"description\":\"Optional label for the matrix's basis, as the covariance producers echo it. "
+                "\\\"annualized\\\" with n_periods_per_year other than 1 is refused; the value applied "
+                "is always n_periods_per_year\"},"
         "\"request_id\":{\"type\":\"string\","
             "\"description\":\"Optional identifier echoed in the response for request tracing\"}"
     "},"
-    "\"required\":[\"n_assets\",\"weights\",\"covariance_matrix\"]"
+    "\"required\":[\"weights\",\"covariance_matrix\"]"
     "}";
 
 static const char SCHEMA_COMPOSE_COVARIANCE[] =
     "{"
     "\"type\":\"object\","
     "\"properties\":{"
-        "\"n_assets\":{\"type\":\"integer\","
+        "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":500,"
             "\"description\":\"Number of assets (max 500: the check is O(n^3) and the reply is O(n^2)). "
                 "Inferred from volatilities if omitted\"},"
         "\"volatilities\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
-            "\"description\":\"Per-asset volatilities, each > 0, as decimals (0.22 = 22%%). "
+            "\"description\":\"Per-asset volatilities, each > 0, as decimals (0.22 = 22%). "
                 "The covariance matrix inherits their time basis: annualized vols give an "
                 "annualized matrix, to be passed to portfolioVariance with n_periods_per_year=1\"},"
         "\"correlation\":{\"type\":\"number\",\"minimum\":-1,\"maximum\":1,"
@@ -111,7 +116,7 @@ static const char SCHEMA_COMPOSE_COVARIANCE[] =
             "\"description\":\"Run a Cholesky decomposition and refuse a matrix that is not "
                 "positive definite. Default true. O(n^3); set false only for very large n\"},"
         "\"asset_ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},"
-            "\"description\":\"Optional labels, one per asset, echoed in the response\"},"
+            "\"description\":\"Optional labels, exactly one string per asset, echoed in the response\"},"
         "\"request_id\":{\"type\":\"string\","
             "\"description\":\"Optional identifier echoed in the response for request tracing\"}"
     "},"
@@ -127,11 +132,11 @@ static const char SCHEMA_COVARIANCE_FROM_RETURNS[] =
                 "(or one flat row-major array with n_assets and n_obs given). Log or simple "
                 "returns, not prices and not percentages. A null element means that "
                 "observation is missing. Max 50 assets x 5000 observations\"},"
-        "\"n_assets\":{\"type\":\"integer\","
+        "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":50,"
             "\"description\":\"Number of assets (max 50). Inferred from a nested returns array\"},"
-        "\"n_obs\":{\"type\":\"integer\","
+        "\"n_obs\":{\"type\":\"integer\",\"minimum\":2,\"maximum\":5000,"
             "\"description\":\"Observations per asset (max 5000). Inferred from the first row\"},"
-        "\"n_periods_per_year\":{\"type\":\"number\","
+        "\"n_periods_per_year\":{\"type\":\"number\",\"exclusiveMinimum\":0,\"default\":252,"
             "\"description\":\"Annualization factor: the covariance is multiplied by it and the "
                 "means with it. 252 for daily returns (the default), 12 for monthly, 1 to keep "
                 "the input's own basis. The result is what portfolioVariance wants with "
@@ -140,7 +145,7 @@ static const char SCHEMA_COVARIANCE_FROM_RETURNS[] =
             "\"description\":\"Run a Cholesky decomposition and refuse a rank-deficient sample. "
                 "Default true\"},"
         "\"asset_ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},"
-            "\"description\":\"Optional labels, one per asset, echoed in the response\"},"
+            "\"description\":\"Optional labels, exactly one string per asset, echoed in the response\"},"
         "\"request_id\":{\"type\":\"string\","
             "\"description\":\"Optional identifier echoed in the response for request tracing\"}"
     "},"
@@ -151,25 +156,27 @@ static const char SCHEMA_MONTE_CARLO[] =
     "{"
     "\"type\":\"object\","
     "\"properties\":{"
-        "\"n_simulations\":{\"type\":\"integer\","
+        "\"n_simulations\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":1000000,"
             "\"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\","
+        "\"n_periods\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":100000,"
+            "\"description\":\"Number of time steps per path (e.g. 252 = one trading year). Default: 252. "
+                "n_simulations x n_periods (x n_assets for a book) may not exceed 1e9\"},"
+        "\"initial_value\":{\"type\":\"number\",\"exclusiveMinimum\":0,"
             "\"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\","
+        "\"volatility\":{\"type\":\"number\",\"minimum\":0,"
             "\"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\","
+        "\"n_periods_per_year\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":10000000,"
             "\"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]\"},"
-        "\"model\":{\"type\":\"string\",\"enum\":[\"gbm\",\"merton\"],"
+        "\"percentiles\":{\"type\":\"array\",\"maxItems\":8,"
+            "\"items\":{\"type\":\"number\",\"exclusiveMinimum\":0,\"exclusiveMaximum\":1},"
+            "\"description\":\"Percentile tail levels for VaR, e.g. [0.01, 0.05] for 99% and 95% VaR. "
+                "Max 8 values, each strictly between 0 and 1. Default: [0.01, 0.05]\"},"
+        "\"model\":{\"type\":\"string\",\"enum\":[\"gbm\",\"merton\"],\"default\":\"gbm\","
             "\"description\":\"gbm (default) or merton jump-diffusion\"},"
         "\"jump_intensity\":{\"type\":\"number\",\"description\":\"Merton: jumps per year. Default 1.0\"},"
         "\"jump_mean\":{\"type\":\"number\",\"description\":\"Merton: mean log-jump. Default -0.03\"},"
@@ -182,7 +189,7 @@ static const char SCHEMA_MONTE_CARLO[] =
         "\"weights\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
             "\"description\":\"Correlated book: n_assets weights, each >= 0 (long-only), summing to 1 unless "
                 "normalize_weights=true\"},"
-        "\"n_assets\":{\"type\":\"integer\","
+        "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":100,"
             "\"description\":\"Correlated book: asset count (max 100). Inferred from weights if omitted\"},"
         "\"expected_returns\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
             "\"description\":\"Correlated book: per-asset annualized drifts. Default: expected_return for every asset\"},"
@@ -200,19 +207,21 @@ static const char SCHEMA_SCENARIO_ANALYSIS[] =
     "{"
     "\"type\":\"object\","
     "\"properties\":{"
+        "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":3000,"
+            "\"description\":\"Optional; if given it must equal the length of assets\"},"
         "\"assets\":{"
-            "\"type\":\"array\","
+            "\"type\":\"array\",\"minItems\":1,\"maxItems\":3000,"
             "\"description\":\"Portfolio assets, each with its scenario list\","
             "\"items\":{"
                 "\"type\":\"object\","
                 "\"properties\":{"
                     "\"asset_id\":{\"type\":\"integer\","
-                        "\"description\":\"Unique asset identifier\"},"
-                    "\"current_price\":{\"type\":\"number\","
+                        "\"description\":\"Unique asset identifier. Default: 1-based position in assets\"},"
+                    "\"current_price\":{\"type\":\"number\",\"exclusiveMinimum\":0,"
                         "\"description\":\"Current market price in currency units\"},"
-                    "\"position_size\":{\"type\":\"number\","
+                    "\"position_size\":{\"type\":\"number\",\"exclusiveMinimum\":0,"
                         "\"description\":\"Units held, > 0 (long only). Scales upside/downside to dollar terms and weights the portfolio expected return\"},"
-                    "\"scenarios\":{\"type\":\"array\",\"minItems\":2,"
+                    "\"scenarios\":{\"type\":\"array\",\"minItems\":1,\"maxItems\":10,"
                         "\"description\":\"One entry per scenario. Probabilities must sum to 1.0\","
                         "\"items\":{\"type\":\"object\","
                             "\"properties\":{"
@@ -224,14 +233,12 @@ static const char SCHEMA_SCENARIO_ANALYSIS[] =
                             "\"required\":[\"price\",\"probability\"]"
                         "}}"
                 "},"
-                "\"required\":[\"asset_id\",\"current_price\",\"position_size\",\"scenarios\"]"
+                "\"required\":[\"current_price\",\"position_size\",\"scenarios\"]"
             "}"
         "},"
-        "\"use_hash_lookup\":{\"type\":\"boolean\","
-            "\"description\":\"Benchmark O(1) HashMap lookup against O(n) linear scan. Default: true\"},"
-        "\"prob_tolerance\":{\"type\":\"number\","
+        "\"prob_tolerance\":{\"type\":\"number\",\"exclusiveMinimum\":0,\"maximum\":0.1,\"default\":0.0001,"
             "\"description\":\"Tolerance for probability sum validation per asset. "
-                "Default: 1e-4 (0.01%%). Clamped to [1e-10, 0.1]\"},"
+                "Default: 1e-4 (0.01%). Must be > 0 and <= 0.1\"},"
         "\"request_id\":{\"type\":\"string\"}"
     "},"
     "\"required\":[\"assets\"]"
@@ -280,7 +287,7 @@ static const finbench_mcp_tool_t finbench_mcp_tools[] = {
         "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%%), "
+        "Returns VaR at caller-specified percentiles, CVaR (Expected Shortfall at 95%), "
         "max drawdown, probability of profit, and simulations-per-second throughput. "
         "Two modes: a single asset (volatility) or, when covariance_matrix + weights are supplied, "
         "a correlated buy-and-hold book whose per-step shocks are L*Z with L the Cholesky factor of "