Changelog
All notable changes to ModelRisk MCP. Follows Keep a Changelog and Semantic Versioning.
[Unreleased]
[0.4.0] — 2026-08-15
Ported to the mcp 2.0 SDK (FastMCP → MCPServer)
- Why now: a field incident. On any machine where another package upgraded the shared
mcpdependency to 2.x (e.g. installing a 2.0-based MCP server into the same Python), modelrisk-mcp 0.3.x died at import —mcp.server.fastmcpno longer exists in 2.0 — and the MCP client reported the server as disconnected. The 0.3.x dependency spec (mcp[cli]>=1.2.0, uncapped) let pip do this silently. - The port:
server.pynow constructsmcp.server.MCPServer(and reports the package version inserverInfo); the HTTP entrypoint adapts to 2.0's renamed transport kwargs (mount_path→streamable_http_path/sse_path) and passes the bind host so 2.0's Host-header validation is configured correctly. The@mcp.tool/@mcp.resource/@mcp.promptsurface is unchanged in 2.0, so all 59 tools, 8 resources, and 5 prompts register exactly as before — verified by the test suite and a live streamable-http smoke test (bearer auth 401/200 paths included). Same migration pattern as modelchoice-mcp 0.0.31. - Dependency spec:
mcp[cli]>=2.0.0,<3— the<3cap is load-bearing; an uncapped range is exactly how 0.3.x broke.httpxis now declared explicitly in the dev extra (2.0 no longer pulls it transitively;starlette.testclientneeds it). - Compatibility: requires an environment with
mcp>=2. If you must stay on mcp 1.x, pinmodelrisk-mcp==0.3.13.
[0.3.13] — 2026-07-20
No-DLL results fallback — full results reading on ModelRisk-only machines
- Every results-reading tool now works without MRService.dll. On machines whose ModelRisk predates the DLL (installers up to 9.1.x),
get_simulation_results,get_samples,get_sensitivity_ranking,get_correlation_matrix,list_vmrs_variables,run_scenarios, and both report builders fall back automatically to reading the live Excel session: a temporary=VoseSimValue(output, ROW())column is written into a spare column past the sheet's used range, bulk-read, and cleared. Live-verified: the extracted samples are sample-for-sample identical to what MRService.dll reads from the.vmrsof the same run (< 1e-12 over the full run), inputs and outputs both readable, 500 samples in ~0.06 s. Fallback results carrysource: "in-session (no MRService.dll; live Excel session)"so callers can tell them from.vmrsreads (source: "vmrs"). - Honest constraints, stated in every error path: the fallback is session-bound (most recent run, in the Excel session that ran it — a closed Excel or a new run replaces the store; the
.vmrsfile stays the durable artifact and needs the DLL to re-open), and it is refused in--read-onlymode because it writes scratch formulas (the DLL path is the write-free reader). - Engine semantics discovered and encoded (the field report's "VoseSimMean says No simulation results" mystery): ModelRisk's
VoseSim*statistic functions are computed once during the end-of-run pass and cached per calling cell — cells written after a run always return "No simulation results", by design, so they can't serve as a post-hoc reader;VoseSimValuereads the sample store directly and works anytime. Two operational landmines found live and engineered around: deleting a worksheet invalidates the in-session store (first silently — every in-range read returns 0.0 — then fully), so the scratch is a cleared column, never a temporary sheet; and the store resolves against the active workbook, so the reader activates the target workbook during extraction and restores focus after. run_simulation's post-run verification no longer fails with "file may be incomplete or corrupt" on no-DLL machines — a successful run is reported as such, with results readable via the fallback.
[0.3.12] — 2026-07-20
- MRService.dll guidance updated: the DLL now ships with ModelRisk. The current ModelRisk release on vosesoftware.com includes MRService.dll, so the "MRService.dll not found" and bundled-key-rejected errors, the README prerequisites/activation/caveats sections, and the installation/glossary docs now lead with the actual fix — update ModelRisk to the latest version — instead of stating the DLL is unavailable (installers up to 9.1.x did not ship it; that historical note is kept so older installs understand what they're seeing). The README's stale suggestion to point
MRSERVICE_DLL_PATHat Tamara's copy is gone — 7.1.x copies are too old and are rejected with the 0.3.11 version diagnosis.
[0.3.11] — 2026-07-20
Field bug report fixes (all five findings addressed — thank you, Peter Vanneck)
- Claude Code registration actually works now.
modelrisk-mcp installwrote the server entry into~/.claude/settings.json— a file Claude Code does not read for MCP servers — while printing a green "added" line: a silent no-op. It now prefersclaude mcp add --scope user(the CLI owns the schema), falling back to a directmcpServersmerge into~/.claude.jsonwhen the CLI is absent; detection keys on~/.claude.json/ theclaudebinary. Verified live end-to-end (claude mcp listshows the server "✓ Connected"). --read-onlyexists and is enforced.Settings.read_onlywas declared but nothing set or checked it. New--read-onlyflag /MODELRISK_MCP_READ_ONLY=1env: every mutating operation — cell/range/array writes, named ranges, clears, simulations, workbook saves (including save-on-close) — raises a clearReadOnlyModeErrorwhile reading and analysis keep working. The natural first-session posture against a live client model.- Multi-instance Excel attach is no longer a coin toss. With several Excel processes running (ModelRisk's shortcut can spawn a second one),
xlwings.apps.activecould attach to an instance without the add-in, making diagnostics report ModelRisk dead while Vose functions worked fine in the user's real session. The bridge now probes each instance with the separator-safeVosePoisson(5)evaluation and prefers one where ModelRisk answers; the add-in-dead error now enumerates running instances (PID, workbooks, live/dead) so a mismatch is visible instead of reading like a locale or licence fault. - MRService.dll too old is diagnosed up front. Required exports are probed at load; a 7.1.x-era DLL (e.g. the copy shipped with Tamara) now fails with "too old — needs ≥ 7.3.2.1 (bundled key covers 7.3.2.1–9.2.2.1)" instead of a cryptic mid-call "function 'MRLIB_SetOfflineActivationKeyEx2' not found". Error messages no longer recommend the Tamara path; they state plainly that ModelRisk installers through 9.1.x do not ship MRService.dll, so
.vmrsreading is unavailable out of the box on ModelRisk-only machines (building and simulating are unaffected). - Claude for Excel instructions corrected — withdrawn, not hedged. Current Claude for Excel builds have no local Connectors panel; they resolve connectors from the claude.ai account, and claude.ai custom connectors are fetched by Anthropic's infrastructure — a loopback URL is unreachable by construction. README + docs/claude-for-excel.md now say this plainly, direct users to Claude Desktop / Claude Code (stdio), document that local stdio servers never appear in the claude.ai "Connectors" panel (absence there is not a failed install), and explicitly warn against public-tunnel workarounds for a server that can write and save workbooks.
[0.3.10] — 2026-07-05
Demo gallery wave 2 — full use-case coverage
Four new demo workbooks close the last coverage gaps (each live-tested end-to-end): 12 fit a time series from history and project with uncertainty (fit_time_series, read_vmrs); 13 compare two suppliers under uncertainty — rank correlation of their histories, then P(A beats B) and stochastic dominance (compare_distributions, compute_correlation_matrix); 14 backtest an overconfident forecast against actuals — PIT and coverage receipts (backtest_output); 15 epistemic vs aleatory — more data or more capital? (decompose_uncertainty). Gallery: 15 demos + index, all recalculating error-free in real Excel.
fit_time_series — the deferred capability, unblocked and shipped
New analysis tool: fit time-series models to a historical range and rank them by AIC/SIC/HQIC — GBM (plus mean-reverting / jump-diffusion / seasonal variants), AR1/AR2, MA1/MA2, ARMA, ARCH/GARCH/EGARCH — then optionally CSE-enter the best model's projection (VoseTime<Family>Fit(...), one period per cell) into a target range, so every simulated path carries the fitted dynamics period to period.
This capability had been deferred since 0.3.2-alpha.6 on a "needs wizard context" diagnosis. A live spike showed that diagnosis was wrong: the real blockers were the phantom OutputSize catalogue parameter and missing CSE array entry — the exact bug class fixed in 0.3.9. With those fixed, =VoseTime<Fam>FitObject(range) returns a valid fitted object in a plain cell, VoseAIC/SIC/HQIC score it, and the *Fit projection array-enters cleanly. Live-verified: on GBM-generated history the tool ranks GBM #1 (ground truth), reports 8 fitted families + 4 honestly skipped, and writes a real 12-period projection. Parameter uncertainty is not supported for time-series fits (the flag mis-binds to the timestamps slot) and the tool says so. Tool count 57.
Engineering note: Common Random Numbers verified across decision-cell mutations
A live spike confirmed that mutating a decision cell's value between two same-seed runs leaves every unchanged input's per-iteration samples byte-identical (the address-seeded RNG keys streams by cell address, and value edits don't move addresses) while outputs respond to the decision. This validates cheap paired-search statistics for the planned simulation-in-the-loop tools (optimizers, chance-constraint solvers). Caveat: structural edits (row/column inserts) move addresses and would break pairing — in-loop tools must only write values/formulas, never restructure.
[0.3.9] — 2026-07-05
Fixes from the demo-gallery live test (10 findings, all addressed)
Every demo scenario was executed end-to-end against real Excel + ModelRisk; the run surfaced 10 product findings, all fixed here and re-verified live (8/8 previously-failing flows now pass):
- Array functions are now CSE-entered (high).
create_copula,create_time_series, andfit_all_data_and_wire's copula U-block are array-entered over the full target range via a new audit-loggedsafe_write_array_formula(Range.FormulaArray). Previously a single-cell write errored ('output array must contain N cells') or — worst case — left U-slots empty so wired marginals silently sampled uncorrelated. Re-verified: wired inputs now show Spearman ≈0.72 from a Clayton fit. - Writes into merged cells now raise (high). COM silently discards
.Formulawrites into a merged region's non-anchor cells — a distribution object 'written' under a merged note row vanished and the downstream aggregate simulated all-zero with no error.write_cell/array writes now detect merged targets and raise an actionable message. - Text cells are no longer treated as live formulas. Instructional text like
wrap as VoseOutput("NPV")registered as a real output (xlwings returns text content via.formula), producing phantom outputs/distributions inplan_risk_model/list_*, duplicate rows in summaries, and audit noise. Centralised the starts-with-=rule across scanners and the audit engine. - Phantom
OutputSizeparameter stripped from the catalogue. An extraction artifact on array functions (VoseTime*,VoseCopulaMulti*Fit, …): the real signatures start at the first true argument (verified live:=VoseTimeGBM(mu,sigma,lastvalue)CSE-entered works; with a size argument it errors).create_time_seriesnow builds correct formulas. - Cross-sheet ranges supported.
fit_all_data_and_wiregainstarget_sheet(data onData, wired model onModel— the realistic layout);fit_tail/fit_and_rank_distributions/fit_copula_to_dataaccept already-qualifiedSheet!rangedata ranges instead of double-prefixing into an opaque COM exception. fit_distribution_to_datagainsas_object— writesVose<Family>FitObject(...)socompute_distributioncan answer analytic questions against the cell (the sampling form remains the default for model inputs). Parameters are keyed off the spec's own names (the Object variants capitaliseDatadifferently).- Executive summary fixes: outputs deduplicated by name (no more triplicated rows), and the P80 column now reports the true P80 —
0.80added to the default percentile set (it silently fell back to P95 before). - Chart names cleaned (
Histogram_Profit,CDF_Profit— no more doubled prefixes) andread_rangeorientation fixed (values and formulas now agree for single-column ranges). - MCP tool
parametersnow also accept positional{"value": …}entries (previously only named entries).
Demo gallery corrections (from the same test)
Demo 02's step 3 now asks for the distribution object form; demo 09 steers helper objects to unmerged cells (Model!B30/B31); demo 10's named range is Revenue2030 (Rev2030 is a valid cell address, which Excel rejects as a name). All 12 workbooks re-verified: zero formula errors in a real-Excel recalculation, formatting and no-path checks clean.
Demo gallery (examples/demos/)
Twelve identically-formatted demo workbooks (index + eleven demos), one per feature area, each a simple real-life use case: store-expansion NPV via build_model_from_brief, bakery demand fitting, correlated construction costs via fit_copula_to_data/fit_all_data_and_wire, data-centre reverse_stress_test, simulation + histogram/CDF/tornado charts, model audit + undo, insurance aggregate loss + tail capital, SaaS pricing run_scenarios, op-risk GPD tails, GBM time series, and a one-prompt board pack. Each workbook's README sheet gives the exact prompt to type to Claude at every step. Shared house formatting (navy bands, blue inputs, yellow Vose-target cells, green key metrics); no live Vose formulas (the agent builds them during the demo); fixed-seed synthetic data; all files recalculate error-free in Excel.
[0.3.8] — 2026-07-05
Four functional tools that widen the "does the work" gap vs. advisory agents
Capabilities that structurally require running the engine and holding the per-iteration joint sample matrix — things an advisory copilot cannot do:
fit_copula_to_data— fit parametric copulas (Normal / T / Clayton / Frank / Gumbel) to a multi-column data range, rank by AIC/SIC/HQIC (Vose<Family>FitObjectscored viaVoseAIC/SIC/HQICon a transient scratch sheet), and report the winner's tail-dependence character (lower = crash-together, upper = boom-together, both = T). Dependence fitted from data, capturing joint-tail risk a single correlation coefficient discards.reverse_stress_test— from a bad output outcome back to the joint input state that causes it: partitions iterations into breach / no-breach (threshold or percentile, above/below), reports each input's mean shift (in its own SDs) inside the breach set and its tail concentration (a breach-driver tornado), and extracts the mean input vector as a concrete named stress scenario. The Solvency II / PRA reverse-stress-test, pure Python over the recorded.vmrssample matrix.fit_all_data_and_wire— fit both the marginals and the copula from a data block and wire the whole correlated model into the workbook (each marginalVose<Family>Fit(range, unc, U)linked to the fitted copula's correlated-U block). One atomic pass with full rollback;dry_run=Truepreviews the exact formulas.build_model_from_brief— turn a deterministic workbook into a simulation-ready model in one atomic, reversible pass: wrap outputs withVoseOutput, replace input cells withVoseInput-wrapped distributions you choose, optionally run a validating simulation, and return the headline percentiles. Every write goes through the audit-logged safe-write path inside a change-set — any mid-build failure rolls back the entire build so the workbook is never left half-converted.
Both orchestrators add a staged change-set/rollback wrapper over safe_write_cell (the repo previously had only per-cell restore). 14 new unit tests (pure breach-partition logic + mocked-bridge wrapper/rollback tests); real-Excel wiring is covered by the gated integration suite. 620 unit tests green; ruff + mypy clean. Tool count 56.
[0.3.7] — 2026-06-18
Distribution charts: create_histogram_chart + create_cdf_chart
Two new tools render one output's simulation result distribution as a native Excel chart on its own sheet — the ModelRisk Results-Viewer view, persisted into the workbook (linked to the spreadsheet, not a screenshot):
create_histogram_chart— frequency columns with the cumulative-probability curve overlaid on a secondary % axis and the central-80% (P10-P90) band highlighted.create_cdf_chart— the ascending cumulative-probability curve on its own ("what's the chance the output is below X").
Both read the per-iteration samples from the active .vmrs (get_samples), bin onto round-number boundaries (_nice_bins), and write a 3-column data table (bin centre / frequency / cumulative %) plus the chart. They reuse the executive report's chart builders (_add_histogram_chart + the bug-#18 bind-with-verification workaround), so a standalone chart looks byte-for-byte identical to the same chart inside build_executive_report. Idempotent — a sheet with the target name (Histogram_<output> / CDF_<output>) is replaced, so re-running after a fresh simulation just refreshes it. The response carries the sheet/chart names plus mean and P10/P50/P90.
[0.3.6] — 2026-06-16
Fix: ModelRisk reported "not loaded" on comma-decimal locales (Russian, German, …)
The add-in liveness probe used Application.Evaluate("VoseNormal(0,1)"). Application.Evaluate parses with the user's locale separators, so where the decimal separator is , the probe read 0,1 as the single number 0.1 — VoseNormal then got one argument instead of two and errored, so the server declared the add-in dead even though Vose functions worked fine when typed in a cell. run_simulation and every action were blocked with a misleading "Vose functions return #NAME?" message. The probe is now the separator-free VosePoisson(5) (one integer arg, no comma → parses identically in every locale), and the "add-in not live" messages no longer claim #NAME? — they tell you that if cell-typed Vose functions work, it's a detection bug to report. Affected anyone running a non-US/locale Excel (often alongside ModelChoice). A regression test asserts the probe stays comma-free.
Beyond the probe, ExcelBridge.evaluate() is now locale-robust for all multi-arg expressions (e.g. compute_distribution, distribution-fit metrics): when Application.Evaluate returns an error it retries through a scratch cell's .Formula, which always uses US conventions (, separator, . decimal) regardless of locale. The common path (valid first result) is unchanged and never touches the workbook.
MRSERVICE_DLL_PATH override for .vmrs reading
New env override MRSERVICE_DLL_PATH (canonical; MRSERVICE_DLL kept as an alias) points the server at a specific MRService.dll. Use it when the DLL isn't in ModelRisk's own folder, or when ModelRisk ships an MRService version the bundled offline key doesn't cover (e.g. ModelRisk's 8.1.4.x vs the key's 7.3.2.1): point it at a version-matched copy you already have — e.g. …\Vose Software\Tamara\MRLibrary\MRService.dll — and the bundled key activates it. Its sibling DLLs resolve from that folder (the loader adds it via add_dll_directory), so you point at the file in place rather than copying it out. Avoids bundling MRService.dll, which isn't standalone (it lives in a ~516 MB / 336-DLL distribution). The "not found" / "key rejected" messages now name this override.
Clearer MRService activation diagnostics
diagnose_workbook now surfaces the specific MRService activation error (e.g. "bundled activation key was rejected — your installed ModelRisk SDK may be too new/old; set MRSERVICE_ACTIVATION_KEY") instead of a generic "not activated" line, and states plainly that this affects only reading saved .vmrs results — running simulations is unaffected.
.mcpb built with the official mcpb CLI
build_mcpb.py now packs with @anthropic-ai/mcpb (validates during pack) instead of a hand-rolled zip, with a structurally-identical plain-zip fallback when node isn't present. README reworked so pip install + modelrisk-mcp install is the recommended path, with a note that the Claude Desktop Extensions installer silently no-ops on the latest Windows MSIX builds (e.g. 1.12603.x) — a client-side bug, not the bundle. Use the auto-wire install until Anthropic patches it.
[0.3.5] — 2026-06-14
One-click install: Claude Desktop Extension (.mcpb)
The release now also builds a .mcpb Desktop Extension and attaches it to the GitHub release. It wraps the standalone Windows .exe (no Python needed) behind a manifest.json, so installing into Claude Desktop is two steps: 1. open modelrisk-mcp.mcpb 2. restart Claude — no pip, no editing claude_desktop_config.json. Built CLI-free in CI (scripts/build_mcpb.py zips manifest.json + server/modelrisk-mcp.exe); the bundle version is injected from the release tag. Excel + a licensed ModelRisk are still required (that's the product, not the connector).
[0.3.4] — 2026-06-14
close_workbook — close an open model
Added close_workbook(workbook_name, save=False): closes an open workbook by file name. By default unsaved changes are discarded (save=False); pass save=True to write them first. Returns the closed name, whether it was saved, and the workbooks still open; raises WorkbookNotFoundError if the named workbook isn't open. The natural counterpart to open_workbook for cleaning up after a scripted session.
open_workbook now also suppresses Excel's interactive open prompts (Update Links, read-only-recommended, file-in-use) — Workbooks.Open is called with update_links=False, ignore_read_only_recommended=True, notify=False, add_to_mru=False and with DisplayAlerts/AskToUpdateLinks toggled off — so a headless open can't hang on a dialog. (External links are therefore not refreshed on open; values stay as last saved.)
[0.3.3] — 2026-06-14
open_workbook — open a model from disk
Added open_workbook(path): opens an .xlsx/.xlsm from disk in the running Excel so the other tools can act on it. Previously the server only attached to already-open workbooks (list_open_workbooks / get_active_workbook) with no way to open a model file itself. Takes an absolute path; if a workbook with the same file name is already open it returns that one (Excel won't open two with the same name); raises WorkbookNotFoundError for a missing file or an Excel open failure. With auto-launch on, a workbook can be opened end-to-end without touching Excel by hand.
[0.3.2] — 2026-06-08
Stable release of the 0.3.2 line (promotion from 0.3.2-alpha.8). Two themes, both validated live end-to-end against a real ModelRisk install.
The server starts ModelRisk itself
If no Excel is running when a tool is called, the server now brings up an attachable Excel via xw.App(add_book=True) and registers the ModelRisk XLL into it — so Vose functions resolve and simulations run without the user opening Excel + ModelRisk by hand first. If Excel is up but the add-in is dead (e.g. "Start with Excel" is off), an activation ladder auto-loads it or returns a clear instruction. Disable with MODELRISK_AUTO_LAUNCH=0. (The earlier modelrisk.exe-launcher approach was found to produce an unattachable start-screen Excel and was replaced; see alpha.1–alpha.3.)
A quantitative analysis & decision layer (40 → 50 tools)
The server could build formulas and read results but was thin on interpreting distributions and results — the part an LLM assistant adds the most value to. Ten new tools close that:
compute_distribution— analytic calculator (pdf / cdf / exceedance / quantile / moments / summary), no simulation.fit_and_rank_distributions— fit many families, rank by AIC / SIC / HQIC.fit_tail— fit a GPD / GEV / extreme-value tail; read its high percentiles analytically.create_aggregate— fast frequency-severity engines (FFT / Panjer) with an analytic-object form, alongside MC.compute_correlation_matrix— rank-order correlation (+ nearest-valid) of a data range, forcreate_copula.get_tail_risk— VaR / CVaR / threshold probabilities from an output's samples.compare_distributions— P(A>B) and first/second-order stochastic dominance between two outputs.backtest_output— PIT calibration, prediction-interval coverage, and bias vs realised actuals.decompose_uncertainty— split variance into epistemic (reducible) vs aleatory (irreducible).plan_risk_model— one-call deterministic→stochastic blueprint (readiness + ranked input candidates + ordered checklist).
The add-uncertainty prompt routes through these. Full per-tool detail is in the alpha.4–alpha.8 entries below.
Known limitation
Time-series fitting (VoseTime*Fit, e.g. AR1/GARCH projection from history) is not exposed: those functions require ModelRisk's time-series-wizard data context rather than a plain formula write, so they don't evaluate through the headless path. Tracked for a future release.
Quality
588 unit tests + a gated live-Excel integration suite for the new tools (7/7 pass against a real add-in); the full simulate→interpret loop validated on a real model. ruff + mypy clean.
[0.3.2-alpha.8] — 2026-06-08
Release-readiness pass for the 10 tools added across alpha.4–alpha.7 — no new tools, just the test + documentation coverage a stable release needs.
Tests
- Gated integration suite for the new tools (
tests/integration/test_analysis.py) — exercises the real ModelRisk evaluation paths (inlineApplication.Evaluate, the scratch-sheet array / fit-object dance, formula building) against a live add-in:compute_distribution(CDF/summary),fit_and_rank_distributions,fit_tail(GPD),compute_correlation_matrix,create_aggregate(FFT object read analytically), andplan_risk_model. All test data is written into a throwaway workbook that is created and closed, so the user's open workbooks are never touched; each test soft-skips if the add-in isn't functional. 7/7 pass live.
Docs
- The 10 new tools are now documented for users: a new "Interrogate distributions and decide between options" section in the user manual (the calculator, tail risk, comparison, backtest, decomposition), plus fit-and-rank /
fit_tailunder fitting,create_aggregate(FFT/Panjer) andcompute_correlation_matrixunder structures, andplan_risk_modelunder the conversion workflow. - New Scenario 7 — choose between two strategies, and check the model is trustworthy (
compare_distributions→decompose_uncertainty→backtest_output). - New glossary entries: VaR/CVaR (updated), aleatory vs epistemic uncertainty, stochastic dominance, PIT / backtest.
[0.3.2-alpha.7] — 2026-06-08
Added
Three workflow/analysis tools (47 → 50) closing the deterministic→stochastic, validation, and uncertainty-decomposition gaps.
-
plan_risk_model— one read-only call that produces the blueprint for turning a deterministic workbook into a risk model: declared outputs, existing distributions, the ranked hard-coded cells that look like uncertain inputs, areadinessverdict (empty/needs-outputs/needs-inputs/ready), and an ordered, state-aware checklist (wrap outputs → fit/propose → correlate → audit → simulate → interpret). Run it first when asked to "add uncertainty" or "make this a risk model." Composesdiscover_inputs+list_outputs+list_distributions. -
backtest_output— validate a simulation output against realised actuals: the Probability Integral Transform (PIT, mean ~0.5 and uniform when calibrated), empirical coverage of central prediction intervals (≈90% of actuals should land in the 90% interval), and bias, with a plain-English verdict. Answers "did the model's predicted distribution actually match what happened?" Pure Python over the output's samples. Verified: a same-distribution actual set reads "well calibrated"; an upper-tail set reads "model runs low — under-forecasting." -
decompose_uncertainty— split an output's variance into epistemic (parameter/knowledge uncertainty — reducible with more data) and aleatory (natural variability — irreducible), via the law of total variance. ModelRisk has no two-dimensional-simulation worksheet function (it's a simulation mode we can't trigger), so this approximates it from two runs you provide: a full run and a run with the epistemic inputs frozen at point estimates. Tells you whether collecting data or hedging variability is the lever. Verified: total 100 = aleatory 36 + epistemic 64 → shares 0.64 / 0.36.
The add-uncertainty prompt already routes through the fit/correlate/tail tools from previous alphas; plan_risk_model is the matching one-call entry point.
Tests
19 new cases — test_tools_analysis_mocked.py (PIT calibration incl. under-forecast detection, exact variance-split arithmetic) and test_tools_workflows_mocked.py (the four plan_risk_model readiness states). 588 unit tests pass; ruff + mypy clean.
[0.3.2-alpha.6] — 2026-06-08
Added
Three more high-value tools (43 → 47 total), filling gaps in dependency modelling, tail fitting, and decision support. All read-only / dry-run-default and verified live.
-
compute_correlation_matrix— the rank-order (Spearman) correlation matrix of a data range viaVoseCorrMatrix, plus its nearest valid (positive-semidefinite) form viaVoseValidCorrmat. Turns historical data into the correlation matrix you feed tocreate_copula, closing the "correlate inputs that move together" gap (previously only copulas could express dependency, with no way to derive the matrix). Runs on a transient scratch sheet (the matrices are CSE array functions); the data is untouched. Verified live: a 3-variable set recovered correlations 0.92 / −0.84 / −0.88. -
fit_tail— fit a Generalised-Pareto (GPD, peaks-over-threshold),GEV, or extreme-value tail to data and read its risk. Writes aVose<Family>FitObject(dry-run previews) and returns the fitted tail's mean and high percentiles (P95–P99.9) computed analytically — tail risk without a simulation. Pairs withget_tail_riskandcompute_distribution. Verified live (GPD): mean 68.4, P99 237.8, P99.9 311.0. -
compare_distributions— head-to-head comparison of two simulation outputs from their per-iteration samples: mean/stdev/percentile deltas, P(A > B), and first- and second-order stochastic dominance (convention: larger is better). The decision-support tool — "is strategy A better than B, and how sure are we?" Pure Python over the samples. Verified: a uniformly-higher output is correctly flagged first-order dominant; an equal-mean lower-variance output is second-order dominant.
The add-uncertainty prompt now routes through these (fit-and-rank / fit_tail for data, compute_correlation_matrix + create_copula for dependency, get_tail_risk + compare_distributions for interpretation). New bridge helpers: a reusable _scratch_sheet context, evaluate_object_metrics, correlation_matrix_of_data; ExcelBridge.get_range_shape.
Deferred
- Time-series fitting (
VoseTime*Fit, e.g. AR1/GARCH projection from history) was investigated and held back: the functions reject the uncertainty argument in every form and the fit object reports "not valid" through the COM/array-formula path, indicating they need ModelRisk's time-series-wizard data context rather than a plain formula write. Not shipped rather than ship something unverified.
Tests
13 new cases in test_tools_analysis_mocked.py (correlation valid/invalid/orientation, GPD fit dry-run/commit/guards, and exact stochastic-dominance arithmetic — first-order, equal-mean second-order, unpaired). 576 unit tests pass; ruff + mypy clean.
[0.3.2-alpha.5] — 2026-06-08
Added
-
create_aggregate— fast frequency-severity aggregation (FFT / Panjer), not just Monte Carlo. The server could only buildVoseAggregateMC(slow, sampling-based); ModelRisk's fast analytic methods — FFT (Fast Fourier Transform) and Panjer (Panjer recursion) — were unreachable, despite being the standard tools for insurance/actuarial aggregate-loss models. The new tool takes amethod('FFT', 'Panjer', or 'MC'), a frequency object cell and a severity object cell, with method-specific options (densityfor FFT;intervals/max_pfor Panjer;min_limit/max_limit/distribution_shiftfor MC).The payoff is
as_object=True(FFT/Panjer only): it writes theVose...Objectform, so the aggregate loss distribution can be read analytically —compute_distribution/get_tail_riskgive its mean, percentiles, VaR and CVaR with no simulation at all. Verified live: a Poisson(5) × Lognormal(1000,400) aggregate via FFT returns mean 5000.0 (exact), P95 9315, P99 11527, and P(loss > 10000) = 3.13%, computed instantly. (De Pril is intentionally not exposed — its probability-array input shape differs from the freq/severity-object pattern.)create_aggregate_mcremains as the dedicated MC-sampling shortcut. Tool count 43 → 44; newbuild_aggregate()builder backs it.
Tests
7 new cases across test_formulas.py (FFT/Panjer/MC formula shapes, the MC-has-no-object guard, unknown-method guard) and test_tools_building_mocked.py (FFT-object and Panjer-sample dry runs). Verified live end-to-end against a running ModelRisk. 563 unit tests pass; ruff + mypy clean.
[0.3.2-alpha.4] — 2026-06-08
Added
Three quantitative-analysis tools (tools/analysis.py) — the server could build formulas and read results but was thin on interpreting distributions and results, the part an LLM assistant adds the most value to. All three are read-only.
-
compute_distribution— an analytic distribution calculator with no simulation. Give afamily+parameters(or point at anobject_cellholding a fitted distribution) and ask forpdf,cdf,exceedance(P(X>x)),quantile(inverse-CDF),mean,stdev,variance,skewness,kurtosis,cov, orsummary(all moments + a P1–P99 percentile ladder in one call). Backed byVoseProb(x, object, cumulative)for density/CDF,VosePercentile(object, u)for quantiles, andVoseMean/VoseVariance/VoseSkewness/VoseKurtosis/VoseCofVfor moments — evaluated inline viaApplication.Evaluate, so it's exact and writes nothing. -
fit_and_rank_distributions— fit many families to a data range and rank them by ModelRisk's own goodness-of-fit scores (AIC, SIC/BIC, HQIC, lower = better), instead of naming a single family and hoping (asfit_distribution_to_datarequires). Families that can't fit are returned underskippedwith a reason. A distribution fit object isn't a plain value —VoseAICneeds it referenced from a cell — so scoring runs on a transient scratch sheet that is always deleted; the data is untouched. -
get_tail_risk— Value-at-Risk and Conditional VaR / expected shortfall (VoseCVARpsemantics) at each confidence level, plus optional threshold probabilities P(X>t) / P(X≤t), computed in pure Python from a simulation output's per-iteration samples.tail='upper'(cost/claims) or'lower'(NPV/profit). VaR is the α-quantile; CVaR is the mean of the worst (1−α) tail.
Tool count 40 → 43. New schemas in schemas/analysis.py; new ModelRiskComputationError; bridge gains evaluate_number() and fit_and_rank().
Tests
test_tools_analysis_mocked.py — 18 cases: metric→expression mapping, summary ladder, object-cell path, fit ranking + criterion-switching + range qualification, and exact VaR/CVaR/threshold arithmetic (e.g. on samples 1…100: VaR₉₅=95.05, CVaR=98.0, P(X>90)=0.10). All three verified live against a running ModelRisk. 556 unit tests pass; ruff + mypy clean.
[0.3.2-alpha.3] — 2026-06-08
Changed
-
Corrected the auto-launch mechanism (supersedes 0.3.2-alpha.2). The α.2 approach — spawning Vose's
modelrisk.exeand waiting for Excel to appear — does not produce an attachable instance.modelrisk.exeis a fire-and-forget stub: it spawnsEXCEL.EXEand exits, and Excel comes up on the start screen with no open workbook. A workbook-less Excel is absent from the COM Running Object Table, soxlwings.apps.activestaysNoneandconnect()timed out (the very "launched but never attaches" failure α.2 set out to fix).connect()now starts Excel viaxw.App(visible=…, add_book=True)— which opens a blank workbook, making the instance immediately COM-attachable — and then runs the 0.3.2-alpha.1 activation ladder to load the add-in: register already-installed ModelRisk XLLs, else locateModelRisk*.xllunderProgram Files\Vose Software\**andRegisterXLLit. Verified live end-to-end on a clean machine: no Excel running →connect()attaches in ~3.6 s → theVoseNormal(0,1)probe returns a number (add-in functional).Auto-launch remains on by default; disable with
MODELRISK_AUTO_LAUNCH=0. -
ExcelBridge.launch_modelrisk()no longer takes atimeout_sargument or polls;_find_modelrisk_launcher()is removed (themodelrisk.exepath is gone).
Tests
test_auto_launch.py rewritten for the xw.App mechanism (starts-attachable-Excel, Excel-fails-to-start, loads-add-in-after-start, connect auto-launch / disabled-raises / skip-when-present, env toggle). 538 unit tests pass; ruff + mypy clean.
[0.3.2-alpha.2] — 2026-05-29
Added
-
The server now starts ModelRisk itself when no Excel is running. Previously the bridge only attached to an already-running Excel and errored otherwise ("No running Excel instance found") — the user had to open Excel + ModelRisk by hand first. Now
connect()launches Vose's ownmodelrisk.exewhen nothing is running, waits for Excel to appear, and attaches.Using Vose's launcher (rather than
xw.App()) is deliberate: it starts Excel with the add-in loaded natively — running its normalxlAutoOpen— so the XLL commands are reachable without the RegisterXLL workaround (it sidesteps bug #29 entirely). Together with the 0.3.2-alpha.1 activation ladder, the full matrix is now covered: nothing running → launch ModelRisk; Excel up but add-in dead → activate it; both fine → just attach.Auto-launch is on by default; disable with
MODELRISK_AUTO_LAUNCH=0. The launcher is found by searching the standardProgram Files\Vose Software\**\modelrisk.exeinstall paths. -
ExcelBridge.launch_modelrisk()and_find_modelrisk_launcher().
Tests
test_auto_launch.py — 8 cases: launcher-not-found, launch-and-attach, timeout, connect auto-launches when no Excel, connect raises when disabled, connect skips launch when Excel already present, and the env toggle. 538 unit tests pass.
Replaces the manual/
computer-useworkaround for getting ModelRisk running — the server does it in-process now.
[0.3.2-alpha.1] — 2026-05-29
Fixed
-
Bug #38 — the server assumed the ModelRisk add-in was already loaded, and failed opaquely when it wasn't. If ModelRisk's "Start with Excel" setting is off (so the add-in is loaded only via its shortcut), or Excel was started programmatically, Vose functions return
#NAME?andrun_simulationfailed with an unhelpful "macro may not be available". Worse,is_modelrisk_loaded()only checked the results-reading DLL (MRService.dll), so the server could report ModelRisk as "loaded" while the in-Excel add-in was dead.Now there's a real liveness check and an auto-activation ladder, run automatically before every simulation:
- Probe —
Application.Evaluate("VoseNormal(0,1)"); a number means the add-in is live,#NAME?means it isn't. This is the missing source of truth. - Re-register installed XLLs (fixes "installed but
xlAutoOpenskipped"). - Enable an add-in that's present but switched off (the "Start with Excel = off" case), then register it.
- Locate
ModelRisk*.xllon disk (standard Vose install dirs) and register it — the "never loaded this session" case. - If still dead, raise a clear, actionable error ("open Excel and click the ModelRisk ribbon tab, or start ModelRisk via its shortcut… enabling 'Start with Excel' loads it automatically") instead of an opaque COM failure.
- Probe —
-
diagnose_workbooknow reports add-in liveness separately from MRService. Newaddin_functionalfield (probe-only, never mutates state) distinguishes "the add-in can build/simulate" from "the results DLL can read .vmrs" — so a confused user gets told which half is broken, with the fix.
Added
ModelRiskBridge.probe_addin_functional(),ensure_modelrisk_functional(),health(), theModelRiskHealthresult, andModelRiskNotFunctionalError.ExcelBridge.evaluate(),register_xll(),register_modelrisk_xlls(),find_modelrisk_xll_paths().
Tests
test_addin_activation.py — 11 cases: probe (live / #NAME? / raises / bool-guard) and the full escalation ladder (already-live short-circuit, recover-by-register, recover-by-enable, recover-from-disk, actionable raise, activate=False). 530 unit tests pass.
Validation note: the ladder logic is fully unit-tested with mocks, but the live escalation paths (installed-but-off, never-loaded-via-shortcut) should be confirmed on a real Excel + ModelRisk in those states.
[0.3.1] — 2026-05-29
First stable release of the 0.3.1 line. Promotion from 0.3.1-alpha.7 with no functional changes beyond the version bump. The line adds three substantial things on top of 0.3.0, all without changing the tool surface:
Designer-quality native charts
Report charts (build_executive_report, build_drivers_report) now follow a complete styling ruleset — round-number axis bins, magnitude-aware labels, central-80% confidence shading on the histogram, decluttered axes, capped cumulative axis, brand typography — while staying native, editable Excel chart objects (no embedded images). Documented in docs/chart-style-guide.md. (α.1)
A risk-analysis knowledge layer
modelrisk://knowledge— a new MCP resource: a curated, attributed distillation of the ModelRisk Help (Vose Software) in ten sections — why to quantify uncertainty, the two forms of uncertainty, selecting a distribution (the five properties), expert opinion, correlation, time series, fitting (MLE + information criteria + the parametric-bootstrap uncertainty parameter), aggregation methods, interpreting sensitivity, and a ~18-family quick-reference. Loaded into the LLM's context by the build/audit prompts. (α.3–α.5)modelrisk://methodologydeepened — each of the 8 principles now carries its reasoning, failure mode, and the audit rule that enforces it, guarded against drift by a test. (α.2)- New human docs:
methodology.md,distribution-selection.md,modeling-patterns.md,knowledge-base.md, plus an annotated bibliography of the standard risk-modelling literature. (α.2, α.6)
Spreadsheet-integrity audit family
audit_model grew from 13 to 17 rules with a new SS-* family — general spreadsheet hygiene, distinct from the VOSE-* Monte-Carlo-methodology rules, drawn from the spreadsheet-control discipline (O'Beirne, Rees, EuSpRIG): SS-001 magic-number-in-formula, SS-002 number-stored-as-text, SS-003 overly-complex-formula, and SS-004 inconsistent-formula-in-block (the classic overtype error, detected with near-zero false positives). (α.6–α.7)
Quality bar at cut
- 519 unit tests pass; ruff + mypy clean
- 17 audit rules across two families, all cross-referenced to the methodology and drift-guarded
- knowledge base ships as a force-included data file (verified in the wheel) and serves as an MCP resource
- 10x faster
iterate_cells/audit_modelcarried over from 0.3.0's perf work
[0.3.1-alpha.7] — 2026-05-29
Added
-
SS-004 inconsistent_formula_in_block (warning) — the single most valuable spreadsheet-integrity check: it catches the classic and most dangerous spreadsheet error (per the EuSpRIG literature) where a row or column of formulas was filled correctly, then one interior cell was overtyped, silently breaking the pattern.
Detection is tuned for near-zero false positives: formulas are normalised to a position-relative form (so a correctly-filled run collapses to one identical string regardless of which cells it references), and a cell is flagged only when it is an interior cell whose pattern differs from both neighbours while those neighbours agree with each other — the unambiguous "odd one out in the middle" signature. Edge cells (legitimate first/last-period differences) are never flagged; heterogeneous rows with no agreeing neighbours are never flagged; relative fills that differ literally but share a pattern are never flagged. Works on both horizontal and vertical runs; skips Vose and errored cells.
audit_modelnow runs 17 rules (13 VOSE methodology + 4 SS spreadsheet-integrity).
Tests
TestInconsistentFormulaInBlock — 6 cases: horizontal overtype, vertical overtype, clean run, heterogeneous row, edge cell, relative-fill-same-pattern. 519 unit tests pass.
[0.3.1-alpha.6] — 2026-05-29
Added
-
A spreadsheet-integrity audit family (SS-001 … SS-003) — a new class of audit rule, distinct from the VOSE-* Monte-Carlo-methodology rules, that checks whether the deterministic scaffolding of the workbook is sound. A model can be methodologically perfect and still wrong if the spreadsheet underneath it is broken. Drawn from the established spreadsheet-error / model-control discipline (O'Beirne, Spreadsheet Check and Control; Rees, Principles of Financial Modelling; the EuSpRIG literature).
- SS-001 magic_number_in_formula (info) — a parameter-like decimal (1.21, 0.85) buried in a formula instead of a labelled input cell. Skips Vose cells (their literals are distribution parameters).
- SS-002 number_stored_as_text (warning) — a numeric value held as text in a cell a formula references; silently dropped by SUM/arithmetic. Only fires when the cell is actually referenced, to keep false positives near zero.
- SS-003 overly_complex_formula (info) — a single non-Vose formula doing too much (many operators / very long); best practice is one calculation step per cell.
audit_modelnow runs 16 rules. The new family is tuned for a low false-positive rate (decimal-only magic numbers, referenced-only text numbers, generous complexity thresholds, Vose cells excluded where their literals are legitimate). -
An annotated bibliography in
docs/methodology.md— the standard references for risk modelling (Vose; Savage's Flaw of Averages; Hubbard's How to Measure Anything; O'Beirne; Rees; Winston; Charnes; Hulett; Grinstead & Snell; Grzelak & Oosterlee), each with a one-line relevance note. External references for further study; no book content reproduced.
Changed
- The
modelrisk://methodologyresource now documents the SS-* family in its appendix. The drift-guard test was generalised to cover any rule-id prefix (PREFIX-###), so every one of the 16 rules must still be cross-referenced.
Tests
TestMagicNumberInFormula, TestNumberStoredAsText, TestOverlyComplexFormula (positive + false-positive-avoidance cases each). 513 unit tests pass.
[0.3.1-alpha.5] — 2026-05-29
Changed
- Added a distribution-family quick-reference (§10) to the
modelrisk://knowledgeresource — a "what it models / when to reach for it / watch-out" table for ~18 of the most-used families (Normal, Lognormal, PERT/ModPERT, Triangle, Uniform, Beta, Gamma, Weibull, Exponential, Pareto, Student-t; Bernoulli, Binomial, Poisson, Negative Binomial, Geometric, Hypergeometric, Discrete). Distilled from the per-family "Uses" sections in the ModelRisk Help's Continuous and Discrete distribution articles. Gives Claude family-level selection guidance at build time to pair with the five-properties framework (§3). Knowledge base is now ten sections; still all from the corpus already on hand, attributed, our own words.
Tests
The knowledge-base resource test asserts the family quick-reference is present. 502 unit tests pass.
[0.3.1-alpha.4] — 2026-05-29
Changed
-
Expanded the
modelrisk://knowledgeresource from six sections to nine, distilling three more ModelRisk Help articles (still attributed, still our own words):- Fitting distributions to data — ModelRisk fits by MLE and ranks competing families by information criteria (SIC / HQIC / AIC); the
uncertaintyflag defaults to FALSE only to match common practice but should be TRUE, generating parameter uncertainty by parametric bootstrapping (which captures parameter correlation and non-normal marginals). This is the concrete mechanism behind methodology principle 2 / audit rule VOSE-003. - Choosing an aggregation method — Monte Carlo (general), Panjer / De Pril (recursive analytic), FFT (fast convolution), and the multivariate variants — which to reach for and when.
- Interpreting sensitivity (tornado types) — conditional mean (default, decision-meaningful), conditional cumulative percentile (tail sensitivity), and rank correlation (crude screening); which to use for which question.
All three drawn from material already in the ModelRisk Help corpus — no new external sources. The build/audit prompts already point Claude at this resource, so the added depth flows into model construction and critique automatically.
- Fitting distributions to data — ModelRisk fits by MLE and ranks competing families by information criteria (SIC / HQIC / AIC); the
Tests
The knowledge-base resource test now asserts all nine topics are present. 502 unit tests pass.
[0.3.1-alpha.3] — 2026-05-29
Added
-
A curated risk-analysis knowledge base, served as the
modelrisk://knowledgeresource. An attributed distillation — in our own words, not verbatim — of foundational guidance from the ModelRisk Help (Vose Software): why to quantify uncertainty (the vaccine/investment insight), the two forms of uncertainty (variability vs risk events), selecting a distribution via the five properties (discrete/continuous, bounded/unbounded, parametric/non-parametric, univariate/multivariate, first/second order), eliciting expert opinion, modeling correlation (rank-order → envelope → lookup → conditional → copulas), and modeling a quantity over time. Each section cites its source article.The new resource is wired into the
/build-risk-modeland/audit-modelprompts so it's loaded into the LLM's context at build/critique time — grounding model construction in the authoritative source, not just general intuition. This is the sixth MCP resource.Single source of truth: the substance ships as the package data file
data/knowledge_base.md(force-included in the wheel), is served by the resource, and is pointed to fromdocs/knowledge-base.mdfor GitHub readers — no duplicated copies to drift.
Tests
test_knowledge_base_resource_returns_cited_markdown (provenance marker + all six topics present) and the registration test now assert modelrisk://knowledge. 502 unit tests pass.
[0.3.1-alpha.2] — 2026-05-29
Changed
-
The
modelrisk://methodologyresource is now a methodology knowledge base, not just a list. Each of the 8 core principles gains a Why, a Failure mode, and an Enforced by line naming the audit rule(s) that police it — so a principle is never just advice, it's tied to a checkaudit_modelruns against the live workbook. The resource is loaded into the LLM's context at/build-risk-modeland/audit-modeltime, so this directly sharpens how Claude builds and critiques models.A closing section maps the remaining rules that are about correctness or distribution selection rather than the core principles (VOSE-001, VOSE-011, VOSE-012, VOSE-013) — so all 13 audit rules are now cross-referenced from the methodology. Nothing in the knowledge base is invented: it's grounded in the existing principles and the existing rule set.
Added
test_methodology_crossref.py— a drift guard: everyVOSE-0NNcited in the methodology must exist inaudit_rules.yaml, and every rule must be referenced back. Renaming, adding, or removing a rule without updating the methodology now fails CI. Knowledge that drifts from code is worse than none; this makes drift unmergeable.
Tests
501 unit tests pass (+4 cross-reference / structure guards).
[0.3.1-alpha.1] — 2026-05-23
Changed
-
Report charts now follow a complete styling ruleset (
docs/chart-style-guide.md), taking the native Excel histogram and tornado from "generic" to "designer-perfect" — while staying native, editable chart objects (no embedded images). The histogram inbuild_executive_reportgains:- Round-number bins (
_nice_bins): bin edges floor/ceil to a1/2/2.5/5 × 10ⁿwidth, so the X axis reads2M 3M 4M…instead of irregular raw bin centres (2,182,219 …). This was the single worst aesthetic problem in the old charts. - Magnitude-aware tick formats (
_axis_scale_format):4M/850K/420depending on scale; thinned labels viaTickLabelSpacing. - Central-80% bar shading: bars inside [P10, P90] solid brand-blue, tails muted — the confidence interval shown directly on the bars, no extra series.
- Decluttered: count-axis labels removed (absolute frequency isn't decision-relevant), gridlines moved to the cumulative-% axis, no tick marks, no chart/plot borders, tight
GapWidth=16. - Secondary axis hard-capped at 100% (
Max=1.0, MajorUnit=0.2). - Brand typography: one font family chart-wide, left-aligned navy semibold title.
The tornado and any chart using the shared
_style_chart_frame/_style_chart_axeshelpers inherit the font, border-removal, tick-mark removal, and left-aligned title automatically. - Round-number bins (
Added
docs/chart-style-guide.md— the 11-rule chart styling standard, documented (not just coded), with the colour palette table and the function map._HistogramBins,_nice_bins,_percentile,_axis_scale_formatinreports.py— the pure, testable core of the binning/scaling logic.
Tests
test_report_binning.py — 18 cases covering round-boundary binning, label spacing, monotone cumulative, degenerate/empty inputs, percentile interpolation, and magnitude-format selection. 490 unit tests pass.
[0.3.0] — 2026-05-23
First stable release of the 0.3 line. Promotion from 0.3.0-alpha.38 with no functional changes — version bump only.
What ships in 0.3.0
A read/build/run MCP server over Vose Software's ModelRisk Excel add-in. The full surface area is callable from Claude Desktop, Claude Code, Claude for Excel, Cursor, Zed, and any MCP-compliant client.
Tools (40 total):
- Reading (12):
list_open_workbooks,get_active_workbook,get_workbook_summary,list_modelrisk_inputs,list_modelrisk_outputs,list_distributions,get_cell,read_range,get_simulation_results,get_correlation_matrix,get_sensitivity_ranking,find_hard_coded_inputs - Building (13):
insert_distribution,wrap_with_input,wrap_with_output,replace_constant_with_distribution,fit_distribution_to_data,create_aggregate_mc,create_copula,create_time_series,create_risk_event,set_named_range,write_formula,propose_distributions_for_inputs,discover_inputs - Simulation (5):
run_simulation,run_scenarios,get_samples,restore_deterministic_state,restore_cell - Workflows / reporting (7):
audit_model,diagnose_workbook,build_drivers_report,build_executive_report,create_tornado_chart,generate_executive_summary,save_workbook_as - VMRS (3):
read_vmrs,set_active_vmrs,list_vmrs_variables
Resources (5 URIs): modelrisk://functions, modelrisk://distributions, modelrisk://methodology, modelrisk://workbook/current, modelrisk://audit-rules.
Prompts (5): build_model, audit_model, interpret_results, add_uncertainty, import_legacy_model.
Audit rules (13): VOSE-001 through VOSE-013. See the 0.3.0-alpha.34/.35 entries for the most recent additions.
Transports: stdio (default for Claude Desktop) and streamable-http / sse with bearer-token auth (for Claude for Excel + remote deployments).
Distribution:
- PyPI:
pip install modelrisk-mcp(via OIDC trusted publishing) - Windows single-file:
modelrisk-mcp.exe(PyInstaller, ~39 MB, attached to the GitHub release) - MCP Registry:
io.github.vosesoftware/modelrisk-mcp - CLI installer:
modelrisk-mcp installconfigures Claude Desktop + Claude Code automatically
Alpha-cycle highlights since 0.2.0-alpha
- MRService.dll integration (α.1–α.7): direct ctypes bridge to ModelRisk's simulation engine — no longer drives Excel's UI for sim execution, ~10× faster and more reliable than COM-driven sims.
- Bridge layer rewrite (α.8–α.20): xlwings + pywin32 with stale-reconnect, OneDrive path-resolution fallback, COM CVErr handling, multi-strategy dispatch.
- Audit rule set (α.4, α.25, α.33–α.35): grew from 8 → 13 rules, with VOSE-012 (errored cells) and VOSE-013 (arg-count mismatch) catching common LLM hallucination classes.
- Reporting (α.13, α.19–α.23): drivers report (tornado + scatter + narrative), executive report (KPI + histogram + chart polish), corporate styling palette.
- Robustness (α.21–α.32): named-range scanner sees cell-reference forms, expression-based VoseInput/VoseOutput names,
SaveCopyAsinstead ofSaveAs,RegisterXLLbefore simulation triggers, post-condition verification on sim completion. - Bug surfacing (α.33–α.36): Excel error cells now distinguished from empty cells in
CellInfo.error, with bulkRange.Value2detection that's robust across Excel versions. - Performance (α.37): 10× speedup on
iterate_cells/audit_model/get_workbook_summaryby caching the sheet name once instead of once-per-cell. - Release pipeline (α.38): hardened GitHub Actions retries on
actions/checkoutandmcp-publisher login/publishagainst transient GitHub auth outages.
Quality bar at cut
- 472 unit tests pass; ruff + mypy clean
- 13 audit rules verified firing end-to-end on a live Excel workbook
- 20 k-cell workbook audits in ~1 second; 100 k-cell extrapolation: ~5 s
- PyPI + MCP Registry: every alpha tag from α.33 through α.38 round-tripped successfully through the publish pipeline (4 of 5 had clean MCP-registry publishes; one hit a transient GitHub auth outage which alpha.38's retry hardening now covers)
- Activation-key obfuscation:
scripts/scan_exe_for_key.pyruns in CI; no plain key in shipped artifacts
[0.3.0-alpha.38] — 2026-05-23
Fixed
-
Bug #37 —
publish-mcp-registryjob has zero retry tolerance. The alpha.37 release ran straight into a transient GitHub Actions auth outage;actions/checkout@v4failed three times in a row inside the action withfatal: could not read Username for 'https://github.com', and the registry update for that tag was lost (PyPI publish succeeded — registry was 1 version behind until manually resynced). The same outage window also fired "Failed to save: Our services aren't available right now" warnings againstactions/upload-artifactand the cache service. Class of failure we'll hit again.Fix to
release.yml'spublish-mcp-registryjob:- Try
actions/checkout@v4up to 3 times across separate step invocations (so each attempt re-issues a fresh GITHUB_TOKEN), with a 30-second sleep before the final attempt. Only the third attempt is fatal. - Wrap
mcp-publisher login github-oidcin a shell retry loop (3 attempts, 20-second sleep between). - Wrap
mcp-publisher publishin a shell retry loop (3 attempts, 30-second sleep between).
Each independent failure mode now needs to lose three coin flips in a row before the job actually fails. Same workflow that produced 4/5 successful registry updates in the past week becomes much more resilient.
- Try
[0.3.0-alpha.37] — 2026-05-22
Fixed
-
Bug #36 —
iterate_cellscalledsh.nameonce per cell. Eachsh.nameaccess on an xlwings sheet wrapper triggers a COM round-trip (ISheet::Get_Name) costing ~150μs. The inner loop'sCellRef(workbook=workbook, sheet=sh.name, cell=ref)made that round-trip for every cell it yielded — so a 10k-cell scan paid 1.5 seconds of pure attribute overhead. Caught bycProfile:sh.nameaccounted for 81% ofiterate_cells' total runtime.Fix: cache
sh.nameonce per sheet before the inner loop. One line. Verified speedups on the 20k-cell live perf probe:Op α.36 α.37 Speedup iterate_cells3.37 s 0.23 s 14.7× get_workbook_summary3.15 s 0.36 s 8.8× find_hard_coded_inputs3.16 s 0.34 s 9.3× run_audit(13 rules)7.35 s 1.04 s 7.1×
Verified
Round-10 perf probe on a 20k-cell synthetic workbook (2 sheets × 200 rows × 50 cols) — every read/audit path now runs in well under 5 seconds. Headroom: a 100k-cell enterprise model would audit in ~5s rather than ~35s.
[0.3.0-alpha.36] — 2026-05-22
Fixed
-
Bug #35 — bulk
Range.TextreturnsNoneon some Excel versions, regressing VOSE-012 on real workbooks. The alpha.33iterate_cells/read_rangebulk error-detection path relied onused_range.api.Textreturning a 2D tuple of cell text. On the dev Excel (Office 365) this property returnsNonefor any multi-cell range — single-cell.Textstill works fine. Result: audit scans sawerror=Noneon every cell of every sheet, and VOSE-012 silently couldn't fire on errored cells even thoughget_cell(which only reads single cells) worked correctly. Round-10 live-workbook probe caught this — the audit found 0 VOSE-012 findings on a workbook with a deliberate=1/0cell.Fix: prefer
Range.Value2for bulk error detection. On a multi-cell range Value2 reliably returns a tuple-of-tuples with the COM CVErr integer code in each errored cell's slot (e.g.-2146826281for#DIV/0!,-2146826259for#NAME?). The mapping is stable across Excel versions because the lower 16 bits are the well-knownxlCVErrorconstants. Text remains as a secondary fallback for any cell Value2 didn't classify. Empirically verified all seven canonical errors round-trip correctly:#DIV/0!,#N/A,#NAME?,#NULL!,#NUM!,#REF!,#VALUE!._detect_excel_error(used byget_cell) now also has the Value2 fallback for defence in depth.
Verified
Round-10 live-workbook audit probe: built one Excel sheet with cells engineered to trigger each of the 13 audit rules, ran audit_model against it, asserted every rule fires at least once. Before alpha.36: 12/13 (VOSE-012 missing). After alpha.36: 13/13 — all rules fire on a real workbook.
Tests
- 12 new cases in
TestCoerceErrorValue(every canonical CVErr code → string, plus plain numbers / floats / strings / None / booleans must NOT be misinterpreted) - 3 new cases in
TestDetectExcelErrorValue2Fallback(Text=None + Value2 code = detected; Text wins when both present; neither = None)
472 unit tests pass.
[0.3.0-alpha.35] — 2026-05-22
Added
-
VOSE-013 audit rule —
arg_count_mismatch(severity:error). Catches the classic LLM hallucination class thatVOSE-001(unknown function) misses: a real Vose function called with the wrong number of arguments. Examples flagged:VosePERT(min, max)(missing mode),VoseLognormal(mean)(missing stdev),VoseTriangle(1, 2)(missing mode),VosePERT(1,2,3,4,5,6,7,8)(too many — beyond the catalogue's max). The cell would#VALUE!or#NUM!at Excel calc time, but the formula is well-formed enough that VOSE-001 stays silent. With VOSE-013 we flag it statically before the sim runs.Rule compares actual arg count against the catalogue's
required(min) andlen(parameters)(max). Skips:- Functions not in the catalogue (VOSE-001's job)
VoseInput/VoseOutputwrappers (VOSE-008's job)VoseChoose,VoseDiscrete,VoseDiscreteUniform— variadic shapes the catalogue can't fully describe.
13 audit rules now ship; all 13 wired up in
RULES_BY_NAME. -
New helper
safety.count_call_args(formula, function_name): returns one count per occurrence offunction_name(...)informula. Walks the raw formula so a single string-literal arg correctly counts as 1 (not 0 — the prototype bug caught by the alpha.35 dev pass). Skips strings, nested calls, and array literals atomically. Underpins VOSE-013 but generally useful for static analysis.
Tests
11 new cases in TestArgCountMismatch (too-few PERT, too-few Normal, too-many, correct arity, optional-trailing-args allowed, wrapper exemption, unknown-function silence, non-Vose silence, nested-call detection, suggested-fix content, one-finding-per-cell) + 16 new cases in TestCountCallArgs (covering string literals with embedded commas + doubled quotes, nested calls, array literals, multi-call formulas, malformed inputs).
[0.3.0-alpha.34] — 2026-05-22
Added
- VOSE-012 audit rule —
cell_evaluates_to_error(severity:error). The natural pairing with bug #34: now thatiterate_cellssurfaces Excel error literals viaCellInfo.error, the audit can flag them. The message is sharper when the errored cell's formula contains a Vose call (e.g.VosePERT(10, #DIV/0!, 30)→ "the distribution call is broken — the simulation will produce error samples from this cell on every iteration") versus a vanilla broken formula ("trace the formula back to find the root cause"). 12 audit rules now ship; all 12 wired up inRULES_BY_NAME.
Tests
5 new cases in TestCellEvaluatesToError: Vose-call diagnostic, generic-formula diagnostic, silent on clean cells, one-finding-per-errored-cell, severity inherits from rule spec.
[0.3.0-alpha.33] — 2026-05-22
Fixed
-
Bug #34 — error cells (
#DIV/0!,#REF!,#NAME?, ...) were indistinguishable from empty cells. When a cell evaluated to an Excel error, xlwings'Range.valuereturnedNone— the same value an empty cell returns.get_celltherefore reportedvalue=null, cell_type="formula"for a broken cell andvalue=null, cell_type="empty"for an empty one, with no way for the LLM to tell them apart. Worse: a Vose call with an errored argument (e.g.VosePERT(10, #DIV/0!, 30)) showed up as a normal formula inread_range, and audit scans missed broken distributions entirely.Fix: detect errors via
Range.Text, which always renders error cells as their literal ("#DIV/0!"etc.).CellInfogains a new optionalerror: str | Nonefield andcell_type="error"is a recognised classification.RangeInfogains a parallelerrors: list[list[str | None]]2D array (empty list when no cells in the range errored, so the common case stays compact).iterate_cells(used byaudit_modelandfind_hard_coded_inputs) does a single bulk Text read per sheet so the per-cell error info costs no extra COM round-trips. All detection paths fail open: if Text isn't available, the read still returns values + formulas as before.
Tests
test_excel_bridge.py gains TestDetectExcelError (12 cases covering Excel error literals, normal cells, edge cases like #hashtag text, COM failure, non-string Text) and TestClassifyCellWithError (error wins over formula classification).
[0.3.0-alpha.32] — 2026-05-22
Fixed
-
Bug #33 — drop the alpha.18
output_namespre-populate. Round-7 testing revealed the alpha.18 hypothesis was wrong. Theoutput_namespayload toVoseStartSimulCustom12is a filter, not an enable list — pass empty and the XLL auto-scans the workbook and registers every VoseOutput; pass a list and only matching outputs get registered. So alpha.18, which pre-populated names from the workbook scanner, was strictly worse for any workbook with expression-named outputs (like Vose's ownInputs Outputs.xlsxsample): the scanner-extracted prefix never matched the runtime-evaluated name, and NOTHING got registered.The alpha.17-era symptom that motivated alpha.18 ("sims completing without registering outputs") was almost certainly bug #29 — XLL commands not callable when Excel was started programmatically — which we fixed properly in alpha.27 via
Application.RegisterXLL. With #29 fixed, the XLL's auto-scan works correctly. Removing alpha.18's pre-populate makes expression-named outputs register too.
Verified
Live test on the Vose Inputs Outputs.xlsx sample: with empty output_names, the .vmrs registered Period 1 (var_id=1) — variables now register where they didn't before. On the NPV workbook: NPV (10%), Market growth, Sales Price, and Conservatives get in? (1=yes) all resolved as before. Both styles of workbook keep working; the expression-named case is now fixed.
Tests
408 unit tests pass. Renamed test_run_simulation_passes_voseoutput_names_to_xll → test_run_simulation_does_not_filter_xll_outputs (sentinel: the bridge MUST NOT pre-filter the XLL).
[0.3.0-alpha.31] — 2026-05-22
Fixed
-
Bug #32 — expression-based VoseInput/VoseOutput names false-positive-failed post-condition verification. Vose's own
Inputs Outputs.xlsxsample declares its output asVoseOutput("Total net revenue from "&B8&" to "&B23,"$k")— the name is an Excel expression, not a static literal. The runtime-evaluated name (e.g."Total net revenue from 2020 to 2027") is only knowable after Excel computes the formula at simulation time. Ourname_parserwas returning the literal prefix as aLiteralName(since it stopped at the closing quote), so:- The bridge's
expected_output_namescontained the partial prefix. run_simulationpassed it to the XLL, which couldn't match it against the actual VoseOutput cell.- Post-condition verification looked it up in the produced .vmrs, didn't find it, and raised
SimulationFailedError— claiming the sim's post-phase had crashed when in fact the only issue was the name-resolution mismatch.
Fix:
- New
ExpressionNametype inname_parser.pyfor first-args that turn out to be expressions (literal followed by&,+, etc., rather than a closing,or)). - The parser detects this by checking what follows the closing quote.
_resolve_vose_namereturns the partial prefix marked with a…ellipsis solist_modelrisk_outputsstill surfaces the cell with an informational name ("Total net revenue from …") instead of dropping it.- Post-condition verification filters out
…-marked names — we can't statically verify them, so we don't try, rather than failing loudly.
- The bridge's
Why this matters
Workbooks that build output names from cell content are a real pattern (year-range labels, scenario-specific outputs, anything dynamic). Before alpha.31 every one of those workbooks looked broken to the bridge. The deeper fix — actually evaluating the Excel expression to get the runtime name and registering THAT with the XLL — is a separate larger investigation; this release is the honesty improvement: don't claim failure when the sim ran fine.
Tests
408 unit tests pass (+4 in test_name_parser.py::TestExpressionForm covering the Vose-sample literal-concat-cellref case, simple "prefix"&A1, two-arg literal-with-units, and whitespace tolerance around the closing quote).
[0.3.0-alpha.30] — 2026-05-22
Fixed
- Bug #31 —
samples<=0passed tobridge.run_simulationreached the XLL and triggered an opaque C++ exception (OLE error 0xe06d7363). The MCP tool layer's Pydantic validation enforcesge=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 >= 1and 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.
Verified end-to-end (no fixes needed)
Round 3 confirmed:
- MCP tool envelope shapes: all
list_*andfind_*tools return the alpha.17{noun: [...], count: N}envelope correctly. Brand prefix on all 40 tool descriptions. - 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 isbetanotmean; the discrete uniform isVoseDiscreteU, notVoseDiscreteUniform). - MCP resources: 5 resources registered (
modelrisk://audit-rules,/distributions,/functions,/methodology,/workbook/current) and readable. - 50K-iteration stress: simulation completes in 18.4s wall-clock; all 50K samples readable in 0.13s.
- 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.
Tests
404 unit tests pass.
[0.3.0-alpha.29] — 2026-05-22
Polished
generate_executive_summarymarkdown formatting. Previously the per-output stats table used.3gformat which switches to scientific notation past1e4— so a mean of $63,300 came out as6.33e+04, unreadable in a corporate context. New_fmt_numhelper uses thousands-separated decimals with two decimal places for normal-range values (63,300.00) and only falls back to.3gfor extreme magnitudes (≥1e9 or <1e−2 in absolute value) where decimal form would be unreadable. Same treatment for the contingency-vs-deterministic delta columns via_fmt_signed, which keeps the explicit +/- sign.
Tests
404 unit tests pass.
[0.3.0-alpha.28] — 2026-05-22
Fixed
- Bug #30 —
diagnose_workbookmixed data sources when called with an explicitworkbook_name. Prior versions always assignedactive_workbook = <Excel-active book's name>andworkbook_path = <active book's path>, regardless of which workbook the caller asked to diagnose. Result: callingdiagnose_workbook("foo.xlsx")whilebar.xlsxwas active in Excel reportedactive_workbook="bar.xlsx"andworkbook_path=<bar's path>alongside foo's input/output counts — misleading. Worse, the downstream.vmrslookup usedworkbook_path(bar's) and would silently find bar's sibling vmrs instead of foo's. Fix: when an explicitworkbook_nameis supplied, look up that book's path fromlist_workbooksand report it inworkbook_path. Theactive_workbookfield still reflects Excel's active book (useful informationally), butworkbook_pathnow consistently describes the workbook being diagnosed.
Tests
404 unit tests pass. Live verification via the round-2 test pass.
[0.3.0-alpha.27] — 2026-05-22
Fixed
- Bug #29 —
run_simulationfailed when Excel was started programmatically (e.g. via xlwings'xw.App()from an automation context, CI, or any service-driven setup). The ModelRisk XLL would show up asInstalled=Truein the AddIns collection, butApplication.Run('VoseStartSimulCustom12', ...)failed withCannot run the macro 'VoseStartSimulCustom12'. Root cause: Excel's normal startup flow runs the XLL'sxlAutoOpenwhich registers each command viaxlfRegister; the programmatic-launch path skips that step. Fix:SimulationControllernow callsApplication.RegisterXLL(path)for every loaded ModelRisk*.xll before its first sim run.RegisterXLLis idempotent (re-runsxlAutoOpen) so safe to call whether or not the XLL is already fully registered. Cached per controller instance so we only run it once per session, not before every sim.
Why this matters
The user-driven Claude Desktop session worked fine all along because Excel's normal startup loaded the XLL properly. But anyone running modelrisk-mcp in a non-interactive context — an automation script, a CI test, an MCP server spawned by a daemon, or the autonomous E2E test harness I just ran — would hit Cannot run the macro and not know why. This unblocks every non-Claude-Desktop usage pattern.
Verified
Live probe before fix: app.api.Run('VoseStartSimulCustom12') → "Cannot run the macro". After app.api.RegisterXLL(path): the same call resolves. Now part of _invoke_start_simulation's preamble.
[0.3.0-alpha.26] — 2026-05-22
Fixed
- Bug #28 —
get_correlation_matrixcrashed when only one variable resolved. For a 1×N input matrix (one variable),numpy.corrcoefreturns a 0-d scalar of value 1.0 (the variable's self-correlation) instead of a 2-d (1, 1) matrix. The downstream_matrix_to_optional_listthen died withTypeError: iteration over a 0-d array. Surfaces in real use whenget_correlation_matrixis called with a single name (or where several names are requested but only one resolves — the failure mode in the autonomous test pass). Fix:_corrcoefnow promotes a 0-d numpy result to a (1, 1) array before returning. The downstream JSON envelope correctly serialises the trivial[[1.0]]matrix.
Tests
404 unit tests pass (+2 in test_mrservice.py::TestCorrcoefHelper covering single-row and multi-row cases — sentinel against the 0-d regression).
[0.3.0-alpha.25] — 2026-05-22
Two bugs surfaced by the autonomous end-to-end test pass against a model with extensive text labels.
Fixed
- Bug #26 — audit's VOSE-002 / VOSE-005 / VOSE-004 / VOSE-010 detectors false-positive on cell-ref-form VoseInput / VoseOutput wrappers. The detectors used a regex (
_VOSE_INPUT_RE) that only matched the string-literal formVoseInput("Name"). Workbooks using the cell-reference formVoseInput(B20)— which is what most real ModelRisk models use — triggered "not wrapped" warnings on every distribution cell. Same root cause as bug #13 (alpha.14), but the audit didn't get the parser-based check at the time. Fix: detectors now useextract_vose_first_arg()(the same function the scanner uses), so cell-ref-form wrappers are recognised consistently. Live test against the NPV workbook: audit findings dropped from 34 (all false-positive VOSE-002/005) to 0 on the same model. - Bug #27 —
_classify_celland downstream tools misclassified text cells as formulas. xlwings'Range.Formulaaccessor returns the cell's text content even for non-formula cells — so a cell holding the label"Total Revenue"came back withformula="Total Revenue", which the prior checkif formula:flagged as a formula. Two consequences:formula_cell_countwas inflated, andfind_hard_coded_inputsreturned[]on any model with text labels (every cell got bucketed as "formula", no numeric inputs were candidates). The "convert this Excel model to ModelRisk" workflow was silently broken on exactly the workbooks where it's most useful. Fix: a cell counts as a formula only when its.Formulastarts with=. Applied at three sites:_classify_cell,ModelRiskBridge.get_workbook_summary,ModelRiskBridge.find_hard_coded_inputs.
Tests
402 unit tests pass. The bug-#26 regression is covered by the live E2E pass (the broken audit was the symptom — same workbook now reports 0 false positives). Bug-#27 likewise: find_hard_coded_inputs against the non-MR workbook now returns candidate numeric cells instead of empty.
[0.3.0-alpha.24] — 2026-05-22
Bug surfaced by the autonomous end-to-end test pass: save_workbook_as was renaming the open workbook in place instead of saving a copy.
Fixed
- Bug #25 —
save_workbook_asrenamed the live workbook. Prior versions usedbook.save(path), which xlwings translates toWorkbook.SaveAs(path).SaveAsdoesn't save a copy — it renames the open workbook to the new path and rebinds it in Excel's books collection. Subsequent tool calls referencing the original workbook name then failed with "Workbook 'X.xlsx' is not open" because Excel only knew the new name. Not the contract callers expect from a "save as" operation in an MCP context where downstream tools chain after a save. - Fix: use
book.api.SaveCopyAs(path)directly via COM. SaveCopyAs writes the file without touching the open workbook's identity — the original stays open under its original name; the saved copy is an independent file on disk. overwrite=Truenow alsounlink()s any pre-existing target first, sinceSaveCopyAsrefuses to overwrite (whereas the oldSaveAshappily clobbered).
Tests
402 unit tests pass (+3 in test_excel_bridge.py::TestSaveWorkbookAsUsesSaveCopyAs): the API actually called is SaveCopyAs (regression sentinel asserting book.save was NOT called), overwrite=True clears the target first, overwrite=False refuses with CellReferenceError.
[0.3.0-alpha.23] — 2026-05-22
Corporate-grade polish pass on the report charts. Live screenshot review showed the data was correct but the visual default-Excel-blue, unformatted axis numbers ("-156508.3276"), single-series legends floating off to the side, and stats-table overlap looked unfinished. This release moves the reports closer to "screenshot-and-paste-into-a-deck" quality.
Polished
- Centralised chart palette. New module-level constants
_COLOR_CHART_PRIMARY(steel blue, matched to the title band),_COLOR_CHART_LINE(burnt orange),_COLOR_BAR_POSITIVE(forest green),_COLOR_BAR_NEGATIVE(brick red), plus axis text + gridline tones. Both report builders now share one identity. - Histogram chart:
- Bars: steel-blue, no outlines (
Format.Line.Visible = False). - Cumulative line: burnt-orange, 2.25pt weight, no markers (clean monotonic curve).
- X-axis: tick labels now thousands-separated via
'#,##0;(#,##0);-'— bin centres render as-156,508instead of-156508.3276. Smaller font, gray colour. - Primary Y-axis: integer counts; subtle gridlines.
- Secondary Y-axis: 0% format on the cumulative line.
- No legend (two-series chart with self-evident roles via title + colour).
- Soft gray border around the chart area.
- Bars: steel-blue, no outlines (
- Tornado chart:
- Bars colour-coded by sign: positive correlations green, negative red — at-a-glance signal of "this driver helps" vs "this driver hurts".
- No legend (single series, colour-coded directly per point).
- X-axis:
0.00format on the correlation values. - No major gridlines on the category axis; subtle ones on the value axis.
- Soft gray border.
- Layout fix:
STATS_TABLE_TOPbumped from 26 → 32. Taller charts (240pt) in alpha.20 had pushed the chart band to end around row 30, which overlapped the stats table. Row 32 leaves a full row of margin between chart bottom and table header.
Tests
399 unit tests pass. 1 stats-table position test updated (B26 → B32).
Why corporate styling, not just default
Decision-maker reports get screenshotted into decks, pasted into emails, printed for board meetings. Default Excel colours and unformatted axis labels read as "generated, not designed" — which lowers trust in the analysis sitting next to them. The cost of styling here is a one-time write of helper functions (_style_chart_axes, _style_chart_frame); the value carries across every report the LLM ever builds.
[0.3.0-alpha.22] — 2026-05-22
Layout consistency fix: alpha.20 polished build_executive_report's layout (narrow gutters at A/M, content in B–L) but I missed applying the same change to build_drivers_report. Live screenshot review caught the asymmetry — exec sheet had the polished gutter pattern, drivers sheet still had labels and tornado flush against column A.
Fixed
- DriversReportBuilder now uses the same B-shifted layout as ExecutiveReportBuilder. Title band B:L (was A:J), KEY FINDINGS at B4 (was A4), bullets in B (was A), tornado chart shifted right by ~16pt to align with column B, driver-ranking table moved from G:J to H:K (sits after the mid-gutter at column G), HOW TO READ THIS CHART and RECOMMENDED ACTIONS narrative sections shifted to B with merge ranges expanded to L. Recommendations rows put labels in B and values in C (was A/B); the value cell merges B-merged-to-L. Now both reports look like siblings.
Tests
399 unit tests pass. 6 drivers-report tests updated to assert against the new column positions (B-shifted findings, H-K driver table, C-shifted recommendations).
[0.3.0-alpha.21] — 2026-05-22
Hotfix for a regression introduced by alpha.16's _ModelRiskReports helper sheet: the second run of build_executive_report (and build_drivers_report) failed with Move method of Worksheet class failed on real Excel.
Fixed
- Bug #24 — adding a sheet after a very-hidden sheet fails. Both report builders anchored the new sheet via
book.sheets.add(name, after=book.sheets[-1]). The trailing sheet became_ModelRiskReports(xlSheetVeryHidden) after the first report build, and Excel refuses to position a new sheet "after" a very-hidden anchor — COM raisesMove method of Worksheet class failed. Manifested only on the secondbuild_*_reportcall within a session. - Fix: new
_last_visible_sheet(book)helper walksbook.sheetsand picks the last sheet whoseVisible = -1(xlSheetVisible). Both report builders + the helper-sheet creator now anchor against that.
Tests
399 unit tests pass. The fake-Excel sheet class doesn't model the Visible attribute precisely, but the production fix is small and the call-site change is purely about which sheet object gets passed to after=. Integration test against real Excel is the regression sentinel.
[0.3.0-alpha.20] — 2026-05-22
Polish pass on build_executive_report after a live screenshot review against the NPV_of_a_capital_investment workbook. Two real issues found: the histogram chart was rendering with completely wrong semantics, and the layout was visually cramped with column A pulling double duty as label-holder and edge.
Fixed
- Histogram chart was inverted (#18b). Prior versions called
SetSourceDataon the full 3-column block[Bin, Count, Cumulative %], which made Excel turn all three columns into data series — visible result was "Bin" values plotted as random-height blue bars (the user's screenshot showed bars at 50k, 100k, 150k etc.), "Count" plotted as a red line, and Cumulative invisible. Fix: bind to ONLY the Count + Cumulative columns, then explicitly assignSeriesCollection(i).XValueson both series to the Bin column. Result: proper frequency-histogram bars with bin centres on the X axis, cumulative-% line overlay on a secondary Y axis. The chart now looks like what the report description says.
Polished
- Column A is now a narrow gutter (width 2). Previously content started flush against the left edge with column A holding both labels AND being the page edge. Now the layout has narrow gutters at A and M, with content in B–L. Same change applied to the title band merge range (B:L instead of A:J), headline numbers (MEAN now at B6 instead of A6), stats table (Output at B26 instead of A26), and callouts (
• ...at B instead of A). - Stats-table CV column no longer overflows. Bumped width to 16 (was 14 implicit) so values like
1.296render in full instead of####. - Alternating row tint on the stats table for readability when there are multiple outputs.
- High-CV cells now bold in addition to coloured, so they survive print-to-PDF where colour fidelity drops.
- Chart sizes bumped to 400×240 (histogram) and 360×240 (tornado) — the original 380×220 / 340×220 felt small relative to the title band. Charts shifted right by ~16pt to align with the new column-B content start.
Why this matters
The report is the primary user-facing deliverable. A broken chart isn't "a bug to fix later" — it's the LLM lying to the user about what got built (chart_count: 2 while the chart was visually wrong). Same goes for the cramped layout: a stakeholder sees the report and forms an opinion about modelrisk-mcp from that single screenshot. Worth getting right.
Tests
399 unit tests pass. 4 existing tests updated to assert against the new column positions (B-shifted).
[0.3.0-alpha.19] — 2026-05-22
Fixes the bug-#23 lookup-after-samples regression discovered while end-to-end testing alpha.18 against a real workbook: get_sensitivity_ranking returned empty on the first call after run_simulation, then worked on the second identical call. The diagnostic trace was unambiguous — the output looked up fine, its samples loaded, then every input lookup against the same handle returned None.
Fixed
- MRLIB_GetModelData poisons subsequent MRLIB_GetModelVarID calls on the same handle. Resolution: every reader that interleaves name lookups with sample fetches now resolves ALL var_ids first, THEN pulls samples. Applies to:
ResultsReader.get_sensitivity_ranking— was failing on the first call after a fresh simulation (output looked up, output samples loaded, all inputs then refused to resolve). Now: output lookup → all input lookups → all sample fetches → ranking.ResultsReader.get_simulation_results— same risk on multi-output calls. Same fix.ResultsReader.get_correlation_matrix— same risk on multi-name correlation requests. Same fix.ResultsReader.get_samples(single name, single fetch — no change needed).
This is a contract finding about MRService.dll: the call sequence within one open handle must be all GetModelVarID calls first, then all GetModelData calls. Inverting them or interleaving is unsafe. Worth flagging upstream to the ModelRisk SDK team — and worth knowing for any future readers that touch the same surface.
Why this matters end-to-end
Without alpha.19, the user's first sensitivity-ranking call after a sim returned silently empty. The LLM would tell them "no drivers detected" — completely wrong on a model that clearly has Spearman correlations up to +0.72. After alpha.19 the first call works correctly. Verified live against the NPV_of_a_capital_investment complete.xlsx workbook: 6 driver entries returned, top driver Market growth (r = +0.72), bottom three in noise territory.
Tests
399 unit tests still pass — the bug only manifests against the real DLL, so the regression test is the integration smoke run.
[0.3.0-alpha.18] — 2026-05-22
Targeted experiment for the empty-.vmrs blocker surfaced by alpha.17's post-condition verification. The bridge correctly detected that VoseStartSimulCustom12 + VoseGetDataSZ12 was producing .vmrs files with zero registered outputs — sim ran, file existed, but no variable metadata. Ribbon-driven simulations on the same workbook worked fine, suggesting the ribbon path threads an option the headless XLL path skipped.
Changed
- Pre-populate
output_namesinto the XLL command payload. PreviouslySimulationOptions.output_namesdefaulted to()based on a C++ header comment that said "empty → all outputs". Real-world testing showed that interpretation was wrong — sims completed but the.vmrsdidn't register any outputs unless the names were enumerated explicitly. alpha.18 changes the bridge to populate this list fromlist_outputs(workbook)before invoking the XLL command, threading the result throughSimulationController.run_simulation(output_names=...)into the payload's[CntNames]:N+[name0]:Profit+ … entries. The ribbon path presumably does this implicitly during its setup phase; we now mimic that explicitly.
If this works
The empty-.vmrs symptom goes away and downstream readers find the outputs. Post-condition verification (added in alpha.17) becomes the test: if it stops firing on workbooks where it fired before, the hypothesis is confirmed.
If it doesn't
The asymmetry is elsewhere (variable-registration timing, session handle threading, save-finalisation phase). Next step would be to compare what the ribbon does on the C++ side that this codepath skips — likely needs a diff against ModelRiskAtl's IModelRiskSimulation::StartSimulation entry vs. the XLL command handler.
Tests
399 unit tests pass (+1: test_run_simulation_passes_voseoutput_names_to_xll confirms the bridge populates the names from the workbook scan).
[0.3.0-alpha.17] — 2026-05-22
Full sweep against the running bug list — the biggest correctness release since the v0.3 pivot. Tackles every still-broken item: the response-envelope cross-cutting fix (#1, #2, validates #15), run_simulation false-positive reliability (#20), and the workbook-recovery tool (#21), plus a CI guard so the envelope category can't regress.
Fixed
- Envelope sweep across every list-returning MCP tool (#1, #2, validates #15). FastMCP serialises a bare
list[T]return as one MCP content block per element — which makes the LLM see N concatenated objects instead of a single array. Symptoms ranged from "list_modelrisk_outputs returns a single record" (#1) to "list_vmrs_variables returns concatenated JSON objects" (#2) to "get_samples wraps each float in a text-block dict" (#15, fixed in alpha.14). Fixed all of them in one pass by wrapping every list-typed response in a dict envelope with a semantic noun key:list_open_workbooks→{workbooks, count},list_modelrisk_inputs→{inputs, count},list_modelrisk_outputs→{outputs, count},list_distributions→{distributions, count},get_simulation_results→{results, count},find_hard_coded_inputs→{candidates, count},list_vmrs_variables→{variables, count},read_vmrs→{results, count},propose_distributions_for_inputs→{proposals, count},discover_inputs→{candidates, count}. New CI guard test (test_no_tool_returns_bare_list) scans every tool module and fails if any uses-> list[, so the next instance of this category gets caught before merge. run_simulationno longer reports false-positive success (#20). Previously the tool returnedsamples: 10000and a valid.vmrspath even when ModelRisk's post-simulation phase crashed silently — leaving the.vmrswithout registered output metadata and every downstream reader unable to find anything. The "samples" number was just echoing the input parameter, not measuring actual completion. Fix: post-condition verification. Before running, the bridge captures the list of expected VoseOutput names. After the simulation returns, it opens the produced.vmrsand confirms at least one expected output resolves to avar_id. If none do, raisesSimulationFailedErrorwith an actionable message ("the simulation's post-sim phase failed to register outputs; runrestore_deterministic_stateto recover").restore_deterministic_staterecovery tool (#21). New MCP tool that recalculates the workbook to clear any VoseOutput cells stuck on per-iteration sample values from a previous run. TriggersApplication.CalculateFullwhich re-evaluates every formula. Wired into the auto-recovery path onrun_simulationpost-condition failure — so the workbook is restored even if the user doesn't call the tool explicitly.- MRService.dll activation error message (#8). The "no key supplied" error now lists both activation flavours (single-int64 via
MRSERVICE_ACTIVATION_KEY, split-int64 viaMRSERVICE_ACTIVATION_KEY1/2), explains whatMRSERVICE_DISABLE_BUNDLED_KEYdoes, and points at the activation docs URL.
Already fixed (acknowledged from the running bug list)
- #4 (
wrap_with_outputrefuses non-Vose formulas) — current code passesallow_overwrite_non_vose=Trueand an existing test covers the Workflow-1-Step-6 pattern. The bug was real in an earlier alpha; the current implementation is correct. - #5 (
save_workbook_astool) — registered as an MCP tool intools/building.pysince alpha.2. - #6 (
set_cell_formula/ guarded write) — exposed aswrite_formulaintools/building.pysince alpha.2. - #7 (
get_active_workbookOneDrive) — fallback path inexcel.py::get_active_workbookalready returns an empty path when xlwings' OneDrive resolution fails. - #12 (unsaved-workbook path) —
_workbook_infodetects path strings missing any separator (the unsaved-workbook signature) and returns empty path.
Obsoleted
- #10 (
use_vba_helper_for_simulationhangs) and #11 (ensure_modelrisk_activeoverfit) — both refer to code that was removed in the v0.3 MRService.dll pivot. The new architecture doesn't have a VBA helper or an add-in-activation gate; simulations run directly viaApplication.Runon the XLL command surface and.vmrsreads go through MRService.
New tools
restore_deterministic_state(workbook_name?)— workbook recovery from the frozen-sample state.
Tests
398 unit tests pass (+5: post-condition happy path, post-condition fails when no output registered, auto-restore fires on post-condition failure, restore tool with explicit workbook, restore tool defaults to active). Plus the new envelope CI guard.
[0.3.0-alpha.16] — 2026-05-22
Two paired bugs in the report builders, both surfaced by the same end-user testing session — charts came out blank, and the staging data leaked onto the user-visible report sheet. Both build_executive_report and build_drivers_report are affected because they share the same chart-construction helpers.
Fixed
- Charts no longer render blank (bug #18). Previously the flow was: create chart, call
chart.set_source_data(range)(xlwings wrapper), then configure type / title / colours. On real Excel this looked correct in the COM trace butSeriesCollection(1).Formulacame back empty — the bind silently dropped, Excel auto-filled a placeholder series during chart creation, and the chart went blank once that placeholder cleared. Fix: bind via the COMchart_api.SetSourceData(Source=range.api, PlotBy=2)call directly (skipping the xlwings wrapper that was where the regression lived), then probeSeriesCollection(1).Formulato verify the binding actually stuck. If the probe comes back empty the chart counts as failed (chart_countdecrements) so the LLM doesn't mislead the user about how much of the report rendered. - Staging data no longer leaks onto the visible report sheet (bug #19). Previously the histogram / tornado source ranges were written to columns M:Q of the report sheet itself and then hidden via
EntireColumn.Hidden. Cosmetic until the user scrolled or printed, then it became visible noise. Fix: all staging data now goes on a workbook-scoped helper sheet_ModelRiskReportsmarkedxlSheetVeryHidden(unreachable from the right-click "Unhide" menu). Block ownership: executive report uses columns A:C (histogram) and E:F (tornado); drivers report uses I:J (tornado). The two reports can coexist in one workbook without stomping. Each builder clears its own block before re-writing so re-running a report doesn't blend new + stale data.
Tests
5 new tests in test_reports.py: no staging-data leak on the visible sheet, helper sheet created with the right headers, helper sheet has Visible = xlSheetVeryHidden, drivers + executive use distinct helper blocks, chart binding produces a non-empty SeriesCollection(1).Formula (with a negative test that proves an empty formula causes chart_count to decrement).
Why this matters
These are the two bugs that made build_executive_report look broken to end users — "you said you built 2 charts but I see blank squares, and there's some weird data in column M". After alpha.16 the charts render, the report sheet has nothing on it but the intended content, and a binding regression on real Excel won't be silent — the chart count drops and the LLM can flag it.
[0.3.0-alpha.15] — 2026-05-22
Fixes the second bug from yesterday's Claude Desktop testing session: get_samples (and every other read tool that resolves a variable name) no longer hangs forever when the workbook contains a VoseInput / VoseOutput name with characters that confuse MRService.dll's name lookup.
Fixed
get_sampleshang on names containing?,(, or)(bug #16).MRLIB_GetModelVarIDhas been observed to spin indefinitely on names with those characters — looks like a wildcard/glob matcher that misinterprets them. Without a timeout, Claude Desktop's 4-minute hard limit was the only thing that stopped the request, and the user never got a useful error.- New
_call_with_timeouthelper inbridge/mrservice.pyruns an individual ctypes call in a daemon thread with a wall-clock deadline. On expiry it raisesSimulationFailedErrorwith an actionable message that names the likely cause (?/(/)in the variable name) and the workaround (rename the input/output in the workbook). - New
VmrsHandle.lookup_var_id(name, *, timeout=None)method moves the name-resolution logic fromResultsReader._lookup_var_idonto the handle where it belongs, and applies the timeout wrapper. Default budget is 8 seconds; overridable via theMRSERVICE_VARID_TIMEOUT_Senvironment variable for environments where the SDK is unusually slow. ResultsReader._lookup_var_idnow delegates tohandle.lookup_var_id. Every read tool that touches a variable by name benefits:get_samples,get_simulation_results,get_correlation_matrix,get_sensitivity_ranking,list_vmrs_variables,read_vmrs,build_drivers_report,build_executive_report.
- New
Why this matters
A workbook with a name like "Conservatives get in? (1=yes)" used to lock up every read tool against it. Post-fix, the user gets a clear error in ~8 seconds telling them which variable is the problem and what to do about it.
5 new tests cover the timeout wrapper itself plus the lookup-via-handle success / not-found / timeout / env-override paths.
[0.3.0-alpha.14] — 2026-05-22
Two real bridge bugs found in a Claude Desktop end-user testing session. The first one is the critical fix — it unblocks roughly half of the read-side tool surface for workbooks that use the cell-reference name form (the most common pattern in production ModelRisk models).
Fixed
- CRITICAL: workbook scanner now recognises
VoseInput(Cell!Ref)andVoseOutput(Cell!Ref)name forms. Previous versions only matched the string-literal formVoseInput("WidgetCost"). But ModelRisk documents and most real workbooks use the cell-reference form — labels live in column headers andVoseInput(A5)/VoseInput(Sheet1!A5)pulls the name from there. The scanner missed every one of these, solist_modelrisk_inputs,list_modelrisk_outputs,list_distributions, andget_workbook_summaryreturned empty lists on these workbooks. That empty list then cascaded:get_sensitivity_ranking,build_drivers_report,diagnose_workbook,audit_model— all empty too.- New
bridge/name_parser.pyexposesextract_vose_first_arg(formula, wrapper)which classifies the first argument asLiteralName(string literal) orCellRefName(sheet, cell)(cell reference, with optional sheet qualifier). Supports same-sheet refs (A5), absolute refs ($A$5), sheet-qualified (Sheet1!A5), and quoted-sheet ('Sheet with spaces'!B12). - New
_resolve_vose_namehelper onModelRiskBridgeresolves aCellRefNameto its actual name by reading the target cell viaExcelBridge.get_cell. - Wired through all four affected methods. 18 new tests in
test_name_parser.pycover literal/cell-ref/unrecognised forms plus exact-wrapper-name matching. - Regression sentinel:
AB123no longer parses assheet="A", col="B", row="123"— the sheet-prefix branch of the regex now requires the!separator, so multi-letter columns are unambiguous.
- New
get_samplesno longer returns each sample wrapped in an MCP content-block dict. FastMCP serialises barelist[T]returns by expanding each element into its own{"type":"text","text":"<value>"}content block, which made the response unusable to LLMs (they saw N opaque text blobs instead of one array of floats). Return type changed to a single dict envelope{"output_name": ..., "sample_count": N, "samples": [...]}so FastMCP sees one structured payload and serialises it once.
Where it slots in
These two fixes together restore most of the read-side tool surface against real workbooks. Before alpha.14, a user with a typical cell-ref-named ModelRisk model would see "no inputs found" everywhere; after alpha.14 the scanner finds them, the sensitivity tools can rank them, and the report builders can describe them.
[0.3.0-alpha.13] — 2026-05-22
A single-sheet uncertainty-drivers report — narrower than the executive dashboard, but with auto-generated narrative that explains what the tornado chart actually means.
Added
build_drivers_report(output_name, title?, subtitle?, sheet_name?, workbook_name?)— new MCP tool. Drops a single sheet onto the workbook with:- Title band — "Uncertainty Drivers — <Output>" + run metadata.
- KEY FINDINGS (3-5 auto-generated plain-English bullets) — names the dominant driver with direction language ("higher widget cost lowers NPV"), quantifies top-N variance share (rough Spearman r² approximation), flags concentration ("Risk is concentrated — most uncertainty from a small number of inputs") vs diffuse profiles, lists negligible inputs the decision-maker can safely deprioritise.
- Tornado chart — full-prominence, sorted, axis-inverted (largest driver at top).
- Driver ranking table — every input with correlation, |r|, approximate variance share. |r| cell coloured by strength tier (dark-red strong, orange medium, gray weak).
- HOW TO READ THIS CHART panel — three short paragraphs explaining Spearman correlation, what bar magnitudes mean, and how to interpret variance share. Written for stakeholders who don't know what r means.
- RECOMMENDED ACTIONS — three tiers: focus mitigation (|r| ≥ 0.4), monitor (0.2 ≤ |r| < 0.4), deprioritise (|r| < 0.2). Each tier lists the inputs that landed in it.
bridge/reports.py::DriversReportBuilder— the new report builder. Shares the title-band styling + helper functions withExecutiveReportBuilder(both live in the same module).- Narrative helpers:
_strength_label,_concentration_label,_variance_share,_compose_findings,_compose_recommendations,_drivers_headline. The narrative is deterministic from the data — same input always produces the same wording.
Where it slots in
The executive report is the broader dashboard ("here's everything about the simulation"). The drivers report is the focused deliverable ("here's what matters and what to do about it"). Use case: a decision-maker asks "what should I worry about?" — build_drivers_report gives them a one-page answer naming the input, the magnitude, the direction, and the recommended action tier.
Tests
8 new tests in test_reports.py::TestDriversReportBuilder covering title placement, findings name+direction generation, top-N variance share text, driver-table population, recommendations-tier assignment, empty-sensitivity edge case, concentration label classification, sheet replacement.
Tool count grows to 39.
[0.3.0-alpha.12] — 2026-05-22
Adds the headline feature for end-user testing this week: a single-sheet executive-report builder that the LLM can produce in response to "create a report for a decision-maker."
Added
build_executive_report(primary_output, title?, subtitle?, secondary_outputs?, contingency_percentile=0.90, top_drivers=5, sheet_name="Executive_Report", workbook_name?)— new MCP tool that drops a one-sheet decision-maker dashboard onto the workbook. Idempotent; re-running replaces the sheet. The layout:- Title band (rows 1-2) — large, on a deep-navy background.
- Headline numbers (rows 5-6) — mean / P5 / P50 / P90 (configurable) / stdev, big bold colored fonts. P5 in green (downside), P-high in red (upside risk), stdev color shifts amber/red as CV (coefficient of variation) rises.
- Charts band (~rows 9-24) — side-by-side: histogram + cumulative overlay of the primary output's samples on the left, tornado mini of the top N sensitivity drivers on the right.
- Statistics table (rows 26+) — full mean/stdev/P5/P50/P95/CV per output (primary first, then secondary). CV values colored by volatility tier.
- Risk callouts (below stats) — auto-generated plain-English sentences for the decision-maker: "90% confident X lands between [A, B]", "Tail risk: PN is Y% above mean", "Primary driver: (r = ±0.65) — focus mitigation here". Volatility callout fires only above the CV thresholds.
bridge/reports.py::ExecutiveReportBuilder— the report orchestrator. Layout constants are class attributes so a redesign is one edit. All formatting wrapped in best-effort try/except so a COM hiccup on a single colour set doesn't tank the report.- New chart variant: histogram + cumulative-overlay on a single chart object (column chart for counts, line on a secondary axis for cumulative %). Reuses the
TornadoChartWriterpattern.
Tests
348 unit tests pass (was 335): +13 new tests for the report builder covering title/subtitle placement, headline cells, secondary output rows in the stats table, callout generation from data, sheet-replacement idempotence, empty-samples / empty-sensitivity edge cases, high-CV volatility callouts, headline-summary string format, the default_subtitle helper, and MCP-tool passthrough.
Tool count grows to 38.
[0.3.0-alpha.11] — 2026-05-21
Five real bugs found by a real end-user testing session. All shipped as fixes; one is a critical correctness bug that silently broke every list-scan against real Excel (unit tests passed because the fakes returned lists where real xlwings returns tuples).
Fixed
- CRITICAL: list-scan collapse against real Excel.
ExcelBridge._as_2donly acceptedlistfor value normalisation, but xlwings on Windows returnsRange.formulaas a tuple of tuples (raw COM SAFEARRAY) — only the.valueaccessor wraps in lists. When a workbook had two or more cells, the formula payload arrived as a tuple,_as_2dtreated it as a scalar, and the whole row's formulas were string-cast into one fake "cell". The regex_VOSE_INPUT_REfound the first match in that string and we yielded exactly one record instead of many — silently losing all but one input acrosslist_modelrisk_inputs,list_modelrisk_outputs,get_workbook_summary,find_hard_coded_inputs,audit_model, and most importantlyrun_simulation's input registration. Unit tests didn't catch this because the fake Excel returned lists. Fix:_as_2daccepts bothlistandtupleat every nesting level and normalises to lists. Pinned by 6 new tests intest_excel_bridge.py::TestAs2dTupleHandlingincluding a regression test that mirrors the exact production failure mode. workbook_pathreturns""for unsaved workbooks instead of the bare name.Workbook.FullNamereturns just the workbook's name ("Book3") when the workbook has never been saved. Previously we propagated that string as thepathfield, which misled downstream code that treated it as a filesystem location (a.vmrssave targeting<path>/<book>.vmrswould resolve to a relative path and land in the user's cwd). Now we detect missing path separators and report empty path explicitly.run_simulationsilently droppediterationsparameter. Callers using the natural English term ("run 5000 iterations") rather than ModelRisk's UI term (samples) saw their argument silently ignored — the default 1000 ran instead. Nowiterationsis an explicit parameter alias forsamples; both work. Loudly raisesValueErrorif both are passed with conflicting values (silent drops are exactly the class of bug we're fixing here).RunSimulationResultno longer duplicatessamplesasiterationsin its response shape. Previously both fields appeared with identical values — confusing, and an attractive nuisance for callers passingiterationsas input (which got silently dropped). The canonical name is nowsamplesfor both input and output.
Notes on adjacent reports
Several other issues raised in the same session were actually already fixed in earlier alphas:
wrap_with_outputrefusing to wrap non-Vose formulas: fixed in alpha.10.- No
save_workbook_as/ no generic write tool: both shipped in alpha.10 assave_workbook_asandwrite_formula. - OneDrive
get_active_workbookhard-fail: fixed in alpha.4. - MRService.dll activation error message clarity: moot since alpha.5 ships a bundled activation key.
Two complaints referenced tools that no longer exist in the v0.3 architecture:
use_vba_helper_for_simulation: deleted in v0.3.0-alpha.1 when we pivoted from the ATL VBA-helper approach to the XLL command surface.ensure_modelrisk_activewith its bitness-mismatch hypothesis: deleted in v0.3.0-alpha.1 along with the rest of the COM-Dispatch diagnostic apparatus.
If a fresh Claude session is still seeing these, it's pulling from training-data documentation of the older architecture, not from the live tool list. The MCP server's actual tools/list returns 37 tools, none of which match those names.
Tests
333 unit tests pass (was 328): +6 covering the tuple-vs-list path through _as_2d, plus 1 for the unsaved-workbook path-degradation case.
[0.3.0-alpha.10] — 2026-05-21
Two new building tools that fill the gap surfaced when Claude tried to build a Monte Carlo model from scratch end-to-end. Previously: no way to write a non-Vose formula (=A1*B1, =SUM(...), =IF(...)) and no way to save the workbook to a path. The "build a tiny test model" prompt couldn't be completed without manual user steps.
Added
write_formula(workbook, sheet, cell, formula, allow_overwrite=False, dry_run=True)— single MCP tool for writing arbitrary formulas / literal values into a cell. Use for wiring inputs into outputs (=A1*B1), aggregations (=SUM(B1:B10)), conditional logic, or anything else not covered by the Vose-specific building tools. Safety: empty cells write freely; non-empty cells requireallow_overwrite=True(protects both user-written formulas and prior Vose distributions). Defaults todry_run=Truelike every other building tool. Adds a leading=automatically for formula-shaped input; numeric literals pass through unchanged.save_workbook_as(workbook, path, overwrite=False)— explicit-path save. Distinct from the user's Ctrl+S — the server still never callsWorkbook.Save()implicitly. Only fires when the caller named a target file. Refuses to overwrite an existing file unlessoverwrite=True. Validates the target has an Excel extension. Returns the resolved absolute path that was written.
Why this matters
Without these, the "build a model from scratch" workflow had a dead-end: Claude could generate distributions and wrap inputs/outputs around existing content, but couldn't put the existing content there to wrap. And it couldn't save the result so a future session could read the .vmrs. End users hit this on the very first "build me a test model" prompt — Claude correctly flagged the missing surface upfront rather than producing a partial result.
Bridge changes
ExcelBridge.save_workbook_as(workbook, path, overwrite=False) -> str— thin wrapper over xlwings'book.save(path)with the file-existence + extension safety checks. RaisesCellReferenceErroron refused overwrites or bad paths.
Tests
Brings total to 327 unit tests (was 320 in alpha.9). +7 new building-tool tests covering:
- dry-run-by-default
- commit to empty cell
- leading
=auto-prepend on formula-shaped input vs. numeric-literal pass-through - refuse-to-overwrite-non-empty (default safety)
allow_overwrite=Truelets caller clobber- the typical "wire-then-wrap" workflow (
write_formula→wrap_with_output) save_workbook_aspassthrough + overwrite flag wiring
Tool count grows to 37 (was 35).
[0.3.0-alpha.9] — 2026-05-21
End-user install friction drops sharply: a single modelrisk-mcp install command now wires the server into every detected MCP client config, with backups and per-client dry-run friendly output. README also documents the zero-install uvx route for users who already have uv set up.
Added
- New CLI subcommand:
modelrisk-mcp install. Detects Claude Desktop (%APPDATA%\Claude\claude_desktop_config.json) and Claude Code (~/.claude/settings.json), backs up each existing config with a timestamped.bak.suffix, merges in themodelriskserver entry without clobbering other servers, and reports per-client status. Uses the absolute path to the installedmodelrisk-mcpexe so the registration works even whenScripts/isn't on PATH for Claude's spawned subprocess. Flags:--name(custom server key for side-by-side dev/prod installs),--force(overwrite an existing entry under the same name). - Reverse subcommand:
modelrisk-mcp uninstall. Removes the entry idempotently — reportsskippedif it isn't there to begin with. - New module
src/modelrisk_mcp/install.pyholds the config-mangling logic;__main__.pyprovides the argparse glue. The legacy "no subcommand → run the server" behaviour is preserved (existingclaude_desktop_config.jsonentries like"command": "modelrisk-mcp"keep working unchanged). - README "Wire into Claude Desktop" section rewritten to present three options ordered by friction:
modelrisk-mcp install(one command),uvx modelrisk-mcp(zero install if you haveuv), and the hand-edit JSON snippet (last resort).
Tests
320 unit tests pass (was 303): +17 for the install module covering create-on-missing, merge-into-existing, custom server names, force-overwrite, idempotent uninstall, malformed-JSON refusal, the CLI dispatch back to the install entry point, and the backward-compat "no subcommand defaults to serve" path.
[0.3.0-alpha.8] — 2026-05-21
End-to-end integration test passed live for the first time, surfacing one operational caveat now documented.
Verified
- All 7 integration tests in
tests/integration/test_e2e_run_simulation.pypass against a real Excel + ModelRisk XLL + MRService.dll round-trip (18.85s total). The empirical moments ofY = 2 * N(0, 1)over 1000 iterations match the analytic moments inside the documented tolerance bands. All v0.3 read-path tools (list_vmrs_variables,get_samples,diagnose_workbook) work end-to-end.
Documented
- Launch order caveat in the README's "Known caveats" section. Excel must be running interactively (Start menu / taskbar) before the MCP server tries to drive
run_simulation. When Excel is launched programmatically by an automation client, ModelRisk's XLL skips part of itsxlAutoOpeninitialisation — XLL functions still register as worksheet UDFs (so cell formulas work), but XLL commands (VoseStartSimulCustom12etc.) never get added to Excel'sApplication.Runtable, and the simulation pipeline depends on those commands. This is a ModelRisk XLL behaviour, not a bug in this server, but the launch-order requirement is now explicit. The integration test discovered this; the earlier real-workbook test by hand worked because Excel had been open interactively.
[0.3.0-alpha.7] — 2026-05-21
Hotfix for the MCP Registry publish step that failed in 0.3.0-alpha.6.
Fixed
server.json::descriptionnow fits the MCP Registry's 100-character limit. 0.3.0-alpha.6 shipped to PyPI cleanly but the registry validator rejected the entry withexpected length <= 100(the example in the SDK docs happened to be 67 chars so the limit wasn't visible). Trimmed to 94 chars.- Adds the v0.3 integration test scaffold (
tests/integration/test_e2e_run_simulation.py) that landed between the alpha.5 and alpha.6 tags — covered in the changelog now that it's published.
Added (previously merged on main, just not part of an earlier tag)
tests/integration/test_e2e_run_simulation.py— 7 gated tests that exercise the real Excel + ModelRisk XLL + MRService.dll round-trip via a programmatically-created 2-cell workbook. Asserts the empirical moments ofY = 2 * N(0, 1)match the analytic ones and that all the v0.3 read-path tools (list_vmrs_variables,get_samples,diagnose_workbook) work end-to-end.
[0.3.0-alpha.6] — 2026-05-21
Registers the server with the official MCP Registry so users can discover it through the canonical channel (and aggregator clients can index it).
Added
server.jsonat repo root — MCP Registry metadata. Server name:io.github.vosesoftware/modelrisk-mcp. Declares the PyPI package as the canonical artifact fortransport: stdioclients.<!-- mcp-name: io.github.vosesoftware/modelrisk-mcp -->marker in the README. The MCP Registry verifies ownership by fetching the PyPI package's README (which is thelong_descriptionbaked into the wheel) and looking for this string. It's a Markdown comment so it stays invisible in rendered views.- New release-pipeline job
publish-mcp-registry. Runs afterpublish-pypi(so PyPI has the new README with the marker before the registry verifier looks for it), waits 60s for PyPI's CDN, then authenticates via GitHub OIDC and runsmcp-publisher publish. No tokens to manage — same trust model as PyPI trusted publishing.
Changed
docs/community-submission.mdrewritten. The old draft targeted themodelcontextprotocol/serversREADME's "Community Servers" section, which has been retired (upstream now redirects all server submissions to the MCP Registry). New doc walks through the actual automated flow, ownership verification, and emergency-manual-publish procedure.
[0.3.0-alpha.5] — 2026-05-21
First publish-ready release. Adds the tornado chart writer, fixes a real discover_inputs scoring bug, closes the security verification loop on the obfuscated activation key, and refreshes internal docs to match v0.3 architecture.
Added
create_tornado_chart(output_name, workbook_name?, sheet_name?)— renders a SensitivityRanking as a native Excel BarClustered chart on a new sheet (Tornado_<output_name>by default, truncated to Excel's 31-char limit). Sheet has a sorted data table (Input | Spearman correlation | |corr| sort key | Regression coefficient) plus the chart with inverted category axis so the largest-magnitude input is at the top — the tornado convention. Idempotent: existing sheets with the target name are replaced, so it's safe to re-run after each new simulation.bridge/charts.py::TornadoChartWriter— first member of the chart-writer family. Future siblings: RiskProfileChartWriter (cumulative + density), HistogramWriter, ScenarioComparisonWriter.scripts/scan_exe_for_key.py— paranoid scan of a built PyInstaller exe for every encoding of the plain activation key (ASCII decimal, UTF-16 LE wide string, little-endian int64 bytes, big-endian int64 bytes, 8-byte compact form, hex string both cases, and composite first4+last4 across printable runs). Exits non-zero on any hit; wired intorelease.ymlas a release-blocker step before the PyPI upload so a regression in the obfuscation can't ship.
Fixed
discover_inputsno longer over-scores zero-valued cells. Thevalue not in (0, 1)exclusion was guarding only the multiple-of-10 bonus; the multiple-of-100 and multiple-of-1000 bonuses still fired forvalue=0because0 % n == 0. Result: a cell holding 0 scored 2.0, identical to a cell holding 100 — flags tied with real scenario assumptions. Also added an explicitnot isinstance(value, bool)guard soFalsecells don't take the same code path (Python'sisinstance(True, int)is True).
Verified
- v0.3.0-alpha.5 builds — wheel (~17 KB), sdist (~140 KB), and PyInstaller exe (~39 MB) — and th