ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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

1. 为什么我要自己动手写一个 MCP

1.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 或者 macOS,Python 版本 3.10 以上。如果你还没装 Python,去官网下载安装包,安装的时候记得勾选“Add Python to PATH”,这个选项能省掉后面很多配置环境变量的麻烦。

装好之后,打开终端或命令行,创建一个项目目录:

mkdir excel-mcp-server cd excel-mcp-server

然后创建虚拟环境。这一步很多人会跳过,但我强烈建议不要省。虚拟环境能把项目依赖和系统 Python 隔离开,避免版本冲突:

python -m venv venv

Windows 下激活:

venv\Scripts\activate

macOS 或 Linux 下激活:

source venv/bin/activate

激活之后,命令行前面会出现 (venv) 标识。接下来安装依赖:

pip install mcp openpyxl

mcp 是官方 SDK,openpyxl 是 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( name="read_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(type="text", text=f"文件不存在:{file_path}")] wb = openpyxl.load_workbook(file_path, data_only=True) if sheet_name: if sheet_name not in wb.sheetnames: return [TextContent(type="text", text=f"工作表不存在:{sheet_name}")] ws = wb[sheet_name] else: ws = wb.active rows = [] for i, row in enumerate(ws.iter_rows(values_only=True)): 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(type="text", text=json.dumps(result, ensure_ascii=False))]

这段代码的逻辑很直白:检查文件是否存在、加载工作簿、定位工作表、读取数据、返回结果。几个关键点需要注意。

data_only=True 这个参数很重要。Excel 文件里如果有公式,默认加载会返回公式本身而不是计算结果。加上 data_only=True 之后,openpyxl 会返回公式计算后的值。但有个坑:如果文件从来没有被 Excel 打开过,公式的计算结果可能没有被缓存,data_only=True 会返回 None。这种情况下的解决方案是用 pandas 读一遍触发计算,或者提示用户先用 Excel 打开保存一次。

ensure_ascii=False 是为了让 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_only=True) 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(type="text", text=f"列不存在:{filter_column},可用列:{headers}")] col_idx = headers.index(filter_column) matched = [] for row in ws.iter_rows(min_row=2, values_only=True): if str(row[col_idx]) == str(filter_value): matched.append(list(row)) result = { "筛选条件": f"{filter_column} = {filter_value}", "匹配行数": len(matched), "数据": matched[:50] } return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]

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_only=True) 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_row=2, values_only=True): 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(type="text", text=json.dumps(result, ensure_ascii=False))]

这两个函数的实现都不复杂,但有几个细节值得注意。筛选的时候我把两边都转成字符串再比较,这样即使用户传的是数字 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_ascii=False检查 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 自己去调工具、读数据、算结果、写回去,我只需要在最后确认一下。这种“动嘴不动手”的体验,用过之后就回不去了。

返回列表