1. 为什么我要自己动手写一个 MCP
1.1 从一次崩溃的 Excel 合并说起
上个月帮朋友处理一批销售数据,二十多个 Excel 文件,每个文件结构还不完全一样,有的表头在第一行,有的在第三行,有的列名是"销售额",有的是"销售金额"。我一开始想着用 Python 写个脚本批量处理,结果光是适配各种格式就写了三百多行代码,跑完还发现有两个文件因为合并单元格导致读取错位。那天晚上我盯着屏幕想,这种活儿为什么不能让 AI 直接帮我干?
后来我接触到了 MCP 这个概念。MCP 全称是 Model Context Protocol,翻译过来叫"模型上下文协议"。你可以把它理解成 AI 和外部工具之间的一套"通用插座标准"。以前 AI 只能跟你聊天,它看不到你电脑里的文件,也没法帮你操作 Excel。但有了 MCP 之后,AI 就能通过这个协议去调用你写好的工具函数,真正地"动手干活"。
这个项目就是我自己从零开发第一个 MCP 服务的完整记录。核心目标很明确:让 AI 能够直接读取、分析、修改 Excel 文件,把原来需要写大量脚本才能完成的工作流,变成一句自然语言指令就能搞定的事情。适合有 Python 基础、想了解 MCP 开发、或者经常被 Excel 折磨的读者参考。哪怕你之前没写过 MCP,跟着走一遍也能跑起来。
1.2 MCP 到底解决了什么痛点
在没有 MCP 之前,想让 AI 处理 Excel 大概有这么几条路。第一条是把数据复制粘贴到对话框里,但 Excel 稍微大一点就贴不进去,而且 AI 看到的只是文本,丢失了行列结构和公式。第二条是让 AI 生成 Python 代码,我自己复制到本地跑,但代码经常有 bug,改来改去效率很低。第三条是用一些现成的自动化平台,但那些平台要么功能受限,要么配置复杂,想定制一个特殊需求非常困难。
MCP 的思路不一样。它把"AI 的推理能力"和"本地工具的执行能力"结合起来。AI 负责理解你的意图、决定调用哪个工具、传什么参数;工具负责真正地读写文件、做计算、生成结果。两者通过 MCP 协议通信,各司其职。
我打个比方。以前的 AI 像一个只能打电话的顾问,你问他问题,他告诉你该怎么做,但具体操作还得你自己来。有了 MCP 之后,AI 变成了一个能亲自上手的助手,你说"帮我把这个月的销售数据按地区汇总一下",它就直接打开文件、读取数据、做汇总、写回结果,全程不需要你干预。
这个项目要做的,就是给 AI 装上"操作 Excel 的手"。具体来说,我会实现几个核心工具:读取 Excel 内容、写入数据到 Excel、按条件筛选和汇总、以及批量处理多个文件。这些工具通过 MCP 协议暴露给 AI,AI 就能根据用户的需求自动调用。
1.3 技术选型背后的考量
为什么用 Python 而不是其他语言?原因很实际。第一,Python 的 openpyxl 和 pandas 这两个库处理 Excel 太方便了,几乎覆盖了所有常见需求。第二,MCP 的官方 SDK 对 Python 支持很好,文档也全。第三,我本身 Python 最熟,遇到问题排查起来快。
为什么用 openpyxl 而不是 pandas 做底层读写?这里有个细节。pandas 适合做数据分析,但它读写 Excel 的时候会丢失一些格式信息,比如单元格颜色、合并单元格、公式等。openpyxl 更底层,能保留这些细节。我的策略是:需要做复杂计算的时候用 pandas,需要保留格式或者做精细操作的时候用 openpyxl。两个库配合使用,各取所长。
MCP 服务本身用什么方式运行?我选择用 stdio 模式,也就是标准输入输出模式。这种模式下,MCP 服务作为一个子进程运行,通过标准输入输出和 AI 客户端通信。好处是不需要额外开端口,配置简单,安全性也好。另一种方式是 SSE 模式,通过 HTTP 通信,适合远程调用场景,但配置起来麻烦一些。对于个人使用来说,stdio 模式足够了。
2. 环境搭建与 MCP 服务骨架
2.1 Python 环境准备与依赖安装
先说 Python 版本。我建议用 3.10 或以上,因为 MCP 的 Python SDK 用了一些较新的语法特性。如果你还在用 3.7 或者 3.8,建议升级一下。安装 Python 本身就不展开了,网上教程很多,注意安装的时候勾选"Add Python to PATH"就行。
依赖方面,核心就三个包。第一个是 mcp,这是 MCP 的官方 SDK。第二个是 openpyxl,用来读写 Excel 文件。第三个是 pandas,用来做数据处理。安装命令很简单:
pip install mcp openpyxl pandas如果你国内下载慢,可以加个镜像源:
pip install mcp openpyxl pandas -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后验证一下。打开命令行,输入 python 进入交互环境,然后依次输入:
import mcp import openpyxl import pandas print(mcp.__version__)如果没有报错,说明环境没问题。这里有个小坑要注意:有些系统里同时装了 Python 2 和 Python 3,pip 命令可能指向的是 Python 2 的 pip。保险起见用 pip3 代替 pip,或者用 python -m pip install 的方式安装。
2.2 MCP 服务的核心结构拆解
一个 MCP 服务本质上就是一个程序,它做三件事:声明自己有哪些工具、监听 AI 发来的调用请求、执行工具并返回结果。听起来复杂,但 SDK 已经帮我们封装好了大部分逻辑。
核心对象是 Server。创建的时候需要传一个名字,这个名字会显示在 AI 客户端的工具列表里。然后我们用装饰器来注册工具。比如:
from mcp.server import Server from mcp.server.stdio import stdio_server app = Server("excel-mcp") @app.list_tools() async def list_tools(): return [ Tool( name="read_excel", description="读取 Excel 文件内容", inputSchema={...} ) ] @app.call_tool() async def call_tool(name, arguments): if name == "read_excel": return await handle_read_excel(arguments)这里有两个关键点。list_tools 告诉 AI"我有哪些工具可用",call_tool 负责实际执行。inputSchema 用 JSON Schema 格式描述参数,AI 会根据这个描述来决定传什么参数。description 字段很重要,写得越清楚,AI 越能正确使用你的工具。
2.3 工具函数的参数设计原则
设计工具参数的时候,我踩过几个坑。第一个坑是参数太多。一开始我想做一个"万能 Excel 工具",参数包括文件路径、工作表名、起始行、结束行、起始列、结束列、筛选条件、排序方式……结果 AI 经常传错参数,因为它记不住这么多选项。
后来我改成拆分成多个小工具,每个工具只做一件事。read_excel 只负责读取,write_excel 只负责写入,filter_excel 只负责筛选。每个工具的参数控制在三到五个,AI 用起来准确率明显提高。
第二个坑是参数类型不明确。比如"行号"这个参数,如果我只写 type: integer,AI 可能传 0 也可能传 1,因为不同编程语言的行号起始不一样。我在 description 里明确写了"行号从 1 开始计数",问题就解决了。
第三个坑是文件路径。AI 不知道你的文件在哪,所以路径必须由用户提供。但用户可能只给一个文件名,比如"销售数据.xlsx",这时候需要工具自己去当前工作目录找。我在代码里做了处理:如果路径是相对路径,就基于当前工作目录解析;如果是绝对路径,直接用。
3. 核心工具的实现细节
3.1 读取 Excel:兼容各种奇葩格式
读取 Excel 看起来简单,实际上是最容易出问题的环节。我遇到过的情况包括:表头不在第一行、有合并单元格、有空行、有隐藏列、日期格式不统一、数字被存成文本等等。
我的 read_excel 工具设计成这样:接收文件路径、工作表名(可选,默认第一个)、是否把第一行当表头(可选,默认是)。返回一个结构化的数据,包含列名和数据行。
import openpyxl def read_excel(file_path, sheet_name=None, header_row=1): wb = openpyxl.load_workbook(file_path, data_only=True) if sheet_name: ws = wb[sheet_name] else: ws = wb.active rows = list(ws.iter_rows(values_only=True)) if not rows: return {"columns": [], "data": []} header = rows[header_row - 1] data = rows[header_row:] columns = [str(c) if c is not None else f"col_{i}" for i, c in enumerate(header)] result = [] for row in data: if all(c is None for c in row): continue result.append(dict(zip(columns, row))) return {"columns": columns, "data": result}这里有几个细节值得说。data_only=True 这个参数很关键,它让 openpyxl 读取公式的计算结果而不是公式本身。如果你需要读公式,就去掉这个参数。header_row 参数让用户可以指定表头在第几行,解决表头不在第一行的问题。空行过滤是必须的,否则 AI 会看到一堆空数据。
还有一个坑是日期。Excel 里的日期存储的是数字,openpyxl 读出来可能是 datetime 对象,也可能是数字。我在返回之前做了统一处理,把 datetime 转成字符串格式,避免 AI 看不懂。
3.2 写入 Excel:保留原有格式的技巧
写入比读取更麻烦,因为你要考虑是覆盖还是追加、要不要保留原有格式、新数据从哪一行开始写。
我的 write_excel 工具支持两种模式:覆盖模式和追加模式。覆盖模式会清空目标工作表再写入,追加模式会在已有数据后面接着写。
def write_excel(file_path, data, sheet_name=None, mode="append"): if os.path.exists(file_path): wb = openpyxl.load_workbook(file_path) else: wb = openpyxl.Workbook() if sheet_name and sheet_name in wb.sheetnames: ws = wb[sheet_name] elif sheet_name: ws = wb.create_sheet(sheet_name) else: ws = wb.active if mode == "overwrite": for row in ws.iter_rows(): for cell in row: cell.value = None start_row = 1 else: start_row = ws.max_row + 1 if isinstance(data, list) and data and isinstance(data[0], dict): headers = list(data[0].keys()) if start_row == 1: for col, h in enumerate(headers, 1): ws.cell(row=1, column=col, value=h) start_row = 2 for i, record in enumerate(data): for col, h in enumerate(headers, 1): ws.cell(row=start_row + i, column=col, value=record.get(h)) wb.save(file_path) return {"status": "ok", "rows_written": len(data)}覆盖模式我没有直接删除工作表重建,而是逐单元格清空值。这样做的好处是保留了原有的列宽、行高、颜色等格式。如果你直接删了重建,格式就全没了。
追加模式用 ws.max_row + 1 来确定起始行。但这里有个坑:如果工作表里有空行,max_row 可能比实际数据行大。更稳妥的做法是从下往上找第一个非空行。不过对于大多数场景,max_row 够用了。
3.3 筛选与汇总:让 AI 做数据分析
这个工具是我用得最多的。它的作用是:给定一个 Excel 文件和一个筛选条件,返回符合条件的数据,并且可以按某个字段做汇总。
def filter_excel(file_path, sheet_name=None, filter_field=None, filter_value=None, group_by=None, sum_field=None): result = read_excel(file_path, sheet_name) data = result["data"] if filter_field and filter_value is not None: data = [row for row in data if str(row.get(filter_field, "")) == str(filter_value)] if group_by and sum_field: groups = {} for row in data: key = row.get(group_by, "未知") value = row.get(sum_field, 0) try: value = float(value) except (ValueError, TypeError): value = 0 groups[key] = groups.get(key, 0) + value return { "group_by": group_by, "sum_field": sum_field, "result": [{"key": k, "sum": v} for k, v in groups.items()] } return {"count": len(data), "data": data}这个工具的设计思路是"一个工具覆盖多种分析需求"。通过参数组合,AI 可以实现:只看某个地区的销售数据(filter)、按产品类别汇总销售额(group_by + sum_field)、或者两者结合。
数值转换那里我加了 try-except,因为 Excel 里的数字有时候是文本格式,直接相加会变成字符串拼接。这个坑我踩过,当时汇总结果出来是"100200300"而不是"600",排查了半天才发现是类型问题。
3.4 批量处理:一次搞定多个文件
批量处理工具接收一个文件夹路径和一个操作类型,对文件夹里所有 Excel 文件执行同样的操作。比如"把这个文件夹里所有 Excel 文件的第一个工作表合并成一个"。
def batch_process(folder_path, operation="merge", output_file="merged.xlsx"): files = [f for f in os.listdir(folder_path) if f.endswith((".xlsx", ".xls"))] if operation == "merge": all_data = [] for f in files: path = os.path.join(folder_path, f) result = read_excel(path) for row in result["data"]: row["_source_file"] = f all_data.append(row) if all_data: write_excel(output_file, all_data, mode="overwrite") return { "files_processed": len(files), "total_rows": len(all_data), "output": output_file }批量处理最容易出问题的地方是文件格式不一致。有的文件表头在第一行,有的在第三行;有的列名是"金额",有的是"销售额"。我在实际使用中的做法是:先让 AI 读取每个文件的表头,然后根据表头做列名映射,再合并。这个逻辑比较复杂,我把它拆成了两步:先调用 read_excel 看结构,再决定怎么合并。
4. 把 MCP 服务接入 AI 客户端
4.1 配置文件怎么写
MCP 服务写好了,怎么让 AI 客户端知道它的存在?答案是配置文件。不同的客户端配置方式略有不同,但核心逻辑是一样的:告诉客户端"有一个 MCP 服务,用这个命令启动它"。
以常见的配置文件格式为例:
{ "mcpServers": { "excel-mcp": { "command": "python", "args": ["/path/to/excel_mcp_server.py"], "env": {} } } }command 是启动命令,args 是参数。如果你的 Python 不在系统 PATH 里,command 要写完整路径,比如 /usr/bin/python3 或者 C:\Python310\python.exe。
这里有个常见的坑:路径里有空格。比如 Windows 下 Python 装在 "C:\Program Files\Python310\python.exe",路径里有空格,JSON 里直接写没问题,但有些客户端解析的时候会出问题。解决办法是用短路径,或者把 Python 装到没有空格的目录。
4.2 调试 MCP 服务的实用方法
MCP 服务不像普通程序那样可以直接看到输出,调试起来有点麻烦。我总结了几个实用的调试方法。
第一个方法是用 MCP Inspector。这是官方提供的一个调试工具,可以模拟 AI 客户端向你的服务发请求,看到完整的请求和响应内容。安装命令是 npx @modelcontextprotocol/inspector,然后按提示操作就行。
第二个方法是在代码里写日志。因为 stdio 模式下标准输出被 MCP 协议占用了,print 的内容会干扰通信。所以日志要写到文件里:
import logging logging.basicConfig( filename="mcp_debug.log", level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" )然后在关键位置加 logging.info 或 logging.error,出问题的时候看日志文件。
第三个方法是单独测试工具函数。把工具函数写成独立的、不依赖 MCP 框架的普通函数,然后用 pytest 或者直接写个测试脚本调用。这样能快速定位是工具逻辑的问题还是 MCP 通信的问题。
4.3 让 AI 正确调用工具的提示词技巧
工具写好了,但 AI 不一定知道什么时候该用。这时候需要在工具的 description 里下功夫。
我一开始写的 description 是"读取 Excel 文件",结果 AI 经常在该用 filter_excel 的时候用了 read_excel。后来我改成"读取 Excel 文件的原始内容,返回所有行。如果需要筛选或汇总,请使用 filter_excel 工具",AI 的选择就准确多了。
另一个技巧是在 description 里写清楚使用场景。比如 filter_excel 的 description 我写成:"对 Excel 数据做筛选和汇总。当用户说'统计'、'汇总'、'按XX分组'、'筛选出XX'时使用此工具。支持按字段筛选、按字段分组求和。"
还有一个技巧是参数描述要具体。比如 filter_value 参数,我写的是"筛选值,支持字符串和数字。注意:如果 Excel 里的数字是文本格式,这里也要传字符串"。这样 AI 就知道要做类型匹配。
5. 实战:用 MCP 重构 Excel 工作流
5.1 场景一:多文件销售数据合并与汇总
这是我最初想解决的问题。二十多个销售文件,每个文件一个地区,需要合并成一张总表,然后按产品类别汇总销售额。
以前的做法是写 Python 脚本,大概长这样:
import pandas as pd import os all_data = [] for f in os.listdir("sales"): if f.endswith(".xlsx"): df = pd.read_excel(f"sales/{f}") df["地区"] = f.replace(".xlsx", "") all_data.append(df) merged = pd.concat(all_data) summary = merged.groupby("产品类别")["销售额"].sum() summary.to_excel("汇总.xlsx")这段代码看起来简单,但实际跑起来问题很多。有的文件表头不在第一行,有的列名不一样,有的销售额是文本格式。每次遇到新问题就要改代码,改完还要重新跑。
用 MCP 之后,流程变成了对话。我先让 AI 读取一个文件看看结构:
用户:帮我看看 sales 文件夹里第一个 Excel 文件的结构
AI:调用 read_excel 工具,返回列名和前三行数据
然后根据返回结果,我告诉 AI 怎么处理:
用户:这个文件夹里所有文件结构类似,请把它们合并,按"产品类别"汇总"销售额",结果写到 summary.xlsx
AI 会自动调用 batch_process 合并文件,再调用 filter_excel 做汇总,最后调用 write_excel 写结果。整个过程不需要我写一行代码。
如果某个文件结构不一样,AI 会在读取的时候发现,然后问我怎么处理。比如它会说"文件 B 的销售额列名是'销售金额'而不是'销售额',是否按相同含义处理?"我确认一下就行。
5.2 场景二:按条件筛选并生成新报表
第二个场景是定期报表。每周要从原始数据里筛选出特定条件的数据,生成一份新报表。
以前的做法是打开 Excel,用筛选功能手动操作,或者写个脚本。手动操作容易出错,脚本每次需求变了就要改。
用 MCP 之后,我只需要说:
用户:从 data.xlsx 里筛选出"地区"是"华东"且"销售额"大于 10000 的记录,按"销售员"分组汇总销售额,结果写到 report.xlsx
AI 会先调用 read_excel 读取数据,然后在内存里做筛选和汇总,最后调用 write_excel 写入结果。如果数据量大,AI 可能会分步做:先筛选,再汇总,再写入。
这里有个细节:AI 做筛选的时候,如果"销售额"列是文本格式,比较大小会出问题。我在 filter_excel 工具里做了类型转换,尝试把值转成数字再比较。如果转换失败,就按字符串比较。这样兼容性更好。
5.3 场景三:Excel 数据清洗与格式统一
第三个场景是数据清洗。原始数据经常有各种问题:日期格式不统一、电话号码有横杠、姓名前后有空格、空值用各种方式表示(空字符串、N/A、null、-)。
以前做数据清洗要写一堆正则和判断逻辑。现在我可以让 AI 帮我做:
用户:清洗 data.xlsx 里的数据。日期统一成 YYYY-MM-DD 格式,电话号码去掉横杠,姓名去掉前后空格,空值统一填"未知"。结果写到 cleaned.xlsx
AI 会读取数据,逐条处理,然后写入新文件。处理逻辑 AI 自己生成,不需要我写代码。如果清洗规则复杂,我可以分步说,AI 会记住上下文。
这里要注意的是,AI 处理大量数据的时候可能会慢,因为它是逐条处理的。如果数据超过几千行,建议先用 pandas 做批量处理,或者把清洗逻辑写成工具函数让 AI 调用。我的做法是写了一个 clean_excel 工具,内置了常见的清洗规则,AI 只需要传参数就行。
6. 踩坑记录与排查技巧
6.1 工具调用失败的五种常见原因
第一种:文件路径错误。AI 不知道你的文件在哪,如果路径写错了,工具会报"文件不存在"。排查方法是先用绝对路径测试,确认没问题再用相对路径。
第二种:权限问题。如果 Excel 文件正在被其他程序打开,写入会失败。排查方法是关闭所有打开该文件的程序,或者用只读模式读取。
第三种:依赖缺失。如果换了电脑,可能没装 openpyxl 或 pandas。排查方法是运行 pip list 看看包在不在。
第四种:参数类型不匹配。比如工具期望整数,AI 传了字符串。排查方法是在工具函数里加类型检查和转换。
第五种:MCP 服务没启动。如果配置文件写错了,AI 客户端根本连不上服务。排查方法是看客户端的日志,确认服务是否成功启动。
6.2 中文乱码与编码问题
中文乱码是 Excel 处理里的经典问题。我遇到过几种情况。
第一种是读取的时候乱码。这通常是文件本身编码问题,openpyxl 一般能正确处理,但如果文件是 CSV 改后缀成 xlsx 的,就可能乱码。解决办法是用 pandas 读取,指定 encoding 参数。
第二种是写入的时候乱码。这通常是 Python 默认编码不是 UTF-8 导致的。解决办法是在文件开头加:
import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')第三种是 MCP 通信的时候乱码。stdio 模式下,如果编码不一致,中文会变成乱码。解决办法是在启动 MCP 服务的时候设置环境变量 PYTHONIOENCODING=utf-8。
6.3 大数据量下的性能优化
Excel 文件超过一万行的时候,处理速度会明显变慢。我总结了几个优化技巧。
第一个技巧是用 read_only 模式读取。openpyxl 的 load_workbook 有个 read_only 参数,设为 True 的时候读取速度能快好几倍,但代价是不能修改文件。只读场景下用这个模式很合适。
wb = openpyxl.load_workbook(file_path, read_only=True, data_only=True)第二个技巧是批量写入。不要一行一行地 ws.append,而是攒一批数据用一次写入。openpyxl 的写入性能瓶颈在保存文件,写入操作本身很快。
第三个技巧是用 pandas 做计算。pandas 的底层是 numpy,做筛选、分组、汇总比纯 Python 循环快得多。我的做法是:读取用 openpyxl,计算用 pandas,写入用 openpyxl。
第四个技巧是分块处理。如果文件特别大,可以分块读取,处理完一块写一块,避免内存溢出。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| AI 说找不到工具 | MCP 服务没启动或配置错误 | 检查配置文件路径和命令 |
| 读取的数据为空 | 表头行设置错误 | 调整 header_row 参数 |
| 数字变成文本 | Excel 单元格格式问题 | 在工具里做类型转换 |
| 写入后格式丢失 | 覆盖模式清空了格式 | 改用追加模式或保留格式 |
| 中文显示乱码 | 编码不一致 | 设置 UTF-8 编码 |
| 处理速度慢 | 数据量大或逐行处理 | 用 read_only 模式或 pandas |
| 日期显示为数字 | 未做日期转换 | 在工具里格式化日期 |
| 合并单元格读取错位 | openpyxl 对合并单元格的处理 | 用 pandas 读取或手动处理 |
7. 后续可以怎么扩展
这个 MCP 服务目前只覆盖了 Excel 处理的基础功能。如果你想继续扩展,有几个方向可以考虑。
第一个方向是支持更多文件格式。除了 xlsx,还可以支持 csv、xls、甚至 Google Sheets。核心逻辑类似,只是读写库不一样。
第二个方向是增加图表生成。用 openpyxl 的 chart 模块可以生成柱状图、折线图、饼图。AI 可以根据数据自动选择合适的图表类型。
第三个方向是接入数据库。把 Excel 数据导入数据库,或者从数据库导出到 Excel。这样就能处理更大规模的数据。
第四个方向是做定时任务。让 MCP 服务定时执行某些操作,比如每天早上自动汇总前一天的数据并生成报表。
第五个方向是增加数据校验。在写入之前检查数据是否符合规则,比如必填字段不能为空、数值范围是否合理、日期格式是否正确。
我自己在实际使用中的体会是,MCP 最大的价值不是替代 Python 脚本,而是把"写脚本"这个门槛降低了。以前遇到一个新的 Excel 处理需求,我要想怎么写代码、调试、改 bug。现在我可以直接用自然语言描述需求,AI 帮我调用工具完成。对于不常写代码的人来说,这个效率提升是巨大的。
最后分享一个小技巧:在工具的 description 里多写一些使用示例,AI 的调用准确率会明显提高。比如 filter_excel 的 description 里我写了"示例:按地区筛选传 filter_field='地区', filter_value='华东';按产品汇总传 group_by='产品类别', sum_field='销售额'"。加了示例之后,AI 几乎不会传错参数了。