Refactor sheet operations and update documentation

- Improve `copy_range_operation` with more robust error handling and style copying
- Enhance `delete_range_operation` with shift direction support
- Update README with Cursor IDE connection instructions
- Remove unnecessary parameters from `create_pivot_table`
- Improve logging and error validation in sheet operations
This commit is contained in:
Haris Musa
2025-02-16 23:12:24 +05:00
committed by Haris
parent 4e82b8a7b2
commit 99ac29915a
3 changed files with 70 additions and 37 deletions

View File

@ -97,8 +97,18 @@ uv run excel-mcp-server
The server will start in SSE mode and wait for connections from MCP clients. The server will start in SSE mode and wait for connections from MCP clients.
### Connecting in Cursor IDE
After starting the server, connect to the SSE endpoint in Cursor IDE:
```
http://localhost:8000/sse
```
The Excel MCP tools will be available through the agent.
For available tools and their usage, please refer to [TOOLS.md](TOOLS.md). For available tools and their usage, please refer to [TOOLS.md](TOOLS.md).
## License ## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

View File

@ -285,7 +285,6 @@ def create_pivot_table(
filepath: str, filepath: str,
sheet_name: str, sheet_name: str,
data_range: str, data_range: str,
target_cell: str,
rows: List[str], rows: List[str],
values: List[str], values: List[str],
columns: List[str] = None, columns: List[str] = None,
@ -298,7 +297,6 @@ def create_pivot_table(
filepath=full_path, filepath=full_path,
sheet_name=sheet_name, sheet_name=sheet_name,
data_range=data_range, data_range=data_range,
target_cell=target_cell,
rows=rows, rows=rows,
values=values, values=values,
columns=columns or [], columns=columns or [],

View File

@ -1,13 +1,14 @@
import logging import logging
from typing import Any from typing import Any
from copy import copy
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter, column_index_from_string
from openpyxl.styles import Font, Border, PatternFill, Side from openpyxl.styles import Font, Border, PatternFill, Side
from .cell_utils import parse_cell_range, validate_cell_reference from .cell_utils import parse_cell_range
from .exceptions import SheetError from .exceptions import SheetError, ValidationError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -246,56 +247,62 @@ def copy_range_operation(
filepath: str, filepath: str,
sheet_name: str, sheet_name: str,
source_start: str, source_start: str,
source_end: str | None, source_end: str,
target_start: str, target_start: str,
target_sheet: str | None = None, target_sheet: str = None
) -> dict[str, Any]: ) -> dict:
"""Copy a range of cells to another location.""" """Copy a range of cells to another location."""
try: try:
wb = load_workbook(filepath) wb = load_workbook(filepath)
if sheet_name not in wb.sheetnames: if sheet_name not in wb.sheetnames:
raise SheetError(f"Sheet '{sheet_name}' not found") logger.error(f"Sheet '{sheet_name}' not found")
raise ValidationError(f"Sheet '{sheet_name}' not found")
source_ws = wb[sheet_name] source_ws = wb[sheet_name]
target_ws = wb[target_sheet] if target_sheet else source_ws target_ws = wb[target_sheet] if target_sheet else source_ws
if target_sheet and target_sheet not in wb.sheetnames: # Parse source range
raise SheetError(f"Target sheet '{target_sheet}' not found")
source_range = f"{source_start}:{source_end}" if source_end else source_start
# Validate source range
try: try:
end_row, end_col = parse_cell_range(source_start, source_end) start_row, start_col, end_row, end_col = parse_cell_range(source_start, source_end)
if end_row and end_row > source_ws.max_row:
raise SheetError(f"End row {end_row} out of bounds (1-{source_ws.max_row})")
if end_col and end_col > source_ws.max_column:
raise SheetError(f"End column {end_col} out of bounds (1-{source_ws.max_column})")
except ValueError as e: except ValueError as e:
raise SheetError(f"Invalid range: {str(e)}") logger.error(f"Invalid source range: {e}")
raise ValidationError(f"Invalid source range: {str(e)}")
# Validate target cell
# Parse target starting point
try: try:
validate_cell_reference(target_start) target_row = int(''.join(filter(str.isdigit, target_start)))
target_col = column_index_from_string(''.join(filter(str.isalpha, target_start)))
except ValueError as e: except ValueError as e:
raise SheetError(f"Invalid target cell: {str(e)}") logger.error(f"Invalid target cell: {e}")
raise ValidationError(f"Invalid target cell: {str(e)}")
copy_range(source_ws, target_ws, source_range, target_start)
# Copy the range
row_offset = target_row - start_row
col_offset = target_col - start_col
for i in range(start_row, end_row + 1):
for j in range(start_col, end_col + 1):
source_cell = source_ws.cell(row=i, column=j)
target_cell = target_ws.cell(row=i + row_offset, column=j + col_offset)
target_cell.value = source_cell.value
if source_cell.has_style:
target_cell._style = copy(source_cell._style)
wb.save(filepath) wb.save(filepath)
return {"message": f"Range copied successfully"} return {"message": f"Range copied successfully"}
except SheetError as e:
logger.error(str(e)) except (ValidationError, SheetError):
raise raise
except Exception as e: except Exception as e:
logger.error(f"Failed to copy range: {e}") logger.error(f"Failed to copy range: {e}")
raise SheetError(str(e)) raise SheetError(f"Failed to copy range: {str(e)}")
def delete_range_operation( def delete_range_operation(
filepath: str, filepath: str,
sheet_name: str, sheet_name: str,
start_cell: str, start_cell: str,
end_cell: str | None = None, end_cell: str | None = None,
shift_direction: str = "up"
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Delete a range of cells and shift remaining cells.""" """Delete a range of cells and shift remaining cells."""
try: try:
@ -307,7 +314,7 @@ def delete_range_operation(
# Validate range # Validate range
try: try:
end_row, end_col = parse_cell_range(start_cell, end_cell) start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
if end_row and end_row > worksheet.max_row: if end_row and end_row > worksheet.max_row:
raise SheetError(f"End row {end_row} out of bounds (1-{worksheet.max_row})") raise SheetError(f"End row {end_row} out of bounds (1-{worksheet.max_row})")
if end_col and end_col > worksheet.max_column: if end_col and end_col > worksheet.max_column:
@ -315,11 +322,29 @@ def delete_range_operation(
except ValueError as e: except ValueError as e:
raise SheetError(f"Invalid range: {str(e)}") raise SheetError(f"Invalid range: {str(e)}")
# Validate shift direction
if shift_direction not in ["up", "left"]:
raise ValidationError(f"Invalid shift direction: {shift_direction}. Must be 'up' or 'left'")
range_string = format_range_string(
start_row, start_col,
end_row or start_row,
end_col or start_col
)
# Delete range contents
delete_range(worksheet, start_cell, end_cell) delete_range(worksheet, start_cell, end_cell)
# Shift cells if needed
if shift_direction == "up":
worksheet.delete_rows(start_row, (end_row or start_row) - start_row + 1)
elif shift_direction == "left":
worksheet.delete_cols(start_col, (end_col or start_col) - start_col + 1)
wb.save(filepath) wb.save(filepath)
return {"message": f"Range deleted successfully"} return {"message": f"Range {range_string} deleted successfully"}
except SheetError as e: except (ValidationError, SheetError) as e:
logger.error(str(e)) logger.error(str(e))
raise raise
except Exception as e: except Exception as e: