excel-mcp-server 2.2.4__tar.gz → 2.2.6__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 (229) hide show
  1. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/PKG-INFO +6 -4
  2. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/README.md +5 -3
  3. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/pyproject.toml +1 -1
  4. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/pyproject.toml.orig +1 -1
  5. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts.py +15 -2
  6. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_check.py +1 -1
  7. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_data.py +30 -11
  8. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_modern.py +19 -11
  9. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_options.py +1 -0
  10. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/layout.py +12 -1
  11. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sheet_shapes.py +2 -1
  12. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/slicer_manage.py +18 -10
  13. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/slicer_place.py +5 -3
  14. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/slicer_xml.py +2 -2
  15. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/slicers.py +12 -1
  16. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/vba_write.py +0 -2
  17. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/capture_sheet.py +1 -1
  18. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/extensions.py +25 -1
  19. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/references.py +1 -1
  20. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/restore.py +4 -2
  21. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/restore_sheet.py +1 -1
  22. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/scan.py +1 -10
  23. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/object_tools.py +1 -0
  24. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/registry.py +25 -0
  25. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/values.py +1 -3
  26. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/workspace.py +15 -7
  27. excel_mcp_server-2.2.6/src/excel_mcp/xml_text.py +18 -0
  28. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/LICENSE +0 -0
  29. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/__init__.py +0 -0
  30. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/__main__.py +0 -0
  31. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/cells.py +0 -0
  32. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/criteria.py +0 -0
  33. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/dependencies.py +0 -0
  34. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/dynamic.py +0 -0
  35. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/engine.py +0 -0
  36. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/__init__.py +0 -0
  37. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/arrays.py +0 -0
  38. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/conditional.py +0 -0
  39. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/datetime.py +0 -0
  40. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/depreciation.py +0 -0
  41. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/distributions.py +0 -0
  42. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/financial.py +0 -0
  43. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/helpers.py +0 -0
  44. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/indirect.py +0 -0
  45. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/information.py +0 -0
  46. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/logical.py +0 -0
  47. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/lookup.py +0 -0
  48. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/math.py +0 -0
  49. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/regression.py +0 -0
  50. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/securities.py +0 -0
  51. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/statistical.py +0 -0
  52. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/subtotals.py +0 -0
  53. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/functions/text.py +0 -0
  54. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/operators.py +0 -0
  55. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/parser.py +0 -0
  56. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/r1c1.py +0 -0
  57. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/registry.py +0 -0
  58. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/scheduler.py +0 -0
  59. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/solver.py +0 -0
  60. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/steps.py +0 -0
  61. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/tablerefs.py +0 -0
  62. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/textformat.py +0 -0
  63. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/values.py +0 -0
  64. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/calc/wildcard.py +0 -0
  65. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/cell_budget.py +0 -0
  66. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/cfb.py +0 -0
  67. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/chart_roundtrip.py +0 -0
  68. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/cli.py +0 -0
  69. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/config.py +0 -0
  70. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/errors.py +0 -0
  71. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/formula_syntax.py +0 -0
  72. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/formulas.py +0 -0
  73. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/function_arguments.json +0 -0
  74. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/inputs.py +0 -0
  75. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/lazy_edit.py +0 -0
  76. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/lazy_find.py +0 -0
  77. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/lazy_lines.py +0 -0
  78. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/lazy_rows.py +0 -0
  79. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/lazy_shared.py +0 -0
  80. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/lazy_source.py +0 -0
  81. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/lazy_workbook.py +0 -0
  82. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/links.py +0 -0
  83. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/macros.py +0 -0
  84. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/__init__.py +0 -0
  85. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/calculated.py +0 -0
  86. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/cells.py +0 -0
  87. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chart_index.py +0 -0
  88. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chart_info.py +0 -0
  89. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex.py +0 -0
  90. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
  91. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
  92. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
  93. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
  94. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
  95. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
  96. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
  97. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/chartex_xml.py +0 -0
  98. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_axes.py +0 -0
  99. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_look.py +0 -0
  100. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_series.py +0 -0
  101. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/charts_style.py +0 -0
  102. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/columns.py +0 -0
  103. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/comparison.py +0 -0
  104. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/conditional.py +0 -0
  105. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/conditional_kinds.py +0 -0
  106. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/conditional_rule.py +0 -0
  107. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/conditional_x14.py +0 -0
  108. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/csv_export.py +0 -0
  109. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/csv_format.py +0 -0
  110. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/csv_import.py +0 -0
  111. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/csv_values.py +0 -0
  112. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/date_width.py +0 -0
  113. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/drawings.py +0 -0
  114. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/duplicates.py +0 -0
  115. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/files.py +0 -0
  116. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/fill.py +0 -0
  117. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/filter_criteria.py +0 -0
  118. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/filter_stored.py +0 -0
  119. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/filters.py +0 -0
  120. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/formatting.py +0 -0
  121. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/goal_seek.py +0 -0
  122. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/hyperlinks.py +0 -0
  123. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/images.py +0 -0
  124. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/inspect.py +0 -0
  125. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/line_edit_objects.py +0 -0
  126. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/line_edit_rules.py +0 -0
  127. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/line_edit_styles.py +0 -0
  128. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/line_edits.py +0 -0
  129. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/names.py +0 -0
  130. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/notes.py +0 -0
  131. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/paste.py +0 -0
  132. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot.py +0 -0
  133. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_axis.py +0 -0
  134. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_cache.py +0 -0
  135. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_calc.py +0 -0
  136. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_definition.py +0 -0
  137. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_fields.py +0 -0
  138. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_groups.py +0 -0
  139. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_index.py +0 -0
  140. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_options.py +0 -0
  141. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
  142. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_records.py +0 -0
  143. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_render.py +0 -0
  144. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_showas.py +0 -0
  145. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_source.py +0 -0
  146. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_specs.py +0 -0
  147. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/pivot_values.py +0 -0
  148. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/print_setup.py +0 -0
  149. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/protection.py +0 -0
  150. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/replace.py +0 -0
  151. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/rule_clear.py +0 -0
  152. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/rules.py +0 -0
  153. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sheet_copy.py +0 -0
  154. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sheet_counts.py +0 -0
  155. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sheet_refs.py +0 -0
  156. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sheet_view.py +0 -0
  157. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sheets.py +0 -0
  158. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/shifting.py +0 -0
  159. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/slicer_index.py +0 -0
  160. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/slicer_pivot.py +0 -0
  161. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/slicer_table.py +0 -0
  162. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sorting.py +0 -0
  163. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/spans.py +0 -0
  164. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sparkline_style.py +0 -0
  165. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sparkline_xml.py +0 -0
  166. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/sparklines.py +0 -0
  167. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/spill.py +0 -0
  168. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/table_options.py +0 -0
  169. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/tables.py +0 -0
  170. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/text_split.py +0 -0
  171. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/trace.py +0 -0
  172. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/transform.py +0 -0
  173. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/used_range.py +0 -0
  174. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/vba.py +0 -0
  175. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/workbook_rewrite.py +0 -0
  176. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/workbook_settings.py +0 -0
  177. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/operations/x14_xml.py +0 -0
  178. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/ovba.py +0 -0
  179. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/ovba_compress.py +0 -0
  180. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/ovba_write.py +0 -0
  181. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/__init__.py +0 -0
  182. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/anchors.py +0 -0
  183. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/arrays.py +0 -0
  184. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/capture.py +0 -0
  185. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/consistency.py +0 -0
  186. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/guards.py +0 -0
  187. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/lazy_records.py +0 -0
  188. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/lines.py +0 -0
  189. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/metadata.py +0 -0
  190. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/model.py +0 -0
  191. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/opc.py +0 -0
  192. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/patch.py +0 -0
  193. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/pivot_records.py +0 -0
  194. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/pivots.py +0 -0
  195. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/properties.py +0 -0
  196. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/reader.py +0 -0
  197. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/shape_names.py +0 -0
  198. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/package/vml.py +0 -0
  199. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/paths.py +0 -0
  200. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/plain_refs.py +0 -0
  201. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/py.typed +0 -0
  202. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/refs.py +0 -0
  203. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/rewrite.py +0 -0
  204. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/__init__.py +0 -0
  205. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/annotation_tools.py +0 -0
  206. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/areas.py +0 -0
  207. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/csv_tools.py +0 -0
  208. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/data_tools.py +0 -0
  209. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/format_tools.py +0 -0
  210. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/formula_tools.py +0 -0
  211. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/http.py +0 -0
  212. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/image_tools.py +0 -0
  213. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/params.py +0 -0
  214. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/pivot_tools.py +0 -0
  215. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/results.py +0 -0
  216. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/sheet_tools.py +0 -0
  217. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/slicer_tools.py +0 -0
  218. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/validation.py +0 -0
  219. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/vba_tools.py +0 -0
  220. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/vba_write_tools.py +0 -0
  221. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/server/workbook_tools.py +0 -0
  222. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/spill.py +0 -0
  223. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/structured.py +0 -0
  224. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/text.py +0 -0
  225. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/upload_links.py +0 -0
  226. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/upload_scan.py +0 -0
  227. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/xlfn.py +0 -0
  228. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/src/excel_mcp/xlm.py +0 -0
  229. {excel_mcp_server-2.2.4 → excel_mcp_server-2.2.6}/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.4
