Skip to main content
Glama
hyunjae-labs

xlwings Excel MCP Server

force_close_workbook_by_path_tool

Force close an Excel workbook by file path without saving changes. Use to terminate unresponsive workbooks or prevent data loss from unsaved modifications.

Instructions

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)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
filepathYes

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • MCP tool handler: wrapper that resolves the full path and calls the core force_close_workbook_by_path function, also handles exceptions and logging.
    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)}"
            }
  • Core helper function implementing the force close logic using pywin32 COM to connect to Excel, find the workbook by path, close it without saving, and optionally quit Excel if no workbooks remain.
    def force_close_workbook_by_path(filepath: str) -> Dict[str, Any]:
        """
        Force close a specific workbook from any running Excel process.
        
        Args:
            filepath: Absolute path to the workbook to close
            
        Returns:
            Dictionary with 'closed' (bool) and 'message' (str)
        """
        
        if not PYWIN32_AVAILABLE:
            return {
                "closed": False,
                "message": "Force close not available (pywin32 not installed or not on Windows)"
            }
        
        # Normalize the file path
        target_path = os.path.abspath(filepath).lower()
        
        try:
            # Initialize COM for this thread
            pythoncom.CoInitialize()
            
            found = False
            closed = False
            
            try:
                # Try to connect to running Excel instance
                xl = win32com.client.GetObject(Class="Excel.Application")
                
                # Check all open workbooks
                for wb in xl.Workbooks:
                    try:
                        # Compare full paths (case-insensitive on Windows)
                        wb_path = os.path.abspath(wb.FullName).lower()
                        
                        if wb_path == target_path:
                            logger.info(f"Found workbook to force close: {wb.FullName}")
                            found = True
                            
                            # Force close without saving
                            wb.Close(SaveChanges=False)
                            closed = True
                            logger.info(f"Successfully force closed: {filepath}")
                            break
                            
                    except Exception as e:
                        logger.warning(f"Error checking/closing workbook: {e}")
                        continue
                
                # If no workbooks remain, optionally quit Excel
                if closed and xl.Workbooks.Count == 0:
                    try:
                        xl.Quit()
                        logger.info("Excel application quit (no remaining workbooks)")
                    except:
                        pass
                        
            except Exception as e:
                # No Excel instance running or other COM error
                logger.debug(f"Could not connect to Excel: {e}")
                return {
                    "closed": False,
                    "message": f"No Excel instance found or cannot connect: {str(e)}"
                }
            
            finally:
                # Uninitialize COM
                pythoncom.CoUninitialize()
            
            if not found:
                return {
                    "closed": False,
                    "message": f"Workbook not found in any Excel instance: {filepath}"
                }
            
            return {
                "closed": closed,
                "message": f"Successfully force closed workbook: {filepath}"
            }
            
        except Exception as e:
            logger.error(f"Force close failed for {filepath}: {e}")
            return {
                "closed": False,
                "message": f"Force close failed: {str(e)}"
            }
  • Import of the helper function used by the tool handler.
    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(
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses key behavioral traits: the action is 'force close' (implying mutation/destructive), it does not save changes, and it returns a dictionary with status. However, it lacks details on permissions, error handling, or side effects, leaving gaps for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose in the first sentence, followed by structured Args and Returns sections. Every sentence earns its place by providing essential information without redundancy, making it highly efficient and well-organized.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (a destructive operation with one parameter) and the presence of an output schema (implied by the Returns section), the description is mostly complete. It covers purpose, parameters, and return values, but as a mutation tool with no annotations, it could benefit from more behavioral context like error conditions or permissions.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds meaningful context beyond the input schema, which has 0% description coverage. It explains that 'filepath' is the 'Path to the workbook to force close', clarifying the parameter's role. With only one parameter, this is sufficient to compensate for the low schema coverage, though it could specify format constraints (e.g., absolute vs. relative paths).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('force close'), the resource ('a specific workbook'), and the method ('by file path'), with explicit mention of 'without saving' distinguishing it from potential save-and-close alternatives. It directly addresses the verb+resource+scope requirement and differentiates from siblings like 'close_workbook' by specifying the forceful, non-saving nature.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage context through 'force close... without saving', suggesting this tool is for discarding unsaved changes, but it does not explicitly state when to use this versus alternatives like 'close_workbook' or provide exclusions. The guidance is clear but lacks explicit comparison to sibling tools or detailed prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

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