excel-mcp-server 2.2.1__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/PKG-INFO +3 -2
  2. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/README.md +1 -0
  3. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/pyproject.toml +3 -3
  4. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/pyproject.toml.orig +3 -3
  5. excel_mcp_server-2.2.2/src/excel_mcp/cell_budget.py +73 -0
  6. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/cli.py +8 -0
  7. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/config.py +2 -0
  8. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_edit.py +6 -8
  9. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_lines.py +3 -1
  10. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_source.py +47 -8
  11. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_workbook.py +27 -9
  12. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/calculated.py +9 -2
  13. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/cells.py +8 -0
  14. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/hyperlinks.py +2 -1
  15. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/inspect.py +2 -2
  16. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/layout.py +1 -1
  17. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/notes.py +2 -1
  18. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/sheet_tools.py +1 -1
  19. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/workspace.py +31 -16
  20. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/LICENSE +0 -0
  21. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/__init__.py +0 -0
  22. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/__main__.py +0 -0
  23. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/cells.py +0 -0
  24. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/criteria.py +0 -0
  25. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dependencies.py +0 -0
  26. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dynamic.py +0 -0
  27. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/engine.py +0 -0
  28. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/__init__.py +0 -0
  29. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/arrays.py +0 -0
  30. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/conditional.py +0 -0
  31. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/datetime.py +0 -0
  32. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/depreciation.py +0 -0
  33. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/distributions.py +0 -0
  34. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/financial.py +0 -0
  35. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/helpers.py +0 -0
  36. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/indirect.py +0 -0
  37. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/information.py +0 -0
  38. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/logical.py +0 -0
  39. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/lookup.py +0 -0
  40. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/math.py +0 -0
  41. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/regression.py +0 -0
  42. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/securities.py +0 -0
  43. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/statistical.py +0 -0
  44. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/subtotals.py +0 -0
  45. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/text.py +0 -0
  46. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/operators.py +0 -0
  47. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/parser.py +0 -0
  48. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/r1c1.py +0 -0
  49. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/registry.py +0 -0
  50. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/scheduler.py +0 -0
  51. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/solver.py +0 -0
  52. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/steps.py +0 -0
  53. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/tablerefs.py +0 -0
  54. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/textformat.py +0 -0
  55. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/values.py +0 -0
  56. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/wildcard.py +0 -0
  57. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/cfb.py +0 -0
  58. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/chart_roundtrip.py +0 -0
  59. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/errors.py +0 -0
  60. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/formula_syntax.py +0 -0
  61. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/formulas.py +0 -0
  62. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/function_arguments.json +0 -0
  63. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/inputs.py +0 -0
  64. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_find.py +0 -0
  65. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_rows.py +0 -0
  66. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_shared.py +0 -0
  67. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/links.py +0 -0
  68. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/macros.py +0 -0
  69. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/__init__.py +0 -0
  70. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_index.py +0 -0
  71. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_info.py +0 -0
  72. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex.py +0 -0
  73. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
  74. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
  75. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
  76. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
  77. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
  78. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
  79. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
  80. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_xml.py +0 -0
  81. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts.py +0 -0
  82. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_axes.py +0 -0
  83. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_check.py +0 -0
  84. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_data.py +0 -0
  85. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_look.py +0 -0
  86. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_modern.py +0 -0
  87. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_options.py +0 -0
  88. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_series.py +0 -0
  89. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_style.py +0 -0
  90. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/columns.py +0 -0
  91. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/comparison.py +0 -0
  92. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional.py +0 -0
  93. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_kinds.py +0 -0
  94. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_rule.py +0 -0
  95. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_x14.py +0 -0
  96. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_export.py +0 -0
  97. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_format.py +0 -0
  98. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_import.py +0 -0
  99. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_values.py +0 -0
  100. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/date_width.py +0 -0
  101. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/drawings.py +0 -0
  102. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/duplicates.py +0 -0
  103. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/files.py +0 -0
  104. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/fill.py +0 -0
  105. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_criteria.py +0 -0
  106. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_stored.py +0 -0
  107. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filters.py +0 -0
  108. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/formatting.py +0 -0
  109. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/goal_seek.py +0 -0
  110. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/images.py +0 -0
  111. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_objects.py +0 -0
  112. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_rules.py +0 -0
  113. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_styles.py +0 -0
  114. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edits.py +0 -0
  115. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/names.py +0 -0
  116. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/paste.py +0 -0
  117. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot.py +0 -0
  118. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_axis.py +0 -0
  119. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_cache.py +0 -0
  120. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_calc.py +0 -0
  121. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_definition.py +0 -0
  122. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_fields.py +0 -0
  123. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_groups.py +0 -0
  124. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_index.py +0 -0
  125. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_options.py +0 -0
  126. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
  127. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_records.py +0 -0
  128. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_render.py +0 -0
  129. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_showas.py +0 -0
  130. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_source.py +0 -0
  131. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_specs.py +0 -0
  132. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_values.py +0 -0
  133. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/print_setup.py +0 -0
  134. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/protection.py +0 -0
  135. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/replace.py +0 -0
  136. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rule_clear.py +0 -0
  137. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rules.py +0 -0
  138. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_copy.py +0 -0
  139. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_counts.py +0 -0
  140. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_refs.py +0 -0
  141. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_shapes.py +0 -0
  142. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_view.py +0 -0
  143. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheets.py +0 -0
  144. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/shifting.py +0 -0
  145. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_index.py +0 -0
  146. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_manage.py +0 -0
  147. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_pivot.py +0 -0
  148. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_place.py +0 -0
  149. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_table.py +0 -0
  150. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_xml.py +0 -0
  151. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicers.py +0 -0
  152. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sorting.py +0 -0
  153. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spans.py +0 -0
  154. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_style.py +0 -0
  155. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_xml.py +0 -0
  156. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparklines.py +0 -0
  157. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spill.py +0 -0
  158. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/table_options.py +0 -0
  159. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/tables.py +0 -0
  160. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/text_split.py +0 -0
  161. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/trace.py +0 -0
  162. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/transform.py +0 -0
  163. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/used_range.py +0 -0
  164. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba.py +0 -0
  165. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba_write.py +0 -0
  166. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_rewrite.py +0 -0
  167. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_settings.py +0 -0
  168. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/x14_xml.py +0 -0
  169. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba.py +0 -0
  170. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_compress.py +0 -0
  171. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_write.py +0 -0
  172. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/__init__.py +0 -0
  173. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/anchors.py +0 -0
  174. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/arrays.py +0 -0
  175. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture.py +0 -0
  176. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture_sheet.py +0 -0
  177. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/consistency.py +0 -0
  178. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/extensions.py +0 -0
  179. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/guards.py +0 -0
  180. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lazy_records.py +0 -0
  181. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lines.py +0 -0
  182. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/metadata.py +0 -0
  183. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/model.py +0 -0
  184. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/opc.py +0 -0
  185. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/patch.py +0 -0
  186. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivot_records.py +0 -0
  187. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivots.py +0 -0
  188. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/properties.py +0 -0
  189. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/reader.py +0 -0
  190. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/references.py +0 -0
  191. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore.py +0 -0
  192. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore_sheet.py +0 -0
  193. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/scan.py +0 -0
  194. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/shape_names.py +0 -0
  195. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/vml.py +0 -0
  196. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/paths.py +0 -0
  197. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/plain_refs.py +0 -0
  198. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/py.typed +0 -0
  199. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/refs.py +0 -0
  200. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/rewrite.py +0 -0
  201. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/__init__.py +0 -0
  202. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/annotation_tools.py +0 -0
  203. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/areas.py +0 -0
  204. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/csv_tools.py +0 -0
  205. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/data_tools.py +0 -0
  206. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/format_tools.py +0 -0
  207. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/formula_tools.py +0 -0
  208. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/http.py +0 -0
  209. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/image_tools.py +0 -0
  210. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/object_tools.py +0 -0
  211. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/params.py +0 -0
  212. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/pivot_tools.py +0 -0
  213. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/registry.py +0 -0
  214. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/results.py +0 -0
  215. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/slicer_tools.py +0 -0
  216. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/validation.py +0 -0
  217. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_tools.py +0 -0
  218. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_write_tools.py +0 -0
  219. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/workbook_tools.py +0 -0
  220. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/spill.py +0 -0
  221. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/structured.py +0 -0
  222. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/text.py +0 -0
  223. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_links.py +0 -0
  224. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_scan.py +0 -0
  225. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/values.py +0 -0
  226. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/xlfn.py +0 -0
  227. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/xlm.py +0 -0
  228. {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/zip_limits.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excel-mcp-server
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: MCP server for creating, reading and editing Excel workbooks without Microsoft Excel
5
5
  Keywords: mcp,mcp-server,model-context-protocol,excel,xlsx,xlsm,spreadsheet,openpyxl,charts,pivot-table,formulas,vba,llm,ai,claude,cursor
6
6
  Author: Haris Musa
@@ -23,7 +23,7 @@ Classifier: Typing :: Typed
23
23
  Requires-Dist: mcp>=2.2,<3
24
24
  Requires-Dist: olefile>=0.47,<1
25
25
  Requires-Dist: openpyxl>=3.1.5,<4
26
- Requires-Dist: pillow>=12,<13
26
+ Requires-Dist: pillow>=13,<14
27
27
  Requires-Dist: pydantic>=2.12,<3
28
28
  Requires-Dist: starlette>=1,<2
29
29
  Requires-Dist: uvicorn>=0.31,<1
@@ -234,6 +234,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
234
234
  | `--max-file-mb N` | `EXCEL_MCP_MAX_FILE_MB` | `100` | Largest workbook the server opens. Also sets the unpacked-size limit (10 times this) that every opened or uploaded workbook must stay under; parts compressed over 500:1 are refused |
235
235
  | `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
236
236
  | `--max-pivot-cells N` | `EXCEL_MCP_MAX_PIVOT_CELLS` | `3000000` | Largest PivotTable source in cells (a 230,000-row, 13-column sheet); creating one from the biggest sources takes a minute and about 2 GB of memory |
237
+ | `--max-loaded-cells N` | `EXCEL_MCP_MAX_LOADED_CELLS` | `3000000` | Most cells and rows of a workbook that may be loaded into memory at once, about 0.5 KB each (a 3,000,000 limit is about 1.5 GB). A workbook with more is refused by the tools that must load whole sheets; `read_range` and the other streaming reads, and edits of large sheets that parse only some rows, are not limited by the size of the sheet |
237
238
  | `--log-level LEVEL` | | `WARNING` | Logging on stderr |
238
239
  | `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
239
240
  | `--port PORT` | `EXCEL_MCP_PORT` | `8017` | HTTP port |
@@ -198,6 +198,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
198
198
  | `--max-file-mb N` | `EXCEL_MCP_MAX_FILE_MB` | `100` | Largest workbook the server opens. Also sets the unpacked-size limit (10 times this) that every opened or uploaded workbook must stay under; parts compressed over 500:1 are refused |
199
199
  | `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
200
200
  | `--max-pivot-cells N` | `EXCEL_MCP_MAX_PIVOT_CELLS` | `3000000` | Largest PivotTable source in cells (a 230,000-row, 13-column sheet); creating one from the biggest sources takes a minute and about 2 GB of memory |
201
+ | `--max-loaded-cells N` | `EXCEL_MCP_MAX_LOADED_CELLS` | `3000000` | Most cells and rows of a workbook that may be loaded into memory at once, about 0.5 KB each (a 3,000,000 limit is about 1.5 GB). A workbook with more is refused by the tools that must load whole sheets; `read_range` and the other streaming reads, and edits of large sheets that parse only some rows, are not limited by the size of the sheet |
201
202
  | `--log-level LEVEL` | | `WARNING` | Logging on stderr |
202
203
  | `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
203
204
  | `--port PORT` | `EXCEL_MCP_PORT` | `8017` | HTTP port |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.1"
3
+ version = "2.2.2"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -43,7 +43,7 @@ dependencies = [
43
43
  "mcp>=2.2,<3",
44
44
  "olefile>=0.47,<1",
45
45
  "openpyxl>=3.1.5,<4",
46
- "pillow>=12,<13",
46
+ "pillow>=13,<14",
47
47
  "pydantic>=2.12,<3",
48
48
  "starlette>=1,<2",
49
49
  "uvicorn>=0.31,<1",
@@ -71,7 +71,7 @@ dev = [
71
71
  ]
72
72
 
73
73
  [build-system]
74
- requires = ["uv_build>=0.12.19,<0.13"]
74
+ requires = ["uv_build>=0.13.1,<0.14"]
75
75
  build-backend = "uv_build"
76
76
 
77
77
  [tool.ruff]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.1"
3
+ version = "2.2.2"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -44,7 +44,7 @@ dependencies = [
44
44
  "mcp>=2.2,<3",
45
45
  "olefile>=0.47,<1",
46
46
  "openpyxl>=3.1.5,<4",
47
- "pillow>=12,<13",
47
+ "pillow>=13,<14",
48
48
  "pydantic>=2.12,<3",
49
49
  "starlette>=1,<2",
50
50
  "uvicorn>=0.31,<1",
@@ -68,7 +68,7 @@ dev = [
68
68
  ]
69
69
 
70
70
  [build-system]
71
- requires = ["uv_build>=0.12.19,<0.13"]
71
+ requires = ["uv_build>=0.13.1,<0.14"]
72
72
  build-backend = "uv_build"
73
73
 
74
74
  [tool.ruff]
@@ -0,0 +1,73 @@
1
+ """A limit on the cells that a workbook's sheets make openpyxl create.
2
+
3
+ Memory use is set by the number of cells created, not by the size of the file: a sheet of
4
+ millions of empty cells is a few hundred kilobytes. So every sheet is counted before it is
5
+ parsed, and what is created cannot pass the limit. Rows count as cells (openpyxl keeps their
6
+ attributes), and so does every cell of a merged range or hyperlink range, which it creates
7
+ from the one element that names the range.
8
+ """
9
+
10
+ from pathlib import Path
11
+ from typing import IO
12
+ from xml.parsers import expat
13
+
14
+ from openpyxl.reader.excel import ExcelReader
15
+ from openpyxl.xml.constants import SHEET_MAIN_NS, WORKSHEET_TYPE
16
+
17
+ from excel_mcp.errors import LimitExceededError
18
+ from excel_mcp.refs import parse_range
19
+
20
+ _CHUNK = 1 << 20
21
+ _NAMESPACE = f"{SHEET_MAIN_NS} "
22
+ _COUNTED = {f"{_NAMESPACE}c", f"{_NAMESPACE}row"}
23
+ _MERGED = f"{_NAMESPACE}mergeCell"
24
+ _LINKED = f"{_NAMESPACE}hyperlink"
25
+ # A cell that holds a link takes about three times the memory of one that does not.
26
+ _LINK_COST = 3
27
+
28
+
29
+ class CellBudget:
30
+ """The cells that may still be created for one workbook, shared by all its sheets."""
31
+
32
+ def __init__(self, limit: int) -> None:
33
+ self.limit = limit
34
+ self.used = 0
35
+
36
+ def spend(self, count: int) -> None:
37
+ self.used += count
38
+ if self.used > self.limit:
39
+ raise LimitExceededError(
40
+ f"The workbook has more than {self.limit:,} cells and rows to load, which "
41
+ "would use too much memory. Read it with read_range, split it into smaller "
42
+ "workbooks, or raise --max-loaded-cells."
43
+ )
44
+
45
+
46
+ def spend_sheet(source: IO[bytes], budget: CellBudget) -> None:
47
+ """Spend the cells and rows of one worksheet's XML, stopping at the first one over the limit."""
48
+ parser = expat.ParserCreate(namespace_separator=" ")
49
+
50
+ def start(name: str, attributes: dict[str, str]) -> None:
51
+ if name in _COUNTED:
52
+ budget.spend(1)
53
+ elif name == _MERGED:
54
+ budget.spend(parse_range(attributes["ref"]).size)
55
+ elif name == _LINKED:
56
+ budget.spend(_LINK_COST * parse_range(attributes["ref"]).size)
57
+
58
+ parser.StartElementHandler = start
59
+ while chunk := source.read(_CHUNK):
60
+ parser.Parse(chunk, False)
61
+ parser.Parse(b"", True)
62
+
63
+
64
+ def check_workbook(path: Path, budget: CellBudget) -> None:
65
+ """Spend the cells and rows that loading all of a workbook's worksheets would create."""
66
+ reader = ExcelReader(path)
67
+ try:
68
+ reader.read_manifest()
69
+ for part in reader.package.findall(WORKSHEET_TYPE):
70
+ with reader.archive.open(part.PartName.lstrip("/")) as source:
71
+ spend_sheet(source, budget)
72
+ finally:
73
+ reader.archive.close()
@@ -78,6 +78,7 @@ def _settings(args: argparse.Namespace) -> Settings:
78
78
  max_file_bytes=args.max_file_mb * MB,
79
79
  max_cells=args.max_cells,
80
80
  max_pivot_source_cells=args.max_pivot_cells,
81
+ max_loaded_cells=args.max_loaded_cells,
81
82
  ),
82
83
  log_level=args.log_level,
83
84
  )
@@ -159,6 +160,13 @@ def _parser() -> argparse.ArgumentParser:
159
160
  help="Largest PivotTable source, in cells (default %(default)s). "
160
161
  "Also EXCEL_MCP_MAX_PIVOT_CELLS.",
161
162
  )
