☰
从零开发MCP服务:用AI重构Excel处理工作流
2026/11/2 9:03:40 网站建设 项目流程

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

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

上个月帮朋友处理一份销售数据,三千多行,需要按区域拆分、按品类汇总、再生成十几张图表。我当时的做法很原始:打开 Excel,写几个公式,手动拖拽,再录个宏。结果做到一半,Excel 卡死了,没保存。那一刻我盯着屏幕,心里只有一个念头——这种重复劳动,凭什么要人来做。

后来我试过用 Python 写脚本处理,确实快,但每次需求一变,就得改代码、调参数、重新跑。再后来我接触到 MCP 这个概念,才意识到问题的本质不是“用不用 Python”,而是“AI 能不能直接理解我的意图,并且安全地操作我的文件”。

MCP,全称 Model Context Protocol,翻译过来叫模型上下文协议。你可以把它理解成 AI 和外部工具之间的一套“通用插座标准”。以前 AI 想操作 Excel,要么靠人写死代码,要么靠插件,要么靠复制粘贴。MCP 出现之后,AI 可以通过标准协议调用你本地定义好的工具函数,你告诉它“把这份表按区域拆分”,它就能调用你写好的拆分函数,而不是瞎猜。

这篇文章要讲的,就是怎么从零开始,开发自己的第一个 MCP 服务,专门用来重构 Excel 处理工作流。适合谁看?如果你经常跟 Excel 打交道,会一点 Python,对 AI 工作流感兴趣,但还没真正动手写过 MCP,那这篇就是写给你的。我会把整个思路、代码、踩过的坑都摊开讲,你照着做就能跑起来。

1.2 MCP 到底解决了什么痛点

在没有 MCP 之前,AI 处理 Excel 的典型路径是这样的:你把文件内容粘贴到对话框,AI 给你一段 Python 代码,你再复制到本地跑。这个流程有三个致命问题。

第一,上下文丢失。AI 看不到你的完整文件结构,只能基于你粘贴的片段猜测。第二,执行断层。AI 给的代码不一定能跑,报错了你还得回来问它,来回折腾。第三,安全风险。你把公司数据粘贴到对话框,数据就出去了,这在很多场景下是不可接受的。

MCP 的思路完全不同。它把“工具”定义在本地,AI 只负责决定调用哪个工具、传什么参数,真正的执行发生在你的机器上。数据不出本地,AI 只拿到必要的元信息。这就像你请了一个顾问,顾问不直接碰你的账本,而是告诉你“去把第三页的合计改成这个数”,你自己动手改。

对于 Excel 处理来说,这个模式特别合适。因为 Excel 操作往往是确定性的:读取、筛选、汇总、写入、生成图表。这些操作完全可以封装成标准工具,让 AI 来编排调用顺序。你不需要让 AI 去理解每一个单元格,只需要让它知道“有哪些工具可用,每个工具干什么”。

1.3 我选择的技术栈和理由

动手之前,先定技术栈。我的选择是:Python + openpyxl + pandas + MCP SDK。

为什么用 Python?因为 Excel 处理的生态在 Python 里最成熟。openpyxl 负责读写 xlsx 文件,保留格式和公式;pandas 负责数据分析和转换;两者配合,覆盖 90% 以上的日常需求。

为什么不用 VBA?VBA 也能做,但它跟 AI 的集成太别扭。MCP 的 SDK 目前对 Python 和 TypeScript 支持最好,Python 的生态又更适合数据处理,所以 Python 是自然选择。

为什么用 MCP SDK 而不是自己造协议?因为 MCP 已经定义了标准的工具描述格式、调用格式、错误处理格式。你自己造一套,AI 那边还得适配,没必要。用官方 SDK,工具定义一次,任何支持 MCP 的客户端都能调用。

这里插一句,MCP 协议本身是软件协议,不是硬件协议。硬件里类似的概念叫“总线标准”,比如 USB。MCP 就是 AI 工具领域的 USB,插上就能用,不用管对面是什么设备。

2. 环境准备与项目骨架搭建

2.1 Python 环境配置的避坑指南

