Skip to content

Commit 8fa7daa

Browse files
vosesoftclaude
andcommitted
Add open_workbook tool (0.0.26) — open a tree workbook from disk
Mirrors the new modelrisk-mcp open_workbook. open_workbook(path) opens an .xlsx in the running Excel via the bridge (Workbooks.Open, behind the GetActiveObject hardening), then reports the workbook's sheets and any ModelChoice tree sheets in its _MC_Store — so you can tell at a glance whether it's a decision-tree workbook. Reuses an already-open book of the same name; raises for a missing file / open failure / no running Excel. - bridge.open_workbook; schemas.OpenWorkbookResult; tools.open_workbook (+__all__). - test_tools: fake-bridge open_workbook + a tool test. README/CHANGELOG; 23 tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent be6127b commit 8fa7daa

9 files changed

Lines changed: 117 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
All notable changes to `modelchoice-mcp`. Versions are tag-driven; pushing a
44
`vX.Y.Z` tag publishes to PyPI via the release workflow.
55

6+
## 0.0.26
7+
- **`open_workbook`** — open a decision-tree workbook (.xlsx) from disk in the
8+
running Excel so the other tools can act on it. Reports the workbook's sheets
9+
and any ModelChoice tree sheets it contains; reuses an already-open workbook
10+
of the same name. (Mirrors the new `open_workbook` in modelrisk-mcp.)
11+
612
## 0.0.25
713
- **`build_mcda` gains AHP weight elicitation (AB#2646)** — set
814
`weight_source="ahp"` and pass an `ahp_matrix` (Saaty 1-9 pairwise comparisons,

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
A sibling to [`modelrisk-mcp`](https://github.com/vosesoftware/modelrisk-mcp): where that server brings Monte Carlo risk modelling into a conversation, this one brings **decision analysis** — building, reading, rolling back, and analysing decision trees in Excel.
66

7-
> **Status: `0.0.25` — Phase 3 (build + drive).** 22 tools: build_tree / build_mcda / edit_tree (incl. add/remove options & outcomes) / set_input_distribution (put a Vose distribution on an input) / build_control_panel / export_tree_json / import_tree_json / import_precisiontree, read + roll + verify, run_scenarios (what-if comparison), plus run_evpi / run_evii / run_risk_profile / run_robustness / run_sensitivity / run_decision_report (strategy/policy/brief/mcda/force-to-outcome/two-way) / run_analysis / read_sheet over ModelChoice's headless commands.
7+
> **Status: `0.0.26` — Phase 3 (build + drive).** 23 tools: build_tree / build_mcda / edit_tree (incl. add/remove options & outcomes) / set_input_distribution (put a Vose distribution on an input) / build_control_panel / export_tree_json / import_tree_json / import_precisiontree, read + roll + verify, run_scenarios (what-if comparison), plus run_evpi / run_evii / run_risk_profile / run_robustness / run_sensitivity / run_decision_report (strategy/policy/brief/mcda/force-to-outcome/two-way) / run_analysis / read_sheet over ModelChoice's headless commands.
88
99
## Tools
1010

@@ -17,6 +17,7 @@ A sibling to [`modelrisk-mcp`](https://github.com/vosesoftware/modelrisk-mcp): w
1717
| `build_control_panel` | **Lift the tree's inputs into a control panel** at the top of the sheet — every probability and cash flow as a labelled cell, with the tree linked back to it, so you drive the whole model from one input block. Drives `MC_BuildControlPanel_Auto`. |
1818
| `export_tree_json` / `import_tree_json` | Round-trip a tree's raw ModelChoice JSON — export to save/share/version, import (validated) to write it back into a workbook. |
1919
| `import_precisiontree` | Import a PrecisionTree workbook (.xls/.xlsx) into ModelChoice — converts a copy (original untouched). Drives `MC_ImportPrecisionTree_Auto`. |
20+
| `open_workbook` | **Open a workbook (.xlsx) from disk** in the running Excel so the other tools can act on it. Reports its sheets + any ModelChoice tree sheets; reuses an already-open workbook of the same name. |
2021
| `list_trees` | List the decision trees in a workbook with node-type counts. |
2122
| `get_tree` | Full structure of one tree — decision / chance / terminal nodes, branches, probabilities, values. |
2223
| `roll_up` | Roll the tree back to its expected values and **optimal policy** — the decision recommendation, in plain English. |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "modelchoice-mcp"
3-
version = "0.0.25"
3+
version = "0.0.26"
44
description = "An open Model Context Protocol server for Vose Software's ModelChoice decision-tree add-in for Excel."
55
readme = "README.md"
66
requires-python = ">=3.11"

src/modelchoice_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""ModelChoice MCP — an open Model Context Protocol server for Vose
22
Software's ModelChoice decision-tree add-in for Excel."""
33

4-
__version__ = "0.0.25"
4+
__version__ = "0.0.26"

src/modelchoice_mcp/bridge.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from __future__ import annotations
1414

1515
import json
16+
import os
1617
from typing import Any, ClassVar
1718

1819
from modelchoice_mcp.store import STORE_SHEET_NAME, parse_store, reassemble_chunks
@@ -59,6 +60,49 @@ def _book(self, workbook: str | None) -> Any:
5960
return b
6061
raise ModelChoiceNotFoundError(f"Workbook {workbook!r} is not open.")
6162

63+
def open_workbook(self, path: str) -> dict[str, Any]:
64+
"""Open a workbook from disk in the running Excel and report its
65+
sheets + any ModelChoice trees it contains. If a workbook with the
66+
same file name is already open, that one is used (Excel won't open two
67+
with the same name). Raises if Excel isn't running, the file is
68+
missing, or Excel can't open it."""
69+
xw = self._load_xw()
70+
app = xw.apps.active
71+
if app is None:
72+
raise ExcelNotRunningError(
73+
"No running Excel instance found. Open Excel (with the ModelChoice "
74+
"add-in) first, then open the workbook."
75+
)
76+
self._harden_attach(app)
77+
if not os.path.isfile(path):
78+
raise ModelChoiceNotFoundError(f"File not found: {path!r}.")
79+
name = os.path.basename(path)
80+
book = None
81+
for b in app.books:
82+
try:
83+
if str(b.name).lower() == name.lower():
84+
book = b
85+
break
86+
except Exception:
87+
continue
88+
if book is None:
89+
try:
90+
book = app.books.open(path)
91+
except Exception as exc:
92+
raise ModelChoiceNotFoundError(f"Excel could not open {path!r}: {exc}") from exc
93+
wb_name = str(book.name)
94+
try:
95+
sheets = [s.name for s in book.sheets]
96+
except Exception:
97+
sheets = []
98+
# ModelChoice tree sheets, if this is a ModelChoice workbook.
99+
try:
100+
raw = self.read_store_raw(wb_name)
101+
trees = list(parse_store(raw)) if raw else []
102+
except Exception:
103+
trees = []
104+
return {"workbook": wb_name, "sheets": sheets, "trees": trees}
105+
62106
@staticmethod
63107
def _harden_attach(app: Any) -> None:
64108
"""Make the COM attach robust against a loaded ModelChoice add-in.

src/modelchoice_mcp/schemas.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,17 @@ class InputDistributionResult(BaseModel):
303303
note: str
304304

305305

306+
class OpenWorkbookResult(BaseModel):
307+
"""Outcome of opening a workbook from disk in the running Excel."""
308+
309+
workbook: str = Field(description="The opened workbook's file name (now active).")
310+
sheets: list[str] = Field(description="Worksheet names in the workbook.")
311+
trees: list[str] = Field(
312+
description="ModelChoice tree sheets found in the workbook (empty if none)."
313+
)
314+
note: str
315+
316+
306317
class TreeExport(BaseModel):
307318
"""A tree's stored ModelChoice model JSON, for saving or sharing."""
308319

src/modelchoice_mcp/tools.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
NodeDiff,
3232
NodeResultView,
3333
NodeView,
34+
OpenWorkbookResult,
3435
RiskProfileReport,
3536
RiskProfileSeries,
3637
RobustnessSummary,
@@ -536,6 +537,36 @@ def _counts(tree: DecisionTree) -> tuple[int, int, int]:
536537
return d, c, t
537538

538539

540+
@mcp.tool(
541+
description=(
542+
"ModelChoice: Open a workbook (.xlsx) from disk in the running Excel so "
543+
"the other tools can act on it. Pass an absolute file path. Reports the "
544+
"workbook's sheets and any ModelChoice tree sheets it contains (so you "
545+
"can tell at a glance whether it's a decision-tree workbook). If a "
546+
"workbook with the same file name is already open, that one is used. "
547+
"Requires Excel running (with the ModelChoice add-in for rendering)."
548+
)
549+
)
550+
def open_workbook(
551+
path: Annotated[
552+
str,
553+
Field(description="Absolute path to the workbook, e.g. r'C:\\models\\oil.xlsx'."),
554+
],
555+
) -> OpenWorkbookResult:
556+
r = get_bridge().open_workbook(path)
557+
trees = r.get("trees", [])
558+
wb = r.get("workbook", "")
559+
note = (
560+
f"Opened {wb!r} with {len(trees)} ModelChoice tree(s): {', '.join(trees)}."
561+
if trees
562+
else f"Opened {wb!r} — no ModelChoice trees found (it's not a ModelChoice workbook, "
563+
"or the tree store is absent)."
564+
)
565+
return OpenWorkbookResult(
566+
workbook=wb, sheets=r.get("sheets", []), trees=trees, note=note
567+
)
568+
569+
539570
@mcp.tool(
540571
description=(
541572
"ModelChoice: List the decision trees stored in a workbook, with a "
@@ -1591,6 +1622,7 @@ def read_sheet(
15911622
"import_precisiontree",
15921623
"import_tree_json",
15931624
"list_trees",
1625+
"open_workbook",
15941626
"read_sheet",
15951627
"roll_up",
15961628
"run_analysis",

tests/test_server.py

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

88

99
def test_version_set() -> None:
10-
assert __version__ == "0.0.25"
10+
assert __version__ == "0.0.26"
1111

1212

1313
def test_server_name() -> None:

tests/test_tools.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ def set_input_formula(self, named_range: str, formula: str,
141141
self.input_formula = (named_range, formula)
142142
return sheet_name or "MC_Tree_1"
143143

144+
def open_workbook(self, path: str) -> dict[str, object]:
145+
self.opened_path = path
146+
return {
147+
"workbook": "oil.xlsx",
148+
"sheets": ["MC_Tree_1", "_MC_Store"],
149+
"trees": ["MC_Tree_1"],
150+
}
151+
144152
def write_tree(self, model_json: str, sheet_name: str | None = None,
145153
workbook: str | None = None) -> str:
146154
self.written_json = model_json
@@ -282,6 +290,17 @@ def test_build_control_panel_parses_inputs(bridge: _FakeBridge) -> None:
282290
assert "persist across re-renders" in out.note
283291

284292

293+
def test_open_workbook(bridge: _FakeBridge) -> None:
294+
from modelchoice_mcp.schemas import OpenWorkbookResult
295+
296+
out = tools.open_workbook(r"C:\models\oil.xlsx")
297+
assert isinstance(out, OpenWorkbookResult)
298+
assert bridge.opened_path == r"C:\models\oil.xlsx"
299+
assert out.workbook == "oil.xlsx"
300+
assert out.trees == ["MC_Tree_1"]
301+
assert "MC_Tree_1" in out.note
302+
303+
285304
def test_set_input_distribution_value(bridge: _FakeBridge) -> None:
286305
from modelchoice_mcp.schemas import InputDistributionResult
287306

0 commit comments

Comments
 (0)