☰
从零手写MCP服务端:让AI直接操作Excel的实战指南
2026/10/1 7:27:24 网站建设 项目流程

1. 为什么我要自己动手写一个 MCP

1.1 从一次崩溃的 Excel 处理说起

上个月帮朋友的公司处理一批销售数据,二十多个 Excel 文件,每个文件里七八个 Sheet,需要按区域拆分、按品类汇总、再生成一份带图表的月报。我一开始的想法很朴素:写个 Python 脚本,pandas 读进来,groupby 一下,to_excel 出去,半小时的事。

结果现实给了我一巴掌。表头不在第一行,有的在第三行,有的在第五行;合并单元格满天飞;日期列一半是文本格式一半是日期格式;金额列里混着"约 1200""1200元""1,200"这种写法。我那个脚本改了十几版,每改一版就要重新跑一遍全量数据,跑一次三分钟,一天下来光等脚本跑完就耗掉了大半天。

更难受的是,每次遇到新问题,我都得停下来想"这个逻辑该怎么写",而不是直接告诉工具"帮我把这一列里带'元'字的数字提出来"。那一刻我突然意识到,我缺的不是一个更复杂的脚本,而是一个能让 AI 直接理解我的意图、并且能真正操作 Excel 文件的"手"。

这个"手",就是 MCP。

1.2 MCP 到底是个什么东西

MCP 全称 Model Context Protocol,翻译过来叫"模型上下文协议"。很多人第一次听到"协议"两个字就头大,觉得又是那种要啃几百页文档的东西。其实你可以把它理解成一个标准插座。

想象一下:你家里有台电脑、一个台灯、一个充电器,如果每个电器的插头形状都不一样,你就得给每个电器配一个专门的插座,墙上挂满各种奇形怪状的接口。MCP 干的事,就是把这些接口统一成一种标准形状——只要你的工具按照这个标准做了一个"插头",任何支持 MCP 的 AI 客户端都能直接插上去用。

具体到技术层面,MCP 定义了一套 AI 模型和外部工具之间通信的规范。AI 这边是"客户端",你的工具这边是"服务端"。客户端告诉服务端"我有哪些能力",服务端告诉客户端"我能做什么事、需要什么参数",然后双方通过标准化的消息格式来回沟通。这套机制最大的价值在于解耦:你写的 Excel 处理工具不需要关心对面是哪个 AI,AI 也不需要为每个工具单独写适配代码。

我选择自己写一个 MCP 服务端,而不是用现成的方案,原因有三个。第一,Excel 处理的场景太碎了,通用工具很难覆盖我遇到的那些奇葩表格结构;第二,我想把一些自己积累的处理经验固化进去,比如"遇到合并单元格先展开再处理"这种默认行为;第三,自己写一遍才能真正理解 MCP 的工作机制,以后遇到别的场景能快速迁移。

1.3 这个项目适合谁来参考

如果你符合下面任意一条,这篇内容应该能帮到你:

  • 经常和 Excel 打交道,被重复性的数据整理、格式转换、报表生成折磨过
  • 会一点 Python,但不想每次都从零写脚本
  • 听说过 MCP 但不知道从哪下手,想找一个完整的、能跑起来的例子
  • 想把 AI 真正接入自己的工作流,而不是只在聊天框里问问题

不需要你是 Python 高手,基础的函数、列表、字典操作会就行。MCP 的 SDK 已经把大部分复杂的东西封装好了,我们要做的是把业务逻辑写清楚。

2. 动手之前:环境准备与核心概念对齐

2.1 Python 环境怎么装才不踩坑

Python 安装这件事看起来简单,但我见过太多人在这里翻车。最常见的坑是:电脑上装了 Python,但命令行里敲python提示找不到命令。这通常是因为安装时没勾选"Add Python to PATH"。

我的建议是直接去 Python 官网下载最新稳定版(写这篇的时候是 3.12.x),安装时务必勾选那两个选项:Add python.exe to PATH和Install launcher for all users。装完之后打开命令行,敲python --version,能正常输出版本号就说明成功了。