第一步装 Python。如果你还没装,去官网下载 3.10 以上的版本。为什么强调 3.10?因为 MCP SDK 用到了不少新语法特性,3.9 以下会报错。安装的时候记得勾选“Add Python to PATH”,这个选项不勾,后面命令行里敲 python 会找不到。

装完之后验证一下:

python --version pip --version

两个命令都能输出版本号,说明环境没问题。如果 pip 版本太老,先升级:

python -m pip install --upgrade pip

接下来建虚拟环境。这一步很多人偷懒跳过,但我强烈建议你做。虚拟环境的好处是依赖隔离,你这个项目装的包不会污染全局环境,以后删项目直接删文件夹就行。

python -m venv mcp-excel-env

Windows 下激活:

mcp-excel-env\Scripts\activate

Mac 或 Linux 下激活:

source mcp-excel-env/bin/activate

激活之后,命令行前面会出现(mcp-excel-env)的标识。这时候再装依赖:

pip install mcp openpyxl pandas

注意:如果你公司网络有代理限制,pip 装包可能会超时。可以换国内镜像源,加-i参数指定。这个属于常规操作,不展开。

2.2 项目目录结构设计

目录结构不用复杂,但要有条理。我习惯这样组织:

mcp-excel/ ├── server.py # MCP 服务主入口 ├── tools/ │ ├── __init__.py │ ├── reader.py # 读取相关工具 │ ├── writer.py # 写入相关工具 │ └── analyzer.py # 分析相关工具 ├── utils/ │ ├── __init__.py │ └── helpers.py # 通用辅助函数 ├── data/ # 存放待处理的 Excel 文件 └── requirements.txt

为什么要把工具拆成多个文件?因为一个 MCP 服务可能会暴露十几个工具,全写在一个文件里,后期维护很痛苦。按功能拆分,读的归读,写的归写,分析的归分析,找起来方便。

data目录用来放测试文件。我建议你准备一个样例 Excel,包含几个 sheet,有文本列、数字列、日期列,这样测试的时候能覆盖各种类型。

2.3 MCP 服务的最小可运行骨架

先写一个最简单的 server.py,让服务能跑起来,再逐步加工具。最小骨架长这样:

from mcp.server import Server from mcp.server.stdio import stdio_server app = Server("excel-mcp") @app.list_tools() async def list_tools(): return [] @app.call_tool() async def call_tool(name: str, arguments: dict): return {"error": f"未知工具: {name}"} 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())

这段代码定义了 MCP 服务的两个核心接口:list_tools告诉客户端有哪些工具可用,call_tool负责执行具体工具。现在工具列表是空的,服务能启动但干不了事。

你可以先跑一下,确认没报错。跑起来之后,服务会等待客户端连接。如果你用的是支持 MCP 的客户端,配置一下就能看到这个服务,只是里面没有工具。

实操心得:MCP 服务默认走 stdio(标准输入输出)通信,这意味着它不能同时处理多个客户端。如果你需要多客户端并发,得换成 SSE 或 WebSocket 传输。对于个人使用,stdio 足够了。

3. 核心工具的设计与实现

3.1 读取 Excel:让 AI 先看懂文件结构

AI 要处理 Excel,第一步是知道文件里有什么。所以第一个工具是“读取文件概览”,返回 sheet 列表、每个 sheet 的行列数、列名、前几行样例数据。

import openpyxl from mcp.types import Tool, TextContent @app.list_tools() async def list_tools(): return [ Tool( name="read_excel_overview", description="读取Excel文件的整体结构,返回sheet列表、每个sheet的列名和前5行数据", inputSchema={ "type": "object", "properties": { "file_path": { "type": "string", "description": "Excel文件的绝对路径" } }, "required": ["file_path"] } ) ]

工具描述很重要。AI 是根据 description 来决定什么时候调用这个工具的。描述要写清楚“这个工具干什么、返回什么、什么时候用”。inputSchema 用 JSON Schema 格式定义参数,AI 会根据这个生成调用参数。

实现部分:

@app.call_tool() async def call_tool(name: str, arguments: dict): if name == "read_excel_overview": file_path = arguments["file_path"] wb = openpyxl.load_workbook(file_path, read_only=True, data_only=True) result = {} for sheet_name in wb.sheetnames: ws = wb[sheet_name] rows = [] for i, row in enumerate(ws.iter_rows(values_only=True)): if i >= 5: break rows.append(list(row)) result[sheet_name] = { "max_row": ws.max_row, "max_column": ws.max_column, "sample": rows } wb.close() return [TextContent(type="text", text=str(result))]

这里有几个细节值得说。read_only=True让 openpyxl 用只读模式打开,大文件时内存占用低很多。data_only=True让公式单元格返回计算结果而不是公式本身,AI 看结果更有意义。

返回格式用TextContent,内容是字符串。为什么不用 JSON 对象直接返回?因为 MCP 的返回类型目前以文本为主,AI 拿到文本后自己解析。你把字典转成字符串,AI 也能理解。

注意事项:文件路径一定要用绝对路径。相对路径在不同客户端下工作目录不一样,很容易找不到文件。我踩过这个坑,调试了半天才发现是路径问题。

3.2 数据筛选与汇总:把重复劳动交给工具

读取概览之后,AI 知道了数据结构,接下来就是具体操作。最常见的需求是“按某列筛选”和“按某列汇总”。

筛选工具的设计:

Tool( name="filter_excel", description="按指定列的值筛选Excel数据,返回符合条件的行", inputSchema={ "type": "object", "properties": { "file_path": {"type": "string"}, "sheet_name": {"type": "string"}, "column": {"type": "string", "description": "用于筛选的列名"}, "value": {"type": "string", "description": "筛选值,支持包含匹配"}, "match_type": { "type": "string", "enum": ["exact", "contains"], "default": "contains" } }, "required": ["file_path", "sheet_name", "column", "value"] } )

match_type参数区分精确匹配和包含匹配。为什么要有这个?因为实际场景里,用户说“筛选华东区”,可能是精确匹配“华东区”,也可能是包含“华东”的所有行。让 AI 根据语境选择,比写死一种更灵活。

实现用 pandas 更省事:

import pandas as pd def filter_excel(file_path, sheet_name, column, value, match_type="contains"): df = pd.read_excel(file_path, sheet_name=sheet_name) if match_type == "exact": filtered = df[df[column] == value] else: filtered = df[df[column].astype(str).str.contains(value, na=False)] return filtered.to_dict(orient="records")

astype(str)是为了防止列里有数字或日期,直接调str.contains会报错。na=False让空值不参与匹配,避免 NaN 引发的异常。

汇总工具类似,核心是groupby:

def summarize_excel(file_path, sheet_name, group_column, value_column, agg_func="sum"): df = pd.read_excel(file_path, sheet_name=sheet_name) result = df.groupby(group_column)[value_column].agg(agg_func) return result.reset_index().to_dict(orient="records")

agg_func支持 sum、mean、count、max、min。AI 根据用户意图选择。比如“统计每个区域的销售额”用 sum,“统计每个区域的订单数”用 count。

3.3 写入与生成新文件:让结果落地

处理完数据,总得输出。写入工具负责把结果写回 Excel,或者生成新文件。

Tool( name="write_excel", description="将数据写入Excel文件,支持新建文件或追加到现有sheet", inputSchema={ "type": "object", "properties": { "file_path": {"type": "string"}, "sheet_name": {"type": "string"}, "data": { "type": "array", "description": "二维数组,第一行为表头", "items": {"type": "array"} }, "mode": { "type": "string", "enum": ["new", "append"], "default": "new" } }, "required": ["file_path", "sheet_name", "data"] } )

实现:

def write_excel(file_path, sheet_name, data, mode="new"): df = pd.DataFrame(data[1:], columns=data[0]) if mode == "append" and os.path.exists(file_path): with pd.ExcelWriter(file_path, mode="a", if_sheet_exists="overlay") as writer: df.to_excel(writer, sheet_name=sheet_name, index=False) else: df.to_excel(file_path, sheet_name=sheet_name, index=False) return f"已写入 {len(df)} 行数据到 {sheet_name}"

