Skip to main content
Glama
yuqie6

MCP Sheet Parser

by yuqie6

apply_changes

Write modified spreadsheet data back to Excel/CSV files while preserving original formatting and creating backup copies for safety.

Instructions

将修改后的数据写回Excel/CSV文件,完成数据编辑闭环。接受parse_sheet返回的JSON格式数据(修改后)。保留原文件格式和样式,默认创建备份文件防止数据丢失。支持添加、删除、修改行和单元格数据。

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
file_pathYes需要写回数据的目标文件的绝对路径。
table_model_jsonYes从 `parse_sheet` 工具获取并修改后的 TableModel JSON 数据。
create_backupNo【可选】是否在写入前创建原始文件的备份。默认为 `true`,以防意外覆盖。

Implementation Reference

  • Core handler implementation of the 'apply_changes' tool. Validates input, creates backup, dispatches to format-specific write-back methods (_write_back_csv, _write_back_xlsx, _write_back_xls), and returns structured result.
    def apply_changes(self, file_path: str, table_model_json: dict[str, Any], create_backup: bool = True) -> dict[str, Any]:
        """
        将TableModel JSON的修改应用回原始文件。
    
        参数:
            file_path: 目标文件路径
            table_model_json: 包含修改的JSON数据
            create_backup: 是否创建备份文件
    
        返回值:
            操作结果
        """
        try:
            # 验证文件存在
            path = Path(file_path)
            if not path.exists():
                raise FileNotFoundError(f"文件不存在: {file_path}")
    
            # 验证文件格式
            file_format = path.suffix.lower()
            supported_formats = ['.csv', '.xlsx', '.xlsm', '.xls']
            if file_format not in supported_formats:
                if file_format == '.xlsb':
                    raise ValueError("XLSB格式暂不支持数据写回,请转换为XLSX格式进行编辑")
                raise ValueError(f"Unsupported file type: {file_format}")
    
            # 创建备份文件(如果需要)
            backup_path = None
            if create_backup:
                backup_path = path.with_suffix(f"{path.suffix}.backup")
                import shutil
                shutil.copy2(path, backup_path)
                logger.info(f"已创建备份文件: {backup_path}")
    
            # 验证JSON格式
            required_fields = ["sheet_name", "headers", "rows"]
            for field in required_fields:
                if field not in table_model_json:
                    raise ValueError(f"缺少必需字段: {field}")
    
            # 实现真正的数据写回功能
            changes_applied = 0
    
            if file_format == '.csv':
                changes_applied = self._write_back_csv(path, table_model_json)
            elif file_format in ['.xlsx', '.xlsm']:
                changes_applied = self._write_back_xlsx(path, table_model_json)
            elif file_format == '.xls':
                changes_applied = self._write_back_xls(path, table_model_json)
    
            return {
                "status": "success",
                "message": "数据修改已成功应用",
                "file_path": str(path.absolute()),
                "backup_path": str(backup_path) if backup_path else None,
                "backup_created": create_backup,
                "changes_applied": changes_applied,
                "sheet_name": table_model_json.get("sheet_name"),
                "headers_count": len(table_model_json.get("headers", [])),
                "rows_count": len(table_model_json.get("rows", []))
            }
    
        except Exception as e:
            logger.error(f"应用修改失败: {e}")
            raise
  • Input schema definition for the 'apply_changes' tool, including parameters file_path, table_model_json (with sheet_name, headers, rows), and optional create_backup.
    Tool(
        name="apply_changes",
        description="将修改后的数据写回Excel/CSV文件,完成数据编辑闭环。接受parse_sheet返回的JSON格式数据(修改后)。保留原文件格式和样式,默认创建备份文件防止数据丢失。支持添加、删除、修改行和单元格数据。",
        inputSchema={
            "type": "object",
            "properties": {
                "file_path": {
                    "type": "string",
                    "description": "需要写回数据的目标文件的绝对路径。"
                },
                "table_model_json": {
                    "type": "object",
                    "description": "从 `parse_sheet` 工具获取并修改后的 TableModel JSON 数据。",
                    "properties": {
                        "sheet_name": {"type": "string"},
                        "headers": {
                            "type": "array",
                            "items": {"type": "string"}
                        },
                        "rows": {
                            "type": "array",
                            "items": {
                                "type": "array",
                                "items": {}
                            }
                        }
                    },
                    "required": ["sheet_name", "headers", "rows"]
                },
                "create_backup": {
                    "type": "boolean",
                    "description": "【可选】是否在写入前创建原始文件的备份。默认为 `true`,以防意外覆盖。"
                }
            },
            "required": ["file_path", "table_model_json"]
        }
    )
  • Tool registration via @server.list_tools() including apply_changes Tool object, and dispatch in @server.call_tool() handle_call_tool with elif for 'apply_changes' calling _handle_apply_changes.
    @server.list_tools()
    async def handle_list_tools() -> list[Tool]:
        return [
            Tool(
                name="convert_to_html",
                description="将Excel/CSV文件转换为可在浏览器中查看的HTML文件。保留原始样式、颜色、字体等格式。支持多工作表文件,可选择特定工作表或转换全部。大文件可使用分页功能。返回结构化JSON,包含成功状态、生成的文件信息和转换摘要。",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "file_path": {
                            "type": "string",
                            "description": "源表格文件的绝对路径,支持 .csv, .xlsx, .xls, .xlsb, .xlsm 格式。"
                        },
                        "output_path": {
                            "type": "string",
                            "description": "输出HTML文件的路径。如果留空,将在源文件目录中生成一个同名的 .html 文件。"
                        },
                        "sheet_name": {
                            "type": "string",
                            "description": "【可选】要转换的单个工作表的名称。如果留空,将转换文件中的所有工作表。"
                        },
                        "page_size": {
                            "type": "integer",
                            "description": "【可选】分页时每页显示的行数。默认为100行。用于控制大型文件转换后HTML的单页大小。"
                        },
                        "page_number": {
                            "type": "integer",
                            "description": "【可选】要查看的页码,从1开始。默认为1。用于浏览大型文件的特定页面。"
                        },
                        "header_rows": {
                            "type": "integer",
                            "description": "【可选】将文件顶部的指定行数视为表头。默认为 1。"
                        }
                    },
                    "required": ["file_path"]
                }
            ),
            Tool(
                name="parse_sheet",
                description="解析Excel/CSV文件为结构化JSON数据。默认返回文件概览信息(行数、列数、数据类型、前几行预览),避免上下文过载。LLM可通过参数控制是否获取完整数据、样式信息等。适合数据分析和处理,修改后可用apply_changes写回。",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "file_path": {
                            "type": "string",
                            "description": "目标表格文件的绝对路径,支持 .csv, .xlsx, .xls, .xlsb, .xlsm 格式。"
                        },
                        "sheet_name": {
                            "type": "string",
                            "description": "【可选】要解析的工作表名称。如果留空,使用第一个工作表。"
                        },
                        "range_string": {
                            "type": "string",
                            "description": "【可选】单元格范围,如'A1:D10'。指定范围时会返回该范围的完整数据。"
                        },
                        "include_full_data": {
                            "type": "boolean",
                            "description": "【可选,默认false】是否返回完整数据。false时只返回概览和预览,true时返回所有行数据。大文件建议先查看概览。"
                        },
                        "include_styles": {
                            "type": "boolean",
                            "description": "【可选,默认false】是否包含样式信息(字体、颜色、边框等)。样式信息会显著增加数据量。"
                        },
                        "preview_rows": {
                            "type": "integer",
                            "description": "【可选,默认5】预览行数。当include_full_data为false时,返回前N行作为数据预览。"
                        },
                        "max_rows": {
                            "type": "integer",
                            "description": "【可选】最大返回行数。用于限制大文件的数据量,超出部分会被截断并提示。"
                        }
                    },
                    "required": ["file_path"]
                }
            ),
            Tool(
                name="apply_changes",
                description="将修改后的数据写回Excel/CSV文件,完成数据编辑闭环。接受parse_sheet返回的JSON格式数据(修改后)。保留原文件格式和样式,默认创建备份文件防止数据丢失。支持添加、删除、修改行和单元格数据。",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "file_path": {
                            "type": "string",
                            "description": "需要写回数据的目标文件的绝对路径。"
                        },
                        "table_model_json": {
                            "type": "object",
                            "description": "从 `parse_sheet` 工具获取并修改后的 TableModel JSON 数据。",
                            "properties": {
                                "sheet_name": {"type": "string"},
                                "headers": {
                                    "type": "array",
                                    "items": {"type": "string"}
                                },
                                "rows": {
                                    "type": "array",
                                    "items": {
                                        "type": "array",
                                        "items": {}
                                    }
                                }
                            },
                            "required": ["sheet_name", "headers", "rows"]
                        },
                        "create_backup": {
                            "type": "boolean",
                            "description": "【可选】是否在写入前创建原始文件的备份。默认为 `true`,以防意外覆盖。"
                        }
                    },
                    "required": ["file_path", "table_model_json"]
                }
            )
        ]
    
    
    @server.call_tool()
    async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
        """处理工具调用请求。"""
        try:
            if name == "convert_to_html":
                return await _handle_convert_to_html(arguments, core_service)
            elif name == "parse_sheet":
                return await _handle_parse_sheet(arguments, core_service)
            elif name == "apply_changes":
                return await _handle_apply_changes(arguments, core_service)
            else:
                return [TextContent(
                    type="text",
                    text=f"未知工具: {name}"
                )]
        except Exception as e:
            logger.error(f"工具调用失败 {name}: {e}")
            return [TextContent(
                type="text",
                text=f"错误: {str(e)}"
            )]
  • Wrapper handler for apply_changes tool call: extracts arguments, calls core_service.apply_changes, formats JSON response with success/error handling.
    async def _handle_apply_changes(arguments: dict[str, Any], core_service: CoreService) -> list[TextContent]:
        """处理 apply_changes 工具调用。"""
    
        try:
            result = core_service.apply_changes(
                arguments["file_path"],
                arguments["table_model_json"],
                arguments.get("create_backup", True)
            )
    
            response = {
                "success": True,
                "operation": "apply_changes",
                "result": result,
                "message": "数据已成功写回文件",
                "backup_created": arguments.get("create_backup", True)
            }
    
            return [TextContent(
                type="text",
                text=json.dumps(response, ensure_ascii=False, indent=2)
            )]
    
        except FileNotFoundError as e:
            return [TextContent(
                type="text",
                text=json.dumps({
                    "success": False,
                    "error_type": "file_not_found",
                    "error_message": f"文件未找到: {str(e)}",
                    "suggestion": "请检查文件路径是否正确,确保目标文件存在"
                }, ensure_ascii=False, indent=2)
            )]
        except PermissionError as e:
            return [TextContent(
                type="text",
                text=json.dumps({
                    "success": False,
                    "error_type": "permission_error",
                    "error_message": f"权限不足: {str(e)}",
                    "suggestion": "请检查文件权限,确保有写入文件的权限,文件可能被其他程序占用"
                }, ensure_ascii=False, indent=2)
            )]
        except ValueError as e:
            return [TextContent(
                type="text",
                text=json.dumps({
                    "success": False,
                    "error_type": "invalid_data",
                    "error_message": f"数据格式错误: {str(e)}",
                    "suggestion": "请确保table_model_json格式正确,包含必需的字段:sheet_name, headers, rows"
                }, ensure_ascii=False, indent=2)
            )]
        except Exception as e:
            return [TextContent(
                type="text",
                text=json.dumps({
                    "success": False,
                    "error_type": "write_error",
                    "error_message": f"写入失败: {str(e)}",
                    "suggestion": "请检查数据格式是否与原文件兼容,或尝试关闭可能占用文件的程序"
                }, ensure_ascii=False, indent=2)
            )]
  • Key helper method for writing changes back to XLSX files: loads workbook with openpyxl, clears data (preserves styles), writes headers and rows handling merged cells and type conversion, saves file.
    def _write_back_xlsx(self, file_path: Path, table_model_json: dict[str, Any]) -> int:
        """
        将修改写回XLSX文件。
    
        参数:
            file_path: XLSX文件路径
            table_model_json: 包含修改的JSON数据
    
        返回值:
            应用的修改数量
        """
        import openpyxl
    
        headers = table_model_json["headers"]
        rows = table_model_json["rows"]
    
        # 打开现有的工作簿
        workbook = openpyxl.load_workbook(file_path)
    
        # 根据提供的sheet_name选择正确的工作表
        sheet_name_to_write = table_model_json["sheet_name"]
        
        if sheet_name_to_write in workbook.sheetnames:
            worksheet = workbook[sheet_name_to_write]
        else:
            raise ValueError(f"工作表 '{sheet_name_to_write}' 在文件中不存在。")
    
        # 清除现有数据(保留样式)
        max_row = worksheet.max_row
        max_col = worksheet.max_column
    
        # 清除单元格内容但保留格式
        for row in range(1, max_row + 1):
            for col in range(1, max_col + 1):
                cell = worksheet.cell(row=row, column=col)
                # 检查是否为合并单元格,跳过MergedCell类型
                try:
                    from openpyxl.cell.cell import MergedCell
                    if isinstance(cell, MergedCell):
                        # 跳过被合并的单元格,因为它们的value属性是只读的
                        continue
                except ImportError:
                    # 如果导入失败,使用字符串检查作为备选方案
                    if 'MergedCell' in str(type(cell)):
                        continue
    
                cell.value = None
    
        # 写入表头
        for col_idx, header in enumerate(headers, 1):
            cell = worksheet.cell(row=1, column=col_idx)
    
            # 检查是否为合并单元格
            try:
                from openpyxl.cell.cell import MergedCell
                if isinstance(cell, MergedCell):
                    # 跳过被合并的单元格,因为它们的value属性是只读的
                    # 只有合并区域的左上角单元格可以写入
                    logger.debug(f"跳过合并单元格 {cell.coordinate}")
                    continue
            except ImportError:
                # 如果导入失败,使用字符串检查作为备选方案
                if 'MergedCell' in str(type(cell)):
                    logger.debug(f"跳过合并单元格 {cell.coordinate}")
                    continue
    
            cell.value = header
    
        # 写入数据行
        changes_count = 0
        for row_idx, row_data in enumerate(rows, 2):  # 从第2行开始(第1行是表头)
            for col_idx, cell_data in enumerate(row_data, 1):
                cell = worksheet.cell(row=row_idx, column=col_idx)
    
                # 提取单元格值
                if isinstance(cell_data, dict) and 'value' in cell_data:
                    value = cell_data['value']
                else:
                    value = cell_data
    
                # 尝试转换数值类型
                # 检查是否为合并单元格(MergedCell的value属性是只读的)
                is_merged_cell = False
                try:
                    from openpyxl.cell.cell import MergedCell
                    if isinstance(cell, MergedCell):
                        is_merged_cell = True
                except ImportError:
                    # 如果导入失败,使用字符串检查作为备选方案
                    if 'MergedCell' in str(type(cell)):
                        is_merged_cell = True
    
                if is_merged_cell:
                    # 跳过被合并的单元格,因为它们的value属性是只读的
                    # 只有合并区域的左上角单元格可以写入
                    logger.debug(f"跳过合并单元格 {cell.coordinate}")
                    continue
    
                # 写入值到普通单元格(此时已确认不是MergedCell)
                try:
                    # 使用类型守卫确保不是 MergedCell
                    from openpyxl.cell.cell import MergedCell
                    if not isinstance(cell, MergedCell):
                        if value is not None and value != "":
                            # 改进的数值转换逻辑
                            if isinstance(value, str):
                                # 尝试转换为数字,使用更安全的方法
                                try:
                                    # 先尝试转换为整数
                                    if '.' not in value and 'e' not in value.lower():
                                        cell.value = int(value)
                                    else:
                                        # 尝试转换为浮点数
                                        cell.value = float(value)
                                except ValueError:
                                    # 如果转换失败,保持为字符串
                                    cell.value = str(value)
                            elif isinstance(value, (int, float, bool)):
                                # 保持原始数据类型
                                cell.value = value
                            else:
                                # 对于其他类型,转换为字符串
                                cell.value = str(value)
                        else:
                            cell.value = None
                except AttributeError as e:
                    # 如果仍然遇到MergedCell问题,记录并跳过
                    if "read-only" in str(e):
                        logger.warning(f"跳过只读单元格 {cell.coordinate}: {e}")
                        continue
                    else:
                        raise
    
                changes_count += 1
    
        # 保存工作簿
        workbook.save(file_path)
        workbook.close()
    
        logger.info(f"XLSX文件已更新: {file_path}")
        return changes_count
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/yuqie6/MCP-Sheet-Parser-cot'

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