3
+ Version: 2.2.6
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
@@ -104,7 +104,8 @@ Excel**, and runs on Windows, macOS and Linux.
104
104
 
105
105
  **Macros (opt-in)**
106
106
  - Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
107
- `--allow-vba-write`; the server never runs macros
107
+ `--allow-vba-write`, which also decides whether `create_workbook` and `import_workbook` may
108
+ make `.xlsm`/`.xltm` files; the server never runs macros
108
109
 
109
110
  **Safe by design**
110
111
  - Optional folder confinement, a formula safety check, read-only mode, localhost-only HTTP by
@@ -323,8 +324,9 @@ next call can refer to it. Charts and images are selected by name, as listed by
323
324
 
324
325
  With `--allow-vba-write` (or `EXCEL_MCP_ALLOW_VBA_WRITE=1`) the server can set the code of
325
326
  standard, class, workbook and sheet modules in `.xlsm` and `.xltm` files, delete standard and
326
- class modules, and create new `.xlsm`/`.xltm` workbooks. The server only stores the code; it
327
- never runs it, and Excel asks before enabling macros. Digitally signed VBA projects are
327
+ class modules, and create or import `.xlsm`/`.xltm` workbooks (without the option both are
328
+ refused, and `import_workbook` removes macros from uploads to `.xlsx`/`.xltx`). The server only
329
+ stores the code; it never runs it, and Excel asks before enabling macros. Digitally signed VBA projects are
328
330
  refused, since any change would invalidate the signature.
329
331
 
330
332
  > **Warning:** macro code runs with your user's rights once you enable macros in Excel. A
@@ -68,7 +68,8 @@ Excel**, and runs on Windows, macOS and Linux.
68
68
 
69
69
  **Macros (opt-in)**
70
70
  - Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