mode="a"是追加模式,if_sheet_exists="overlay"表示如果 sheet 已存在就覆盖写入。这两个参数是 pandas 1.4 以上才支持的,版本低了会报错。

实操心得:写入之前一定要检查目标文件是否被 Excel 打开。如果文件正在被 Excel 占用,写入会失败,报 PermissionError。我建议在工具里加个 try-except,捕获这个异常并返回友好提示,让 AI 告诉用户“请先关闭文件”。

3.4 工具描述怎么写 AI 才听得懂

工具描述是 MCP 的灵魂。写得好,AI 调用准确率 90% 以上;写得差,AI 要么不调用,要么乱传参数。

我的经验是遵循三个原则。

第一,描述里说清楚“什么时候用”。比如“当用户需要查看 Excel 文件结构时使用”,而不是干巴巴的“读取 Excel”。AI 是根据场景匹配工具的,场景描述越具体,匹配越准。

第二,参数描述要带示例。比如column参数,描述写成“用于筛选的列名,例如‘区域’、‘销售额’”,AI 就知道该传什么。

第三,枚举值要列全。match_type的 enum 里写清楚 exact 和 contains,AI 就不会传别的值。

还有一个技巧:工具名用动词开头,read_、filter_、write_、summarize_,AI 一看就知道是干什么的。别用excel_tool_1这种名字,AI 理解不了。

4. 完整实操流程与调试记录

4.1 从零跑通第一个 MCP 调用

代码写完了,怎么验证?我分三步走。

第一步,本地单元测试。不经过 MCP,直接调函数,确认逻辑正确。

# test_local.py from tools.reader import read_excel_overview result = read_excel_overview("data/sample.xlsx") print(result)

跑一下,看输出是否符合预期。这一步能排除 80% 的代码 bug。

第二步,启动 MCP 服务,用客户端连接。我用的是支持 MCP 的编辑器,配置里加上:

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

配置完重启客户端,在对话里问“帮我看看 data/sample.xlsx 里有什么”,AI 应该会调用read_excel_overview,返回文件结构。

第三步,逐步测试复杂场景。先测筛选,再测汇总,最后测写入。每测一个,确认结果正确再往下。

注意事项:MCP 服务启动失败时,客户端往往只显示“连接失败”,不显示具体错误。这时候去客户端的日志目录找日志文件,里面会有 Python 的报错堆栈。我一开始没看日志,瞎猜了半天,后来发现是某个依赖没装。

4.2 参数传递中的类型陷阱

调试过程中我遇到一个典型问题:AI 传参数时,数字被传成了字符串。

比如value参数,用户说“筛选销售额大于 1000 的行”,AI 可能传"1000"而不是1000。如果你的代码直接拿它做数值比较,就会报 TypeError。

解决办法有两个。一是在 inputSchema 里明确类型为 number,AI 会尽量遵守。二是在代码里做类型转换和容错:

def safe_number(value): try: return float(value) except (ValueError, TypeError): return None

对于筛选工具,如果列是数值类型,先把 value 转成数字再比较。如果转不了,就按字符串处理。这样无论 AI 传什么,都不会崩。

另一个陷阱是日期。Excel 里的日期在 pandas 里可能是 Timestamp 对象,转成字符串格式五花八门。我的做法是统一转成YYYY-MM-DD格式再返回,AI 看起来清晰,用户也容易理解。

4.3 处理大文件时的性能优化

三千行的文件,pandas 秒开。但如果是十万行呢?我实测过,pd.read_excel读十万行 xlsx 大概要 10 到 20 秒,取决于机器性能。这个延迟在对话场景里很明显,用户会以为卡死了。

优化思路有三个。

第一,读取时指定usecols,只读需要的列。如果用户只关心三列,没必要把二十列全读进来。

第二,用dtype参数指定列类型。pandas 默认会推断类型,推断过程很耗时。如果你知道某列是字符串,直接指定dtype={"编号": str},能省不少时间。

