从零手写 MCP Server:让 Copilot 精准调用四则运算工具
2026/9/23 10:42:32 网站建设 项目流程

1. 为什么我要手写一个 MCP Server

1.1 从一个真实痛点说起

事情是这样的,我平时写代码大量依赖 VS Code 里的 Copilot 做辅助,补全、解释代码、生成单元测试这些场景确实省了不少时间。但用久了就发现一个尴尬的地方:Copilot 能聊天、能补全,可它没法直接帮我算数。你可能会说,算数这种事随便找个计算器不就行了?问题在于,当我在写一段涉及金额换算、单位转换、或者复杂公式的业务代码时,我希望的是"在对话里直接问它,它给我一个准确结果",而不是我自己切出去按计算器再回来。

大语言模型本身做算术这件事,稍微用过的人都知道,它经常一本正经地胡说八道。3 位数以上的乘法、带小数的除法,它给出的答案经常是错的,而且错得很自信。这不是模型不行,而是它的本质是"预测下一个 token",不是"执行精确计算"。所以业界通用的做法就是:把精确计算这件事交给外部工具(Tool),让模型负责理解意图和调度,让工具负责干活。

这就是MCP(Model 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 Code),MCP Client是餐厅里的服务员,MCP Server是后厨。顾客(你)跟服务员点菜,服务员把订单传给后厨,后厨做好菜再通过服务员端回来。

对应到技术层面:

角色在本次项目里是谁职责
MCP HostVS Code + Copilot承载 AI 对话界面,管理多个 Client
MCP 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。你需要:

  1. 在 VS Code 扩展市场里安装GitHub CopilotGitHub Copilot Chat两个扩展;
  2. 登录你的 GitHub 账号并确保 Copilot 订阅处于可用状态;
  3. 确认 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:工具名,模型靠它来识别调用哪个工具,建议用英文、语义清晰,比如addsubtract
  • description:工具描述,这段文字会进入模型的上下文,模型根据它判断"什么时候该用这个工具",所以描述要写清楚用途和适用场景;
  • inputSchema:参数结构,用 JSON Schema 描述,模型据此生成正确的调用参数。

这三者里,description 和 inputSchema 的质量直接决定工具能不能被正确调用。我踩过一个坑:一开始 description 写得太简单,就一句"做加法",结果模型经常在该调用工具的时候选择自己硬算。后来我把描述改成"对两个数字执行精确加法运算,当需要进行数值相加且要求结果准确时使用",命中率立刻上来了。这说明描述不只是给人看的,更是给模型看的"使用说明书"。

3.3 请求响应流程:一次调用的完整链路

把整个链路串起来看,一次工具调用是这样的:

  1. 你在 Copilot Chat 里输入"帮我算一下 1234 加 5678";
  2. Copilot 的模型判断这需要调用工具,于是通过 MCP Client 发出tools/call请求,带上工具名add和参数{a: 1234, b: 5678}
  3. 我们的 Server 收到请求,执行加法,得到 6912;
  4. Server 把结果按协议格式返回;
  5. 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 里接入 Copilot

5.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下面挂着addsubtractmultiplydivide四个工具。

如果看不到,按这个顺序排查:

  1. 配置文件 JSON 格式是否正确(多余逗号、括号不匹配是最常见的);
  2. 路径是否指向真实存在的文件;
  3. 手动node index.js能否启动;
  4. VS Code 版本是否支持 MCP;
  5. 查看输出面板里 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.js,VS 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 的resourcesprompts能力就是干这个的。到那时候,你的 Server 就不只是个计算器,而是 Copilot 连接你整个工作环境的桥梁。

我个人在实际操作中的体会是:MCP 这套东西的门槛不在协议本身,协议其实很简单,难的是怎么把工具描述写得让模型"看得懂、用得对"。这需要反复调试和观察,是个经验活。我建议你从最简单的工具开始,跑通链路,然后逐步增加复杂度,每加一个工具就实测一遍调用效果,别一次性堆一堆工具然后发现全都不工作。

最后分享一个小技巧:给工具起名时尽量用动词开头的英文短语,比如calculateTaxfetchUserInfo,模型对这类命名的理解准确率明显更高。命名规范这件事,在 MCP 工具开发里比在普通代码里更重要,因为它直接影响模型的判断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询