
1. 为什么我要自己动手写一个 MCP1.1 从一次崩溃的 Excel 处理说起上个月帮朋友处理一批销售数据二十多个 Excel 文件每个文件里都有七八张表需要按区域拆分、按月份汇总、再统一生成一份带图表的分析报告。我一开始想的是老办法——写个 Python 脚本用 pandas 读、用 openpyxl 写跑一遍完事。结果朋友第二天又发来一批说格式稍微变了点列名从销售额改成了销售金额还多了两列备注。脚本直接报错我又得改代码、重新调试、重新跑。这种场景我相信做过数据处理的人都遇到过。Excel 处理这件事麻烦的从来不是能不能做而是每次都要重新做。需求方永远在变表格结构永远在调你写的脚本永远只能解决上一次的问题。后来我接触到了 MCP 这个概念。MCP 全称是 Model Context Protocol翻译过来叫模型上下文协议。你可以把它理解成一套标准接口让 AI 模型能够调用外部工具、读取外部数据、执行外部操作。打个比方以前的 AI 就像一个只能动嘴的顾问你问它问题它给你建议但具体干活还得你自己来。有了 MCP 之后AI 就像装上了手和脚它能直接去操作你的文件、调用你的脚本、完成你的任务。我当时的想法很简单如果我能把 Excel 处理的常用操作封装成 MCP 工具那以后不管表格怎么变我只需要用自然语言告诉 AI帮我把这批文件按区域汇总一下它就能自动调用对应的工具去完成。不用再写脚本不用再改代码不用再担心列名变了怎么办。这个想法让我花了大概两周的业余时间从零开始搭出了自己的第一个 MCP 服务。过程中踩了不少坑也积累了一些经验这篇文章就把整个思路和实操过程完整地分享出来。1.2 MCP 到底解决了什么问题在动手之前有必要先把 MCP 的价值说清楚。很多人第一次听到 MCP 会以为是某种硬件协议其实它是软件层面的东西核心作用是标准化 AI 与外部工具的交互方式。在没有 MCP 之前如果你想让 AI 帮你操作 Excel通常有几种做法。一种是直接把数据粘贴到对话框里让 AI 分析完再复制回来这种方式适合小数据量稍微大一点就卡死了。另一种是写一个专门的插件或者脚本让 AI 通过某种方式调用但每个 AI 平台的调用方式都不一样你为 A 平台写的工具换到 B 平台就用不了。MCP 的出现改变了这个局面。它定义了一套统一的协议包括工具怎么注册、参数怎么传递、结果怎么返回。只要你按照这个协议实现一个 MCP 服务任何支持 MCP 的 AI 客户端都能直接调用你的工具。这就像 USB 接口一样以前每个设备都有自己的充电口现在统一成 Type-C一根线走天下。对于 Excel 处理这个场景来说MCP 带来的最大好处是把写代码变成了配工具。你不需要每次都写新的 Python 脚本只需要把常用的 Excel 操作——读取、筛选、汇总、写入、生成图表——封装成一个个独立的工具。之后不管面对什么表格你只需要用自然语言描述需求AI 会自动判断该调用哪个工具、传什么参数。1.3 这个项目适合谁来参考这篇文章面向的读者我大致分三类。第一类是经常和 Excel 打交道的职场人。你可能不会写代码但每天都要处理各种表格重复性的操作让你很烦。这类读者可以重点看思路部分理解 MCP 能帮你做什么然后直接使用我后面提供的现成工具。第二类是有一定 Python 基础的数据处理从业者。你写过 pandas 脚本知道怎么读写 Excel但每次需求变化都要改代码让你很累。这类读者可以跟着实操部分把自己的常用脚本改造成 MCP 工具一劳永逸。第三类是对 AI 应用开发感兴趣的开发者。你想了解 MCP 协议怎么落地想看看一个完整的 MCP 服务长什么样。这类读者可以重点关注架构设计和代码实现部分把 Excel 处理当成一个案例来学习 MCP 的开发模式。不管你是哪一类我建议都先动手跑一遍。MCP 这个东西看十篇文章不如自己搭一个服务来得实在。2. 整体架构设计与技术选型2.1 为什么选 Python 作为实现语言做 MCP 服务语言选择其实挺多的。官方提供了 Python、TypeScript、Java 等多种 SDK理论上你用哪个都行。我最终选了 Python原因有三个。第一个原因是生态成熟。Excel 处理这个领域Python 的库是最全的。pandas 负责数据处理openpyxl 负责读写 xlsxxlrd 负责读老格式xlsxwriter 负责生成带格式的文件matplotlib 和 plotly 负责画图。这些库经过多年打磨稳定性和功能都没得说。换成其他语言你可能要花大量时间在找库和踩坑上。第二个原因是代码简洁。Python 写数据处理逻辑几行代码就能完成其他语言几十行的工作。MCP 工具的核心是接收参数、处理数据、返回结果Python 在这方面的表达力很强代码可读性也好方便后续维护。第三个原因是调试方便。MCP 服务在开发阶段需要频繁测试Python 有交互式解释器可以随时验证某个函数的输出。而且 Python 的报错信息比较友好排查问题效率高。当然Python 也有缺点比如性能不如编译型语言处理超大文件时可能慢一些。但对于绝大多数 Excel 处理场景来说这个性能差距可以忽略不计。真遇到性能瓶颈也可以用多进程或者异步来优化。2.2 MCP 服务的核心组成一个完整的 MCP 服务从结构上看包含四个部分。传输层负责和客户端通信。MCP 支持两种传输方式一种是标准输入输出stdio适合本地运行的服务另一种是 HTTP 加 SSEServer-Sent Events适合远程部署的服务。我一开始用的是 stdio因为配置简单后来为了让多个客户端都能连改成了 HTTP 方式。协议层负责消息的编解码。MCP 的消息格式是 JSON-RPC 2.0客户端发过来的请求、服务端返回的响应都遵循这个格式。这部分 SDK 已经封装好了你不需要手动处理。工具层是核心也就是你注册的那些工具。每个工具包含名称、描述、参数定义和执行函数。AI 会根据工具的 description 来判断什么时候调用它所以描述写得清不清楚直接决定了 AI 能不能正确使用你的工具。资源层是可选的用来暴露一些静态数据给 AI 读取比如配置文件、模板文件。Excel 处理场景里我主要用工具层资源层用得不多。2.3 Excel 处理工具的功能划分把 Excel 处理拆成工具不能太粗也不能太细。太粗了一个工具干所有事参数复杂到 AI 都搞不清楚太细了工具数量爆炸AI 选择困难。我最终的划分方案是这样的工具名称功能典型场景read_excel读取 Excel 文件返回结构化数据查看表格内容、获取列名filter_data按条件筛选行筛选某区域、某时间段的数据group_aggregate分组汇总按区域汇总销售额、按月份统计write_excel将数据写入 Excel生成汇总表、导出结果merge_files合并多个 Excel 文件汇总多个分公司的报表create_chart生成图表生成柱状图、折线图这六个工具覆盖了日常 Excel 处理 90% 以上的需求。每个工具的参数都控制在三到五个AI 理解起来不费劲。2.4 数据流转的整体思路整个工作流是这样的用户在 AI 客户端里用自然语言描述需求AI 解析需求后决定调用哪些工具、按什么顺序调用。比如用户说把 D 盘 sales 文件夹里所有 Excel 按区域汇总生成一个新文件AI 会先调用 merge_files 把文件合并再调用 group_aggregate 按区域汇总最后调用 write_excel 输出结果。这个过程中数据在工具之间传递。为了减少 IO 开销我让工具之间传递的是内存中的 DataFrame 对象而不是反复读写文件。具体做法是维护一个会话级的缓存每个工具执行完把结果存进缓存并返回一个引用 ID下一个工具通过这个 ID 拿到数据。这个设计有个好处就是用户可以在多轮对话中逐步细化需求。比如先合并文件看到结果后说只保留华东区AI 调用 filter_data 在之前的结果上继续处理不用重新读文件。3. 核心工具的实现细节3.1 环境准备与依赖安装先把环境搭起来。我用的 Python 版本是 3.11太老的版本可能不支持某些新特性。安装依赖直接用 pippip install mcp pandas openpyxl xlsxwriter matplotlib这里解释一下每个包的作用。mcp 是官方 SDK提供 MCP 服务的框架pandas 是数据处理核心openpyxl 负责读写 xlsx 格式xlsxwriter 用来生成带格式的输出文件matplotlib 用来画图。注意openpyxl 和 xlsxwriter 功能有重叠但各有侧重。openpyxl 擅长读写已有文件xlsxwriter 擅长从零生成带复杂格式的文件。两个都装上按场景选用。安装完之后建议先跑一个最小示例验证环境。创建一个test_mcp.py内容如下from mcp.server.fastmcp import FastMCP mcp FastMCP(test-server) mcp.tool() def hello(name: str) - str: 打招呼工具 return fHello, {name}! if __name__ __main__: mcp.run()运行这个脚本如果没报错说明环境没问题。这个 FastMCP 是官方提供的高层封装用装饰器的方式注册工具比底层 API 简洁很多。3.2 read_excel 工具的实现读取 Excel 看起来简单其实有不少细节要注意。比如文件可能有多个 sheet列名可能不在第一行数据类型可能被自动转换。import pandas as pd from mcp.server.fastmcp import FastMCP mcp FastMCP(excel-server) _cache {} mcp.tool() def read_excel(file_path: str, sheet_name: str None, header_row: int 0) - dict: 读取 Excel 文件内容 Args: file_path: Excel 文件的完整路径 sheet_name: 工作表名称不填则读取第一个 header_row: 表头所在行号从 0 开始 try: df pd.read_excel(file_path, sheet_namesheet_name, headerheader_row) cache_id fdf_{len(_cache)} _cache[cache_id] df return { cache_id: cache_id, shape: list(df.shape), columns: df.columns.tolist(), preview: df.head(5).to_dict(orientrecords) } except Exception as e: return {error: str(e)}这个实现有几个关键点。第一返回结果里包含 cache_id后续工具可以通过这个 ID 拿到数据避免重复读文件。第二返回 preview 只取前五行避免数据量太大导致响应超时。第三用 try-except 捕获异常把错误信息返回给 AI让 AI 能告诉用户哪里出了问题。实操心得pandas 读 Excel 时如果某列既有数字又有文本会自动把整列转成 object 类型。如果你需要保持原始类型可以在读取后手动处理或者用 dtype 参数指定。3.3 filter_data 工具的实现筛选是 Excel 处理里最高频的操作。用户的需求千变万化销售额大于一万的、华东区的、上个月的本质上都是按条件过滤行。mcp.tool() def filter_data(cache_id: str, column: str, operator: str, value: str) - dict: 按条件筛选数据行 Args: cache_id: 数据引用ID column: 筛选依据的列名 operator: 比较运算符支持 eq/ne/gt/lt/ge/le/contains value: 比较值 if cache_id not in _cache: return {error: 数据不存在请先读取文件} df _cache[cache_id] if column not in df.columns: return {error: f列 {column} 不存在可用列{df.columns.tolist()}} ops { eq: lambda s, v: s v, ne: lambda s, v: s ! v, gt: lambda s, v: s float(v), lt: lambda s, v: s float(v), ge: lambda s, v: s float(v), le: lambda s, v: s float(v), contains: lambda s, v: s.astype(str).str.contains(v, naFalse) } if operator not in ops: return {error: f不支持的运算符 {operator}} try: result df[ops[operator](df[column], value)] new_id fdf_{len(_cache)} _cache[new_id] result return { cache_id: new_id, shape: list(result.shape), preview: result.head(5).to_dict(orientrecords) } except Exception as e: return {error: str(e)}这里我把运算符做成了字典映射好处是扩展方便以后要加以...开头、在...之间之类的操作直接往字典里加就行。注意contains 操作符用了astype(str)强制转字符串因为有些列可能是数字类型直接调 str.contains 会报错。这个细节不注意的话AI 调用时会频繁出错。3.4 group_aggregate 工具的实现分组汇总是数据分析的核心操作。用户说按区域汇总销售额翻译成代码就是 groupby 加 sum。mcp.tool() def group_aggregate(cache_id: str, group_by: str, agg_column: str, agg_func: str sum) - dict: 按指定列分组并汇总 Args: cache_id: 数据引用ID group_by: 分组依据的列名 agg_column: 需要汇总的列名 agg_func: 汇总方式支持 sum/mean/count/max/min if cache_id not in _cache: return {error: 数据不存在} df _cache[cache_id] for col in [group_by, agg_column]: if col not in df.columns: return {error: f列 {col} 不存在} funcs {sum: sum, mean: mean, count: count, max: max, min: min} if agg_func not in funcs: return {error: f不支持的汇总方式 {agg_func}} try: result df.groupby(group_by)[agg_column].agg(funcs[agg_func]).reset_index() result.columns [group_by, f{agg_column}_{agg_func}] new_id fdf_{len(_cache)} _cache[new_id] result return { cache_id: new_id, shape: list(result.shape), data: result.to_dict(orientrecords) } except Exception as e: return {error: str(e)}这个工具返回的是完整数据而不是预览因为汇总后的数据量通常不大全部返回能让 AI 直接看到结果方便后续对话。实操心得groupby 之后如果不 reset_index分组列会变成索引后续写入 Excel 时会丢失。这个坑我踩过排查了半天才发现。3.5 write_excel 工具的实现写文件是最后一步也是用户最关心的结果。这里要考虑格式问题比如列宽、表头样式、数字格式。mcp.tool() def write_excel(cache_id: str, output_path: str, sheet_name: str Sheet1) - dict: 将数据写入 Excel 文件 Args: cache_id: 数据引用ID output_path: 输出文件路径 sheet_name: 工作表名称 if cache_id not in _cache: return {error: 数据不存在} df _cache[cache_id] try: with pd.ExcelWriter(output_path, enginexlsxwriter) as writer: df.to_excel(writer, sheet_namesheet_name, indexFalse) worksheet writer.sheets[sheet_name] for i, col in enumerate(df.columns): max_len max(df[col].astype(str).map(len).max(), len(str(col))) 2 worksheet.set_column(i, i, min(max_len, 50)) return {success: True, path: output_path, rows: len(df)} except Exception as e: return {error: str(e)}列宽自适应这段代码很实用。不设置的话生成的 Excel 列宽都是默认值长文本会显示不全用户还得手动调整。设置逻辑是取列名长度和该列内容最大长度的较大值加 2 作为留白上限 50 防止某列特别长把表格撑爆。3.6 merge_files 工具的实现合并多个文件是批量处理的常见需求。这个工具接收一个文件夹路径把里面所有 Excel 合并成一个。import os mcp.tool() def merge_files(folder_path: str, pattern: str .xlsx) - dict: 合并文件夹内所有 Excel 文件 Args: folder_path: 文件夹路径 pattern: 文件后缀过滤默认 .xlsx if not os.path.isdir(folder_path): return {error: 文件夹不存在} files [f for f in os.listdir(folder_path) if f.endswith(pattern)] if not files: return {error: 未找到匹配的文件} dfs [] for f in files: try: df pd.read_excel(os.path.join(folder_path, f)) df[_source_file] f dfs.append(df) except Exception as e: return {error: f读取 {f} 失败{str(e)}} result pd.concat(dfs, ignore_indexTrue) new_id fdf_{len(_cache)} _cache[new_id] result return { cache_id: new_id, file_count: len(files), total_rows: len(result), columns: result.columns.tolist() }这里加了一个_source_file列记录每行数据来自哪个文件。这个设计很实用合并后如果发现数据有问题可以快速定位到源文件。注意合并的前提是各文件的列结构一致。如果列名不同concat 会产生大量 NaN。实际使用中我建议先让 AI 读取几个文件看看结构确认一致再合并。4. 完整工作流的搭建与调试4.1 服务启动与客户端配置工具写完了接下来要让 AI 客户端能连上。我用的是支持 MCP 的桌面客户端配置方式是在配置文件里加一段{ mcpServers: { excel-server: { command: python, args: [D:/mcp/excel_server.py] } } }这段配置的意思是客户端启动时会自动运行excel_server.py通过标准输入输出和服务通信。配置保存后重启客户端如果一切正常你会在工具列表里看到 read_excel、filter_data 这些工具。如果没看到先检查 Python 路径对不对再检查脚本能不能独立运行。我遇到过因为脚本里有语法错误导致服务启动失败的情况客户端不会报详细错误只能自己手动跑一遍脚本看输出。4.2 用自然语言驱动完整流程服务连上之后就可以用自然语言下指令了。我拿一批测试数据跑了一遍完整流程对话大致是这样的我说读取 D:/data/sales_2024.xlsx看看里面有什么。AI 调用 read_excel返回了列名和五行预览。我看到列有区域、月份、销售额、负责人。我接着说按区域汇总销售额。AI 调用 group_aggregategroup_by 传区域agg_column 传销售额agg_func 传sum。返回了华东、华北、华南、西南四个区域的汇总数据。我说把结果写到 D:/data/summary.xlsx。AI 调用 write_excel输出文件。整个过程我没写一行代码全是自然语言。这个体验和以前写脚本完全不一样。以前我要想清楚每一步的代码怎么写现在我只需要想清楚我要什么结果。AI 负责把需求翻译成工具调用我负责判断结果对不对。4.3 多轮对话中的数据管理多轮对话有个关键问题数据怎么在轮次之间保持。我的方案是用内存缓存每个工具执行完把结果存起来返回一个 ID。下一个工具通过 ID 拿数据。这个方案的好处是响应快不用反复读写文件。但也有个隐患就是服务重启后缓存就没了。对于长时间运行的场景我建议加一个持久化机制把缓存定期写到磁盘。另外缓存会占用内存。如果处理的数据量特别大比如几十万行缓存可能会撑爆内存。这种情况我建议在工具里加一个判断数据量超过阈值就自动落盘用文件路径代替内存对象。实操心得缓存 ID 的生成我用的是df_{len(_cache)}简单但有个问题——如果缓存被清理过ID 可能重复。更稳妥的做法是用 uuid虽然长一点但不会冲突。4.4 错误处理与用户反馈AI 调用工具时如果工具返回 errorAI 会把错误信息转述给用户。所以错误信息写得好不好直接影响用户体验。我一开始的错误信息写得很技术化比如KeyError: 销售额用户看了完全不知道什么意思。后来改成列 销售额 不存在可用列[区域, 月份, 销售金额]用户一看就明白哦列名写错了应该是销售金额。这个改进看起来小但实际使用中体验差别很大。好的错误信息应该包含三要素出了什么问题、可能的原因、下一步怎么办。4.5 性能优化的几个手段处理大文件时性能是个绕不开的话题。我总结了几个优化手段。第一个是只读需要的列。pandas 的 read_excel 支持 usecols 参数如果你只需要几列指定一下能省不少内存和时间。第二个是用 dtype 指定类型。pandas 会自动推断列类型这个过程比较慢。如果你知道某列是整数直接指定 dtype能加快读取速度。第三个是分块处理。对于超大文件可以用 chunksize 参数分块读取处理完一块写一块避免一次性加载到内存。第四个是缓存中间结果。这个前面说过了避免重复计算。这几个手段组合使用处理十万行级别的文件基本能控制在几秒内。5. 踩坑记录与常见问题排查5.1 工具描述写不好AI 就不会用这是我最开始遇到的问题。我写的工具描述是筛选数据结果 AI 经常不知道该传什么参数或者传错参数。后来我把描述改详细了按条件筛选数据行。支持等于、不等于、大于、小于、大于等于、小于等于、包含七种运算符。 column 参数必须是数据中已存在的列名operator 参数从 eq/ne/gt/lt/ge/le/contains 中选择 value 参数是用于比较的值。改完之后AI 调用的准确率明显提升。工具描述就是给 AI 看的说明书写得越清楚AI 用得越准。5.2 数据类型不匹配导致的报错Excel 里的数据类型很混乱。同一列里可能有数字、有文本、有空值。pandas 读进来之后类型可能是 object也可能是 float还可能是 int。我遇到过一个典型问题用户说筛选销售额大于 10000 的记录AI 调用 filter_dataoperator 传 gtvalue 传 10000。但销售额列里有些单元格是空的pandas 读进来变成了 NaN比较的时候直接报错。解决办法是在比较前先做类型转换和空值处理series pd.to_numeric(df[column], errorscoerce) result df[series float(value)]errorscoerce的作用是把无法转换的值变成 NaN这样比较就不会报错NaN 会被自动过滤掉。5.3 中文列名和路径的处理中文列名本身没问题pandas 支持。但中文路径在某些环境下会出问题特别是 Windows 系统。我遇到过一次文件路径里有中文读取时报文件不存在。排查后发现是编码问题Python 默认用系统编码打开文件而系统编码可能不是 UTF-8。解决办法是在代码里显式指定编码或者用 pathlib 处理路径from pathlib import Path file_path Path(D:/数据/销售表.xlsx) df pd.read_excel(file_path)pathlib 会自动处理编码问题比字符串拼接路径靠谱。5.4 常见问题速查表问题现象可能原因解决方法客户端看不到工具服务启动失败手动运行脚本查看报错AI 调用工具报参数错误工具描述不清晰补充参数说明和示例读取文件报不存在路径编码问题用 pathlib 处理路径筛选时报类型错误列中有混合类型用 to_numeric 转换汇总结果为空分组列有 NaN先 dropna 再 groupby写入文件格式乱未设置列宽用 xlsxwriter 设置列宽合并后数据错位各文件列名不一致先统一列名再合并服务响应慢数据量太大分块处理或只读需要的列5.5 几个独家避坑技巧第一个技巧给工具加一个 dry_run 参数。执行前先返回将要做什么让用户确认。这个在批量操作时特别有用避免误操作。第二个技巧限制单次返回的数据量。我一开始让工具返回全部数据结果数据量大时客户端直接卡死。后来改成默认只返回前 100 行需要全部数据时用户明确要求。第三个技巧记录工具调用日志。每次工具被调用把参数和结果写进日志文件。出问题时翻日志比凭记忆排查快得多。第四个技巧给常用操作做快捷工具。比如读取并汇总这个组合操作很常见我单独封装了一个工具一次调用完成两步减少 AI 的决策负担。6. 后续扩展方向6.1 支持更多文件格式目前只支持 xlsx实际工作中还会遇到 xls、csv、甚至 pdf 里的表格。扩展思路是加一个格式判断根据后缀选择不同的读取方式。csv 用 pd.read_csvxls 用 xlrd 引擎pdf 用 pdfplumber 提取表格后再转 DataFrame。6.2 加入图表生成能力数据分析的结果如果能配上图表说服力会强很多。我计划加一个 create_chart 工具接收数据引用和图表类型用 matplotlib 生成图片再嵌入到 Excel 里。这样用户说生成一个按区域的柱状图AI 就能自动完成。6.3 定时任务与自动化现在的流程是用户主动触发。如果加上定时任务就能实现每天早上 9 点自动汇总昨天的数据并发邮件这种自动化场景。实现方式是用 APScheduler 在服务里注册定时任务到点自动执行工具链。6.4 多用户与权限管理如果部署成团队共享的服务就需要考虑多用户隔离。每个用户的缓存要分开敏感操作要加权限校验。这部分可以用会话 ID 来区分用户用配置文件定义权限规则。我在实际使用中最大的体会是MCP 这个东西的价值不在于技术多复杂而在于它改变了人和工具的关系。以前是我适应工具学它的用法、记它的参数、迁就它的限制。现在是工具适应我我用自然语言描述需求工具自己想办法完成。这个转变看起来小但用起来之后真的回不去了。最后分享一个小技巧如果你刚开始做 MCP不要一上来就追求功能全面。先把一个最简单的工具跑通确认整个链路没问题再逐步加功能。我见过太多人卡在环境配置上就放弃了其实只要跑通第一个工具后面的都是复制粘贴加改改参数的事。