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.
- .gitignore +34 -0
- PKG-INFO +359 -0
- README.md +344 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/METADATA +359 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/RECORD +18 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/WHEEL +4 -0
- iflow_mcp_jenstangen1_pptx-0.1.0.dist-info/entry_points.txt +2 -0
- language.json +1 -0
- mcp_excel_server_win32.py +554 -0
- mcp_powerpoint_server.py +1348 -0
- mcp_powerpoint_server_win32.py +766 -0
- package-lock.json +1054 -0
- package.json +5 -0
- package_name +1 -0
- push_info.json +5 -0
- pyproject.toml +26 -0
- requirements.txt +9 -0
- server_config.json +1 -0
|
@@ -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,,
|
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
|