1. 为什么我要自己做MCP:被Excel重复劳动逼出来的决定
做数据分析的人,应该都有过这种体验:每周固定时间,打开同一个Excel表格,做同一套清洗逻辑,删掉空行、统一日期格式、把分组合并、再生成透视表。一开始用VBA写过宏,后来改用Python脚本,再后来发现脚本虽好,但每次都要手动改文件路径、手动改参数,AI根本插不进手。
直到我接触了MCP(Model Context Protocol,模型上下文协议),才意识到问题出在哪儿:AI模型本身再聪明,它也只能“看”你贴给它的文本,不能直接操作你电脑上的Excel文件。而MCP给我提供了一个标准化的管道,让我可以把“操作Excel”这件事本身,变成一种AI可以调用的服务。
简单解释一下,MCP是一个开放协议,它定义了大模型和外部工具之间的通信方式。你可以把它理解成一种“万能插座”:一边插上AI助手(比如Claude、各类支持MCP的IDE),另一边插上你写的工具服务(比如Excel处理服务)。AI通过MCP协议发请求,你的服务去执行真正的工作,再把结果返回给AI。这样,AI就不只是会聊天,而是真的能帮你“干活”了。
这篇文章想分享的,不是我做了什么高深的研究,而是我踩了不少坑之后,成功开发出第一个MCP服务、用它重构了日常Excel处理流程的完整过程。包括协议原理、架构设计、代码实现、调试技巧,以及我在实战中遇到的几个典型问题和解决方案。如果你也在天天处理Excel表格、想试试让AI帮你打理这些重复劳动,这篇文章应该能给你一条可以直接上手的路径。
2. MCP到底是什么:拆掉概念滤镜,看协议本质
2.1 一次AI调用工具,背后发生了什么
先说清楚MCP的工作方式。MCP采用客户端-服务器架构,但这里说的“服务器”不是跑在云端的Web服务,而是一个本地进程。整个链路是这样的:
- MCP客户端跑在AI助手里面,负责和模型交互,同时作为通信的发起方。
- MCP服务器是你自己写的一个程序,它暴露出一组“工具”(Tool)。
- 当AI模型判断任务需要某个工具时,客户端会生成一个工具调用请求,通过MCP协议发给服务器。
- 服务器执行操作(比如读取Excel文件、修改单元格),把结果返回给客户端,客户端再把结果交还模型,让模型基于结果继续推理。
这个机制很像人类用电话遥控另一个人做事:你告诉电话那头的人“打开桌面上那个文件,把第二列的数字求和”,对方执行后告诉你结果。区别在于,MCP协议的格式是标准化的,请求和响应都是结构化数据,模型不需要理解你的服务是怎么实现的,只要知道“有哪些工具可以调用”就行。
这里有一个很重要的设计理念:工具是“手”,模型是“大脑”。MCP只负责大脑和手之间的通信,不负责大脑怎么思考、手怎么动作。这个隔离意味着,你可以随时替换AI模型(从这个模型换成另一个),也可以随时替换工具实现(从Python换Node.js),两端互不影响。
2.2 为什么不是直接让AI生成Python代码
你可能想问:我直接用Python写脚本处理Excel,再让AI帮我生成脚本,不是更简单吗?为什么要绕一个MCP?
我的实际体会是,这种方式有两个痛点。第一,AI生成的代码经常需要人工确认环境依赖,数据文件的路径一变化就要改代码;第二,对于“边看边做”的任务——比如“看一下这个表的前10行,判断哪列是日期,然后帮我规范化”——生成代码后,你得跑一遍、把输出贴回去给AI,AI再继续生成下一段代码。这个循环非常慢。
MCP的优势在于交互粒度。AI是分步调用工具的,每一步只做一件明确的事,比如“打开文件”“读取前10行”“修改A列格式”。每一步的结果都回传给模型,模型根据结果决定下一步做什么。整个决策过程是动态的,不需要你提前把所有操作逻辑写死在脚本里。
这也带来了一个额外的好处:同一个MCP服务,可以被多个AI应用复用。你今天在IDE里用,明天在另一个聊天工具里用,服务不用改。这就好比USB接口普及之前,每个设备都要专门的连接线;MCP就是那个统一接口,接上就能用。
2.3 协议规范里的三个核心概念
MCP协议定义了三个核心概念,我用自己的话翻译一下:
- Tools(工具):AI能执行的操作单元,用JSON Schema描述输入参数。比如“读取Excel工作表”是一个工具,它接收文件路径和表名两个参数。
- Resources(资源):AI可以读取的数据源,可以是文件内容、数据库记录、URL等。Resources是“可读”的,AI可以把内容拉进上下文。
- Prompts(提示词模板):预定义的复用提示,相当于给AI准备好的“操作手册”,告诉它碰到什么情况该怎么做。
在我们这个场景里,最核心的是Tools。Resources可以有(比如让AI直接读取某个生成好的报告文件),但第一版可以不做。
2.4 传输层选型:stdio还是HTTP
MCP支持两种主流传输方式:stdio(标准输入输出)和HTTP(含SSE)。
stdio模式最简单,客户端直接启动你的服务进程,通过标准输入输出流传输JSON消息。这意味着服务是随客户端生命周期走的,客户端关掉,服务进程也结束,不需要管理端口和生命周期。本地开发、个人使用,我强烈建议用stdio。
SSE模式适合远程场景,比如你有一台服务器专门跑MCP服务,多个客户端通过网络连接它。但代价是你要处理鉴权、跨域、端口暴露等一堆问题,第一版完全没必要。
我自己的选择是stdio优先,后续如果有远程调用需求,再加一个HTTP包装层。这个思路推荐给你:先把功能跑通,再考虑部署形态。
注意:MCP官方SDK会持续更新,接口名可能调整。我的代码是在当前版本下验证过的,你实际操作时建议先看一眼官方文档确认有无破坏性变更。
3. 整体设计思路:一个Excel处理MCP服务应该怎么拆
3.1 核心需求盘点
在动手写代码之前,我先花了半小时列了一个需求清单。自己做东西最容易犯的错误是一上来就写代码,写到一半才发现“这个功能我根本不需要”或者“这个功能架构上没法扩展”。需求清单不用很复杂,但要覆盖你最高频的Excel操作。
我列的清单是这样的:
- 读取Excel文件的基础信息:工作表列表、行列数、列名。
- 读取指定范围的数据:支持按行列切片,方便AI先“看一眼”再决定怎么处理。
- 按条件筛选行:比如“C列大于100的所有行”。
- 单元格写入和修改:这是清洗操作的基础。
- 格式化处理:日期格式统一、去重、去空行。
- 生成汇总报告:把处理结果输出成Markdown或CSV,方便AI阅读。
- 错误处理:文件不存在、格式错误、权限问题,都要给出清晰的错误反馈。
注意,我没有一上来就做“自动生成图表”这样的重功能。原因是,MCP服务的工具粒度要小而专注,AI才能组合出复杂的处理流程。你工具做得越“大而全”,AI越难学会怎么用。就好比你教一个人做事,跟他讲“去做饭”不如告诉他“先洗菜,再切菜,最后下锅”来得可控。
3.2 技术选型:Python + openpyxl还是其他
选型的时候我做了些对比,简单说下结论。
Python生态里,操作Excel主要有三个库:openpyxl、xlrd/xlwt、pandas。pandas虽然数据分析最强,但它的强项是“批量计算”,对Excel的“格式保持”支持很差。用pandas读一遍再写一遍,你的表格样式、公式、合并单元格基本就废了。而Excel处理工作流里,很多时候是需要保留原格式的。
所以我选了openpyxl。它是一个直接操作xlsx文件的库,可以精确控制单元格样式、合并区域、公式,而且不需要本机装了Excel软件。
MCP Server这边,我用官方Python SDK(mcp库)来搭建。SDK封装了协议细节:你只需要写函数,然后用装饰器把函数暴露成工具,SDK会自动处理JSON-RPC消息和传输层逻辑。
3.3 目录结构和模块划分
我的项目结构是这样的:
excel-mcp-server/ ├── server.py # MCP服务入口,注册所有工具 ├── excel_ops.py # Excel操作核心模块,封装openpyxl ├── utils.py # 路径校验、日志等辅助函数 ├── requirements.txt └── README.md拆成两个核心模块的原因很简单:excel_ops.py里是纯Excel操作逻辑,不关心MCP协议;server.py里是协议层,负责参数解析和结果封装。以后如果我要把这个服务改成HTTP接口,只需要重写server.py,Excel操作完全可以复用。这个解耦思路值得你在自己项目里也贯彻一下。
4. 代码实现:从初始化到第一个工具跑通
4.1 环境准备和依赖安装
建议用虚拟环境隔离依赖,避免污染全局Python环境。Python 3.10以上版本,然后安装两个依赖:
pip install mcp openpyxlopenpyxl一般没什么坑,mcp库如果安装失败,多半是Python版本太低。
4.2 Excel操作模块的实现
我先写了excel_ops.py,因为它是纯业务逻辑,可以脱离MCP独立测试。核心函数如下:
# excel_ops.py import openpyxl from openpyxl.utils import get_column_letter from datetime import datetime from typing import List, Dict, Any, Optional def load_workbook(file_path: str, data_only: bool = False): """加载工作簿,统一异常处理""" try: wb = openpyxl.load_workbook(file_path, data_only=data_only) return wb except FileNotFoundError: raise ValueError(f"文件不存在: {file_path}") except Exception as e: raise ValueError(f"无法打开Excel文件: {str(e)}") def get_sheet_info(file_path: str) -> List[Dict[str, Any]]: """获取工作簿所有工作表的基础信息""" wb = load_workbook(file_path) sheets = [] for ws in wb.worksheets: sheets.append({ "名称": ws.title, "最大行数": ws.max_row, "最大列数": ws.max_column, }) return sheets def read_range(file_path: str, sheet_name: str, start_row: int = 1, end_row: Optional[int] = None, start_col: int = 1, end_col: Optional[int] = None) -> str: """读取指定范围数据,转为Markdown表格字符串返回""" wb = load_workbook(file_path, data_only=True) ws = wb[sheet_name] end_row = end_row or ws.max_row end_col = end_col or ws.max_column # 限制读取范围,防止一次性把整个大表拉进上下文 end_row = min(end_row, start_row + 100) headers = [] rows = [] for r in range(start_row, end_row + 1): row_data = [] for c in range(start_col, end_col + 1): cell = ws.cell(row=r, column=c) val = cell.value if val is None: val = "" row_data.append(str(val)) rows.append(row_data) # 转为Markdown表格 lines = [] if rows: header = rows[0] lines.append("| " + " | ".join(header) + " |") lines.append("|" + "---|" * len(header)) for row in rows[1:]: lines.append("| " + " | ".join(row) + " |") return "\n".join(lines)这里有三个细节我想单独说明。
第一,读取数据时用了data_only=True,这个参数很重要。它让openpyxl返回单元格的“计算后数值”而不是公式字符串。比如单元格里写的是=A1+B1,data_only=False时你取到的是这个公式文本,data_only=True时取到的是计算结果(前提是该文件被Excel保存过一次,缓存里才有结果)。
第二,我在read_range里做了读取范围限制(end_row = min(end_row, start_row + 100))。这一步完全是为了控制上下文长度。AI模型的上下文窗口是有限的,如果你让AI去读一个5万行的Excel表,直接把所有内容塞进上下文,轻则浪费token,重则报“上下文溢出”。更聪明的做法是:让AI每次只读一小块数据(前20行、前10列),基于采样做判断,然后再用筛选工具精确取出需要的子集。这个“抽样-判断-精确提取”的工作模式,是Excel MCP服务能否高效运行的关键。
第三,返回值用Markdown表格而不是JSON列表,是我反复调试后的决定。AI对Markdown表格的理解力比对嵌套JSON要好,而且Markdown表格可以直接展示在对话界面里,你肉眼看着也直观。JSON适合程序解析,Markdown适合模型理解和人阅读,在这个场景里Markdown明显更合适。
4.3 筛选和写入功能的实现
接下来是筛选操作。核心逻辑是:遍历指定列,逐行判断条件是否成立,收集匹配的行号和数据。
def filter_rows(file_path: str, sheet_name: str, column: int, operator: str, value: str) -> str: """按列筛选行,支持 >, <, >=, <=, ==, !=, contains""" wb = load_workbook(file_path, data_only=True) ws = wb[sheet_name] matched = [] headers = [str(ws.cell(row=1, column=c).value) if ws.cell(row=1, column=c).value else "" for c in range(1, ws.max_column + 1)] for row in range(2, ws.max_row + 1): cell_val = ws.cell(row=row, column=column).value if cell_val is None: continue # 根据操作符判断 try: if operator == ">": ok = float(cell_val) > float(value) elif operator == "<": ok = float(cell_val) < float(value) elif operator == ">=": ok = float(cell_val) >= float(value) elif operator == "<=": ok = float(cell_val) <= float(value) elif operator == "==": ok = str(cell_val) == value elif operator == "!=": ok = str(cell_val) != value elif operator == "contains": ok = value in str(cell_val) else: raise ValueError(f"不支持的操作符: {operator}") except (ValueError, TypeError): continue if ok: matched.append([str(ws.cell(row=row, column=c).value) if ws.cell(row=row, column=c).value else "" for c in range(1, ws.max_column + 1)]) # 组装Markdown表格 lines = ["| " + " | ".join(headers) + " |", "|" + "---|" * len(headers) + "|"] for row_data in matched: lines.append("| " + " | ".join(row_data) + " |") return "\n".join(lines)写入和格式化的工具有点类似,都是定位单元格然后调用openpyxl的API。写入时要注意,修改后用wb.save(file_path)保存,但保存会覆盖原文件。如果你不希望破坏原始数据,可以在工具参数里加一个output_path,把结果写到新文件。我实际使用中,几乎总是用新文件路径,防止AI操作出错把原始表改了找不回来。
4.4 MCP服务入口的实现
有了业务函数,接下来就是把它们包装成MCP工具。server.py代码如下:
# server.py from mcp.server.fastmcp import FastMCP import excel_ops as ops mcp = FastMCP("excel-server") @mcp.tool() def get_sheet_info(file_path: str) -> str: """获取Excel文件的所有工作表名称、行数和列数""" try: result = ops.get_sheet_info(file_path) return str(result) except Exception as e: return f"错误: {str(e)}" @mcp.tool() def read_excel_range(file_path: str, sheet_name: str, start_row: int = 1, end_row: int = None, start_col: int = 1, end_col: int = None) -> str: """读取Excel指定区域的数据,返回Markdown表格""" try: return ops.read_range(file_path, sheet_name, start_row, end_row, start_col, end_col) except Exception as e: return f"错误: {str(e)}" @mcp.tool() def filter_excel_rows(file_path: str, sheet_name: str, column: int, operator: str, value: str) -> str: """按条件筛选Excel行,operator可选: >, <, >=, <=, ==, !=, contains""" try: return ops.filter_rows(file_path, sheet_name, column, operator, value) except Exception as e: return f"错误: {str(e)}" if __name__ == "__main__": mcp.run(transport="stdio")这里用了FastMCP这个SDK封装类,相比直接手写协议处理,简直像是从石器时代来到了现代社会。它只需要你定义函数、加一个@mcp.tool()装饰器,就会自动完成工具注册、参数解析、错误包装等工作。工具的描述(docstring)尤其重要,因为AI模型是靠工具描述来决定什么时候调用、怎么传参数的。描述写得太笼统,AI会不知道该调;写得太复杂,AI会迷失。
4.5 在Claude Desktop里接上第一个MCP服务
把服务跑起来之后,我第一个测试平台是Claude Desktop(支持MCP的桌面版客户端都有类似配置)。
配置方式是修改客户端的配置文件,加入MCP服务器项目信息:
{ "mcpServers": { "excel-server": { "command": "python", "args": ["/你的绝对路径/excel-mcp-server/server.py"], "cwd": "/你的绝对路径/excel-mcp-server" } } }重启客户端后,在对话里发一句“帮我看一下这个Excel文件里的sheet列表”,如果返回了工作表信息,说明MCP链路已经打通了。第一次跑通这步的时候,那种感觉就像你教会了一个人怎么用你的工具箱,接下来能做什么,取决于你怎么指挥它。
5. 实操记实:让AI真正帮我处理一张脏表格
5.1 一张实际业务表格的处理对话
光说不练没什么说服力。下面是我处理一张实际用户反馈表的全过程记录。这张表是多个渠道导出的数据合并来的,问题很多:日期格式有四种、城市字段有空格、部分行数值是文本格式、还有重复记录。
我把它交给接入了MCP的AI助手,对话过程大致是这样的:
- 我问:“先看看这张表的结构。”
- AI调用
get_sheet_info,返回工作表名称、行列数。 - AI说:“共2个工作表,主表有138行、7列,我先看前10行数据。”
- AI调用
read_excel_range,读取前10行,确认列结构是:姓名、城市、下单日期、金额、渠道、状态、备注。 - 我指示:“日期列格式统一成yyyy-mm-dd,城市去掉空格。”
- AI第一步先执行一个“查看该列所有唯一值”的操作——这一步我没有写专门的工具,但AI聪明地用了筛选工具,逐个条件去查,确认了到底有哪几种日期格式。
我在第一版工具里没有提供“查看唯一值”这个工具,但AI通过组合filter_rows的contains操作,自己“创造”了一个解决问题的路径。这其实就是MCP架构最有魅力的地方——因为工具粒度小,所以AI可以灵活组合,完成我设计工具时根本没想到的流程。
格式化写入的环节,AI逐一调用写入工具,把清洗后的数据写到新文件的新工作表里。全程大概花了三分钟,其中大部分时间花在AI“思考”和调用工具上。如果是人工用Excel操作,同样的事至少得半小时。
5.2 处理过程中的几个细节问题
这个案例暴露了几个细节问题,值得单独说一下。
第一,openpyxl写入日期时,如果直接用datetime对象写入单元格,单元格会是默认的日期格式,但如果你需要特定的显示格式(比如“2025-01-01”而不是“2025/1/1”),必须显式设置单元格的number_format。
第二,中文字符串的“空格”不一定是半角空格,可能是全角空格(U+3000)或者不间断空格(U+00A0)。清洗时不能只做str.strip(),最好把所有空白字符统一替换掉。我在工具设计时增加了一个“清洗列”的工具,内部会预处理这些特殊情况。这也是实战中踩过坑、需要额外处理的地方。
第三,AI在对话中会“遗忘”工具返回的大块数据,如果中间插入太多无关对话,它可能忘了之前读到的数据细节。实操时,我倾向于让AI每一步处理后都简短总结当前状态(“目前已完成日期清洗,城市字段还需处理”),这样既有过程记录,也能帮助AI保持上下文连贯。
5.3 从“能用”到“好用”:我加的三个关键工具
第一个版本跑通后,我很快发现有几个场景是高频的,于是迭代增加了三个工具。
第一个是“列唯一值统计工具”。返回指定列的去重值和出现次数,这对AI判断数据质量非常有用,能快速发现拼写变体、分类标签不统一等问题。
第二个是“写入更新工具”,支持指定行号、列号写入值,并且可以选择只对某个工作表操作。这个工具是清洗操作的基础,基本上所有“修改单元格”的操作都走这个入口。
第三个是“导出CSV工具”。因为openpyxl保存的xlsx文件,AI直接读取文本内容并不方便(xlsx本质是zip包),但如果AI需要做进一步数据分析,导出CSV让它读文本要方便得多。实测中AI对CSV格式的理解和总结比直接读xlsx显著更好。
说真的,这三个工具花了我不到半天时间,但让整个服务的实用性直接上了一个台阶。第一个版本的“演示品”变成了真正能用的“工具”。
6. 工具选型和协议理解:为什么FastMCP是我的首选
6.1 FastMCP vs 手写协议 vs 其他SDK
MCP SDK目前有TypeScript和Python两个官方版本,社区也有第三方封装。我两种SDK都用过,最终Python+FastMCP的组合最顺手,原因也很直观。
TS SDK的优势在于很多IDE插件(比如JetBrains系列、VS Code的AI助手)本身就是TypeScript生态,对接更自然。但Python SDK更轻量,尤其配合openpyxl做Excel处理,整个技术栈更统一——Excel逻辑是Python、服务也是Python,调试不用跨语言。
在Python的MCP库中,直接用mcp库的底层API需要手写请求处理循环,代码量大且容易出错。FastMCP封装得更彻底,你用装饰器把函数暴露成工具,一行代码就能启动服务。它还能自动生成OpenAPI风格的schema,这对调试和接口文档都很有帮助。
不过FastMCP有一点要注意:它的更新速度非常快,API可能存在一些调整。我的建议是锁定版本号,或者直接记录你验证过的SDK版本,避免隔了几个月重新跑项目时因为API变动踩坑。
6.2 一个重要的判断:MCP解决的是“互通”问题,不是“智能”问题
用了MCP一段时间后,我想通了一个很重要的边界问题。MCP本身并没有让AI变得“更聪明”,它只是把AI的能力范围扩展到了外部世界。AI的推理能力、语言理解能力,全部来自大模型本身,跟MCP无关。MCP做的是让AI可以“伸手够到”那些工具和数据。
这个理解意味着:如果你的AI助手本身很笨,接上MCP并不会让它变聪明;如果你的某个工具效率很低,AI也救不了它——它调用还是那个工具,慢就是慢,毛躁就是毛躁。
所以设计好“工具”本身的质量仍然非常重要。你提供的工具越可靠、越符合人类直觉、越容错,AI就越能发挥出它的调度能力。反过来说,如果工具设计得模糊、边界不清晰,再强的AI也会被带偏。
7. 调试心得:让MCP服务稳定跑起来的关键细节
7.1 日志和错误返回:给AI一条“看得见”的出错路径
MCP服务运行时,客户端和服务器通过stdio通信,你没法直接print日志到控制台看。原因是print会污染标准输出流,导致协议解析失败。这是一个很隐蔽的坑——你在代码里写了print("hello"),整个客户端直接卡死,没有任何报错。
调试时正确的姿势有两种。第一,写到日志文件,比如logging.basicConfig(filename='mcp.log'),然后正常打日志。第二,用客户端自带的调试模式运行,会自己输出详细日志。
另外一个很重要的设计是,所有工具函数里我都把“错误信息”作为普通字符串返回,而不是抛出异常让SDK处理。原因很简单:异常信息可能会以长堆栈的形式传到AI那里,不仅占上下文,而且不利于AI理解。我直接返回“错误: 文件不存在”,AI一看就知道该换个路径或者提示用户检查文件。这种“把调试信息翻译成人话再返回”的思路,在MCP工具开发中很值得养成。
7.2 路径参数的绝对化处理
还有一个高频坑:路径问题。MCP客户端的工作目录不一定和你服务器的工作目录一致(比如IDE插件的工作目录可能是项目根目录,而你当前处理的文件在别处)。如果你在工具里用相对路径,就会遇到“文件找不到”的诡异问题。
我的解决方案很简单,在工具入口处对file_path做一次规范化处理,如果是相对路径就基于服务器的工作目录拼接成绝对路径。更进一步,我会建议AI在对话中直接传绝对路径——你可以给AI一句固定的提示语“处理文件时使用绝对路径”,它会认真遵守的。
7.3 并发和状态隔离的简单处理方式
FastMCP默认是单线程处理请求的,这意味着同一个时刻只有一个工具调用在执行。对Excel处理这类短操作来说足够用了,但如果你的某个工具会执行长时间任务(比如处理几千行的筛选和写入),期间如果用户又发了新请求,是会被阻塞的。
第一版我不建议做复杂的并发控制。保持简单、可预测,比追求高性能更重要。如果以后真的遇到性能瓶颈,再考虑用独立进程池去跑耗时任务,现在这一步先把正确性搞定。
7.4 测试工具的常用手段
测试MCP工具,有一个很实用的小技巧:先用一个简单的客户端脚本去调用工具,不需要启动完整的AI助手,只需要模拟协议消息。
自测脚本大概是这样的:
# test_client.py import asyncio from mcp import ClientSession, StdioServerParameters async def main(): server_params = StdioServerParameters( command="python", args=["server.py"] ) async with ClientSession(server_params) as session: tools = await session.list_tools() print("工具列表:", [t.name for t in tools]) result = await session.call_tool( "read_excel_range", {"file_path": "你的测试文件.xlsx", "sheet_name": "Sheet1", "start_row": 1, "end_row": 10} ) print(result) asyncio.run(main())这个脚本的价值在于,它绕过了AI模型的“理解”环节,直接测试服务本身是否正常。如果这个脚本能跑通,说明服务和工具都没问题;如果再接入AI后出问题,那多半是提示词、参数传参的问题,定位范围会小很多。
8. 踩坑实录:我在开发MCP过程中遇到的典型问题
这里整理几个我实际遇到的、非常典型的坑,附上排查思路和解决方案,做成一个速查表。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 客户端启动后报“找不到server.py” | 配置文件里的路径是相对路径 | 改为绝对路径,并检查Python是否在PATH中 |
| 服务启动但AI看不到任何工具 | 工具函数没有加@mcp.tool()装饰器,或装饰器写错位置 | 检查装饰器语法,确认工具函数在if __name__之前定义 |
| 读取数据全为空 | 用了data_only=False,碰到公式单元格取到的是公式文本或空值 | 改为data_only=True,或确保文件被Excel保存过生成缓存值 |
| 修改单元格后保存,但格式丢失 | 直接打开原文件修改后保存,覆盖了样式;或写入时未保留原样式 | 操作副本文件;写入时先用copy保留样式或重新设置样式 |
| AI反复调用同一个工具不生效 | 工具参数传错了;比如列号从1开始而不是从0开始 | 在工具描述里明确“列号从1开始”,并检查AI传参 |
| 日志打印导致通信紊乱 | 在服务代码里用了print | 使用logging写文件日志,不print到标准输出 |
| 大表一次读取过慢 | read_range没做行数限制,拉了全表 | 加上读取上限,比如每次最多读100行;或要求AI用筛选工具 |
8.1 关于“工具描述”的再思考
这个问题太重要了,单独拿出来再说一遍。AI决定是否调用工具、怎么调用工具,全靠读你的工具描述(docstring)。如果你的描述写得像“读取数据”,它可能不太确定什么时候该用;如果你写“读取Excel指定区域并返回Markdown表格,适用于查看数据概况、检查列内容”,它就会在需要“查看数据”的时候果断触发。
我的经验是,每个工具描述都包含这几类信息:工具的作用、输入参数的含义(特别是列号从1开始这类约定)、输出格式、典型使用场景。描述清晰但不冗长,一句话说清“何时用”是最关键的。
8.2 别忘了给AI定义“工具的使用边界”
还有一个容易忽视的问题是,AI可能会“擅自”用它认为合适的工具,即使这个工具并不适合当前任务。比如我遇到过,AI明明需要“筛选”,却调用了“读取范围”,然后在返回的结果里自己数了数哪些行符合条件。虽然也能得到答案,但效率低很多。
这个问题的解法是:在系统提示(system prompt)里给AI一个“工作流建议”,比如“读取数据用read_excel_range、筛选数据用filter_excel_rows、清洗数据用update_cell”。AI严格遵循提示词的程度,比你想象的要高。
9. 后续演进和扩展思路
9.1 该不该接入HTTP服务,支持远程调用
随着体验深入,你会发现本地stdio的局限逐渐显现——不能跨设备、不能多人共用。如果后续要团队协作,把MCP服务部署到一台服务器,用HTTP+SSE方式访问,是自然的演进方向。
但这里有个现实的坑:HTTP模式需要处理鉴权,否则谁拿到你的地址都能调用工具,安全风险较大。而且网络延迟会让AI调用工具变得更慢,交互体验会下降不少。我的建议是,本地单人使用坚持stdio;团队协作再上HTTP,并且务必在网关层加Token鉴权。
9.2 接入更多的Excel能力:图表、公式、宏
目前我的MCP服务覆盖了读写、筛选、清洗三大类操作,但Excel还有几个大的领域没覆盖到。一是图表生成,openpyxl支持创建柱状图、折线图等,接口也不复杂,可以作为下一迭代的方向;二是公式操作(写入公式),不过这个要慎重,因为如果你后续用pandas读取该文件,公式缓存值可能不存在,处理会有坑;三是批量处理多个工作表的工作流,这个其实不需要额外的工具,只需要让AI通过对话把你的意图拆成多个工具调用,自动循环执行。
9.3 把MCP接入IDE,真正融入日常开发
MCP对我最大的改变,其实是它融入了日常开发环境。支持MCP的IDE插件并不限于Claude Desktop,VSCode的一些AI插件也支持配置MCP服务器。我在IDE里配好excel-server后,写数据处理代码时可以直接让AI读Excel拿到数据再生成Python代码,整个链路顺畅很多。如果你平时主要在用IDE里的AI助手写代码,强烈建议配上MCP玩一玩,体验完全不一样。
10. 最后再分享一个小技巧:给AI一个固定的工作流指令
做了一个多月MCP服务之后,我最大的体会是:工具本身写得好只占一半,另一半在于怎么“教”AI用这些工具。我把自己常用的工作流指令写了一小段提示词,现在每次接新的Excel处理任务时都会粘贴到对话开头:
你是Excel数据处理专家。请按以下流程处理用户提供的Excel文件: 1. 先用get_sheet_info查看工作表结构。 2. 用read_excel_range读取前10-20行,识别列的含义和数据格式。 3. 识别数据质量问题(空值、重复、格式不一致)。 4. 与用户确认清洗规则后,逐步调用update工具清洗。 5. 清洗完成后,导出CSV或生成总结报告。 处理时所有文件路径使用绝对路径,每次操作前先说明你要做什么。实测下来,这套提示词能显著提高AI按工具操作的成功率。要记住,MCP只是给AI装了一双手,而怎么指挥这双手,仍然是一门提示词的艺术。希望这篇文章能帮你少走点弯路,早点做出自己的第一个MCP服务,让AI真正帮你把Excel里那些机械重复的活给干了。