excel-mcp-audit 0.1.0
MCP Server for Excel file parsing and editing
Requires Python: >=3.10

# Excel 解析服务

基于 `openpyxl` 的轻量级 Excel 解析与 Markdown 转换工具，用于将 `.xlsx` 文件解析为**适合大模型使用**的结构化摘要信息和表格片段。

## 适用场景

- 需要在大模型里“浏览”和理解复杂的 Excel 表格
- 只希望把关键 sheet 的 **结构化摘要** 和 **少量行的 Markdown 预览** 喂给大模型
- 按需抽取某个区域（例如一个明细表片段）并转成 Markdown 后交给大模型继续分析
- 希望通过 MCP 把本地 Excel 文件作为一个工具暴露给兼容 MCP 的客户端

## 功能概述

- 提取工作簿元信息：Sheet 数量、名称列表
- 识别每个 Sheet 的 Used Range（起始/结束单元格、行列索引）
- 将 Used Range 的前/后 N 行转换为 Markdown 表格（默认 N=20）
- Markdown 表头使用单元格地址（如 `A1 | B1 | C1`），便于在对话中引用
- 支持按重复值展开合并单元格区域（方便大模型理解合并单元格）
- 按区域渲染完整 Markdown 表格，适合裁剪出小片段交给大模型
- **编辑类能力**：
  - 创建新工作簿，以及创建 / 重命名 / 删除工作表
  - 编辑单个单元格、批量写入区域、写入公式
  - 插入 / 删除行列
  - 统一格式化单元格（颜色、字体、边框、合并、锁定等）
  - 按值查找单元格位置（支持精确 / 模糊匹配）
  - 复制粘贴单元格区域（仅复制值，支持同工作簿和跨工作簿）

## 项目结构

- `excel_toolkit/`
  - `workbook.py`: 工作簿级解析与摘要（`parse_workbook` 等）
  - `sheet.py`: 工作表级 used range 推断、预览构建与插入行列、创建 sheet 等操作
  - `range.py`: 范围/单元格级 Markdown 渲染、编辑、复制、填色、查找等操作
  - `models.py`: 内部使用的数据模型（`UsedRange`、`SheetPreview`、`SheetSummary` 等 dataclass），不直接对外暴露
- `mcp_server/`
  - `server.py`: 基于 FastMCP 的 MCP Server 封装，统一暴露在 `excel-extractor` 应用下
- `examples/demo.py`: 基于 `excel_toolkit` 的本地示例脚本
- `excel_tools.py`: 将 workbook/sheet/range 能力整合为单文件，可直接通过命令行执行

## 命令行工具 `excel_tools.py`

无需引入整个包时，可以直接使用仓库根目录下的 `excel_tools.py`：

```powershell
python excel_tools.py parse <file>                           # 解析工作簿结构
python excel_tools.py sheets <file>                          # 列出所有工作表
python excel_tools.py read <file> <sheet> <range>            # 读取指定区域
python excel_tools.py find <file> <sheet> <keyword>          # 在单个 sheet 中搜索内容
python excel_tools.py find_all <file> <keyword>              # 在所有 sheet 中搜索
python excel_tools.py cell <file> <sheet> <cell>             # 读取单个单元格
python excel_tools.py edit <file> <sheet> <cell> <value>     # 编辑单元格
python excel_tools.py create-workbook <path> [--sheet-name]  # 创建新工作簿
python excel_tools.py create-sheet <file> <sheet>            # 创建工作表（自动去重名）
python excel_tools.py rename-sheet <file> <old> <new>        # 重命名工作表
python excel_tools.py delete-sheet <file> <sheet>            # 删除工作表
python excel_tools.py insert-dimension <file> <sheet> <axis> <index> [--count]
python excel_tools.py delete-dimension <file> <sheet> <axis> <index> [--count]
python excel_tools.py apply-formula <file> <sheet> <cell> <formula>
python excel_tools.py edit-range <file> <sheet> <range> --values-json JSON
python excel_tools.py format-range <file> <sheet> <start> [--end-cell]
python excel_tools.py copy-range <file> <sheet> <source> <target>
python excel_tools.py copy-between <src_file> <src_sheet> <src_range> \
                                      <dst_file> <dst_sheet> <dst_start>
```