163
+ common.add_argument(
164
+ "--max-loaded-cells",
165
+ type=_positive_int,
166
+ default=_env_default("EXCEL_MCP_MAX_LOADED_CELLS", DEFAULT_LIMITS.max_loaded_cells),
167
+ help="Most cells and rows of a workbook that may be loaded into memory at once, about "
168
+ "0.5 KB each (default %(default)s). Also EXCEL_MCP_MAX_LOADED_CELLS.",
169
+ )
162
170
  common.add_argument(
163
171
  "--log-level",
164
172
  default="WARNING",
@@ -14,6 +14,8 @@ class Limits:
14
14
  max_cells: int = 100_000
15
15
  max_pivot_source_cells: int = 3_000_000
16
16
  max_copy_cells: int = 1_000_000
17
+ # Cells and rows that all the sheets of a workbook may create in memory, about 0.5 KB each.
18
+ max_loaded_cells: int = 3_000_000
17
19
  # Zip bomb guards, applied to every workbook the server opens or receives. Real workbooks
18
20
  # compress 10 to 30 times and sparse sheets far more, so these leave room for them.
19
21
  max_unpack_factor: int = 10
@@ -33,6 +33,7 @@ from openpyxl.worksheet.worksheet import Worksheet
33
33
  from openpyxl.xml.constants import SHARED_STRINGS
34
34
 
35
35
  from excel_mcp import lazy_rows, lazy_shared
36
+ from excel_mcp.cell_budget import CellBudget
36
37
  from excel_mcp.lazy_source import SheetSource, split_sheet
37
38
  from excel_mcp.lazy_workbook import BLOCK_ROWS, LazyCells, Parts, lazy_sheets, open_lazily
38
39
  from excel_mcp.package.opc import REL_BASE, Package, Rel, free_id
@@ -48,7 +49,6 @@ _ROW = re.compile(rb"<row\b[^>]*?/>|<row\b[^>]*>.*?</row>", re.S)
48
49
  _ROW_NUMBER = re.compile(rb'\br="(\d+)"')
49
50
  _DIMENSION = re.compile(rb'<dimension\s+ref="[^"]*"\s*/>')
50
51
  _CELL_COLUMN = re.compile(rb'<c\b[^>]*?\br="([A-Z]+)\d')
51
- _UNADDRESSED_CELL = re.compile(rb"<c\b(?![^>]*\br=)")
52
52
  # A row with attributes of its own (a height, a style, hidden), not just its number.
53
53
  _ATTRIBUTED_ROW = re.compile(rb"<row\b[^>]*\s(?!r=|spans=|[\w.-]+:)[\w.-]+=[^>]*>")
54
54
  # What a formula stored as its result when Excel last calculated it, which an edit makes stale.
@@ -67,8 +67,6 @@ def _split(data: bytes) -> tuple[bytes, SheetSource] | None:
67
67
  cross_cell = (b't="array"', b't="dataTable"', b' cm="', b' vm="')
68
68
  if len(data) < LAZY_SHEET_BYTES or any(marker in data for marker in cross_cell):
69
69
  return None
70
- if _UNADDRESSED_CELL.search(data): # Its column is not known until the row is read.
71
- return None
72
70
  split = split_sheet(data)
73
71
  return split if split and split[1].row_numbers else None
74
72
 
@@ -200,22 +198,22 @@ class LazyEdit:
200
198
  rels.append(Rel(free_id(rels, ""), REL_BASE + "sharedStrings", name))
201
199
 
202
200
 
203
- def load(path: Path) -> tuple[Workbook, LazyEdit | None]:
201
+ def load(path: Path, budget: CellBudget) -> tuple[Workbook, LazyEdit | None]:
204
202
  """Load a workbook for editing, its large sheets without their rows."""
205
203
  parts: Parts = {}
206
- reader = open_lazily(path, parts, _split)
204
+ reader = open_lazily(path, parts, _split, budget)
207
205
  reader.read()
208
206
  strings = reader.package.find(SHARED_STRINGS)
209
207
  edit = LazyEdit(path, strings.PartName.lstrip("/") if strings is not None else None)
210
208
  lazy = list(lazy_sheets(reader, parts))
211
209
  for sheet, source in lazy:
212
- _install(sheet, source, reader.shared_strings)
210
+ _install(sheet, source, reader.shared_strings, budget)
213
211
  return reader.wb, edit if lazy else None
214
212
 
215
213
 
216
- def _install(sheet: Worksheet, source: SheetSource, strings: list[str]) -> None:
214
+ def _install(sheet: Worksheet, source: SheetSource, strings: list[str], budget: CellBudget) -> None:
217
215
  carried = sheet._cells
218
- cells = LazyCells(sheet, source, strings)
216
+ cells = LazyCells(sheet, source, strings, budget)
219
217
  sheet._cells = cells
220
218
  _bind_row_dimensions(sheet, source, strings)
221
219
  for (row, column), carried_cell in carried.items():
@@ -154,7 +154,9 @@ def copied(cells: LazyCells, sheet: Worksheet) -> LazyCells:
154
154
  if not cells.is_loaded(number)
155
155
  ]
