Skip to main content

excel_mcp_server/
server.rs

1//! ExcelMcpServer — 43 consolidated MCP tools backed by zavora-xlsx.
2
3use std::sync::Arc;
4use tokio::sync::RwLock;
5
6use rmcp::{
7    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
8    model::{ServerCapabilities, ServerInfo},
9    tool, tool_handler, tool_router, ServerHandler,
10};
11
12use crate::store::WorkbookStore;
13use crate::tools;
14use crate::types::inputs::*;
15
16#[derive(Debug, Clone)]
17pub struct ExcelMcpServer {
18    tool_router: ToolRouter<Self>,
19    store: Arc<RwLock<WorkbookStore>>,
20}
21
22impl ExcelMcpServer {
23    pub fn new(store: Arc<RwLock<WorkbookStore>>) -> Self {
24        Self {
25            tool_router: Self::tool_router(),
26            store,
27        }
28    }
29}
30
31fn unexpected_error(e: anyhow::Error) -> String {
32    crate::types::responses::error(
33        crate::types::responses::ErrorCategory::IoError,
34        &format!("Unexpected error: {}", e),
35        "Please try again.",
36    )
37}
38
39macro_rules! tool_fn {
40    ($store:expr, $module:path, $input:expr) => {{
41        let mut store = $store.write().await;
42        match $module(&mut store, $input) {
43            Ok(j) => j,
44            Err(e) => unexpected_error(e),
45        }
46    }};
47}
48
49#[tool_router]
50impl ExcelMcpServer {
51    // ── Workbook lifecycle (4) ──
52
53    #[tool(description = "Create a new empty Excel workbook in memory")]
54    async fn create_workbook(&self, _p: Parameters<CreateWorkbookInput>) -> String {
55        let mut store = self.store.write().await;
56        match tools::workbook::create_workbook(&mut store) {
57            Ok(j) => j,
58            Err(e) => unexpected_error(e),
59        }
60    }
61
62    #[tool(description = "Open an existing Excel file for reading or editing")]
63    async fn open_workbook(&self, Parameters(i): Parameters<OpenWorkbookInput>) -> String {
64        tool_fn!(self.store, tools::workbook::open_workbook, i)
65    }
66
67    #[tool(description = "Save a workbook to disk as an xlsx file")]
68    async fn save_workbook(&self, Parameters(i): Parameters<SaveWorkbookInput>) -> String {
69        tool_fn!(self.store, tools::workbook::save_workbook, i)
70    }
71
72    #[tool(description = "Close a workbook and free its memory")]
73    async fn close_workbook(&self, Parameters(i): Parameters<CloseWorkbookInput>) -> String {
74        tool_fn!(self.store, tools::workbook::close_workbook, i)
75    }
76
77    // ── Workbook configuration (1) ──
78
79    #[tool(
80        description = "Configure workbook: set calc mode (auto/manual), active sheet, document properties (title, author, company)"
81    )]
82    async fn configure_workbook(
83        &self,
84        Parameters(i): Parameters<ConfigureWorkbookInput>,
85    ) -> String {
86        tool_fn!(self.store, tools::expanded::configure_workbook, i)
87    }
88
89    // ── Sheet management (7) ──
90
91    #[tool(description = "List all sheet names in a workbook")]
92    async fn list_sheets(&self, Parameters(i): Parameters<ListSheetsInput>) -> String {
93        tool_fn!(self.store, tools::sheets::list_sheets, i)
94    }
95
96    #[tool(description = "Get the dimensions of a sheet (used range, row count, column count)")]
97    async fn get_sheet_dimensions(
98        &self,
99        Parameters(i): Parameters<GetSheetDimensionsInput>,
100    ) -> String {
101        tool_fn!(self.store, tools::sheets::get_sheet_dimensions, i)
102    }
103
104    #[tool(
105        description = "Describe a workbook's structure including sheet names, dimensions, and sample data"
106    )]
107    async fn describe_workbook(&self, Parameters(i): Parameters<DescribeWorkbookInput>) -> String {
108        tool_fn!(self.store, tools::sheets::describe_workbook, i)
109    }
110
111    #[tool(description = "Add a new empty worksheet to a workbook")]
112    async fn add_sheet(&self, Parameters(i): Parameters<AddSheetInput>) -> String {
113        tool_fn!(self.store, tools::sheets::add_sheet, i)
114    }
115
116    #[tool(description = "Rename an existing worksheet")]
117    async fn rename_sheet(&self, Parameters(i): Parameters<RenameSheetInput>) -> String {
118        tool_fn!(self.store, tools::sheets::rename_sheet, i)
119    }
120
121    #[tool(description = "Delete a worksheet from a workbook")]
122    async fn delete_sheet(&self, Parameters(i): Parameters<DeleteSheetInput>) -> String {
123        tool_fn!(self.store, tools::sheets::delete_sheet, i)
124    }
125
126    #[tool(description = "Move a worksheet to a different position in the workbook")]
127    async fn move_worksheet(&self, Parameters(i): Parameters<MoveWorksheetInput>) -> String {
128        tool_fn!(self.store, tools::expanded::move_worksheet, i)
129    }
130
131    // ── Read (4) ──
132
133    #[tool(description = "Read data from a worksheet with optional range and pagination")]
134    async fn read_sheet(&self, Parameters(i): Parameters<ReadSheetInput>) -> String {
135        tool_fn!(self.store, tools::read::read_sheet, i)
136    }
137
138    #[tool(description = "Read a single cell's value, type, and formula")]
139    async fn read_cell(&self, Parameters(i): Parameters<ReadCellInput>) -> String {
140        tool_fn!(self.store, tools::read::read_cell, i)
141    }
142
143    #[tool(description = "Search for cells matching a value or pattern across sheets")]
144    async fn search_cells(&self, Parameters(i): Parameters<SearchCellsInput>) -> String {
145        tool_fn!(self.store, tools::read::search_cells, i)
146    }
147
148    #[tool(description = "Export a sheet as a CSV-formatted string")]
149    async fn sheet_to_csv(&self, Parameters(i): Parameters<SheetToCsvInput>) -> String {
150        tool_fn!(self.store, tools::read::sheet_to_csv, i)
151    }
152
153    // ── Write (4) ──
154
155    #[tool(
156        description = "Write values to multiple cells. Strings starting with '=' are formulas. Numbers, booleans, ISO dates auto-detected."
157    )]
158    async fn write_cells(&self, Parameters(i): Parameters<WriteCellsInput>) -> String {
159        tool_fn!(self.store, tools::write::write_cells, i)
160    }
161
162    #[tool(description = "Write a row of values starting from a cell, filling rightward")]
163    async fn write_row(&self, Parameters(i): Parameters<WriteRowInput>) -> String {
164        tool_fn!(self.store, tools::write::write_row, i)
165    }
166
167    #[tool(description = "Write a column of values starting from a cell, filling downward")]
168    async fn write_column(&self, Parameters(i): Parameters<WriteColumnInput>) -> String {
169        tool_fn!(self.store, tools::write::write_column, i)
170    }
171
172    #[tool(description = "Write rich text (mixed bold/italic/color runs) to a cell")]
173    async fn write_rich_text(&self, Parameters(i): Parameters<WriteRichTextInput>) -> String {
174        tool_fn!(self.store, tools::expanded::write_rich_text, i)
175    }
176
177    // ── Formulas (1) ──
178
179    #[tool(
180        description = "Write a formula. Set formula_type to 'array' for CSE array formulas (cell = range), 'dynamic' for Excel 365 spill formulas, or omit for regular. Optional cached_result."
181    )]
182    async fn write_formula(
183        &self,
184        Parameters(i): Parameters<WriteFormulaConsolidatedInput>,
185    ) -> String {
186        tool_fn!(self.store, tools::expanded::write_formula_consolidated, i)
187    }
188
189    // ── Cell operations (1) ──
190
191    #[tool(
192        description = "Manage a cell: action='blank' writes a formatted blank cell, action='clear' removes content and formatting"
193    )]
194    async fn manage_cell(&self, Parameters(i): Parameters<ManageCellInput>) -> String {
195        tool_fn!(self.store, tools::expanded::manage_cell, i)
196    }
197
198    // ── Formatting (2) ──
199
200    #[tool(
201        description = "Apply formatting (bold, italic, colors, borders, number format, alignment) to a range of cells"
202    )]
203    async fn set_cell_format(&self, Parameters(i): Parameters<SetCellFormatInput>) -> String {
204        tool_fn!(self.store, tools::format::set_cell_format, i)
205    }
206
207    #[tool(description = "Merge a range of cells into a single cell")]
208    async fn merge_cells(&self, Parameters(i): Parameters<MergeCellsInput>) -> String {
209        tool_fn!(self.store, tools::format::merge_cells, i)
210    }
211
212    // ── Row/column format (1) ──
213
214    #[tool(
215        description = "Apply formatting to an entire row or column. Set target='row' with identifier='5' or target='column' with identifier='B'."
216    )]
217    async fn set_row_column_format(
218        &self,
219        Parameters(i): Parameters<SetRowColumnFormatInput>,
220    ) -> String {
221        tool_fn!(self.store, tools::expanded::set_row_column_format, i)
222    }
223
224    // ── Dimensions (1) ──
225
226    #[tool(
227        description = "Set dimensions: target='column_width' (column+value), 'row_height' (row+value), 'column_range_width' (first_column+last_column+value), 'default_row_height' (value)"
228    )]
229    async fn set_dimensions(&self, Parameters(i): Parameters<SetDimensionsInput>) -> String {
230        tool_fn!(self.store, tools::expanded::set_dimensions, i)
231    }
232
233    // ── Layout (4) ──
234
235    #[tool(
236        description = "Freeze panes at a cell position so rows above and columns left remain visible while scrolling"
237    )]
238    async fn freeze_panes(&self, Parameters(i): Parameters<FreezePanesInput>) -> String {
239        tool_fn!(self.store, tools::layout::freeze_panes, i)
240    }
241
242    #[tool(description = "Auto-fit all column widths based on cell content")]
243    async fn autofit_columns(&self, Parameters(i): Parameters<AutofitColumnsInput>) -> String {
244        tool_fn!(self.store, tools::expanded::autofit_columns, i)
245    }
246
247    #[tool(description = "Set the selected/active cell in a sheet")]
248    async fn set_selection(&self, Parameters(i): Parameters<SetSelectionInput>) -> String {
249        tool_fn!(self.store, tools::expanded::set_selection, i)
250    }
251
252    #[tool(
253        description = "Hide or unhide a row or column. Set target='row' or 'column', identifier='5' or 'B'."
254    )]
255    async fn set_visibility(&self, Parameters(i): Parameters<SetVisibilityInput>) -> String {
256        tool_fn!(self.store, tools::expanded::set_visibility, i)
257    }
258
259    // ── Sheet settings (1) ──
260
261    #[tool(
262        description = "Configure sheet display: hidden, very_hidden, zoom, hide_gridlines, hide_headings, tab_color, right_to_left"
263    )]
264    async fn set_sheet_settings(&self, Parameters(i): Parameters<SetSheetSettingsInput>) -> String {
265        tool_fn!(self.store, tools::expanded::set_sheet_settings, i)
266    }
267
268    // ── Charts (1) ──
269
270    #[tool(
271        description = "Add a chart with full control: type (bar/column/line/pie/scatter/area/doughnut), multiple series with colors/labels/trendlines/markers, pivot source, position cell"
272    )]
273    async fn add_chart(&self, Parameters(i): Parameters<AddChartEnhancedInput>) -> String {
274        tool_fn!(self.store, tools::expanded::add_chart_enhanced, i)
275    }
276
277    // ── Tables, CF, DV, Sparklines (4) ──
278
279    #[tool(description = "Create an Excel Table with headers, autofilter, and style")]
280    async fn add_table(&self, Parameters(i): Parameters<AddTableInput>) -> String {
281        tool_fn!(self.store, tools::tables::add_table, i)
282    }
283
284    #[tool(
285        description = "Add conditional formatting: cell_value, color_scale_2, color_scale_3, data_bar, or icon_set"
286    )]
287    async fn add_conditional_format(
288        &self,
289        Parameters(i): Parameters<AddConditionalFormatInput>,
290    ) -> String {
291        tool_fn!(self.store, tools::conditional::add_conditional_format, i)
292    }
293
294    #[tool(
295        description = "Add data validation: list, list_range, whole_number, decimal, date_range, text_length, or custom_formula"
296    )]
297    async fn add_data_validation(
298        &self,
299        Parameters(i): Parameters<AddDataValidationInput>,
300    ) -> String {
301        tool_fn!(self.store, tools::validation::add_data_validation, i)
302    }
303
304    #[tool(description = "Add a sparkline (line, column, or win/loss) to a cell")]
305    async fn add_sparkline(&self, Parameters(i): Parameters<AddSparklineInput>) -> String {
306        tool_fn!(self.store, tools::sparklines::add_sparkline, i)
307    }
308
309    // ── Images (1) ──
310
311    #[tool(description = "Embed a PNG or JPEG image into a worksheet at a specified cell")]
312    async fn add_image(&self, Parameters(i): Parameters<AddImageInput>) -> String {
313        tool_fn!(self.store, tools::images::add_image, i)
314    }
315
316    // ── Pivot tables (1) ──
317
318    #[tool(
319        description = "Create a pivot table with row/column/value/filter fields, aggregation (sum/count/average/max/min), layout (compact/outline/tabular)"
320    )]
321    async fn add_pivot_table(&self, Parameters(i): Parameters<AddPivotTableInput>) -> String {
322        tool_fn!(self.store, tools::expanded::add_pivot_table, i)
323    }
324
325    // ── Page setup (1) ──
326
327    #[tool(
328        description = "Configure page setup: landscape, paper_size, margins, fit_to_pages, print_scale, print_area, repeat_rows, header, footer, print_gridlines, center, page_breaks"
329    )]
330    async fn set_page_setup(&self, Parameters(i): Parameters<SetPageSetupInput>) -> String {
331        tool_fn!(self.store, tools::expanded::set_page_setup, i)
332    }
333
334    // ── Comments & links (2) ──
335
336    #[tool(
337        description = "Manage comments: action='add' (cell, text, author) or action='read' to list all comments"
338    )]
339    async fn manage_comments(&self, Parameters(i): Parameters<ManageCommentsInput>) -> String {
340        tool_fn!(self.store, tools::expanded::manage_comments, i)
341    }
342
343    #[tool(
344        description = "Add a link: link_type='url' for external URLs, link_type='internal' for sheet references (e.g. 'Sheet2!A1')"
345    )]
346    async fn add_link(&self, Parameters(i): Parameters<AddLinkInput>) -> String {
347        tool_fn!(self.store, tools::expanded::add_link, i)
348    }
349
350    // ── Named ranges (1) ──
351
352    #[tool(
353        description = "Manage defined names: action='add' (name, formula) or action='list' to show all"
354    )]
355    async fn manage_defined_names(
356        &self,
357        Parameters(i): Parameters<ManageDefinedNamesInput>,
358    ) -> String {
359        tool_fn!(self.store, tools::expanded::manage_defined_names, i)
360    }
361
362    // ── Row/column manipulation (2) ──
363
364    #[tool(
365        description = "Insert or delete rows: action='insert' or 'delete', at_row (1-based), count"
366    )]
367    async fn modify_rows(&self, Parameters(i): Parameters<ModifyRowsInput>) -> String {
368        tool_fn!(self.store, tools::expanded::modify_rows, i)
369    }
370
371    #[tool(
372        description = "Insert or delete columns: action='insert' or 'delete', at_column (letter), count"
373    )]
374    async fn modify_columns(&self, Parameters(i): Parameters<ModifyColumnsInput>) -> String {
375        tool_fn!(self.store, tools::expanded::modify_columns, i)
376    }
377
378    // ── Grouping (1) ──
379
380    #[tool(
381        description = "Group rows or columns into expandable outlines. target='rows' (start/end as numbers) or 'columns' (start/end as letters)"
382    )]
383    async fn group(&self, Parameters(i): Parameters<GroupInput>) -> String {
384        tool_fn!(self.store, tools::expanded::group_consolidated, i)
385    }
386
387    // ── Protection (1) ──
388
389    #[tool(
390        description = "Protect: target='sheet' (sheet_name, password), 'workbook' (password), or 'unprotect_range' (sheet_name, range, range_title, password)"
391    )]
392    async fn protect(&self, Parameters(i): Parameters<ProtectInput>) -> String {
393        tool_fn!(self.store, tools::expanded::protect_consolidated, i)
394    }
395
396    // ── Autofilter (1) ──
397
398    #[tool(
399        description = "Set autofilter on a range. Optionally filter a specific column with filter_column and filter_values."
400    )]
401    async fn manage_autofilter(&self, Parameters(i): Parameters<ManageAutofilterInput>) -> String {
402        tool_fn!(self.store, tools::expanded::manage_autofilter, i)
403    }
404
405    // ── Error suppression (1) ──
406
407    #[tool(
408        description = "Suppress Excel error indicators (green triangles) on a range for a specific error type"
409    )]
410    async fn ignore_error(&self, Parameters(i): Parameters<IgnoreErrorInput>) -> String {
411        tool_fn!(self.store, tools::expanded::ignore_error, i)
412    }
413
414    // ── ChartEx charts (3) ──
415
416    #[tool(
417        description = "Add a waterfall chart (Excel 2016+ ChartEx). Points have category, value, and point_type (increase/decrease/total)."
418    )]
419    async fn add_waterfall_chart(
420        &self,
421        Parameters(i): Parameters<AddWaterfallChartInput>,
422    ) -> String {
423        tool_fn!(self.store, tools::expanded::add_waterfall_chart, i)
424    }
425
426    #[tool(
427        description = "Add a funnel chart (Excel 2016+ ChartEx). Points have category and value, rendered as a narrowing funnel."
428    )]
429    async fn add_funnel_chart(&self, Parameters(i): Parameters<AddFunnelChartInput>) -> String {
430        tool_fn!(self.store, tools::expanded::add_funnel_chart, i)
431    }
432
433    #[tool(
434        description = "Add a treemap chart (Excel 2016+ ChartEx). Points have category, value, and optional color."
435    )]
436    async fn add_treemap_chart(&self, Parameters(i): Parameters<AddTreemapChartInput>) -> String {
437        tool_fn!(self.store, tools::expanded::add_treemap_chart, i)
438    }
439
440    // ── Shapes (1) ──
441
442    #[tool(
443        description = "Add a drawing shape (rectangle, rounded_rectangle, ellipse, triangle, diamond, arrow, callout, text_box) with optional text, fill, outline, and font settings."
444    )]
445    async fn add_shape(&self, Parameters(i): Parameters<AddShapeInput>) -> String {
446        tool_fn!(self.store, tools::expanded::add_shape, i)
447    }
448
449    // ── Document properties (1) ──
450
451    #[tool(
452        description = "Set document properties: title, author, subject, description, keywords, category, company"
453    )]
454    async fn set_doc_properties(&self, Parameters(i): Parameters<SetDocPropertiesInput>) -> String {
455        tool_fn!(self.store, tools::expanded::set_doc_properties, i)
456    }
457
458    // ── New chart types (4) ──
459
460    #[tool(
461        description = "Add a sunburst chart (Excel 2016+ ChartEx). Hierarchical data with category labels and values."
462    )]
463    async fn add_sunburst_chart(
464        &self,
465        Parameters(i): Parameters<AddSunburstChartInput>,
466    ) -> String {
467        tool_fn!(self.store, tools::expanded::add_sunburst_chart, i)
468    }
469
470    #[tool(
471        description = "Add a histogram chart (Excel 2016+ ChartEx). Raw data values with optional bin_count, bin_width, and pareto overlay."
472    )]
473    async fn add_histogram_chart(
474        &self,
475        Parameters(i): Parameters<AddHistogramChartInput>,
476    ) -> String {
477        tool_fn!(self.store, tools::expanded::add_histogram_chart, i)
478    }
479
480    #[tool(
481        description = "Add a box & whisker chart (Excel 2016+ ChartEx). Data sets with outliers, mean markers, and inner points options."
482    )]
483    async fn add_box_whisker_chart(
484        &self,
485        Parameters(i): Parameters<AddBoxWhiskerChartInput>,
486    ) -> String {
487        tool_fn!(self.store, tools::expanded::add_box_whisker_chart, i)
488    }
489
490    #[tool(
491        description = "Add a map chart (Excel 2016+ ChartEx). Geographic data with location names and values. map_level: 'country' or 'region'."
492    )]
493    async fn add_map_chart(&self, Parameters(i): Parameters<AddMapChartInput>) -> String {
494        tool_fn!(self.store, tools::expanded::add_map_chart, i)
495    }
496
497    // ── Interactive controls (3) ──
498
499    #[tool(
500        description = "Add a slicer — interactive filter for a pivot table. Specify pivot_table_name and field_name."
501    )]
502    async fn add_slicer(&self, Parameters(i): Parameters<AddSlicerInput>) -> String {
503        tool_fn!(self.store, tools::expanded::add_slicer, i)
504    }
505
506    #[tool(
507        description = "Add a timeline — date filter for a pivot table. Specify pivot_table_name and date field_name."
508    )]
509    async fn add_timeline(&self, Parameters(i): Parameters<AddTimelineInput>) -> String {
510        tool_fn!(self.store, tools::expanded::add_timeline, i)
511    }
512
513    #[tool(
514        description = "Add a form control: control_type='button', 'checkbox', 'dropdown', or 'spinner'. Checkbox supports cell_link, dropdown takes comma-separated input_range."
515    )]
516    async fn add_form_control(&self, Parameters(i): Parameters<AddFormControlInput>) -> String {
517        tool_fn!(self.store, tools::expanded::add_form_control, i)
518    }
519
520    // ── Advanced save/open (2) ──
521
522    #[tool(
523        description = "Save workbook in different formats: 'xlsx' (default), 'template' (.xltx), 'encrypted' (password-protected), 'parallel' (fast compression). Formulas are recalculated before save."
524    )]
525    async fn save_workbook_advanced(
526        &self,
527        Parameters(i): Parameters<SaveWorkbookAdvancedInput>,
528    ) -> String {
529        tool_fn!(self.store, tools::expanded::save_workbook_advanced, i)
530    }
531
532    #[tool(description = "Open a password-protected (encrypted) Excel workbook")]
533    async fn open_workbook_encrypted(
534        &self,
535        Parameters(i): Parameters<OpenWorkbookEncryptedInput>,
536    ) -> String {
537        tool_fn!(self.store, tools::expanded::open_workbook_encrypted, i)
538    }
539
540    // ── Named ranges CRUD (1) ──
541
542    #[tool(
543        description = "Manage named ranges: action='add' (name, formula), 'add_scoped' (name, formula, sheet_index), 'update' (name, formula), 'remove' (name, sheet_index), 'list'"
544    )]
545    async fn manage_named_ranges(
546        &self,
547        Parameters(i): Parameters<ManageNamedRangesInput>,
548    ) -> String {
549        tool_fn!(self.store, tools::expanded::manage_named_ranges, i)
550    }
551
552    // ── Sheet metadata (1) ──
553
554    #[tool(
555        description = "Read sheet metadata: info='used_range', 'hyperlinks', 'merge_ranges', 'charts', or 'all'"
556    )]
557    async fn read_sheet_metadata(
558        &self,
559        Parameters(i): Parameters<ReadSheetMetadataInput>,
560    ) -> String {
561        tool_fn!(self.store, tools::expanded::read_sheet_metadata, i)
562    }
563
564    // ── Chart sheet (1) ──
565
566    #[tool(
567        description = "Add a dedicated chart-only sheet (no cells, just a chart). Specify chart_type, series or data_range, title."
568    )]
569    async fn add_chart_sheet(&self, Parameters(i): Parameters<AddChartSheetInput>) -> String {
570        tool_fn!(self.store, tools::expanded::add_chart_sheet, i)
571    }
572
573    // ── Threaded comments (1) ──
574
575    #[tool(
576        description = "Add a modern threaded comment with author, text, optional timestamp, and replies. Unlike legacy comments, these support conversation threads."
577    )]
578    async fn add_threaded_comment(
579        &self,
580        Parameters(i): Parameters<AddThreadedCommentInput>,
581    ) -> String {
582        tool_fn!(self.store, tools::expanded::add_threaded_comment, i)
583    }
584
585    // ── Granular protection (1) ──
586
587    #[tool(
588        description = "Protect a sheet with granular options: allow_insert_rows, allow_delete_rows, allow_format_cells, allow_sort, etc. Optional password."
589    )]
590    async fn protect_sheet_advanced(
591        &self,
592        Parameters(i): Parameters<ProtectSheetAdvancedInput>,
593    ) -> String {
594        tool_fn!(self.store, tools::expanded::protect_sheet_advanced, i)
595    }
596
597    // ── Custom properties (1) ──
598
599    #[tool(
600        description = "Set a custom document property. value_type: 'text' (default), 'number', 'integer', 'bool', 'datetime'."
601    )]
602    async fn set_custom_property(
603        &self,
604        Parameters(i): Parameters<SetCustomPropertyInput>,
605    ) -> String {
606        tool_fn!(self.store, tools::expanded::set_custom_property, i)
607    }
608
609    // ── Read enhancements (2) ──
610
611    #[tool(description = "Read a single cell's comment (author and text). Returns null if no comment exists.")]
612    async fn read_cell_comment(
613        &self,
614        Parameters(i): Parameters<ReadCellCommentInput>,
615    ) -> String {
616        tool_fn!(self.store, tools::expanded::read_cell_comment, i)
617    }
618
619    #[tool(description = "Read a cell's format (bold, italic, colors, number format, etc.)")]
620    async fn read_cell_format(
621        &self,
622        Parameters(i): Parameters<ReadCellFormatInput>,
623    ) -> String {
624        tool_fn!(self.store, tools::expanded::read_cell_format, i)
625    }
626
627    // ── Custom XML (1) ──
628
629    #[tool(
630        description = "Manage custom XML parts: action='add' (namespace, content) or action='read' (namespace)"
631    )]
632    async fn manage_custom_xml(
633        &self,
634        Parameters(i): Parameters<ManageCustomXmlInput>,
635    ) -> String {
636        tool_fn!(self.store, tools::expanded::manage_custom_xml, i)
637    }
638
639    // ── External connections (1) ──
640
641    #[tool(description = "Add an external data connection (connection_string, command)")]
642    async fn add_connection(&self, Parameters(i): Parameters<AddConnectionInput>) -> String {
643        tool_fn!(self.store, tools::expanded::add_connection, i)
644    }
645
646    // ── SST optimization (1) ──
647
648    #[tool(description = "Set the shared string table threshold for optimization. Lower values use more memory but faster writes.")]
649    async fn set_sst_threshold(
650        &self,
651        Parameters(i): Parameters<SetSstThresholdInput>,
652    ) -> String {
653        tool_fn!(self.store, tools::expanded::set_sst_threshold, i)
654    }
655
656    // ── JSON rows (1) ──
657
658    #[tool(
659        description = "Write JSON objects as rows. Each object's keys become column headers (if write_headers=true), values become cells. Auto-detects types."
660    )]
661    async fn write_json_rows(&self, Parameters(i): Parameters<WriteJsonRowsInput>) -> String {
662        tool_fn!(self.store, tools::expanded::write_json_rows, i)
663    }
664}
665
666#[tool_handler]
667impl ServerHandler for ExcelMcpServer {
668    fn get_info(&self) -> ServerInfo {
669        ServerInfo::new(ServerCapabilities::builder().enable_tools().build()).with_instructions(
670            "Excel file manipulation server powered by zavora-xlsx. 74 tools covering: \
671                 workbook lifecycle, sheet management, cell reading/writing, formatting, \
672                 charts (11 types + pivot charts + waterfall/funnel/treemap/sunburst/histogram/box-whisker/map \
673                 with data tables, 3D views, error bars, axis formatting, drop/high-low lines, gradients), \
674                 images, shapes, tables, conditional formatting, data validation, sparklines, \
675                 pivot tables (calculated fields, date/range grouping, subtotals, grand totals, value formats), \
676                 slicers, timelines, form controls, threaded comments, page setup, hyperlinks, \
677                 named ranges (CRUD with scoping), row/column manipulation, grouping, \
678                 protection (basic + granular with per-feature allow/deny), autofilter, \
679                 formulas (regular/array/dynamic with recalculation), rich text, \
680                 document properties, custom properties, sheet metadata, chart sheets, \
681                 encrypted open/save, template save, parallel save, and CSV export."
682                .to_string(),
683        )
684    }
685}