如果你电脑上已经有多个 Python 版本,或者之前装过 Anaconda,我强烈建议用虚拟环境来隔离这个项目。虚拟环境的好处是:这个项目需要的库不会污染你系统里的其他 Python 环境,删掉的时候也干净。

# 创建虚拟环境 python -m venv mcp-excel-env # Windows 激活 mcp-excel-env\Scripts\activate # macOS / Linux 激活 source mcp-excel-env/bin/activate

激活之后命令行前面会出现(mcp-excel-env)的标识,这时候装的库都只在这个环境里生效。

2.2 需要装哪些库

这个项目的依赖其实不多,核心就几个:

pip install mcp openpyxl pandas

逐个说一下它们的作用。mcp是官方提供的 Python SDK,封装了协议通信的底层细节,我们只需要关注业务逻辑。openpyxl是操作 Excel 文件的主力库,读写 xlsx 格式都靠它,而且它能处理单元格样式、合并单元格这些 pandas 搞不定的东西。pandas用来做数据分析和转换,处理表格数据比纯 Python 循环高效得多。

注意:openpyxl 只能处理 .xlsx 格式,如果你手上有 .xls 的老文件,需要先用 Excel 另存为 xlsx,或者额外装一个xlrd库来读取。

2.3 MCP 的三个核心概念

在写代码之前,必须把三个概念搞清楚,否则后面看代码会一头雾水。

Tool(工具):这是 MCP 服务端对外暴露的能力。一个 Tool 就是一个函数,有名字、有描述、有参数定义。AI 客户端看到这些信息后,就知道"哦,这个服务端能帮我读 Excel、能帮我写 Excel"。AI 决定调用某个 Tool 时,会按照参数定义传进来一个 JSON,我们的函数收到后执行,再把结果返回去。

Resource(资源):资源是只读的数据,比如一个文件的内容、一个数据库的查询结果。和 Tool 的区别在于,Resource 是被动读取的,Tool 是主动执行的。Excel 场景里,我一般把"读取某个 Sheet 的内容"做成 Resource,把"修改某个单元格"做成 Tool。

Transport(传输方式):客户端和服务端之间怎么通信。最常见的是 stdio(标准输入输出),适合本地运行的工具;还有 SSE(Server-Sent Events),适合远程服务。我们做本地 Excel 处理,用 stdio 就够了,配置简单,不需要开端口。

理解了这三个概念,整个开发过程就清晰了:定义 Tool 和 Resource,选好 Transport,把业务逻辑填进去。

3. 核心设计:我的 Excel MCP 长什么样

3.1 功能边界的划定

一开始我想把所有能想到的 Excel 操作都做成 Tool,列了个清单:读取、写入、合并、拆分、排序、筛选、透视、画图、格式转换……列到二十多个的时候我停下来了。Tool 太多会带来两个问题:一是 AI 面对几十个工具容易选错,二是每个 Tool 都要写描述和参数定义,维护成本高。

最后我砍到了六个核心 Tool,覆盖 90% 的日常场景:

Tool 名称功能典型场景
read_sheet读取指定 Sheet 的数据查看表格内容、获取数据做分析
write_cells向指定区域写入数据填充计算结果、更新状态列
list_sheets列出文件里所有 Sheet了解文件结构
get_sheet_info获取 Sheet 的维度、表头位置处理前先摸清结构
transform_column对某一列做批量转换清洗数据、格式统一
create_report按模板生成汇总报表生成月报、周报

这个设计的关键思路是:把高频操作做成原子 Tool,把复杂流程留给 AI 组合。比如"按区域拆分文件"这个需求,AI 可以先用 list_sheets 看结构,再用 read_sheet 读数据,然后用 write_cells 写到新文件里。我不需要为每个组合场景单独写 Tool。

3.2 为什么用 openpyxl 而不是 pandas 做主力

这是个值得展开说的选择。pandas 处理数据确实快,一行pd.read_excel()就能把表格读成 DataFrame,各种 groupby、pivot 用起来很爽。但 pandas 有个致命问题:它不保留格式。