156
156
  masters = dict(source.masters)
157
- return LazyCells(sheet, _assembled(source, rows, masters, source.hidden_rows), cells.strings)
157
+ return LazyCells(
158
+ sheet, _assembled(source, rows, masters, source.hidden_rows), cells.strings, cells.budget
159
+ )
158
160
 
159
161
 
160
162
  def _moved_masters(source: SheetSource, edit: LineEdit) -> dict[str, SharedMaster]:
@@ -4,8 +4,9 @@ import re
4
4
  from bisect import bisect_left, bisect_right
5
5
  from dataclasses import dataclass, field
6
6
  from functools import cached_property
7
+ from itertools import pairwise
7
8
 
8
- from openpyxl.utils import column_index_from_string
9
+ from openpyxl.utils import column_index_from_string, get_column_letter
9
10
  from openpyxl.xml.constants import SHEET_MAIN_NS
10
11
 
11
12
  from excel_mcp import lazy_shared
@@ -17,6 +18,9 @@ _ROW_NUMBER = re.compile(rb'\br="(\d+)"')
17
18
  _HIDDEN = re.compile(rb'\bhidden="(?:1|true)"')
18
19
  _CELL = re.compile(rb"<c[ >/]")
19
20
  _CELL_COLUMN = re.compile(rb'<c\b[^>]*?\br="([A-Z]+)\d')
