iflow-mcp-jenstangen1-pptx 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,18 @@
1
+ ./.gitignore,sha256=VZZVEztXNgHB-UX2ksc-RUqk7uXrfwgUYKI88iyg140,321
2
+ ./PKG-INFO,sha256=sEtRC8jt9vMkVdc0yrkChJi4xpN7NAEK4UKOVHYzQEA,11072
3
+ ./README.md,sha256=mA8UJQDpYWRv9SAgBjnUzUg2iuBd6SOILlcUshV8VlU,10590
4
+ ./language.json,sha256=J-z0poNcsv31IHB413--iOY8LoHBKiTHeybHX3abokI,7
5
+ ./mcp_excel_server_win32.py,sha256=3ACK4m3CiKM_l3kJilZFLh4l2Rtu0xYOE668wAfz0Kc,24922
6
+ ./mcp_powerpoint_server.py,sha256=oR54tX-aR-6VoeuIkMvEV9zWCBTAWhQYjWBs2b1-FnM,52256
7
+ ./mcp_powerpoint_server_win32.py,sha256=WnNyeJj6V99UyRVAafnxpotQtks8_xfFxZo8SFMZ-BQ,33676
8
+ ./package-lock.json,sha256=aOgpaHdi-MKx94hpAlYN_BO-EIPFsjIgJRCfbqg00yw,37402
9
+ ./package.json,sha256=5zFcveBAdcrB1Cj32wWbEIJdu0yrBbTjl2-0jwJxVyQ,70
10
+ ./package_name,sha256=GJk1VblmnVgt3NCdthPzOF8vFqXG1KP6b6h7Ddc04FI,36
11
+ ./push_info.json,sha256=gTrf-37VG9wi4oewcjYGT_afBNNli5UuoMproVkSx40,129
12
+ ./pyproject.toml,sha256=Cv2JKu9ifeCBgX0Lr5VyDoF38zhqLnoE1Pd54EI40EE,605
13
+ ./requirements.txt,sha256=gcdSIfqqAxmmcPDOXqi65JJCZc_AJFyRy06EdgM1Kjw,151
14
+ ./server_config.json,sha256=mnOqZt-hddK034_o4ZV4UnAvZ7r_TYQqczv99JJ3lpk,196
15
+ iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/METADATA,sha256=sEtRC8jt9vMkVdc0yrkChJi4xpN7NAEK4UKOVHYzQEA,11072
16
+ iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/entry_points.txt,sha256=EE1M88zPF7wOW8FI5F45466QgkHBXJ32OFJhpIIfrkw,56
18
+ iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pptx-mcp = mcp_powerpoint_server:main
language.json ADDED
@@ -0,0 +1 @@
1
+ python
@@ -0,0 +1,554 @@
1
+ import win32com.client
2
+ import pythoncom
3
+ import pywintypes # Import for specific exception types
4
+ from typing import List, Optional, Dict, Any, Union, Tuple
5
+ from mcp.server.fastmcp import FastMCP
6
+ import os
7
+ from win32com.client import constants
8
+
9
+ # Constants from Excel VBA Object Library (obtained via makepy or documentation)
10
+ # Using magic numbers for simplicity here, but using makepy is recommended
11
+ xlOpenXMLWorkbook = 51 # .xlsx format
12
+ xlUp = -4162
13
+ xlDown = -4121
14
+ xlToLeft = -4159
15
+ xlToRight = -4161
16
+
17
+ class ExcelEditorWin32:
18
+ def __init__(self):
19
+ self.app = None
20
+ self._connect_or_launch_excel()
21
+
22
+ def _connect_or_launch_excel(self):
23
+ """Connects to a running instance of Excel or launches a new one."""
24
+ try:
25
+ # Use the Pywin32 CoInitializeEx to avoid threading issues with COM
26
+ pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED)
27
+ self.app = win32com.client.GetActiveObject("Excel.Application")
28
+ print("Connected to running Excel application.")
29
+ except pywintypes.com_error:
30
+ try:
31
+ self.app = win32com.client.Dispatch("Excel.Application")
32
+ self.app.Visible = True # Make the application visible
33
+ print("Launched new Excel application.")
34
+ except Exception as e:
35
+ print(f"Error launching Excel: {e}")
36
+ self.app = None
37
+ except Exception as e:
38
+ print(f"An unexpected error occurred connecting to Excel: {e}")
39
+ self.app = None
40
+ # Make sure interaction errors are visible to the user
41
+ if self.app:
42
+ self.app.DisplayAlerts = True # Show Excel's own alerts
43
+
44
+ def _ensure_connection(self):
45
+ """Ensures the Excel application object is valid."""
46
+ if self.app is None:
47
+ self._connect_or_launch_excel()
48
+ if self.app is None:
49
+ raise ConnectionError("Could not connect to or launch Excel.")
50
+ try:
51
+ _ = self.app.Version
52
+ except Exception as e:
53
+ print(f"Excel connection lost or unresponsive: {e}")
54
+ self._connect_or_launch_excel() # Try reconnecting
55
+ if self.app is None:
56
+ raise ConnectionError("Could not reconnect to Excel.")
57
+
58
+ def list_open_workbooks(self) -> List[Dict[str, Any]]:
59
+ """Lists all currently open workbooks."""
60
+ self._ensure_connection()
61
+ workbooks_info = []
62
+ try:
63
+ if self.app.Workbooks.Count == 0:
64
+ return []
65
+ for i in range(1, self.app.Workbooks.Count + 1):
66
+ wb = self.app.Workbooks(i)
67
+ workbooks_info.append({
68
+ "name": wb.Name,
69
+ "path": wb.FullName if not wb.ReadOnly else wb.FullName + " (ReadOnly)",
70
+ "sheets_count": wb.Worksheets.Count,
71
+ "saved": wb.Saved,
72
+ "index": i # Provide the 1-based index for reference
73
+ })
74
+ except Exception as e:
75
+ print(f"Error listing workbooks: {e}")
76
+ if "RPC server is unavailable" in str(e):
77
+ self._connect_or_launch_excel()
78
+ raise
79
+ return workbooks_info
80
+
81
+ def get_workbook(self, identifier: Union[str, int]) -> Optional[Any]:
82
+ """
83
+ Gets a workbook object by its name, path, or 1-based index.
84
+
85
+ Args:
86
+ identifier (Union[str, int]): The name (e.g., "Book1.xlsx"),
87
+ full path, or 1-based index.
88
+ """
89
+ self._ensure_connection()
90
+ try:
91
+ if isinstance(identifier, int):
92
+ idx = identifier
93
+ if 1 <= idx <= self.app.Workbooks.Count:
94
+ return self.app.Workbooks(idx)
95
+ else:
96
+ print(f"Workbook index {identifier} out of range.")
97
+ return None
98
+ elif isinstance(identifier, str) and identifier.isdigit():
99
+ idx = int(identifier)
100
+ if 1 <= idx <= self.app.Workbooks.Count:
101
+ return self.app.Workbooks(idx)
102
+ else:
103
+ print(f"Workbook index {identifier} out of range.")
104
+ return None
105
+ elif isinstance(identifier, str):
106
+ for i in range(1, self.app.Workbooks.Count + 1):
107
+ wb = self.app.Workbooks(i)
108
+ if wb.Name.lower() == identifier.lower() or \
109
+ (wb.Path and wb.FullName.lower() == identifier.lower()):
110
+ return wb
111
+ print(f"Workbook '{identifier}' not found.")
112
+ return None
113
+ else:
114
+ print(f"Invalid workbook identifier type: {type(identifier)}. Use str or int.")
115
+ return None
116
+ except Exception as e:
117
+ print(f"Error getting workbook '{identifier}': {e}")
118
+ raise
119
+
120
+ def save_workbook(self, identifier: Union[str, int], save_path: Optional[str] = None):
121
+ """
122
+ Saves the specified workbook.
123
+
124
+ Args:
125
+ identifier (Union[str, int]): Name, path, or index of the workbook.
126
+ save_path (Optional[str]): Path to save to (e.g., 'C:\MyFolder\NewName.xlsx').
127
+ If None, saves to its current path.
128
+ If the workbook is new, save_path is required.
129
+ """
130
+ self._ensure_connection()
131
+ wb = self.get_workbook(identifier)
132
+ if not wb:
133
+ raise ValueError(f"Workbook '{identifier}' not found.")
134
+
135
+ try:
136
+ if save_path:
137
+ abs_path = os.path.abspath(save_path)
138
+ os.makedirs(os.path.dirname(abs_path), exist_ok=True)
139
+ # Determine file format based on extension, default to xlsx
140
+ file_format = None
141
+ if abs_path.lower().endswith(".xlsx"):
142
+ file_format = xlOpenXMLWorkbook # 51
143
+ elif abs_path.lower().endswith(".xlsm"):
144
+ file_format = 52 # xlOpenXMLWorkbookMacroEnabled
145
+ elif abs_path.lower().endswith(".xlsb"):
146
+ file_format = 50 # xlExcel12 (Binary)
147
+ elif abs_path.lower().endswith(".xls"):
148
+ file_format = 56 # xlExcel8
149
+ # Add other formats if needed (e.g., CSV = 6)
150
+
151
+ print(f"Attempting to save as '{abs_path}' with format {file_format}")
152
+ wb.SaveAs(abs_path, FileFormat=file_format)
153
+ print(f"Workbook saved as '{abs_path}'.")
154
+ elif wb.Path: # Can only save if it has a path already
155
+ wb.Save()
156
+ print(f"Workbook '{wb.Name}' saved.")
157
+ else:
158
+ raise ValueError("save_path is required for a new workbook that hasn't been saved before.")
159
+ except Exception as e:
160
+ print(f"Error saving workbook '{identifier}': {e}")
161
+ # Provide more COM error details if possible
162
+ if isinstance(e, pywintypes.com_error):
163
+ print(f"COM Error Details: HRESULT={e.hresult}, Message={e.excepinfo}")
164
+ raise
165
+
166
+ def list_worksheets(self, identifier: Union[str, int]) -> List[Dict[str, Any]]:
167
+ """Lists all worksheets in the specified workbook."""
168
+ self._ensure_connection()
169
+ sheets_info = []
170
+ wb = self.get_workbook(identifier)
171
+ if not wb:
172
+ print(f"Cannot list worksheets: Workbook '{identifier}' not found.")
173
+ return []
174
+
175
+ try:
176
+ for i in range(1, wb.Worksheets.Count + 1):
177
+ ws = wb.Worksheets(i)
178
+ sheets_info.append({
179
+ "name": ws.Name,
180
+ "index": ws.Index, # 1-based index
181
+ "visible": ws.Visible == -1 # -1=xlSheetVisible, 0=xlSheetHidden, 2=xlSheetVeryHidden
182
+ })
183
+ except Exception as e:
184
+ print(f"Error listing worksheets in workbook '{identifier}': {e}")
185
+ raise
186
+ return sheets_info
187
+
188
+ def get_worksheet(self, identifier: Union[str, int], sheet_identifier: Union[str, int]) -> Optional[Any]:
189
+ """
190
+ Gets a worksheet object from a workbook.
191
+
192
+ Args:
193
+ identifier (Union[str, int]): Workbook identifier (name, path, index).
194
+ sheet_identifier (Union[str, int]): Worksheet identifier (name or 1-based index).
195
+
196
+ Returns:
197
+ Optional[Any]: The worksheet object or None if not found.
198
+ """
199
+ self._ensure_connection()
200
+ wb = self.get_workbook(identifier)
201
+ if not wb:
202
+ return None
203
+
204
+ try:
205
+ # Try by index first
206
+ if isinstance(sheet_identifier, int):
207
+ idx = sheet_identifier
208
+ if 1 <= idx <= wb.Worksheets.Count:
209
+ return wb.Worksheets(idx)
210
+ else:
211
+ print(f"Worksheet index {idx} out of range for workbook '{wb.Name}'.")
212
+ return None
213
+ # Try by name
214
+ elif isinstance(sheet_identifier, str):
215
+ # Direct access by name is usually reliable in Excel COM
216
+ return wb.Worksheets(sheet_identifier)
217
+ else:
218
+ print(f"Invalid sheet identifier type: {type(sheet_identifier)}. Use str or int.")
219
+ return None
220
+ except pywintypes.com_error as e:
221
+ if e.hresult == -2147352565: # Often indicates item not found
222
+ print(f"Worksheet '{sheet_identifier}' not found in workbook '{wb.Name}'.")
223
+ else:
224
+ print(f"COM Error getting worksheet '{sheet_identifier}' from '{wb.Name}': HRESULT={e.hresult}")
225
+ return None
226
+ except Exception as e:
227
+ print(f"Error getting worksheet '{sheet_identifier}' from '{identifier}': {e}")
228
+ return None # Don't raise here, allow tool to report error
229
+
230
+ def add_worksheet(self, identifier: Union[str, int], sheet_name: Optional[str] = None) -> Dict[str, Any]:
231
+ """
232
+ Adds a new worksheet to the workbook.
233
+
234
+ Args:
235
+ identifier (Union[str, int]): Workbook identifier.
236
+ sheet_name (Optional[str]): Name for the new worksheet. If None, Excel assigns a default.
237
+
238
+ Returns:
239
+ Dict[str, Any]: Information about the added sheet (name and index).
240
+ """
241
+ self._ensure_connection()
242
+ wb = self.get_workbook(identifier)
243
+ if not wb:
244
+ raise ValueError(f"Workbook '{identifier}' not found.")
245
+
246
+ try:
247
+ # Add sheet at the end
248
+ new_sheet = wb.Worksheets.Add(After=wb.Worksheets(wb.Worksheets.Count))
249
+ if sheet_name:
250
+ try:
251
+ new_sheet.Name = sheet_name
252
+ except Exception as name_e:
253
+ print(f"Warning: Could not set sheet name to '{sheet_name}'. Using default '{new_sheet.Name}'. Error: {name_e}")
254
+
255
+ print(f"Added worksheet '{new_sheet.Name}' (Index: {new_sheet.Index}) to workbook '{wb.Name}'.")
256
+ return {"name": new_sheet.Name, "index": new_sheet.Index}
257
+ except Exception as e:
258
+ print(f"Error adding worksheet to workbook '{identifier}': {e}")
259
+ raise
260
+
261
+ def get_cell_value(self, identifier: Union[str, int], sheet_identifier: Union[str, int], cell_address: str) -> Any:
262
+ """
263
+ Gets the value of a single cell.
264
+
265
+ Args:
266
+ identifier: Workbook identifier.
267
+ sheet_identifier: Worksheet identifier.
268
+ cell_address (str): Cell address (e.g., "A1", "B5").
269
+
270
+ Returns:
271
+ Any: The value of the cell (can be str, float, int, None, datetime, etc.).
272
+ """
273
+ self._ensure_connection()
274
+ ws = self.get_worksheet(identifier, sheet_identifier)
275
+ if not ws:
276
+ raise ValueError(f"Worksheet '{sheet_identifier}' not found.")
277
+
278
+ try:
279
+ cell = ws.Range(cell_address)
280
+ value = cell.Value
281
+ # Convert COM dates (often floats) to Python datetime if they look like dates
282
+ # This is heuristic - might need adjustment
283
+ if isinstance(value, float) and value > 1 and value < 300000: # Plausible Excel date serial numbers
284
+ try:
285
+ # Excel dates are days since 1900-01-01 (or 1899-12-30 depending on settings)
286
+ # Using a known COM date conversion
287
+ dt_val = pywintypes.Time(int(value))
288
+ return dt_val # Returns a pywintypes time object, convertable to datetime
289
+ except ValueError:
290
+ pass # Wasn't a valid date float
291
+ # Handle potential currency type (VT_CY) coming back as Decimal
292
+ if type(value).__name__ == 'Decimal':
293
+ return float(value)
294
+ return value
295
+ except Exception as e:
296
+ print(f"Error getting value from cell '{cell_address}' on sheet '{sheet_identifier}': {e}")
297
+ raise
298
+
299
+ def set_cell_value(self, identifier: Union[str, int], sheet_identifier: Union[str, int], cell_address: str, value: Any):
300
+ """
301
+ Sets the value of a single cell.
302
+
303
+ Args:
304
+ identifier: Workbook identifier.
305
+ sheet_identifier: Worksheet identifier.
306
+ cell_address (str): Cell address (e.g., "A1", "C10").
307
+ value (Any): The value to set in the cell.
308
+ """
309
+ self._ensure_connection()
310
+ ws = self.get_worksheet(identifier, sheet_identifier)
311
+ if not ws:
312
+ raise ValueError(f"Worksheet '{sheet_identifier}' not found.")
313
+
314
+ try:
315
+ cell = ws.Range(cell_address)
316
+ cell.Value = value
317
+ print(f"Set value of cell '{cell_address}' on sheet '{ws.Name}' to: {value}")
318
+ except Exception as e:
319
+ print(f"Error setting value for cell '{cell_address}' on sheet '{sheet_identifier}': {e}")
320
+ raise
321
+
322
+ def get_range_values(self, identifier: Union[str, int], sheet_identifier: Union[str, int], range_address: str) -> Union[Tuple[Tuple[Any, ...], ...], None]:
323
+ """
324
+ Gets the values from a range of cells.
325
+
326
+ Args:
327
+ identifier: Workbook identifier.
328
+ sheet_identifier: Worksheet identifier.
329
+ range_address (str): Range address (e.g., "A1:C5", "D10:D20").
330
+
331
+ Returns:
332
+ Union[Tuple[Tuple[Any, ...], ...], None]: A tuple of tuples containing the cell values,
333
+ or None if the range is invalid or empty.
334
+ Returns a single value if range_address is a single cell.
335
+ """
336
+ self._ensure_connection()
337
+ ws = self.get_worksheet(identifier, sheet_identifier)
338
+ if not ws:
339
+ raise ValueError(f"Worksheet '{sheet_identifier}' not found.")
340
+
341
+ try:
342
+ data_range = ws.Range(range_address)
343
+ values = data_range.Value
344
+
345
+ # Handle single cell case
346
+ if not isinstance(values, tuple):
347
+ # If it's a single cell, data_range.Value returns the value directly.
348
+ # We'll wrap it to match the expected tuple-of-tuples structure for consistency.
349
+ return ((values,),) # Return as tuple containing a tuple with the single value
350
+
351
+ # Convert pywintypes time objects in the tuple to datetime
352
+ # TODO: Need a more robust way to detect and convert dates/times/currency
353
+ # This basic version just returns the raw tuple from COM
354
+ return values
355
+ except pywintypes.com_error as e:
356
+ if e.hresult == -2146827284: # Typically invalid range address
357
+ print(f"Error: Invalid range address '{range_address}' on sheet '{sheet_identifier}'.")
358
+ raise ValueError(f"Invalid range address '{range_address}'.") from e
359
+ else:
360
+ print(f"COM Error getting values from range '{range_address}' on sheet '{sheet_identifier}': {e}")
361
+ raise
362
+ except Exception as e:
363
+ print(f"Error getting values from range '{range_address}' on sheet '{sheet_identifier}': {e}")
364
+ raise
365
+
366
+ def set_range_values(self, identifier: Union[str, int], sheet_identifier: Union[str, int], start_cell: str, values: List[List[Any]]):
367
+ """
368
+ Sets values in a range of cells, starting from the specified cell.
369
+
370
+ Args:
371
+ identifier: Workbook identifier.
372
+ sheet_identifier: Worksheet identifier.
373
+ start_cell (str): The top-left cell of the range to write to (e.g., "A1").
374
+ values (List[List[Any]]): A list of lists representing rows and columns of values.
375
+ """
376
+ self._ensure_connection()
377
+ ws = self.get_worksheet(identifier, sheet_identifier)
378
+ if not ws:
379
+ raise ValueError(f"Worksheet '{sheet_identifier}' not found.")
380
+
381
+ if not values or not isinstance(values, list) or not isinstance(values[0], list):
382
+ raise ValueError("Input 'values' must be a non-empty list of lists.")
383
+
384
+ try:
385
+ num_rows = len(values)
386
+ num_cols = len(values[0])
387
+
388
+ # Determine the target range based on start_cell and dimensions
389
+ start_range = ws.Range(start_cell)
390
+ # Use Resize property to define the target range
391
+ target_range = start_range.Resize(num_rows, num_cols)
392
+
393
+ # Set the values
394
+ target_range.Value = values
395
+ print(f"Set values in range {target_range.Address} on sheet '{ws.Name}'.")
396
+ except Exception as e:
397
+ print(f"Error setting values starting at cell '{start_cell}' on sheet '{sheet_identifier}': {e}")
398
+ raise
399
+
400
+ # --- MCP Server Setup --- #
401
+
402
+ # Create the Excel editor instance
403
+ try:
404
+ editor = ExcelEditorWin32()
405
+ except Exception as start_exc:
406
+ print(f"CRITICAL: Failed to initialize ExcelEditorWin32: {start_exc}")
407
+ editor = None # Ensure editor is None if initialization fails
408
+
409
+ # Create MCP server
410
+ mcp = FastMCP("Excel MCP (Win32)")
411
+
412
+ def _handle_excel_tool_error(tool_name: str, error: Exception) -> Dict[str, str]:
413
+ """Standardizes error reporting for Excel tools."""
414
+ err_msg = f"Error in tool '{tool_name}': {str(error)}"
415
+ print(err_msg) # Log the error server-side
416
+ if isinstance(error, ConnectionError) or "RPC server is unavailable" in str(error):
417
+ return {"error": "Could not connect to Excel. Please ensure it is running."}
418
+ if isinstance(error, ValueError):
419
+ # Often used for 'not found' or invalid input errors from the editor class
420
+ return {"error": str(error)}
421
+ # Add specific handling for COM errors if needed
422
+ if isinstance(error, pywintypes.com_error):
423
+ return {"error": f"Excel Communication Error in {tool_name}: {str(error)}"}
424
+ return {"error": err_msg}
425
+
426
+ @mcp.tool()
427
+ def list_open_workbooks():
428
+ """Lists currently open Excel workbooks."""
429
+ if not editor: return {"error": "Excel editor not initialized."}
430
+ try:
431
+ return {"workbooks": editor.list_open_workbooks()}
432
+ except Exception as e:
433
+ return _handle_excel_tool_error("list_open_workbooks", e)
434
+
435
+ @mcp.tool()
436
+ def save_workbook(identifier: Union[str, int], save_path: str = None):
437
+ """Saves the specified workbook. Use index, name, or full path as identifier."""
438
+ if not editor: return {"error": "Excel editor not initialized."}
439
+ try:
440
+ editor.save_workbook(identifier, save_path)
441
+ return {"message": f"Save command issued for workbook '{identifier}' successfully."}
442
+ except Exception as e:
443
+ return _handle_excel_tool_error("save_workbook", e)
444
+
445
+ @mcp.tool()
446
+ def list_worksheets(identifier: Union[str, int]):
447
+ """Lists worksheets in the specified workbook."""
448
+ if not editor: return {"error": "Excel editor not initialized."}
449
+ try:
450
+ sheets = editor.list_worksheets(identifier)
451
+ return {"worksheets": sheets}
452
+ except Exception as e:
453
+ return _handle_excel_tool_error("list_worksheets", e)
454
+
455
+ @mcp.tool()
456
+ def add_worksheet(identifier: Union[str, int], sheet_name: str = None):
457
+ """Adds a worksheet to the specified workbook. Optionally provide a sheet_name."""
458
+ if not editor: return {"error": "Excel editor not initialized."}
459
+ try:
460
+ new_sheet_info = editor.add_worksheet(identifier, sheet_name)
461
+ return {"message": "Worksheet added successfully.", "sheet_info": new_sheet_info}
462
+ except Exception as e:
463
+ return _handle_excel_tool_error("add_worksheet", e)
464
+
465
+ @mcp.tool()
466
+ def get_cell_value(identifier: Union[str, int], sheet_identifier: Union[str, int], cell_address: str):
467
+ """Gets the value from a specific cell (e.g., 'A1')."""
468
+ if not editor: return {"error": "Excel editor not initialized."}
469
+ try:
470
+ value = editor.get_cell_value(identifier, sheet_identifier, cell_address)
471
+ # Attempt basic serialization for common types COM might return
472
+ if type(value).__name__ == 'datetime': # Handle pywintypes time object
473
+ value = str(value)
474
+ return {"value": value}
475
+ except Exception as e:
476
+ return _handle_excel_tool_error("get_cell_value", e)
477
+
478
+ @mcp.tool()
479
+ def set_cell_value(identifier: Union[str, int], sheet_identifier: Union[str, int], cell_address: str, value: Any):
480
+ """Sets the value of a specific cell (e.g., 'A1')."""
481
+ if not editor: return {"error": "Excel editor not initialized."}
482
+ try:
483
+ editor.set_cell_value(identifier, sheet_identifier, cell_address, value)
484
+ return {"message": f"Successfully set cell '{cell_address}' to {value}."}
485
+ except Exception as e:
486
+ return _handle_excel_tool_error("set_cell_value", e)
487
+
488
+ @mcp.tool()
489
+ def get_range_values(identifier: Union[str, int], sheet_identifier: Union[str, int], range_address: str):
490
+ """Gets values from a range (e.g., 'A1:B5'). Returns a list of lists (rows)."""
491
+ if not editor: return {"error": "Excel editor not initialized."}
492
+ try:
493
+ values_tuple = editor.get_range_values(identifier, sheet_identifier, range_address)
494
+ # Convert tuple of tuples to list of lists for JSON compatibility
495
+ values_list = [list(row) for row in values_tuple] if values_tuple else []
496
+ return {"values": values_list}
497
+ except Exception as e:
498
+ return _handle_excel_tool_error("get_range_values", e)
499
+
500
+ @mcp.tool()
501
+ def set_range_values(identifier: Union[str, int], sheet_identifier: Union[str, int], start_cell: str, values: List[List[Any]]):
502
+ """Sets values in a range starting at start_cell. Expects 'values' as a list of lists."""
503
+ if not editor: return {"error": "Excel editor not initialized."}
504
+ try:
505
+ editor.set_range_values(identifier, sheet_identifier, start_cell, values)
506
+ num_rows = len(values)
507
+ num_cols = len(values[0]) if num_rows > 0 else 0
508
+ return {"message": f"Successfully set {num_rows}x{num_cols} range starting at '{start_cell}'."}
509
+ except Exception as e:
510
+ return _handle_excel_tool_error("set_range_values", e)
511
+
512
+ # --- Server Execution --- #
513
+
514
+ # Optional: Add cleanup for COM objects like in the PowerPoint script
515
+ # import atexit
516
+ # def cleanup_excel_com():
517
+ # global editor
518
+ # if editor and editor.app:
519
+ # # editor.app.Quit() # Careful: This closes Excel! Only use if intended.
520
+ # editor.app = None
521
+ # print("Released Excel application object.")
522
+ # pythoncom.CoUninitialize()
523
+ # print("COM Uninitialized.")
524
+ # atexit.register(cleanup_excel_com)
525
+
526
+ if __name__ == "__main__":
527
+ print("Starting Excel MCP Server (Win32)...")
528
+ if editor is None:
529
+ print("CRITICAL: Excel editor could not be initialized. Server may not function correctly.")
530
+ elif editor.app is None:
531
+ print("Warning: Failed to connect to or launch Excel on startup.")
532
+ else:
533
+ print(f"Successfully connected to Excel version: {editor.app.Version}")
534
+
535
+ # Run the MCP server
536
+ mcp.run()
537
+
538
+ # --- Installation Notes --- #
539
+ # Make sure pywin32 is installed: uv pip install pywin32 (or pip install pywin32)
540
+ # If COM interactions fail unexpectedly after installation, you might need to run
541
+ # the post-install script from an ADMINISTRATOR command prompt:
542
+ # python C:\path\to\your\env\Scripts\pywin32_postinstall.py -install
543
+ # (Adjust path to your environment's Scripts folder)
544
+
545
+ # To get constants like xlOpenXMLWorkbook correctly (instead of magic numbers):
546
+ # 1. Run from python prompt:
547
+ # import win32com.client
548
+ # # Use the correct CLSID for your Excel version (this is for Excel)
549
+ # win32com.client.gencache.EnsureModule('{00020813-0000-0000-C000-000000000046}', 0, 1, 9) # Adjust version numbers if needed (1.9 for Office 365/Excel 2016+)
550
+ # 2. Then you can use:
551
+ # from win32com.client import constants
552
+ # save_format = constants.xlOpenXMLWorkbook
553
+
554
+ save_format = constants.xlOpenXMLWorkbook