第三,对于超大文件,考虑转成 CSV 或 Parquet 再处理。xlsx 本质是个压缩包,解析开销大。CSV 读取快得多。你可以在工具里加个判断,文件超过一定行数就提示用户转换格式。

def read_with_optimization(file_path, sheet_name, usecols=None): df = pd.read_excel( file_path, sheet_name=sheet_name, usecols=usecols, engine="openpyxl" ) return df

engine="openpyxl"明确指定引擎,避免 pandas 自动选择时的开销。

4.4 错误处理与用户友好提示

MCP 工具报错时,如果直接抛异常,AI 拿到的是一堆堆栈信息,它不知道怎么跟用户解释。更好的做法是捕获异常,返回结构化的错误信息。

@app.call_tool() async def call_tool(name: str, arguments: dict): try: if name == "read_excel_overview": return await handle_read_overview(arguments) elif name == "filter_excel": return await handle_filter(arguments) else: return [TextContent(type="text", text=f"错误:未知工具 {name}")] except FileNotFoundError: return [TextContent(type="text", text="错误:文件不存在,请检查路径是否正确")] except PermissionError: return [TextContent(type="text", text="错误:文件被占用,请先关闭Excel再试")] except Exception as e: return [TextContent(type="text", text=f"错误:{str(e)}")]

这样 AI 拿到的是“文件不存在”这样的自然语言,它可以直接转述给用户,体验好很多。

实操心得:错误信息里不要暴露文件系统的绝对路径。虽然本地使用无所谓,但养成好习惯,万一以后要分享日志,不会泄露敏感信息。

5. 常见问题排查与避坑清单

5.1 服务连不上怎么办

这是新手最常见的问题。症状是客户端显示 MCP 服务离线,或者调用工具没反应。

排查顺序如下。

先确认 Python 路径。客户端配置里的command如果是python,可能指向了系统 Python 而不是虚拟环境里的。建议写绝对路径,比如/Users/xxx/mcp-excel-env/bin/python。

再确认脚本路径。args里的 server.py 路径必须是绝对路径。相对路径在不同工作目录下解析结果不同。

然后手动跑一遍。在命令行里执行python /path/to/server.py,看有没有报错。如果命令行能跑,客户端连不上,那就是配置问题。如果命令行也报错,先解决代码问题。

最后看日志。不同客户端日志位置不同,一般在设置里能找到“查看日志”的入口。日志里会有详细的连接过程和错误信息。

5.2 工具调用返回空结果

AI 调用了工具,但返回是空的。可能原因有三个。

一是文件路径不对。AI 可能传了相对路径,或者路径里有中文没转义。在工具里加个os.path.abspath转换,能解决大部分路径问题。

二是 sheet 名不对。Excel 的 sheet 名区分大小写,AI 可能传了“Sheet1”而实际是“sheet1”。在工具里做大小写不敏感匹配:

def find_sheet(wb, name): for s in wb.sheetnames: if s.lower() == name.lower(): return s raise ValueError(f"找不到sheet: {name}")

三是筛选条件太严格。比如用户说“筛选华东”,但数据里是“华东区”,精确匹配就找不到。默认用包含匹配,能减少这类问题。

5.3 写入后格式丢失

用 pandas 写入 Excel,原有的格式(字体、颜色、边框)会丢失。因为 pandas 只写数据,不管样式。

如果你需要保留格式,得用 openpyxl 直接操作:

wb = openpyxl.load_workbook(file_path) ws = wb[sheet_name] for row in data: ws.append(row) wb.save(file_path)

openpyxl 的append会保留已有单元格的格式,只在新行写入数据。但它的速度比 pandas 慢,数据量大时要有心理准备。

注意事项:openpyxl 保存时如果文件被占用,同样会报 PermissionError。而且 openpyxl 保存是整体重写,文件越大越慢。十万行以上的文件,建议用 pandas 处理数据,用 openpyxl 只做最后的格式调整。

5.4 常见问题速查表

