当 AI 编程助手需要访问自定义能力时,单靠提示词并不能完成真实的函数执行,工具还需要通过统一方式暴露给宿主应用。以一个加减乘除计算器为例,可以把 MCP 的协议角色、工具注册、stdio 通信和 Copilot 接入串成一条清晰链路,同时避开输出污染与错误返回等常见问题。
本文从「MCP 是什么」讲起,然后手写一个只做加减乘除的 MCP Server(Node.js 实现),最后把它接到 VS Code 的 GitHub Copilot Chat 里,用自然语言让 AI 调用我们自己的工具。
MCP(Model Context Protocol,模型上下文协议) 是一套开放协议,用来标准化 AI 应用 与 外部工具 / 数据 / 服务 之间的连接方式。
一句话理解:
MCP 就像是 AI 世界的 USB-C 接口。以前每接一个工具就要写一套专用适配代码,现在大家都按同一个协议说话,插上就能用。
在 MCP 出现之前,如果你有 N 个 AI 应用(VS Code、Claude Desktop、Cursor、自研 Agent…)和 M 个工具(GitHub、数据库、文件系统、内部 API…),理论上需要写 N × M 份适配代码:
AI 应用 工具
┌─────────┐ ┌─────────┐
│ VS Code │──────│ GitHub │
├─────────┤ ├─────────┤
│ Claude │──────│ MySQL │
├─────────┤ ├─────────┤
│ Cursor │──────│ 内部 API │
└─────────┘ └─────────┘
N 个 M 个
连线条数 = N × M
有了 MCP 之后,工具只需按 MCP 协议暴露一次能力,所有支持 MCP 的 AI 应用都能接入,连线条数从 N × M 变成 N + M:
AI 应用 MCP 协议 MCP Server
┌─────────┐ ┌─────────┐
│ VS Code │──┐ ┌──│ GitHub │
├─────────┤ │ │ ├─────────┤
│ Claude │──┼── MCP ──────┼──│ MySQL │
├─────────┤ │ │ ├─────────┤
│ Cursor │──┘ └──│ 内部 API |
└─────────┘ └─────────┘
连线条数 = N + M
MCP 只专注于「上下文交换」这一件事:
MCP 采用 客户端-服务器(Client-Server)架构:

