1. 为什么我要自己动手写一个 MCP
1.1 从一次崩溃的 Excel 处理经历说起
上个月帮朋友处理一批销售数据,二十多个 Excel 文件,每个文件里都有七八张表,需要按区域拆分、按品类汇总、再生成一份带图表的月报。我一开始想的是老办法:写个 Python 脚本,用 pandas 读进来,处理完再写回去。脚本写到一半我就烦了——每换一个需求,比如“把华东区的数据单独拎出来”“按月份做个透视”,我就得改代码、重跑、再检查。更麻烦的是,朋友自己不会 Python,我每次改完还得截图告诉他结果对不对。
那天晚上我盯着屏幕想,如果有一个东西能让我用自然语言直接指挥它去操作 Excel,比如我说“把 Sheet2 里销售额大于 5000 的行标黄”,它就能自己写代码、自己执行、自己把结果给我看,那该多省事。这个念头一冒出来,我就想到了 MCP。
MCP 全称是 Model Context Protocol,你可以把它理解成一套“让 AI 模型和外部工具对话的通用插头”。以前我们想让 AI 操作 Excel,要么把数据贴进对话框让它分析,要么自己写一堆胶水代码把 AI 的输出转成可执行的操作。MCP 出现之后,这件事变得标准化了:你写一个 MCP Server,把“读 Excel”“写 Excel”“筛选数据”这些能力暴露成工具,AI 就能像调用函数一样调用它们。
我决定拿 Excel 处理这个场景练手,原因有三个。第一,Excel 是几乎所有人都绕不开的工具,需求足够普遍;第二,Python 生态里有 openpyxl、pandas 这些成熟的库,底层能力不用从零造;第三,这个场景足够“痛”,痛到我愿意花一个周末把它做出来。
1.2 MCP 到底解决了什么问题
在没有 MCP 之前,AI 和 Excel 之间的交互大概有三种方式。第一种是“人肉中转”:你把 Excel 内容复制到聊天窗口,AI 给你一段分析或代码,你再手动执行。这种方式的问题是数据量一大就崩,而且 AI 看不到执行结果,没法自我纠错。第二种是“写死脚本”:你提前把可能用到的操作都写成函数,AI 只能在这些函数里选。这种方式的问题是灵活性差,遇到没预设的需求就抓瞎。第三种是“让 AI 直接生成代码然后执行”:听起来很美,但安全性和可控性都是问题,万一它生成一句删库的代码呢。
MCP 的思路不太一样。它把“工具的定义”和“工具的调用”分开了。你作为开发者,负责定义工具的名字、参数、返回值;AI 负责根据用户的自然语言,决定调用哪个工具、传什么参数。执行过程在你的 MCP Server 里完成,AI 只拿到结果。这样一来,既保留了灵活性,又保证了可控性——因为所有能执行的操作都是你提前定义好的。
拿 Excel 场景来说,我可以定义这么几个工具:read_sheet读取指定工作表的数据,write_cell写入某个单元格,filter_rows按条件筛选行,create_chart创建图表。AI 收到“把销售额超过 5000 的行标黄”这个指令后,会先调用filter_rows拿到符合条件的行号,再调用write_cell或者一个专门的highlight_rows工具去标黄。整个过程我都能在日志里看到,出了问题也能追溯。
1.3 这个项目适合谁来参考
如果你满足下面任意一条,这个项目就值得你花时间看看。第一,你日常跟 Excel 打交道很多,厌倦了重复的复制粘贴和手动公式;第二,你有一点点 Python 基础,能看懂函数和类,但不算精通;第三,你对 AI Agent 感兴趣,想找个具体的场景练手,而不是停留在概念层面;第四,你想了解 MCP 协议到底怎么落地,而不是只看官方文档里的抽象描述。
我整个项目做下来,代码量不算大,核心逻辑大概三百多行,加上测试和调试大概花了一个周末。你如果有 Python 基础,跟着走一遍应该能在一到两天内跑通。下面我把整个思路、关键细节和踩过的坑都拆开讲。
2. 整体设计与技术选型
2.1 为什么选 Python 而不是其他语言
MCP 官方提供了多种语言的 SDK,包括 Python、TypeScript、Java 等。我选 Python 的原因很直接:Excel 处理的生态在 Python 里最成熟。openpyxl 能精确控制单元格样式、公式、图表;pandas 擅长数据分析和转换;xlwings 还能跟 Excel 应用本身交互。如果用 TypeScript 写 MCP Server,底层还是得调 Python 脚本或者找 Node 版的 Excel 库,多一层转发就多一层麻烦。
另外,Python 的 MCP SDK 用起来很顺手。它基于 asyncio,定义工具就是写一个带装饰器的函数,参数用类型注解标清楚,SDK 会自动生成 JSON Schema 给 AI 看。我试过用 TypeScript 写一个类似的,类型定义要写两遍,一遍给 TypeScript 编译器,一遍给 MCP 的 schema,比较啰嗦。
还有一个实际考虑:调试方便。Python 可以随时在 REPL 里试一段代码,看看 openpyxl 的某个 API 到底返回什么。TypeScript 虽然也能调试,但编译步骤和类型检查有时候会拖慢探索节奏。对于这种“边写边试”的项目,Python 的反馈循环更短。
2.2 MCP Server 的两种通信方式怎么选
MCP Server 和 Client 之间的通信主要有两种方式:stdio 和 SSE。stdio 就是标准输入输出,Client 启动 Server 进程,通过管道发消息;SSE 是 Server-Sent Events,走 HTTP 长连接,Server 可以独立运行,多个 Client 都能连。
我一开始想用 SSE,因为听起来更“现代”,而且可以部署到远程服务器上。但实际试下来,stdio 更适合我这个场景。原因有几个:第一,Excel 文件通常在本机,Server 跑在本机直接读写文件最方便,走 SSE 还得考虑文件路径映射;第二,stdio 的调试更简单,我可以在终端里直接跑 Server,看它打印的日志;第三,安全性更好,stdio 不暴露网络端口,不用担心别人连上来乱调工具。
当然,如果你想让多个 AI 助手共享同一个 Excel 处理服务,或者想把 Server 部署到一台专门的机器上,那 SSE 更合适。我后来也试了一下 SSE 模式,把 Server 跑在本地的一个端口上,用 curl 发请求测试,也能跑通。但日常使用我还是切回了 stdio,因为启动更快,不用管端口占用。
2.3 工具粒度怎么把握
这是设计 MCP Server 时最纠结的地方:工具应该切多细?切得太细,AI 要调用很多次才能完成一个任务,效率低;切得太粗,一个工具做太多事,AI 传参数容易出错,而且不好复用。
我一开始设计了一个“万能工具”叫process_excel,参数包括操作类型、目标工作表、条件、动作等等。结果发现 AI 经常传错参数,比如把“筛选”和“排序”的参数搞混。后来我把它拆成了几个独立的工具:read_sheet、filter_rows、sort_rows、write_data、highlight_rows、create_chart。每个工具只做一件事,参数也简单明了。
拆完之后,AI 的调用准确率明显上升。比如“把销售额超过 5000 的行标黄”这个指令,AI 会先调filter_rows拿到行号列表,再调highlight_rows传入行号和颜色。虽然多了一次调用,但每一步都可验证,出错也容易定位。
我的经验是:工具粒度应该跟“用户可能提出的最小操作单元”对齐。如果用户经常说“筛选并排序”,那可以考虑提供一个组合工具;但如果用户有时候只筛选、有时候只排序,那就拆开。另外,工具的返回值要尽量结构化,比如返回行号列表而不是一段文字描述,这样 AI 更容易理解。
2.4 数据在工具之间怎么传递
MCP 工具之间不能直接共享内存,每次调用都是独立的。这意味着如果filter_rows返回了行号列表,highlight_rows需要重新读取 Excel 文件来定位这些行。这听起来有点低效,但实际影响不大,因为 Excel 文件通常不大,openpyxl 读一个几万行的表也就几百毫秒。
不过有一个坑要注意:如果 AI 连续调用多个工具,而中间 Excel 文件被修改了,后面的工具可能读到不一致的数据。我的做法是让每个工具都接受一个file_path参数,并且在工具内部重新加载文件。这样虽然多了一些 IO,但保证了每次操作都是基于最新状态。如果你追求性能,可以加一个缓存层,但记得在写操作后失效缓存。
另一个问题是行号的定义。openpyxl 的行号是从 1 开始的,而 pandas 的索引是从 0 开始的。我在工具内部统一用 1-based 行号,并且在文档字符串里写清楚。AI 看到文档后一般不会搞错,但偶尔还是会传 0-based 的行号,所以我在代码里加了一个校验:如果行号小于 1,就自动加 1 并打一条警告日志。
3. 核心细节与实操要点
3.1 环境准备与依赖安装
先把环境搭起来。我假设你已经装了 Python 3.10 或更高版本,因为 MCP SDK 用了一些较新的 asyncio 特性。如果你还没装 Python,去官网下载安装包,安装时记得勾选“Add Python to PATH”。装完之后在终端里跑python --version确认一下。
接下来创建一个虚拟环境,这是好习惯,避免污染全局包。命令如下:
python -m venv mcp-excel-env source mcp-excel-env/bin/activate # Windows 用 mcp-excel-env\Scripts\activate然后安装依赖。核心是三个包:mcp是 MCP SDK,openpyxl用来读写 Excel,pandas用来做数据处理。如果你还想生成图表,可以加一个matplotlib,但 openpyxl 自带的图表功能也够用。
pip install mcp openpyxl pandas安装完成后,可以跑一个简单的测试确认 openpyxl 能用:
import openpyxl wb = openpyxl.Workbook() ws = wb.active ws['A1'] = 'hello' wb.save('test.xlsx') print('ok')如果终端输出ok,说明环境没问题。这里有个小坑:有些系统上 openpyxl 的默认字体设置会导致中文显示异常,如果你处理的是中文 Excel,建议在保存时指定字体,或者用 pandas 的to_excel方法,它对中文的支持更好。
3.2 定义第一个 MCP 工具:读取工作表
MCP SDK 的核心是Server类和@server.tool()装饰器。下面是我写的第一个工具,用来读取指定工作表的数据:
from mcp.server import Server from mcp.server.stdio import stdio_server import openpyxl server = Server("excel-mcp") @server.tool() async def read_sheet(file_path: str, sheet_name: str, max_rows: int = 100) -> str: """读取 Excel 指定工作表的数据,返回 CSV 格式的文本。 Args: file_path: Excel 文件的绝对路径 sheet_name: 工作表名称 max_rows: 最多返回多少行,默认 100 """ wb = openpyxl.load_workbook(file_path, data_only=True) if sheet_name not in wb.sheetnames: return f"错误:工作表 {sheet_name} 不存在,可用的有 {wb.sheetnames}" ws = wb[sheet_name] rows = [] for i, row in enumerate(ws.iter_rows(values_only=True)): if i >= max_rows: break rows.append(','.join(str(cell) if cell is not None else '' for cell in row)) return '\n'.join(rows)这个工具看起来简单,但有几个细节值得说。第一,data_only=True让 openpyxl 返回公式的计算结果而不是公式本身,这对 AI 理解数据很重要。第二,我加了max_rows参数,防止 AI 一次性拉取太多数据导致上下文爆炸。第三,返回值用 CSV 格式而不是 JSON,因为 CSV 更紧凑,AI 解析起来也更容易。
文档字符串(docstring)非常关键。MCP SDK 会把 docstring 里的描述和参数说明转成 JSON Schema 给 AI 看。我试过不写 docstring,结果 AI 完全不知道这个工具是干嘛的,调用时经常传错参数。所以每个工具都要写清楚:这个工具做什么、每个参数是什么意思、返回值是什么格式。
3.3 筛选与排序工具的实现
筛选和排序是 Excel 处理里最高频的操作。我一开始想用一个工具搞定,后来还是拆开了。先看筛选:
@server.tool() async def filter_rows(file_path: str, sheet_name: str, column: str, operator: str, value: str) -> str: """按条件筛选行,返回符合条件的行号列表。 Args: file_path: Excel 文件路径 sheet_name: 工作表名称 column: 列名(第一行的值) operator: 比较运算符,支持 gt, lt, eq, contains value: 比较的值 """ wb = openpyxl.load_workbook(file_path, data_only=True) ws = wb[sheet_name] headers = [cell.value for cell in ws[1]] if column not in headers: return f"错误:列 {column} 不存在,可用的有 {headers}" col_idx = headers.index(column) + 1 matched = [] for row_idx in range(2, ws.max_row + 1): cell_val = ws.cell(row=row_idx, column=col_idx).value if cell_val is None: continue if operator == 'gt' and float(cell_val) > float(value): matched.append(row_idx) elif operator == 'lt' and float(cell_val) < float(value): matched.append(row_idx) elif operator == 'eq' and str(cell_val) == value: matched.append(row_idx) elif operator == 'contains' and value in str(cell_val): matched.append(row_idx) return f"匹配到 {len(matched)} 行:{matched}"这里有几个实操心得。第一,列名匹配我用的是精确匹配,但实际 Excel 里经常有空格或大小写差异,后来我改成了strip().lower()后再比较。第二,float(cell_val)在遇到非数字时会抛异常,我加了一个 try-except 包起来,遇到非数字就跳过。第三,返回行号列表而不是数据本身,这样 AI 可以决定下一步是标黄、删除还是提取。
排序工具类似,但要注意排序会改变行号,所以如果 AI 先筛选再排序,筛选得到的行号就失效了。我的做法是在排序工具的文档里明确写“排序后行号会变化,请重新筛选”,并且在代码里返回排序后的前几行数据,让 AI 知道新顺序。
3.4 写入与样式工具的实现
写入工具负责修改单元格的值,样式工具负责改颜色、字体、边框。这两个工具我分开写,因为写入是数据操作,样式是展示操作,混在一起容易乱。
@server.tool() async def write_cell(file_path: str, sheet_name: str, row: int, column: int, value: str) -> str: """写入单个单元格。 Args: file_path: Excel 文件路径 sheet_name: 工作表名称 row: 行号,从 1 开始 column: 列号,从 1 开始 value: 要写入的值 """ wb = openpyxl.load_workbook(file_path) ws = wb[sheet_name] ws.cell(row=row, column=column, value=value) wb.save(file_path) return f"已写入 {sheet_name} 的 ({row}, {column})"样式工具稍微复杂一点,因为 openpyxl 的样式 API 比较绕:
from openpyxl.styles import PatternFill @server.tool() async def highlight_rows(file_path: str, sheet_name: str, rows: list[int], color: str = "FFFF00") -> str: """将指定行标黄。 Args: file_path: Excel 文件路径 sheet_name: 工作表名称 rows: 行号列表 color: 十六进制颜色,默认黄色 """ wb = openpyxl.load_workbook(file_path) ws = wb[sheet_name] fill = PatternFill(start_color=color, end_color=color, fill_type="solid") for row_idx in rows: for col_idx in range(1, ws.max_column + 1): ws.cell(row=row_idx, column=col_idx).fill = fill wb.save(file_path) return f"已标黄 {len(rows)} 行"这里有个性能坑:如果行数很多,逐单元格设置样式会很慢。我试过标黄 1000 行,花了将近 10 秒。后来改成先收集所有单元格,再用ws.iter_rows批量设置,速度快了不少。另外,PatternFill的颜色参数是 ARGB 格式,前两位是透明度,FFFF00其实是“不透明的黄色”,如果你写FF0000会得到红色。
3.5 把 Server 跑起来
工具定义完之后,需要写一个入口函数来启动 Server:
async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())跑起来之后,Server 会等待 Client 发消息。如果你用的是 Claude Desktop 或者其他的 MCP Client,需要在配置文件里指定这个 Server 的启动命令。比如在 Claude Desktop 的配置里加一段:
{ "mcpServers": { "excel": { "command": "python", "args": ["/path/to/your/server.py"] } } }配置完之后重启 Client,它就会自动启动这个 Server。你可以在对话里说“读取 test.xlsx 的 Sheet1”,AI 就会调用read_sheet工具。如果没反应,先检查 Server 能不能在终端里独立跑起来,再看 Client 的日志。
4. 实操过程与核心环节
4.1 从零跑通第一个指令
环境搭好、代码写完之后,我做的第一件事是跑一个最简单的指令:“读取 test.xlsx 的 Sheet1 的前 5 行”。为了测试,我先用 openpyxl 造了一个测试文件:
import openpyxl wb = openpyxl.Workbook() ws = wb.active ws.title = "Sheet1" ws.append(["姓名", "部门", "销售额"]) ws.append(["张三", "华东", 5200]) ws.append(["李四", "华南", 3800]) ws.append(["王五", "华东", 6100]) ws.append(["赵六", "华北", 4500]) wb.save("test.xlsx")然后在 Client 里发指令。第一次跑的时候,AI 回复说“无法找到文件”,我检查了一下,发现是文件路径的问题——Client 的工作目录跟我终端的不一样。后来我把路径改成绝对路径,问题解决。这个坑很常见,建议在工具里加一个路径校验,如果文件不存在就返回明确的错误信息,而不是让 openpyxl 抛异常。
第二次跑,AI 成功调用了read_sheet,返回了 CSV 格式的数据。但我发现它把表头也当成了数据行,因为我的max_rows是从第一行开始数的。后来我改了一下,让read_sheet默认跳过第一行(表头),或者加一个include_header参数。这个细节虽小,但影响 AI 对数据的理解。
4.2 一个完整的处理流程:筛选、标黄、汇总
跑通读取之后,我试了一个完整的流程:“把销售额超过 5000 的行标黄,然后统计华东区的总销售额”。AI 的执行过程是这样的:
第一步,调用filter_rows,参数是column="销售额",operator="gt",value="5000"。返回[2, 4],因为张三的 5200 和王五的 6100 都超过 5000。
第二步,调用highlight_rows,参数是rows=[2, 4]。返回“已标黄 2 行”。
第三步,调用filter_rows,参数是column="部门",operator="eq",value="华东"。返回[2, 4]。
第四步,AI 需要计算这两个行的销售额之和。但我的工具里没有“求和”功能,所以 AI 只能再调用read_sheet拿到数据,然后自己算。这暴露了一个问题:工具集不够完整。后来我加了一个sum_column工具,接受行号列表和列名,直接返回求和结果。
这个流程跑下来,我最大的感受是:AI 的规划能力比我想象的强,但它依赖工具提供的“原子能力”。如果你只给了筛选和标黄,它就没法做求和;如果你给了求和但没给筛选,它就得先把所有数据拉出来自己算。所以工具集的设计要尽量覆盖常见的操作组合,但又不能太粗。
4.3 参数计算与选择:max_rows 设多少合适
max_rows这个参数我调了好几次。一开始设的是 1000,结果 AI 经常一次性拉取上千行数据,上下文直接被撑爆,后面的对话就乱了。后来我降到 100,发现大部分场景够用,因为 AI 通常只需要看前几行来理解数据结构,具体处理靠工具完成。
但有些场景确实需要看更多数据,比如“找出所有销售额为空的记录”。这时候 100 行不够,AI 会反复调用read_sheet并翻页。我的解决方案是加一个offset参数,让 AI 可以分页读取。同时,我在文档里建议 AI“先用 filter_rows 缩小范围,再读取具体行”。
另一个参数是color。我一开始只支持黄色,后来发现用户有时候想标红、标绿。改成十六进制颜色之后,AI 需要知道颜色代码,这增加了它的认知负担。折中方案是提供几个预设颜色名:yellow、red、green、blue,在代码里映射到对应的十六进制值。这样 AI 只需要传color="red",不用记颜色代码。
4.4 实操现场:一次失败的调试记录
有一次我改了一个工具的返回值格式,从“匹配到 3 行:[2, 4, 6]”改成了纯列表[2, 4, 6]。结果 AI 收到之后,不知道这个列表是什么意思,把它当成了数据本身,回复用户说“销售额是 2、4、6”。这个错误让我意识到:工具的返回值不仅要结构化,还要有自描述性。
后来我把返回值改成 JSON 格式:
{"matched_rows": [2, 4, 6], "count": 3, "message": "匹配到 3 行"}这样 AI 能清楚地知道每个字段的含义。虽然 JSON 比纯文本占更多 token,但准确性提升明显,值得。
还有一个调试技巧:在 Server 里加日志。MCP SDK 默认不打印工具调用的详细信息,我加了一个装饰器,在每次工具调用前后打印参数和返回值:
import functools def log_tool(func): @functools.wraps(func) async def wrapper(*args, **kwargs): print(f"[TOOL] {func.__name__} called with {kwargs}") result = await func(*args, **kwargs) print(f"[TOOL] {func.__name__} returned: {result[:200]}") return result return wrapper然后把它加到每个工具上。这样在终端里就能看到 AI 到底调了什么、传了什么参数、返回了什么。调试的时候非常有用。
5. 常见问题与排查技巧
5.1 工具调用失败排查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| AI 说找不到工具 | Server 没启动或配置错误 | 检查 Client 日志,看 Server 进程是否在跑 | 确认配置文件路径正确,手动在终端跑 Server |
| 工具调用返回空 | 文件路径不对或工作表名错误 | 在工具里打印实际路径和 sheetnames | 用绝对路径,加路径校验 |
| AI 传错参数类型 | docstring 描述不清 | 看日志里 AI 传的参数 | 在 docstring 里写明类型和示例 |
| 写入后文件损坏 | 多个工具同时写同一个文件 | 检查是否有并发调用 | 加文件锁,或者串行化写操作 |
| 中文乱码 | 编码问题 | 用 pandas 读取看是否正常 | 指定 encoding='utf-8',或用 pandas 替代 openpyxl |
| 样式不生效 | 颜色格式错误 | 检查 PatternFill 参数 | 用 ARGB 格式,前两位是透明度 |
| 行号对不上 | 0-based 和 1-based 混用 | 在工具里打印实际行号 | 统一用 1-based,加校验 |
这个表是我踩坑之后整理的,基本上覆盖了八成的问题。其中“行号对不上”是最常见的,因为 pandas 和 openpyxl 的行号规则不一样,AI 有时候会搞混。我的做法是在所有涉及行号的工具里,第一行代码就是校验:如果行号小于 1,自动加 1 并打日志。
5.2 性能优化的几个实用技巧
Excel 文件大了之后,openpyxl 的读写会变慢。我试过处理一个 5 万行的文件,读取花了 3 秒,写入花了 5 秒。对于交互式场景,这个延迟有点难受。后来我做了几个优化:
第一,用read_only=True模式读取。openpyxl 默认会把整个文件加载到内存,read_only模式是流式读取,内存占用小,速度快。但缺点是只能顺序读,不能随机访问。对于read_sheet这种顺序读取的场景,read_only很合适。
第二,写入时用write_only=True模式。这个模式创建一个新的工作簿,只能追加行,不能修改已有单元格。对于“生成新文件”的场景很快,但对于“修改现有文件”就不适用。
第三,批量操作代替逐个操作。比如标黄 1000 行,逐个设置样式很慢,改成先收集所有单元格对象,再统一设置。openpyxl 的iter_rows返回的是生成器,可以一次性拿到所有行。
第四,缓存工作簿对象。如果 AI 连续调用多个工具操作同一个文件,每次重新加载很浪费。我加了一个简单的缓存:用文件路径和修改时间做 key,缓存工作簿对象。写操作后清除缓存。这样连续读操作就快了。
5.3 安全性与边界处理
MCP Server 跑在本机,理论上只能操作本机文件,但还是要加一些边界检查。第一,限制可访问的目录。我在工具里加了一个ALLOWED_DIR环境变量,如果文件路径不在这个目录下,直接拒绝。第二,限制文件大小。超过 50MB 的 Excel 文件直接返回错误,避免内存爆炸。第三,限制操作行数。highlight_rows最多标黄 10000 行,超过就拒绝,防止 AI 误操作把整个表标黄。
还有一个容易被忽略的点:公式注入。如果 AI 写入的值以=开头,openpyxl 会把它当成公式。这本身没问题,但如果 AI 被诱导写入恶意公式,可能会有风险。我的做法是在write_cell里加一个检查:如果值以=开头,在前面加一个单引号,强制当成文本。如果确实需要写公式,提供一个单独的write_formula工具。
5.4 独家避坑心得
第一个心得:不要相信 AI 会记住上下文。我试过在一个对话里先让 AI 读取 Sheet1,然后说“把刚才那些行标黄”。结果 AI 忘了“刚才那些行”是哪些,因为它没有保存行号。后来我让 AI 在需要跨步骤操作时,先把中间结果写到一个临时文件或者返回给用户确认。这样虽然多了一步,但避免了“AI 失忆”的问题。
第二个心得:工具的命名要一致。我一开始有的工具叫read_sheet,有的叫get_data,有的叫fetch_rows。AI 经常搞混,调用时传错工具名。后来我统一成“动词_名词”的格式:read_sheet、write_cell、filter_rows、sort_rows、highlight_rows、create_chart。命名一致之后,AI 的调用准确率明显提升。
第三个心得:给 AI 留“退路”。有些操作可能失败,比如筛选时列名不存在。如果工具直接抛异常,AI 会收到一个错误堆栈,它不知道怎么处理。我的做法是返回结构化的错误信息,比如{"error": "column_not_found", "available_columns": ["姓名", "部门", "销售额"]}。这样 AI 可以自己纠正,比如换一个列名重试。
第四个心得:测试用例要覆盖“坏输入”。我写了一个测试脚本,故意传空文件路径、不存在的 sheet 名、非数字的筛选值,看工具怎么反应。结果发现好几个工具直接崩了。后来我加了 try-except,把所有异常转成友好的错误信息。这个习惯在 MCP 开发里特别重要,因为 AI 不会像人一样“小心操作”,它可能会传各种奇怪的参数。
6. 后续可以怎么扩展
6.1 加入图表生成能力
openpyxl 支持创建柱状图、折线图、饼图。我后来加了一个create_chart工具,接受数据范围、图表类型、存放位置,生成图表并插入到指定工作表。实现的时候要注意:图表的引用范围要用Reference对象,而且数据区域和类别区域要分开指定。这个工具让 AI 可以完成“生成月报”这种端到端的任务。
6.2 支持多文件批量处理
现在的工具都是针对单个文件的。如果用户说“把这三个文件都处理一下”,AI 需要循环调用。我后来加了一个batch_process工具,接受文件列表和一个操作描述,内部循环执行。但这样又回到了“工具太粗”的问题。折中方案是提供一个list_files工具,让 AI 先拿到文件列表,再逐个调用单文件工具。这样既灵活又可控。
6.3 接入更多数据源
Excel 只是起点。同样的 MCP 架构可以扩展到 CSV、JSON、数据库。我后来把read_sheet抽象成了一个read_data工具,根据文件扩展名自动选择解析器。这样 AI 不需要知道底层是 Excel 还是 CSV,只需要说“读取这个文件的数据”。这个抽象让工具集更通用,但也增加了实现的复杂度,需要权衡。
6.4 用 AI 自动生成工具描述
工具的 docstring 是给 AI 看的,写得好不好直接影响调用准确率。我试过让 AI 自己根据函数签名生成 docstring,效果还不错。具体做法是:写一个脚本,读取所有工具的签名,调用 AI 生成描述,然后人工审核。这样比手写快,而且 AI 更了解“AI 需要什么信息”。不过要注意,生成的描述要经过人工检查,避免出现误导性的内容。
6.5 把 Server 打包成可执行文件
如果你想让不懂 Python 的人也能用,可以用 PyInstaller 把 Server 打包成单个可执行文件。这样用户不需要装 Python 和依赖,直接运行 exe 就行。打包的时候要注意把 openpyxl 的数据文件也包含进去,否则会报“找不到样式表”的错误。我试过打包,生成的 exe 大概 15MB,启动速度比 Python 脚本慢一点,但可以接受。
整个项目做下来,我最大的体会是:MCP 不是一个“高大上”的东西,它就是一个让 AI 和外部工具对话的协议。你不需要理解协议的所有细节,只需要把工具定义清楚、把参数校验做好、把错误处理完善,就能做出一个可用的 MCP Server。Excel 处理只是一个切入点,同样的思路可以用在文件管理、数据库查询、API 调用等任何场景。如果你也想动手做一个,建议从最小的工具开始,跑通之后再逐步扩展。遇到问题先看日志,再查文档,最后才改代码——这个顺序能帮你省很多时间。