常用参数：

- `--preview-rows`、`--no-expand`：用于 `parse`/`read` 等命令，控制 Markdown 预览的行数和是否展开合并单元格。
- `--mode`、`--match-case`：用于 `find`/`find_all` 命令，控制匹配方式及是否区分大小写。
- `--values-json`：`edit-range` 的二维数组载体，传入 JSON 字符串即可。
- `format-range` 中的 `--bold`、`--font-color`、`--bg-color`、`--border-style`、`--merge`、`--locked` 等参数可混合使用，无法全部列出时可通过 `-h` 查看完整帮助。
- `insert-dimension` / `delete-dimension` 的 `axis` 只能是 `row` 或 `column`，`--count` 默认为 1。

示例：

```powershell
python excel_tools.py parse examples/测试.xlsx > summary.json
python excel_tools.py read examples/测试.xlsx Sheet1 A1:D20
python excel_tools.py edit examples/测试.xlsx Sheet1 B2 "Hello"
```

## 安装依赖

建议使用虚拟环境，然后安装依赖：

```powershell
pip install -r requirements.txt
```

## 基本用法：解析整个工作簿

```python
from excel_toolkit.workbook import parse_workbook

summary = parse_workbook("测试.xlsx", preview_rows=20, expand_merged_cells=True)

print(summary["sheet_count"])
for sheet in summary["sheets"]:
  # parse_workbook 已经返回 JSON-safe dict 结构
  print(sheet["sheet_name"], sheet["row_count"], sheet["col_count"])
  preview = sheet.get("preview")
  if preview:
    print(preview.get("head_markdown"))

# summary 的典型结构示例（简化版）：
# {
#   "file_name": "测试.xlsx",
#   "sheet_count": 2,
#   "sheets": [
#     {
#       "sheet_index": 0,
#       "sheet_name": "Sheet1",
#       "row_count": 120,
#       "col_count": 5,
#       "used_range": {
#         "start_cell": "A1", "end_cell": "E120",
#         "start_row": 1, "start_col": 1, "end_row": 120, "end_col": 5,
#       },
#       "preview": {
#         "mode": "head_tail",
#         "preview_row_count": 20,
#         "head_row_count": 20,
#         "tail_row_count": 20,
#         "head_markdown": "| A1 | B1 | C1 | ...",
#         "tail_markdown": "| A101 | B101 | C101 | ...",
#       },
#     },
#     ...
#   ],
# }
```

你也可以直接运行示例脚本：

```powershell
python examples/demo.py
```

默认会尝试解析当前目录下的 `测试.xlsx` 文件，你可以根据需要修改 `examples/demo.py` 中的文件名或在交互中输入其他路径。

## 编辑和操作工作簿

除了只读解析以外，本项目还提供了一些轻量级的编辑能力，方便在 MCP / 本地脚本中对 Excel 做简单修改。

### 创建工作簿

```python
from excel_toolkit.workbook import create_workbook

first_sheet = create_workbook("新建工作簿.xlsx", sheet_name="Summary")
print("first sheet:", first_sheet)  # 例如 "Summary"
```

行为说明：

- 仅支持通过文件路径字符串创建工作簿；
- 若目标路径已存在，将被 **覆盖**（openpyxl 默认行为）；
- 可以可选指定首个工作表名称：
  - 首尾空白会被去除，去除后为空则抛出 `ValueError`；
  - 工作表名最大长度 31 个字符，超长会被截断；
  - 名称中不允许包含 `: \\ / ? * [ ]`，否则抛出 `ValueError`；
- 返回值为实际使用的首个工作表名称。

### 创建工作表

```python
from excel_toolkit.sheet import create_sheet

final_name = create_sheet("测试.xlsx", "Summary")
print("created sheet:", final_name)  # 若已存在，会自动变成 "Summary (2)" 等
```

行为说明：

- 名称会先去掉首尾空白，若为空则抛出 `ValueError`。
- Excel 工作表名最长 31 个字符，超长会被截断。
- 名称中不允许包含 `: \\ / ? * [ ]`。
- 如与现有工作表重名，会在末尾自动追加 ` (2)`, ` (3)` ... 直到不重名。

### 修改工作表名称

