从零手写 MCP Server:让 Copilot 精准调用四则运算工具
1. 为什么我要手写一个 MCP Server1.1 从一个真实痛点说起事情是这样的我平时写代码大量依赖 VS Code 里的 Copilot 做辅助补全、解释代码、生成单元测试这些场景确实省了不少时间。但用久了就发现一个尴尬的地方Copilot 能聊天、能补全可它没法直接帮我算数。你可能会说算数这种事随便找个计算器不就行了问题在于当我在写一段涉及金额换算、单位转换、或者复杂公式的业务代码时我希望的是在对话里直接问它它给我一个准确结果而不是我自己切出去按计算器再回来。大语言模型本身做算术这件事稍微用过的人都知道它经常一本正经地胡说八道。3 位数以上的乘法、带小数的除法它给出的答案经常是错的而且错得很自信。这不是模型不行而是它的本质是预测下一个 token不是执行精确计算。所以业界通用的做法就是把精确计算这件事交给外部工具Tool让模型负责理解意图和调度让工具负责干活。这就是MCPModel Context Protocol要解决的问题。MCP 是一套让 AI 应用比如 Copilot、各种 AI 编辑器能够标准化调用外部能力的协议。而MCP Server就是这套协议里提供能力的那一端。我这次要做的就是从零手写一个 MCP Server暴露一个四则运算的 Tool让 Copilot 在对话里能直接调用它完成加减乘除。1.2 这个项目适合谁看如果你满足下面任意一条这篇内容就对你有用你天天用 Copilot / VS Code但只会用它补全代码没试过让它调用自定义工具你听说过 MCP 这个词但一直没搞明白 MCP Host、MCP Client、MCP Server 三者到底啥关系你想自己写一个 MCP Server但官方文档看得云里雾里想要一份能直接跑起来的实操记录你有 Node.js 基础想找一个不大不小、刚好能练手的项目。我这次的技术栈选的是Node.js VS Code原因很简单Node.js 生态里写 MCP Server 的 SDK 最成熟VS Code 又是 Copilot 的主场整条链路最顺。整个项目从零到跑通我实测下来大概 40 分钟其中一半时间花在环境配置和踩坑上。下面我把完整过程拆开讲包括我踩过的坑。1.3 先搞清楚三个角色Host、Client、Server在动手之前必须先把 MCP 的架构理清楚不然写着写着就晕了。我用一个生活化的类比来解释把 MCP 想象成一家餐厅。MCP Host是餐厅本身比如 VS CodeMCP Client是餐厅里的服务员MCP Server是后厨。顾客你跟服务员点菜服务员把订单传给后厨后厨做好菜再通过服务员端回来。对应到技术层面角色在本次项目里是谁职责MCP HostVS Code Copilot承载 AI 对话界面管理多个 ClientMCP ClientVS Code 内部为每个 Server 创建的连接器与 Server 建立连接、转发请求MCP Server我们自己写的 Node.js 程序暴露 Tool执行实际计算关键点在于我们只负责写 Server。Host 和 Client 由 VS Code 和 Copilot 提供我们不用管。我们要做的就是让 Server 按照 MCP 协议说对话告诉 Client 我这里有个叫 add 的工具参数是两个数字然后等 Client 把调用请求发过来算完把结果返回去。这个认知非常重要。很多人一开始会以为要自己写 Client其实完全不用。VS Code 已经内置了 MCP Client 能力你只要在配置文件里登记一下你的 Server它就会自动帮你连上。2. 环境准备与工具选型2.1 Node.js 版本选择与安装MCP 的官方 TypeScript/JavaScript SDK 对 Node.js 版本有要求我实测下来Node.js 18 及以上是硬性门槛推荐直接用 20 LTS 或者 22 LTS。为什么强调这个因为 SDK 内部用到了较新的 ESM 特性和一些 Node 内置模块的 API版本太低会直接报错而且报错信息往往很隐晦容易让人以为是代码写错了。安装步骤我不啰嗦官网下载对应系统的安装包一路下一步就行。装完之后一定要验证node -v npm -v两条命令都能正常输出版本号才算成功。如果你之前装过旧版本建议先卸载干净再装避免 PATH 里残留旧版本导致node -v显示的还是老版本。我自己就遇到过这种情况明明装了 20命令行里还是 16折腾了半天才发现是环境变量顺序问题。提示Windows 用户如果遇到npm命令找不到多半是安装时没勾选Add to PATH重新跑一遍安装程序勾上即可。2.2 VS Code 与 Copilot 的准备VS Code 下载安装没什么好说的重点说 Copilot。你需要在 VS Code 扩展市场里安装GitHub Copilot和GitHub Copilot Chat两个扩展登录你的 GitHub 账号并确保 Copilot 订阅处于可用状态确认 Copilot Chat 面板能正常打开、能正常对话。这里有个高频坑很多人反馈Copilot 在 VS Code 里突然不能用了或者对话丢失。根据我的经验90% 的情况是这几种原因扩展版本过旧、登录态失效、或者网络波动导致连接中断。解决办法依次是更新扩展到最新版、退出 GitHub 账号重新登录、重启 VS Code。如果还不行打开命令面板执行Developer: Reload Window强制重载一次基本能解决。另外要确认你的 VS Code 版本足够新因为MCP 支持是较新版本才引入的能力。如果你的 VS Code 是很久以前装的先去官网更新到最新稳定版。这一步别偷懒版本不够的话后面配置文件写了也不生效。2.3 项目初始化与依赖安装找个空目录初始化一个 Node.js 项目mkdir mcp-calc-server cd mcp-calc-server npm init -y然后把package.json里的type字段改成module因为 MCP SDK 推荐用 ESM 方式引入。改完大概长这样{ name: mcp-calc-server, version: 1.0.0, type: module, main: index.js }接着装核心依赖npm install modelcontextprotocol/sdk这个 SDK 就是官方提供的 MCP 服务端开发包里面封装了协议通信、消息序列化、传输层等一堆底层细节我们只需要关注注册工具和实现逻辑两件事。装完之后你的node_modules里会出现modelcontextprotocol目录看到它就说明装对了。注意如果你所在的环境 npm 下载慢可以配置国内镜像源加速这个属于常规操作不展开。3. 核心原理MCP Server 到底怎么和 Copilot 对话3.1 传输层stdio 是最省事的选择MCP 支持多种传输方式常见的有stdio标准输入输出和HTTP/SSE。对于本地工具类 Server我强烈推荐用 stdio。原因有三零网络配置不需要开端口、不需要处理跨域、不需要考虑鉴权Host 直接以子进程方式启动你的 Server通过 stdin/stdout 收发消息生命周期好管理VS Code 启动时拉起进程关闭时自动回收不用你手动管调试直观日志直接打到 stderr你在 VS Code 的输出面板里就能看到。HTTP 方式更适合远程部署、多客户端共享的场景但对我们这个四则运算的小工具来说完全是杀鸡用牛刀。所以本次全程用 stdio。这里有个细节要特别注意stdout 是协议通信专用通道绝对不能往里面打印任何调试信息。你如果习惯性地console.log(debug)会直接污染协议消息导致 Client 解析失败表现为工具莫名其妙不工作。调试信息一律用console.error它走的是 stderr不会干扰协议。3.2 工具注册告诉 Copilot 我能干什么MCP Server 的核心工作之一是向 Client 声明自己提供哪些 Tool。每个 Tool 需要三样东西name工具名模型靠它来识别调用哪个工具建议用英文、语义清晰比如add、subtractdescription工具描述这段文字会进入模型的上下文模型根据它判断什么时候该用这个工具所以描述要写清楚用途和适用场景inputSchema参数结构用 JSON Schema 描述模型据此生成正确的调用参数。这三者里description 和 inputSchema 的质量直接决定工具能不能被正确调用。我踩过一个坑一开始 description 写得太简单就一句做加法结果模型经常在该调用工具的时候选择自己硬算。后来我把描述改成对两个数字执行精确加法运算当需要进行数值相加且要求结果准确时使用命中率立刻上来了。这说明描述不只是给人看的更是给模型看的使用说明书。3.3 请求响应流程一次调用的完整链路把整个链路串起来看一次工具调用是这样的你在 Copilot Chat 里输入帮我算一下 1234 加 5678Copilot 的模型判断这需要调用工具于是通过 MCP Client 发出tools/call请求带上工具名add和参数{a: 1234, b: 5678}我们的 Server 收到请求执行加法得到 6912Server 把结果按协议格式返回Client 把结果交回给模型模型用自然语言组织成1234 加 5678 等于 6912回复你。理解这条链路的意义在于当工具不工作时你能快速定位是哪一环出了问题。是 Server 没启动是工具没注册成功是模型没选择调用还是参数传错了每一环都有对应的排查手段后面我会专门讲。4. 手写 Server 完整实现4.1 搭建基础骨架新建index.js先把最基础的 Server 骨架搭起来import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, } from modelcontextprotocol/sdk/types.js; const server new Server( { name: calc-server, version: 1.0.0, }, { capabilities: { tools: {}, }, } );这段代码做了两件事创建一个 Server 实例并声明自己具备tools能力。capabilities这个字段很关键它相当于能力清单Client 会根据它决定要不要向你发工具相关的请求。如果你这里没声明tools后面注册的工具根本不会被识别。4.2 注册四则运算工具接下来注册工具列表。我们用ListToolsRequestSchema来响应你有哪些工具的询问server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: add, description: 对两个数字执行精确加法运算需要数值相加且要求结果准确时使用, inputSchema: { type: object, properties: { a: { type: number, description: 第一个加数 }, b: { type: number, description: 第二个加数 }, }, required: [a, b], }, }, { name: subtract, description: 对两个数字执行精确减法运算计算 a 减 b 的差值, inputSchema: { type: object, properties: { a: { type: number, description: 被减数 }, b: { type: number, description: 减数 }, }, required: [a, b], }, }, { name: multiply, description: 对两个数字执行精确乘法运算需要数值相乘且要求结果准确时使用, inputSchema: { type: object, properties: { a: { type: number, description: 第一个乘数 }, b: { type: number, description: 第二个乘数 }, }, required: [a, b], }, }, { name: divide, description: 对两个数字执行精确除法运算计算 a 除以 b 的商除数不能为零, inputSchema: { type: object, properties: { a: { type: number, description: 被除数 }, b: { type: number, description: 除数不能为零 }, }, required: [a, b], }, }, ], }; });四个工具的结构完全一致只是名字、描述和语义不同。这里我特意把每个参数的description也写清楚了因为模型生成参数时同样会参考它。比如除法的b我标注了不能为零模型在遇到除零场景时会更谨慎。4.3 实现调用逻辑与除零保护工具声明完了还得实现真正的执行逻辑。用CallToolRequestSchema来响应调用请求server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; const a Number(args.a); const b Number(args.b); let result; switch (name) { case add: result a b; break; case subtract: result a - b; break; case multiply: result a * b; break; case divide: if (b 0) { return { content: [{ type: text, text: 错误除数不能为零 }], isError: true, }; } result a / b; break; default: return { content: [{ type: text, text: 未知工具${name} }], isError: true, }; } return { content: [{ type: text, text: String(result) }], }; });这段逻辑里有几个值得说的点第一参数强制转数字。虽然 inputSchema 里声明了type: number但实际传过来的可能是字符串不同 Client 实现有差异所以用Number()兜一层避免出现1 2 12这种字符串拼接的经典事故。第二除零必须显式处理。JavaScript 里1/0得到的是Infinity不会抛错。如果不拦模型会收到一个Infinity然后一脸懵地告诉你结果是无穷大体验很差。所以我提前判断并返回isError: true让模型知道这是一次失败的调用。第三返回值格式固定。MCP 规定工具返回的content是一个数组每项有type字段。文本结果用type: text。这个格式不能随便改否则 Client 解析不了。4.4 启动 Server 并连接传输层最后一步把 Server 和 stdio 传输层接起来const transport new StdioServerTransport(); await server.connect(transport); console.error(Calc MCP Server 已启动);注意这里用的是console.error而不是console.log原因前面讲过——stdout 是协议通道不能污染。启动日志走 stderr你在 VS Code 的输出面板里能看到方便确认 Server 是否真的起来了。到这里一个完整的 MCP Server 就写完了总共不到 100 行代码。你可以先手动跑一下node index.js如果没报错、只打印了启动日志然后挂起等待输入说明 Server 本身没问题。5. 在 VS Code 里接入 Copilot5.1 配置文件怎么写Server 写好了得让 VS Code 知道它的存在。VS Code 通过一个 MCP 配置文件来管理 Server 列表。你可以在工作区里创建.vscode/mcp.json内容如下{ servers: { calc-server: { command: node, args: [${workspaceFolder}/index.js] } } }这里command是启动命令args是参数。用${workspaceFolder}变量指向当前工作区避免写死绝对路径。如果你想让这个 Server 在所有项目里都能用可以把它配到用户级别的设置里具体位置在 VS Code 设置中搜索 MCP 相关配置项。注意路径一定要写对。我见过最常见的失败原因就是路径错了Server 根本没被拉起来但界面上又不会明确告诉你路径错误只是工具列表里空空如也。排查时先在终端里手动执行一遍node 你的路径/index.js确认能跑起来再写进配置。5.2 验证工具是否被识别配置保存后重启 VS Code 或者执行Developer: Reload Window。然后打开 Copilot Chat 面板切换到 Agent 模式这一点很重要普通问答模式不会调用工具。在工具选择入口里你应该能看到calc-server下面挂着add、subtract、multiply、divide四个工具。如果看不到按这个顺序排查配置文件 JSON 格式是否正确多余逗号、括号不匹配是最常见的路径是否指向真实存在的文件手动node index.js能否启动VS Code 版本是否支持 MCP查看输出面板里 MCP 相关日志看有没有报错。5.3 实测调用效果一切就绪后在 Copilot Chat 里输入帮我算一下 1234 乘以 5678 等于多少正常情况下你会看到 Copilot 显示正在调用 multiply 工具然后返回结果7006652。这个数字你自己按计算器验证一下是准确的。对比一下如果你直接问模型1234 乘以 5678它有一定概率算错尤其是位数多的时候。这就是工具调用的价值——把不擅长的精确计算外包出去。再试一个除零场景帮我算 100 除以 0这时工具会返回错误信息Copilot 会告诉你除数不能为零而不是给你一个莫名其妙的无穷大。这种边界处理让整个交互显得很靠谱。6. 常见问题与排查技巧实录6.1 工具不生效的排查速查表我把实际调试中遇到的问题整理成一张表方便你对照排查现象可能原因解决办法工具列表为空配置文件路径错误手动执行 node 命令验证路径工具列表为空JSON 格式错误用编辑器校验 JSON 语法Server 启动即退出依赖未安装重新 npm install调用无响应stdout 被日志污染检查是否用了 console.log模型不调用工具工具描述太模糊补充 description 的使用场景参数传错inputSchema 不严谨补全 required 和类型声明结果不对参数未转数字用 Number() 强制转换6.2 几个我踩过的坑坑一Agent 模式没开。我一开始在普通对话模式里测试怎么问都不调用工具还以为是 Server 写错了。后来才反应过来只有 Agent 模式才会主动调度工具。这个坑很隐蔽因为界面上不会提示你当前模式不支持工具。坑二description 写得太随意。前面提过工具描述是给模型看的。我最初写加法模型经常自己算。改成明确的适用场景描述后调用率大幅提升。这个经验对所有 MCP 工具开发都适用——把模型当成一个需要清晰指令的新同事。坑三忘记处理异常。一开始除法没做除零判断结果模型收到Infinity后回复得乱七八糟。加上isError标记后模型能正确理解这次调用失败了并给出合理回复。异常处理不是可选项是必选项。坑四路径用了相对路径。配置里如果写./index.jsVS Code 的工作目录可能和你想象的不一样导致找不到文件。用${workspaceFolder}或者绝对路径最稳妥。6.3 调试小技巧调试 MCP Server 有个很实用的方法先脱离 VS Code用命令行直接测。MCP 官方提供了一个 inspector 工具可以模拟 Client 向你的 Server 发请求你能直观看到请求和响应的原始报文。这样能把Server 本身的问题和VS Code 集成的问题分开排查效率高很多。另一个技巧是善用 stderr 日志。在关键分支里打console.error比如收到调用请求时打印工具名和参数返回结果时打印结果。这些日志会出现在 VS Code 的输出面板里是定位问题的一手信息。7. 后续可以怎么扩展四则运算只是个引子这套骨架能扩展的方向很多。比如你可以加一个power工具做幂运算加一个sqrt做开方甚至加一个evaluate工具接收一个表达式字符串做整体求值。工具越多模型能帮你干的事就越多。再往深了走你可以把工具从纯计算扩展到访问外部资源比如查数据库、调内部 API、读本地文件。MCP 的resources和prompts能力就是干这个的。到那时候你的 Server 就不只是个计算器而是 Copilot 连接你整个工作环境的桥梁。我个人在实际操作中的体会是MCP 这套东西的门槛不在协议本身协议其实很简单难的是怎么把工具描述写得让模型看得懂、用得对。这需要反复调试和观察是个经验活。我建议你从最简单的工具开始跑通链路然后逐步增加复杂度每加一个工具就实测一遍调用效果别一次性堆一堆工具然后发现全都不工作。最后分享一个小技巧给工具起名时尽量用动词开头的英文短语比如calculateTax、fetchUserInfo模型对这类命名的理解准确率明显更高。命名规范这件事在 MCP 工具开发里比在普通代码里更重要因为它直接影响模型的判断。