71
- `--allow-vba-write`; the server never runs macros
71
+ `--allow-vba-write`, which also decides whether `create_workbook` and `import_workbook` may
72
+ make `.xlsm`/`.xltm` files; the server never runs macros
72
73
 
73
74
  **Safe by design**
74
75
  - Optional folder confinement, a formula safety check, read-only mode, localhost-only HTTP by
@@ -287,8 +288,9 @@ next call can refer to it. Charts and images are selected by name, as listed by
287
288
 
288
289
  With `--allow-vba-write` (or `EXCEL_MCP_ALLOW_VBA_WRITE=1`) the server can set the code of
289
290
  standard, class, workbook and sheet modules in `.xlsm` and `.xltm` files, delete standard and
290
- class modules, and create new `.xlsm`/`.xltm` workbooks. The server only stores the code; it
291
- never runs it, and Excel asks before enabling macros. Digitally signed VBA projects are
291
+ class modules, and create or import `.xlsm`/`.xltm` workbooks (without the option both are
292
+ refused, and `import_workbook` removes macros from uploads to `.xlsx`/`.xltx`). The server only
293
+ stores the code; it never runs it, and Excel asks before enabling macros. Digitally signed VBA projects are
292
294
  refused, since any change would invalidate the signature.
293
295
 
294
296
  > **Warning:** macro code runs with your user's rights once you enable macros in Excel. A
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.4"
3
+ version = "2.2.6"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.4"
3
+ version = "2.2.6"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -78,6 +78,7 @@ def create_chart(
78
78
  options: ChartOptions,
79
79
  replace: str | None,
80
80
  name: str | None,
81
+ max_cells: int,
81
82
  ) -> tuple[str | None, str | None]:
82
83
  """Add a chart (or replace the chart called `replace`); return its name and cells."""
83
84
  host = None if at is None else get_sheet(workbook, sheet)
@@ -96,7 +97,17 @@ def create_chart(
96
97
  f"{chart_type} charts read their data in columns; list rows with `series`."
97
98
  )
98
99
  return _create_modern(
99
- workbook, host, anchor, chart_type, source, series, categories, options, replace, name
100
+ workbook,
101
+ host,
102
+ anchor,
103
+ chart_type,
104
+ source,
105
+ series,
106
+ categories,
107
+ options,
108
+ replace,
109
+ name,
110
+ max_cells,
100
111
  )
