我的第一个MCP,以及开发过程中的经验感悟

起心动念

上周开发完 sheetex 后,发了条朋友圈。有小伙伴建议搞个 MCP 玩,正好我本来也想学,于是这周就花了一天完成了 sheetex-mcp-server,一个将对话中生成的表格保存成 Excel 的 MCP 服务。

做之前快速调查了一下 smithery 和 modelscope ,发现已经有好几个 Excel 相关的:实现上既有调用本机上的 Office 软件进行操作的,也有用库读写文件的;功能就更加眼花缭乱,从简单读写数据,到插入图表,甚至可以截图保存。

看来是打不过了,好在只是做个练习,开搞。

一天下来,学到不少东西,也填了好几个坑,本文以坑为主。

那么下面就按顺序来了。

新手上路

Build an MCP Server 是官方的教程,新手入门刚刚好,它通过调用天气相关的接口演示了 MCP Server 的开发过程。

第1个知识点:MCP有两个模式,基于 stdio 或 http,基于 stdio 时不能使用 console.log 等向标准输出输出内容的方法,会扰乱 JSON-RPC 消息

紧接着,第1个坑:

文档中用下面这段代码引导读者创建项目目录、初始化、安装依赖

html 复制代码
# Create a new directory for our project
mkdir weather
cd weather

# Initialize a new npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript

# Create our files
mkdir src
touch src/index.ts

问题出在第9行,安装时没有指定版本,目前运行这行命令,安装的 zod 是最新版本4.x.x,写文档时应该还是 3.x.x,两者存在兼容问题,最终表现就是接入 MCP 客户端后,客户端无法识别工具需要的参数,以这个项目为例,正常情况下,参数长这个样子

误装 zod@4.x.x 后,properties 是空的,required 也没了,这样模型就不知道该传什么参数,导致完全无法使用。

实际开发中,很可能把功能写完了,要测试才发现问题,很容易怀疑是不是自己代码写错了,比如参数定义是不是出了问题。第1个提示:建议第一次尝试的开发者,先写一个最基础的工具进行测试 ,类似输入参数只有 name,输出则是 hello, ${name} 这样的,确保环境以及基本的语法没有问题。

剩下的就比较简单,只要按照文档中的指引操作,并把其中天气相关的逻辑,改成导出 Excel 就好了。

模型能力

接下来是第2个坑:模型规模导致能力有限,无法很好的调用工具

第一个测试能跑通后,我换着模型进行了测试,看看会不会由于模型的能力导致调用发生意外。刚开始,我的接口参数是这样定义的(部分描述进行了简化)。

javascript 复制代码
{
  folderName: z.string().describe(`The output directory.`),
  fileName: z.string().max(32).describe('The name of the Excel file to create, excluding the .xlsx extension'),
  caption: z.string().max(32).optional().default('').describe('Sheet title'),
  headers: z.array(z.string()).describe('Column headers for the sheet'),
  data: z.array(
    z.array(
      z.string().or(z.number()).describe('Each value can be a string or a number.')
    ).describe('Each inner array represents one row of the sheet.')
  ).describe('The full dataset to export, provided as a two-dimensional array.'),
}

其中最复杂的是 data 这参数,用 TypeScript 来描述是 (string | number)[][] ,就是让大模型用二维数组来表达单元格中的数据。

我一开始担心的是小规模模型无法正确理解和组织复杂的参数,结果证明这并不是问题,本地跑 Qwen3:7B 也能很正确的理解并输出这个结构,并且在大多数时候还能正确的区分文本和数字,比如让它模拟一份成绩单,他会用文本表示姓名,用数字表示成绩,大概就是这样:

javascript 复制代码
{
  "caption":"学生成绩单",
  "data":[
    ["张三",85,90,88,82,95,80,78,85,92,700],
    ["李四",78,85,92,88,80,95,82,85,78,800],
    ["王五",92,88,76,90,85,88,92,80,85,756],
    ["赵六",80,85,90,82,88,95,76,88,85,734],
    ["陈七",88,92,85,90,82,88,95,80,85,735],
    ["周八",76,80,85,88,92,80,85,90,82,716],
    ["吴九",90,85,88,82,80,92,85,88,80,700],
    ["郑十",82,88,90,85,92,80,88,85,76,716],
    ["孙十一",85,80,82,90,88,95,82,85,80,707],
    ["李十二",88,90,85,82,80,88,92,85,76,716]
  ],
  "fileName":"学生成绩单",
  "folderName":"Downloads",
  "headers":["姓名","语文","数学","英语","物理","化学","生物","历史","地理","政治","总分"]
}

刚想说自己的担心是多余的,结果却在其他问题上翻了车,比如当要求它把前面对话中出现过的表格保存成文件时,会把表头当作数据传进来(headers里传了表头,但是数据第一行也是表头),导致表头重复,如:

甚至在我要求合并三份数据时,出现了表头和内容不一致的情况,表头是想并列展示,内容却想按列表展示。

基本上可以确定,在模型能力较弱的时候,将表头和数据分成两个参数是错误的决定。