```python
from excel_toolkit.sheet import rename_sheet

new_name = rename_sheet(
  file_source="测试.xlsx",
  old_name="Sheet1",
  new_name="明细表",
)
print("renamed to:", new_name)
```

行为说明：

- `old_name` 必须在工作簿中存在，否则抛出 `ValueError`；
- `new_name` 会先去除首尾空白，若为空则抛出 `ValueError`；
- 名称规则与 Excel 一致：
  - 最长 31 个字符，超长会被截断；
  - 不允许包含 `: \\ / ? * [ ]`；
- 若新名称与**其它**工作表重名（排除自身），会抛出 `ValueError`，不会自动追加后缀——由调用方自行决定重名时的策略；
- 仅当 `file_source` 为路径字符串时才会写回磁盘。

### 删除工作表

```python
from excel_toolkit.sheet import delete_sheet

delete_sheet(
  file_source="测试.xlsx",
  sheet_name="临时表",
)
```

行为说明：

- 若 `sheet_name` 不存在，则抛出 `ValueError`；
- 若删除后工作簿将不再包含任何工作表（openpyxl 不允许），则抛出 `ValueError`；
- 仅当 `file_source` 为路径字符串时才写回原文件。

### 编辑单个单元格

```python
from excel_toolkit.range import edit_cell

edit_cell(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  cell_address="B2",
  value="Hello",
)
```

- 仅支持文件路径字符串，以便原地保存。
- `cell_address` 必须是单个单元格（如 `A1`），传入区域（如 `A1:B2`）会抛出 `ValueError`。

### 批量写入单元格区域

```python
from excel_toolkit.range import edit_range

edit_range(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  cell_range="A1:C3",
  values=[
    ["R1C1", "R1C2", "R1C3"],
    ["R2C1", "R2C2"],  # 第二行只写前两列
  ],
)
```

语义：

- 仅对提供的数据范围进行写入：
  - 若 `cell_range="A1:C3"`，但只提供一行 3 列数据，则只会填充 `A1:C1`，其余行保持不变。
- 每行按该行的实际长度写入，超出 `cell_range` 的列会被自动截断。

### 插入行 / 列

```python
from excel_toolkit.sheet import insert_dimension

info = insert_dimension(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  index=5,       # 从第 5 行开始插入
  count=3,       # 插入 3 行
  axis="row",  # 或 "column" 插入列
)

print(info)  # {"start_cell": "A5", "end_cell": "D7"} 之类，取决于当前 Used Range
```

说明：

- `axis="row"` 表示插入行，`axis="column"` 表示插入列。
- 返回的 `start_cell` / `end_cell` 表示此次插入出来的新区域，另一维度的边界基于当前 Used Range 推断。

### 删除行 / 列

```python
from excel_toolkit.sheet import delete_dimension

info = delete_dimension(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  index=5,        # 从第 5 行开始删除
  count=3,        # 删除 3 行
  axis="row",  # 或 "column" 删除列
)

print(info)  # {"start_cell": "A5", "end_cell": "DXX"} 之类，表示受影响的大致区域
```

说明：

- 接口形式与 `insert_dimension` 对称：`index` 为起始行/列号，`count` 为删除数量，`axis` 为 "row" / "column"；
- 仅支持 `file_source` 为路径字符串，否则抛出 `TypeError`；
- `count <= 0` 或 `axis` 非 "row"/"column" 时抛出 `ValueError`；
- 返回的 `start_cell` / `end_cell` 表示**删除后**受影响区域的大致范围，主要用于提示大模型或前端“哪里发生了结构变化”，不是精确的可视范围。

### 按值查找单元格位置

```python
from excel_toolkit.range import find_cells

matches = find_cells(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  value="目标值",
  match_case=False,   # 是否区分大小写
  mode="exact",      # "exact" 精确匹配，"contains" 包含匹配（仅对字符串）
)

for m in matches:
  print(m["cell"], m["value"])
```

匹配规则：

- 非字符串目标值：无论 `mode` 如何，始终使用 `==` 比较。
- 字符串目标值：
  - `mode="exact"`：等值比较（可选是否忽略大小写）。
  - `mode="contains"`：判断目标值是否为单元格字符串的子串（可选是否忽略大小写）。

返回值为一个列表，每个元素形如：