注意:MCP Server 指的是「提供上下文数据的程序」,和它跑在哪里无关。
stdio 是 standard input/output(标准输入/输出)的缩写,也就是键盘输入、控制台输出。可以实现跨进程通信。
MCP Server 可以对外提供三种基础能力(Primitives):
| 能力 | 说明 | 典型用途 |
|---|---|---|
| Tools | AI 可调用的可执行函数(需要用户授权) | 查数据库、调 API、做计算、写文件 |
| Resources | 只读的上下文数据源 | 文件内容、数据库记录、接口返回 |
| Prompts | 预置的提示词模板 | 代码审查模板、周报生成模板 |
本文的实战只用到 Tools,因为计算器就是典型的「可执行函数」。
MCP 把协议分成两层,理解这两层基本就理解了 MCP 的全貌:
MCP
├── 数据层(Data Layer) → 「传什么」
│ └── 基于 JSON-RPC 2.0:能力发现、版本协商、Tools / Resources / Prompts / 通知
│
└── 传输层(Transport Layer) → 「怎么传」
├── stdio:标准输入输出,进程间通信,本地部署,延迟最低
└── Streamable HTTP:HTTP POST(可选 SSE 流式),本地或远程部署,支持 OAuth 等鉴权
无论用哪种传输方式,消息本身都统一是 JSON-RPC 2.0 格式。SDK 已经帮我们把这一层封装好了,日常开发基本只需要关心「注册了什么工具」。
JSON-RPC 是一种基于 JSON 格式的远程过程调用协议,让客户端可以通过发送 JSON 请求来调用远程服务器上的方法并获取结果。
很多人会把两者搞混,实际关系是:
| 维度 | Function Calling | MCP |
|---|---|---|
| 是什么 | 大模型的一种能力(输出「调用哪个函数」) | 一套协议(规定 AI 应用如何连接外部工具/数据) |
| 解决问题 | 模型如何决定调用工具 | 工具如何被标准化地接入各种各样的 AI 应用 |
| 代码写在哪 | 写在 AI 应用内部 | 写在独立的 Server 进程里,可复用 |
| 关系 | MCP 是“工具的插座”,Function Calling 是“模型伸出手去插”的那个动作 |
简单说:MCP 让工具变成可复用的标准件,Function Calling 让模型有能力去使用这些标准件。
构建 MCP 需要用到官方提供的 SDK 。本文使用 TypeScript 语言版本的 SDK 。需要 Node.js >= 20
官方 TypeScript/Node SDK(v2)要求 Node.js >= 20。
关于包名,需要注意版本差异:
| 版本 | 包名 | 说明 |
|---|---|---|
| v1 | @modelcontextprotocol/sdk | 早期单体包,网上大部分教程用的是它 |
| v2 | @modelcontextprotocol/server | 当前稳定版,实现 2026-07-28 版 MCP 规范 |
本文使用 v2 的 @modelcontextprotocol/server(服务端包已经拆成独立包,客户端是 @modelcontextprotocol/client)。
mkdir mcp-calculator
cd mcp-calculator
npm init -y
# 安装依赖:MCP 服务端 SDK + zod(用于声明工具入参的类型)
npm install @modelcontextprotocol/server zod
修改 package.json,加上 ESM 支持(SDK 是纯 ESM 包):
{
"name": "mcp-calculator",
"version": "1.0.0",
"type": "module",
"main": "calculator-server.js",
"scripts": {
"start": "node calculator-server.js"
}
}
新建 calculator-server.js:
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
// 1. 创建 MCP Server 实例,name/version 会通过 initialize 响应暴露给客户端
const server = new McpServer({ name: "calculator", version: "1.0.0" });
// 2. 抽一个公共的入参 schema:a、b 都是数字
const binaryInput = z.object({
a: z.number().describe("第一个操作数"),
b: z.number().describe("第二个操作数"),
});
// 3. 抽一个统一的返回格式:MCP 工具必须返回 content 数组
const text = (t) => ({ content: [{ type: "text", text: String(t) }] });
// 4. 注册加法工具
server.registerTool(
"add",
{
title: "加法",
description: "计算两个数字的和 a + b",
inputSchema: binaryInput,
},
async ({ a, b }) => text(a + b)
);
// 5. 注册减法工具
server.registerTool(
"subtract",
{
title: "减法",
description: "计算两个数字的差 a - b",
inputSchema: binaryInput,
},
async ({ a, b }) => text(a - b)
);
// 6. 注册乘法工具
server.registerTool(
"multiply",
{
title: "乘法",
description: "计算两个数字的积 a * b",
inputSchema: binaryInput,
},
async ({ a, b }) => text(a * b)
);
// 7. 注册除法工具(需要处理除零)
server.registerTool(
"divide",
{
title: "除法",
description: "计算两个数字的商 a / b,b 不能为 0",
inputSchema: binaryInput,
},
async ({ a, b }) => {
if (b === 0) {
return {
content: [{ type: "text", text: "错误:除数不能为 0" }],
isError: true,
};
}
return text(a / b);
}
);
// 8. 使用 stdio 传输启动服务
const transport = new StdioServerTransport();
await server.connect(transport);
// 注意:stdio 场景下日志必须走 stderr,绝不能 console.log
console.error("Calculator MCP Server running on stdio");
1)registerTool(name, config, callback) 的签名
server.registerTool(
"工具名(模型看到的函数名)",
{
title: "给用户看的标题",
description: "给模型看的说明 —— 这段文字直接决定模型会不会用、用对不用对",
inputSchema: z.object({ ... }), // 入参类型,SDK 会转成 JSON Schema
outputSchema: z.object({ ... }), // 可选:声明返回值结构
},
async (args, ctx) => {
return { content: [{ type: "text", text: "..." }] };
},
);
2)description 比代码更重要
模型只能通过 name + description + inputSchema 来判断该不该调用这个工具。所以:
description: "计算" —— 模型不知道算加还是减;description: "计算两个数字的商 a / b,b 不能为 0" —— 意图清晰,参数含义明确。3)stdio 场景下绝对不能 console.log
stdio 传输是用 标准输出 来传 JSON-RPC 消息的。你 console.log 一句日志,就会把 JSON-RPC 报文污染掉,连接直接断开。
console.log("server started"); // ❌ 会破坏 stdio 通信
console.error("server started"); // ✅ 写 stderr,安全
4)错误要用 isError 标记,而不是抛异常
抛异常会让整个请求失败,而 isError: true 会把错误信息作为工具结果交给模型,模型可以据此自我修正(比如「除数不能为 0」它会解释给用户听)。
node calculator-server.js
因为 stdio 服务在等 JSON-RPC 报文,终端里看起来「卡住不动」是正常的。要真正验证,用官方调试工具 MCP Inspector:
npx @modelcontextprotocol/inspector node $(pwd)/calculator-server.js
打开它给出的地址后,可以看到 Web 界面:

add / subtract / multiply / divide 四个工具;a=6, b=7 运行 multiply,返回 42。
mcp.jsonVS Code 通过 mcp.json 管理 MCP Server,有两个位置:
.vscode/mcp.json(推荐,可以提交到 git 共享给团队)MCP: Open User Configuration(所有工作区可用)在 mcp-calculator 目录下新建 .vscode/mcp.json:
{
"servers": {
"calculator": {
"type": "stdio",
"command": "node",
"args": ["${workspaceFolder}/calculator-server.js"]
}
}
}
字段说明(stdio 类型):
| 字段 | 必填 | 说明 |
|---|---|---|
type | 是 | stdio(本地)/ http、sse(远程) |
command | 是 | 启动命令,必须在 PATH 中或写全路径 |
args | 否 | 传给命令的参数数组 |
${workspaceFolder} 要输入真实的文件目录路径。
配置保存后,mcp.json 里对应 server 上方会出现 启动 的 CodeLens,点击 启动。
启动后,VS Code 会连接进程并发现该 Server 暴露的工具。
也可以用命令面板:MCP: List Servers → 选中 calculator → 启动服务器。
⌃⌘I);calculator 这个 MCP Server 下的四个工具;
add / subtract / multiply / divide。
在 Agent 模式下依次输入:
帮我算一下 (128 * 37) - 456 等于多少?

上述 mcp server 的源码已上传 github 仓库:github.com/Panda-plus5…
N × M 的适配爆炸变成 N + M。@modelcontextprotocol/server 写一个 MCP Server 只需要三步:建实例 → registerTool 注册工具 → connect(StdioServerTransport)。.vscode/mcp.json:配置 → 启动 → Agent 模式下勾选工具 → 用自然语言提问。