21
+ _UNADDRESSED = re.compile(rb"<row\b(?![^>]*\br=)|<c\b(?![^>]*\br=)")
22
+ _TAG = re.compile(rb"<(row|c)\b([^>]*)>")
23
+ _CELL_LETTERS = re.compile(rb'\br="([A-Z]+)\d')
20
24
  _ROOT = re.compile(rb'<worksheet\b[^>]*?\bxmlns="([^"]+)"[^>]*>')
21
25
 
22
26
 
@@ -73,24 +77,24 @@ class SheetSource:
73
77
  def split_sheet(data: bytes) -> tuple[bytes, SheetSource] | None:
74
78
  """The sheet without its rows, and the rows; None for a layout this does not handle.
75
79
 
76
- Rows without a number are left out because they are numbered by their position. The
77
- masters of shared formulas are indexed, as a row's formula can depend on the row that
78
- declares it.
80
+ Rows and cells without a number or address are given one first, as their position
81
+ says. The masters of shared formulas are indexed, as a row's formula can depend on the
82
+ row that declares it.
79
83
  """
80
84
  root = _ROOT.search(data)
81
85
  start = data.find(_SHEET_DATA)
82
86
  end = data.find(_SHEET_DATA_END)
83
87
  if root is None or root[1] != SHEET_MAIN_NS.encode() or start < 0 or end < start:
84
88
  return None
89
+ if not _all_addressed(data, start, end) and _UNADDRESSED.search(data, start, end):
90
+ data = data[:start] + _address(data[start:end]) + data[end:]
91
+ end = data.find(_SHEET_DATA_END)
85
92
  numbers: list[int] = []
86
93
  spans: list[tuple[int, int]] = []
87
94
  hidden: list[int] = []
88
95
  starts = list(_ROW.finditer(data, start, end))
89
96
  for position, tag in enumerate(starts):
90
- number = _ROW_NUMBER.search(tag.group())
91
- if number is None:
92
- return None
93
- numbers.append(int(number.group(1)))
97
+ numbers.append(int(_ROW_NUMBER.search(tag.group())[1])) # pyright: ignore[reportOptionalSubscript]
94
98
  following = starts[position + 1].start() if position + 1 < len(starts) else end
95
99
  spans.append((tag.start(), following))
96
100
  if _HIDDEN.search(tag.group()):
@@ -100,3 +104,38 @@ def split_sheet(data: bytes) -> tuple[bytes, SheetSource] | None:
100
104
  without_rows = data[:start] + _SHEET_DATA + b"/>" + data[end + len(_SHEET_DATA_END) :]
101
105
  masters = lazy_shared.index(data, start, end) if b't="shared"' in data else {}
