Skip to content

Commit cd5b17b

Browse files
vosesoftclaude
andcommitted
Release v0.3.0-alpha.30: defensive samples validation in bridge
Bug #31 surfaced by the round-3 input-validation probe. The MCP tool layer's Pydantic validation rejects samples<1 cleanly, but direct callers (integration tests, automation scripts, future Python clients) bypassed that. samples=-1 reached the XLL which threw an opaque C++ exception (OLE error 0xe06d7363), surfacing to the user as "Application.Run failed" — useless. Fix: defensive sanity check at the bridge boundary. samples >= 1 is enforced and a 10M soft cap rejects clearly rather than letting MRService.dll thrash on absurd inputs. Round 3 also verified (no fixes needed): - MCP tool envelope shapes are correct per the alpha.17 sweep - All 40 tool descriptions have the ModelRisk: brand prefix - 9 distribution families round-trip through insert_distribution - MCP resources (5 modelrisk:// URIs) registered and readable - 50K-iteration sim completes in 18.4s wall-clock - End-to-end convert workflow on noMR produces real randomness 404 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 11cbace commit cd5b17b

7 files changed

Lines changed: 51 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ All notable changes to ModelRisk MCP. Follows [Keep a Changelog](https://keepach
44

55
## [Unreleased]
66

7+
## [0.3.0-alpha.30] — 2026-05-22
8+
9+
### Fixed
10+
11+
- **Bug #31`samples<=0` passed to `bridge.run_simulation` reached the XLL and triggered an opaque C++ exception** (`OLE error 0xe06d7363`). The MCP tool layer's Pydantic validation enforces `ge=1`, but direct callers (integration tests, automation scripts, future Python clients) bypassed that. Surfaced by the round-3 input-validation probe. Fix: defensive sanity check at the bridge boundary (`samples >= 1` and a 10M soft cap) so every code path produces a clear actionable error before invoking ModelRisk's XLL. The message names the offending value and explains why we're rejecting it.
12+
13+
### Verified end-to-end (no fixes needed)
14+
15+
Round 3 confirmed:
16+
- **MCP tool envelope shapes**: all `list_*` and `find_*` tools return the alpha.17 `{noun: [...], count: N}` envelope correctly. Brand prefix on all 40 tool descriptions.
17+
- **Distribution catalogue breadth**: 9 of 11 sampled families (Lognormal, Uniform, Triangle, Beta, Gamma, Weibull, Poisson, Binomial, Bernoulli) round-trip through `insert_distribution`. The other two failures were test-script errors (`VoseExpon`'s param is `beta` not `mean`; the discrete uniform is `VoseDiscreteU`, not `VoseDiscreteUniform`).
18+
- **MCP resources**: 5 resources registered (`modelrisk://audit-rules`, `/distributions`, `/functions`, `/methodology`, `/workbook/current`) and readable.
19+
- **50K-iteration stress**: simulation completes in 18.4s wall-clock; all 50K samples readable in 0.13s.
20+
- **End-to-end convert workflow** (separate run): non-MR workbook → `discover_inputs``propose_distributions_for_inputs``replace_constant_with_distribution``wrap_with_output``run_simulation``get_simulation_results``build_executive_report`. Produced real randomness (mean $34.9M, stdev $537) on a fully-converted noMR model.
21+
22+
### Tests
23+
24+
404 unit tests pass.
25+
726
## [0.3.0-alpha.29] — 2026-05-22
827

928
### Polished

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "modelrisk-mcp"
3-
version = "0.3.0a29"
3+
version = "0.3.0a30"
44
description = "Open MCP server bridging Anthropic Claude (and any MCP-compatible client) with the ModelRisk Excel add-in."
55
readme = "README.md"
66
requires-python = ">=3.11"

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"name": "io.github.vosesoftware/modelrisk-mcp",
44
"title": "ModelRisk",
55
"description": "Read, build, fit, and run Monte Carlo risk models in Excel through Vose Software's ModelRisk.",
6-
"version": "0.3.0a29",
6+
"version": "0.3.0a30",
77
"packages": [
88
{
99
"registryType": "pypi",
1010
"identifier": "modelrisk-mcp",
11-
"version": "0.3.0a29",
11+
"version": "0.3.0a30",
1212
"transport": {
1313
"type": "stdio"
1414
}

src/modelrisk_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.0a29"
1+
__version__ = "0.3.0a30"

src/modelrisk_mcp/bridge/simulation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ def run_simulation(
165165
The call blocks until the simulation completes — that's how
166166
`VoseStartSimulCustom12` is implemented (synchronous Application.Run).
167167
168+
Bug #31 (alpha.30): defensive sanity check on `samples`.
169+
When a non-MCP caller (a direct script, an integration test,
170+
future Python clients) passes `samples <= 0`, the value used
171+
to flow straight through to the XLL which would throw a
172+
C++ exception (OLE error 0xe06d7363). The user saw
173+
"Application.Run failed" — opaque. The MCP tool layer's
174+
Pydantic field validates `ge=1` but the bridge had no such
175+
guard. Adding it here ensures every caller path produces a
176+
clear message before hitting the XLL.
177+
168178
`output_names`, when supplied, is threaded into the XLL command's
169179
options payload as `[CntNames]:N` + `[name0]:...` etc. The
170180
original C++ header comment claims "empty → all outputs" but
@@ -180,6 +190,22 @@ def run_simulation(
180190
Raises SimulationFailedError if the file doesn't appear after
181191
the save call returns.
182192
"""
193+
if samples < 1:
194+
raise SimulationFailedError(
195+
f"samples must be >= 1; got {samples}. ModelRisk's "
196+
f"XLL command would throw an opaque C++ exception "
197+
f"otherwise."
198+
)
199+
if samples > 10_000_000:
200+
# Soft cap: nothing in the bridge enforces this, but
201+
# production users rarely want more than a few million
202+
# iterations and the .vmrs file gets huge. Reject with
203+
# a clear message rather than letting MRService thrash.
204+
raise SimulationFailedError(
205+
f"samples={samples} exceeds the 10M soft cap. "
206+
f"If you genuinely need more, run multiple sims and "
207+
f"aggregate."
208+
)
183209
wb_info = self._resolve_workbook(workbook_name)
184210
opts = SimulationOptions(
185211
samples=samples,

tests/unit/test_server_boot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
def test_version_is_set() -> None:
8-
assert __version__ == "0.3.0a29"
8+
assert __version__ == "0.3.0a30"
99

1010

1111
def test_server_name() -> None:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)