首页 / 资讯中心 / 文章详情

从零开发第一个MCP服务:用AI重构Excel自动化工作流

从零开发第一个MCP服务:用AI重构Excel自动化工作流 ★ FEATURED ARTICLE
Excel 处理这件事说简单也简单说折磨也是真折磨。我做了七八年数据相关的工作从最早手动复制粘贴、到写 VBA 宏、再到用 Python 脚本批处理一路走过来每次觉得自己已经自动化了结果业务方甩过来一个新格式的表格又得重写一遍逻辑。直到最近我开始接触 MCPModel Context Protocol才真正意识到过去我们做的自动化本质上还是人写规则、机器执行而 MCP 带来的是一种全新的范式——让 AI 直接理解你的意图自己去调用工具完成 Excel 的读写、清洗、统计和输出。这篇文章就是记录我从零开发第一个 MCP 服务的完整过程包括为什么选这个方向、协议怎么理解、代码怎么写、踩了哪些坑以及最终跑通之后我的一些真实感受。不管你是 Python 刚入门的新手还是已经写过不少自动化脚本的老手只要你对用 AI 重构 Excel 工作流这件事感兴趣这篇内容应该都能给你一些可以直接抄作业的东西。1. 为什么我决定用 MCP 而不是继续写 Python 脚本1.1 传统 Excel 自动化的三个死结先说说我之前的做法。最常见的方案就是 Python openpyxl 或者 pandas写一个脚本读入 Excel做一堆处理再写出去。这套东西我用了好几年确实能解决很多重复劳动但它有三个绕不过去的死结。第一个死结是需求变更成本极高。比如业务方说帮我把这一列里包含华东两个字的行对应的销售额加总一下我写了个脚本。第二天他说不对我要的是包含华东或华南的而且只统计金额大于一万的。听起来只是加个条件但代码里可能涉及筛选逻辑、条件组合、边界处理改完还得重新测试。需求变三次脚本基本就得重写。第二个死结是非技术人员完全无法参与。我写的脚本只有我自己能改。同事想调整一下统计口径要么来找我要么自己学 Python。这就导致自动化变成了个人效率工具而不是团队生产力工具。第三个死结是跨工具协作极其割裂。Excel 处理完的数据可能要发给邮件、可能要录入系统、可能要生成图表。每一步都是独立的脚本或手动操作中间靠人来串联。一旦某个环节出错排查起来非常痛苦。1.2 MCP 到底改变了什么MCP 全称是 Model Context Protocol翻译过来叫模型上下文协议。你可以把它理解成一套标准接口规范让 AI 模型能够以统一的方式去调用外部工具和数据源。打个比方以前每个 AI 应用要对接一个工具都得单独写一套适配代码就像每个手机品牌都用不同的充电口MCP 相当于把接口统一成了 Type-C只要工具实现了 MCP 协议任何支持 MCP 的 AI 客户端都能直接调用。这个改变对 Excel 处理意味着什么意味着我不再需要预先写死所有处理逻辑。我只需要把读 Excel写 Excel筛选数据统计求和格式转换这些基础能力封装成 MCP 工具然后 AI 就能根据用户的自然语言描述自己决定调用哪个工具、传什么参数、按什么顺序执行。举个具体例子。用户说把销售表里华东区金额超过一万的记录挑出来按月份汇总输出到新表。在传统模式下我得写一个完整的脚本。在 MCP 模式下我只需要提供几个原子工具AI 会自动规划先调用读取工具拿到数据再调用筛选工具按条件过滤再调用分组统计工具按月份聚合最后调用写入工具输出结果。需求变了用户直接改说法就行工具不用动。1.3 为什么选 Excel 作为第一个 MCP 项目说实话MCP 能做的事情很多但我选 Excel 作为第一个练手项目有几个很实际的考虑。一是场景足够刚需。不管什么行业Excel 都是绕不开的工具。数据统计、报表生成、信息汇总几乎每家公司每天都在做。把这个场景跑通立刻就能产生价值。二是反馈足够直观。Excel 处理的结果对不对打开文件一看就知道。不像有些 AI 应用输出的是文本好不好全凭感觉。Excel 有明确的输入输出调试起来心里有底。三是技术栈足够熟悉。Python 操作 Excel 的库非常成熟openpyxl、pandas、xlrd 各有各的适用场景。我不需要花大量时间研究底层可以把精力集中在 MCP 协议本身的理解和实现上。四是扩展性足够强。一旦 Excel 的 MCP 服务跑通了后面要加 Word、PDF、CSV 的处理能力架构上完全可以复用。这就是所谓的一次投入长期受益。2. MCP 协议的核心概念拆解别被术语吓到2.1 MCP 的三种核心角色刚接触 MCP 的时候我被一堆术语搞得有点晕。后来画了个图才理清楚其实核心就三个角色。Host宿主就是用户直接交互的那个 AI 应用比如某个 AI 聊天客户端、某个 IDE 插件。它负责接收用户输入决定要不要调用工具以及怎么把工具返回的结果呈现给用户。Client客户端宿主内部的一个组件负责和 Server 建立连接、发送请求、接收响应。你可以把它理解成宿主和服务器之间的通信员。Server服务端就是我们开发者要写的东西。它暴露一组工具Tools、资源Resources和提示模板Prompts等待 Client 来调用。用生活化的类比Host 是餐厅前台Client 是服务员Server 是厨房。顾客用户跟前台说想吃什么服务员把订单传到厨房厨房做好菜再通过服务员端回来。我们开发 MCP本质上就是在开厨房定义好菜单工具列表和做菜流程工具实现。2.2 Tools、Resources、Prompts 的区别MCP Server 可以暴露三类东西刚开始我经常搞混这里说清楚。**Tools工具**是最重要的也是 Excel 场景下用得最多的。工具是可执行的操作AI 可以主动调用它来完成某件事。比如读取 Excel 文件筛选数据写入新表这些都是工具。工具的特点是有输入参数有执行过程有返回结果。**Resources资源**是可读取的数据更像是被动的信息源。比如某个 Excel 文件的当前内容、某个配置文件的参数。AI 可以读取资源但资源本身不执行操作。**Prompts提示模板**是预定义的提示词模板帮助用户快速发起某类请求。比如帮我分析这个 Excel 的销售趋势可以做成一个模板用户点一下就能触发。在 Excel 处理场景下我主要实现的是 Tools因为核心需求是执行操作而不是读取静态数据。Resources 可以用来暴露当前打开的文件信息Prompts 可以作为快捷入口但这两个优先级相对低一些。2.3 通信方式stdio 和 SSE 怎么选MCP Server 和 Client 之间需要通信目前主要有两种方式。stdio标准输入输出Server 作为一个子进程运行通过标准输入输出和 Client 通信。这种方式最简单适合本地工具不需要网络配置。我第一个版本就是用 stdio调试起来非常方便直接在终端就能看到日志。SSEServer-Sent EventsServer 作为一个独立的 HTTP 服务运行Client 通过网络连接。这种方式适合远程调用、多客户端共享的场景。但配置相对复杂涉及端口、跨域、认证等问题。对于 Excel 处理这种本地场景我强烈建议从 stdio 开始。原因很简单Excel 文件通常在本地处理逻辑也在本地没必要绕一圈网络。而且 stdio 模式下Server 的生命周期由 Client 管理不用操心进程守护、端口占用这些破事。提示如果你后续想让多个 AI 客户端共享同一个 Excel 处理服务再考虑迁移到 SSE。但第一个项目stdio 足够了。3. 从零搭建 Excel MCP Server 的完整实操3.1 环境准备与依赖选择先说环境。我用的是 Python 3.10这个版本比较稳定各种库的兼容性也好。如果你还在用 3.7 或 3.8建议升级一下因为 MCP 的 Python SDK 对版本有一定要求。依赖方面核心是三个mcp官方 Python SDK提供 Server 的基础框架和协议实现。openpyxl读写 xlsx 文件的主力库支持公式、样式、多 sheet 等高级特性。pandas数据处理和统计分析配合 openpyxl 使用效果很好。安装命令很简单pip install mcp openpyxl pandas这里有个坑要注意不要用 xlrd 读 xlsx。xlrd 从 2.0 版本开始就不支持 xlsx 了只支持老的 xls 格式。我一开始没注意报了一堆错后来换成 openpyxl 才解决。另外如果你处理的是 xls 格式老版本 Excel可以用 xlrd 1.2.0 或者转换成 xlsx 再处理。但说实话现在还在用 xls 的场景越来越少了我建议直接统一成 xlsx。3.2 第一个工具读取 Excel 文件内容先写最基础的工具——读取 Excel。这个工具的目标是给定文件路径和 sheet 名称返回该 sheet 的数据。from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import openpyxl import json app Server(excel-mcp) app.list_tools() async def list_tools(): return [ Tool( nameread_excel, description读取指定 Excel 文件的指定工作表内容返回 JSON 格式的数据, inputSchema{ type: object, properties: { file_path: { type: string, description: Excel 文件的完整路径 }, sheet_name: { type: string, description: 工作表名称不填则读取第一个工作表 }, max_rows: { type: integer, description: 最大读取行数默认 1000, default: 1000 } }, required: [file_path] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name read_excel: file_path arguments[file_path] sheet_name arguments.get(sheet_name) max_rows arguments.get(max_rows, 1000) wb openpyxl.load_workbook(file_path, data_onlyTrue) ws wb[sheet_name] if sheet_name else wb.active data [] for i, row in enumerate(ws.iter_rows(values_onlyTrue)): if i max_rows: break data.append(list(row)) return [TextContent( typetext, textjson.dumps(data, ensure_asciiFalse, defaultstr) )]这段代码有几个关键点值得说明。data_onlyTrue这个参数非常重要。Excel 里很多单元格是公式如果不加这个参数openpyxl 读到的是公式本身比如 SUM(A1:A10)而不是计算结果。加上之后读到的是公式算出来的值。当然前提是这个文件被 Excel 打开过并保存过否则缓存值可能不存在。defaultstr是为了处理日期、时间等特殊类型。openpyxl 读出来的日期是 datetime 对象直接 json.dumps 会报错。用 defaultstr 把它们转成字符串虽然不够优雅但足够实用。max_rows 限制是防止读取超大文件时内存爆炸。实际使用中如果确实需要处理几十万行数据建议用 pandas 的 read_excel 配合 chunksize 分块读取而不是一次性加载。3.3 第二个工具按条件筛选与统计读取只是第一步真正的价值在于处理。这个工具的目标是给定筛选条件和统计列返回统计结果。app.list_tools() async def list_tools(): return [ # ... 前面的 read_excel Tool( namefilter_and_sum, description在 Excel 中按条件筛选行并对指定列求和, inputSchema{ type: object, properties: { file_path: {type: string}, sheet_name: {type: string}, filter_column: { type: string, description: 用于筛选的列名或列索引 }, filter_keyword: { type: string, description: 筛选关键词包含即匹配 }, sum_column: { type: string, description: 需要求和的列名或列索引 } }, required: [file_path, filter_column, filter_keyword, sum_column] } ) ]实现逻辑上我用了 pandas 来做因为它的筛选和聚合能力比 openpyxl 强太多import pandas as pd if name filter_and_sum: df pd.read_excel( arguments[file_path], sheet_namearguments.get(sheet_name, 0) ) filter_col arguments[filter_column] keyword arguments[filter_keyword] sum_col arguments[sum_column] # 筛选包含关键词 mask df[filter_col].astype(str).str.contains(keyword, naFalse) filtered df[mask] # 求和 total filtered[sum_col].sum() result { matched_rows: len(filtered), sum_result: float(total), details: filtered[[filter_col, sum_col]].head(20).to_dict(orientrecords) } return [TextContent( typetext, textjson.dumps(result, ensure_asciiFalse, defaultstr) )]这里有个细节astype(str)。因为 Excel 里的数据可能是数字、日期、混合类型直接做 str.contains 会报错。先转成字符串再匹配虽然会损失一些类型精度但对于包含关键词这种模糊匹配来说是最稳妥的做法。返回结果里我加了 details 字段返回前 20 条匹配记录的明细。这是为了让 AI 能够看到具体匹配了什么方便它判断结果是否符合用户预期。如果只返回一个总数AI 没法验证逻辑是否正确。3.4 第三个工具写入与格式化输出处理完的数据总得输出。这个工具的目标是把处理结果写入新的 Excel 文件并做一些基础格式化。app.list_tools() async def list_tools(): return [ # ... 前面的工具 Tool( namewrite_excel, description将数据写入 Excel 文件支持指定 sheet 名称和是否加表头, inputSchema{ type: object, properties: { file_path: {type: string}, sheet_name: {type: string, default: Sheet1}, data: { type: array, description: 二维数组第一行为表头 }, auto_width: { type: boolean, description: 是否自动调整列宽, default: True } }, required: [file_path, data] } ) ]实现部分from openpyxl.styles import Font, Alignment from openpyxl.utils import get_column_letter if name write_excel: file_path arguments[file_path] sheet_name arguments.get(sheet_name, Sheet1) data arguments[data] auto_width arguments.get(auto_width, True) wb openpyxl.Workbook() ws wb.active ws.title sheet_name for row_idx, row_data in enumerate(data, start1): for col_idx, value in enumerate(row_data, start1): cell ws.cell(rowrow_idx, columncol_idx, valuevalue) if row_idx 1: cell.font Font(boldTrue) cell.alignment Alignment(horizontalcenter) if auto_width: for col_idx in range(1, len(data[0]) 1): max_len 0 for row_data in data: if col_idx len(row_data): max_len max(max_len, len(str(row_data[col_idx - 1]))) ws.column_dimensions[get_column_letter(col_idx)].width min(max_len 4, 50) wb.save(file_path) return [TextContent( typetext, textf已成功写入 {len(data)} 行数据到 {file_path} )]自动列宽这个功能看似小但实际用起来体验差别很大。没有它输出的表格列宽全是默认值长文本显示不全还得手动调整。加上之后打开就是可读的状态。不过要注意列宽计算用的是字符长度不是像素宽度。中文字符和英文字符的实际显示宽度不一样所以计算出来的列宽对中文来说可能偏窄。我的做法是给 max_len 加 4 的余量并且限制最大宽度为 50避免某一列特别长导致整个表格变形。3.5 启动 Server 与本地调试三个核心工具写完之后需要把 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 Inspector 这个官方工具。它能模拟 Client 的行为让你手动调用工具、查看输入输出非常直观。安装和启动npx modelcontextprotocol/inspector python your_server.py启动后浏览器会打开一个界面左边是工具列表右边是调用面板。填入参数点执行就能看到返回结果。这个工具帮我省了大量调试时间强烈推荐。4. 实际跑通后的踩坑记录与经验总结4.1 路径问题相对路径的坑第一个坑是文件路径。我在测试的时候用的是相对路径比如./data/test.xlsx在终端里跑没问题。但接入 AI 客户端之后Server 的工作目录变了相对路径就找不到了。解决方案很简单统一用绝对路径。在工具描述里明确写请提供完整路径并且在代码里做一次校验import os file_path arguments[file_path] if not os.path.isabs(file_path): file_path os.path.abspath(file_path) if not os.path.exists(file_path): return [TextContent(typetext, textf文件不存在{file_path})]这个校验看起来多余但实际能避免很多莫名其妙的错误。AI 有时候会脑补路径加上校验之后至少能给出明确的错误提示。4.2 数据类型问题数字变成字符串第二个坑是数据类型。Excel 里的数字读出来有时候是 int有时候是 float有时候是字符串。特别是从网页复制粘贴到 Excel 的数据经常带着不可见的空格或者特殊字符导致求和的时候变成字符串拼接。我的处理方式是在关键操作前做类型转换df[sum_col] pd.to_numeric(df[sum_col], errorscoerce)errorscoerce的意思是能转成数字的就转转不了的变成 NaN。这样即使有几行脏数据也不会影响整体的求和结果。当然转换之后要检查一下有多少 NaN如果太多说明数据源本身有问题需要先清洗。4.3 大文件处理内存和超时第三个坑是大文件。我测试的时候用了一个 5 万行的 Excelopenpyxl 加载花了将近 10 秒内存占用飙升到 500MB 以上。如果同时处理多个文件很容易把内存吃满。后来我改成了 pandas 的 read_excel 配合 usecols 参数只读需要的列df pd.read_excel(file_path, usecols[姓名, 部门, 金额])这样内存占用直接降了一半多。如果还是太大可以用 chunksize 分块读取逐块处理。虽然代码复杂一点但稳定性提升明显。另外MCP 的工具调用是有超时限制的。如果某个操作超过 30 秒还没返回Client 可能会断开连接。所以对于大文件我建议在工具描述里明确说明适用于 1 万行以内的文件超过的话建议先拆分。4.4 AI 调用工具时的幻觉问题第四个坑比较有意思。AI 在调用工具的时候有时候会自作主张地编造参数。比如用户说帮我统计一下销售表AI 可能会自己编一个文件路径传给 read_excel然后报错。这个问题没法完全避免但可以通过工具描述来引导。我的做法是在 description 里写清楚file_path 必须是用户明确提供的真实文件路径不要自行推测或编造。同时在返回错误的时候给出明确的提示return [TextContent( typetext, textf读取失败文件 {file_path} 不存在。请确认路径是否正确或让用户提供完整的文件路径。 )]这样 AI 收到错误后会转而向用户询问正确的路径而不是继续瞎猜。4.5 工具粒度的权衡最后一个经验是关于工具粒度的。一开始我想做一个万能工具输入自然语言描述输出处理结果。但后来发现这样不行因为 AI 很难准确理解一个过于复杂的工具该怎么用。正确的做法是保持工具的原子性。每个工具只做一件事参数尽量简单明确。比如读取筛选求和写入分开让 AI 自己组合。这样虽然工具数量多了但每个工具的行为可预测组合起来反而更灵活。我现在的工具列表大概是这样的工具名功能典型调用场景read_excel读取工作表内容查看数据、获取列名filter_and_sum按条件筛选并求和统计特定条件的汇总值write_excel写入新文件输出处理结果list_sheets列出所有工作表确认 sheet 名称get_columns获取列名列表确认可用列这五个工具覆盖了大部分日常需求而且每个都足够简单AI 调用起来准确率很高。5. 这套方案还能怎么扩展5.1 加入图表生成能力Excel 处理完下一步往往是可视化。我最近在尝试加入图表生成工具用 openpyxl 的 chart 模块根据数据自动生成柱状图、折线图、饼图。这样用户说把结果画成柱状图AI 就能直接调用工具生成不用再手动操作。实现思路是接收数据范围和图表类型创建对应的 Chart 对象插入到指定位置。虽然 openpyxl 的图表功能不如 Excel 原生那么丰富但应付常规需求足够了。5.2 对接更多数据源Excel 只是起点。同样的 MCP 架构可以扩展到 CSV、JSON、数据库查询结果。我下一步计划是把数据库查询也封装成工具这样用户说把上个月的订单数据导出来分析AI 就能先查数据库再写入 Excel全程自动化。5.3 多步骤工作流的编排目前每个工具都是独立的AI 需要自己规划调用顺序。未来可以考虑加入工作流模板把常见的多步骤操作预定义好。比如月度报表生成这个模板内部固定了读取、清洗、统计、格式化、输出五个步骤用户只需要提供文件路径和月份剩下的自动完成。这其实就是把 Prompts 和 Tools 结合起来用。Prompts 定义流程Tools 提供能力两者配合能大幅降低使用门槛。5.4 错误恢复与重试机制实际使用中工具调用失败是常态。文件被占用、格式不对、权限不足各种问题都可能出现。我现在的做法是简单返回错误信息但更好的方式是加入重试逻辑。比如文件被占用时等待几秒再试格式不对时尝试自动转换。这部分我还在摸索核心难点是如何区分可恢复错误和不可恢复错误。前者值得重试后者重试也是浪费时间。一个可行的方案是给每个工具定义错误码AI 根据错误码决定下一步动作。6. 一些掏心窝子的实操建议如果你也打算开发自己的第一个 MCP我有几个建议。从最小可用版本开始。不要一上来就想做全能工具先实现一个 read_excel跑通整个链路确认 Client 能正常调用、能收到返回结果。这个Hello World级别的验证非常重要能帮你排除掉环境配置、协议对接等基础问题。工具描述比代码实现更重要。AI 能不能正确调用你的工具很大程度上取决于 description 写得好不好。要写清楚这个工具做什么、什么时候用、参数是什么意思、有什么限制。我花在写 description 上的时间几乎和写代码一样多。日志一定要打全。MCP Server 运行在后台出错了如果没日志根本不知道发生了什么。我在每个工具入口和出口都加了日志记录输入参数和返回结果。调试的时候直接看日志文件比猜快得多。不要怕工具多。我一开始担心工具太多 AI 会选错后来发现只要每个工具的职责清晰、描述准确AI 的选择准确率很高。反而是一个工具做太多事的时候AI 容易用错。测试用例要覆盖边界情况。空文件、只有表头没有数据、列名有空格、数字列混了文本这些情况我都遇到过。每遇到一个就补一个测试用例慢慢就积累出一套比较完整的测试集。最后说一点感受。开发 MCP 的过程本质上是在重新思考人和工具的关系。过去我们写脚本是把人的意图翻译成机器指令现在我们写 MCP是给 AI 提供能力让 AI 去理解人的意图。这个转变听起来只是换了个位置但实际体验差别巨大。我现在的 Excel 处理工作流已经从我写脚本让电脑执行变成了我告诉 AI 我要什么AI 自己想办法完成。这种体验用过之后就回不去了。
阅读完成 · 觉得有帮助?
咨询建站