mirror of
https://github.com/haris-musa/excel-mcp-server.git
synced 2025-12-08 17:12:41 +08:00
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:
12
README.md
12
README.md
@ -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.
|
||||||
|
|||||||
@ -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 [],
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user