问题现象可能原因解决方法
服务连不上Python 路径错误配置里用虚拟环境的绝对路径
服务连不上脚本路径错误args 里用绝对路径
调用无返回文件路径不对工具内做 abspath 转换
调用无返回sheet 名大小写不匹配做大小写不敏感匹配
筛选结果为空匹配模式太严格默认用 contains 匹配
写入失败文件被 Excel 占用提示用户关闭文件
写入后格式丢失pandas 不保留样式改用 openpyxl 写入
大文件读取慢xlsx 解析开销大指定 usecols 和 dtype
数字被当字符串AI 传参类型不对代码里做类型转换容错
日期格式混乱Timestamp 转字符串统一格式化为 YYYY-MM-DD

这张表是我调试过程中实际遇到的问题汇总。你遇到新问题,可以先对照这张表排查,大部分情况都能覆盖。

5.5 几个让我少走弯路的经验

第一个经验:先跑通最小闭环,再加功能。我一开始想一口气把读取、筛选、汇总、写入、图表全写完,结果调试时不知道是哪部分出的问题。后来改成先写读取,跑通,再加筛选,再加汇总,每步都验证,效率反而高。

第二个经验:工具粒度要适中。太粗,AI 不好组合;太细,AI 调用次数多,慢。我的标准是“一个工具对应一个明确的用户意图”。比如“筛选”是一个意图,“汇总”是一个意图,“筛选后汇总”就是两个工具的组合,不单独做一个工具。

第三个经验:给 AI 留容错空间。AI 不是万能的,它可能传错参数。你的工具要能处理“参数不太对但意图明确”的情况。比如用户说“按区域汇总销售额”,AI 可能把group_column传成“区域”,也可能传成“地区”。你可以在工具里做列名模糊匹配,找到最接近的列。

第四个经验:日志要打全。每个工具调用进来,先打一行日志,记录工具名和参数。出问题时,看日志就知道 AI 传了什么,省去很多猜测。

6. 这套工作流还能怎么扩展

6.1 接入更多数据源

现在只处理 Excel,但实际工作里数据来源很多。你可以用同样的模式,加几个工具处理 CSV、JSON、甚至数据库查询。MCP 的好处是工具定义标准化,加新工具不影响已有的。

比如加一个read_csv工具,实现跟read_excel_overview类似,只是用pd.read_csv。AI 会根据文件扩展名自动选择工具。

6.2 生成图表与报告

Excel 处理完,往往要出图。你可以加一个create_chart工具,用 openpyxl 的图表功能,或者用 matplotlib 生成图片再插入。

from openpyxl.chart import BarChart, Reference def create_bar_chart(file_path, sheet_name, data_range, chart_title): wb = openpyxl.load_workbook(file_path) ws = wb[sheet_name] chart = BarChart() chart.title = chart_title data = Reference(ws, range_string=data_range) chart.add_data(data) ws.add_chart(chart, "E2") wb.save(file_path)

这样 AI 就能根据用户需求,自动生成柱状图、折线图、饼图。

6.3 与工作流引擎结合

如果你用过 Coze 工作流、Camunda 这类工具,会发现 MCP 和它们是互补的。工作流引擎负责编排多个步骤,MCP 负责提供具体的工具能力。你可以把 MCP 服务当成工作流里的一个节点,让工作流调用 MCP 工具处理 Excel,处理完再传给下一个节点。

这种组合特别适合批量处理场景。比如每天定时处理一批报表,工作流负责调度,MCP 负责执行。

6.4 我个人的一些体会

写这个 MCP 服务最大的收获,不是学会了某个 API,而是理解了“AI 工具化”的思路。以前总觉得 AI 要无所不能,现在明白,AI 最擅长的是理解和编排,具体执行交给确定性工具,两者配合才是最优解。

Excel 处理只是一个小场景,但这个模式可以复制到很多地方。文件管理、数据清洗、格式转换、甚至自动化测试,都可以用 MCP 封装成工具,让 AI 来调度。

最后分享一个小技巧:工具写完之后,别急着接 AI,先自己手动调用几遍,把各种边界情况都试一遍。空文件、超大文件、格式错误的文件、列名有特殊字符的文件,这些情况 AI 不一定能处理好,但你的工具要能兜住。工具稳了,AI 的表现才稳。

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

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

立即咨询