你用 pandas 读一个带合并单元格、带颜色标记、带公式的表格,读进来就是一堆纯数据,原来的格式全丢了。写回去的时候,所有单元格都是默认样式。对于"我要生成一份给老板看的报表"这种需求,格式丢了等于白干。

openpyxl 则相反,它把 Excel 文件当成一个对象树来操作,每个单元格、每个样式、每个合并区域都是独立的对象。你可以精确控制"把 A1 到 C1 合并,背景色设成浅蓝,字体加粗"。代价是操作起来比 pandas 啰嗦,遍历几万行数据会慢一些。

我的方案是两者结合:用 openpyxl 打开文件、处理格式、定位数据区域,把需要计算的数据转成 pandas DataFrame 做分析,算完再写回 openpyxl 的对象里。这样既保留了格式控制能力,又享受了 pandas 的计算效率。

3.3 表头识别的处理策略

前面提到,我遇到的表格表头位置五花八门。这个问题不解决,后面所有操作都是空中楼阁。我的处理策略是写一个detect_header函数,逻辑是这样的:

从第一行开始往下扫,对每一行做评分。评分规则包括:这一行非空单元格的比例、是否包含常见表头关键词(如"日期""金额""数量""名称")、下一行的数据类型是否与这一行不同(表头通常是文本,数据行会有数字或日期)。综合得分最高的那一行,就认定为表头行。

这个逻辑不是百分百准确,但能覆盖大部分情况。对于识别错的,我在 Tool 的参数里留了一个header_row参数,允许手动指定。这种"自动为主、手动兜底"的设计,在实际使用中比纯自动或纯手动都好用。

4. 代码实现:从零到能跑起来

4.1 项目结构

我习惯把项目结构保持简单,一眼能看清哪个文件干什么:

mcp-excel/ ├── server.py # MCP 服务端入口,定义 Tool 和 Resource ├── excel_ops.py # Excel 操作的核心逻辑 ├── utils.py # 工具函数,如表头识别、数据清洗 └── requirements.txt # 依赖清单

server.py 只负责"对接 MCP 协议",excel_ops.py 负责"真正干活"。这样分开的好处是,如果以后要换一个 MCP SDK 或者加别的传输方式,业务逻辑不用动。

4.2 服务端骨架

先看 server.py 的核心结构:

from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import excel_ops app = Server("excel-mcp") @app.list_tools() async def list_tools(): return [ Tool( name="read_sheet", description="读取 Excel 文件中指定 Sheet 的数据,返回二维数组", inputSchema={ "type": "object", "properties": { "file_path": {"type": "string", "description": "Excel 文件路径"}, "sheet_name": {"type": "string", "description": "Sheet 名称"}, "header_row": {"type": "integer", "description": "表头所在行号,从1开始,不填则自动识别"} }, "required": ["file_path", "sheet_name"] } ), # ... 其他 Tool 定义 ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "read_sheet": result = excel_ops.read_sheet( arguments["file_path"], arguments["sheet_name"], arguments.get("header_row") ) return [TextContent(type="text", text=result)] # ... 其他 Tool 的分发 async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())

这段代码里有两个关键点。第一,inputSchema用的是 JSON Schema 格式,它告诉 AI"这个工具需要什么参数、每个参数是什么类型、哪些是必填的"。描述写得越清楚,AI 调用时越不容易出错。第二,call_tool是一个分发器,根据工具名路由到对应的业务函数。这种模式在 MCP 开发里很常见,因为 Tool 数量多了之后,把所有逻辑堆在一个函数里会很难维护。

4.3 表头自动识别的实现

utils.py 里的detect_header是整个项目里我觉得最有价值的一个函数,值得详细说说:

import re HEADER_KEYWORDS = ['日期', '时间', '金额', '数量', '名称', '编号', '类型', '状态', '备注', '部门', '客户', '产品', '单价', '总计'] def detect_header(ws, max_scan_rows=10): best_row = 1 best_score = -1 for row_idx in range(1, min(max_scan_rows, ws.max_row) + 1): score = 0 non_empty = 0 keyword_hits = 0 for cell in ws[row_idx]: if cell.value is not None: non_empty += 1 text = str(cell.value) if any(kw in text for kw in HEADER_KEYWORDS): keyword_hits += 1 # 非空单元格比例得分 if ws.max_column > 0: score += (non_empty / ws.max_column) * 40 # 关键词命中得分 score += keyword_hits * 15 # 下一行数据类型差异得分 if row_idx < ws.max_row: next_row = ws[row_idx + 1] type_diff = 0 for c1, c2 in zip(ws[row_idx], next_row): if c1.value is not None and c2.value is not None: if type(c1.value) != type(c2.value): type_diff += 1 score += type_diff * 5 if score > best_score: best_score = score best_row = row_idx return best_row

这个函数的评分逻辑分三块。非空比例占 40 分,因为表头行通常是填得比较满的。关键词命中每个加 15 分,这是最强的信号。类型差异每个加 5 分,因为表头是文本、数据行是数字的情况很常见。

我实测下来,对于结构规整的表格,这个函数基本能 100% 识别正确。对于那种表头里全是自定义字段名的(比如"Q1销售额""区域负责人"),关键词命中会少一些,但非空比例和类型差异通常能把分数拉上来。如果三个信号都不明显,那就只能靠手动指定了。

4.4 列数据批量转换

transform_column这个 Tool 是我用得最频繁的,因为数据清洗的需求太多了。它的设计思路是:接收一个转换规则,对指定列的所有单元格应用这个规则。

def transform_column(file_path, sheet_name, column, rule, header_row=None): wb = openpyxl.load_workbook(file_path) ws = wb[sheet_name] if header_row is None: header_row = detect_header(ws) col_idx = openpyxl.utils.column_index_from_string(column) changes = [] for row_idx in range(header_row + 1, ws.max_row + 1): cell = ws.cell(row=row_idx, column=col_idx) old_value = cell.value if old_value is None: continue new_value = apply_rule(old_value, rule) if new_value != old_value: cell.value = new_value changes.append({ "row": row_idx, "old": str(old_value), "new": str(new_value) }) wb.save(file_path) return changes

apply_rule支持几种常见的规则类型。strip_currency去掉金额里的"元""¥"等符号并转成数字;normalize_date把各种日期格式统一成 YYYY-MM-DD;extract_number从混合文本里提取数字;trim_space去掉首尾空格。这些规则覆盖了我遇到的大部分清洗需求。

实操心得:批量修改前一定要先备份原文件。我吃过一次亏,规则写错了把一整列数据改成了 None,又没有备份,只能从回收站里翻。现在我的习惯是,任何写操作之前先shutil.copy一份带时间戳的备份。

4.5 报表生成的设计

create_report这个 Tool 稍微复杂一点,它接收一个配置字典,描述"要生成什么样的报表"。配置里包括:数据源文件、分组字段、汇总字段、汇总方式(求和/计数/平均)、输出文件路径。

def create_report(config): df = pd.read_excel(config["source"], sheet_name=config["sheet"]) grouped = df.groupby(config["group_by"])[config["agg_column"]].agg(config["agg_func"]) grouped = grouped.reset_index() grouped.to_excel(config["output"], index=False) # 用 openpyxl 做格式美化 wb = openpyxl.load_workbook(config["output"]) ws = wb.active for cell in ws[1]: cell.font = openpyxl.styles.Font(bold=True) cell.fill = openpyxl.styles.PatternFill( start_color="D9E1F2", fill_type="solid" ) wb.save(config["output"]) return f"报表已生成:{config['output']}"

这里体现了前面说的"pandas 算数据、openpyxl 做格式"的组合思路。pandas 负责 groupby 和聚合,算完写出去;openpyxl 再把表头加粗、加背景色。两步分开,各司其职。

5. 接入 AI 客户端与实战验证

5.1 客户端配置

MCP 服务端写好了,怎么让 AI 用上它?需要在支持 MCP 的客户端里加一段配置。以常见的桌面客户端为例,配置文件里加这么一段:

{ "mcpServers": { "excel-mcp": { "command": "python", "args": ["/path/to/mcp-excel/server.py"], "env": {} } } }

command是启动命令,args是参数。如果你用的是虚拟环境,command要指向虚拟环境里的 python 可执行文件,否则会找不到依赖。Windows 上是mcp-excel-env\Scripts\python.exe,macOS 和 Linux 上是mcp-excel-env/bin/python。

配置好之后重启客户端,如果一切正常,AI 就能看到我们定义的六个 Tool 了。你可以直接问它"帮我看看这个 Excel 文件里有哪些 Sheet",它会自动调用list_sheets。

5.2 一个完整的实战案例

我拿一个真实的销售数据文件来演示。文件叫sales_2024.xlsx,里面有 12 个 Sheet,每个 Sheet 是一个月的销售记录。表头在第 3 行,列包括:订单号、日期、区域、产品、数量、单价、金额。金额列里混着"1200""1,200元""约 800"这几种写法。

第一步,我告诉 AI:"帮我看看 sales_2024.xlsx 里有哪些 Sheet,每个 Sheet 的结构是什么样的。"

AI 调用list_sheets拿到 12 个 Sheet 名,然后对第一个 Sheet 调用get_sheet_info,返回了维度信息和自动识别的表头行号(识别出是第 3 行)。AI 把结果整理给我看,确认结构一致。

第二步,我说:"金额列的数据格式很乱,帮我统一成纯数字。"

AI 调用transform_column,参数是column="G"、rule="extract_number"。函数执行后返回了修改记录,告诉我哪些单元格被改了、改成了什么。我抽查了几条,确认"1,200元"变成了 1200,"约 800"变成了 800。

第三步,我说:"把所有月份的数据合并,按区域和产品汇总金额,生成一份汇总报表。"

AI 先对每个 Sheet 调用read_sheet读取数据,在对话里做合并(这一步是 AI 自己在推理,不是调 Tool),然后调用create_report,传入分组字段和汇总配置。几十秒后,一份带格式的汇总报表就生成了。

整个过程我一行代码没写,只是用自然语言描述需求。这就是 MCP 的价值:把"写脚本"变成了"说需求"。

5.3 性能实测数据

我用一个 5 万行的文件做了个简单的性能测试,对比纯 Python 脚本和 MCP 方式的耗时:

操作纯 Python 脚本MCP 方式差异原因
读取单 Sheet1.2s1.4sMCP 多了一层协议通信开销
列转换(5万行)3.5s3.8s开销可忽略
生成汇总报表2.1s2.3s主要是 pandas 计算时间
端到端(含 AI 推理)不适用15-30s大头是 AI 理解需求的时间

结论很清楚:MCP 本身的性能开销很小,主要耗时在 AI 推理环节。对于交互式的、一次性的任务,这个耗时完全可以接受。对于需要跑几千次的批处理任务,还是写脚本更合适。MCP 的定位是"让 AI 帮你处理那些不常做、但每次都要想半天的任务"。

6. 踩过的坑与排查手册

6.1 常见问题速查

现象可能原因解决方法
客户端看不到 Tool服务端启动失败手动运行 server.py 看报错
调用 Tool 报"文件不存在"路径用了相对路径改用绝对路径
读取中文乱码文件编码问题确认文件是 xlsx 而非 csv
写入后格式丢失用了 pandas 写入改用 openpyxl 写入
合并单元格读取为 Noneopenpyxl 特性先展开合并单元格再读
大文件内存溢出一次性加载用 read_only 模式

6.2 合并单元格这个坑

openpyxl 读取合并单元格时,只有左上角的单元格有值,其他单元格都是 None。这个特性坑了我很久。比如一个表头"销售数据"合并了 A1 到 D1,你读 B1、C1、D1 都是 None。

解决办法是先用ws.merged_cells.ranges拿到所有合并区域,然后写一个展开函数,把合并区域的值填充到每个单元格:

def unmerge_and_fill(ws): for merged_range in list(ws.merged_cells.ranges): top_left = ws.cell( row=merged_range.min_row, column=merged_range.min_col ).value for row in range(merged_range.min_row, merged_range.max_row + 1): for col in range(merged_range.min_col, merged_range.max_col + 1): ws.cell(row=row, column=col).value = top_left ws.unmerge_cells(str(merged_range))

这个函数要在读取数据之前调用。展开之后,每个单元格都有值了,后续处理就正常了。

6.3 路径问题的排查思路

MCP 服务端启动时的"当前工作目录"和你想的可能不一样。客户端启动服务端时,工作目录通常是客户端自己的目录,不是 server.py 所在的目录。所以代码里用相对路径data/sales.xlsx大概率会找不到文件。

我的做法是:所有文件路径都要求传绝对路径。在 Tool 的描述里明确写"请传入绝对路径"。如果 AI 传了相对路径,函数里做一个转换,基于 server.py 所在目录来解析:

import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) def resolve_path(path): if os.path.isabs(path): return path return os.path.join(BASE_DIR, path)

