
1. 为什么我要自己动手写一个 MCP1.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% 以上的日常需求。为什么不用 VBAVBA 也能做但它跟 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-envWindows 下激活mcp-excel-env\Scripts\activateMac 或 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( nameread_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_onlyTrue, data_onlyTrue) result {} for sheet_name in wb.sheetnames: ws wb[sheet_name] rows [] for i, row in enumerate(ws.iter_rows(values_onlyTrue)): 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(typetext, textstr(result))]这里有几个细节值得说。read_onlyTrue让 openpyxl 用只读模式打开大文件时内存占用低很多。data_onlyTrue让公式单元格返回计算结果而不是公式本身AI 看结果更有意义。返回格式用TextContent内容是字符串。为什么不用 JSON 对象直接返回因为 MCP 的返回类型目前以文本为主AI 拿到文本后自己解析。你把字典转成字符串AI 也能理解。注意事项文件路径一定要用绝对路径。相对路径在不同客户端下工作目录不一样很容易找不到文件。我踩过这个坑调试了半天才发现是路径问题。3.2 数据筛选与汇总把重复劳动交给工具读取概览之后AI 知道了数据结构接下来就是具体操作。最常见的需求是“按某列筛选”和“按某列汇总”。筛选工具的设计Tool( namefilter_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_typecontains): df pd.read_excel(file_path, sheet_namesheet_name) if match_type exact: filtered df[df[column] value] else: filtered df[df[column].astype(str).str.contains(value, naFalse)] return filtered.to_dict(orientrecords)astype(str)是为了防止列里有数字或日期直接调str.contains会报错。naFalse让空值不参与匹配避免 NaN 引发的异常。汇总工具类似核心是groupbydef summarize_excel(file_path, sheet_name, group_column, value_column, agg_funcsum): df pd.read_excel(file_path, sheet_namesheet_name) result df.groupby(group_column)[value_column].agg(agg_func) return result.reset_index().to_dict(orientrecords)agg_func支持 sum、mean、count、max、min。AI 根据用户意图选择。比如“统计每个区域的销售额”用 sum“统计每个区域的订单数”用 count。3.3 写入与生成新文件让结果落地处理完数据总得输出。写入工具负责把结果写回 Excel或者生成新文件。Tool( namewrite_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, modenew): df pd.DataFrame(data[1:], columnsdata[0]) if mode append and os.path.exists(file_path): with pd.ExcelWriter(file_path, modea, if_sheet_existsoverlay) as writer: df.to_excel(writer, sheet_namesheet_name, indexFalse) else: df.to_excel(file_path, sheet_namesheet_name, indexFalse) return f已写入 {len(df)} 行数据到 {sheet_name}modea是追加模式if_sheet_existsoverlay表示如果 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 和 containsAI 就不会传别的值。还有一个技巧工具名用动词开头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 里明确类型为 numberAI 会尽量遵守。二是在代码里做类型转换和容错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, usecolsNone): df pd.read_excel( file_path, sheet_namesheet_name, usecolsusecols, engineopenpyxl ) return dfengineopenpyxl明确指定引擎避免 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(typetext, textf错误未知工具 {name})] except FileNotFoundError: return [TextContent(typetext, text错误文件不存在请检查路径是否正确)] except PermissionError: return [TextContent(typetext, text错误文件被占用请先关闭Excel再试)] except Exception as e: return [TextContent(typetext, textf错误{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_stringdata_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 的表现才稳。