从零开发MCP服务:vibe coding的正确打开方式
聊 vibe coding我见过最多的翻车现场是开发者把需求原封不动丢给 AI然后端着一杯咖啡等它变出整个项目。结果通常是第一遍编译失败第二遍依赖冲突第三遍改坏了之前的逻辑最后只能宣布AI 写不了大项目。这还真不是 AI 能力的问题是打开方式的问题。我这一年反复实践下来比较靠谱的理解是vibe coding 不是无脑让 AI 写而是你定边界、AI 干体力活、系统给反馈。而要把这个闭环做得顺畅MCPModel Context Protocol模型上下文协议是一个极其关键的基础设施。它能让 AI 不靠猜直接调用你本地的工具和数据。这篇文章我会从零开始手把手带你把一个 MCP 服务开发出来并把 vibe coding 的正确工作流揉进每一步——包括怎么给 AI 写任务规格、怎么测试、怎么排错。不管你是后端、前端、还是刚入门没多久的开发者只要跟着走完都能跑出自己的第一个 MCP 服务并且从此对让 AI 真正干活这件事有完全不一样的理解。1. 先想清楚vibe coding 的正确打开方式到底是什么1.1 vibe coding 最常见的误区把聊天当成协作很多人对 vibe coding 的理解就是打开一个 AI 编程工具用自然语言描述需求AI 输出一大段代码然后复制粘贴跑起来。这种一问一答模式在写脚本、写函数的时候很好用但一旦项目变大就会立刻露馅。我举一个实际例子。有人让 AI 做一个团队内部用的项目周报生成器需求大概是能选项目、能填工时、能导出 Markdown。听起来很简单对吧AI 也确实给出了一个看起来完整的页面和脚本。但接下来呢路由写少了、状态管理跟需求不匹配、导出格式跟实际表格对不上、依赖版本和本机环境冲突。而这些错误都发生在 AI 看不到真实项目上下文的前提下。AI 的世界里只有你给它的那几段对话它猜不到你的项目目录结构、猜不到你的代码规范、猜不到你的真实数据格式。所以 vibe coding 的正确姿势根本不是把需求交给 AI而是把任务边界和真凭实据交给 AI。它负责把已经明确的东西快速实现你负责提供足够真实的上下文和检验标准。你可以把它想象成带新人。新人上手最好的方式不是你让他随便看看把公司业务理解一下而是给他明确的任务、真实的工具、以及验证结果的方法。vibe coding 也是一样的逻辑AI 是那个有手速、没常识的新人MCP 是它连接真实系统的工作台。1.2 MCP 是什么给 AI 装一个通用的外设接口MCP 的全称是 Model Context Protocol模型上下文协议。翻译成人话就是一套标准化的规则让 AI 应用也就是客户端能通过统一的方式去调用外部的工具、读取外部的数据。以前每个 AI 工具都要做一堆私有接口你要给它接一个数据源就得写一个专属集成。这就像每个电脑品牌都用自己的充电口特别痛苦。MCP 做的就是把接口标准化你可以把任意能力封装成一个服务任何支持 MCP 的客户端都能即插即用。对就是那个被说烂了的比喻USB-C 之于 AI。我特别想强调的是这个协议里三个你可能经常听到的概念工具Tools模型可以主动调用的函数比如扫描目录执行 Git 查询查天气。这个是这篇文章的重头戏。资源Resources模型可以读取的数据比如一个配置文件、一份文档、一张表。提示词模板Prompts预先写好的、可复用的提问模板。协议本身用的是 JSON-RPC 2.0走的是消息式的请求/响应。开发者在自己的机器上跑一个服务端程序客户端启动时会跟它握手然后客户端问它你有什么工具模型觉得需要时就让客户端调用某个工具最后把结果塞回给模型继续推理。整个过程对使用者来说是无感的。所以 MCP 对 vibe coding 的意义在哪答案四个字真实反馈。AI 不需要再靠想象力回答这个项目里有没有未完成的 TODO它可以直接调用你的 MCP 工具去扫描AI 也不需要靠猜来告诉你昨天的改动有多大影响它可以直接读 Git 状态。有了真实反馈AI 的每一轮输出质量都会高一个量级。2. 动手前的设计选题、分工和技术选型2.1 做一个有用又不复杂的 MCP 服务从需求到接口我见过很多人第一次做 MCP上来就想做一个全能助手什么数据都接、什么工具都放。结果代码量大到失控运行半天连工具列表都加载不出来。第一次做 MCP 服务的正确姿势是挑一个你觉得要是 AI 能直接调它就爽了的小功能。我这次选的主题是开发助手专门帮 AI 快速了解一个代码仓库。这非常贴合 vibe coding 的使用场景你正在让 AI 帮你改代码AI 却对你的项目毫无概念这时候它如果能自己调用工具去扫描源码、看 Git 状态整个协作效率完全是两回事。我们计划实现两个工具工具名入参输出解决的问题scan_todos项目目录路径、要匹配的关键字默认 TODO所有命中注释的文件、行号、内容摘要AI 快速知道现在有多少历史遗留工作git_status_summary仓库路径当前分支、工作区改动数、暂存区改动数、最近一次提交信息AI 快速掌握代码库当前状态这个选题还有一个好处文件系统和 Git 命令都是确定性行为结果可以人工验证。一旦服务跑起来你可以先用命令行手动执行同样的逻辑再让 AI 调用工具两边的结果一对比你就知道服务有没有写对。这种可验证性是 vibe coding 项目最珍贵的东西。2.2 技术选型为什么是 TypeScript 官方 SDK开发 MCP 服务可选的路有很多直接用协议手搓、用官方 SDK、用社区封装库语言可以用 TypeScript、Python、Go、Java。我建议第一次尝试就用 TypeScript MCP 官方 SDK原因很实际。第一官方 SDK 帮你封装了握手、生命周期、消息解析这些脏活。如果你手搓 JSON-RPC虽然能更深刻理解协议但也很容易在细节上踩坑比如某个消息的字段名不对、初始化顺序错了客户端直接静默失败。而这些坑跟你的业务逻辑一点关系都没有不值得在第一次就硬碰。第二TypeScript 的类型系统对写这种工具型服务特别友好。入参用 zod 做运行时校验AI 传过来的参数哪怕少了个字段也能在到达你的业务函数之前就被拦截住返回一个有意义的错误信息。这种健壮性在 AI 调用的场景里非常重要因为大模型生成的参数经常不那么规矩。第三本地工具服务优先选 stdio标准输入输出传输而不是 HTTP。对个人开发场景来说stdio 意味着客户端直接起一个本地进程用标准输入输出传协议消息配置最简单、启动最快也没有端口冲突、CORS、鉴权这些破事。等以后你要做成远程服务给多人用再上 HTTP 传输也不迟。我最后确定的依赖清单很克制modelcontextprotocol/sdk、zod、typescript、tsx。就这么几个后面全部是实战代码。3. 核心实现亲手写好一个 MCP 服务3.1 搭建项目骨架项目初始化先把项目目录建好依赖装上。这一步没什么魔法就是常规的 Node 工程初始化。mkdir dev-helper-mcp cd dev-helper-mcp npm init -y npm install modelcontextprotocol/sdk zod npm install -D typescript tsx types/node npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext这里我把 TypeScript 的模块配置成 NodeNext是为了让modelcontextprotocol/sdk的 ESM 导入正常工作。如果你用的是 CommonJS 风格的老项目也可以不改但新项目我更推荐直接 ESM。接着在package.json里加三个脚本{ scripts: { build: tsc, dev: tsx src/index.ts, start: node dist/index.js } }开发阶段用tsx直接跑 TypeScript 源码不用等编译交付时再用tsc编译到dist目录。这个区分在 vibe coding 的工作流里很关键它能让你每天无数次改代码-测试-再改代码的循环变得飞快。3.2 用 vibe coding 的方式让 AI 写核心代码重点在任务规格现在到了整篇文章的核心戏份怎么用 vibe coding 的方式把 MCP server 的核心代码写出来。很多人的习惯是直接问 AI帮我写一个 MCP server。这个问法太糙了AI 只能给你一个最小示例。正确的做法是在动手写代码之前先花十分钟写一段足够具体的任务规格然后把规格丢给 AI。我自己常用的提示词模板大概是这样的你可以直接照着改请用 TypeScript 和 modelcontextprotocol/sdk 实现一个 MCP server。 服务名dev-helper版本号 1.0.0。 实现两个工具 1. scan_todos 入参dirPath字符串必填、pattern字符串可选默认 TODO 行为递归扫描 dirPath 目录下的 .ts/.js/.tsx/.jsx/.py/.go/.md 文件 找出内容中包含 pattern 的行。 返回{ total: number, samples: [{ file, line, content }] } 2. git_status_summary 入参repoPath字符串必填 行为在 repoPath 下执行 git status 和 git log -1。 返回{ branch, modifiedCount, stagedCount, lastCommitMessage } 其它要求 - 使用 StdioServerTransport。 - 工具返回格式使用 MCP 的 content 数组文本类型。 - 出错时返回 { isError: true, content: [...] }。 - 不要使用 console.log 输出业务信息。这段规格里我写清楚了每个工具的入参、行为、返回结构还有协议层面的约束。AI 看到这种规格不需要猜需求它只需要把翻译工作干完。你把这段提示词喂给任何一个主流 AI 编程工具它大概率会给你一份跟下面结构差不多的代码实际生成会根据你选的 SDK 版本略有差异重点是理解结构import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; import { promises as fs } from node:fs; import path from node:path; import { execFile } from node:child_process; import { promisify } from node:util; const execFileAsync promisify(execFile); const server new McpServer({ name: dev-helper, version: 1.0.0, }); async function walk(dir: string): Promisestring[] { const entries await fs.readdir(dir, { withFileTypes: true }); const results: string[] []; for (const entry of entries) { const full path.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name node_modules || entry.name.startsWith(.)) continue; results.push(...(await walk(full))); } else { results.push(full); } } return results; } server.tool( scan_todos, { dirPath: z.string(), pattern: z.string().optional().default(TODO), }, async ({ dirPath, pattern }) { const allowedExt /\.(ts|js|tsx|jsx|py|go|md)$/; const files (await walk(dirPath)).filter((f) allowedExt.test(f)); const samples: Array{ file: string; line: number; content: string } []; for (const file of files.slice(0, 300)) { const lines (await fs.readFile(file, utf8)).split(\n); lines.forEach((lineText, idx) { if (lineText.toLowerCase().includes(pattern.toLowerCase())) { samples.push({ file: path.relative(dirPath, file), line: idx 1, content: lineText.trim(), }); } }); } return { content: [ { type: text, text: JSON.stringify( { total: samples.length, samples: samples.slice(0, 50), }, null, 2 ), }, ], }; } ); server.tool( git_status_summary, { repoPath: z.string(), }, async ({ repoPath }) { try { const { stdout } await execFileAsync(git, [status, --porcelainv1], { cwd: repoPath, }); const lines stdout ? stdout.split(\n).filter(Boolean) : []; const staged lines.filter((line) /^[MADRC]/.test(line)).length; const modified lines.filter((line) /^.[MD]/.test(line)).length; const { stdout: logStdout } await execFileAsync(git, [log, -1, --pretty%s], { cwd: repoPath, }); return { content: [ { type: text, text: JSON.stringify( { branch: , stagedCount: staged, modifiedCount: modified, lastCommitMessage: logStdout.trim(), }, null, 2 ), }, ], }; } catch (err) { return { content: [ { type: text, text: git 命令执行失败${(err as Error).message}, }, ], isError: true, }; } } ); const transport new StdioServerTransport(); await server.connect(transport);有没有发现这份 AI 生成的代码已经能用但有一个小问题git_status_summary没有返回分支名。我在规格里明确写了要返回branch它用一个空字符串糊弄过去了。这就是 vibe coding 最真实的一面AI 生成的代码需要人工检查你要带着验收的心态去读它而不是欣赏的心态。让 AI 接着把分支名补上也很简单我直接把报错/缺失反馈给它git_status_summary 返回的 branch 是空字符串请用 git branch --show-current 获取。它一般一两轮就能改好。这种规格明确—生成—检测—回灌的循环就是 vibe coding 的日常。3.3 理解一下你刚写的协议握手过程代码能跑不代表你懂了。我建议你花两分钟理解一下程序启动后到底发生了什么因为后面调试时你会天天跟这些概念打照面。整个过程是这样的客户端启动dev-helper进程进程的标准输入输出被客户端接管。客户端往标准输入写一条initialize请求包含协议版本、客户端信息、能力声明。服务端回一条initialize响应带上自己支持的协议版本和服务端信息。客户端再发一个notifications/initialized通知表示握手完成。客户端发送tools/list请求拿到服务端所有的工具描述列表。当模型决定调用某个工具时客户端发送tools/call请求服务端执行函数返回content数组。上面这些消息都是 JSON-RPC 2.0 格式一行一条通过标准输入输出传输。官方 SDK 把 2 到 5 步全部封装好了你真正要写的只有第 6 步——也就是每个工具的处理函数。有一个极其容易踩的坑。因为协议走 stdio进程的stdout标准输出是协议的专用通道你如果在业务代码里用console.log打印调试信息会把协议消息冲散客户端直接解析失败表现就是工具列表加载不出来或者连接莫名其妙断开。调试信息要写到stderr标准错误或者走 SDK 提供的日志通知机制。这个坑我可以说十个新人里八个踩过。3.4 本地测试让 MCP 服务真正跑起来写完代码第一件事是编译npm run build编译通过后你可以用 MCP 官方 Inspector 调试器来测试服务。它的作用相当于浏览器开发者工具可以手动连上服务端、查看工具列表、调用工具、看请求响应全链路。npx modelcontextprotocol/inspector node dist/index.js启动 Inspector 后在 UI 里点一下连接你就能看到dev-helper的两个工具出现在列表里。调用scan_todos让它扫描你当前这个项目目录返回结果里应该能看到我们刚才写的那些 TODO 注释。想更硬核地验证也可以不用调试器直接在终端里模拟客户端发一条tools/list请求echo {jsonrpc:2.0,id:1,method:initialize,params:{}} {jsonrpc:2.0,method:notifications/initialized,params:{}} {jsonrpc:2.0,id:2,method:tools/list,params:{}} | node dist/index.js你会在终端看到一长串 JSON 响应里面包含serverInfo和工具列表。到这一步说明你的 MCP 服务核心链路是通的。最后把这个服务注册到任意一个支持 MCP 的 AI 编程客户端里。配置文件长的都差不多关键内容如下{ mcpServers: { dev-helper: { command: node, args: [/绝对路径/到/dev-helper-mcp/dist/index.js] } } }配置好之后你在 AI 的对话窗口问一句帮我看看这个项目里还有多少没完成的 TODO它就会自己去调用scan_todos而不是编一个数字给你。这一刻你会真实感受到AI 从猜变成了查。4. 新手最容易踩的坑定位问题与排查实录4.1 一张速查表常见的 MCP 服务问题我把自己和朋友们反复踩过的坑整理了一下做成一个速查表你如果以后遇到类似现象直接按表索骥。现象可能原因解决办法客户端连接后工具列表为空server 启动失败或 stdout 被业务日志污染先手动跑node dist/index.js看有没有报错删除所有console.log调用工具时报connection closedserver 进程崩溃或未捕获异常导致退出在 handler 外层加 try/catch用 stderr 打印错误调用工具后模型说没有这个工具客户端缓存了旧的工具列表改完代码后必须重启 AI 客户端重新建立连接返回结果显示未定义工具返回格式不合规少了content数组检查返回值是否符合 MCP 协议格式工具能跑但结果明显不对路径参数被 AI 猜错了在工具描述和语法里写清楚传绝对路径不要让它猜扫描大项目时超时遍历目录没有排除node_modules加忽略规则限制文件数量超出就返回摘要错误信息被模型当成正常结果失败时没有设置isError: true所有异常分支都返回isError: true这里面我特别想展开说两条。第一条console.log污染 stdout 的问题它的隐蔽性在于服务端本地跑的时候完全正常只有通过客户端连接时才会出问题。你排查半天以为是协议写错了结果就是把一个调试日志删了的事。建议从一开始就养成习惯所有业务日志一律走console.error。第二条路径问题。AI 调工具时如果工具描述里没写清楚路径怎么传它很可能传一个相对路径然后扫描结果为空。你在做给 AI 用的工具时立场要站到 AI 那边它能拿到什么信息、怎么表达最不容易出错。我的做法是在工具描述第一行就写请使用绝对路径并在入参的 zod 描述里再强调一遍。4.2 三个让 vibe coding 效率翻倍的排查习惯除了具体的错误我更想分享三个工作习惯。它们不算技术但直接影响你用 vibe coding 了一天之后的血压。第一个习惯是先人工后 AI。任何工具接入 AI 之前先自己在命令行里手动跑一遍背后的逻辑。比如git_status_summary底层就是git status你先在仓库里手动执行确认输出符合预期再让 AI 去调。如果 AI 调用失败你就知道问题大概率出在协议封装或参数传递上而不是业务逻辑本身。这个隔离排查法能帮你省掉绝大多数的瞎折腾。第二个习惯是报错要整段回灌。AI 在 vibe coding 过程中代码写错了你千万不要自己看完报错之后把错误总结成一句话再告诉它。直接把原始报错贴给它让它自己读。因为你在转述的过程中很容易丢失关键字段比如某个库版本的兼容性提示。我自己试过很多次把TypeError: xxx is not a function整段贴过去AI 往往能一眼定位到是 API 版本变了。第三个习惯是一次只让它改一个点。让 AI 同时改五个地方它改到第三个的时候通常已经把前两个改没了。正确做法是一个循环只处理一个问题验证通过再进下一个。你会发现AI 的上下文窗口越小、任务越聚焦代码质量越稳定。这也是 vibe coding 和传统编程思维最大的区别它适合大量小步快跑的迭代不适合一锅端的重构。5. 进阶方向与我的实操心得5.1 这个服务还能往哪走从工具到资源、到远程服务现在你已经有了一个能跑的 MCP 服务接下来可以很自然地往几个方向延伸。一个是加 resources。比如把项目的 README 或配置文件暴露成资源AI 在对话时可以直接读取不需要你再手动复制粘贴。这在让 AI 快速理解项目的场景里特别有用。另一个是加 prompts。把帮我 review 最近的代码改动这种常用指令做成一个模板AI 客户端里一键就能唤起不用每次重新敲一遍。再一个是传输层的升级。如果你希望这个服务部署在服务器上给多个人用就需要把传输方式从 stdio 改成 HTTP加上鉴权信息。这个方向适合团队工具可以等你的服务稳定了再考虑。我个人觉得MCP 的价值不在于协议本身多复杂而在于它让 AI 工具生态有了一个统一的插槽。你今天给 AI 写的小工具明天可能就跑到别的客户端里被别的模型调用不需要重写。这种标准化的杠杆效应才是它真正迷人的地方。5.2 最后分享一点关于 vibe coding 的真话写完这个服务再回头看我最大的体会是vibe coding 能不能用好的分水岭不在模型强弱而在你有没有给 AI 创造可验证的环境。MCP 恰好就是那个把真实世界接入对话的工具。我踩过很多坑之后现在的工作流已经很固定了先用十分钟把任务规格写清楚再让 AI 写代码写完立刻编译编译不过就把报错整段回灌能跑通之后再通过 MCP 工具让 AI 自己检查结果。整个循环里我负责判断AI 负责执行大家各司其职。如果你要亲手试的话我的建议是从一个比开发助手更小的工具开始比如只做一个扫描 TODO 的 MCP 服务。跑通之后你会对整个链路有体感再回头去看其它成熟的 MCP 服务思路会清晰很多。最后帮你留个记忆点MCP 服务的工具描述一定用给同事看的接口文档的标准去写因为你的下一个调用者大概率是一个不太有耐心、但非常有执行力的 AI。