```python
{
    "cell": "B2",   # 单元格地址
    "row": 2,
    "column": 2,
    "value": "目标值",  # 单元格原始值
}
```

### 为单元格区域设置填充颜色

```python
from excel_toolkit.range import fill_range_color

fill_range_color(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  cell_range="A1:C3",
  fill_color="FFFF0000",  # 不透明红色
)
```

颜色编码规范：

- 推荐使用 8 位 ARGB 十六进制字符串 `AARRGGBB`：
  - `AA`: Alpha 通道（透明度），`FF` 表示不透明。
  - `RR`: 红色分量（00-FF）。
  - `GG`: 绿色分量（00-FF）。
  - `BB`: 蓝色分量（00-FF）。
- 示例：
  - 红色：`FFFF0000`
  - 绿色：`FF00FF00`
  - 蓝色：`FF0000FF`
- 若只提供 6 位 RGB（如 `FF0000`），内部会自动补全为 `FFFF0000`（假定 Alpha 为 FF）。

### 统一格式化单元格区域（字体/边框/对齐等）

如果需要一次性设置字体、背景、边框、对齐等多种属性，可以使用更通用的 `format_range`：

```python
from excel_toolkit.range import format_range

info = format_range(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  start_cell="A1",
  end_cell="C3",       # 可选，不传则只格式化单个单元格
  bold=True,
  italic=False,
  underline=False,
  font_size=12,
  font_color="FFFFFF",  # 或 "FFFFFFFF"，6 位/8 位都支持
  bg_color="0000FF",    # 蓝底白字
  border_style="thin",
  border_color="000000",
  number_format="0.00",
  alignment="center",
  wrap_text=True,
  merge_cells=False,      # 若为 True 且提供 end_cell，则会合并区域
  locked=None,            # 可选：True/False，设置单元格锁定标记
)

print(info)  # {"start_cell": "A1", "end_cell": "C3"}
```

要点说明：

- 颜色参数（`font_color` / `bg_color` / `border_color`）支持：
  - 6 位 RGB（`RRGGBB`），例如 `"FF0000"`；
  - 8 位 ARGB（`AARRGGBB`），例如 `"FFFF0000"`；
  - 内部会统一补全为 8 位 ARGB；
- `border_style` 透传给 openpyxl（常见值：`thin` / `medium` / `thick` / `double` 等），四条边统一样式；
- `alignment` 透传给 openpyxl（常见值：`left` / `center` / `right` / `justify`），垂直方向默认居中；
- `locked` 只会设置单元格的锁定属性，不会自动启用工作表保护。

### 复制粘贴单元格区域（仅复制值）

当前复制能力**只会复制单元格值，不会复制任何格式/样式**，以避免跨工作簿样式兼容问题，也更适合“数据整合”类场景。

支持两种常见情况：

1. 同一工作簿内、同一工作表中的区域复制；
2. 在**不同工作簿**之间复制区域内容。

#### 同一工作簿内复制

```python
from excel_toolkit.range import copy_range

copy_range(
  file_source="测试.xlsx",
  sheet_name="Sheet1",
  source_range="B2:D90",   # 源区域
  target_start_cell="H2",  # 以该单元格为左上角粘贴
)
```

行为说明：

- 仅复制 `source_range` 中每个单元格的 `.value`；
- 在 `target_start_cell` 位置开始，按相同的行数 × 列数创建目标区域并写入值；
- 不会复制字体、边框、背景色、数字格式等任何样式；
- 若源/目标区域重叠，仍按“从上到下、从左到右”的顺序覆盖写入。

#### 在不同工作簿之间复制

```python
from excel_toolkit.range import copy_range_between_workbooks

copy_range_between_workbooks(
  source_path=r"C:\Users\you\Desktop\源表.xlsx",
  source_sheet_name="Sheet1",
  source_range="B2:D90",
  target_path=r"C:\Users\you\Desktop\目标表.xlsx",
  target_sheet_name="合并",
  target_start_cell="B2",
)
```

行为说明：

