Skip to main content
Glama

xlwings Excel MCP Server

by hyunjae-labs
advanced_xlw.py19.9 kB
""" xlwings implementation for advanced Excel features. Includes charts, pivot tables, and Excel tables functionality. """ import xlwings as xw from typing import Dict, Any, List, Optional import logging import os logger = logging.getLogger(__name__) # Import session-based functions from .advanced_xlw_with_wb import ( create_chart_xlw_with_wb, create_pivot_table_xlw_with_wb, create_table_xlw_with_wb ) def create_chart_xlw( filepath: str, sheet_name: str, data_range: str, chart_type: str, target_cell: str, title: str = "", x_axis: str = "", y_axis: str = "" ) -> Dict[str, Any]: """ Create a chart in Excel using xlwings. Args: filepath: Path to Excel file sheet_name: Name of worksheet data_range: Range of data for chart (e.g., "A1:C10") chart_type: Type of chart (line, bar, pie, scatter, area, column) target_cell: Cell where chart will be positioned title: Chart title x_axis: X-axis label y_axis: Y-axis label Returns: Dict with success message or error """ app = None wb = None try: logger.info(f"📈 Creating {chart_type} chart 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] # Map chart types to Excel constants chart_type_map = { 'line': 4, # xlLine 'bar': 57, # xlBarClustered 'column': 51, # xlColumnClustered 'pie': 5, # xlPie 'scatter': 74, # xlXYScatter 'area': 1, # xlArea } if chart_type.lower() not in chart_type_map: return {"error": f"Unsupported chart type: {chart_type}"} excel_chart_type = chart_type_map[chart_type.lower()] # Get data range first data_range_obj = sheet.range(data_range) # Create chart using xlwings method chart = sheet.charts.add() # Set data source chart.set_source_data(data_range_obj) # Set chart type - handle COM API properly try: if hasattr(chart, 'chart_type'): # Use xlwings built-in chart type property chart.chart_type = chart_type.lower() else: # Use COM API more carefully chart_api = chart.api if hasattr(chart_api, 'ChartType'): chart_api.ChartType = excel_chart_type else: logger.warning("Cannot set chart type - using default") except Exception as e: logger.warning(f"Chart type setting failed: {e}, using default") # Set chart position target = sheet.range(target_cell) chart.top = target.top chart.left = target.left # Calculate chart size based on data range data_rows = data_range_obj.rows.count data_cols = data_range_obj.columns.count # Dynamic sizing based on data chart.width = min(600, max(400, data_cols * 80)) # Adjust width based on columns chart.height = min(450, max(300, data_rows * 15)) # Adjust height based on rows # Set chart properties safely try: chart_com = chart.api # Set title if title and hasattr(chart_com, 'HasTitle'): chart_com.HasTitle = True if hasattr(chart_com, 'ChartTitle'): chart_com.ChartTitle.Text = title # Set axis labels if hasattr(chart_com, 'Axes'): try: if x_axis: x_axis_obj = chart_com.Axes(1) # xlCategory x_axis_obj.HasTitle = True x_axis_obj.AxisTitle.Text = x_axis if y_axis: y_axis_obj = chart_com.Axes(2) # xlValue y_axis_obj.HasTitle = True y_axis_obj.AxisTitle.Text = y_axis except Exception as e: logger.warning(f"Axis label setting failed: {e}") except: # Some chart types don't have axes pass # Save the workbook wb.save() logger.info(f"✅ Successfully created {chart_type} chart") return { "message": f"Successfully created {chart_type} chart", "chart_type": chart_type, "data_range": data_range, "position": target_cell, "sheet": sheet_name } except Exception as e: logger.error(f"❌ Error creating chart: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() def create_pivot_table_xlw( filepath: str, sheet_name: str, data_range: str, rows: List[str], values: List[str], columns: Optional[List[str]] = None, agg_func: str = "sum", target_sheet: Optional[str] = None, target_cell: str = None, pivot_name: Optional[str] = None ) -> Dict[str, Any]: """ Create a pivot table in Excel using xlwings. Args: filepath: Path to Excel file sheet_name: Name of worksheet containing source data data_range: Source data range (e.g., "A1:E100" or "Sheet2!A1:E100") rows: Field names for row labels values: Field names for values columns: Field names for column labels (optional) agg_func: Aggregation function (sum, count, average, max, min) target_sheet: Target sheet for pivot table (optional) target_cell: Target cell for pivot table (optional, default finds empty area) pivot_name: Custom name for pivot table (optional) Returns: Dict with success message or error """ app = None wb = None try: logger.info(f"📊 Creating pivot table 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"} # Parse data range to support cross-sheet references if "!" in data_range: # Format: "SheetName!A1:E100" source_sheet_name, range_part = data_range.split("!", 1) # Remove quotes if present source_sheet_name = source_sheet_name.strip("'\"") if source_sheet_name not in sheet_names: return {"error": f"Source sheet '{source_sheet_name}' not found"} source_sheet = wb.sheets[source_sheet_name] source_range = source_sheet.range(range_part) else: # Use the provided sheet_name source_sheet = wb.sheets[sheet_name] source_range = source_sheet.range(data_range) # Determine target sheet for pivot table if target_sheet: # Use specified target sheet if target_sheet not in sheet_names: # Create if doesn't exist pivot_sheet = wb.sheets.add(target_sheet) else: pivot_sheet = wb.sheets[target_sheet] else: # Auto-generate unique pivot sheet name pivot_sheet_name = "PivotTable" counter = 1 while pivot_sheet_name in sheet_names: pivot_sheet_name = f"PivotTable{counter}" counter += 1 pivot_sheet = wb.sheets.add(pivot_sheet_name) # Determine target cell position if not target_cell: # Find empty area automatically used_range = pivot_sheet.used_range if used_range: # Place below existing content with some spacing target_cell = f"A{used_range.last_cell.row + 3}" else: target_cell = "A3" # Default position if sheet is empty # Use COM API to create pivot table pivot_cache = wb.api.PivotCaches().Create( SourceType=1, # xlDatabase SourceData=source_range.api ) # Generate unique pivot table name if not provided if not pivot_name: existing_pivots = [] try: # Try to get existing pivot table names for sheet in wb.sheets: try: sheet_pivots = sheet.api.PivotTables() for i in range(1, sheet_pivots.Count + 1): existing_pivots.append(sheet_pivots.Item(i).Name) except: pass except: pass # Generate unique name pivot_name = "PivotTable1" counter = 1 while pivot_name in existing_pivots: counter += 1 pivot_name = f"PivotTable{counter}" pivot_table = pivot_cache.CreatePivotTable( TableDestination=pivot_sheet.range(target_cell).api, TableName=pivot_name ) # Get field names from first row of data (use source_range which is already parsed) header_range = source_range.rows[0] field_names = [cell.value for cell in header_range] # Track warnings for partial failures warnings = [] # Add row fields - try different COM API access methods for row_field in rows: if row_field in field_names: success = False try: # Method 1: Direct string access field = pivot_table.PivotFields(row_field) field.Orientation = 1 # xlRowField success = True except: try: # Method 2: Index access field_index = field_names.index(row_field) + 1 field = pivot_table.PivotFields(field_index) field.Orientation = 1 # xlRowField success = True except Exception as e: error_msg = f"Failed to add row field '{row_field}': {str(e)}" logger.warning(error_msg) warnings.append(error_msg) else: warnings.append(f"Row field '{row_field}' not found in data headers") # Add column fields if columns: for col_field in columns: if col_field in field_names: success = False try: # Method 1: Direct string access field = pivot_table.PivotFields(col_field) field.Orientation = 2 # xlColumnField success = True except: try: # Method 2: Index access field_index = field_names.index(col_field) + 1 field = pivot_table.PivotFields(field_index) field.Orientation = 2 # xlColumnField success = True except Exception as e: error_msg = f"Failed to add column field '{col_field}': {str(e)}" logger.warning(error_msg) warnings.append(error_msg) else: warnings.append(f"Column field '{col_field}' not found in data headers") # Add value fields with aggregation # Note: Aggregation function setting is simplified for stability # Users can change aggregation type in Excel after creation for value_field in values: if value_field in field_names: success = False try: # Method 1: Direct string access field = pivot_table.PivotFields(value_field) field.Orientation = 4 # xlDataField success = True logger.info(f"Added value field '{value_field}' successfully") except: try: # Method 2: Index access field_index = field_names.index(value_field) + 1 field = pivot_table.PivotFields(field_index) field.Orientation = 4 # xlDataField success = True logger.info(f"Added value field '{value_field}' using index") except Exception as e: error_msg = f"Failed to add value field '{value_field}': {str(e)}" logger.warning(error_msg) warnings.append(error_msg) # Try to set aggregation function if field was added successfully # This is optional - if it fails, the default (usually Sum) will be used if success and agg_func.lower() != 'sum': try: # Safer approach: iterate through DataFields to find our field agg_map = { 'count': -4112, # xlCount 'average': -4106, # xlAverage 'mean': -4106, # xlAverage (alias) 'max': -4136, # xlMax 'min': -4139, # xlMin } if agg_func.lower() in agg_map: # Wait a moment for COM to update import time time.sleep(0.1) # Try to find and update the data field for i in range(1, pivot_table.DataFields.Count + 1): try: data_field = pivot_table.DataFields(i) # Check if this is our field (name contains the original field name) if value_field in str(data_field.SourceName): data_field.Function = agg_map[agg_func.lower()] logger.info(f"Set aggregation to {agg_func} for {value_field}") break except: continue except Exception as e: # Non-critical: aggregation function setting failed logger.debug(f"Could not set aggregation function for {value_field}: {e}") # Don't add to warnings - field was added successfully else: warnings.append(f"Value field '{value_field}' not found in data headers") # Apply default pivot table style pivot_table.TableStyle2 = "PivotStyleMedium9" # Save the workbook wb.save() # Prepare result result = { "message": f"Successfully created pivot table '{pivot_name}'", "pivot_name": pivot_name, "pivot_sheet": pivot_sheet.name, "pivot_cell": target_cell, "source_range": data_range, "source_sheet": source_sheet.name, "rows": rows, "columns": columns or [], "values": values, "aggregation": agg_func } # Add warnings if any if warnings: result["warnings"] = warnings logger.info(f"⚠️ Pivot table created with warnings: {warnings}") else: logger.info(f"✅ Successfully created pivot table '{pivot_name}' at {pivot_sheet.name}!{target_cell}") return result except Exception as e: logger.error(f"❌ Error creating pivot table: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit() def create_table_xlw( filepath: str, sheet_name: str, data_range: str, table_name: Optional[str] = None, table_style: str = "TableStyleMedium9" ) -> Dict[str, Any]: """ Create an Excel table (ListObject) using xlwings. Args: filepath: Path to Excel file sheet_name: Name of worksheet data_range: Range of data to convert to table (e.g., "A1:D10") table_name: Name for the table (optional) table_style: Excel table style name Returns: Dict with success message or error """ app = None wb = None try: logger.info(f"📋 Creating Excel table 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 data range range_obj = sheet.range(data_range) # Generate table name if not provided if not table_name: existing_tables = sheet.api.ListObjects table_name = f"Table{existing_tables.Count + 1}" # Create table using COM API sheet_com = sheet.api table = sheet_com.ListObjects.Add( SourceType=1, # xlSrcRange Source=range_obj.api, XlListObjectHasHeaders=1 # xlYes ) # Set table name table.Name = table_name # Apply table style table.TableStyle = table_style # Enable filtering table.ShowAutoFilter = True # Enable total row (optional, disabled by default) table.ShowTotals = False # Save the workbook wb.save() logger.info(f"✅ Successfully created table '{table_name}'") return { "message": f"Successfully created Excel table", "table_name": table_name, "data_range": data_range, "style": table_style, "sheet": sheet_name, "has_headers": True, "has_filter": True } except Exception as e: logger.error(f"❌ Error creating table: {str(e)}") return {"error": str(e)} finally: if wb: wb.close() if app: app.quit()

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