102
106
  return without_rows, SheetSource(data, root.group(), numbers, spans, hidden, masters)
107
+
108
+
109
+ def _all_addressed(data: bytes, start: int, end: int) -> bool:
110
+ """Whether every row and cell opens with its address, as counting them (quickly) shows."""
111
+ return all(
112
+ data.count(tag, start, end) == data.count(tag + b' r="', start, end)
113
+ for tag in (b"<row", b"<c")
114
+ )
115
+
116
+
117
+ def _address(rows: bytes) -> bytes:
118
+ """The rows with the number of each row and the address of each cell that has none.
119
+
120
+ Excel and openpyxl number them by position: a row after the one before it, a cell after
121
+ the one before it in its row. Writers leave the numbers out to make the file smaller.
122
+ """
123
+ row = column = 0
124
+
125
+ def addressed(tag: re.Match[bytes]) -> bytes:
126
+ nonlocal row, column
127
+ attributes = tag[2]
128
+ if tag[1] == b"row":
129
+ given = _ROW_NUMBER.search(attributes)
130
+ row = int(given[1]) if given else row + 1
131
+ column = 0
132
+ return tag[0] if given else b'<row r="%d"%b>' % (row, attributes)
133
+ given = _CELL_LETTERS.search(attributes)
134
+ column = column_index_from_string(given[1].decode()) if given else column + 1
135
+ if given:
136
+ return tag[0]
137
+ return b'<c r="%b%d"%b>' % (get_column_letter(column).encode(), row, attributes)
138
+
139
+ bounds = [0, *(tag.start() for tag in _ROW.finditer(rows)), len(rows)]
140
+ # One row at a time, so that no list of all the pieces of a sheet is held besides the sheet.
141
+ return b"".join(_TAG.sub(addressed, rows[a:b]) for a, b in pairwise(bounds))
@@ -22,6 +22,7 @@ from openpyxl.worksheet.worksheet import Worksheet
22
22
  from openpyxl.xml.constants import WORKSHEET_TYPE
23
23
 
24
24
  from excel_mcp import lazy_rows, lazy_shared
25
+ from excel_mcp.cell_budget import CellBudget, spend_sheet
25
26
  from excel_mcp.lazy_source import SheetSource, split_sheet
26
27
 
27
28
  BLOCK_ROWS = 128
@@ -39,11 +40,14 @@ class LazyCells(dict[CellKey, Any]):
39
40
  parsed holds all of its rows: what is not in ``loaded`` is as the file has it.
40
41
  """
41
42
 
42
- def __init__(self, sheet: Worksheet, source: SheetSource, strings: list[str]) -> None:
43
+ def __init__(
44
+ self, sheet: Worksheet, source: SheetSource, strings: list[str], budget: CellBudget
45
+ ) -> None:
43
46
  super().__init__()
44
47
  self.sheet = sheet
45
48
  self.source = source
46
49
  self.strings = strings
50
+ self.budget = budget
47
51
  self.loaded: set[int] = set()
48
52
  self.columns: dict[int, list[int]] = {}
49
53
  self.last_row = self.last_column = 0
@@ -246,6 +250,9 @@ class LazyCells(dict[CellKey, Any]):
246
250
 
247
251
  def _parse(self, spans: list[tuple[int, int]]) -> None:
248
252
  """Parse rows: those in the usual forms as text, the others with openpyxl's parser."""
253
+ data = self.source.data
254
+ # The rows and their cells, counted before they are parsed as `spend_sheet` counts a sheet.
255
+ self.budget.spend(len(spans) + sum(data.count(b"<c", start, end) for start, end in spans))
249
256
  book: Any = self.sheet.parent