- `source_path` 和 `target_path` 必须是有效的 `.xlsx` 文件路径；
- 函数会分别加载源/目标工作簿，只复制值到目标区域，然后保存目标工作簿；
- 同样不复制任何格式/样式，仅移动数据。

  ## MCP 工具一览

  本仓库提供的所有能力，都会通过 `mcp_server/server.py` 暴露为 MCP 工具，便于在 Cherry Studio 等客户端中直接调用。

  > MCP 应用名称：`excel-extractor`

  ### excel_parse_workbook

  解析 Excel，并返回结构化的工作簿摘要信息和每个工作表的 Markdown 预览。

  - 参数：
    - `path: string` – Excel 文件在 Server 端本机上的路径。
    - `preview_rows: int = 20` – 头 / 尾各预览多少行。
    - `expand_merged_cells: bool = true` – 是否展开合并单元格为重复值。
  - 返回：
    - 一个包含 `file_name` / `sheet_count` / `sheets[]` 的结构化对象（字段结构见上文“核心解析结果结构”章节）。

  常见用途：读取整个工作簿的结构、行列数、Used Range、以及 Markdown 预览片段，用于“先浏览再决定要渲染的范围”。

  ### excel_render_range_markdown

  渲染指定工作表中某个单元格区域为 Markdown 表格，表头使用单元格地址。

  - 参数：
    - `path: string` – Excel 文件路径。
    - `sheet_name: string` – 工作表名称（精确匹配）。
    - `cell_range: string` – 区域，例如 `"A1:D20"`。
    - `expand_merged_cells: bool = true` – 是否展开合并单元格。
  - 返回：
    - 该区域的完整 Markdown 表格字符串。

  ### excel_edit_cell

  编辑单个单元格，并保存回原 Excel 文件。

  - 参数：
    - `path: string` – Excel 文件路径。
    - `sheet_name: string` – 工作表名称。
    - `cell_address: string` – 单元格地址，例如 `"B2"`。
    - `value: any` – 要写入的值（在 MCP 中通常以 JSON 字符串或基础类型传递）。
  - 返回：
    - 文本确认信息，例如：`"Cell Sheet1!B2 updated"`。

  注意：`cell_address` 必须是单个单元格地址，传入诸如 `"A1:B2"` 将抛出错误。

  ### excel_edit_range

  按给定的单元格区域和二维数组批量写入，不做广播，超出部分不写。

  - 参数：
    - `path: string`
    - `sheet_name: string`
    - `cell_range: string` – 例如 `"A1:C3"`。
    - `values: any[][]` – 二维数组，外层为行，内层为列。
  - 返回：
    - 文本确认信息，例如：`"Range Sheet1!A1:C3 updated"`。

  行为要点：

  - 仅对提供的数据范围进行写入（不做广播）；未提供的行/列保持原样。
  - 若 `values` 为空或每行都为空，则不会进行任何写入。

  ### excel_create_sheet

  在指定 Excel 文件中创建新工作表，处理好名称合法性和重名问题。

  - 参数：
    - `path: string`
    - `sheet_name: string` – 希望创建的名称（可能被截断或加后缀）。
  - 返回：
    - 实际创建成功的工作表名称，例如：`"Summary (2)"`。

  ### excel_create_workbook

  创建一个新的 Excel 工作簿文件，并返回首个工作表的名称。

  - 参数：
    - `path: string` – 新工作簿要保存到的路径（若已存在将被覆盖）。
    - `sheet_name?: string` – 可选的首个工作表名称，不传则使用 openpyxl 默认名称（通常是 `Sheet`）。
  - 返回：
    - 实际使用的首个工作表名称。

  ### excel_insert_dimension

  插入若干行或列，并返回这次新插入区域的单元格范围地址。

  - 参数：
    - `path: string`
    - `sheet_name: string`
    - `index: int` – 插入起始行号 / 列号（1 基）。
    - `count: int = 1` – 插入数量，必须为正整数。
    - `axis: "row" | "column" = "row"` – 插入行还是列。
  - 返回：
    - 对象 `{ start_cell: string, end_cell: string }`，例如：`{"start_cell": "A5", "end_cell": "D7"}`。

  ### excel_delete_dimension

  删除若干行或列，并返回这次操作后受影响区域的大致单元格范围。

  - 参数：
    - `path: string`
    - `sheet_name: string`
    - `index: int` – 起始行/列号（1 基）。
    - `count: int = 1` – 删除数量，必须为正整数。
    - `axis: "row" | "column" = "row"` – 删除行还是列。
  - 返回：
    - 对象 `{ start_cell: string, end_cell: string }`，表示**删除后**受影响区域的大致范围。
  - 异常：
    - `sheet_name` 不存在、`count <= 0`、`axis` 非法等情况都会抛出错误。

  ### excel_find_cells

  按值查找某个工作表中所有匹配的单元格位置，支持精确匹配和包含匹配。

  - 参数：
    - `path: string`
    - `sheet_name: string`
    - `value: any` – 要查找的目标值，可以是字符串或数字等。
    - `match_case: bool = false` – 当 value 为字符串时，是否区分大小写。
    - `mode: "exact" | "contains" = "exact"` – 匹配模式。
  - 返回：
    - 列表，每个元素形如：
      - `{ cell: string, row: number, column: number, value: any }`。

  匹配规则简述：

  - 非字符串：始终使用 `==` 比较。
  - 字符串：
    - `exact`：等值比较（可选是否忽略大小写）。
    - `contains`：判断目标值是否为单元格字符串的子串（可选是否忽略大小写）。

  ### excel_fill_range_color

  对单元格区域设置填充颜色（PatternFill），例如高亮某个表头或标记出查找到的行。

  - 参数：
    - `path: string`
    - `sheet_name: string`
    - `cell_range: string` – 例如 `"A1:C3"`。
    - `fill_color: string` – 6 位 RGB 或 8 位 ARGB 十六进制，例如 `"FFFF0000"`。
  - 返回：
    - 文本确认信息，例如：`"Filled color FFFF0000 to range Sheet1!A1:C3"`。

  ### excel_format_range

  为一段单元格区域应用统一的格式设置，是 `excel_fill_range_color` 的超集：

  - 参数（主要）：
    - `path: string`
    - `sheet_name: string`
    - `start_cell: string`
    - `end_cell?: string` – 可选，若省略则只格式化单个单元格；
    - `bold?: bool`
    - `italic?: bool`
    - `underline?: bool`
    - `font_size?: number`
    - `font_color?: string` – 6/8 位十六进制颜色；
    - `bg_color?: string` – 6/8 位十六进制颜色；
    - `border_style?: string` – 例如 `"thin"`；
    - `border_color?: string` – 6/8 位十六进制颜色；
    - `number_format?: string` – Excel 数字格式字符串；
    - `alignment?: string` – `left` / `center` / `right` / `justify`；
    - `wrap_text?: bool` – 是否自动换行；
    - `merge_cells?: bool` – 是否合并区域；
    - `locked?: bool` – 是否标记单元格锁定。
  - 返回：
    - `{ start_cell: string, end_cell: string }`，表示实际应用格式的区域范围。

  颜色规则与上文“为单元格区域设置填充颜色”一节相同：推荐使用 `AARRGGBB`，仅写 `RRGGBB` 时会自动补全 Alpha 为 `FF`。

  ### excel_copy_paste_range（同一工作表内仅复制值）

  在**同一 Excel 文件、同一工作表**中复制粘贴一个单元格区域，只复制值、不复制格式。

  - 参数：
    - `path: string` – Excel 文件路径。
    - `sheet_name: string` – 工作表名称。
    - `source_range: string` – 源区域，例如 `"B2:D90"`。
    - `target_start_cell: string` – 目标区域左上角，例如 `"H2"`。
  - 返回：
    - 文本确认信息，例如：`"Copied values from Sheet1!B2:D90 to Sheet1!H2 (values only)"`。

  ### excel_copy_paste_between_workbooks（跨工作簿仅复制值）

  在**不同 Excel 工作簿**之间复制一个单元格区域，只复制值、不复制格式。

  - 参数：
    - `source_path: string` – 源工作簿路径。
    - `source_sheet_name: string` – 源工作表名称。
    - `source_range: string` – 源区域，例如 `"B2:D90"`。
    - `target_path: string` – 目标工作簿路径。
    - `target_sheet_name: string` – 目标工作表名称。
    - `target_start_cell: string` – 目标区域左上角，例如 `"B2"`。
  - 返回：
    - 文本确认信息，例如：
      `"Copied values from Sheet1!B2:D90 in 源表.xlsx to 合并!B2 in 目标表.xlsx (values only)"`。

