1. 为什么我要自己动手写一个 MCP1.1 从一次崩溃的 Excel 处理经历说起上个月帮朋友处理一批销售数据二十多个 Excel 文件每个文件里七八个 Sheet需要把指定列的数据提取出来、做清洗、再合并成一张总表。我一开始想的是用 Python 写个脚本批量跑结果打开文件一看傻眼了——表头位置不固定有的在第三行有的在第五行列名还有合并单元格日期格式五花八门有的是文本有的是日期序列号。写脚本改了三版每改一版就要重新跑一遍全量数据调试成本极高。后来我换了个思路能不能让 AI 直接理解我的意图我告诉它“把每个文件里‘销售额’那一列抽出来按月份汇总”它自己去看文件结构、自己决定怎么处理这就是我接触 MCP 的起点。MCP 全称 Model Context Protocol翻译过来叫“模型上下文协议”。你可以把它理解成一套标准接口让 AI 模型能够调用外部工具、读取外部数据。打个比方AI 本身是个很聪明但被关在房间里的人MCP 就是给这个房间开了一扇门让它能伸手拿到外面的东西——文件、数据库、API都能通过 MCP 接进来。我做的这个项目核心就是写一个 MCP Server专门用来处理 Excel。做完之后的效果是我在 AI 对话里说“帮我看看这个 Excel 里有哪些 Sheet”AI 就能通过我写的 MCP 工具去读文件、返回结果我说“把 Sheet1 里 B 列到 F 列的数据提取出来”它也能直接操作。整个过程不需要我手动打开 Excel也不需要写死脚本。1.2 这个项目适合谁来参考如果你符合下面任意一条这篇内容应该对你有用日常工作中大量接触 Excel但不想每次都手动重复操作有 Python 基础想了解怎么把自己的代码能力接入 AI 工作流听说过 MCP 但不知道从哪下手想找一个完整的实战案例已经在用 AI 辅助办公但觉得只能聊天不够想让 AI 真正“动手干活”不需要你是 AI 专家也不需要你写过 MCP。我会从环境搭建开始把每一步都拆开讲。但前提是你得会一点 Python至少知道怎么装包、怎么运行脚本。完全零基础的话建议先花两天把 Python 基础过一遍再来看。1.3 整体方案的设计思路我一开始考虑过几种方案。第一种是直接用 AI 平台自带的代码解释器功能上传 Excel 让它写代码处理。这个方案的问题是每次都要重新上传文件而且 AI 写的代码我看不到中间过程出了问题不好排查。第二种是写一个 Web 服务用 API 的方式调用。这个方案太重了要部署、要维护、要考虑并发对于个人使用来说完全是杀鸡用牛刀。最终选 MCP 的原因很简单它是专门为 AI 调用外部工具设计的协议天然适合“AI 决定什么时候调用什么工具”这个场景。而且 MCP Server 可以跑在本地文件不用上传到任何地方数据安全性有保障。我只需要定义好工具的名称、参数和返回值AI 就能根据对话内容自动判断该调用哪个工具。整个架构分三层最底层是 Excel 文件操作层用 openpyxl 和 pandas 来做实际的读写中间是 MCP Server 层把文件操作封装成一个个工具函数最上层是 AI 对话层AI 根据用户意图决定调用哪些工具。我写代码的主要工作量在中间层底层用现成的库上层用现成的客户端。这里有个关键决策我选择用 Python 而不是 Node.js 或 Go 来写 MCP Server。原因是我处理 Excel 最熟悉的库都在 Python 生态里openpyxl、pandas、xlrd 这些库成熟稳定遇到问题查资料也方便。如果你更熟悉其他语言MCP 协议本身是语言无关的用你顺手的就行。2. MCP 协议核心概念快速上手2.1 MCP 到底是什么用生活化类比讲清楚官方文档对 MCP 的定义比较抽象我用自己的话重新讲一遍。想象你是一个公司的老板AI 模型你想让助理帮你做事。但助理刚来不知道公司的文件放在哪、打印机怎么用、报销流程是什么。MCP 就是一套标准化的“助理培训手册”里面规定了公司有哪些资源可以用Resources、有哪些操作可以执行Tools、有哪些预设的提示模板Prompts。Resources 是只读的数据源比如“销售数据表”“员工名单”。Tools 是可以执行的操作比如“读取 Excel 指定区域”“写入数据到指定单元格”。Prompts 是预设的对话模板比如“帮我分析这个月的销售趋势”。AI 通过 MCP 协议和 Server 通信Server 告诉 AI“我这里有这三个工具分别叫什么名字、需要什么参数、返回什么格式。”AI 在对话中判断用户意图决定调用哪个工具、传什么参数。整个过程是动态的不需要提前把所有可能性都写死。2.2 MCP Server 和 Client 的关系MCP 采用客户端-服务端架构。Server 是提供能力的一方Client 是使用能力的一方。在我的项目里我写的是 Server负责提供 Excel 处理能力。Client 是 AI 应用本身比如某些支持 MCP 的桌面 AI 工具或开发环境插件。两者之间的通信支持两种方式stdio标准输入输出和 SSEServer-Sent Events。stdio 适合本地运行Server 和 Client 在同一台机器上通过标准输入输出流通信配置简单、延迟低。SSE 适合远程场景Server 跑在一台机器上Client 通过网络连接。我选的是 stdio 方式因为我的使用场景就是本地处理文件不需要远程访问。配置的时候只需要在 Client 的配置文件里写上 Server 的启动命令就行比如python /path/to/my_excel_server.py。2.3 一个最小可用的 MCP Server 长什么样先看一个最简单的例子让你对 MCP Server 的代码结构有个直观感受from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(excel-mcp) app.list_tools() async def list_tools(): return [ Tool( nameread_excel_sheets, description读取Excel文件中的所有Sheet名称, inputSchema{ type: object, properties: { file_path: {type: string, description: Excel文件路径} }, required: [file_path] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name read_excel_sheets: import openpyxl wb openpyxl.load_workbook(arguments[file_path], read_onlyTrue) sheets wb.sheetnames wb.close() return [TextContent(typetext, textfSheet列表: {, .join(sheets)})] async def main(): async with stdio_server() as (read_stream, write_stream): await app.run(read_stream, write_stream, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())这段代码虽然短但已经包含了 MCP Server 的核心要素声明工具列表、实现工具调用逻辑、启动 stdio 服务。后面我要做的就是在这个骨架上不断添加更多工具、处理更复杂的逻辑。3. 环境搭建与依赖安装3.1 Python 环境准备我用的 Python 版本是 3.11。建议不要用太老的版本因为 MCP 的 Python SDK 用到了不少较新的异步特性。3.10 以上都可以3.11 或 3.12 更稳。安装 Python 本身就不展开讲了官网下载安装包一路下一步就行。重点讲一下虚拟环境的创建这个很重要因为 MCP Server 的依赖和你的其他项目可能会冲突。python -m venv mcp-excel-env source mcp-excel-env/bin/activate # Windows 用 mcp-excel-env\Scripts\activate创建完虚拟环境后所有依赖都装在这个环境里不会污染全局。3.2 核心依赖清单我列一下这个项目用到的所有依赖以及每个依赖的作用依赖包版本要求用途说明mcp1.0.0MCP 协议 Python SDK提供 Server 和 Client 实现openpyxl3.1.0读写 xlsx 格式 Excel 文件支持公式、样式、合并单元格pandas2.0.0数据处理和分析适合批量操作和复杂计算xlrd2.0.0读取旧版 xls 格式文件openpyxl 不支持 xlspydantic2.0.0数据校验MCP 内部依赖它做参数验证安装命令pip install mcp openpyxl pandas xlrd pydantic注意pandas 安装的时候可能会编译一些 C 扩展如果遇到报错可以先升级 pippip install --upgrade pip然后再装。Windows 用户如果遇到编译问题建议直接装预编译的 wheel 包或者用 conda 安装。3.3 验证环境是否可用装完之后跑一段测试代码确认所有库都能正常导入import mcp import openpyxl import pandas as pd import xlrd print(mcp version:, mcp.__version__) print(openpyxl version:, openpyxl.__version__) print(pandas version:, pd.__version__) print(所有依赖导入成功)如果这段代码能跑通不报错环境就没问题了。如果报 ModuleNotFoundError检查一下虚拟环境是否激活、包是否装到了正确的环境里。4. Excel 处理工具的核心实现4.1 工具设计先想清楚 AI 需要什么能力在写代码之前我先列了一下 AI 处理 Excel 时最需要哪些能力。这个列表不是拍脑袋想的是我回顾了自己平时处理 Excel 的流程把重复度最高的操作提取出来读取文件基本信息有哪些 Sheet、每个 Sheet 有多少行多少列读取指定区域的数据给定 Sheet 名和单元格范围返回数据按条件筛选数据比如“找出销售额大于 1000 的行”写入数据在指定位置写入值或公式创建新 Sheet、删除 Sheet数据格式转换比如把文本日期转成标准日期格式批量处理多个文件对文件夹下所有 Excel 执行相同操作我最终实现了前六个第七个因为涉及文件系统遍历我单独做了一个工具。每个工具都对应 MCP 里的一个 Tool 定义。4.2 读取 Excel 文件结构这是最基础的工具AI 拿到一个文件路径后第一件事就是了解这个文件里有什么。app.list_tools() async def list_tools(): return [ Tool( nameget_excel_info, description获取Excel文件的基本信息包括所有Sheet名称、每个Sheet的行数和列数, inputSchema{ type: object, properties: { file_path: { type: string, description: Excel文件的绝对路径 } }, required: [file_path] } ), # ... 其他工具 ]对应的处理逻辑app.call_tool() async def call_tool(name: str, arguments: dict): if name get_excel_info: file_path arguments[file_path] try: wb openpyxl.load_workbook(file_path, read_onlyTrue, data_onlyTrue) info [] for sheet_name in wb.sheetnames: ws wb[sheet_name] info.append({ sheet_name: sheet_name, max_row: ws.max_row, max_column: ws.max_column }) wb.close() result json.dumps(info, ensure_asciiFalse, indent2) return [TextContent(typetext, textresult)] except Exception as e: return [TextContent(typetext, textf读取失败: {str(e)})]这里有几个细节值得说。read_onlyTrue让 openpyxl 以只读模式打开文件速度更快、内存占用更低适合大文件。data_onlyTrue表示如果单元格里是公式返回公式计算后的值而不是公式本身。这两个参数搭配使用在只需要读取数据不需要修改的场景下是最优选择。4.3 读取指定区域数据这个工具比上一个复杂一些因为要处理单元格范围解析。用户可能说“A1 到 D10”也可能说“从第三行到第十行”我需要把这些自然语言描述转成 openpyxl 能理解的格式。Tool( nameread_range, description读取指定Sheet中指定单元格区域的数据, inputSchema{ type: object, properties: { file_path: {type: string, description: Excel文件路径}, sheet_name: {type: string, description: Sheet名称}, start_cell: {type: string, description: 起始单元格如A1}, end_cell: {type: string, description: 结束单元格如D10} }, required: [file_path, sheet_name, start_cell, end_cell] } )处理逻辑用 openpyxl 的iter_rows方法if name read_range: file_path arguments[file_path] sheet_name arguments[sheet_name] start_cell arguments[start_cell] end_cell arguments[end_cell] wb openpyxl.load_workbook(file_path, read_onlyTrue, data_onlyTrue) ws wb[sheet_name] rows [] for row in ws[f{start_cell}:{end_cell}]: row_data [cell.value for cell in row] rows.append(row_data) wb.close() # 转成Markdown表格格式方便AI理解和展示 if rows: header rows[0] md_table | | .join(str(h) for h in header) |\n md_table | | .join([---] * len(header)) |\n for row in rows[1:]: md_table | | .join(str(v) if v is not None else for v in row) |\n return [TextContent(typetext, textmd_table)] return [TextContent(typetext, text指定区域没有数据)]这里我特意把返回结果转成了 Markdown 表格格式。原因是 AI 对 Markdown 表格的理解能力很强能直接从中提取结构化信息。如果返回的是 JSON 数组AI 也能理解但展示给用户看的时候不够直观。这个设计在实际使用中效果很好AI 经常能直接基于表格内容做分析。4.4 数据筛选与条件查询这个工具让 AI 能够根据条件筛选数据。我设计的时候考虑了一个问题条件表达式怎么传如果让 AI 直接传 Python 表达式有安全风险如果设计一套复杂的查询 DSLAI 学习成本高。最后我选了一个折中方案支持简单的比较条件用 JSON 格式传递。Tool( namefilter_data, description根据条件筛选Excel中的数据行, inputSchema{ type: object, properties: { file_path: {type: string}, sheet_name: {type: string}, header_row: {type: integer, description: 表头所在行号从1开始}, conditions: { type: array, items: { type: object, properties: { column: {type: string}, operator: {type: string, enum: [eq, gt, lt, gte, lte, contains]}, value: {type: string} } }, description: 筛选条件列表多个条件之间是AND关系 } }, required: [file_path, sheet_name, header_row, conditions] } )实现逻辑用 pandas 来做因为 pandas 的筛选功能非常成熟if name filter_data: df pd.read_excel( arguments[file_path], sheet_namearguments[sheet_name], headerarguments[header_row] - 1 ) mask pd.Series([True] * len(df)) for cond in arguments[conditions]: col cond[column] op cond[operator] val cond[value] if op eq: mask (df[col].astype(str) val) elif op gt: mask (pd.to_numeric(df[col], errorscoerce) float(val)) elif op lt: mask (pd.to_numeric(df[col], errorscoerce) float(val)) elif op gte: mask (pd.to_numeric(df[col], errorscoerce) float(val)) elif op lte: mask (pd.to_numeric(df[col], errorscoerce) float(val)) elif op contains: mask df[col].astype(str).str.contains(val, naFalse) filtered df[mask] result filtered.to_markdown(indexFalse) return [TextContent(typetext, textf筛选到 {len(filtered)} 行数据:\n\n{result})]用 pandas 的to_markdown方法直接输出 Markdown 表格省去了手动拼接的麻烦。不过要注意to_markdown需要安装tabulate库记得加到依赖里。4.5 写入数据与创建新 Sheet写入操作比读取要小心因为一旦写错就可能覆盖原始数据。我的做法是所有写入操作默认创建一个新文件不直接修改原文件。如果用户明确要求修改原文件才在原文件上操作。Tool( namewrite_data, description向Excel指定位置写入数据默认创建新文件, inputSchema{ type: object, properties: { file_path: {type: string, description: 源文件路径}, output_path: {type: string, description: 输出文件路径不填则覆盖源文件}, sheet_name: {type: string}, start_cell: {type: string}, data: { type: array, items: {type: array, items: {type: string}}, description: 二维数组每个子数组代表一行 } }, required: [file_path, sheet_name, start_cell, data] } )写入逻辑if name write_data: src arguments[file_path] dst arguments.get(output_path, src) wb openpyxl.load_workbook(src) ws wb[arguments[sheet_name]] start_cell arguments[start_cell] start_col openpyxl.utils.column_index_from_string( .join(filter(str.isalpha, start_cell)) ) start_row int(.join(filter(str.isdigit, start_cell))) for i, row_data in enumerate(arguments[data]): for j, value in enumerate(row_data): ws.cell(rowstart_row i, columnstart_col j, valuevalue) wb.save(dst) wb.close() return [TextContent(typetext, textf数据已写入 {dst})]这里有个坑我踩过openpyxl 的column_index_from_string只接受纯字母如果传入 A1 这种带数字的会报错。所以要先分离字母和数字部分。这个细节在官方文档里没有明确说是我调试的时候发现的。5. 把 MCP Server 接入 AI 工作流5.1 配置文件的写法MCP Server 写好了接下来要让 AI 客户端知道它的存在。不同的客户端配置方式略有差异但核心都是告诉客户端启动命令是什么、参数是什么。以常见的配置文件格式为例{ mcpServers: { excel-processor: { command: /path/to/mcp-excel-env/bin/python, args: [/path/to/excel_server.py], env: { PYTHONUNBUFFERED: 1 } } } }关键点command要指向虚拟环境里的 Python 解释器不是系统全局的 Python。因为依赖装在虚拟环境里用全局 Python 会找不到包。args是 Server 脚本的路径。env里设置PYTHONUNBUFFERED1是为了让输出实时刷新避免日志延迟。5.2 实际对话中的调用效果配置好之后我在 AI 对话里测试了几个场景。第一个场景我输入“帮我看看 D:\data\sales.xlsx 这个文件里有哪些 Sheet”。AI 自动调用了get_excel_info工具返回了 Sheet 列表和每个 Sheet 的规模。整个过程我没有写任何代码就是一句话的事。第二个场景我说“把 Sheet1 里 A1 到 E20 的数据读出来给我看看”。AI 调用read_range返回了一个 Markdown 表格。我接着问“销售额超过 5000 的有哪些”AI 又调用filter_data在刚才读取的数据基础上做筛选。注意这里 AI 并没有重新读文件而是基于上下文里的数据直接筛选说明它理解了工具返回结果的含义。第三个场景我说“把筛选出来的结果写到新文件 D:\data\result.xlsx 的 Sheet1 里”。AI 调用write_data把数据写入了新文件。我打开文件检查数据完全正确。5.3 工具描述的重要性这里我要特别强调一点MCP 工具的description字段非常关键。AI 是根据这个描述来判断什么时候该调用哪个工具的。如果描述写得含糊AI 就可能调错工具或者不调用。我一开始把read_range的描述写成“读取 Excel 数据”结果 AI 经常在只需要 Sheet 列表的时候也调用这个工具。后来改成“读取指定 Sheet 中指定单元格区域的数据需要提供起始和结束单元格”AI 的调用准确率明显提升。同理参数的description也要写清楚。比如start_cell我写的是“起始单元格如 A1”给了一个具体例子AI 就知道格式是什么样的。如果不写例子AI 可能会传“第1行第1列”这种自然语言描述导致解析失败。6. 踩坑记录与问题排查6.1 常见报错与解决方法报错信息原因解决方法ModuleNotFoundError: No module named mcp依赖没装或装错环境确认虚拟环境已激活重新 pip install mcpFileNotFoundError文件路径不对用绝对路径Windows 路径注意反斜杠转义openpyxl.utils.exceptions.InvalidFileException文件格式不支持确认是 xlsx 格式xls 需要用 xlrdPermissionError文件被其他程序占用关闭 Excel 再试或者写入新文件JSONDecodeError参数格式不对检查 AI 传的参数是否符合 inputSchema6.2 大文件处理的性能问题我测试过一个 50MB 的 Excel 文件大约 20 万行数据。用read_onlyTrue模式读取 Sheet 列表很快不到一秒。但用 pandas 读取全部数据做筛选时内存占用飙升到 1GB 以上耗时将近半分钟。优化方案对于大文件不要一次性读全部数据。可以先用get_excel_info了解规模然后分批次读取。或者用 openpyxl 的iter_rows逐行处理避免一次性加载到内存。# 分批读取示例 batch_size 10000 for i, row in enumerate(ws.iter_rows(min_row2, values_onlyTrue)): if i % batch_size 0: # 处理一批数据 pass6.3 AI 调用工具时的参数错误AI 有时候会传一些意料之外的参数。比如header_row我定义的是 integerAI 可能传字符串 3。虽然 pydantic 会做类型转换但为了保险我在代码里加了显式转换header_row int(arguments[header_row])还有一种情况是 AI 传了不存在的 Sheet 名。我在代码里加了检查if sheet_name not in wb.sheetnames: return [TextContent(typetext, textfSheet {sheet_name} 不存在可用的Sheet: {wb.sheetnames})]这样 AI 收到错误信息后会自动根据可用 Sheet 列表重新尝试。6.4 中文编码问题处理中文 Excel 时遇到过乱码问题。原因是文件本身编码不对或者 pandas 读取时默认编码不匹配。解决方法是在读取时指定编码df pd.read_excel(file_path, sheet_namesheet_name, encodingutf-8)如果还是乱码可能是文件本身的问题用 Excel 打开另存为一次通常能解决。7. 后续扩展方向7.1 支持更多文件格式目前只支持 xlsx 和 xls后续可以加上 csv、tsv 的支持。pandas 本身就能读这些格式只需要在工具里加一个格式判断分支就行。7.2 增加图表生成能力用 openpyxl 的 chart 模块可以在 Excel 里生成柱状图、折线图、饼图。这个功能对于做报表很有用。我打算下一步加一个create_chart工具让 AI 能根据数据自动生成图表。7.3 批量文件处理现在的工具都是针对单个文件的。实际工作中经常需要处理一个文件夹下的所有 Excel。可以加一个batch_process工具接受文件夹路径和一个操作列表对每个文件执行相同操作。7.4 与数据库打通Excel 处理完之后数据可能需要入库。可以加一个工具把 Excel 数据写入 SQLite 或 PostgreSQL。这样 AI 就能完成从文件读取到数据入库的完整流程。7.5 错误恢复与重试机制目前如果某个工具调用失败整个流程就中断了。可以加一个重试机制对于临时性错误如文件被占用自动重试几次。这个用 Python 的tenacity库很容易实现。from tenacity import retry, stop_after_attempt, wait_fixed retry(stopstop_after_attempt(3), waitwait_fixed(2)) def load_workbook_with_retry(path): return openpyxl.load_workbook(path)8. 一些实操心得写这个 MCP Server 的过程中我最大的体会是工具的设计比代码的实现更重要。代码写得再漂亮如果工具定义不符合 AI 的使用习惯效果就是不好。反过来工具定义得清晰合理代码稍微粗糙一点AI 也能用得很好。具体来说有几点经验值得分享。第一工具粒度要适中。太粗了 AI 不好控制比如一个“处理 Excel”工具AI 不知道具体能做什么太细了调用次数太多比如把“读取单元格”和“读取区域”分成两个工具AI 经常搞混。我的经验是按操作类型分读、写、筛选、转换各一个工具每个工具内部处理细节。第二返回值格式要统一。我所有工具都返回 Markdown 格式的文本AI 处理起来最顺畅。如果有的返回 JSON、有的返回纯文本、有的返回表格AI 需要花精力去理解不同格式容易出错。第三错误信息要具体。不要只返回“操作失败”要返回“Sheet Sheet3 不存在可用的Sheet有Sheet1, Sheet2”。这样 AI 能根据错误信息自动纠正不需要人工干预。第四先在本地测试再接入 AI。MCP Server 写完后可以写一个简单的测试脚本直接调用工具函数确认逻辑正确后再配置到 AI 客户端。这样排查问题的时候能快速定位是工具本身的问题还是 AI 调用的问题。第五日志要打全。我在每个工具函数的入口和出口都加了日志记录传入参数和返回结果。调试的时候看日志就能知道 AI 传了什么、工具返回了什么比在 AI 对话里猜要高效得多。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) # 在工具函数里 logger.info(f调用工具: {name}, 参数: {arguments})这套东西搭起来之后我处理 Excel 的效率确实上了一个台阶。以前要写脚本、调试、跑数据现在就是几句话的事。当然也不是万能的特别复杂的逻辑还是得写代码但日常百分之七八十的重复操作都能覆盖了。如果你也经常跟 Excel 打交道建议花一个周末把这个搭起来后面省下的时间绝对值得。
阅读完成 · 觉得有帮助?