首页 / 资讯中心 / 文章详情

从零手写MCP Server:用AI自然语言操控Excel实战

从零手写MCP Server:用AI自然语言操控Excel实战 ★ FEATURED ARTICLE
1. 为什么我要自己动手写一个 MCP1.1 从一次崩溃的 Excel 批量处理说起上个月帮朋友处理一批销售数据二十多个 Excel 文件每个文件里都有七八张表需要按区域拆分、按月份汇总、再把结果回写到新的工作簿里。我一开始用的是最笨的办法——打开文件、复制、粘贴、另存为干到第三个文件的时候手就开始抖了。后来换成 Python 脚本pandas 读进来、groupby 汇总、to_excel 写出去逻辑本身不复杂但问题出在“改需求”上。朋友看了一眼结果说“能不能把华东和华南合并成一个大区另外每个月的表头要加上负责人名字。”就这么两句话我得回去改代码、重新跑、再发给他确认来回折腾了四五轮。这件事让我意识到一个很现实的问题Excel 处理的痛点从来不是“算不出来”而是“需求变来变去”和“操作路径太长”。你写一个脚本本质上是在把一套固定的处理逻辑固化下来但真实业务里表格的结构、口径、输出格式几乎每次都在变。这时候如果有一个东西能让我用自然语言直接指挥它去操作 Excel我只需要说“把华东华南合并表头加负责人”它就能自己完成读取、变换、写回的全过程那效率提升就不是一点半点了。MCP 就是在这个背景下进入我视野的。全称 Model Context Protocol翻译过来叫“模型上下文协议”你可以把它理解成一套让 AI 模型和外部工具之间“对话”的标准接口。以前我们想让 AI 操作 Excel要么把数据贴进对话框让它分析数据量一大就废了要么自己写一堆函数调用让模型去触发每个模型平台的写法还不一样。MCP 做的事情是把“AI 能调用哪些工具、怎么调用、传什么参数、返回什么结果”这件事标准化了。你按照 MCP 的规范写一个 Server任何支持 MCP 的客户端都能直接连上来用不用为每个平台单独适配。1.2 MCP 到底解决了什么问题打个比方。以前的 AI 就像一个坐在办公室里的人你只能隔着门跟他喊话让他帮你算个数、写段文字。他看不到你桌上的文件也碰不到你电脑里的软件。你想让他帮你处理 Excel只能把表格内容念给他听他算完了再念回来给你你再手动填回去。MCP 相当于给这个办公室装了一扇门、一条通道AI 可以顺着通道走到你的工作台前直接拿起 Excel 文件来操作。当然这个“操作”是需要你提前定义好的——你得告诉它哪些工具可以用、怎么用这就是 MCP Server 要干的事。具体到 Excel 场景一个 MCP Server 可以暴露这些能力读取指定路径的 Excel 文件、列出所有工作表、读取某张表的数据、按条件筛选行、按列分组汇总、写入新的工作表、保存文件等等。每个能力就是一个“工具”AI 根据你的自然语言指令自动判断该调用哪个工具、传什么参数。比如你说“把销售数据里华东区的记录筛出来按月份汇总金额”AI 就会先调用“读取文件”工具拿到数据再调用“筛选”工具过滤华东区最后调用“分组汇总”工具按月份统计把结果返回给你。这里有个关键点需要说清楚MCP 本身不负责“理解”你的需求它只负责“执行”。理解需求是 AI 模型的事MCP 提供的是标准化的执行通道。所以你在设计 MCP Server 的时候工具的定义要尽可能清晰——工具名、参数说明、返回值格式这些都会直接影响 AI 能不能正确调用。我踩过的坑后面会详细说。1.3 谁适合跟着这篇内容动手做如果你满足下面任意一条这篇内容就是写给你的经常跟 Excel 打交道受够了重复的复制粘贴和手动汇总想用 AI 来提效有 Python 基础哪怕只是入门水平能看懂函数和基本语法听说过 MCP 但没实际写过想找一个完整的、能跑通的例子来练手已经在用某些 AI 编程工具想把自己的业务逻辑接入进去不需要你懂什么高深的协议设计也不需要你有 AI 模型训练的经验。MCP Server 本质上就是一个普通的程序只不过它遵循了一套特定的接口规范。你把它当成一个“给 AI 用的 API”来写就行。2. 动手之前的整体设计2.1 技术选型为什么是 Python openpyxl做 Excel 处理的 Python 库有好几个pandas、openpyxl、xlrd、xlsxwriter各有各的适用场景。我最终选了 openpyxl 作为核心操作库理由如下。pandas 确实强大读进来就是 DataFrame各种变换一行代码搞定。但它的问题在于“重”——对于简单的读写和格式操作pandas 会把整个文件加载成内存对象处理完再写回去中间如果涉及单元格样式、合并单元格、公式保留这些pandas 处理起来就很别扭。而且 pandas 的写入是全量覆盖你没法只改某一个单元格然后保存。openpyxl 更接近“直接操作 Excel 文件”的感觉。你可以打开一个工作簿定位到某张表的某个单元格读它的值、改它的值、设置它的格式然后保存。对于 MCP 这种需要“精细操作”的场景openpyxl 的控制粒度更合适。当然如果你需要做复杂的数据透视和统计可以在 openpyxl 读取数据之后用 pandas 做计算再把结果写回去。两者结合使用是最务实的方案。MCP Server 的实现方面官方提供了 Python SDK安装之后用装饰器就能定义工具非常方便。通信方式我选的是 stdio标准输入输出这是最简单也最通用的方式——客户端启动 Server 进程通过标准输入输出交换 JSON 消息。不需要额外开端口不需要配置网络本地跑最省事。2.2 工具设计暴露哪些能力给 AI这是整个项目最核心的设计决策。工具设计得好AI 用起来顺手设计得不好AI 要么不知道怎么调要么调了报错体验极差。我的设计原则是工具粒度要适中既不能太粗也不能太细。太粗的话一个工具干太多事参数复杂到 AI 理解不了太细的话完成一个简单任务要调十几个工具效率低还容易出错。最终我定义了六个核心工具工具名功能关键参数read_excel读取 Excel 文件内容文件路径、工作表名、是否包含表头list_sheets列出所有工作表名称文件路径filter_rows按条件筛选行文件路径、工作表名、筛选列、筛选条件group_summary按列分组汇总文件路径、工作表名、分组列、汇总列、汇总方式write_excel写入数据到 Excel文件路径、工作表名、数据、起始单元格get_cell读取指定单元格的值文件路径、工作表名、单元格坐标这六个工具覆盖了“读、查、算、写”四个基本操作。AI 拿到这些工具之后可以根据用户的需求自由组合。比如“统计每个销售员的订单总额”就是先 read_excel 拿到数据再 group_summary 按销售员分组汇总金额。“把结果写到新表里”就是再调一个 write_excel。注意工具的参数命名要尽量用自然语言中常见的词汇。比如用“文件路径”而不是“file_path”用“工作表名”而不是“sheet_name”。虽然 MCP 协议本身对参数名没有限制但 AI 在理解工具描述时中文参数名往往能获得更准确的调用结果。这一点是我实测下来的经验后面会详细展开。2.3 数据流设计从用户指令到 Excel 操作整个数据流是这样的用户在 AI 客户端里输入自然语言指令比如“帮我看看销售数据.xlsx 里有哪些工作表”。客户端把这句话和当前可用的工具列表一起发给 AI 模型。模型分析之后判断需要调用 list_sheets 工具参数是文件路径“销售数据.xlsx”。客户端收到模型的工具调用请求通过 stdio 把请求转发给 MCP Server。Server 执行 list_sheets 函数读取文件返回工作表名称列表。客户端把结果回传给模型模型用自然语言组织成回答“这个文件里有三张表分别是‘一月’、‘二月’、‘三月’。”这个流程里MCP Server 的角色很纯粹——它就是一个执行器收到请求、干活、返回结果。不需要它做任何“智能”的判断那些都交给模型。这种职责分离的好处是Server 的逻辑可以写得很简单、很稳定不容易出 bug。3. 核心细节与实操要点3.1 环境搭建从零开始的完整步骤先把环境搞起来。我假设你用的是 Windows 或者 macOSPython 版本 3.10 以上。如果你还没装 Python去官网下载安装包安装的时候记得勾选“Add Python to PATH”这个选项能省掉后面很多配置环境变量的麻烦。装好之后打开终端或命令行创建一个项目目录mkdir excel-mcp-server cd excel-mcp-server然后创建虚拟环境。这一步很多人会跳过但我强烈建议不要省。虚拟环境能把项目依赖和系统 Python 隔离开避免版本冲突python -m venv venvWindows 下激活venv\Scripts\activatemacOS 或 Linux 下激活source venv/bin/activate激活之后命令行前面会出现 (venv) 标识。接下来安装依赖pip install mcp openpyxlmcp 是官方 SDKopenpyxl 是 Excel 操作库。安装完成后可以跑一个简单的验证import mcp import openpyxl print(ok)没有报错就说明环境没问题。实操心得如果你在国内网络环境下 pip 安装速度慢可以换用国内镜像源。在 pip install 命令后面加上-i https://pypi.tuna.tsinghua.edu.cn/simple就能显著提速。这个技巧在安装大型依赖包的时候特别管用。3.2 工具函数的参数设计让 AI 一次就能调对参数设计是 MCP Server 开发中最容易翻车的地方。我一开始设计的 read_excel 工具参数名叫 file_path、sheet_name、has_header结果 AI 经常把 sheet_name 传成中文的“工作表名”或者把 has_header 传成字符串“true”而不是布尔值。后来我把参数名全部改成中文并且在描述里写清楚类型和示例调用准确率立刻上去了。具体来说每个工具的参数定义要包含这几个要素参数名用中文用业务人员能听懂的词类型明确是字符串、数字还是布尔值是否必填必填参数要标清楚可选参数给默认值描述用一句话说明这个参数是干什么的最好带一个示例值比如 read_excel 的参数定义{ 文件路径: { type: string, description: Excel 文件的完整路径或相对路径例如销售数据.xlsx }, 工作表名: { type: string, description: 要读取的工作表名称不填则默认读取第一张表, default: }, 是否包含表头: { type: boolean, description: 第一行是否为表头默认为 true, default: True } }这样定义之后AI 在调用时就能准确理解每个参数的含义和格式。我实测下来中文参数名 详细描述的组合调用准确率比英文参数名高出不少。3.3 返回值格式AI 能读懂才是好格式工具执行完之后返回什么直接决定了 AI 能不能正确理解结果并组织回答。返回格式的设计原则是结构化、简洁、包含必要上下文。比如 list_sheets 返回工作表列表不要只返回一个数组 [一月, 二月, 三月]而是返回一个带说明的对象{ 文件: 销售数据.xlsx, 工作表数量: 3, 工作表列表: [一月, 二月, 三月] }这样 AI 拿到结果之后能直接说出“销售数据.xlsx 里有 3 张表分别是一月、二月、三月”不需要再做额外的推断。read_excel 返回数据的时候要注意控制数据量。如果一张表有几千行全部返回给 AI 会超出上下文限制。我的做法是默认只返回前 50 行并在返回值里注明“仅显示前 50 行共 XXX 行”。如果 AI 需要更多数据可以再调用一次并指定行数范围。注意返回值的字段名也要用中文和参数名保持一致。这样 AI 在组织回答时能直接复用这些字段名减少理解成本。4. 完整实操流程4.1 编写 MCP Server 主程序新建一个文件 server.py开始写代码。先导入必要的库import json import os from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import openpyxl然后创建 Server 实例app Server(excel-mcp-server)接下来定义工具列表。每个工具需要包含名称、描述和参数定义app.list_tools() async def list_tools(): return [ Tool( nameread_excel, description读取 Excel 文件的内容返回指定工作表的数据, inputSchema{ type: object, properties: { 文件路径: {type: string, description: Excel 文件路径}, 工作表名: {type: string, description: 工作表名称不填则读第一张表}, 最大行数: {type: integer, description: 最多返回多少行默认50, default: 50} }, required: [文件路径] } ), # 其他工具类似定义... ]这里只展示了 read_excel 的定义其他五个工具按照同样的格式补充即可。关键是把 description 写清楚这是 AI 判断该不该调用这个工具的主要依据。4.2 实现核心工具函数工具定义好之后需要实现对应的处理函数。以 read_excel 为例app.call_tool() async def call_tool(name: str, arguments: dict): if name read_excel: return await handle_read_excel(arguments) elif name list_sheets: return await handle_list_sheets(arguments) # 其他工具的分发...handle_read_excel 的具体实现async def handle_read_excel(args): file_path args[文件路径] sheet_name args.get(工作表名, ) max_rows args.get(最大行数, 50) if not os.path.exists(file_path): return [TextContent(typetext, textf文件不存在{file_path})] wb openpyxl.load_workbook(file_path, data_onlyTrue) if sheet_name: if sheet_name not in wb.sheetnames: return [TextContent(typetext, textf工作表不存在{sheet_name})] ws wb[sheet_name] else: ws wb.active rows [] for i, row in enumerate(ws.iter_rows(values_onlyTrue)): if i max_rows: break rows.append(list(row)) total_rows ws.max_row result { 文件: file_path, 工作表: ws.title, 总行数: total_rows, 显示行数: len(rows), 数据: rows } return [TextContent(typetext, textjson.dumps(result, ensure_asciiFalse))]这段代码的逻辑很直白检查文件是否存在、加载工作簿、定位工作表、读取数据、返回结果。几个关键点需要注意。data_onlyTrue 这个参数很重要。Excel 文件里如果有公式默认加载会返回公式本身而不是计算结果。加上 data_onlyTrue 之后openpyxl 会返回公式计算后的值。但有个坑如果文件从来没有被 Excel 打开过公式的计算结果可能没有被缓存data_onlyTrue 会返回 None。这种情况下的解决方案是用 pandas 读一遍触发计算或者提示用户先用 Excel 打开保存一次。ensure_asciiFalse 是为了让 JSON 序列化时保留中文字符。不加这个参数的话中文会被转义成 \uXXXX 的形式虽然 AI 也能理解但可读性差很多。4.3 实现筛选和汇总工具filter_rows 和 group_summary 是使用频率最高的两个工具实现逻辑稍微复杂一点。filter_rows 的核心是按指定列的值筛选行async def handle_filter_rows(args): file_path args[文件路径] sheet_name args.get(工作表名, ) filter_column args[筛选列] filter_value args[筛选值] wb openpyxl.load_workbook(file_path, data_onlyTrue) ws wb[sheet_name] if sheet_name else wb.active headers [cell.value for cell in ws[1]] if filter_column not in headers: return [TextContent(typetext, textf列不存在{filter_column}可用列{headers})] col_idx headers.index(filter_column) matched [] for row in ws.iter_rows(min_row2, values_onlyTrue): if str(row[col_idx]) str(filter_value): matched.append(list(row)) result { 筛选条件: f{filter_column} {filter_value}, 匹配行数: len(matched), 数据: matched[:50] } return [TextContent(typetext, textjson.dumps(result, ensure_asciiFalse))]group_summary 需要做分组和聚合计算async def handle_group_summary(args): file_path args[文件路径] sheet_name args.get(工作表名, ) group_col args[分组列] sum_col args[汇总列] agg_type args.get(汇总方式, 求和) wb openpyxl.load_workbook(file_path, data_onlyTrue) ws wb[sheet_name] if sheet_name else wb.active headers [cell.value for cell in ws[1]] group_idx headers.index(group_col) sum_idx headers.index(sum_col) groups {} for row in ws.iter_rows(min_row2, values_onlyTrue): key row[group_idx] val row[sum_idx] if key is None or val is None: continue if key not in groups: groups[key] [] try: groups[key].append(float(val)) except (ValueError, TypeError): continue result_data [] for key, values in groups.items(): if agg_type 求和: agg_val sum(values) elif agg_type 平均: agg_val sum(values) / len(values) elif agg_type 计数: agg_val len(values) else: agg_val sum(values) result_data.append({group_col: key, f{sum_col}_{agg_type}: round(agg_val, 2)}) result { 分组列: group_col, 汇总列: sum_col, 汇总方式: agg_type, 分组数量: len(result_data), 结果: result_data } return [TextContent(typetext, textjson.dumps(result, ensure_asciiFalse))]这两个函数的实现都不复杂但有几个细节值得注意。筛选的时候我把两边都转成字符串再比较这样即使用户传的是数字 100而表格里存的是文本 100也能匹配上。汇总的时候做了异常捕获遇到非数字的值直接跳过避免整个流程因为一个脏数据就崩掉。4.4 启动 Server 并接入客户端所有工具实现完之后加上启动入口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 的配置。不同客户端的配置方式不一样但核心信息就三个启动命令、脚本路径、工作目录。以常见的配置格式为例{ mcpServers: { excel-server: { command: python, args: [/path/to/server.py], cwd: /path/to/project } } }配置好之后重启客户端在对话里输入“列出销售数据.xlsx 的所有工作表”如果一切正常AI 就会调用 list_sheets 工具并返回结果。实操心得第一次接入的时候建议先用最简单的工具测试比如 list_sheets。确认通信链路通了之后再测试复杂的工具。如果直接上 group_summary 这种复杂工具出了问题不好定位是通信问题还是逻辑问题。5. 常见问题与排查技巧5.1 工具调用失败排查表现象可能原因排查方法AI 不调用工具直接回答工具描述不清晰AI 没理解用途检查 description 是否说明了“什么时候用这个工具”调用工具但参数传错参数名或类型定义不明确在 description 里加示例值明确类型工具执行报错文件路径不对或文件被占用检查路径是否存在文件是否被 Excel 打开返回结果 AI 理解不了返回格式太复杂或字段名不直观简化返回结构字段名用中文中文乱码JSON 序列化没加 ensure_asciiFalse检查 json.dumps 参数5.2 我踩过的三个坑第一个坑文件被 Excel 占用导致写入失败。有一次我让 AI 处理一个 Excel 文件读取没问题但写入的时候一直报 PermissionError。排查了半天才发现那个文件正在 Excel 里打开着。Windows 下 Excel 会锁定文件其他程序没法写入。解决方案是在写入之前先检查文件是否可写如果不可写就提示用户关闭 Excel。或者换一种思路写入到一个新文件而不是原文件这样就不会有占用问题。第二个坑日期格式读出来是数字。Excel 里的日期本质上是一个数字比如 2024-01-15 在底层存储的是 45306。openpyxl 读取的时候如果没有特别处理返回的就是这个数字。AI 拿到 45306 根本不知道这是什么。解决方案是在读取的时候判断单元格的 number_format如果是日期格式就转换成字符串。或者更简单粗暴一点在返回结果之前把所有值都转成字符串日期数字让 AI 自己去推断——但这样体验不好。我最终的做法是在 read_excel 里加了一个日期检测逻辑遇到日期格式的单元格自动转成 YYYY-MM-DD 的字符串。第三个坑大文件读取超时。有一次处理一个 5 万行的 Excel 文件read_excel 读了将近 30 秒客户端那边直接超时断开了。后来我加了行数限制默认只读前 50 行并且在返回值里注明总行数。如果 AI 需要更多数据可以指定读取范围。这样既避免了超时又不会一次性把大量数据塞给 AI。5.3 提升调用准确率的三个技巧技巧一在工具描述里写清楚“什么时候用”。不要只写“读取 Excel 文件”要写“当用户需要查看 Excel 文件内容、获取表格数据时使用此工具”。这样 AI 在判断意图时会更准确。技巧二给参数加示例值。比如“文件路径”的描述写成“Excel 文件的完整路径例如C:/data/销售数据.xlsx”。AI 看到示例之后传参的格式会规范很多。技巧三返回值里带上“下一步建议”。比如 list_sheets 返回之后可以加一个字段“提示可以使用 read_excel 工具读取指定工作表的数据”。这样 AI 在组织回答时会自然地引导用户进行下一步操作体验更流畅。6. 后续可以怎么扩展这套 MCP Server 目前只覆盖了 Excel 处理的基础操作但框架已经搭好了往上加功能并不难。我目前正在做的几个扩展方向供你参考。第一个方向是增加图表生成能力。用 openpyxl 的 chart 模块可以生成柱状图、折线图、饼图。加一个 create_chart 工具AI 就能根据数据自动生成图表并插入到 Excel 里。这个功能在做汇报材料的时候特别实用。第二个方向是支持多文件批量处理。现在的工具都是针对单个文件的但实际工作中经常需要处理一批文件。可以加一个 batch_process 工具接收文件列表和处理规则批量执行。实现上就是循环调用现有的工具函数但要注意控制并发和错误处理。第三个方向是接入更多数据源。除了 Excel还可以支持 CSV、JSON、甚至数据库查询。MCP 的好处就在这里——你只需要按照同样的规范定义工具AI 就能无缝使用。我下一步打算加一个 read_csv 工具这样处理 CSV 文件就不用先转成 Excel 了。第四个方向是加一个“预览”功能。在写入 Excel 之前先返回将要写入的数据让用户确认。这样能避免 AI 理解错误导致写错数据。实现方式是在 write_excel 工具里加一个 dry_run 参数为 true 时只返回数据不实际写入。最后分享一个小技巧如果你想让 AI 更准确地理解你的 Excel 处理需求可以在对话开头先告诉它表格的结构比如“这个文件有三列日期、销售员、金额”。这样 AI 在调用工具时能更准确地选择分组列和汇总列。虽然 MCP 工具本身能读取表头但提前告知能减少一轮工具调用响应更快。这套东西我前后折腾了大概一周从看 MCP 文档到跑通第一个工具再到把六个工具全部实现并调优中间踩了不少坑但最终的效果确实值得。现在处理 Excel 数据我基本就是一句话的事AI 自己去调工具、读数据、算结果、写回去我只需要在最后确认一下。这种“动嘴不动手”的体验用过之后就回不去了。
阅读完成 · 觉得有帮助?
咨询建站