## 按区域渲染完整 Markdown 表格

如果你只关心某个工作表中的一小块区域，可以使用 `excel_toolkit.range.render_range_markdown` 直接把该区域渲染成完整的 Markdown 表格：

```python
from excel_toolkit.range import render_range_markdown

markdown = render_range_markdown(
  file_source="测试.xlsx",      # 文件路径、字节流或二进制文件对象
  sheet_name="Sheet1",        # 工作表名称
  cell_range="A1:D20",        # 区域：左上到右下，例如 A1:D20
  expand_merged_cells=True,    # 是否展开合并单元格，默认 True
)

print(markdown)

# markdown 的典型输出示例（假设 cell_range="A1:C3"）：
# | A1 | B1 | C1 |
# | --- | --- | --- |
# | 姓名 | 部门 | 金额 |
# | 张三 | 销售部 | 1000 |
# | 李四 | 技术部 | 2000 |
```

参数说明：

- `file_source`: Excel 文件来源，可以是文件路径字符串、字节串或二进制文件对象（如 `open("foo.xlsx", "rb")`）。
- `sheet_name`: 要渲染的工作表名称，若不存在会抛出 `ValueError`。
- `cell_range`: 形如 `"A1:D20"` 的区域字符串；函数会将这一矩形区域的所有单元格按行读出：
  - 第一行会被当作 Markdown 表头对应的数据行；
  - Markdown 实际的表头行使用单元格地址（例如 `A1 | B1 | C1 | ...`）。
