
如果你路过我的工位大概率会看到我一边开着 Excel 一边跟 AI 对话框说话。别误会我不是在摸鱼——这是我折腾了两个月的成果一个自己写的 MCP Server用来让 AI 直接读写 Excel 文件。以前处理表格靠的是手动复制粘贴配 vlookup后来改成临时写 Python 脚本再后来我发现真正靠谱的方案不是让 AI 帮我写脚本而是给 AI 一双手让它直接操作文件。这个思路就是大家常说的用 AI 重构 Excel 处理工作流。这篇文章把从零开发第一个 Excel MCP Server 的完整过程写出来包括三个核心工具的实现、客户端配置、安全边界以及让我崩溃过的几个坑。适合有一点 Python 基础、正在琢磨怎么给工作提效的读者代码可以直接抄。1. 为什么我决定自己写一个 MCP从手动搓 Excel 到让 AI 替我干活1.1 每天和 Excel 死磕的日子我受够了我手里有一堆每月都要更新的报表销售明细、库存台账、客户对账单、渠道汇总。以前的工作方式是这样的先打开 Excel手动筛选复制到新表写 vlookup 匹配客户名称再拉透视表最后调格式。简单吧架不住每月都来一遍。而且只要数据源格式稍微改一下整套操作就得重新来过。也试过让 AI 给我写处理脚本。但 AI 生成脚本有个致命问题——它看不到实际文件只能靠我描述的表结构去猜。你告诉它“客户名称在 B 列金额在 E 列”它写出来的代码确实没错可一旦某个文件里多了一行合并单元格表头脚本当场崩掉。然后你又得把报错贴回去让它改来回几个回合人已经麻了。后来我意识到问题的本质不是“让 AI 写代码”而是“让 AI 直接面对文件本身”。它如果能自己打开 Excel、自己看结构、自己调数据那它就能像人一样处理这些琐碎活儿。这正是 MCP 的用武之地。1.2 MCP 到底是什么先忘记“协议”这个词Model Context Protocol模型上下文协议。翻译成大白话就是AI 应用和外部工具之间的一套统一通信标准。你可以把它理解成给 AI 装上 USB-C 接口。以前你买个键盘要一个接口买个硬盘又要另一个接口线材乱成一团MCP 统一之后任何 AI 客户端想调用任何外部能力只要对方实现了 MCP 协议就行。对 AI 来说MCP Server 就是一个“外挂工具箱”它通过标准方式把工具暴露给 AIAI 在需要的时候按名称调用这些工具工具执行完把结果返回给 AI。这套协议包含三个核心概念工具Tools、资源Resources和提示Prompts。做 Excel 处理的主角是工具——也就是我们可以注册给 AI 调用的函数。资源用于提供上下文文件提示用于预置模板后面有需要再研究也不迟第一版先把工具做扎实。1.3 为什么现成的 MCP Server 不够用网上开源的 Excel MCP Server 已经不少GitHub 一搜一大把。但我在实际用了一圈之后还是决定自己写原因有三。第一现成方案大多只覆盖通用的读写。我的表格有固定的模板格式、固定表头还有不少历史遗留的合并单元格和复杂表头通用工具读出来是一堆乱麻AI 根本没法理解。第二安全边界不匹配。有些现成 Server 给了挺宽泛的操作权限我不放心让它在我的财务目录里乱逛。第三也是最实际的——自己写一遍才能真正理解工具设计思路。后面每加一个新工具、改一个参数你都会知道它对 AI 行为有什么影响这种掌控感是拿别人封装好的包换不来的。所以这篇文章里我会沿着自己从零做一遍的路线把我踩过的坑和最终沉淀下来的工具设计方式完整讲一遍。2. 动手前必懂的三件事协议骨架、工具注册与参数设计2.1 MCP 的三种能力和一个“USB-C 类比”前面说了 MCP 有三个核心能力。我再展开讲一下它们的分工。ToolsAI 可以主动调用的函数。一个工具对应一段能力比如“读取 Excel”“写入 Excel”。Resources暴露给 AI 引用的文件、数据库记录或代码片段相当于给它提供“背景资料”。Prompts预定义好的提示词模板方便把常用操作流固化下来。对 Excel 场景来说最核心的是 Tools。你可以把 Tools 想成给 AI 装上的手指读文件是“摸”写文件是“握”聚合计算是“捏”。手指不需要多但每一根都要灵活、可控、边界清晰。关于通信机制我快速说一下客户端AI 应用和 Server 之间有两种常见传输方式本地用 stdio标准输入输出远程用 HTTP 或 WebSocket。本地开发用 stdio 就够了配置简单调试方便如果想把 Server 部署到内网共享给团队用再考虑远程方式但那会引入鉴权和网络安全的复杂度。2.2 FastMCP 最小骨架十行代码跑起来我这里用的是 FastMCP它是 MCP 官方 SDK 的简化封装。选它的理由很简单装饰器语法非常直观注册一个工具就是给普通函数加一个mcp.tool()不需要手写 JSON-RPC 的那套通信细节。一个最小的 Server 是这样的from fastmcp import FastMCP mcp FastMCP(ExcelAssistant) mcp.tool() def say_hello(name: str) - str: 问候对方便于测试工具调用链路是否通。 return f你好{name} if __name__ __main__: mcp.run()跑起来之后配置了 MCP 的 AI 客户端就能发现自己多了个say_hello工具。第一次看到自己写的函数出现在 AI 的工具列表里那种感觉还挺奇妙——你已经不是在写脚本而是在给一个会思考的实体“装备技能”。2.3 工具描述是 AI 的“说明书”比代码本身更重要这里要划重点FastMCP 会自动把你的函数名、docstring、参数类型和注释转换成工具的 JSON-Schema 描述。AI 在决定“什么时候调用这个工具、怎么传参数”时靠的就是这些描述。换句话说docstring 不只是给程序员看的注释它是 AI 理解工具的说明书。我自己吃过亏。第一版 read_excel 的工具描述只写了“读取 Excel 文件”结果 AI 在这个文件有几百行数据时直接把 max_rows 传成 1000返回结果把上下文撑爆了。后来我把描述改成下面这样读取 Excel 文件的结构信息和前 max_rows 行预览数据。适用于用户想查看表格内容时。对于大型表格请先使用默认 30 行预览不要一次性读取全部数据以免占用过多上下文。改了描述之后AI 的行为立刻变得合理——它会先预览再决定是否扩大读取范围。这说明工具设计的重点并不仅仅是实现功能而是要站在 AI 的角度想清楚它会在什么场景下调用我这个函数它应该怎么用才不出错把这个想明白了工具才真正好用。3. 从零写三个核心工具读、写、算 Excel3.1 环境准备先列一下依赖python 3.10 fastmcp 2.0 pandas openpyxl安装命令pip install fastmcp pandas openpyxlpandas 负责数据处理底层会自动调用 openpyxl 来读写 .xlsx 文件。解释一句如果你只需要简单读写而不做数据统计只装 openpyxl 也够但既然目标是“重构工作流”聚合、筛选、合并这类需求很快就会冒出来所以直接上 pandas 更省心。3.2 工具一 read_excel让表格变成 AI 能理解的文本直接看代码from pathlib import Path import pandas as pd from openpyxl import load_workbook from fastmcp import FastMCP mcp FastMCP(ExcelAssistant) mcp.tool() def read_excel(file_path: str, sheet_name: str , max_rows: int 30) - str: 读取 Excel 文件返回工作表结构信息和数据预览。 Args: file_path: Excel 文件的绝对路径。 sheet_name: 工作表名称留空则读取第一个工作表。 max_rows: 预览数据上限默认 30 行设为 -1 则读取全部数据。 path Path(file_path).resolve() if not path.exists(): return f错误文件不存在请检查路径{path} wb load_workbook(path, read_onlyTrue) if sheet_name and sheet_name not in wb.sheetnames: return f错误找不到工作表 {sheet_name}。当前工作表有{wb.sheetnames} ws wb[sheet_name] if sheet_name else wb[wb.sheetnames[0]] total_rows ws.max_row total_cols ws.max_column wb.close() df pd.read_excel( path, sheet_namesheet_name if sheet_name else 0, nrowsmax_rows if max_rows ! -1 else None, ) preview df.to_string(indexFalse) return ( f文件{path.name}\n f工作表{sheet_name or 第一个工作表}\n f总行数{total_rows}\n总列数{total_cols}\n f列名{list(df.columns)}\n f预览数据前 {max_rows} 行\n{preview} )这个工具做了两件事先用 openpyxl 以只读模式拿到总行数和列名这样即使文件很大也能快速获得结构信息再用 pandas 读取预览数据。返回给 AI 的内容里既有结构信息又有样例行AI 就能据此判断接下来该怎么处理。说说我为什么不用pd.read_excel()一次读到底当文件有十几万行时read_excel 会把全部数据加载进内存然后to_string()生成几千行文本塞进对话上下文。AI 的上下文窗口是有限的这等于直接把它的“记忆力”烧光了。预览加结构信息对大多数决策场景来说已经足够。3.3 工具二 write_excel让 AI 把结果落回文件AI 只读不写没有意义重构工作流必须把结果写回 .xlsx。这个工具需要注意的细节更多我直接给完整实现from openpyxl import Workbook, load_workbook from openpyxl.utils.cell import coordinate_from_string, column_index_from_string import shutil mcp.tool() def write_excel( file_path: str, data: list, sheet_name: str Sheet1, start_cell: str A1, overwrite_sheet: bool False, ) - str: 批量写入数据到 Excel 文件支持新建文件或追加写入现有工作表。 Args: file_path: 目标 Excel 文件路径若不存在则新建。 data: 二维数组每个内部列表代表一行。 sheet_name: 目标工作表名称。 start_cell: 起始单元格地址如 A1。 overwrite_sheet: 是否清空原有数据后重写默认追加。 path Path(file_path).resolve() if not path.parent.exists(): path.parent.mkdir(parentsTrue, exist_okTrue) if path.exists(): # 写前备份防止 AI 加工出错把原始数据覆盖掉 shutil.copy2(path, path.with_suffix(path.suffix .bak)) try: wb load_workbook(path) except PermissionError: return f错误文件 {path} 正被占用请先关闭 Excel 程序再试。 else: wb Workbook() ws wb[sheet_name] if sheet_name in wb.sheetnames else wb.create_sheet(sheet_name) if overwrite_sheet: ws.delete_rows(1, ws.max_row) row_start, col_start coordinate_from_string(start_cell) col_idx column_index_from_string(col_start) for r_offset, row_data in enumerate(data): for c_offset, value in enumerate(row_data): ws.cell(rowrow_start r_offset, columncol_idx c_offset, valuevalue) wb.save(path) wb.close() return f写入完成{len(data)} 行数据已写入 {path}工作表为 {sheet_name}这个工具里最核心的一点是写前备份。原因很简单AI 在中间推理时可能会先用 read 读出数据再经过自己的“加工”覆盖写回——如果它加工错了原数据就没了。一个 .bak 备份成本极低但能救命的次数不少。3.4 工具三 aggregate_excel把计算放到工具端而不是塞给 AI有个问题你可能已经想到了AI 虽然能调用工具但它本身的数值计算能力并不强把十几万行数据喂给它让它求和纯属浪费。正确的做法是在工具端用 pandas 做计算只把结果返回给 AI。mcp.tool() def aggregate_excel( file_path: str, group_col: str, value_col: str, agg_func: str sum, ) - str: 按指定列分组对数值列执行聚合统计返回统计结果文本。 Args: file_path: Excel 文件的绝对路径。 group_col: 分组依据的列名。 value_col: 需要聚合的数值列名。 agg_func: 聚合方式支持 sum、mean、count、max、min。 df pd.read_excel(file_path) if group_col not in df.columns or value_col not in df.columns: return f错误列名不存在。当前列名{list(df.columns)} result df.groupby(group_col)[value_col].agg(agg_func) return f按 {group_col} 分组对 {value_col} 执行 {agg_func} 的结果\n{result.to_string()}这个工具的意义是“把处理逻辑留在数据旁边只把答案带回去”。你让 AI 算“每个月的销售额总和”它不用把每一条明细都读进上下文只需要调用一次aggregate_excel拿到一个几十行的分组结果文本就够了。这大大减轻了 AI 的上下文压力也让错误率大幅下降。到这里三个工具已经覆盖了工作流的完整闭环读、写、算。接下来就可以接入客户端进行真实测试了。4. 在 MCP 客户端里跑通全流程从多表合并到报表产出4.1 客户端配置本地命令方式与远程服务方式我用的是支持 MCP 的桌面客户端配置方式大同小异核心就是一份 JSON{ mcpServers: { excel-server: { command: python, args: [D:/work/excel-mcp/server.py], cwd: D:/work/excel-mcp, env: { EXCEL_ROOT: D:/data } } } }这里有几个容易踩的坑。第一command里的python必须是你装了 fastmcp 的那个解释器。我一开始在系统默认 Python 环境里没装 fastmcp客户端连接了一分钟然后报“工具列表为空”排查半天才发现是虚拟环境没对其。第二Windows 下路径里的反斜杠要写正斜杠或者在 JSON 里写成\\。我在D:\work\excel-mcp这种路径上踩过 JSON 转义的坑。第三env里的EXCEL_ROOT是我约定的数据根目录server 端会用它做路径校验限制 AI 只能操作指定目录下的文件。如果你想部署成远程服务JSON 的写法会变成带url的方式并且在服务端配置鉴权 token。本地开发阶段完全没必要直接用 stdio 模式跑起来最省事。4.2 实测任务三个月销售明细自动汇总配好之后我做的第一个真实任务是这样的把 D:/data 目录下的 销售_2025_01.xlsx、销售_2025_02.xlsx、销售_2025_03.xlsx 合并成一张总表按产品类别汇总销售额并把汇总结果保存到 汇总_2025_Q1.xlsx。你猜 AI 是怎么干活的它在客户端里会做这样一连串行为先列一下自己有哪些工具看到 read_excel、write_excel、aggregate_excel。调用 read_excel 读第一个文件拿到表头和前 30 行确认列名。又读了 02、03 两个文件确认它们的表头是否一致。如果发现表头一致就连续三次调用 aggregate_excel分别做分组汇总。最后把三次返回的汇总文本中提取数据拼成二维列表调用 write_excel 写入新文件。整个过程它会自己纠错。比如在第一次读文件时发现列名和我口头描述的不一样它会停下来根据实际表头调整后续调用参数。这在以前用脚本的方式里是需要我人工去发现的差异。4.3 AI 的规划能力工具调用链是如何形成的你可能好奇AI 是怎么做到“自己规划”的。其实原理不神秘AI 在对话过程中观察到自己能调用哪些工具遇到一个任务时它会先生成需要的工具调用序列然后根据每个工具的返回结果决定下一步。MCP 只是把“工具的存在”这件事暴露给了它至于怎么组合使用是模型本身推理能力的体现。这里有一个点值得展开合并动作发生在 AI 的推理中——它拿到三次汇总文本提取出数据行拼成统一的列表传给 write_excel。这意味着我们设计的工具要尽量让返回结果“结构化、可解析”。如果 aggregate_excel 返回的是一段混乱文本AI 就很难从中提取有效数据。所以我刻意让它输出分组结果的行文本目的就是让 AI 能直接把结果喂给下一个工具。这也是为什么 MCP Server 的设计要“小而精”的原因。工具职责越单一、描述越清晰AI 就越容易做出正确的组合。如果你把一个工具做成“大杂烩”什么都能干AI 反而不知道应该在什么时候用它——它太难判断边界了。这和你带实习生是一个道理你说“帮我把这些数据整理整理”他懵你说“先做前三行的清洗再按日期排序”他马上就能执行。5. 工作流重构的边界安全、权限与异常处理5.1 给 AI 一双手也要给它一个“保险箱”MCP Server 最恐怖的地方在于它给了 AI 直接修改文件的能力而你不可能预判 AI 会怎么调用工具。所以工具设计的底线就是不能给 AI 超出任务需要的权限。我的约定有三条。第一路径白名单。所有工具在拿到路径后先调用一个内部函数做校验确认目标在EXCEL_ROOT目录内否则直接拒绝。实现起来很简单EXCEL_ROOT Path(os.environ.get(EXCEL_ROOT, .)).resolve() def within_workdir(path: Path) - bool: return path.is_relative_to(EXCEL_ROOT)第二不提供删除类工具。我整个 Server 里没有 delete、remove、clear只保留了“写入时覆盖指定工作表”的能力且必须先备份。第三写文件前自动备份上一次版本。这个在前面 write_excel 的实现里提过了这里再强调一遍AI 一次写错的代价比脚本时代高了不止一个数量级备份必须成为默认行为。5.2 异常信息是最佳提示词让 AI 自己修复问题这是个很有意思的观察MCP 工具如果抛了异常AI 可以看到错误信息它真的会尝试自己修复。举个真实例子。有一次我让 AI 聚合 A 列的销售额结果它传错参数把分组列写成了“产品类别”而这个列名在表里实际叫“产品名称”。工具端返回了一行错误“错误列名不存在。当前列名[客户名称, 产品名称, 销售额, 日期]”。AI 看到这个错误后没有停下来问用户而是立刻重新调用 read_excel 确认列名然后改用“产品名称”重新聚合。所以工具内部捕获异常时不要只返回一句“处理失败”而要把尽可能多的可读信息返回出去——尤其是“当前有哪些合法选项”这个信息。它能让 AI 瞬间理解问题并自我修正。这比任何刻意设计的错误处理逻辑都管用。5.3 日志别用 printstdout 是协议通道最后一个不太明显但非常要命的坑用 stdio 方式运行 MCP Server 时标准输出是协议通信的专用通道绝对不能直接往 stdout 打日志。一旦打了客户端收到的流就被污染了轻则日志乱插在 JSON-RPC 消息里重则连接直接中断。正确的做法是用 logging 输出到 stderrimport logging import sys logging.basicConfig(levellogging.INFO, streamsys.stderr) logger logging.getLogger(excel-mcp) logger.info(read_excel called with %s, file_path)这是我第一版开发时踩得最惨的坑之一服务端明明在干活客户端却连接报错查了半天发现是 print 干的好事。从此我养成了一个习惯——服务端日志一律走 stderr调试看得清清楚楚协议通道干干净净。6. 开发过程中踩过的 6 个坑每一个都让我多花了至少一小时6.1 工具输出太长AI 直接“失忆”第一版 read_excel 没有 max_rows 限制AI 一旦读了完整表格返回文本能到几千行。客户端那边对话立刻开始卡顿更重要的是 AI 会“忘记”之前几步的计划开始胡言乱语。解决办法就是我前面说的返回结构信息 预览 30 行。数据太多时让 AI 分批读或者直接调用聚合工具。记住一个原则返回给 AI 的内容永远是最小必要信息。如果一段文字和当前任务无关就不要出现在工具返回值里。6.2 相对路径是一场灾难MCP Server 启动时的当前工作目录通常是客户端配置里的 cwd而不是你想象的项目目录。我第一版读写 Excel 全部用相对路径结果在客户端里测试时AI 明明说“写入完成”我打开文件却什么都找不到——因为文件写到 cwd 去了跟数据目录完全不是一回事。从此我的每个工具签名里都有明确要求file_path 必须传绝对路径内部再调用 Path.resolve() 归一化。给出声明的路径不存在的错误提示不要给 AI“猜路径”的机会。6.3 openpyxl 读公式单元格返回 NoneExcel 单元格如果存的是公式openpyxl 默认读到的不是计算后的值而是公式字符串本身如果文件从没被 Excel 程序打开过更只有一个公式没有缓存值读出来就是 None。我踩过这个坑后在 read_excel 的工具描述里特别加了一句话如果某列大量出现 None可能是公式单元格请提示用户先用 Excel 打开并保存一次以便生成公式缓存。这个提示能引导 AI 向用户解释清楚而不是傻傻把 None 当作空数据处理。6.4 文件被占用进程锁与并发Windows 环境下如果目标 .xlsx 正被 Excel 程序打开openpyxl 保存时会抛 PermissionError。处理方式有两个一是工具内尝试保存出错则返回“文件被占用请先关闭 Excel”二是写临时文件再 os.replace 替换减少占用窗口期。并发问题同样出现在 MCP Server 内部。如果 AI 同时发起多个写请求两个进程同时操作同一个文件就可能损坏文件。我给写工具加了一个简单的全局线程锁threading.Lock同一时间只允许一个写操作稳妥。6.5 中文路径与编码我第一次在测试里用中文文件名“销售明细.xlsx”结果没问题但读一个从网上下载的 CSV 时遇到了 gbk 编码报错。pandas 的 read_csv 默认按 utf-8 解码遇到 gbk 编码的 CSV 直接崩掉。后来我把读取这类文本文件的逻辑改成尝试多种编码先 utf-8失败就 gbk再失败就 latin-1。这样至少能在第一时间把文件内容读出来而不至于因为编码问题卡住整个流程。6.6 同步阻塞卡死整个 Server尽量用 asyncMCP Server 本身是异步框架如果你的工具函数写成同步的框架虽然能跑但遇到长时间任务比如遍历几十个大文件时事件循环会被阻塞其他工具的调用都会排队客户端看起来就像卡死了。FastMCP 支持 async def 工具如果有潜在耗时的操作比如合并多个大文件用 async 方式加 await asyncio.sleep(0) 让步或者用 asyncio.to_thread 把阻塞调用丢到线程池用户体验会好很多。当然普通小文件处理同步就够了不必过度设计。最后再分享一点个人体会。我从决定写这个 MCP Server 到真正把它跑稳前前后后花了两周时间真正写代码的时间可能不到三天其余时间全耗在调试、调优工具描述和理解 AI 的行为模式上。但这两周非常值——因为我现在处理报表的方式彻底变了不再是“打开 Excel手动操作保存关闭”而是打开 AI 客户端说一句“把上个月的渠道数据按区域汇总产出对比表格”然后等它自己完成验证。如果你也想把手里的 Excel 工作流重构一遍我的建议很简单不要一上来就规划一个大而全的 Server从 read_excel 这一个工具开始配上客户端跑通闭环然后发现缺哪个能力就加哪个。工具会越用越顺手你对 AI 能做什么、不能做什么的理解也会在一次次实跑中清晰起来。这大概就是“亲手造一把趁手的工具”最有意思的地方了。