250
257
  shared = lazy_shared.Translators(self.source.masters)
251
258
  reading = lazy_rows.Reading(
@@ -307,11 +314,14 @@ Splitter = Callable[[bytes], tuple[bytes, SheetSource] | None]
307
314
  class _SheetArchive(ZipFile):
308
315
  """An archive that serves each worksheet without its rows, which are kept in ``parts``."""
309
316
 
310
- def __init__(self, path: Path, parts: Parts, sheet_parts: set[str], split: Splitter) -> None:
317
+ def __init__(
318
+ self, path: Path, parts: Parts, sheet_parts: set[str], split: Splitter, budget: CellBudget
319
+ ) -> None:
311
320
  super().__init__(path)
312
321
  self.parts = parts
313
322
  self.sheet_parts = sheet_parts
314
323
  self.split = split
324
+ self.budget = budget
315
325
  self.worksheets: list[str] = []
316
326
 
317
327
  def open(self, name: Any, mode: Any = "r", pwd: Any = None, **options: Any) -> IO[bytes]:
@@ -324,16 +334,24 @@ class _SheetArchive(ZipFile):
324
334
  split = self.split(data)
325
335
  self.parts[name] = split if split else (data, None)
326
336
  self.worksheets.append(name)
327
- return BytesIO(self.parts[name][0])
337
+ parsed = self.parts[name][0]
338
+ spend_sheet(BytesIO(parsed), self.budget)
339
+ return BytesIO(parsed)
328
340
 
329
341
 
330
- def open_lazily(path: Path, parts: Parts, split: Splitter, **options: Any) -> ExcelReader:
331
- """A reader that has not read the workbook yet, whose sheets are served by ``split``."""
342
+ def open_lazily(
343
+ path: Path, parts: Parts, split: Splitter, budget: CellBudget, **options: Any
344
+ ) -> ExcelReader:
345
+ """A reader that has not read the workbook yet, whose sheets are served by ``split``.
346
+
347
+ The cells and rows of the sheets that are not split are spent from ``budget`` when they
348
+ are read, and those of the split sheets when their rows are parsed.
349
+ """
332
350
  reader = ExcelReader(path, **options)
333
351
  reader.read_manifest()
334
352
  sheet_parts = {part.PartName.lstrip("/") for part in reader.package.findall(WORKSHEET_TYPE)}
335
353
  reader.archive.close()
336
- reader.archive = _SheetArchive(path, parts, sheet_parts, split)
354
+ reader.archive = _SheetArchive(path, parts, sheet_parts, split, budget)
337
355
  return reader
338
356
 
339
357
 
@@ -346,16 +364,16 @@ def lazy_sheets(reader: ExcelReader, parts: Parts) -> Iterator[tuple[Worksheet,
346
364
  yield sheet, source
347
365
 
348
366
 
349
- def load_lazy(path: Path, parts: Parts, *, data_only: bool) -> Workbook:
367
+ def load_lazy(path: Path, parts: Parts, budget: CellBudget, *, data_only: bool) -> Workbook:
350
368
  """Load a workbook the way ``load_workbook`` does, leaving the cells to be read on demand.
351
369
 
352
370
  A sheet this cannot split into rows is loaded whole. ``parts`` is shared by the loads of
353
371
  one file, so that each sheet is read from the file once.
354
372
  """
355
- reader = open_lazily(path, parts, split_sheet, data_only=data_only, keep_links=False)
373
+ reader = open_lazily(path, parts, split_sheet, budget, data_only=data_only, keep_links=False)
356
374
  reader.read()
357
375
  for sheet, source in lazy_sheets(reader, parts):
358
- cells = LazyCells(sheet, source, reader.shared_strings)
376
+ cells = LazyCells(sheet, source, reader.shared_strings, budget)
359
377
  for cell in sheet._cells.values():
360
378
  if isinstance(cell, MergedCell):
361
379
  cells.keep(cell)
@@ -12,7 +12,7 @@ from openpyxl.worksheet.worksheet import Worksheet
12
12
 
13
13
  from excel_mcp.calc.engine import Engine
14
14
  from excel_mcp.calc.values import ExcelError, Scalar, UncalculableError
15
- from excel_mcp.errors import InvalidArgumentError
15
+ from excel_mcp.errors import InvalidArgumentError, LimitExceededError
16
16
  from excel_mcp.operations import cells
17
17
  from excel_mcp.operations.cells import RangeData
18
18
  from excel_mcp.operations.comparison import FormulaResults
@@ -70,7 +70,14 @@ def formula_values(
70
70
  ]
71
71
  if not formula_cells:
72
72
  return {}
73
- data = read_calculated(workspace, path, sheet.title, str(area), area.size)
73
+ try:
74
+ data = read_calculated(workspace, path, sheet.title, str(area), area.size)
75
+ except LimitExceededError as error:
76
+ raise LimitExceededError(
77
+ f"{error} Calculating the formulas of {area} needs the cells in memory because "
78
+ "the file stores no results for them. Open the workbook in Excel and save it, or "
79
+ "use a smaller range."
80
+ ) from None
74
81
  if data.uncalculated:
75
82
  cells_list = ", ".join(data.uncalculated)
76
83
  raise InvalidArgumentError(
@@ -57,6 +57,14 @@ def stored_cells(sheet: Worksheet) -> Iterator[Cell]:
57
57
  yield cell
58
58
 
59
59
 
60
+ def described_used_range(sheet: Worksheet) -> CellRange:
61
+ """`used_range` of a sheet that is not being changed: a large sheet's is read from its XML."""
62
+ cells = sheet._cells
63
+ if isinstance(cells, LazyCells) and (found := used_range_in(cells.source.data)):
64
+ return found
65
+ return used_range(sheet)
66
+
67
+
60
68
  def stored_cells_in(sheet: Worksheet, area: CellRange) -> Iterator[Cell]:
61
69
  """The cells that hold a value inside ``area``; a large sheet parses only those rows."""
62
70
  cells = sheet._cells
@@ -12,6 +12,7 @@ from pydantic import BaseModel, Field
12
12
  from excel_mcp.errors import InvalidArgumentError
13
13
  from excel_mcp.formulas import split_top_level, unquote
14
14
  from excel_mcp.inputs import InputModel
15
+ from excel_mcp.lazy_workbook import held_cells
15
16
  from excel_mcp.links import is_allowed_address
16
17
  from excel_mcp.refs import parse_range
17
18
 
@@ -58,7 +59,7 @@ def list_links(sheet: Worksheet) -> list[LinkInfo]:
58
59
  + ("#" + cell.hyperlink.location if cell.hyperlink.location else ""),
59
60
  tooltip=cell.hyperlink.tooltip,
60
61
  )
61
- for _, cell in sorted(sheet._cells.items())
62
+ for _, cell in sorted(held_cells(sheet).items())
62
63
  if cell.hyperlink
63
64
  ]
64
65
 
@@ -7,7 +7,7 @@ from openpyxl.workbook import Workbook
7
7
  from openpyxl.worksheet.worksheet import Worksheet
8
8
  from pydantic import BaseModel
9
9
 
10
- from excel_mcp.operations.cells import streamed_used_range, used_range
10
+ from excel_mcp.operations.cells import described_used_range, streamed_used_range
11
11
  from excel_mcp.operations.chart_index import list_charts
12
12
  from excel_mcp.operations.chart_info import ChartInfo
13
13
  from excel_mcp.operations.conditional_x14 import extended_rules
@@ -139,7 +139,7 @@ def describe_workbook(
139
139
 
140
140
  def describe_sheet(sheet: Worksheet) -> SheetDetails:
141
141
  return SheetDetails(
142
- used_range=str(used_range(sheet)),
142
+ used_range=str(described_used_range(sheet)),
143
143
  freeze_panes=sheet.freeze_panes,
144
144
  auto_filter=sheet.auto_filter.ref,
145
145
  merged_ranges=sorted(str(merged) for merged in sheet.merged_cells.ranges),
@@ -157,7 +157,7 @@ def set_visibility(sheet: Worksheet, visibility: Literal["visible", "hidden"]) -
157
157
 
158
158
  def hidden_lines(sheet: Worksheet, axis: Axis) -> list[str]:
159
159
  if axis == "rows":
160
- indices = {index for index, row in sheet.row_dimensions.items() if row.hidden}
160
+ indices = {index for index, row in dict.items(sheet.row_dimensions) if row.hidden}
161
161
  else:
162
162
  indices = set()
163
163
  for column in sheet.column_dimensions.values():
@@ -5,6 +5,7 @@ from openpyxl.worksheet.worksheet import Worksheet
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from excel_mcp.errors import InvalidArgumentError
8
+ from excel_mcp.lazy_workbook import held_cells
8
9
  from excel_mcp.operations.cells import writable_cell
9
10
  from excel_mcp.refs import parse_cell
10
11
 
@@ -18,7 +19,7 @@ class NoteInfo(BaseModel):
18
19
  def list_notes(sheet: Worksheet) -> list[NoteInfo]:
19
20
  return [
20
21
  NoteInfo(cell=cell.coordinate, text=cell.comment.text, author=cell.comment.author or "")
21
- for _, cell in sorted(sheet._cells.items())
22
+ for _, cell in sorted(held_cells(sheet).items())
22
23
  if cell.comment
23
24
  ]
24
25
 
@@ -23,7 +23,7 @@ def register(tools: ToolRegistry, workspace: Workspace) -> None:
23
23
  """Describe a sheet: used range, panes, merges and everything on it (tables, charts,
24
24
  PivotTables, slicers, images, notes, hyperlinks, validation, conditional formats,
25
25
  sparklines...) with names and cells. Slow on very large files."""
26
- with workspace.read(path, with_package=True) as workbook:
26
+ with workspace.read(path) as workbook:
27
27
  return inspect.describe_sheet(get_sheet(workbook, sheet))
28
28
 
29
29
  @tools.writer("Create sheet")