Skip to main content
Glama

xlwings Excel MCP Server

by hyunjae-labs
range_xlw.py34.2 kB
""" xlwings implementation for range operations. Focuses on batch processing efficiency and native Excel capabilities. """ import xlwings as xw from typing import List, Dict, Any, Optional, Tuple import logging import os logger = logging.getLogger(__name__) def merge_cells_xlw(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]: """ Merge cells in Excel using xlwings. Args: filepath: Path to Excel file sheet_name: Name of worksheet start_cell: Top-left cell of merge range end_cell: Bottom-right cell of merge range Returns: Dict with success message or error """ app = None wb = None try: logger.info(f"🔗 Merging cells {start_cell}:{end_cell} in {sheet_name}") # Check if file exists if not os.path.exists(filepath): return {"error": f"File not found: {filepath}"} # Open Excel app and workbook app = xw.App(visible=False, add_book=False) wb = app.books.open(filepath) # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get the range to merge merge_range = sheet.range(f"{start_cell}:{end_cell}") # Check if range is already merged if merge_range.merge_cells: return {"error": f"Range {start_cell}:{end_cell} is already merged"} # Merge the cells merge_range.merge() # Save the workbook wb.save() logger.info(f"✅ Successfully merged cells {start_cell}:{end_cell}") return { "message": f"Successfully merged cells {start_cell}:{end_cell}", "range": f"{start_cell}:{end_cell}", "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error merging cells: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() def unmerge_cells_xlw(filepath: str, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]: """ Unmerge cells in Excel using xlwings. Args: filepath: Path to Excel file sheet_name: Name of worksheet start_cell: Top-left cell of merge range end_cell: Bottom-right cell of merge range Returns: Dict with success message or error """ app = None wb = None try: logger.info(f"🔓 Unmerging cells {start_cell}:{end_cell} in {sheet_name}") # Check if file exists if not os.path.exists(filepath): return {"error": f"File not found: {filepath}"} # Open Excel app and workbook app = xw.App(visible=False, add_book=False) wb = app.books.open(filepath) # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get the range to unmerge unmerge_range = sheet.range(f"{start_cell}:{end_cell}") # Check if range is merged if not unmerge_range.merge_cells: return {"error": f"Range {start_cell}:{end_cell} is not merged"} # Unmerge the cells unmerge_range.unmerge() # Save the workbook wb.save() logger.info(f"✅ Successfully unmerged cells {start_cell}:{end_cell}") return { "message": f"Successfully unmerged cells {start_cell}:{end_cell}", "range": f"{start_cell}:{end_cell}", "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error unmerging cells: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() def get_merged_cells_xlw(filepath: str, sheet_name: str) -> Dict[str, Any]: """ Get all merged cell ranges in a worksheet using xlwings. Args: filepath: Path to Excel file sheet_name: Name of worksheet Returns: Dict with list of merged ranges or error """ app = None wb = None try: logger.info(f"📊 Getting merged cells in {sheet_name}") # Check if file exists if not os.path.exists(filepath): return {"error": f"File not found: {filepath}"} # Open Excel app and workbook app = xw.App(visible=False, add_book=False) wb = app.books.open(filepath) # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get all merged ranges merged_ranges = [] # Use a simpler approach with COM API try: # Access the worksheet's COM object directly ws_com = sheet.api # Check if there are any merged cells in the sheet used_range = sheet.used_range if used_range: # Track processed merged areas to avoid duplicates processed_areas = set() # Get dimensions of used range max_row = used_range.last_cell.row max_col = used_range.last_cell.column # Iterate through the used range for row in range(1, max_row + 1): for col in range(1, max_col + 1): try: # Get the cell using COM API cell_com = ws_com.Cells(row, col) # Check if this cell is part of a merged range if cell_com.MergeCells: # Get the merge area merge_area = cell_com.MergeArea merge_address = merge_area.Address.replace("$", "") # Skip if we've already processed this merged area if merge_address in processed_areas: continue # Add to processed areas processed_areas.add(merge_address) # Get details about the merged range first_row = merge_area.Row first_col = merge_area.Column row_count = merge_area.Rows.Count col_count = merge_area.Columns.Count # Calculate last cell last_row = first_row + row_count - 1 last_col = first_col + col_count - 1 # Create cell addresses def get_column_letter(col_idx): """Convert column index to Excel column string""" result = "" while col_idx > 0: col_idx -= 1 result = chr(col_idx % 26 + ord('A')) + result col_idx //= 26 return result start_addr = f"{get_column_letter(first_col)}{first_row}" end_addr = f"{get_column_letter(last_col)}{last_row}" merged_ranges.append({ "range": merge_address, "start": start_addr, "end": end_addr, "rows": row_count, "columns": col_count }) except: # Skip cells that cause errors continue except Exception as e: logger.warning(f"Could not get merged cells: {e}") logger.info(f"✅ Found {len(merged_ranges)} merged ranges") return { "merged_ranges": merged_ranges, "count": len(merged_ranges), "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error getting merged cells: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() def copy_range_xlw( filepath: str, sheet_name: str, source_start: str, source_end: str, target_start: str, target_sheet: Optional[str] = None ) -> Dict[str, Any]: """ Copy a range of cells to another location using xlwings. Args: filepath: Path to Excel file sheet_name: Name of source worksheet source_start: Top-left cell of source range source_end: Bottom-right cell of source range target_start: Top-left cell of target location target_sheet: Name of target worksheet (optional, defaults to source sheet) Returns: Dict with success message or error """ app = None wb = None try: # Use target_sheet if provided, otherwise use source sheet target_sheet = target_sheet or sheet_name logger.info(f"📋 Copying range {source_start}:{source_end} to {target_start} in {target_sheet}") # Check if file exists if not os.path.exists(filepath): return {"error": f"File not found: {filepath}"} # Open Excel app and workbook app = xw.App(visible=False, add_book=False) wb = app.books.open(filepath) # Check if sheets exist sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Source sheet '{sheet_name}' not found"} if target_sheet not in sheet_names: return {"error": f"Target sheet '{target_sheet}' not found"} source_sheet = wb.sheets[sheet_name] dest_sheet = wb.sheets[target_sheet] # Get source range source_range = source_sheet.range(f"{source_start}:{source_end}") # Copy to target location # xlwings copy method preserves formatting and formulas source_range.copy(destination=dest_sheet.range(target_start)) # Calculate target end cell rows = source_range.rows.count cols = source_range.columns.count target_end_row = dest_sheet.range(target_start).row + rows - 1 target_end_col = dest_sheet.range(target_start).column + cols - 1 target_end = dest_sheet.cells(target_end_row, target_end_col).address.replace("$", "") # Save the workbook wb.save() logger.info(f"✅ Successfully copied range to {target_start}:{target_end}") return { "message": f"Successfully copied range {source_start}:{source_end} to {target_start}", "source_range": f"{source_start}:{source_end}", "target_range": f"{target_start}:{target_end}", "source_sheet": sheet_name, "target_sheet": target_sheet } except Exception as e: logger.error(f"❌ Error copying range: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() def delete_range_xlw( filepath: str, sheet_name: str, start_cell: str, end_cell: str, shift_direction: str = "up" ) -> Dict[str, Any]: """ Delete a range of cells and shift remaining cells using xlwings. Args: filepath: Path to Excel file sheet_name: Name of worksheet start_cell: Top-left cell of range to delete end_cell: Bottom-right cell of range to delete shift_direction: Direction to shift cells ("up" or "left") Returns: Dict with success message or error """ app = None wb = None try: logger.info(f"🗑️ Deleting range {start_cell}:{end_cell} in {sheet_name}, shift {shift_direction}") # Validate shift direction if shift_direction not in ["up", "left"]: return {"error": f"Invalid shift direction: {shift_direction}. Must be 'up' or 'left'"} # Check if file exists if not os.path.exists(filepath): return {"error": f"File not found: {filepath}"} # Open Excel app and workbook app = xw.App(visible=False, add_book=False) wb = app.books.open(filepath) # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get the range to delete delete_range = sheet.range(f"{start_cell}:{end_cell}") # Delete and shift cells if shift_direction == "up": # Shift cells up (xlShiftUp = -4162) delete_range.api.Delete(Shift=-4162) else: # shift_direction == "left" # Shift cells left (xlShiftToLeft = -4159) delete_range.api.Delete(Shift=-4159) # Save the workbook wb.save() logger.info(f"✅ Successfully deleted range {start_cell}:{end_cell}") return { "message": f"Successfully deleted range {start_cell}:{end_cell} and shifted cells {shift_direction}", "deleted_range": f"{start_cell}:{end_cell}", "shift_direction": shift_direction, "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error deleting range: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() # Batch operation helper for efficiency def batch_range_operations_xlw( filepath: str, operations: List[Dict[str, Any]] ) -> Dict[str, Any]: """ Execute multiple range operations in a single Excel session for efficiency. Args: filepath: Path to Excel file operations: List of operations to perform, each with 'type' and parameters Returns: Dict with results of all operations """ app = None wb = None results = [] try: logger.info(f"⚡ Executing {len(operations)} batch operations") # Check if file exists if not os.path.exists(filepath): return {"error": f"File not found: {filepath}"} # Open Excel app and workbook once app = xw.App(visible=False, add_book=False) wb = app.books.open(filepath) # Execute each operation for idx, op in enumerate(operations): op_type = op.get("type") result = {"operation": idx + 1, "type": op_type} try: if op_type == "merge": sheet = wb.sheets[op["sheet_name"]] merge_range = sheet.range(f"{op['start_cell']}:{op['end_cell']}") merge_range.merge() result["status"] = "success" result["message"] = f"Merged {op['start_cell']}:{op['end_cell']}" elif op_type == "unmerge": sheet = wb.sheets[op["sheet_name"]] unmerge_range = sheet.range(f"{op['start_cell']}:{op['end_cell']}") unmerge_range.unmerge() result["status"] = "success" result["message"] = f"Unmerged {op['start_cell']}:{op['end_cell']}" elif op_type == "copy": source_sheet = wb.sheets[op["source_sheet"]] target_sheet = wb.sheets[op.get("target_sheet", op["source_sheet"])] source_range = source_sheet.range(f"{op['source_start']}:{op['source_end']}") source_range.copy(destination=target_sheet.range(op["target_start"])) result["status"] = "success" result["message"] = f"Copied range to {op['target_start']}" elif op_type == "delete": sheet = wb.sheets[op["sheet_name"]] delete_range = sheet.range(f"{op['start_cell']}:{op['end_cell']}") shift = -4162 if op.get("shift_direction", "up") == "up" else -4159 delete_range.api.Delete(Shift=shift) result["status"] = "success" result["message"] = f"Deleted {op['start_cell']}:{op['end_cell']}" else: result["status"] = "error" result["message"] = f"Unknown operation type: {op_type}" except Exception as e: result["status"] = "error" result["message"] = str(e) results.append(result) # Save once after all operations wb.save() # Count successes and failures successes = sum(1 for r in results if r["status"] == "success") failures = sum(1 for r in results if r["status"] == "error") logger.info(f"✅ Batch operations complete: {successes} succeeded, {failures} failed") return { "total_operations": len(operations), "successes": successes, "failures": failures, "results": results } except Exception as e: logger.error(f"❌ Error in batch operations: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() def merge_cells_xlw_with_wb(wb, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]: """ Session-based cell merging using existing workbook object. Args: wb: Workbook object from session sheet_name: Name of worksheet start_cell: Top-left cell of merge range end_cell: Bottom-right cell of merge range Returns: Dict with success message or error """ try: logger.info(f"🔗 Merging cells {start_cell}:{end_cell} in {sheet_name}") # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get the range to merge merge_range = sheet.range(f"{start_cell}:{end_cell}") # Check if range is already merged if merge_range.merge_cells: return {"error": f"Range {start_cell}:{end_cell} is already merged"} # Merge the cells merge_range.merge() # Save the workbook wb.save() logger.info(f"✅ Successfully merged cells {start_cell}:{end_cell}") return { "message": f"Successfully merged cells {start_cell}:{end_cell}", "range": f"{start_cell}:{end_cell}", "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error merging cells: {str(e)}") return {"error": str(e)} def unmerge_cells_xlw_with_wb(wb, sheet_name: str, start_cell: str, end_cell: str) -> Dict[str, Any]: """ Session-based cell unmerging using existing workbook object. Args: wb: Workbook object from session sheet_name: Name of worksheet start_cell: Top-left cell of merge range end_cell: Bottom-right cell of merge range Returns: Dict with success message or error """ try: logger.info(f"🔓 Unmerging cells {start_cell}:{end_cell} in {sheet_name}") # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get the range to unmerge unmerge_range = sheet.range(f"{start_cell}:{end_cell}") # Check if range is merged if not unmerge_range.merge_cells: return {"error": f"Range {start_cell}:{end_cell} is not merged"} # Unmerge the cells unmerge_range.unmerge() # Save the workbook wb.save() logger.info(f"✅ Successfully unmerged cells {start_cell}:{end_cell}") return { "message": f"Successfully unmerged cells {start_cell}:{end_cell}", "range": f"{start_cell}:{end_cell}", "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error unmerging cells: {str(e)}") return {"error": str(e)} def get_merged_cells_xlw_with_wb(wb, sheet_name: str) -> Dict[str, Any]: """ Session-based merged cells retrieval using existing workbook object. Args: wb: Workbook object from session sheet_name: Name of worksheet Returns: Dict with list of merged ranges or error """ try: logger.info(f"📊 Getting merged cells in {sheet_name}") # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get all merged ranges merged_ranges = [] # Use a simpler approach with COM API try: # Access the worksheet's COM object directly ws_com = sheet.api # Check if there are any merged cells in the sheet used_range = sheet.used_range if used_range: # Track processed merged areas to avoid duplicates processed_areas = set() # Get dimensions of used range max_row = used_range.last_cell.row max_col = used_range.last_cell.column # Iterate through the used range for row in range(1, max_row + 1): for col in range(1, max_col + 1): try: # Get the cell using COM API cell_com = ws_com.Cells(row, col) # Check if this cell is part of a merged range if cell_com.MergeCells: # Get the merge area merge_area = cell_com.MergeArea merge_address = merge_area.Address.replace("$", "") # Skip if we've already processed this merged area if merge_address in processed_areas: continue # Add to processed areas processed_areas.add(merge_address) # Get details about the merged range first_row = merge_area.Row first_col = merge_area.Column row_count = merge_area.Rows.Count col_count = merge_area.Columns.Count # Calculate last cell last_row = first_row + row_count - 1 last_col = first_col + col_count - 1 # Create cell addresses def get_column_letter(col_idx): """Convert column index to Excel column string""" result = "" while col_idx > 0: col_idx -= 1 result = chr(col_idx % 26 + ord('A')) + result col_idx //= 26 return result start_addr = f"{get_column_letter(first_col)}{first_row}" end_addr = f"{get_column_letter(last_col)}{last_row}" merged_ranges.append({ "range": merge_address, "start": start_addr, "end": end_addr, "rows": row_count, "columns": col_count }) except: # Skip cells that cause errors continue except Exception as e: logger.warning(f"Could not get merged cells: {e}") logger.info(f"✅ Found {len(merged_ranges)} merged ranges") return { "merged_ranges": merged_ranges, "count": len(merged_ranges), "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error getting merged cells: {str(e)}") return {"error": str(e)} def copy_range_xlw_with_wb( wb, sheet_name: str, source_start: str, source_end: str, target_start: str, target_sheet: Optional[str] = None ) -> Dict[str, Any]: """ Session-based range copying using existing workbook object. Args: wb: Workbook object from session sheet_name: Name of source worksheet source_start: Top-left cell of source range source_end: Bottom-right cell of source range target_start: Top-left cell of target location target_sheet: Name of target worksheet (optional, defaults to source sheet) Returns: Dict with success message or error """ try: # Use target_sheet if provided, otherwise use source sheet target_sheet = target_sheet or sheet_name logger.info(f"📋 Copying range {source_start}:{source_end} to {target_start} in {target_sheet}") # Check if sheets exist sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Source sheet '{sheet_name}' not found"} if target_sheet not in sheet_names: return {"error": f"Target sheet '{target_sheet}' not found"} source_sheet = wb.sheets[sheet_name] dest_sheet = wb.sheets[target_sheet] # Get source range source_range = source_sheet.range(f"{source_start}:{source_end}") # Copy to target location # xlwings copy method preserves formatting and formulas source_range.copy(destination=dest_sheet.range(target_start)) # Calculate target end cell rows = source_range.rows.count cols = source_range.columns.count target_end_row = dest_sheet.range(target_start).row + rows - 1 target_end_col = dest_sheet.range(target_start).column + cols - 1 target_end = dest_sheet.cells(target_end_row, target_end_col).address.replace("$", "") # Save the workbook wb.save() logger.info(f"✅ Successfully copied range to {target_start}:{target_end}") return { "message": f"Successfully copied range {source_start}:{source_end} to {target_start}", "source_range": f"{source_start}:{source_end}", "target_range": f"{target_start}:{target_end}", "source_sheet": sheet_name, "target_sheet": target_sheet } except Exception as e: logger.error(f"❌ Error copying range: {str(e)}") return {"error": str(e)} def delete_range_xlw_with_wb( wb, sheet_name: str, start_cell: str, end_cell: str, shift_direction: str = "up" ) -> Dict[str, Any]: """ Session-based range deletion using existing workbook object. Args: wb: Workbook object from session sheet_name: Name of worksheet start_cell: Top-left cell of range to delete end_cell: Bottom-right cell of range to delete shift_direction: Direction to shift cells ("up" or "left") Returns: Dict with success message or error """ try: logger.info(f"🗑️ Deleting range {start_cell}:{end_cell} in {sheet_name}, shift {shift_direction}") # Validate shift direction if shift_direction not in ["up", "left"]: return {"error": f"Invalid shift direction: {shift_direction}. Must be 'up' or 'left'"} # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found"} sheet = wb.sheets[sheet_name] # Get the range to delete delete_range = sheet.range(f"{start_cell}:{end_cell}") # Delete and shift cells if shift_direction == "up": # Shift cells up (xlShiftUp = -4162) delete_range.api.Delete(Shift=-4162) else: # shift_direction == "left" # Shift cells left (xlShiftToLeft = -4159) delete_range.api.Delete(Shift=-4159) # Save the workbook wb.save() logger.info(f"✅ Successfully deleted range {start_cell}:{end_cell}") return { "message": f"Successfully deleted range {start_cell}:{end_cell} and shifted cells {shift_direction}", "deleted_range": f"{start_cell}:{end_cell}", "shift_direction": shift_direction, "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error deleting range: {str(e)}") return {"error": str(e)} def validate_excel_range_xlw_with_wb( wb, sheet_name: str, start_cell: str, end_cell: str = None ) -> Dict[str, Any]: """ Session-based range validation using existing workbook object. Args: wb: Workbook object from session sheet_name: Name of worksheet start_cell: Starting cell address end_cell: Ending cell address (optional) Returns: Dict containing validation result and range information """ try: logger.info(f"🔍 Validating range {start_cell}:{end_cell or start_cell} in {sheet_name}") # Check if sheet exists sheet_names = [s.name for s in wb.sheets] if sheet_name not in sheet_names: return {"error": f"Sheet '{sheet_name}' not found", "valid": False} sheet = wb.sheets[sheet_name] # Validate the range try: if end_cell: range_obj = sheet.range(f"{start_cell}:{end_cell}") else: range_obj = sheet.range(start_cell) # Get range information range_info = { "message": f"Range validation successful: {range_obj.address}", "valid": True, "range": range_obj.address, "start_cell": start_cell, "end_cell": end_cell, "rows": range_obj.rows.count, "columns": range_obj.columns.count, "size": range_obj.rows.count * range_obj.columns.count, "sheet": sheet_name, "has_data": bool(range_obj.value is not None) } # Check if range has any data if range_obj.value: if isinstance(range_obj.value, (list, tuple)): non_empty_count = sum(1 for row in range_obj.value if row and any(cell for cell in (row if isinstance(row, (list, tuple)) else [row]) if cell is not None)) else: non_empty_count = 1 if range_obj.value is not None else 0 range_info["non_empty_cells"] = non_empty_count else: range_info["non_empty_cells"] = 0 logger.info(f"✅ Range validation successful: {range_obj.address}") return range_info except Exception as range_error: return { "error": f"Invalid range: {range_error}", "valid": False, "start_cell": start_cell, "end_cell": end_cell, "sheet": sheet_name } except Exception as e: logger.error(f"Error validating range: {e}") return {"error": str(e), "valid": False}

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hyunjae-labs/xlwings-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server