Three internal layers plus two external integration paths. The split is deliberate: ModelRisk MCP writes through Excel (builds + simulation trigger) and reads through ModelRisk's official SDK directly. Each path is picked for what it does best, and the boundaries are where every prior attempt at a unified-COM design fractured.
┌──────────────────────────────────┐
│ MCP client │
│ (Claude Desktop, Code, etc.) │
└────────────────┬─────────────────┘
│ JSON-RPC / stdio (or HTTP)
▼
┌──────────────────────────────────┐
│ FastMCP layer │ server.py, tools/, resources/, prompts/
│ — tools, resources, prompts │
├──────────────────────────────────┤
│ ModelRiskBridge (domain) │ bridge/modelrisk.py
│ + SimulationController │ bridge/simulation.py
│ + ResultsReader │ bridge/results.py
├──────────────────────────────────┤
│ ExcelBridge MrServiceBridge│ bridge/excel.py
│ (xlwings) (ctypes) │ bridge/mrservice.py
└──────┬───────────────────┬───────┘
│ Application.Run │ MRLIB_*
│ + cell I/O │ (read .vmrs)
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ Excel + │ │ MRService.dll │
│ ModelRisk XLL│ │ (SDK) │
└──────────────┘ └──────────────────┘
Protocol surface. Pure Python. Stateless except for the registered
FastMCP instance. Tools, resources, and prompts live under
src/modelrisk_mcp/server.py,
tools/, resources/,
and prompts/. Nothing in this layer
talks to Excel or to MRService; it only calls into the domain layer.
Owns the function catalogue, formula validation, simulation control, and results parsing. Pure Python; no direct COM or ctypes calls.
ModelRiskBridge(bridge/modelrisk.py) — top-level domain object. ComposesExcelBridge,SimulationController,ResultsReader, plus theFunctionCatalogue. Every tool entry point goes through this object.SimulationController(bridge/simulation.py) — drives Monte Carlo runs through the XLL command surface (no ATL dispatch). PacksCSimulationOptionsinto the[Key]:Valuearray shapeVoseStartSimulCustom12expects, then callsVoseGetDataSZ12with theh<hwnd>_SaveResultsToFile_<book>session name to persist the.vmrs.ResultsReader(bridge/results.py) — opens.vmrsfiles viaMrServiceBridge, resolves variable IDs, pulls sample arrays, computes statistics and percentiles viaMRLIB_CalcStatistics/MRLIB_CalcPercentilesArray. Correlation matrices and tornado sensitivity are computed in Python (numpy) from the per-iteration sample arrays.
The only layer that touches Excel or MRService.dll. Two bridges, two external surfaces.
ExcelBridge(bridge/excel.py) — wraps xlwings for ordinary Excel I/O: workbook enumeration, cell and range reads, formula writes, named-range management, and add-in inspection. The simulation kickoff is also an Excel COM call (via theapp.api.Runaccessor underSimulationController), but it reaches the XLL through Excel's standardApplication.Runsurface — not through any ModelRisk COM dispatch.MrServiceBridge(bridge/mrservice.py) — ctypes wrapper aroundMRService.dll. Discovers the DLL via theMRSERVICE_DLLenv var or the standard install paths, activates it per-process withMRLIB_SetOfflineActivationKey(key bundled and reconstructed inbridge/_keymat.py), opens.vmrsfiles withMRLIB_OpenSimulationModel, and exposes aVmrsHandlethat proxies the per-variable sample, statistics, and percentile calls.
Each path is chosen for what it does best, and each one bypasses exactly the COM surface that doesn't work.
Everything that mutates the workbook or asks Excel to do work goes through Excel's own COM surface, attached via xlwings:
- Cell and range I/O, named ranges, formula writes, add-in inspection — direct xlwings calls.
- Simulation kickoff —
Application.Run("VoseStartSimulCustom12", options_array)followed byApplication.Run("VoseGetDataSZ12", session_name, target_path).
Application.Run is a documented Excel COM endpoint that any registered
XLL command can be reached through. We replicate exactly what
ModelRiskAtl's IModelRiskSimulation::StartSimulation and
IModelRiskSimulationResults::SaveResultsToFile do internally when
their methods fire — the XLL handlers route the Application.Run call
to CSimulationsManager the same way they route the ATL's internal
call. The version that did not work, and which we no longer attempt,
was dispatching the ATL CoClasses directly: ModelRiskAtl.dll declares
them as IDispatch in the IDL, but the runtime objects don't return
IDispatch from QueryInterface, so cross-process automation falls
over. The XLL command surface side-steps that entirely.
Vose's official SDK opens .vmrs files, returns sample arrays, and
computes statistics natively:
MRLIB_OpenSimulationModel(path, &model_ptr)— opens a.vmrs..xlsxis rejected; Excel + the XLL still has to run the simulation.MRLIB_GetModelDataLength,MRLIB_GetModelData— per-variable sample arrays.MRLIB_CalcStatistics,MRLIB_CalcPercentilesArray— population moments and arbitrary percentiles, computed in-DLL on numpy buffers.MRLIB_GetModelVarID— name-to-ID lookup keyed on the names already read out of Excel bylist_modelrisk_inputs/_outputs.
No COM round-trips per output; the per-iteration sample arrays are materialised in Python for any downstream analysis we want (correlation matrices, tornados, custom percentiles, hand-rolled VaR/TVaR).
| Concern | File |
|---|---|
| MCP tool registration | tools/*.py |
| MCP resources, prompts | resources/*.py, prompts/*.py |
| Domain orchestration | bridge/modelrisk.py |
| Function catalogue load | bridge/catalogue.py |
| Formula construction | bridge/formulas.py |
| Simulation control | bridge/simulation.py |
.vmrs reading + stats |
bridge/results.py |
| xlwings + Excel COM | bridge/excel.py |
| MRService.dll ctypes | bridge/mrservice.py |
| Bundled activation key | bridge/_keymat.py |
| Safety rails (mutex, log) | safety.py |
| Typed errors | errors.py |
See com-surface.md for the precise XLL commands and
MRLIB_* exports we depend on.