1. 为什么我要自己动手写一个 MCP
1.1 从一次崩溃的 Excel 处理经历说起
上个月帮朋友处理一份销售数据,三千多行,需要按区域拆分、按品类汇总、再生成一份带图表的周报。我当时的做法很原始:打开 Excel,手动筛选,复制粘贴到新表,写几个 SUMIF 公式,再插入柱状图。整个过程花了将近两个小时,中间还因为一次误操作把公式引用搞乱了,又回头重做了一遍。
做完之后我就在想,这种活儿明明有规律可循,为什么不能让 AI 直接帮我干?我试过把数据丢给一些在线的 AI 工具,但问题很明显:数据要上传,格式经常被搞乱,而且每次都要重新描述需求。更关键的是,这些工具跟我的本地文件系统是割裂的,它们不知道我的文件放在哪,也没法直接操作我的 Excel 文件。
后来我接触到了 MCP 这个概念。MCP 全称是 Model Context Protocol,翻译过来叫“模型上下文协议”。你可以把它理解成一套标准接口,让 AI 模型能够跟外部的工具、数据源、服务进行交互。打个比方,以前的 AI 就像一个只会聊天的朋友,你问他什么他答什么,但他没法帮你动手做事。有了 MCP 之后,这个朋友突然长出了手,能帮你打开文件、读写数据、调用程序。
我决定自己写一个 MCP,专门用来处理 Excel。目标很明确:让 AI 能够直接读取我本地的 Excel 文件,理解表格结构,执行筛选、汇总、拆分、写入等操作,最后把结果保存成新的文件。整个过程不需要我手动干预,也不需要把数据传到任何云端。
1.2 MCP 到底解决了什么问题
在动手之前,我需要先搞清楚 MCP 的核心价值。传统的 AI 应用开发模式是:你写一个程序,在里面调用 AI 的 API,把数据传过去,拿到结果再处理。这种模式的问题在于,AI 和你的本地环境是隔离的。你想让 AI 操作一个 Excel 文件,就得先把文件内容读出来,转成文本,传给 AI,AI 返回结果,你再写回文件。每一步都要你自己写代码衔接。
MCP 的思路不一样。它定义了一套协议,AI 模型可以通过这套协议主动调用你提供的工具。你只需要把“读 Excel”“写 Excel”“筛选数据”这些能力注册成 MCP 工具,AI 就能在需要的时候自己调用。这就像你给 AI 配了一个工具箱,它需要什么就拿什么,而不是你每次都要帮它递工具。
对于 Excel 处理这个场景来说,MCP 的优势特别明显。Excel 文件的结构很复杂,有工作表、单元格、公式、格式、图表等等。如果每次都要把整个文件转成文本传给 AI,信息损失会很大,而且 token 消耗也高。通过 MCP,AI 可以按需读取特定区域的数据,执行特定操作,效率高得多。
还有一个很重要的点:MCP 是本地运行的。你的数据不需要离开你的电脑,AI 只是通过协议调用你本地的工具。这对于处理敏感数据来说,是一个很大的优势。
1.3 技术选型:为什么用 Python
确定了要做 MCP 之后,下一个问题是用什么语言。我考虑了三个选项:Python、Node.js、Go。
Node.js 的生态里有很多 MCP 相关的库,社区活跃度也高。Go 的性能好,编译出来是单个二进制文件,部署方便。但最终我还是选了 Python,原因有几个。
第一,Python 处理 Excel 的库非常成熟。openpyxl可以读写 xlsx 文件,pandas可以做数据分析和转换,xlrd可以读旧版 xls 文件。这些库我都很熟悉,用起来顺手。如果用 Node.js,虽然也有exceljs这样的库,但功能和生态还是差一些。
第二,Python 的 MCP SDK 已经比较完善了。官方提供了mcp包,可以很方便地定义工具、处理请求、返回结果。而且 Python 的异步编程模型(asyncio)跟 MCP 的通信机制很契合。
第三,Python 的调试和迭代速度快。写 MCP 的过程中需要不断测试工具是否正常工作,Python 的动态特性让这个过程很流畅。我可以直接在 REPL 里测试某个函数,确认没问题再注册成 MCP 工具。
当然,Python 也有缺点。打包成可执行文件比较麻烦,性能不如 Go。但对于个人使用来说,这些都不是大问题。我只需要在自己的电脑上运行,用uv或者pip管理依赖就够了。
1.4 整体架构设计
在写代码之前,我先画了一个简单的架构图。整个系统分为三层:
最底层是 Excel 操作层,封装了openpyxl和pandas的常用操作,比如读取工作表、获取单元格范围、筛选行、分组汇总、写入新文件等。这一层是纯 Python 函数,不涉及 MCP 协议。
中间层是 MCP 工具层,把 Excel 操作层的函数注册成 MCP 工具。每个工具都有名称、描述、参数定义和返回值。AI 模型通过 MCP 协议调用这些工具,传入参数,拿到结果。
最上层是 MCP 服务器,负责处理与 AI 客户端的通信。它监听标准输入输出或者网络端口,接收请求,路由到对应的工具,返回结果。
这种分层设计的好处是,Excel 操作层的代码可以独立测试,不依赖 MCP 协议。如果以后想换一个协议或者换一个 AI 客户端,只需要改中间层和上层,底层的逻辑不用动。
2. 环境准备与核心依赖安装
2.1 Python 环境配置的坑
在开始写代码之前,我需要先确保 Python 环境是干净的。我见过太多人因为环境混乱导致各种奇怪的问题,所以这一步我特别小心。
首先确认 Python 版本。MCP 的 Python SDK 要求 Python 3.10 以上,我本地装的是 3.11,没问题。如果你还在用 3.8 或者 3.9,建议先升级。可以用python --version查看当前版本。
接下来是虚拟环境。我强烈建议为这个项目单独创建一个虚拟环境,不要跟系统环境或者其他项目混在一起。我用的工具是uv,它比传统的venv快很多,而且依赖解析更智能。安装uv的命令很简单:
pip install uv然后创建虚拟环境:
uv venv mcp-excel-env激活虚拟环境:
# Windows mcp-excel-env\Scripts\activate # macOS/Linux source mcp-excel-env/bin/activate如果你不想用uv,用标准的venv也可以:
python -m venv mcp-excel-env激活之后,安装核心依赖:
uv pip install mcp openpyxl pandas这里解释一下每个包的作用。mcp是官方 SDK,提供了 MCP 服务器的基类和工具注册的装饰器。openpyxl用来读写 xlsx 文件,支持公式、格式、图表等高级功能。pandas用来做数据处理,比如筛选、分组、透视表等。
注意:
openpyxl只能处理 xlsx 文件,不能处理旧的 xls 格式。如果你需要处理 xls,需要额外安装xlrd。但xlrd从 2.0 版本开始只支持 xls,不支持 xlsx,所以两个库要配合使用。
2.2 MCP SDK 的核心概念
安装完依赖之后,我需要理解 MCP SDK 的几个核心概念。
第一个是Server类。这是 MCP 服务器的入口,你需要创建一个 Server 实例,然后注册工具和资源。Server 负责处理客户端的连接、请求路由、错误处理等。
第二个是Tool。工具是 MCP 的核心概念,代表一个可以被 AI 调用的功能。每个工具都有名称、描述、输入参数的 JSON Schema、以及一个处理函数。当 AI 决定调用某个工具时,它会传入符合 Schema 的参数,你的处理函数执行操作并返回结果。
第三个是Resource。资源代表可以被 AI 读取的数据,比如文件内容、数据库记录等。资源跟工具的区别在于,资源是只读的,工具是可以执行操作的。对于 Excel 处理来说,我主要用工具,因为需要执行读写操作。
第四个是stdio传输。MCP 支持多种传输方式,最常用的是标准输入输出(stdio)。AI 客户端启动你的 MCP 服务器进程,通过 stdin 发送请求,通过 stdout 接收响应。这种方式简单可靠,不需要网络配置。
理解了这些概念之后,我就可以开始写代码了。
2.3 项目目录结构
我习惯在写代码之前先把目录结构规划好。这个项目的结构很简单:
mcp-excel/ ├── server.py # MCP 服务器入口 ├── excel_tools.py # Excel 操作函数 ├── requirements.txt # 依赖列表 └── test_data/ # 测试用的 Excel 文件server.py是主入口,负责创建 MCP 服务器、注册工具、启动服务。excel_tools.py放具体的 Excel 操作函数,比如读取、筛选、写入等。test_data目录放一些测试文件,方便调试。
这种拆分方式的好处是,Excel 操作逻辑跟 MCP 协议解耦。我可以单独测试excel_tools.py里的函数,确认没问题之后再注册到 MCP 服务器。
3. 核心工具函数的实现细节
3.1 读取 Excel 文件:不只是打开那么简单
第一个要实现的工具是读取 Excel 文件。看起来很简单,用openpyxl.load_workbook就能打开,但实际上有很多细节需要考虑。
首先是文件路径的处理。AI 传入的路径可能是相对路径,也可能是绝对路径。我需要统一转换成绝对路径,并且检查文件是否存在。如果文件不存在,要返回一个清晰的错误信息,而不是让程序崩溃。
其次是工作表的选择。一个 Excel 文件可能有多个工作表,AI 需要指定读取哪一个。如果 AI 没有指定,默认读取第一个工作表。我还可以提供一个工具,让 AI 先列出所有工作表的名称,然后再决定读取哪一个。
第三是数据范围的确定。Excel 表格的数据可能从 A1 开始,也可能从其他位置开始。我需要自动检测数据的边界,或者让 AI 指定范围。自动检测的逻辑是:找到第一个非空单元格作为左上角,找到最后一个非空单元格作为右下角。
第四是数据类型的处理。Excel 单元格里可能是字符串、数字、日期、公式等。openpyxl读取公式单元格时,默认返回公式本身而不是计算结果。如果需要计算结果,要用data_only=True参数。但要注意,如果文件没有被 Excel 打开过并保存,data_only=True可能返回 None。
下面是我实现的读取函数的核心逻辑:
import openpyxl from pathlib import Path def read_excel(file_path: str, sheet_name: str = None, start_cell: str = None, end_cell: str = None, data_only: bool = True) -> dict: """ 读取 Excel 文件内容,返回二维数组和元数据。 """ path = Path(file_path).resolve() if not path.exists(): return {"error": f"文件不存在: {path}"} wb = openpyxl.load_workbook(path, data_only=data_only) if sheet_name: if sheet_name not in wb.sheetnames: return {"error": f"工作表不存在: {sheet_name}", "available_sheets": wb.sheetnames} ws = wb[sheet_name] else: ws = wb.active # 自动检测数据范围 if start_cell and end_cell: min_row, min_col = openpyxl.utils.cell.coordinate_to_tuple(start_cell) max_row, max_col = openpyxl.utils.cell.coordinate_to_tuple(end_cell) else: min_row, min_col = ws.min_row, ws.min_column max_row, max_col = ws.max_row, ws.max_column data = [] for row in ws.iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col, values_only=True): data.append(list(row)) return { "file": str(path), "sheet": ws.title, "sheets": wb.sheetnames, "range": f"{openpyxl.utils.cell.get_column_letter(min_col)}{min_row}:" f"{openpyxl.utils.cell.get_column_letter(max_col)}{max_row}", "rows": len(data), "cols": len(data[0]) if data else 0, "data": data }这个函数返回的data是一个二维数组,第一行通常是表头。AI 拿到这个数组之后,就可以理解表格的结构,决定下一步操作。
实操心得:
data_only=True这个参数很关键。如果你的 Excel 文件里有公式,而且你希望 AI 看到的是计算结果而不是公式本身,一定要加上这个参数。但前提是这个文件被 Excel 打开过并保存过,否则公式的计算结果不会被缓存,data_only=True会返回 None。
3.2 筛选与查询:让 AI 理解你的数据
读取数据只是第一步,更重要的是让 AI 能够筛选和查询数据。比如“找出销售额大于 10000 的记录”“统计每个区域的订单数量”“筛选出日期在 2024 年 1 月之后的记录”。
这些操作如果让 AI 直接处理二维数组,效率很低,而且容易出错。更好的做法是提供专门的筛选工具,让 AI 传入筛选条件,我来执行。
我设计了一个通用的筛选函数,支持多种条件:
def filter_excel(file_path: str, sheet_name: str = None, conditions: list = None, columns: list = None) -> dict: """ 根据条件筛选 Excel 数据。 conditions: [{"column": "销售额", "op": ">", "value": 10000}, ...] columns: 需要返回的列名列表,None 表示返回所有列 """ result = read_excel(file_path, sheet_name) if "error" in result: return result data = result["data"] if not data: return {"error": "表格为空"} headers = data[0] rows = data[1:] # 构建列名到索引的映射 col_idx = {name: i for i, name in enumerate(headers)} # 应用筛选条件 filtered = [] for row in rows: match = True for cond in conditions or []: col_name = cond["column"] if col_name not in col_idx: return {"error": f"列不存在: {col_name}", "available_columns": headers} idx = col_idx[col_name] cell_value = row[idx] op = cond["op"] target = cond["value"] if op == ">": match = match and (cell_value is not None and cell_value > target) elif op == "<": match = match and (cell_value is not None and cell_value < target) elif op == "==": match = match and (cell_value == target) elif op == "!=": match = match and (cell_value != target) elif op == "contains": match = match and (target in str(cell_value)) elif op == "in": match = match and (cell_value in target) if not match: break if match: filtered.append(row) # 选择返回的列 if columns: indices = [col_idx[c] for c in columns if c in col_idx] filtered = [[row[i] for i in indices] for row in filtered] headers = [headers[i] for i in indices] return { "headers": headers, "rows": filtered, "count": len(filtered), "total": len(rows) }这个函数支持>、<、==、!=、contains、in六种操作符,基本覆盖了常见的筛选需求。AI 只需要把用户的自然语言转换成这些条件,就能完成筛选。
注意事项:数值比较的时候要小心类型问题。Excel 里的数字可能是 int、float,也可能是字符串形式的数字。如果直接比较,可能会出错。我在实际使用中加了一个类型转换的逻辑,如果两边都是数字类型或者可以转成数字,就先转换再比较。
3.3 分组汇总:Excel 最常用的功能之一
分组汇总是 Excel 里用得最多的功能之一。比如“按区域汇总销售额”“按月份统计订单量”“按品类计算平均价格”。这些操作在 Excel 里用透视表或者 SUMIF 公式实现,在 Python 里用 pandas 的groupby更简单。
我实现了一个group_summarize函数:
import pandas as pd def group_summarize(file_path: str, sheet_name: str = None, group_by: str = None, agg_column: str = None, agg_func: str = "sum") -> dict: """ 按指定列分组,对目标列进行聚合。 agg_func: sum, mean, count, max, min """ result = read_excel(file_path, sheet_name) if "error" in result: return result data = result["data"] if not data: return {"error": "表格为空"} headers = data[0] df = pd.DataFrame(data[1:], columns=headers) if group_by not in df.columns: return {"error": f"分组列不存在: {group_by}", "available_columns": list(df.columns)} if agg_column and agg_column not in df.columns: return {"error": f"聚合列不存在: {agg_column}", "available_columns": list(df.columns)} if agg_func == "count": grouped = df.groupby(group_by).size().reset_index(name="count") else: if not agg_column: return {"error": "聚合操作需要指定 agg_column"} grouped = df.groupby(group_by)[agg_column].agg(agg_func).reset_index() grouped.columns = [group_by, f"{agg_column}_{agg_func}"] return { "group_by": group_by, "agg_column": agg_column, "agg_func": agg_func, "result": grouped.to_dict(orient="records"), "count": len(grouped) }这个函数用 pandas 做分组聚合,支持 sum、mean、count、max、min 五种聚合方式。返回的结果是一个字典列表,AI 可以直接读取或者进一步处理。
实操心得:pandas 的
groupby默认会忽略 NaN 值。如果你的数据里有空值,聚合结果可能跟 Excel 的透视表不一致。如果需要包含空值,可以在groupby里加dropna=False参数。另外,groupby之后的结果默认是按分组列排序的,如果不需要排序,可以加sort=False提升性能。
3.4 写入 Excel:把结果保存下来
处理完数据之后,最后一步是把结果写入新的 Excel 文件。这个操作看起来简单,但也有很多细节。
首先是文件名的处理。如果目标文件已经存在,是覆盖还是新建一个带时间戳的文件?我选择的是:如果文件存在,自动加一个数字后缀,避免覆盖用户的数据。
其次是格式的处理。写入的时候可以设置列宽、字体、边框、数字格式等。我实现了一个基础的写入函数,支持设置表头和数据的样式。
from datetime import datetime def write_excel(file_path: str, data: list, headers: list = None, sheet_name: str = "Sheet1") -> dict: """ 将二维数组写入 Excel 文件。 """ path = Path(file_path).resolve() # 如果文件存在,自动重命名 if path.exists(): stem = path.stem suffix = path.suffix timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") path = path.parent / f"{stem}_{timestamp}{suffix}" wb = openpyxl.Workbook() ws = wb.active ws.title = sheet_name # 写入表头 if headers: ws.append(headers) for cell in ws[1]: cell.font = openpyxl.styles.Font(bold=True) cell.fill = openpyxl.styles.PatternFill( start_color="DDDDDD", end_color="DDDDDD", fill_type="solid") # 写入数据 for row in data: ws.append(row) # 自动调整列宽 for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if cell.value: max_length = max(max_length, len(str(cell.value))) except: pass ws.column_dimensions[column_letter].width = min(max_length + 2, 50) wb.save(path) return { "file": str(path), "sheet": sheet_name, "rows": len(data), "cols": len(headers) if headers else (len(data[0]) if data else 0) }这个函数会自动处理文件名冲突,设置表头样式,调整列宽。返回的结果包含最终的文件路径,AI 可以告诉用户文件保存在哪里。
注意事项:
openpyxl写入大量数据时性能一般。如果数据超过几万行,建议用pandas的to_excel方法,底层用的是xlsxwriter,速度快很多。但xlsxwriter不支持读取和修改已有文件,只能新建。所以如果是新建文件,用 pandas;如果是修改已有文件,用 openpyxl。
4. 把函数注册成 MCP 工具
4.1 MCP 服务器的基本骨架
有了 Excel 操作函数之后,下一步是把它们注册成 MCP 工具。我先搭一个服务器的骨架:
import asyncio import json from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent # 创建服务器实例 app = Server("mcp-excel") # 注册工具列表 @app.list_tools() async def list_tools() -> list[Tool]: return [ Tool( name="read_excel", description="读取 Excel 文件内容,返回二维数组和元数据", inputSchema={ "type": "object", "properties": { "file_path": {"type": "string", "description": "Excel 文件路径"}, "sheet_name": {"type": "string", "description": "工作表名称,默认第一个"}, "start_cell": {"type": "string", "description": "起始单元格,如 A1"}, "end_cell": {"type": "string", "description": "结束单元格,如 D100"} }, "required": ["file_path"] } ), # 其他工具... ] # 处理工具调用 @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "read_excel": result = read_excel(**arguments) elif name == "filter_excel": result = filter_excel(**arguments) elif name == "group_summarize": result = group_summarize(**arguments) elif name == "write_excel": result = write_excel(**arguments) else: result = {"error": f"未知工具: {name}"} return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, default=str))] # 启动服务器 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__": asyncio.run(main())这个骨架包含了 MCP 服务器的三个核心部分:list_tools返回可用工具列表,call_tool处理工具调用,main启动服务器。
4.2 工具描述怎么写才能让 AI 用对
工具描述是 MCP 里最容易被忽视但最重要的部分。AI 是根据描述来决定调用哪个工具、传什么参数的。如果描述写得含糊,AI 就可能用错工具或者传错参数。
我总结了几个写工具描述的原则:
第一,描述要具体,不要抽象。比如“读取 Excel 文件”就比“处理 Excel”好,“按条件筛选数据”就比“查询数据”好。AI 需要知道这个工具具体能做什么。
第二,参数描述要说明格式和示例。比如start_cell参数,要说明是“起始单元格,如 A1”而不是“起始位置”。AI 看到示例就知道该怎么传。
第三,要说明返回值的结构。虽然 MCP 协议不强制要求,但在描述里说明返回什么,能帮助 AI 更好地理解结果。比如“返回二维数组和元数据”就比“返回数据”更清晰。
第四,要说明限制和注意事项。比如“只支持 xlsx 格式”“数据量超过 10 万行可能较慢”。这些信息能帮助 AI 在调用前做出判断。
下面是我优化后的工具描述示例:
Tool( name="filter_excel", description="根据条件筛选 Excel 数据,支持 >、<、==、!=、contains、in 六种操作符。" "返回筛选后的行和列。适用于数据查询、条件过滤等场景。", inputSchema={ "type": "object", "properties": { "file_path": { "type": "string", "description": "Excel 文件的绝对路径或相对路径,如 ./data/sales.xlsx" }, "sheet_name": { "type": "string", "description": "工作表名称,不传则使用第一个工作表" }, "conditions": { "type": "array", "description": "筛选条件列表,多个条件之间是 AND 关系", "items": { "type": "object", "properties": { "column": {"type": "string", "description": "列名"}, "op": {"type": "string", "enum": [">", "<", "==", "!=", "contains", "in"]}, "value": {"description": "比较值"} }, "required": ["column", "op", "value"] } }, "columns": { "type": "array", "items": {"type": "string"}, "description": "需要返回的列名列表,不传则返回所有列" } }, "required": ["file_path", "conditions"] } )实操心得:工具描述里的
description字段会直接进入 AI 的上下文,所以每一个字都很重要。我试过把描述写得很长很详细,结果 AI 反而抓不住重点。后来我改成“一句话说明功能 + 一句话说明适用场景 + 参数说明”,效果最好。
4.3 错误处理:让 AI 知道哪里出了问题
MCP 工具调用可能会失败,比如文件不存在、列名不对、参数格式错误等。如果直接抛异常,AI 可能不知道发生了什么,也没法给用户有用的反馈。
我的做法是:所有工具函数都返回一个字典,成功时包含结果数据,失败时包含error字段和可能的available_*字段。这样 AI 看到错误信息后,可以尝试修正参数重新调用,或者告诉用户具体问题。
比如文件不存在时,返回:
{ "error": "文件不存在: /path/to/file.xlsx", "hint": "请检查文件路径是否正确" }列名不存在时,返回:
{ "error": "列不存在: 销售额", "available_columns": ["区域", "产品", "数量", "金额"] }这样 AI 就知道可用的列有哪些,可以自动修正或者提示用户。
注意事项:错误信息里不要包含敏感数据。比如不要把整个表格内容放在错误信息里返回。只返回必要的元数据,比如列名列表、工作表名称等。
5. 实际测试:从零跑通一个完整流程
5.1 准备测试数据
为了测试 MCP 是否正常工作,我准备了一份模拟的销售数据。文件名叫sales_test.xlsx,包含以下列:订单日期、区域、产品、数量、单价、金额。
数据大概长这样:
| 订单日期 | 区域 | 产品 | 数量 | 单价 | 金额 |
|---|---|---|---|---|---|
| 2024-01-05 | 华东 | A | 10 | 100 | 1000 |
| 2024-01-06 | 华南 | B | 5 | 200 | 1000 |
| 2024-01-07 | 华东 | C | 8 | 150 | 1200 |
| 2024-01-08 | 华北 | A | 12 | 100 | 1200 |
| 2024-01-09 | 华南 | C | 6 | 150 | 900 |
一共 50 行数据,覆盖三个区域、三种产品、一个月的时间范围。
5.2 配置 AI 客户端连接 MCP
我用的 AI 客户端是 Claude Desktop,它支持 MCP 协议。配置方法是在claude_desktop_config.json里添加一个 MCP 服务器:
{ "mcpServers": { "excel": { "command": "python", "args": ["/path/to/mcp-excel/server.py"] } } }如果你用的是其他支持 MCP 的客户端,配置方式类似,核心是告诉客户端怎么启动你的 MCP 服务器。
配置好之后重启客户端,如果一切正常,客户端会显示已连接的 MCP 服务器和可用的工具。
5.3 第一次对话:读取文件
我在客户端里输入:“帮我读取 sales_test.xlsx 文件,看看里面有什么数据。”
AI 调用了read_excel工具,传入了文件路径。工具返回了数据的前几行和元数据。AI 根据返回结果告诉我:“这个文件有 50 行数据,包含订单日期、区域、产品、数量、单价、金额六列。前几行显示……”
第一次调用成功,说明 MCP 服务器正常启动,工具注册正确,AI 能够理解工具描述并正确调用。
5.4 第二次对话:筛选数据
接着我输入:“帮我筛选出华东区域金额大于 1000 的记录。”
AI 调用了filter_excel工具,传入了条件:
{ "file_path": "sales_test.xlsx", "conditions": [ {"column": "区域", "op": "==", "value": "华东"}, {"column": "金额", "op": ">", "value": 1000} ] }工具返回了筛选结果,AI 整理后告诉我:“华东区域金额大于 1000 的记录有 8 条,总金额是 9600。具体记录如下……”
这一步验证了筛选功能正常工作,而且 AI 能够正确地把自然语言转换成筛选条件。
5.5 第三次对话:分组汇总
我继续输入:“按区域汇总一下金额,看看每个区域的总销售额。”
AI 调用了group_summarize工具:
{ "file_path": "sales_test.xlsx", "group_by": "区域", "agg_column": "金额", "agg_func": "sum" }返回结果:
{ "group_by": "区域", "agg_column": "金额", "agg_func": "sum", "result": [ {"区域": "华东", "金额_sum": 12500}, {"区域": "华南", "金额_sum": 9800}, {"区域": "华北", "金额_sum": 11200} ], "count": 3 }AI 告诉我:“华东区域总销售额 12500,华南 9800,华北 11200。华东最高。”
5.6 第四次对话:写入新文件
最后我输入:“把华东区域的汇总结果保存到一个新文件里。”
AI 先调用了filter_excel筛选出华东区域的数据,然后调用了group_summarize按产品汇总,最后调用了write_excel写入新文件。返回结果:
{ "file": "/path/to/华东区域汇总_20240115_143022.xlsx", "sheet": "Sheet1", "rows": 3, "cols": 2 }AI 告诉我:“已保存到 华东区域汇总_20240115_143022.xlsx,包含 3 行数据。”
整个流程跑通,从读取、筛选、汇总到写入,全部通过 MCP 工具完成,我没有手动操作任何 Excel。
6. 踩过的坑与排查技巧
6.1 文件路径问题
最常见的错误是文件路径不对。AI 传入的路径可能是相对路径,但 MCP 服务器的工作目录可能跟 AI 客户端不一样。我一开始没注意这个问题,导致文件明明存在却报“文件不存在”。
解决方法是:在工具函数里用Path(file_path).resolve()把相对路径转成绝对路径。但resolve()是相对于当前工作目录的,如果工作目录不对,还是会出错。更稳妥的做法是让 AI 传入绝对路径,或者在 MCP 服务器启动时设置一个基准目录,所有相对路径都相对于这个基准目录解析。
实操心得:我在实际使用中发现,让 AI 传绝对路径最可靠。可以在工具描述里明确说明“请传入绝对路径”,或者在服务器配置里设置一个
base_dir环境变量,工具函数根据这个变量来解析路径。
6.2 数据类型不匹配
Excel 里的数据类型很灵活,同一个列里可能混着数字和文本。比如“数量”列,大部分是数字,但偶尔有一个“N/A”或者空值。用 pandas 读取时,这一列的类型会变成 object,做数值比较时会报错。
我的处理方式是:在比较之前先尝试类型转换。如果两边都能转成数字,就按数字比较;否则按字符串比较。这样虽然不能覆盖所有情况,但能解决大部分问题。
还有一个坑是日期类型。Excel 里的日期存储为数字,openpyxl读取时返回的是datetime对象,但pandas读取时可能返回字符串。如果 AI 传入的日期是字符串格式,比较时就会出错。我的做法是统一转成字符串比较,或者在工具描述里说明日期格式要求。
6.3 大文件性能问题
我测试的时候用的是一个 50 行的小文件,速度很快。但后来处理一个 5 万行的文件时,发现读取和筛选都很慢,有时候要等十几秒。
原因是openpyxl读取大文件时性能一般,而且我的筛选函数是逐行遍历,没有用 pandas 的向量化操作。后来我改成了用 pandas 读取和筛选,速度快了很多。
具体做法是:如果文件行数超过一定阈值(比如 1 万行),就用 pandas 的read_excel读取,用query或布尔索引筛选。如果文件较小,用 openpyxl 更灵活。
注意事项:pandas 的
read_excel默认会读取整个文件到内存。如果文件特别大(比如几十万行),可能会占用大量内存。这种情况下可以考虑分块读取,或者用openpyxl的只读模式逐行处理。
6.4 AI 调用工具时的参数错误
有时候 AI 会传错参数,比如把列名写成“销售额”但实际列名是“金额”,或者把操作符写成“大于”而不是“>”。这些错误会导致工具返回错误信息。
我的处理方式是:在错误信息里返回可用的列名和操作符列表,让 AI 能够自我修正。比如:
{ "error": "列不存在: 销售额", "available_columns": ["区域", "产品", "数量", "金额"], "hint": "请使用 available_columns 中的列名" }这样 AI 看到之后,会自动改用正确的列名重新调用。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 文件不存在 | 路径错误或工作目录不对 | 使用绝对路径,或设置 base_dir |
| 列不存在 | 列名拼写错误或有多余空格 | 检查 available_columns,去除空格 |
| 数据类型错误 | 列中混有文本和数字 | 统一转换类型,或按字符串比较 |
| 读取速度慢 | 文件太大或用了逐行遍历 | 改用 pandas 向量化操作 |
| 写入失败 | 文件被占用或没有权限 | 关闭文件,检查写入权限 |
| AI 不调用工具 | 工具描述不清晰 | 优化 description,增加示例 |
| 返回结果为空 | 筛选条件太严格 | 放宽条件,或检查数据范围 |
7. 后续可以扩展的方向
7.1 支持更多 Excel 操作
目前我实现了读取、筛选、分组汇总、写入四个核心功能。实际上 Excel 的操作远不止这些。后续可以扩展的方向包括:
- 排序:按指定列升序或降序排列
- 去重:删除重复行
- 透视表:更复杂的分组聚合
- 图表生成:根据数据生成柱状图、折线图、饼图
- 条件格式:根据数值设置单元格颜色
- 公式计算:在写入时插入公式
每增加一个功能,就多一个 MCP 工具。AI 的能力边界也随之扩展。
7.2 支持更多文件格式
除了 xlsx,还可以支持 csv、xls、ods 等格式。csv 用 pandas 读写很简单,xls 需要 xlrd,ods 需要 odfpy。把这些格式统一封装成相同的接口,AI 就不需要关心底层是什么格式。
7.3 增加数据校验功能
在实际使用中,数据质量是个大问题。比如日期格式不统一、数字列里有文本、必填字段为空等。可以增加一个数据校验工具,让 AI 在分析之前先检查数据质量,发现问题并给出修复建议。
7.4 与更多 AI 客户端集成
目前我只测试了 Claude Desktop,但 MCP 是一个开放协议,理论上支持任何实现了 MCP 的客户端。后续可以测试跟其他客户端的集成,比如一些开源的 AI 编程助手、自动化工作流工具等。
7.5 性能优化
对于大文件处理,可以考虑以下优化:
- 使用
openpyxl的只读模式(read_only=True)减少内存占用 - 使用
pandas的chunksize参数分块读取 - 缓存已读取的文件,避免重复读取
- 使用异步 IO 处理多个文件的并发操作
这些优化需要根据实际使用场景来决定优先级。如果只是处理几千行的小文件,目前的性能已经足够了。
7.6 安全考虑
MCP 工具运行在本地,理论上只能访问本地文件。但为了安全起见,可以增加一些限制:
- 限制可访问的目录范围,防止 AI 读取敏感文件
- 限制写入操作,防止意外覆盖重要文件
- 记录所有工具调用日志,方便审计和排查问题
这些安全措施在个人使用时可能不是必须的,但如果要把 MCP 分享给其他人使用,就很有必要了。
7.7 打包与分发
目前运行 MCP 需要手动安装 Python 环境和依赖。如果想让更多人使用,可以考虑打包成可执行文件。Python 有pyinstaller、nuitka等工具可以把脚本打包成单个可执行文件。这样用户只需要下载一个文件,配置到 AI 客户端里就能用,不需要安装 Python。
不过打包也有缺点:文件体积大,启动速度慢,而且不同平台的打包方式不一样。对于个人使用来说,直接跑 Python 脚本更简单。
7.8 测试与持续集成
如果这个项目要长期维护,建议加上自动化测试。可以用pytest写单元测试,覆盖每个工具函数的正常和异常情况。还可以用 GitHub Actions 做持续集成,每次提交代码自动跑测试,确保不会引入回归问题。
测试数据可以放在test_data目录里,用一些小的 Excel 文件覆盖各种场景:空文件、只有表头、有公式、有合并单元格、有特殊字符等。
7.9 文档与示例
最后,好的文档能让别人更快上手。可以写一个 README,说明项目的作用、安装方法、配置方式、可用工具列表、常见问题等。还可以录一个演示视频,展示从安装到使用的完整流程。
示例方面,可以准备几个典型的场景,比如“销售数据分析”“库存管理”“财务报表处理”,每个场景配一个示例 Excel 文件和一段对话记录,让用户直接照着做就能跑通。
这个 MCP 项目我从有这个想法到跑通第一个完整流程,大概花了一个周末的时间。中间踩了不少坑,但整体来说比想象中简单。MCP 协议的设计很清晰,Python SDK 也足够好用。如果你也有类似的需求,建议从一个小工具开始,先跑通流程,再逐步扩展功能。不要一上来就想做一个大而全的系统,那样很容易卡住。
我在实际使用中最大的体会是:工具描述比工具实现更重要。AI 能不能用对工具,很大程度上取决于描述写得清不清楚。花时间打磨描述,比花时间优化代码的收益更高。另外,错误处理要做好,让 AI 能够从错误中恢复,而不是直接崩溃。这两点做到了,整个系统的可用性就会高很多。