- `expand_merged_cells`: 是否按“填充重复值”的方式展开合并单元格；如果为 `True`（默认），某个合并区域内的所有单元格都会显示为左上角单元格的值，方便大模型理解表格结构。

## 作为 MCP Server 使用

本仓库自带一个基于 [FastMCP](https://github.com/modelcontextprotocol/python-fastmcp) 的 MCP 服务器，位于 `mcp_server/server.py`，会把解析、渲染和编辑等能力统一暴露为上面列出的所有 MCP 工具。

### 额外依赖

除了基础依赖外，你需要安装 `fastmcp`：

```powershell
pip install fastmcp
```

如果你已经通过 `requirements.txt` 安装了依赖，请确认其中已包含 `fastmcp`，或自行补充。

### 本地启动（调试用）

在项目根目录下运行：

```powershell
python -m mcp_server.server
```

`server.py` 中的入口会调用 `app.run()`，启动一个符合 MCP 协议的进程。实际在支持 MCP 的客户端（例如部分 AI 助手 / MCP 开发工具）中，一般会通过配置命令行启动参数来拉起该进程，而不是手动执行上面的命令。

### 暴露的工具

#### 1. excel_parse_workbook

解析整个工作簿，返回 sheet 元信息以及 Markdown 预览。

签名（Python 侧）：

```python
@app.tool()
def excel_parse_workbook(
    path: str,
    preview_rows: int = 20,
    expand_merged_cells: bool = True,
) -> Dict[str, Any]:
    ...
```

关键参数：

- `path`: 服务器本机上的 Excel 文件路径。
- `preview_rows`: 前/后预览的行数，默认 20。
- `expand_merged_cells`: 是否展开合并单元格，默认 `True`。

返回值是一个 JSON 友好的字典结构，包含：

- `file_name`: 文件名
- `sheet_count`: sheet 总数
- `sheets`: 每个 sheet 的行列统计、Used Range 以及 head/tail 的 Markdown 预览

#### 2. excel_render_range_markdown

返回指定工作表中某个区域的完整 Markdown 表格。

签名：

```python
@app.tool()
def excel_render_range_markdown(
    path: str,
    sheet_name: str,
    cell_range: str,
    expand_merged_cells: bool = True,
) -> str:
    ...
```

参数：

- `path`: 服务器本机上的 Excel 文件路径。
- `sheet_name`: 工作表名称，例如 `"Sheet1"`。
- `cell_range`: 单元格区域，例如 `"A10:D30"`。
- `expand_merged_cells`: 是否展开合并单元格，默认 `True`。

返回值是一个 Markdown 表格字符串，区域第一行作为数据表头；Markdown 实际的表头行仍然使用单元格地址，方便调用方在自然语言层面描述这一段区域。