Skip to main content
Glama
excel_tools.py16.9 kB
"""Excel 工具 - excel_edit(单工具). 整合前(42个工具): - read_excel, write_excel, update_excel_cells, list_sheets, get_excel_info, excel_to_csv, csv_to_excel, create_workbook, create_worksheet, copy_worksheet, delete_worksheet, rename_worksheet, get_workbook_metadata, format_range, apply_formula, validate_formula_syntax, read_excel_metadata, merge_cells, unmerge_cells, get_merged_cells, copy_range, delete_range, validate_excel_range, get_data_validation_info, insert_rows, insert_columns, delete_sheet_rows, delete_sheet_columns, create_chart, create_table, create_pivot_table, ... 整合后(1个工具): - excel_edit: 精确编辑 Excel 文件 - cells: 更新单元格 - formula: 应用公式 - format: 格式化范围 - range: 范围操作(copy/delete/merge/unmerge) - sheet_ops: 工作表操作(create/delete/rename/copy) - row_col_ops: 行列操作(insert_rows/insert_cols/delete_rows/delete_cols) - chart: 创建图表(bar/line/pie/scatter) 说明: - Excel 文件的读取使用 fs_read - Excel 文件的写入使用 fs_write - Excel 元数据获取使用 fs_ops("info") """ from typing import Any, Dict, List, Literal, Optional, Union from mcp.server.fastmcp.utilities.logging import get_logger from ..security import PathValidator logger = get_logger(__name__) def _normalize_mcp_param(value: Any, expected_type: type = None) -> Any: """规范化 MCP client 传递的参数. MCP client 可能将单个值错误包装成列表,此函数用于解包。 Args: value: 原始参数值 expected_type: 期望的类型(用于类型检查) Returns: 规范化后的值 Examples: _normalize_mcp_param(['hello'], str) -> 'hello' # 单元素列表解包为字符串 _normalize_mcp_param([{'a': 1}], dict) -> {'a': 1} # 单元素列表解包为字典 _normalize_mcp_param([['a', 'b']], list) -> [['a', 'b']] # 2D数组保持原样 _normalize_mcp_param([['a'], ['b']], list) -> [['a'], ['b']] # 2D数组保持原样 """ # 如果是列表且只有一个元素 if isinstance(value, list) and len(value) == 1: single_item = value[0] # 如果期望类型是字符串,且单元素是字符串,则解包 if expected_type == str and isinstance(single_item, str): return single_item # 如果期望类型是字典,且单元素是字典,则解包 if expected_type == dict and isinstance(single_item, dict): return single_item # 如果期望类型是列表 if expected_type == list: if isinstance(single_item, list): # 检查是否是 2D 数组(列表的列表) if single_item and isinstance(single_item[0], list): # 这是 2D 数组,保持原样(不解包) return value else: # 这是普通列表(如字符串列表),解包 return single_item # 其他情况:如果单元素类型匹配期望类型,解包 if expected_type and isinstance(single_item, expected_type): return single_item return value async def excel_edit( path: str, edit_type: Literal["cells", "formula", "format", "range", "sheet_ops", "row_col_ops", "chart"], excel_ops: Any, validator: PathValidator, *, sheet: Optional[str] = None, # cells 模式 updates: Optional[List[Dict[str, Any]]] = None, # formula 模式 cell: Optional[str] = None, formula: Optional[str] = None, # format 模式 range: Optional[str] = None, style: Optional[Dict[str, Any]] = None, # range 模式 operation: Optional[Literal["copy", "delete", "merge", "unmerge"]] = None, source_range: Optional[str] = None, target_range: Optional[str] = None, # sheet_ops 模式 sheet_operation: Optional[Literal["create", "delete", "rename", "copy"]] = None, target_sheet: Optional[str] = None, new_name: Optional[str] = None, # row_col_ops 模式 row_col_operation: Optional[Literal["insert_rows", "insert_cols", "delete_rows", "delete_cols"]] = None, start_index: Optional[int] = None, count: int = 1, # chart 模式 chart_type: Optional[Literal["bar", "line", "pie", "scatter"]] = None, data_range: Optional[str] = None, target_cell: Optional[str] = None, chart_title: Optional[str] = None, ) -> Dict[str, Any]: """精确编辑 Excel 文件的特定部分. 整合了 30+ 个 Excel 编辑工具,通过 edit_type 参数区分: - cells: update_excel_cells - formula: apply_formula - format: format_range - range: copy_range, delete_range, merge_cells, unmerge_cells - sheet_ops: create_worksheet, delete_worksheet, rename_worksheet, copy_worksheet Args: path: 文件路径 edit_type: 编辑类型 excel_ops: Excel 操作实例 validator: 路径验证器 sheet: 工作表名称 # cells 模式参数 updates: 更新列表 [{"cell": "A1", "value": "new"}, ...] # formula 模式参数 cell: 单元格地址 formula: 公式(如 "=SUM(A1:A10)") # format 模式参数 range: 格式化范围 style: 样式字典 { "bold": bool, "italic": bool, "font_size": int, "font_color": str, "bg_color": str, "border": str, "number_format": str, "alignment": str } # range 模式参数 operation: 范围操作类型 (copy/delete/merge/unmerge) source_range: 源范围 target_range: 目标范围 # sheet_ops 模式参数 sheet_operation: 工作表操作类型 (create/delete/rename/copy) target_sheet: 目标工作表名 new_name: 新名称(用于 rename) Returns: 操作结果字典 Examples: # 更新单元格 excel_edit("data.xlsx", "cells", updates=[ {"cell": "A1", "value": "Hello"}, {"cell": "B1", "value": 123} ]) # 应用公式 excel_edit("data.xlsx", "formula", cell="D1", formula="=SUM(A1:C1)") # 格式化 excel_edit("data.xlsx", "format", range="A1:C10", style={ "bold": True, "bg_color": "FFFF00", "border": "thin" }) # 合并单元格 excel_edit("data.xlsx", "range", operation="merge", range="A1:C1") # 创建工作表 excel_edit("data.xlsx", "sheet_ops", sheet_operation="create", target_sheet="NewSheet") """ abs_path, allowed = await validator.validate_path(path) if not allowed: raise ValueError(f"Path outside allowed directories: {path}") # 规范化参数(处理 MCP client 可能将值包装成列表的情况) if updates is not None: # updates 应该是字典列表,如果是单元素列表且元素是字典列表,解包 updates = _normalize_mcp_param(updates, expected_type=list) if style is not None: # style 应该是字典,如果是单元素列表且元素是字典,解包 style = _normalize_mcp_param(style, expected_type=dict) if edit_type == "cells": if not updates: raise ValueError("updates is required for cells edit_type") result = await excel_ops.update_excel_cells( path=path, updates=updates, sheet=sheet, ) return { "success": True, "edit_type": "cells", "path": validator.real_to_virtual(abs_path), **result, } elif edit_type == "formula": if not cell or not formula: raise ValueError("cell and formula are required for formula edit_type") result = await excel_ops.apply_formula( path=path, sheet=sheet or "Sheet1", cell=cell, formula=formula, ) return { "success": True, "edit_type": "formula", "path": validator.real_to_virtual(abs_path), "cell": cell, "formula": formula, **result, } elif edit_type == "format": if not range and not cell: raise ValueError("range or cell is required for format edit_type") if not style: raise ValueError("style is required for format edit_type") format_range = range or cell result = await excel_ops.format_range( path=path, sheet=sheet or "Sheet1", cell_range=format_range, bold=style.get("bold"), italic=style.get("italic"), font_size=style.get("font_size"), font_color=style.get("font_color"), bg_color=style.get("bg_color"), border=style.get("border"), number_format=style.get("number_format"), alignment=style.get("alignment"), ) return { "success": True, "edit_type": "format", "path": validator.real_to_virtual(abs_path), "range": format_range, **result, } elif edit_type == "range": if not operation: raise ValueError("operation is required for range edit_type") if operation == "copy": if not source_range or not target_range: raise ValueError("source_range and target_range are required for copy operation") # Parse source_range (e.g., "A1:C10" -> start_cell="A1", end_cell="C10") if ":" in source_range: src_start, src_end = source_range.split(":") else: src_start = src_end = source_range # target_range is just start position tgt_start = target_range.split(":")[0] if ":" in target_range else target_range result = await excel_ops.copy_range( path=path, sheet=sheet or "Sheet1", source_start=src_start, source_end=src_end, target_start=tgt_start, ) elif operation == "delete": delete_range = range or source_range if not delete_range: raise ValueError("range is required for delete operation") # Parse range (e.g., "A1:C10" -> start_cell="A1", end_cell="C10") if ":" in delete_range: start_cell, end_cell = delete_range.split(":") else: start_cell = end_cell = delete_range result = await excel_ops.delete_range( path=path, sheet=sheet or "Sheet1", start_cell=start_cell, end_cell=end_cell, ) elif operation == "merge": merge_range = range or source_range if not merge_range: raise ValueError("range is required for merge operation") # Parse range (e.g., "A1:C1" -> start_cell="A1", end_cell="C1") if ":" in merge_range: start_cell, end_cell = merge_range.split(":") else: raise ValueError("merge operation requires a range with format 'A1:C1'") result = await excel_ops.merge_cells( path=path, sheet=sheet or "Sheet1", start_cell=start_cell, end_cell=end_cell, ) elif operation == "unmerge": unmerge_range = range or source_range if not unmerge_range: raise ValueError("range is required for unmerge operation") # Parse range (e.g., "A1:C1" -> start_cell="A1", end_cell="C1") if ":" in unmerge_range: start_cell, end_cell = unmerge_range.split(":") else: raise ValueError("unmerge operation requires a range with format 'A1:C1'") result = await excel_ops.unmerge_cells( path=path, sheet=sheet or "Sheet1", start_cell=start_cell, end_cell=end_cell, ) else: raise ValueError(f"Unknown range operation: {operation}") return { "success": True, "edit_type": "range", "operation": operation, "path": validator.real_to_virtual(abs_path), **result, } elif edit_type == "sheet_ops": if not sheet_operation: raise ValueError("sheet_operation is required for sheet_ops edit_type") if sheet_operation == "create": if not target_sheet: raise ValueError("target_sheet is required for create operation") result = await excel_ops.create_worksheet( path=path, sheet_name=target_sheet, ) elif sheet_operation == "delete": delete_sheet = sheet or target_sheet if not delete_sheet: raise ValueError("sheet or target_sheet is required for delete operation") result = await excel_ops.delete_worksheet( path=path, sheet_name=delete_sheet, ) elif sheet_operation == "rename": if not sheet or not new_name: raise ValueError("sheet and new_name are required for rename operation") result = await excel_ops.rename_worksheet( path=path, old_name=sheet, new_name=new_name, ) elif sheet_operation == "copy": if not sheet or not target_sheet: raise ValueError("sheet and target_sheet are required for copy operation") result = await excel_ops.copy_worksheet( path=path, source_sheet=sheet, target_sheet=target_sheet, ) else: raise ValueError(f"Unknown sheet operation: {sheet_operation}") return { "success": True, "edit_type": "sheet_ops", "sheet_operation": sheet_operation, "path": validator.real_to_virtual(abs_path), **result, } elif edit_type == "row_col_ops": if not row_col_operation: raise ValueError("row_col_operation is required for row_col_ops edit_type") if start_index is None: raise ValueError("start_index is required for row_col_ops edit_type") if row_col_operation == "insert_rows": result = await excel_ops.insert_rows( path=path, sheet=sheet or "Sheet1", start_row=start_index, count=count, ) elif row_col_operation == "insert_cols": result = await excel_ops.insert_columns( path=path, sheet=sheet or "Sheet1", start_col=start_index, count=count, ) elif row_col_operation == "delete_rows": result = await excel_ops.delete_rows( path=path, sheet=sheet or "Sheet1", start_row=start_index, count=count, ) elif row_col_operation == "delete_cols": result = await excel_ops.delete_columns( path=path, sheet=sheet or "Sheet1", start_col=start_index, count=count, ) else: raise ValueError(f"Unknown row_col_operation: {row_col_operation}") return { "success": True, "edit_type": "row_col_ops", "row_col_operation": row_col_operation, "path": validator.real_to_virtual(abs_path), **result, } elif edit_type == "chart": if not chart_type or not data_range or not target_cell: raise ValueError("chart_type, data_range, and target_cell are required for chart edit_type") result = await excel_ops.create_chart( path=path, sheet=sheet or "Sheet1", data_range=data_range, chart_type=chart_type, target_cell=target_cell, title=chart_title or "", ) return { "success": True, "edit_type": "chart", "chart_type": chart_type, "path": validator.real_to_virtual(abs_path), **result, } else: raise ValueError(f"Unknown edit_type: {edit_type}")

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/answerlink/MCP-Workspace-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server