101
112
  plots = resolve_series(
102
113
  workbook,
@@ -106,6 +117,7 @@ def create_chart(
106
117
  series_in,
107
118
  series,
108
119
  categories,
120
+ max_cells,
109
121
  )
110
122
  check_chart(plots, chart_type, options)
111
123
  chart = build_chart(plots, chart_type, options)
@@ -146,10 +158,11 @@ def _create_modern(
146
158
  options: ChartOptions,
147
159
  replace: str | None,
148
160
  name: str | None,
161
+ max_cells: int,
149
162
  ) -> tuple[str, str]:
150
163
  if host is None or anchor is None:
151
164
  raise InvalidArgumentError(f"A {chart_type} chart sits on the sheet: give at, e.g. 'E2'.")
152
- plots = resolve_modern(workbook, host.title, chart_type, source, series, categories)
165
+ plots = resolve_modern(workbook, host.title, chart_type, source, series, categories, max_cells)
153
166
  check_modern(plots, chart_type, options)
154
167
  index, name = _place(host, replace, name)
155
168
  if index is None:
@@ -3,6 +3,7 @@
3
3
  from excel_mcp.errors import InvalidArgumentError
4
4
  from excel_mcp.operations.charts_data import Plot
5
5
  from excel_mcp.operations.charts_options import (
6
+ MAX_SERIES,
6
7
  ROUND_TYPES,
7
8
  Axis,
8
9
  ChartOptions,
@@ -12,7 +13,6 @@ from excel_mcp.operations.charts_options import (
12
13
  )
13
14
  from excel_mcp.operations.formatting import parse_color
14
15
 
15
- MAX_SERIES = 255
16
16
  _COMBINABLE = ("column", "line", "area")
17
17
  _SECONDARY = ("column", "line", "area", "scatter")
18
18
  _GROUPED = ("column", "bar", "line", "area")
@@ -9,7 +9,7 @@ from openpyxl.chart.series import SeriesLabel
9
9
  from openpyxl.utils import absolute_coordinate, quote_sheetname
10
10
 
11
11
  from excel_mcp.errors import InvalidArgumentError
12
- from excel_mcp.operations.charts_options import ChartType, SeriesSpec
12
+ from excel_mcp.operations.charts_options import MAX_SERIES, ChartType, SeriesSpec
13
13
  from excel_mcp.refs import CellRange, parse_cell, parse_range
14
14
  from excel_mcp.workspace import get_sheet
15
15
 
@@ -36,6 +36,7 @@ def resolve_series(
36
36
  series_in: SeriesIn,
37
37
  series: list[SeriesSpec],
38
38
  categories: str | None,
39
+ max_cells: int,
39
40
  ) -> list[Plot]:
40
41
  if (source is None) == (not series):
41
42
  raise InvalidArgumentError(
@@ -43,24 +44,34 @@ def resolve_series(
43
44
  "and not neither."
44
45
  )
45
46
  if source is not None:
46
- series = _block_series(workbook, sheet, source, chart_type, series_in)
47
+ series = _block_series(workbook, sheet, source, chart_type, series_in, max_cells)
47
48
  categories = None
48
- return [_resolve(workbook, sheet, item, categories) for item in series]
49
+ check_series_count(len(series))
50
+ return [_resolve(workbook, sheet, item, categories, max_cells) for item in series]
49
51
 
50
52
 
51
- def _resolve(workbook: Workbook, sheet: str, spec: SeriesSpec, categories: str | None) -> Plot:
52
- values, points = one_line(workbook, sheet, spec.values, "values")
53
+ def _resolve(
54
+ workbook: Workbook, sheet: str, spec: SeriesSpec, categories: str | None, max_cells: int
55
+ ) -> Plot:
56
+ values, points = one_line(workbook, sheet, spec.values, "values", max_cells)
53
57
  labels = spec.categories or categories
54
58
  return Plot(
55
59
  spec=spec,
56
60
  values=values,
57
- categories=one_line(workbook, sheet, labels, "categories")[0] if labels else None,
58
- sizes=one_line(workbook, sheet, spec.sizes, "sizes")[0] if spec.sizes else None,
61
+ categories=one_line(workbook, sheet, labels, "categories", max_cells)[0]
62
+ if labels
63
+ else None,
64
+ sizes=one_line(workbook, sheet, spec.sizes, "sizes", max_cells)[0] if spec.sizes else None,
59
65
  name=series_name(workbook, spec.name),
60
66
  points=points,
61
67
  )
62
68
 
63
69
 
70
+ def check_series_count(count: int) -> None:
71
+ if count > MAX_SERIES:
72
+ raise InvalidArgumentError(f"A chart has at most {MAX_SERIES} series, got {count}.")
73
+
74
+
64
75
  def split_sheet(workbook: Workbook, default: str, text: str) -> tuple[str, str]:
65
76
  """Split 'Data!B2:B9' (or 'My Sheet'!B2:B9) into the worksheet's title and the cells."""
66
77
  qualifier, mark, cells = text.rpartition("!")
@@ -74,10 +85,12 @@ def qualified(sheet: str, area: CellRange) -> str:
74
85
  return f"{quote_sheetname(sheet)}!{absolute_coordinate(str(area))}"
75
86
 
76
87
 
77
- def one_line(workbook: Workbook, default: str, text: str, what: str) -> tuple[str, int]:
88
+ def one_line(
89
+ workbook: Workbook, default: str, text: str, what: str, max_cells: int
90
+ ) -> tuple[str, int]:
78
91
  """A one-row or one-column range as an absolute formula, and its number of cells."""
79
92
  sheet, cells = split_sheet(workbook, default, text)
80
- area = parse_range(cells)
93
+ area = parse_range(cells).within(max_cells)
81
94
  if area.rows > 1 and area.cols > 1:
82
95
  raise InvalidArgumentError(
83
96
  f"Series {what} must be one row or one column, got {text!r}. List one series per "
@@ -99,11 +112,16 @@ def series_name(workbook: Workbook, text: str | None) -> SeriesLabel | None:
99
112
 
100
113
 
101
114
  def _block_series(
102
- workbook: Workbook, default: str, source: str, chart_type: ChartType, series_in: SeriesIn
115
+ workbook: Workbook,
116
+ default: str,
117
+ source: str,
118
+ chart_type: ChartType,
119
+ series_in: SeriesIn,
120
+ max_cells: int,
103
121
  ) -> list[SeriesSpec]:
104
122
  """Series for a block with a header line and a label line, by columns or by rows."""
105
123
  sheet, cells = split_sheet(workbook, default, source)
106
- area = parse_range(cells)
124
+ area = parse_range(cells).within(max_cells)
107
125
  if area.rows < 2 or area.cols < 2:
108
126
  raise InvalidArgumentError(
109
127
  "source needs a header row and a label column plus at least one series, "
@@ -112,6 +130,7 @@ def _block_series(
112
130
  by_columns = series_in == "columns"
113
131
  lines = area.cols if by_columns else area.rows
114
132
  points = (area.rows if by_columns else area.cols) - 1
133
+ check_series_count(lines - 1)
115
134
 
116
135
  def segment(line: int, start: int, stop: int) -> str:
117
136
  """Cells `start` to `stop` along line number `line`, the label line being 0."""
@@ -5,9 +5,10 @@ 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.operations.charts_check import MAX_SERIES, check_labels, require
8
+ from excel_mcp.operations.charts_check import check_labels, require
9
9
  from excel_mcp.operations.charts_data import (
10
10
  Plot,
11
+ check_series_count,
11
12
  one_line,
12
13
  qualified,
13
14
  series_name,
@@ -51,6 +52,7 @@ def resolve_modern(
51
52
  source: str | None,
52
53
  series: list[SeriesSpec],
53
54
  categories: str | None,
55
+ max_cells: int,
54
56
  ) -> list[Plot]:
55
57
  if (source is None) == (not series):
56
58
  raise InvalidArgumentError(
@@ -58,33 +60,41 @@ def resolve_modern(
58
60
  "and not neither."
59
61
  )
60
62
  if source is not None:
61
- series, categories = _block(workbook, sheet, chart_type, source), None
62
- plots = [_plot(workbook, sheet, chart_type, spec, categories) for spec in series]
63
+ series, categories = _block(workbook, sheet, chart_type, source, max_cells), None
64
+ check_series_count(len(series))
65
+ plots = [_plot(workbook, sheet, chart_type, spec, categories, max_cells) for spec in series]
63
66
  if chart_type not in _MANY and len(plots) != 1:
64
67
  raise InvalidArgumentError(f"A {chart_type} chart plots one series, got {len(plots)}.")
65
68
  return plots
66
69
 
67
70
 
68
71
  def _plot(
69
- workbook: Workbook, sheet: str, chart_type: str, spec: SeriesSpec, categories: str | None
72
+ workbook: Workbook,
73
+ sheet: str,
74
+ chart_type: str,
75
+ spec: SeriesSpec,
76
+ categories: str | None,
77
+ max_cells: int,
70
78
  ) -> Plot:
71
- values, points = one_line(workbook, sheet, spec.values, "values")
79
+ values, points = one_line(workbook, sheet, spec.values, "values", max_cells)
72
80
  labels = spec.categories or categories
73
81
  if labels is None:
74
82
  found = None
75
83
  elif chart_type in _HIERARCHIES:
76
84
  title, cells = split_sheet(workbook, sheet, labels)
77
- found = qualified(title, parse_range(cells))
85
+ found = qualified(title, parse_range(cells).within(max_cells))
78
86
  else:
79
- found = one_line(workbook, sheet, labels, "categories")[0]
87
+ found = one_line(workbook, sheet, labels, "categories", max_cells)[0]
80
88
  return Plot(spec, values, found, None, series_name(workbook, spec.name), points)
81
89
 
82
90
 
83
- def _block(workbook: Workbook, default: str, chart_type: str, source: str) -> list[SeriesSpec]:
91
+ def _block(
92
+ workbook: Workbook, default: str, chart_type: str, source: str, max_cells: int
93
+ ) -> list[SeriesSpec]:
84
94
  """Series for a block with a header row, read as Excel reads it: leading text columns are
85
95
  labels (one, or the levels of a treemap or sunburst), the number columns are the series."""
86
96
  sheet, cells = split_sheet(workbook, default, source)
87
- area = parse_range(cells)
97
+ area = parse_range(cells).within(max_cells)
88
98
  if area.rows < 2:
89
99
  raise InvalidArgumentError("source needs a header row and at least one row of data.")
90
100
  labels = _text_columns(workbook[sheet], area)
@@ -120,8 +130,6 @@ def _text_columns(sheet: Worksheet, area: CellRange) -> int:
120
130
 
121
131
 
122
132
  def check_modern(plots: list[Plot], chart_type: ChartType, options: ChartOptions) -> None:
123
- if len(plots) > MAX_SERIES:
124
- raise InvalidArgumentError(f"A chart has at most {MAX_SERIES} series, got {len(plots)}.")
125
133
  _check_options(options, chart_type)
126
134
  for number, plot in enumerate(plots, start=1):
127
135
  try:
@@ -10,6 +10,7 @@ ChartType = Literal[
10
10
  "column", "bar", "line", "area", "pie", "doughnut", "radar", "scatter", "bubble",
11
11
  "waterfall", "histogram", "pareto", "box_whisker", "treemap", "sunburst", "funnel",
12
12
  ] # fmt: skip
13
+ MAX_SERIES = 255
13
14
  SeriesType = Literal["column", "line", "area"]
14
15
  Grouping = Literal["standard", "stacked", "percent_stacked"]
15
16
  LegendPosition = Literal["right", "left", "top", "bottom", "none"]
@@ -16,7 +16,7 @@ from excel_mcp.operations.formatting import parse_color
16
16
  from excel_mcp.operations.print_setup import PrintSetup, apply_print_setup
17
17
  from excel_mcp.operations.protection import Protection, apply_protection
18
18
  from excel_mcp.operations.sheet_view import ViewOptions, apply_view, move_sheet
19
- from excel_mcp.operations.spans import parse_span, to_spans
19
+ from excel_mcp.operations.spans import MAX_ROW_SPAN, parse_span, to_spans
20
20
  from excel_mcp.package.lines import Axis
21
21
  from excel_mcp.refs import MAX_ROW, parse_cell
22
22
 
@@ -88,6 +88,7 @@ def apply_layout(
88
88
  apply_auto_filter(sheet, layout.auto_filter, formula_values, max_cells)
89
89
  if layout.tab_color is not None:
90
90
  sheet.sheet_properties.tabColor = parse_color(layout.tab_color)
91
+ check_row_spans(layout.rows or [])
91
92
  for action in layout.rows or []:
92
93
  change_lines(sheet, "rows", action)
93
94
  for action in layout.columns or []:
@@ -102,6 +103,16 @@ def apply_layout(
102
103
  apply_view(sheet, layout.view)
103
104
 
104
105
 
106
+ def check_row_spans(actions: list[LineAction]) -> None:
107
+ """Every row of a span gets an object, so the spans of one call share the span limit."""
108
+ total = 0
109
+ for action in actions:
110
+ first, last = parse_span(action.span, "rows")
111
+ total += last - first + 1
112
+ if total > MAX_ROW_SPAN:
113
+ raise InvalidArgumentError(f"One call can change at most {MAX_ROW_SPAN:,} rows.")
114
+
115
+
105
116
  def split_column_dimensions(sheet: Worksheet) -> None:
106
117
  """Give every column of a multi-column definition its own, as openpyxl expects.
107
118
 
@@ -4,6 +4,7 @@ connectors, groups and form controls."""
4
4
  import re
5
5
  import uuid
6
6
  from collections.abc import Callable
7
+ from html import unescape
7
8
  from xml.sax.saxutils import escape, quoteattr
8
9
 
9
10
  from openpyxl.workbook import Workbook
@@ -15,7 +16,7 @@ from excel_mcp.package import PackageState, Part, SheetPackage, state_of
15
16
  from excel_mcp.package.consistency import THREADED_COMMENTS
16
17
  from excel_mcp.package.extensions import CONDITIONAL_FORMATS, SPARKLINES
17
18
  from excel_mcp.package.model import cloned
18
- from excel_mcp.package.scan import relationship_ids, unescape
19
+ from excel_mcp.package.scan import relationship_ids
19
20
 
20
21
  # Charts of Excel 2016 and slicers are copied with the parts they need, by their own code.
21
22
  _COPIED_ELSEWHERE = (
@@ -2,7 +2,6 @@
2
2
 
3
3
  import re
4
4
  import uuid
5
- from xml.sax.saxutils import quoteattr
6
5
 
7
6
  from openpyxl.pivot.table import TableDefinition
8
7
  from openpyxl.workbook import Workbook
@@ -29,7 +28,9 @@ from excel_mcp.operations.slicer_index import (
29
28
  from excel_mcp.operations.slicer_place import add_cache, add_entry, next_shape_id
30
29
  from excel_mcp.package import state_of
31
30
  from excel_mcp.package.anchors import Corner
31
+ from excel_mcp.package.scan import attributes
32
32
  from excel_mcp.text import quoted
33
+ from excel_mcp.xml_text import attribute
33
34
 
34
35
 
35
36
  class SlicerInfo(BaseModel):
@@ -112,10 +113,12 @@ def placed_range(workbook: Workbook, sheet: Worksheet, name: str) -> str | None:
112
113
 
113
114
 
114
115
  def _anchor_of(anchors: list[str], name: str) -> str | None:
115
- wanted = f'name="{name}"'
116
- return next(
117
- (a for a in anchors if re.search(rf"<\w+:(?:slicer|timeslicer)\b[^>]*{wanted}", a)), None
118
- )
116
+ return next((a for a in anchors if _shape_name(a) == name), None)
117
+
118
+
119
+ def _shape_name(anchor: str) -> str | None:
120
+ tag = re.search(r"<\w+:(?:slicer|timeslicer)\b[^>]*>", anchor)
121
+ return attributes(tag[0]).get("name") if tag else None
119
122
 
120
123
 
121
124
  def delete_slicer(
@@ -141,7 +144,11 @@ def remove_entry(workbook: Workbook, entry: Entry) -> None:
141
144
  """Take a slicer or timeline out of its sheet; its cache goes when nothing uses it."""
142
145
  package = state_of(workbook).sheet(entry.sheet)
143
146
  text = entry.part.data.decode("utf-8")
144
- text = re.sub(rf'<(?:slicer|timeline)\b[^>]*\bname="{re.escape(entry.name)}"[^>]*/>', "", text)
147
+ text = re.sub(
148
+ r"<(?:slicer|timeline)\b[^>]*/>",
149
+ lambda tag: "" if attributes(tag[0]).get("name") == entry.name else tag[0],
150
+ text,
151
+ )
145
152
  entry.part.data = text.encode("utf-8")
146
153
  if not xml.read_entries(text):
147
154
  package.links.remove(entry.link)
@@ -200,6 +207,7 @@ __all__ = ["Corner", "SlicerInfo", "delete_slicer", "list_slicers", "sheet_of"]
200
207
  # -- copying a sheet ---------------------------------------------------------------------------
201
208
 
202
209
  _SHAPE_ID = re.compile(r'(<xdr:cNvPr\b[^>]*?\bid=")\d+(")')
210
+ _SHAPE_NAME = re.compile(r'(<xdr:cNvPr\b[^>]*?\bname=)"[^"]*"')
203
211
  _GUID = re.compile(r'(<a16:creationId\b[^>]*?\bid=")[^"]*(")')
204
212
  _UID = re.compile(r'(\b\w+:uid=")[^"]*(")')
205
213
 
@@ -236,7 +244,7 @@ def copy_slicers(
236
244
  def _join_cache(workbook: Workbook, name: str, pivots: list[tuple[int, str]], tab: int) -> None:
237
245
  link = caches(workbook)[name][0]
238
246
  text = link.target.data.decode("utf-8") # pyright: ignore[reportAttributeAccessIssue]
239
- added = "".join(f'<pivotTable tabId="{tab}" name="{p}"/>' for _, p in pivots)
247
+ added = "".join(f'<pivotTable tabId="{tab}" name={attribute(p)}/>' for _, p in pivots)
240
248
  link.target.data = text.replace("</pivotTables>", added + "</pivotTables>").encode("utf-8") # pyright: ignore[reportAttributeAccessIssue]
241
249
 
242
250
 
@@ -259,7 +267,7 @@ def _copy_cache(
259
267
  number += 1
260
268
  new = f"{stem}{number}"
261
269
  text = link.target.data.decode("utf-8") # pyright: ignore[reportAttributeAccessIssue]
262
- text = text.replace(f'name="{name}"', f'name="{new}"', 1)
270
+ text = re.sub(r'\bname="[^"]*"', lambda _: f"name={attribute(new)}", text, count=1)
263
271
  text = _UID.sub(lambda m: m[1] + _new_guid() + m[2], text, count=1)
264
272
  if tab_id is not None:
265
273
  text = re.sub(r'(<pivotTable\b[^>]*?\btabId=")\d+', rf"\g<1>{tab_id}", text)
@@ -281,11 +289,11 @@ def _copy_entry(workbook: Workbook, entry: Entry, target: Worksheet, cache: str,
281
289
  "name": name,
282
290
  "cache": cache,
283
291
  }
284
- tag = entry.kind + "".join(f" {k}={quoteattr(v)}" for k, v in values.items()) + "/>"
292
+ tag = entry.kind + "".join(f" {k}={attribute(v)}" for k, v in values.items()) + "/>"
285
293
  package = state_of(workbook).sheet(entry.sheet)
286
294
  shape = _anchor_of(package.anchors, entry.name)
287
295
  assert shape is not None
288
- shape = shape.replace(f'name="{entry.name}"', f'name="{name}"')
296
+ shape = _SHAPE_NAME.sub(lambda m: f"{m[1]}{attribute(name)}", shape, count=1)
289
297
  shape = _SHAPE_ID.sub(
290
298
  rf"\g<1>{next_shape_id(state_of(workbook).sheet(target))}\g<2>", shape, count=1
291
299
  )
@@ -18,6 +18,7 @@ from excel_mcp.package.scan import (
18
18
  root_scope,
19
19
  used_prefixes,
20
20
  )
21
+ from excel_mcp.xml_text import attribute
21
22
 
22
23
  _XDR = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
23
24
  _A = "http://schemas.openxmlformats.org/drawingml/2006/main"
@@ -154,12 +155,13 @@ def anchor(
154
155
  end_col, col_off = _end(geometry, False, column, left + size[0], left)
155
156
  end_row, row_off = _end(geometry, True, row, top + size[1], top)
156
157
  timeline = kind == "timeline"
158
+ shape_name = attribute(name)
157
159
  requires = "tsle" if timeline else "sle15" if kind == "table" else "a14"
158
160
  declared = {"tsle": _TSLE, "sle15": _SLE15, "a14": _A14}[requires]
159
161
  graphic = (
160
- f'<tsle:timeslicer xmlns:tsle="{_TSLE}" name="{name}"/>'
162
+ f'<tsle:timeslicer xmlns:tsle="{_TSLE}" name={shape_name}/>'
161
163
  if timeline
162
- else f'<sle:slicer xmlns:sle="{_SLE}" name="{name}"/>'
164
+ else f'<sle:slicer xmlns:sle="{_SLE}" name={shape_name}/>'
163
165
  )
164
166
  uri = _TSLE if timeline else _SLE
165
167
  guid = "{" + str(uuid.uuid4()).upper() + "}"
@@ -173,7 +175,7 @@ def anchor(
173
175
  f'<mc:AlternateContent xmlns:mc="{xml.MC}">'
174
176
  f'<mc:Choice xmlns:{requires}="{declared}" Requires="{requires}">'
175
177
  f'<xdr:graphicFrame macro=""><xdr:nvGraphicFramePr>'
176
- f'<xdr:cNvPr id="{shape_id}" name="{name}"><a:extLst>'
178
+ f'<xdr:cNvPr id="{shape_id}" name={shape_name}><a:extLst>'
177
179
  f'<a:ext uri="{{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}}">'
178
180
  f'<a16:creationId xmlns:a16="{_A16}" id="{guid}"/></a:ext></a:extLst></xdr:cNvPr>'
179
181
  f"<xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr>"
@@ -3,10 +3,10 @@
3
3
  import datetime as dt
4
4
  import re
5
5
  from dataclasses import dataclass
6
- from xml.sax.saxutils import quoteattr
7
6
 
8
7
  from excel_mcp.package.opc import XML_DECLARATION
9
8
  from excel_mcp.package.scan import attributes
9
+ from excel_mcp.xml_text import attribute
10
10
 
11
11
  X14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"
12
12
  X15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"
@@ -47,7 +47,7 @@ class CacheOptions:
47
47
 
48
48
 
49
49
  def _attrs(**values: object) -> str:
50
- return "".join(f" {k}={quoteattr(str(v))}" for k, v in values.items() if v is not None)
50
+ return "".join(f" {k}={attribute(str(v))}" for k, v in values.items() if v is not None)
51
51
 
52
52
 
53
53
  def _date(moment: dt.date | dt.datetime) -> str:
@@ -31,6 +31,7 @@ from excel_mcp.package import state_of
31
31
  from excel_mcp.refs import parse_cell
32
32
 
33
33
  EMU_PER_CM = 360_000
34
+ MAX_TEXT_LENGTH = 255
34
35
  _SLICER_SIZE = (1_828_800, 2_667_000)
35
36
  _TIMELINE_SIZE = (3_333_750, 1_371_600)
36
37
 
@@ -71,6 +72,8 @@ def add_slicer(
71
72
  ) -> tuple[str, bool]:
72
73
  """Add the slicer or timeline to ``sheet``; returns its name, and whether the figures of
73
74
  PivotTables it limits were left for Excel to recalculate when it opens the file."""
75
+ if request.caption:
76
+ _check_text(request.caption, "caption")
74
77
  owner, target = find_source(workbook, request.source.sheet, request.source.name)
75
78
  if isinstance(target, Table):
76
79
  if request.timeline or request.connect:
@@ -235,6 +238,7 @@ def _names(workbook: Workbook, request: SlicerRequest, stem: str) -> tuple[str,
235
238
  """The slicer's name and its cache's, unique in the workbook as Excel makes them."""
236
239
  taken = {entry.name.casefold() for entry in entries(workbook)}
237
240
  if request.name is not None:
241
+ _check_text(request.name, "name")
238
242
  if request.name.casefold() in taken:
239
243
  raise InvalidArgumentError(f"A slicer or timeline named {request.name!r} exists.")
240
244
  name = request.name
@@ -243,7 +247,8 @@ def _names(workbook: Workbook, request: SlicerRequest, stem: str) -> tuple[str,
243
247
  while name.casefold() in taken:
244
248
  number += 1
245
249
  name = f"{request.field} {number}"
246
- stem += re.sub(r"\W", "_", request.field).rstrip("_")
250
+ # Excel keeps letters, digits and . \ ? in a cache name, and a defined name is at most 255.
251
+ stem += re.sub(r"[^\w.\\?]", "_", request.field).rstrip("_")[: MAX_TEXT_LENGTH - 20]
247
252
  used = {n.casefold() for n in workbook.defined_names} | {c.casefold() for c in caches(workbook)}
248
253
  cache, number = stem, 0
249
254
  while cache.casefold() in used:
@@ -252,6 +257,12 @@ def _names(workbook: Workbook, request: SlicerRequest, stem: str) -> tuple[str,
252
257
  return name, cache
253
258
 
254
259
 
260
+ def _check_text(text: str, what: str) -> None:
261
+ """Excel takes any slicer name, caption and cache name of 1 to 255 characters."""
262
+ if not text.strip() or len(text) > MAX_TEXT_LENGTH:
263
+ raise InvalidArgumentError(f"A slicer's {what} must be 1 to {MAX_TEXT_LENGTH} characters.")
264
+
265
+
255
266
  def _check_style(style: str | None, kind: str) -> None:
256
267
  wanted = "TimeSlicerStyle" if kind == "timeline" else "SlicerStyle"
257
268
  if style is not None and not re.fullmatch(wanted + r"(Light|Dark|Other)\d", style):
@@ -30,8 +30,6 @@ def write_module(
30
30
  raise InvalidArgumentError(
31
31
  "Code lines cannot start with 'Attribute': the server writes module attributes itself."
32
32
  )
33
- if "\0" in code:
34
- raise InvalidArgumentError("The code contains a NUL character.")
35
33
  project = _project(workbook)
36
34
  existing = project.find(name)
37
35
  if existing is None and not _MODULE_NAME.fullmatch(name):
@@ -1,6 +1,7 @@
1
1
  """Reading from the original file what openpyxl is going to drop from a worksheet."""
2
2
 
3
3
  import re
4
+ from html import unescape
4
5
  from typing import Literal
5
6
  from xml.etree import ElementTree
6
7
 
@@ -21,7 +22,6 @@ from excel_mcp.package.scan import (
21
22
  relationship_ids,
22
23
  scan,
23
24
  scan_fragment,
24
- unescape,
25
25
  with_namespaces,
26
26
  )
27
27
  from excel_mcp.package.shape_names import anchored_name, restore_names
@@ -7,6 +7,7 @@ in the sheet proper. They hold ranges and formulas that must follow the cells th
7
7
  import re
8
8
  import uuid
9
9
  from collections.abc import Callable
10
+ from html import unescape
10
11
  from typing import Protocol
11
12
  from xml.sax.saxutils import escape
12
13
 
@@ -14,7 +15,6 @@ from openpyxl.utils.cell import column_index_from_string, get_column_letter
14
15
 
15
16
  from excel_mcp.errors import WorkbookError
16
17
  from excel_mcp.package.lines import LineEdit
17
- from excel_mcp.package.scan import unescape
18
18
  from excel_mcp.refs import CellRange, clip_areas, parse_range
19
19
 
20
20
  SPARKLINES = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"
@@ -24,6 +24,30 @@ DATA_VALIDATIONS = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"
24
24
  RULE_ID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
25
25
 
26
26
 
27
+ # The order Excel writes the entries it knows of in an <extLst>: in the workbook, the slicer
28
+ # and timeline caches and the workbook properties; in a sheet, the lists of slicers, table
29
+ # slicers and timelines. Excel does not open a file with them in another order.
30
+ _EXCEL_ORDER = [
31
+ "{BBE1A952-AA13-448e-AADC-164F8A28A991}",
32
+ "{79F54976-1DA5-4618-B147-4CDE4B953A38}",
33
+ "{D0CA8CA8-9F24-4464-BF8E-62219DCF47F9}",
34
+ "{46BE6895-7355-4a93-B00E-2C351335B9C9}",
35
+ "{140A7094-0E35-4892-8432-C4D2E57EDEB5}",
36
+ "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}",
37
+ "{A8765BA9-456A-4dab-B4F3-ACF838C121DE}",
38
+ "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}",
39
+ "{7E03D99C-DC04-49d9-9315-930204A7B6E9}",
40
+ ]
41
+
42
+
43
+ def in_excel_order(extensions: dict[str, str]) -> dict[str, str]:
44
+ """The ``<ext>`` entries of an ``<extLst>`` with those that Excel orders in its order;
45
+ the others stay where they are."""
46
+ known = iter(sorted((u for u in extensions if u in _EXCEL_ORDER), key=_EXCEL_ORDER.index))
47
+ uris = [next(known) if uri in _EXCEL_ORDER else uri for uri in extensions]
48
+ return {uri: extensions[uri] for uri in uris}
49
+
50
+
27
51
  class Rewriter(Protocol):
28
52
  """What the rewriting of references needs to know; see `package.references`."""
29
53
 
@@ -21,6 +21,7 @@ and panes of custom sheet views, and consolidation sources.
21
21
 
22
22
  import re
23
23
  from collections.abc import Callable
24
+ from html import unescape
24
25
  from xml.sax.saxutils import escape
25
26
 
26
27
  from openpyxl import Workbook
@@ -31,7 +32,6 @@ from excel_mcp.package import extensions
31
32
  from excel_mcp.package.anchors import Geometry, Moved, move_anchors, move_vml_shape
32
33
  from excel_mcp.package.lines import LineEdit
33
34
  from excel_mcp.package.model import Part, SheetPackage, state_of
34
- from excel_mcp.package.scan import unescape
35
35
  from excel_mcp.refs import parse_range
36
36
 
37
37
  Formula = Callable[[str, str], str]