6.4 独家避坑技巧

技巧一:Tool 描述要写得像给新人看的文档。AI 选 Tool 和填参数,全靠描述。描述里要写清楚"这个工具做什么""什么时候用""参数格式是什么""返回什么"。我见过有人描述只写"读取 Excel",结果 AI 经常传错参数。改成"读取指定 Excel 文件中指定 Sheet 的数据,返回二维数组,第一行是表头"之后,准确率明显提升。

技巧二:写操作一定要有返回值。修改类 Tool 不要返回"成功"两个字就完事,要返回具体改了什么。我让transform_column返回修改记录列表,AI 拿到后可以告诉我"改了 37 个单元格,其中 5 个从'约 800'变成了 800"。这样我能快速判断改得对不对。

技巧三:给 Tool 加一个 dry_run 参数。对于破坏性操作,加一个dry_run布尔参数,为 true 时只返回"将会做什么"而不实际执行。这样 AI 可以先跑一遍 dry_run 给我看,我确认后再真正执行。这个设计在批量修改场景下特别有用。

技巧四:日志要写到文件里。MCP 服务端用 stdio 通信,print 的内容会干扰协议消息。调试信息不要用 print,用 logging 写到文件里。我配置了一个 rotating file handler,每次调用 Tool 都记一条日志,出问题的时候翻日志比猜快得多。

