e1114bdcf9
The salvaged fix gave `_find_missing` and `_flatten_messages` each their own visited-set loop, next to the one `_is_session_expired_error` already had — three copies of the same idiom in one module. Collapse them into `_iter_exception_nodes` (pre-order, left-to-right, each node once, bounded by `_EXC_TRAVERSAL_MAX_NODES`) and read all three scans off that list. Acyclic output is byte-identical: the missing-executable search keeps its depth-first order and a message-less leaf still renders as its class name. Tests move from the issue-numbered file into `tests/tools/test_mcp_tool_errors.py` (mirror of the source module): a two-node cycle renders the real messages, and a missing stdio binary wrapped deeper than the recursion limit with the chain looping back to the top is still reported as the missing executable. Both are red on origin/main (RecursionError). Co-authored-by: Stephan Mongstad <stephan@users.noreply.github.com>