Skip to content

Commit dfdcfc1

Browse files
vosesoftclaude
andcommitted
Fix Excel attach: bind via GetActiveObject, not xlwings hwnd-walk
The server timed out on every Excel-touching call once the ModelChoice add-in was loaded into Excel. Root cause: xlwings binds to Excel by walking window handles (AccessibleObjectFromWindow -> .Application), and that path fails with OLE 0x800A01A8 — and can hang the call outright — when the Excel-DNA add-in is present. The ROT-based GetActiveObject('Excel.Application') dispatch is reliable. _book now injects that dispatch into app.impl._xl before any worksheet access (per call, so an Excel restart can't strand a handle). Verified live end to end against a loaded add-in: write_tree, render, list_trees, roll_up, run_evpi, run_evii, and verify_rollback all work (root EV 1460 from the add-in's MC_V_ cells matched the Python rollback exactly). No-ops off Windows / if win32com is unavailable, so the fake-bridge tests are unaffected. 50 tests pass; ruff + mypy clean. Version 0.0.13. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 670fea6 commit dfdcfc1

4 files changed

Lines changed: 23 additions & 3 deletions

File tree

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.12"
3+
version = "0.0.13"
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.12"
4+
__version__ = "0.0.13"

src/modelchoice_mcp/bridge.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,33 @@ def _book(self, workbook: str | None) -> Any:
5151
raise ExcelNotRunningError(
5252
"No running Excel instance found. Open the workbook in Excel first."
5353
)
54+
self._harden_attach(app)
5455
if workbook is None:
5556
return app.books.active
5657
for b in app.books:
5758
if b.name == workbook:
5859
return b
5960
raise ModelChoiceNotFoundError(f"Workbook {workbook!r} is not open.")
6061

62+
@staticmethod
63+
def _harden_attach(app: Any) -> None:
64+
"""Make the COM attach robust against a loaded ModelChoice add-in.
65+
66+
xlwings binds to Excel by walking window handles
67+
(``AccessibleObjectFromWindow`` → ``.Application``). Once ModelChoice's
68+
Excel-DNA add-in is loaded, that path fails with OLE ``0x800A01A8``
69+
(and can hang the call entirely). The ROT-based
70+
``GetActiveObject('Excel.Application')`` dispatch is reliable, so we
71+
inject it as the app's COM object before any worksheet access. Done
72+
per call so an Excel restart can't leave a stale handle. No-ops off
73+
Windows or if win32com isn't importable."""
74+
try:
75+
import win32com.client # type: ignore[import-untyped]
76+
77+
app.impl._xl = win32com.client.GetActiveObject("Excel.Application")
78+
except Exception:
79+
pass # fall back to xlwings' default attach
80+
6181
def read_store_raw(self, workbook: str | None = None) -> str:
6282
"""Return the reassembled ``_MC_Store`` A1 payload, or '' if the
6383
sheet is absent."""

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.12"
10+
assert __version__ == "0.0.13"
1111

1212

1313
def test_server_name() -> None:

0 commit comments

Comments
 (0)