1. 为什么我要自己动手写一个 MCP
1.1 从一次崩溃的 Excel 处理经历说起
上个月帮朋友处理一份销售数据,三千多行,需要按区域拆分、按品类汇总、再生成几张透视表。我当时的做法很原始:打开 Excel,手动筛选,复制粘贴到新表,写几个 SUMIF 公式,再拖一遍格式。整个过程花了将近两个小时,中间还因为一次误操作把原始数据覆盖了,只能从头再来。
那天晚上我就在想,这种重复性极高、逻辑又很固定的活儿,为什么不能让 AI 直接帮我干?我平时用对话式 AI 处理文本已经很顺手了,但一碰到 Excel 文件,AI 就只能给我一段代码,我还得自己复制到本地跑,跑完再检查结果。这个链路是断的——AI 能理解我的意图,但没法直接操作我的文件。
后来我接触到了 MCP 这个概念。MCP 全称是 Model Context Protocol,翻译过来叫“模型上下文协议”。你可以把它理解成一套标准接口,让 AI 模型能够调用外部工具、读取外部资源。打个比方,以前的 AI 像一个只会说话的顾问,你问它什么它答什么,但它碰不到你的电脑;有了 MCP 之后,AI 就像装上了一双手,可以直接去读你的文件、调你的脚本、把结果写回你的表格。
这个变化对我来说意义很大。我不再需要“AI 给代码 → 我复制 → 我运行 → 我检查”这个四步循环,而是变成“我提需求 → AI 调工具 → 结果直接落地”。省掉的不仅是时间,更是那种反复切换上下文的烦躁感。
1.2 MCP 到底解决了什么问题
在动手之前,我先花时间把 MCP 的定位想清楚了。市面上做 AI 工作流的方案不少,有在线的自动化平台,有本地脚本加定时任务,也有各种插件体系。MCP 跟它们最大的区别在于:它是一个协议,不是一个平台。
协议意味着什么?意味着只要双方都遵守这套规则,任何 AI 客户端都能跟任何工具服务端对话。我写的这个 Excel 处理服务,理论上可以被不同的 AI 助手调用,而不需要为每个助手单独适配一遍。这跟我以前写脚本的思路完全不同——以前是“为某个具体场景写死一个脚本”,现在是“写一个能力,让 AI 自己决定什么时候用”。
具体到 Excel 处理这个场景,MCP 帮我解决了三个核心痛点。第一是文件访问,AI 能直接读取指定路径的 Excel 文件,不需要我手动上传下载。第二是操作执行,AI 能调用我封装好的函数去筛选、汇总、写入,而不是只给我一段参考代码。第三是结果回传,处理完的数据能直接写回文件或者返回给 AI 做进一步分析。
这三个点串起来,就形成了一条完整的自动化链路。我提一句“把这份表按区域拆成多个 sheet”,AI 理解意图后调用我的工具,工具执行完把结果告诉我,整个过程我只需要说一句话。
1.3 适合谁来读这篇内容
这篇内容适合三类人。第一类是经常跟 Excel 打交道、被重复劳动折磨的职场人,你不需要是程序员,但最好能看懂基本的 Python 代码,因为我会给出完整的实现。第二类是想入门 MCP 开发的技术爱好者,Excel 处理是一个很好的练手项目,逻辑清晰、边界明确、容易验证。第三类是对 AI 工作流感兴趣的产品或运营同学,你可以通过这个案例理解 MCP 的能力边界和落地方式。
如果你完全没写过代码,也不用急着关掉。我会把每一步的意图讲清楚,你可以先理解思路,再决定要不要动手。工具选型上我尽量选了最通用的方案,Python 加 openpyxl,环境配置不算复杂,跟着走一遍基本能跑通。
2. 整体设计与技术选型拆解
2.1 为什么选 Python 而不是其他语言
做 Excel 处理,语言选择其实不少。JavaScript 有 SheetJS,Java 有 Apache POI,Go 也有 excelize。我最终选 Python,理由很实际:生态成熟、代码量少、调试方便。
openpyxl 这个库我用了好几年,读写 xlsx 文件非常稳,支持公式、样式、图表,社区文档也全。pandas 更不用说,做数据筛选和汇总几乎是一行代码的事。相比之下,用 Java 写同样的逻辑,光是把 Workbook 对象传来传去就要多写几十行。MCP 服务本身是个轻量级的工具服务,没必要上重型框架,Python 的简洁性在这里是实打实的优势。
另一个考虑是 AI 生成代码的友好度。我让 AI 帮我补全一些函数的时候,Python 的代码它写得最准,因为训练数据里 Python 的 Excel 处理示例最多。这个因素在开发效率上影响很大,尤其是当你需要快速迭代的时候。
2.2 MCP 服务的基本结构
一个 MCP 服务端,核心就三样东西:工具定义、资源定义、传输方式。
工具定义是告诉 AI“我能干什么”。比如我定义了一个read_excel工具,描述是“读取指定路径的 Excel 文件并返回前若干行”,参数是文件路径和行数。AI 看到这个描述,就知道在需要读表的时候可以调它。
资源定义是告诉 AI“我能提供什么数据”。比如我把某个目录下的 Excel 文件列表暴露成资源,AI 就能知道有哪些文件可用,而不需要我每次都告诉它路径。
传输方式决定了 AI 客户端怎么跟我的服务通信。常见的有标准输入输出和基于 HTTP 的传输。我本地开发用标准输入输出就够了,简单直接,不需要额外起端口。
这三样东西的组合方式,决定了整个服务的形态。我的设计思路是:工具尽量原子化,一个工具只做一件事;资源按需暴露,不一次性把所有文件都列出来;传输先用最简单的方案,跑通再说。
2.3 工具粒度怎么切才合理
这是我在设计阶段花时间最多的地方。工具切得太粗,AI 调用一次要做很多事,参数复杂,出错难排查;切得太细,AI 要调很多次才能完成一个任务,效率低。
我最后定了一个原则:按操作类型切,不按业务场景切。具体来说,我定义了这么几个工具:
| 工具名 | 功能 | 关键参数 |
|---|---|---|
| read_excel | 读取表格内容 | 文件路径、sheet 名、最大行数 |
| write_excel | 写入数据到表格 | 文件路径、sheet 名、数据、起始位置 |
| filter_data | 按条件筛选行 | 文件路径、列名、条件表达式 |
| group_sum | 按列分组求和 | 文件路径、分组列、求和列 |
| list_sheets | 列出所有 sheet | 文件路径 |
| create_sheet | 新建 sheet | 文件路径、sheet 名 |
这样切的好处是,每个工具职责单一,AI 可以根据任务自由组合。比如“按区域汇总销售额”,AI 会先调read_excel看数据结构,再调group_sum做汇总,最后调write_excel写结果。每一步我都能看到,出问题也知道是哪一步。
提示:工具描述一定要写清楚参数格式和返回值结构。AI 是靠描述来决定怎么调的,描述模糊它就会瞎猜。我一开始把
filter_data的条件写成“字符串”,结果 AI 传了个自然语言句子进来,直接报错。后来改成“形如 '列名>100' 的表达式”,就稳了。
2.4 数据安全与边界控制
让 AI 直接操作本地文件,安全边界必须想清楚。我的做法是三层限制。
第一层是目录白名单。服务启动时指定一个工作目录,所有文件操作都必须在这个目录内。AI 传进来的路径如果跳出这个目录,直接拒绝。这防止了误操作或者恶意路径。
第二层是操作类型限制。我只暴露读、写、筛选、汇总这几类操作,不暴露删除文件、执行任意命令这类高危能力。写操作也只允许写到指定目录,不允许覆盖系统文件。
第三层是数据量上限。读取时默认最多返回 1000 行,防止一次性拉太多数据把上下文撑爆。如果确实需要处理大文件,AI 可以分批读,或者直接用汇总工具拿结果。
这三层加起来,基本能保证 AI 在可控范围内干活。我实测下来,正常任务不会触发任何限制,只有当我故意传个越界路径时才会被拦。
3. 核心细节解析与实操要点
3.1 环境准备:Python 与依赖安装
先把环境搭起来。我假设你用的是 Windows 或者 macOS,Linux 也一样,命令稍微改改就行。
第一步,确认 Python 版本。MCP 的 Python SDK 要求 3.10 以上,我建议直接用 3.11 或 3.12,兼容性好。打开终端输入:
python --version如果显示低于 3.10,去官网下个新版装上。安装的时候记得勾选“Add Python to PATH”,不然命令行找不到它。
第二步,建一个独立的环境。我习惯用 venv,避免跟系统里的包打架:
python -m venv mcp-excel-envWindows 下激活:
mcp-excel-env\Scripts\activatemacOS 或 Linux 下激活:
source mcp-excel-env/bin/activate激活后命令行前面会出现环境名,说明生效了。
第三步,装依赖。核心就三个:
pip install mcp openpyxl pandasmcp 是协议 SDK,openpyxl 负责读写 xlsx,pandas 负责数据处理。三个装完,环境就齐了。
注意:如果你公司网络有限制,pip 装包可能会慢。可以加个国内镜像源,比如
-i https://pypi.tuna.tsinghua.edu.cn/simple,速度会快很多。这个不是必须的,但能省不少等待时间。
3.2 第一个工具:读取 Excel 的实现细节
环境好了,开始写代码。我先把最基础的读取工具做出来,跑通了再往上加。
新建一个文件叫excel_server.py,先写导入和初始化:
import os import json from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import openpyxl import pandas as pd WORK_DIR = os.path.abspath("./workspace") os.makedirs(WORK_DIR, exist_ok=True) app = Server("excel-mcp")这里WORK_DIR就是前面说的白名单目录,所有操作都限制在这里面。app是服务实例,后面所有工具都注册到它上面。
接下来写路径校验函数,这是安全的第一道关:
def safe_path(relative_path: str) -> str: full = os.path.abspath(os.path.join(WORK_DIR, relative_path)) if not full.startswith(WORK_DIR): raise ValueError("路径越界,拒绝访问") return full这个函数把 AI 传进来的相对路径拼成绝对路径,然后检查是否还在工作目录内。如果 AI 传了../../etc/passwd这种,直接抛异常。
然后是读取工具的核心逻辑:
def read_excel_file(path: str, sheet: str = None, max_rows: int = 100) -> str: full = safe_path(path) if not os.path.exists(full): return f"文件不存在: {path}" wb = openpyxl.load_workbook(full, read_only=True, data_only=True) sheet_name = sheet or wb.sheetnames[0] if sheet_name not in wb.sheetnames: return f"sheet 不存在: {sheet_name}" ws = wb[sheet_name] rows = [] for i, row in enumerate(ws.iter_rows(values_only=True)): if i >= max_rows: break rows.append(row) wb.close() if not rows: return "表格为空" header = rows[0] result = {"sheet": sheet_name, "header": list(header), "rows": [list(r) for r in rows[1:]]} return json.dumps(result, ensure_ascii=False, default=str)几个细节值得说。read_only=True让 openpyxl 用流式方式读,大文件不会一次性加载到内存。data_only=True让公式单元格返回计算后的值,而不是公式本身,这个在处理汇总表的时候很关键。default=str是防止日期类型序列化报错,openpyxl 读出来的日期是 datetime 对象,json 默认处理不了。
3.3 工具注册与描述怎么写
函数写好了,得注册到 MCP 服务上,AI 才能看到。注册的时候要提供工具名、描述、参数 schema:
@app.list_tools() async def list_tools(): return [ Tool( name="read_excel", description="读取 Excel 文件内容。返回表头和指定行数的数据。", inputSchema={ "type": "object", "properties": { "path": {"type": "string", "description": "相对于工作目录的文件路径"}, "sheet": {"type": "string", "description": "sheet 名称,不填则用第一个"}, "max_rows": {"type": "integer", "description": "最多返回多少行,默认 100"} }, "required": ["path"] } ) ]描述这块我踩过坑。一开始我写的是“读取 Excel”,结果 AI 有时候不知道该不该调,因为它不确定这个工具能不能处理它手头的文件。后来我把描述改成“读取 Excel 文件内容,支持指定 sheet 和行数上限”,AI 的调用准确率明显上去了。
参数描述也一样。path我特意写了“相对于工作目录”,这样 AI 就知道不该传绝对路径。max_rows写了默认值,AI 不传的时候也知道怎么处理。
3.4 写入与汇总工具的实现要点
读取跑通之后,写入和汇总就是顺水推舟。写入工具的关键是处理数据格式和追加模式:
def write_excel_file(path: str, data: list, sheet: str = None, mode: str = "append") -> str: full = safe_path(path) if os.path.exists(full): wb = openpyxl.load_workbook(full) else: wb = openpyxl.Workbook() sheet_name = sheet or wb.sheetnames[0] if sheet_name not in wb.sheetnames: wb.create_sheet(sheet_name) ws = wb[sheet_name] if mode == "append": for row in data: ws.append(row) else: for i, row in enumerate(data, start=1): for j, val in enumerate(row, start=1): ws.cell(row=i, column=j, value=val) wb.save(full) return f"已写入 {len(data)} 行到 {sheet_name}"mode参数区分追加和覆盖。追加用于往已有表里加数据,覆盖用于重新生成一张表。这个设计让 AI 可以根据场景选择,不用我每次手动清空。
汇总工具用 pandas 实现,代码量很少但很实用:
def group_sum_file(path: str, group_col: str, sum_col: str, sheet: str = None) -> str: full = safe_path(path) df = pd.read_excel(full, sheet_name=sheet or 0) if group_col not in df.columns or sum_col not in df.columns: return f"列不存在,可用列: {list(df.columns)}" result = df.groupby(group_col)[sum_col].sum().reset_index() return result.to_json(orient="records", force_ascii=False)pandas 的groupby一行搞定分组求和,reset_index把分组列变回普通列,to_json直接输出结构化结果。AI 拿到这个 JSON 之后,可以决定是展示给我看,还是再调写入工具存下来。
实操心得:pandas 读 Excel 的时候,如果表头不在第一行,需要加
header参数指定。我遇到过一份表前两行是标题和说明,第三行才是表头,直接读会把标题当列名。这种情况要么让 AI 先读一遍确认结构,要么在工具里加个header_row参数。我后来加了,省得每次手动调。
4. 完整实操流程与关键环节
4.1 从零跑通第一个 MCP 服务
代码写完了,怎么让它跑起来?MCP 服务本身是个独立进程,需要 AI 客户端来连接。我本地用的是支持 MCP 的编辑器插件,配置方式是在设置里加一段服务定义。
配置文件大概长这样:
{ "mcpServers": { "excel-tools": { "command": "python", "args": ["/path/to/excel_server.py"], "env": {} } } }command是启动命令,args是脚本路径。保存之后重启客户端,它就会自动拉起这个 Python 进程,通过标准输入输出跟它通信。
验证是否连上,可以在 AI 对话里问一句“你有哪些 Excel 相关的工具”。如果配置对了,AI 会列出我注册的那几个工具名。这一步很关键,连不上后面都白搭。
我第一次配的时候卡了半小时,原因是脚本路径写成了相对路径,客户端的工作目录跟我预期的不一样,找不到文件。改成绝对路径就好了。这个坑很常见,建议一开始就用绝对路径。
4.2 一个真实任务的完整执行记录
服务跑通后,我拿一份真实的销售数据做了测试。文件叫sales.xlsx,放在 workspace 目录下,结构是:日期、区域、品类、销售额、数量,大概两千行。
我在对话里输入:“读取 sales.xlsx,看看有哪些列,然后按区域汇总销售额。”
AI 的执行过程是这样的:
第一步,它调了read_excel,参数path="sales.xlsx",max_rows=5。返回了表头和前五行数据,它确认了列名是“日期、区域、品类、销售额、数量”。
第二步,它调了group_sum,参数group_col="区域",sum_col="销售额"。返回了一个 JSON 数组,每个区域一条记录,带汇总金额。
第三步,它把结果整理成表格展示给我,同时问我要不要写回文件。
我说“写到新 sheet 里,叫区域汇总”。它调了write_excel,参数path="sales.xlsx",sheet="区域汇总",data=[...],mode="overwrite"。返回“已写入 5 行”。
整个过程我数了一下,从输入到完成大概十几秒。同样的活儿我手动做要十几分钟,而且容易出错。这个对比让我觉得前面花的时间值了。
4.3 参数计算与边界处理的实际案例
有一个场景我特意测了边界:如果 AI 要汇总的列名写错了怎么办。
我故意说“按地区汇总销售额”,但表里的列名是“区域”不是“地区”。AI 调group_sum的时候传了group_col="地区",我的工具检测到列不存在,返回了“列不存在,可用列: ['日期', '区域', '品类', '销售额', '数量']”。
AI 拿到这个返回后,自动纠正成“区域”,重新调了一次,成功了。这个体验很好,因为我的工具把可用列返回去了,AI 有足够信息自我修正。如果我只返回“列不存在”,AI 可能就卡住了。
这让我意识到,工具的错误返回信息要尽量详细。报错不是终点,而是给 AI 提供纠正线索的机会。后来我把所有工具的报错都改成了“问题描述 + 可用选项”的格式,AI 的自主纠错能力明显提升。
另一个边界是空值处理。销售数据里有些行的销售额是空的,pandas 默认会跳过,但我想确认一下。我让 AI 先读一遍看有没有空值,它调read_excel拿到数据后自己判断出来了,告诉我“第 47、112、389 行销售额为空,汇总时已忽略”。这个判断是 AI 做的,不是我工具做的,说明 AI 在拿到原始数据后有自己的分析能力。
4.4 多步骤任务的编排逻辑
单个工具调用跑通后,我试了个复杂点的任务:“把 sales.xlsx 按区域拆成多个 sheet,每个 sheet 放该区域的数据,并且每个 sheet 最后加一行汇总。”
这个任务 AI 拆成了好几步。它先调read_excel拿到全部数据,然后在自己的上下文里按区域分组,接着对每个区域调一次create_sheet建表,再调write_excel写入数据,最后调write_excel追加汇总行。
我数了一下,它一共调了 1 次读取、5 次建表、5 次写入、5 次追加,总共 16 次工具调用。整个过程大概半分钟,中间没有出错。
这个案例让我看到了 MCP 的编排能力。AI 不是简单地一对一调工具,而是能根据任务目标自己规划调用顺序和次数。我作为开发者,只需要把原子工具做好,编排交给 AI。
提示:多步骤任务里,工具的幂等性很重要。比如
create_sheet如果 sheet 已存在,我的实现是直接返回成功而不是报错,这样 AI 重试的时候不会因为“已存在”而卡住。写入工具也是,追加模式天然幂等,重复写最多多几行,不会破坏结构。
5. 常见问题与排查技巧实录
5.1 服务连不上或工具不显示
这是最常见的问题,表现是 AI 说“我没有 Excel 相关工具”。排查顺序我总结成一张表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 工具完全不显示 | 配置文件路径错 | 检查 args 里的脚本路径是否为绝对路径 |
| 工具显示但调用报错 | 依赖没装全 | 在服务端环境里手动跑一遍 import |
| 时好时坏 | 进程被回收 | 看客户端日志,确认服务是否频繁重启 |
| 报编码错误 | 系统默认编码非 UTF-8 | 在脚本开头加# -*- coding: utf-8 -*- |
我遇到最多的是路径问题。客户端的启动目录跟我以为的不一样,导致相对路径找不到脚本。改成绝对路径后就没再出过。
还有一个隐蔽的坑:如果脚本里有语法错误,服务启动会直接失败,但客户端不一定把错误显示出来。我的做法是先在命令行手动跑一遍python excel_server.py,看能不能正常启动。如果能启动但没输出,说明在等标准输入,这是正常的。如果直接报错,那就是代码问题,先修好再配客户端。
5.2 读取数据乱码或格式错乱
Excel 里的数据类型很杂,日期、数字、文本混在一起,读出来容易出问题。我遇到过几种典型情况。
日期变成一串数字,比如45000。这是因为 Excel 内部用序列号存日期,openpyxl 在某些模式下会返回原始值。解决办法是读的时候加data_only=True,并且用 pandas 读会更省心,pandas 会自动识别日期列。
数字变成字符串,比如"1234"而不是1234。这通常是单元格格式设成了文本。如果后续要做求和,得先转换类型。我在汇总工具里加了自动转换,用pd.to_numeric处理,errors='coerce'让转换失败的变成 NaN,避免整个任务崩掉。
中文乱码一般不会出现在 xlsx 里,因为 xlsx 本身是 UTF-8。但如果你的脚本文件保存成了 GBK,字符串里的中文就会出问题。统一用 UTF-8 保存脚本就行。
5.3 写入后文件打不开或数据丢失
写入是最容易出事的环节。我踩过的坑有这么几个。
第一个是文件被占用。如果 Excel 正开着这个文件,openpyxl 保存会失败。我的做法是在写入前先检查文件是否可写,不可写就返回明确提示“文件被占用,请关闭后重试”。AI 拿到这个提示会告诉我,我就知道该关 Excel 了。
第二个是覆盖了不想覆盖的数据。mode="overwrite"会清空整个 sheet 再写,如果 AI 判断错了模式,数据就没了。我的防护是:覆盖模式下,如果原 sheet 有数据且新数据行数少于原来的一半,返回警告让 AI 确认。这个规则有点保守,但能防住大部分误操作。
第三个是写入过程中程序崩溃,导致文件损坏。openpyxl 保存是原子操作,一般不会写一半。但为了保险,我加了个临时文件机制:先写到.tmp文件,成功了再替换原文件。这样即使中途出错,原文件也不受影响。
5.4 AI 调用工具时的典型错误
AI 不是每次都调对,我记录了几个高频错误和应对方式。
传错参数类型。比如max_rows传了字符串"100"而不是数字100。我的工具里加了类型转换,int(max_rows)兜底,转不了就用默认值。这样不会因为类型问题直接失败。
路径传了绝对路径。AI 有时候会自作主张传/Users/xxx/sales.xlsx。我的safe_path会拒绝,返回“请使用相对于工作目录的路径”。AI 看到这个提示会改成相对路径重试。
一次传太多数据。写入的时候 AI 可能把几千行数据一次性塞进参数里,导致请求过大。我的做法是在工具描述里写明“单次最多 500 行”,AI 会自己分批。实测下来它很听话,看到限制就会拆。
实操心得:给 AI 的工具描述里,限制条件要写得跟功能描述一样清楚。我一开始只写“写入数据”,AI 不知道有行数限制,经常传超。后来改成“写入数据,单次最多 500 行,超出请分批”,就再没出过问题。AI 对明确的数字限制很敏感,你写多少它就遵守多少。
5.5 性能优化与批量处理建议
数据量上去之后,性能问题会显现。我测过一万行的表,读取大概两秒,汇总一秒,写入三秒。这个速度可以接受,但如果频繁调用,累积起来也慢。
优化手段有几个。读取用read_only=True流式读,内存占用小。汇总尽量用 pandas 而不是 openpyxl 遍历,pandas 底层是 C 实现,快很多。写入如果数据量大,用write_only=True模式,openpyxl 会流式写,速度快一倍。
还有一个策略是缓存。如果 AI 短时间内多次读同一个文件,可以在服务端缓存读取结果,设个过期时间。我加了个简单的字典缓存,key 是文件路径加修改时间,文件没变就直接返回缓存。这个优化让重复读取几乎零耗时。
不过缓存要小心,如果文件被外部修改了,缓存就脏了。我用文件的修改时间做 key 的一部分,文件一变缓存自动失效。这个方案简单可靠,没出过问题。
6. 后续扩展与个人体会
这套东西跑通之后,我又陆续加了几个工具。一个是find_replace,在指定列里查找替换文本,处理数据清洗很实用。一个是pivot_table,直接生成透视表,省得 AI 自己拼汇总逻辑。还有一个export_csv,把 sheet 导出成 CSV,方便跟其他系统对接。
扩展的方向其实很多。往深了做,可以支持公式写入、条件格式、图表生成。往宽了做,可以接入更多文件格式,比如 CSV、JSON、甚至数据库。MCP 的好处就在这里,工具是插件式的,加一个注册一个,不影响已有的。
我个人的体会是,MCP 开发最难的不是写代码,而是想清楚“AI 会怎么用这个工具”。你得站在 AI 的角度去设计参数和返回值,让它容易理解、容易调用、容易纠错。这个思路跟传统的 API 设计不太一样,传统 API 是给人用的,文档写清楚就行;MCP 工具是给 AI 用的,描述和错误信息本身就是交互的一部分。
最后分享一个小技巧:每次加新工具,先自己手动模拟一遍 AI 的调用过程,把参数和返回值都写出来,看看逻辑通不通。我这么做之后,工具一次跑通的概率高了很多,省去了反复调试的麻烦。