紧接着,接下来的一个测试让我彻底放弃了二维数组。我用的客户端是 BoltAI,是 Setapp 里的,有一定的免费额度可以通过 Setapp 间接调用 ChatGPT 的模型,结果直接报错,无法识别这个参数。第3个坑:MCP需要客户端、服务端以及模型接口三方配合,但支持并不是简单的 yes or no,在参数的实现上还有更细粒度的差异。

问题遇到不少,但乐观一点想,卖点不就来了,可以把 sheetex-mcp-server 搞成兼容性最好,对模型要求最低的。

于是我直接把 headersdata 两个参数废了,换成 table ,让模型用 markdown 格式把表格传进来,我还真没见过AI能在对话里把表格给画歪了。

javascript 复制代码
{
  folderName: z.string().describe(`...`),
  fileName: z.string().max(32).describe('The name of the Excel file to create, excluding the .xlsx extension'),
  caption: z.string().max(32).optional().default('').describe('Sheet title'),
  table: z.string().describe('The dataset to export, provided as a markdown table'),
}

一下子清爽了很多。

既然用了 markdown,额外的信息也不要浪费,大模型很喜欢在表格里加些小小的样式,比如加粗啥的,用二维数组这些信息就无法表达,而 markdown 格式能传达。虽然 sheetex 搞不了富文本,但是整个单元格的加粗、斜体、删除线还是可以胜任的,配合 remove-markdown 把多余样式擦除,就成了。

再进一步,把列宽也整一整,根据全角和半角字符的数量估算一下宽度,设置好上限,再配合自动换行。

又顺手解决了一个 gpt-oss:20b 会漏掉结尾 | 符号的问题,效果就出来:

讲完输入参数,再讲讲输出。MCP 工具调用完毕后,需要给大模型反馈一个结果:

javascript 复制代码
return {
  content: [
    {
      type: 'text',
      text: `Operation successful, the file has been saved to: ${dist}`,
    },
  ],
};

这里的提示也不能写的太随意,我第一稿写成 Save to ${dist} ,想表达文件最终是保存到这个位置了,但大模型可能是误解成我给了它一个新的指令,让它将文件保存到这个位置,结果又调用一遍工具进行保存,陷入了循环。所以,提示2:MCP工具返回的信息要明确,大模型是可能将它当作新的指令来执行的。 我想这应该也能让多个MCP工具的配合更加灵活,让一个工具主动要求使用其他工具变得更加方便,而不像传统的接口调用,只能由主程序根据上一个接口返回的结果做基于 if else 的简单判定

模型偏好

接下来这个问题,与其说是模型的能力补足,更像是某种偏好。

第4个坑:模型倾向于过度或过早调用 MCP 工具。

特别是非思考模式的模型。

当你让他帮你"模拟一张学生成绩单,提供10条数据时",你可能是想先让它先输出出来,看一眼,然后决定要不要保存到文件,但是模型经常过于积极,直接就保存成文件了。

第二个场景,就是当你让他将前文中出现的数据整理成表格并保存成文件时(前文中还不是表格形式),会出现模型将没有完全思考完的结果保存成文件,然后继续进行思考,返回给你一个更加完善的版本,在对话框里呈现出来。把我给看懵了,还能这么搞的。

目前对于这个问题,我还没有太好的解决方法,只能先把 mcp-server 给关了,等跟模型聊透了,对话中已经有了我想要的表格,然后再打开对应的 mcp-server,再明确的让它对上文的指定部分进行保存。

尾声

本文总结了我在第一次开发 MCP Server 时遇到的问题和几点心得,碍于篇幅与时间,并没有将发布阶段以及尝试接入 smithery 平台时遇到的相关的内容纳入,有机会再单开一篇吧,再见!

相关推荐
没有不重的名么2 分钟前
Tmux Xftp及Xshell的服务器使用方法
服务器·人工智能·深度学习·机器学习·ssh
wayman_he_何大民20 分钟前
初识机器学习算法 - AUM时间序列分析
前端·人工智能
什么都想学的阿超1 小时前
【大语言模型 00】导读
人工智能·语言模型·自然语言处理
lxmyzzs1 小时前
【图像算法 - 16】庖丁解牛:基于YOLO12与OpenCV的车辆部件级实例分割实战(附完整代码)
人工智能·深度学习·opencv·算法·yolo·计算机视觉·实例分割
明心知2 小时前
DAY 45 Tensorboard使用介绍
人工智能·深度学习
维维180-3121-14552 小时前
AI大模型+Meta分析:助力发表高水平SCI论文
人工智能·meta分析·医学·地学
程序员陆通2 小时前
CloudBase AI ToolKit + VSCode Copilot:打造高效智能云端开发新体验
人工智能·vscode·copilot
程高兴2 小时前
遗传算法求解冷链路径优化问题matlab代码
开发语言·人工智能·matlab
拾零吖2 小时前
吴恩达 Machine Learning(Class 1)
人工智能·机器学习
数据皮皮侠3 小时前
最新上市公司业绩说明会文本数据(2017.02-2025.08)
大数据·数据库·人工智能·笔记·物联网·小程序·区块链