Skip to main content
Glama

xlwings Excel MCP Server

by hyunjae-labs
server.py61.6 kB
import logging import os from typing import Any, List, Dict, Optional from mcp.server.fastmcp import FastMCP # Excel 엔진: xlwings 구현으로 완전 전환 완료 # Import exceptions from xlwings_mcp.exceptions import ( ValidationError, WorkbookError, SheetError, DataError, FormattingError, CalculationError, PivotError, ChartError ) # All functions now use xlwings_impl - legacy imports removed for clean architecture # Import session management from xlwings_mcp.session import SESSION_MANAGER from xlwings_mcp.force_close import force_close_workbook_by_path # Get project root directory path for log file path. # When using the stdio transmission method, # relative paths may cause log files to fail to create # due to the client's running location and permission issues, # resulting in the program not being able to run. # Thus using os.path.join(ROOT_DIR, "excel-mcp.log") instead. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) LOGS_DIR = os.path.join(ROOT_DIR, "logs") os.makedirs(LOGS_DIR, exist_ok=True) LOG_FILE = os.path.join(LOGS_DIR, "excel-mcp.log") # Initialize EXCEL_FILES_PATH variable without assigning a value EXCEL_FILES_PATH = None # xlwings 구현 사용 (openpyxl 마이그레이션 완료) # Configure logging with rotation to prevent infinite log growth from logging.handlers import RotatingFileHandler logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ # Referring to https://github.com/modelcontextprotocol/python-sdk/issues/409#issuecomment-2816831318 # The stdio mode server MUST NOT write anything to its stdout that is not a valid MCP message. # Use RotatingFileHandler to prevent infinite log growth (5MB max, keep 3 backup files) RotatingFileHandler(LOG_FILE, maxBytes=5*1024*1024, backupCount=3) ], ) logger = logging.getLogger("excel-mcp") logger.info("🚀 Excel MCP Server starting - xlwings 모드 활성화") # Error message templates for consistent error reporting ERROR_TEMPLATES = { 'SESSION_NOT_FOUND': "SESSION_NOT_FOUND: Session '{session_id}' not found. It may have expired after {ttl} minutes of inactivity. Use open_workbook() to create a new session.", 'SESSION_TIMEOUT': "SESSION_TIMEOUT: Session '{session_id}' expired at {time}. Create new session with open_workbook()", 'FILE_LOCKED': "FILE_ACCESS_ERROR: '{filepath}' is locked by another process. Use force_close_workbook_by_path() to force close it first.", 'FILE_NOT_FOUND': "FILE_NOT_FOUND: '{filepath}' does not exist. Check the path or create a new workbook with create_workbook().", 'SHEET_NOT_FOUND': "SHEET_NOT_FOUND: Sheet '{sheet_name}' not found in workbook. Available sheets: {sheets}", 'INVALID_RANGE': "INVALID_RANGE: Range '{range}' is not valid. Use format like 'A1' or 'A1:B10'.", 'PARAMETER_MISSING': "PARAMETER_MISSING: Either {param1} or {param2} must be provided.", } # Session validation decorator for DRY principle def get_validated_session(session_id: str): """ Helper function to validate session_id and return session object. Centralizes session validation logic for DRY principle. Args: session_id: Session ID to validate Returns: Session object if valid, error message string if invalid """ if not session_id: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='valid session' ) session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 ) return session # Initialize FastMCP server mcp = FastMCP( "excel-mcp", instructions="Excel MCP Server for manipulating Excel files" ) def get_excel_path(filename: str) -> str: """Get full path to Excel file. Args: filename: Name of Excel file Returns: Full path to Excel file """ # If filename is already an absolute path, return it if os.path.isabs(filename): return filename # Check if in SSE mode (EXCEL_FILES_PATH is not None) if EXCEL_FILES_PATH is None: # Must use absolute path raise ValueError(f"Invalid filename: {filename}, must be an absolute path when not in SSE mode") # In SSE mode, if it's a relative path, resolve it based on EXCEL_FILES_PATH return os.path.join(EXCEL_FILES_PATH, filename) # ============================================================================ # SESSION MANAGEMENT TOOLS (NEW) # ============================================================================ @mcp.tool() def open_workbook( filepath: str, visible: bool = False, read_only: bool = False ) -> Dict[str, Any]: """ Open an Excel workbook and create a session. Args: filepath: Path to Excel file visible: Whether to show Excel window (default: False) read_only: Whether to open in read-only mode (default: False) Returns: Dictionary with session_id, filepath, visible, read_only, and sheets """ try: full_path = get_excel_path(filepath) session_id = SESSION_MANAGER.open_workbook(full_path, visible, read_only) # Get session info session = SESSION_MANAGER.get_session(session_id) if not session: raise WorkbookError(f"Failed to create session for {filepath}") return { "session_id": session_id, "filepath": session.filepath, "visible": session.visible, "read_only": session.read_only, "sheets": [sheet.name for sheet in session.workbook.sheets] } except Exception as e: logger.error(f"Error opening workbook: {e}") raise WorkbookError(f"Failed to open workbook: {str(e)}") @mcp.tool() def close_workbook( session_id: str, save: bool = True ) -> str: """ Close a workbook session. Args: session_id: Session ID from open_workbook save: Whether to save changes (default: True) Returns: Success message """ try: success = SESSION_MANAGER.close_workbook(session_id, save) if not success: raise WorkbookError(f"Session {session_id} not found") return f"Workbook session {session_id} closed successfully" except Exception as e: logger.error(f"Error closing workbook: {e}") raise WorkbookError(f"Failed to close workbook: {str(e)}") @mcp.tool() def list_workbooks() -> List[Dict[str, Any]]: """ List all open workbook sessions. Returns: List of session information dictionaries """ try: return SESSION_MANAGER.list_sessions() except Exception as e: logger.error(f"Error listing workbooks: {e}") raise WorkbookError(f"Failed to list workbooks: {str(e)}") @mcp.tool() def force_close_workbook_by_path_tool( filepath: str ) -> Dict[str, Any]: """ Force close a specific workbook by file path (without saving). Args: filepath: Path to the workbook to force close Returns: Dictionary with 'closed' (bool) and 'message' (str) """ try: full_path = get_excel_path(filepath) return force_close_workbook_by_path(full_path) except Exception as e: logger.error(f"Error force closing workbook: {e}") return { "closed": False, "message": f"Failed to force close workbook: {str(e)}" } # ============================================================================ # EXISTING TOOLS (TO BE UPDATED WITH SESSION SUPPORT) # ============================================================================ @mcp.tool() def apply_formula( session_id: str, sheet_name: str, cell: str, formula: str ) -> str: """ Apply Excel formula to cell. Args: session_id: Session ID from open_workbook (required) sheet_name: Name of worksheet cell: Cell address (e.g., "A1") formula: Excel formula to apply """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.calculations_xlw import apply_formula_xlw_with_wb result = apply_formula_xlw_with_wb(session.workbook, sheet_name, cell, formula) return result.get("message", "Formula applied successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, CalculationError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error applying formula: {e}") raise @mcp.tool() def validate_formula_syntax( sheet_name: str, cell: str, formula: str, session_id: Optional[str] = None, filepath: Optional[str] = None ) -> str: """ Validate Excel formula syntax without applying it. Args: sheet_name: Name of worksheet cell: Cell address (e.g., "A1") formula: Excel formula to validate session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.calculations_xlw import validate_formula_syntax_xlw_with_wb result = validate_formula_syntax_xlw_with_wb(session.workbook, sheet_name, cell, formula) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.calculations_xlw import validate_formula_syntax_xlw result = validate_formula_syntax_xlw(full_path, sheet_name, cell, formula) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Formula validation completed") if "error" not in result else f"Error: {result['error']}" except (ValidationError, CalculationError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error validating formula: {e}") raise @mcp.tool() def format_range( sheet_name: str, start_cell: str, session_id: Optional[str] = None, filepath: Optional[str] = None, end_cell: Optional[str] = None, bold: bool = False, italic: bool = False, underline: bool = False, font_size: Optional[int] = None, font_color: Optional[str] = None, bg_color: Optional[str] = None, border_style: Optional[str] = None, border_color: Optional[str] = None, number_format: Optional[str] = None, alignment: Optional[str] = None, wrap_text: bool = False, merge_cells: bool = False, protection: Optional[Dict[str, Any]] = None, conditional_format: Optional[Dict[str, Any]] = None ) -> str: """ Apply formatting to a range of cells. Args: sheet_name: Name of worksheet start_cell: Starting cell session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) end_cell: Ending cell (optional) bold: Apply bold formatting italic: Apply italic formatting underline: Apply underline formatting font_size: Font size font_color: Font color bg_color: Background color border_style: Border style border_color: Border color number_format: Number format alignment: Text alignment wrap_text: Enable text wrapping merge_cells: Merge cells in range protection: Cell protection settings (optional) conditional_format: Conditional formatting settings (optional) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.formatting_xlw import format_range_xlw_with_wb result = format_range_xlw_with_wb( session.workbook, sheet_name=sheet_name, start_cell=start_cell, end_cell=end_cell, bold=bold, italic=italic, underline=underline, font_size=font_size, font_color=font_color, bg_color=bg_color, border_style=border_style, border_color=border_color, number_format=number_format, alignment=alignment, wrap_text=wrap_text, merge_cells=merge_cells ) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.formatting_xlw import format_range_xlw result = format_range_xlw( filepath=full_path, sheet_name=sheet_name, start_cell=start_cell, end_cell=end_cell, bold=bold, italic=italic, underline=underline, font_size=font_size, font_color=font_color, bg_color=bg_color, border_style=border_style, border_color=border_color, number_format=number_format, alignment=alignment, wrap_text=wrap_text, merge_cells=merge_cells ) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Range formatted successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, FormattingError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error formatting range: {e}") raise @mcp.tool() def read_data_from_excel( session_id: str, sheet_name: str, start_cell: Optional[str] = None, end_cell: Optional[str] = None, preview_only: bool = False ) -> str: """ Read data from Excel worksheet with cell metadata including validation rules. Args: session_id: Session ID from open_workbook (required) sheet_name: Name of worksheet start_cell: Starting cell (default A1) end_cell: Ending cell (optional, auto-expands if not provided) preview_only: Whether to return preview only """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.data_xlw import read_data_from_excel_xlw_with_wb return read_data_from_excel_xlw_with_wb(session.workbook, sheet_name, start_cell, end_cell, preview_only) except (ValidationError, DataError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error reading data: {e}") raise @mcp.tool() def write_data_to_excel( session_id: str, sheet_name: str, data: List[List], start_cell: Optional[str] = None ) -> str: """ Write data to Excel worksheet. Excel formula will write to cell without any verification. Args: session_id: Session ID from open_workbook (required) sheet_name: Name of worksheet to write to data: List of lists containing data to write to the worksheet, sublists are assumed to be rows start_cell: Cell to start writing to (optional, auto-finds appropriate location) """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.data_xlw import write_data_to_excel_xlw_with_wb result = write_data_to_excel_xlw_with_wb(session.workbook, sheet_name, data, start_cell) return result.get("message", "Data written successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, DataError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error writing data: {e}") raise @mcp.tool() def create_workbook( session_id: Optional[str] = None, filepath: Optional[str] = None ) -> str: """ Create new Excel workbook. Args: session_id: Session ID for creating workbook in existing session (optional) filepath: Path to create new Excel file (legacy, deprecated) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session (though this is less common for creating new workbooks) session = SESSION_MANAGER.get_session(session_id) if not session: return f"Error: Session {session_id} not found. Please open the workbook first using open_workbook()." with session.lock: from xlwings_mcp.xlwings_impl.workbook_xlw import create_workbook_xlw_with_wb result = create_workbook_xlw_with_wb(session.workbook) return result.get("message", "Workbook created successfully") if "error" not in result else f"Error: {result['error']}" elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.workbook import create_workbook as create_workbook_impl create_workbook_impl(full_path) return f"Created workbook at {full_path}" else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) except WorkbookError as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error creating workbook: {e}") raise @mcp.tool() def create_worksheet( session_id: str, sheet_name: str ) -> str: """ Create new worksheet in workbook. Args: session_id: Session ID from open_workbook (required) sheet_name: Name of the new worksheet """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.sheet_xlw import create_worksheet_xlw_with_wb result = create_worksheet_xlw_with_wb(session.workbook, sheet_name) return result.get("message", "Worksheet created successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, WorkbookError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error creating worksheet: {e}") raise @mcp.tool() def create_chart( sheet_name: str, data_range: str, chart_type: str, target_cell: str, session_id: Optional[str] = None, filepath: Optional[str] = None, title: str = "", x_axis: str = "", y_axis: str = "" ) -> str: """ Create chart in worksheet. Args: sheet_name: Name of worksheet data_range: Data range for chart chart_type: Type of chart target_cell: Cell where chart will be placed session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) title: Chart title (optional) x_axis: X-axis label (optional) y_axis: Y-axis label (optional) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.advanced_xlw import create_chart_xlw_with_wb result = create_chart_xlw_with_wb( session.workbook, sheet_name=sheet_name, data_range=data_range, chart_type=chart_type, target_cell=target_cell, title=title, x_axis=x_axis, y_axis=y_axis ) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.advanced_xlw import create_chart_xlw result = create_chart_xlw( filepath=full_path, sheet_name=sheet_name, data_range=data_range, chart_type=chart_type, target_cell=target_cell, title=title, x_axis=x_axis, y_axis=y_axis ) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Chart created successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, ChartError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error creating chart: {e}") raise @mcp.tool() def create_pivot_table( sheet_name: str, data_range: str, rows: List[str], values: List[str], session_id: Optional[str] = None, filepath: Optional[str] = None, columns: Optional[List[str]] = None, agg_func: str = "mean", target_sheet: Optional[str] = None, target_cell: Optional[str] = None, pivot_name: Optional[str] = None ) -> str: """ Create pivot table in worksheet. Args: 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 session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) columns: Field names for column labels (optional) agg_func: Aggregation function (sum, count, average, max, min) target_sheet: Target sheet for pivot table (optional, auto-created if not exists) target_cell: Target cell for pivot table (optional, finds empty area if not provided) pivot_name: Custom name for pivot table (optional, auto-generated if not provided) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.advanced_xlw import create_pivot_table_xlw_with_wb result = create_pivot_table_xlw_with_wb( session.workbook, sheet_name=sheet_name, data_range=data_range, rows=rows, values=values, columns=columns, agg_func=agg_func, target_sheet=target_sheet, target_cell=target_cell, pivot_name=pivot_name ) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.advanced_xlw import create_pivot_table_xlw result = create_pivot_table_xlw( filepath=full_path, sheet_name=sheet_name, data_range=data_range, rows=rows, values=values, columns=columns, agg_func=agg_func, target_sheet=target_sheet, target_cell=target_cell, pivot_name=pivot_name ) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) # Handle warnings in response if "warnings" in result and result["warnings"]: warning_msg = "; ".join(result["warnings"]) return f"{result.get('message', 'Pivot table created')} (Warnings: {warning_msg})" return result.get("message", "Pivot table created successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, PivotError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error creating pivot table: {e}") raise @mcp.tool() def create_table( sheet_name: str, data_range: str, session_id: Optional[str] = None, filepath: Optional[str] = None, table_name: Optional[str] = None, table_style: str = "TableStyleMedium9" ) -> str: """ Creates a native Excel table from a specified range of data. Args: sheet_name: Name of worksheet data_range: Range of data to create table from session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) table_name: Name for the table (optional) table_style: Style for the table (optional) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.advanced_xlw import create_table_xlw_with_wb result = create_table_xlw_with_wb( session.workbook, sheet_name=sheet_name, data_range=data_range, table_name=table_name, table_style=table_style ) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.advanced_xlw import create_table_xlw result = create_table_xlw( filepath=full_path, sheet_name=sheet_name, data_range=data_range, table_name=table_name, table_style=table_style ) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Table created successfully") if "error" not in result else f"Error: {result['error']}" except DataError as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error creating table: {e}") raise @mcp.tool() def copy_worksheet( session_id: str, source_sheet: str, target_sheet: str ) -> str: """ Copy worksheet within workbook. Args: session_id: Session ID from open_workbook (required) source_sheet: Name of the source worksheet target_sheet: Name of the target worksheet """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.sheet_xlw import copy_worksheet_xlw_with_wb result = copy_worksheet_xlw_with_wb(session.workbook, source_sheet, target_sheet) return result.get("message", "Worksheet copied successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error copying worksheet: {e}") raise @mcp.tool() def delete_worksheet( session_id: str, sheet_name: str ) -> str: """ Delete worksheet from workbook. Args: session_id: Session ID from open_workbook (required) sheet_name: Name of the worksheet to delete """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.sheet_xlw import delete_worksheet_xlw_with_wb result = delete_worksheet_xlw_with_wb(session.workbook, sheet_name) return result.get("message", "Worksheet deleted successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error deleting worksheet: {e}") raise @mcp.tool() def rename_worksheet( session_id: str, old_name: str, new_name: str ) -> str: """ Rename worksheet in workbook. Args: session_id: Session ID from open_workbook (required) old_name: Current name of the worksheet new_name: New name for the worksheet """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.sheet_xlw import rename_worksheet_xlw_with_wb result = rename_worksheet_xlw_with_wb(session.workbook, old_name, new_name) return result.get("message", "Worksheet renamed successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error renaming worksheet: {e}") raise @mcp.tool() def get_workbook_metadata( session_id: str, include_ranges: bool = False ) -> str: """ Get metadata about workbook including sheets, ranges, etc. Args: session_id: Session ID from open_workbook (required) include_ranges: Whether to include range information """ try: # Validate session using centralized helper session = get_validated_session(session_id) if isinstance(session, str): # Error message returned return session with session.lock: from xlwings_mcp.xlwings_impl.workbook_xlw import get_workbook_metadata_xlw_with_wb result = get_workbook_metadata_xlw_with_wb(session.workbook, include_ranges=include_ranges) if "error" in result: return f"Error: {result['error']}" import json return json.dumps(result, indent=2, default=str, ensure_ascii=False) except (ValidationError, WorkbookError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error getting workbook metadata: {e}") raise @mcp.tool() def merge_cells( sheet_name: str, start_cell: str, end_cell: str, session_id: Optional[str] = None, filepath: Optional[str] = None ) -> str: """ Merge a range of cells. Args: sheet_name: Name of worksheet start_cell: Starting cell end_cell: Ending cell session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.range_xlw import merge_cells_xlw_with_wb result = merge_cells_xlw_with_wb(session.workbook, sheet_name, start_cell, end_cell) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.range_xlw import merge_cells_xlw result = merge_cells_xlw(full_path, sheet_name, start_cell, end_cell) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Cells merged successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error merging cells: {e}") raise @mcp.tool() def unmerge_cells( sheet_name: str, start_cell: str, end_cell: str, session_id: Optional[str] = None, filepath: Optional[str] = None ) -> str: """ Unmerge a range of cells. Args: sheet_name: Name of worksheet start_cell: Starting cell end_cell: Ending cell session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.range_xlw import unmerge_cells_xlw_with_wb result = unmerge_cells_xlw_with_wb(session.workbook, sheet_name, start_cell, end_cell) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.range_xlw import unmerge_cells_xlw result = unmerge_cells_xlw(full_path, sheet_name, start_cell, end_cell) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Cells unmerged successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error unmerging cells: {e}") raise @mcp.tool() def get_merged_cells( sheet_name: str, session_id: Optional[str] = None, filepath: Optional[str] = None ) -> str: """ Get merged cells in a worksheet. Args: sheet_name: Name of worksheet session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.range_xlw import get_merged_cells_xlw_with_wb result = get_merged_cells_xlw_with_wb(session.workbook, sheet_name) if "error" in result: return f"Error: {result['error']}" import json return json.dumps(result, indent=2, default=str) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.range_xlw import get_merged_cells_xlw result = get_merged_cells_xlw(full_path, sheet_name) if "error" in result: return f"Error: {result['error']}" import json return json.dumps(result, indent=2, default=str) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error getting merged cells: {e}") raise @mcp.tool() def copy_range( sheet_name: str, source_start: str, source_end: str, target_start: str, session_id: Optional[str] = None, filepath: Optional[str] = None, target_sheet: Optional[str] = None ) -> str: """ Copy a range of cells to another location. Args: sheet_name: Name of source worksheet source_start: Starting cell of source range source_end: Ending cell of source range target_start: Starting cell of target range session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) target_sheet: Target worksheet (optional, uses source sheet if not provided) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.range_xlw import copy_range_xlw_with_wb result = copy_range_xlw_with_wb( session.workbook, sheet_name, source_start, source_end, target_start, target_sheet or sheet_name # Use source sheet if target_sheet is None ) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.range_xlw import copy_range_xlw result = copy_range_xlw( full_path, sheet_name, source_start, source_end, target_start, target_sheet or sheet_name # Use source sheet if target_sheet is None ) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Range copied successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error copying range: {e}") raise @mcp.tool() def delete_range( sheet_name: str, start_cell: str, end_cell: str, session_id: Optional[str] = None, filepath: Optional[str] = None, shift_direction: str = "up" ) -> str: """ Delete a range of cells and shift remaining cells. Args: sheet_name: Name of worksheet start_cell: Starting cell end_cell: Ending cell session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) shift_direction: Direction to shift cells ("up" or "left") Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.range_xlw import delete_range_xlw_with_wb result = delete_range_xlw_with_wb( session.workbook, sheet_name, start_cell, end_cell, shift_direction ) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.range_xlw import delete_range_xlw result = delete_range_xlw( full_path, sheet_name, start_cell, end_cell, shift_direction ) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Range deleted successfully") if "error" not in result else f"Error: {result['error']}" except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error deleting range: {e}") raise @mcp.tool() def validate_excel_range( sheet_name: str, start_cell: str, session_id: Optional[str] = None, filepath: Optional[str] = None, end_cell: Optional[str] = None ) -> str: """ Validate if a range exists and is properly formatted. Args: sheet_name: Name of worksheet start_cell: Starting cell session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) end_cell: Ending cell (optional) Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.validation_xlw import validate_excel_range_xlw_with_wb result = validate_excel_range_xlw_with_wb(session.workbook, sheet_name, start_cell, end_cell) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.validation_xlw import validate_excel_range_xlw result = validate_excel_range_xlw(full_path, sheet_name, start_cell, end_cell) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) return result.get("message", "Range validation completed") if "error" not in result else f"Error: {result['error']}" except (ValidationError, DataError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error validating range: {e}") raise @mcp.tool() def get_data_validation_info( sheet_name: str, session_id: Optional[str] = None, filepath: Optional[str] = None ) -> str: """ Get all data validation rules in a worksheet. This tool helps identify which cell ranges have validation rules and what types of validation are applied. Args: sheet_name: Name of worksheet session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) Note: Use session_id for better performance. filepath parameter is deprecated. Returns: JSON string containing all validation rules in the worksheet """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.validation_xlw import get_data_validation_info_xlw_with_wb result = get_data_validation_info_xlw_with_wb(session.workbook, sheet_name) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.validation_xlw import get_data_validation_info_xlw result = get_data_validation_info_xlw(full_path, sheet_name) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) if "error" in result: return f"Error: {result['error']}" import json return json.dumps(result, indent=2, default=str) except (ValidationError, DataError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error getting validation info: {e}") raise @mcp.tool() def insert_rows( sheet_name: str, start_row: int, session_id: Optional[str] = None, filepath: Optional[str] = None, count: int = 1 ) -> str: """ Insert one or more rows starting at the specified row. Args: sheet_name: Name of worksheet start_row: Row number to start inserting at session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) count: Number of rows to insert Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.rows_cols_xlw import insert_rows_xlw_with_wb result = insert_rows_xlw_with_wb(session.workbook, sheet_name, start_row, count) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.rows_cols_xlw import insert_rows_xlw result = insert_rows_xlw(full_path, sheet_name, start_row, count) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) if "error" in result: return f"Error: {result['error']}" return result["message"] except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error inserting rows: {e}") raise @mcp.tool() def insert_columns( sheet_name: str, start_col: int, session_id: Optional[str] = None, filepath: Optional[str] = None, count: int = 1 ) -> str: """ Insert one or more columns starting at the specified column. Args: sheet_name: Name of worksheet start_col: Column number to start inserting at session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) count: Number of columns to insert Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.rows_cols_xlw import insert_columns_xlw_with_wb result = insert_columns_xlw_with_wb(session.workbook, sheet_name, start_col, count) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.rows_cols_xlw import insert_columns_xlw result = insert_columns_xlw(full_path, sheet_name, start_col, count) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) if "error" in result: return f"Error: {result['error']}" return result["message"] except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error inserting columns: {e}") raise @mcp.tool() def delete_sheet_rows( sheet_name: str, start_row: int, session_id: Optional[str] = None, filepath: Optional[str] = None, count: int = 1 ) -> str: """ Delete one or more rows starting at the specified row. Args: sheet_name: Name of worksheet start_row: Row number to start deleting from session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) count: Number of rows to delete Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.rows_cols_xlw import delete_sheet_rows_xlw_with_wb result = delete_sheet_rows_xlw_with_wb(session.workbook, sheet_name, start_row, count) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.rows_cols_xlw import delete_sheet_rows_xlw result = delete_sheet_rows_xlw(full_path, sheet_name, start_row, count) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) if "error" in result: return f"Error: {result['error']}" return result["message"] except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error deleting rows: {e}") raise @mcp.tool() def delete_sheet_columns( sheet_name: str, start_col: int, session_id: Optional[str] = None, filepath: Optional[str] = None, count: int = 1 ) -> str: """ Delete one or more columns starting at the specified column. Args: sheet_name: Name of worksheet start_col: Column number to start deleting from session_id: Session ID from open_workbook (preferred) filepath: Path to Excel file (legacy, deprecated) count: Number of columns to delete Note: Use session_id for better performance. filepath parameter is deprecated. """ try: # Support both new (session_id) and old (filepath) API if session_id: # New API: use session session = SESSION_MANAGER.get_session(session_id) if not session: return ERROR_TEMPLATES['SESSION_NOT_FOUND'].format( session_id=session_id, ttl=10 # Default TTL is 10 minutes (600 seconds) ) with session.lock: from xlwings_mcp.xlwings_impl.rows_cols_xlw import delete_sheet_columns_xlw_with_wb result = delete_sheet_columns_xlw_with_wb(session.workbook, sheet_name, start_col, count) elif filepath: # Legacy API: backwards compatibility logger.warning("Using deprecated filepath parameter. Please use session_id instead.") full_path = get_excel_path(filepath) from xlwings_mcp.xlwings_impl.rows_cols_xlw import delete_sheet_columns_xlw result = delete_sheet_columns_xlw(full_path, sheet_name, start_col, count) else: return ERROR_TEMPLATES['PARAMETER_MISSING'].format( param1='session_id', param2='filepath' ) if "error" in result: return f"Error: {result['error']}" return result["message"] except (ValidationError, SheetError) as e: return f"Error: {str(e)}" except Exception as e: logger.error(f"Error deleting columns: {e}") raise async def run_sse(): """Run Excel MCP server in SSE mode.""" # Assign value to EXCEL_FILES_PATH in SSE mode global EXCEL_FILES_PATH EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files") # Create directory if it doesn't exist os.makedirs(EXCEL_FILES_PATH, exist_ok=True) try: logger.info(f"Starting Excel MCP server with SSE transport (files directory: {EXCEL_FILES_PATH})") await mcp.run_sse_async() except KeyboardInterrupt: logger.info("Server stopped by user") except Exception as e: logger.error(f"Server failed: {e}") raise finally: # Clean up all sessions on shutdown try: SESSION_MANAGER.close_all_sessions() logger.info("All Excel sessions closed") except Exception as e: logger.error(f"Error closing sessions during shutdown: {e}") logger.info("Server shutdown complete") async def run_streamable_http(): """Run Excel MCP server in streamable HTTP mode.""" # Assign value to EXCEL_FILES_PATH in streamable HTTP mode global EXCEL_FILES_PATH EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files") # Create directory if it doesn't exist os.makedirs(EXCEL_FILES_PATH, exist_ok=True) try: logger.info(f"Starting Excel MCP server with streamable HTTP transport (files directory: {EXCEL_FILES_PATH})") await mcp.run_streamable_http_async() except KeyboardInterrupt: logger.info("Server stopped by user") except Exception as e: logger.error(f"Server failed: {e}") raise finally: # Clean up all sessions on shutdown try: SESSION_MANAGER.close_all_sessions() logger.info("All Excel sessions closed") except Exception as e: logger.error(f"Error closing sessions during shutdown: {e}") logger.info("Server shutdown complete") def run_stdio(): """Run Excel MCP server in stdio mode.""" # No need to assign EXCEL_FILES_PATH in stdio mode try: logger.info("Starting Excel MCP server with stdio transport") mcp.run(transport="stdio") except KeyboardInterrupt: logger.info("Server stopped by user") except Exception as e: logger.error(f"Server failed: {e}") raise finally: # Clean up all sessions on shutdown try: SESSION_MANAGER.close_all_sessions() logger.info("All Excel sessions closed") except Exception as e: logger.error(f"Error closing sessions during shutdown: {e}") logger.info("Server shutdown complete")

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