7. 后续可以怎么扩展

这个 MCP 服务端目前只覆盖了 Excel 处理的基础场景,但框架已经搭好了,扩展起来很方便。我列几个我打算做的方向。

第一个是加图表生成能力。openpyxl 支持创建柱状图、折线图、饼图,可以做一个create_chartTool,接收数据区域和图表类型,自动插入到指定位置。这样生成月报的时候连图都不用自己画了。

第二个是加模板填充能力。很多报表的格式是固定的,只是数据在变。可以做一个fill_templateTool,接收一个模板文件和数据字典,把数据填到模板的占位符里。这个在财务、人事场景下特别实用。

第三个是加多文件批量处理。现在的 Tool 都是针对单个文件的,可以加一个batch_processTool,接收一个目录路径和一个操作列表,对目录下所有 Excel 文件依次执行。配合 dry_run 参数,批量操作也能很安全。

第四个是加数据校验。在写入之前检查数据是否符合预期,比如"金额不能为负""日期不能超过今天""必填字段不能为空"。校验不通过就返回错误信息,而不是默默写进去。这个能避免很多低级错误。

写 MCP 这件事,最大的收获不是做出了一个工具,而是理解了 AI 和外部世界交互的这套机制。一旦你掌握了这个模式,任何重复性的、有明确规则的工作,都可以包装成一个 MCP 服务端,让 AI 帮你干。Excel 只是一个开始。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询