从空文件夹创建一个 TypeScript MCP Server,依次写出 hello、add 和时间 Tool,用 MCP Inspector 调试后接入 CodeBuddy。
用 TypeScript 写一个本地 MCP Server,再交给 AI Client 使用。
这篇教程,我们从一个空文件夹开始,写一个简单的 MCP Server。
它会提供三个小功能:打招呼、计算加法、查询指定时区的当前时间。我们先写最小版本,用 MCP Inspector 手动调用;确认代码没问题后,再把它接入 CodeBuddy。
MCP 全称 Model Context Protocol。它规定 AI 应用怎样发现和调用外部能力。先记住两个词就够了:Tool 是一个具体能力,MCP Server 是把这些能力开放给 AI 的程序。
严格来说,本文写的是一个 MCP Server。标题里的“写一个 MCP”沿用大家平时更常见的说法。
1. 准备环境
这次使用 Node.js、npm 和 TypeScript。MCP TypeScript SDK v2 的官方入门文档要求 Node.js 20 或更高版本;本文原始实验使用 Node.js 22.23.1,之后又在 Node.js 24.15.0 上复验了一遍。
打开 PowerShell,检查版本:
node -v
npm -v只要两条命令都能返回版本号,就可以继续。版本不同没关系,Node.js 不低于 20 即可。
如果提示“无法识别 node”,请从 Node.js 官网安装 LTS 版本,安装后重新打开 PowerShell。已经能看到版本号时,不需要重装。
代码编辑器可以使用 VS Code、CodeBuddy 或其他能编辑 TypeScript 的工具。本文截图来自 CodeBuddy,但写 MCP 并不依赖它。
2. 创建项目
先创建文件夹,再初始化 npm 项目:
mkdir my_mcp
cd my_mcp
npm init -y看到 package.json 就说明 npm 项目初始化成功了。
接着开启 ES Module,并添加启动命令:
npm pkg set type=module
npm pkg set scripts.start="tsx src/index.ts"安装这次要用的三个依赖:
npm install @modelcontextprotocol/server@2.0.0 zod@4.4.3 tsx@4.23.12它们各自负责一件事:
@modelcontextprotocol/server:创建 MCP Server;zod:描述并检查 Tool 收到的参数;tsx:直接运行 TypeScript 文件,省去单独编译。
最后创建源码目录和入口文件:
mkdir src
New-Item -ItemType File -Path .\src\index.ts现在的目录应该是:
my_mcp/
├─ node_modules/
├─ src/
│ └─ index.ts
├─ package-lock.json
└─ package.json到这里的成功标志: package.json 中能看到 "type": "module",依赖安装没有以红色报错结束。
如果安装失败,先运行 node -v 和 npm -v。环境本身没有版本号时,继续改代码也解决不了问题。
3. 写第一个 hello Tool
打开 src/index.ts,粘贴下面的代码:
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";
function createServer(): McpServer {
const server = new McpServer({
name: "my-first-mcp",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "向一个人打招呼",
inputSchema: z.object({
name: z.string().describe("要打招呼的人的名字"),
}),
},
async ({ name }) => {
return {
content: [
{
type: "text",
text: `你好,${name}!`,
},
],
};
},
);
return server;
}
void serveStdio(createServer);
console.error("My MCP server running on stdio");先看真正会影响调用的几处:
new McpServer(...)创建 Server,my-first-mcp是它向客户端报告的名字;registerTool('hello', ...)注册一个名为hello的 Tool;description告诉 AI 这个 Tool 是干什么的;inputSchema规定参数里必须有一个字符串name;- 最后的异步函数是 handler,也就是收到调用后真正执行的代码;
content是 Tool 交回客户端的结果。
z.object(...) 写出的规则叫 Schema。它像入口处的检查表:这里要求 name 必须是字符串。客户端少传参数或传错类型时,SDK 会在进入 handler 前拒绝这次调用。
最下面的 serveStdio(createServer) 让 Server 通过标准输入和标准输出与客户端通信。这种方式叫 stdio,适合由本地 AI 客户端启动的 MCP Server。
还有一个容易踩的坑:stdio 模式不要用 `console.log()` 打日志。 stdout 要留给协议消息,普通日志请写到 stderr,所以这里使用 console.error()。
4. 先运行一次
保存文件,在项目根目录运行:
npm start终端应该显示:
My MCP server running on stdio然后它会停在那里,没有继续输出。这是正常现象。MCP Server 已经启动,正在等待 Client 从 stdin 发来请求。
成功标志: 终端没有报错,进程持续运行。
按 Ctrl+C 停止它。下一步由 Inspector 帮我们启动 Server,因此这里不要继续占着终端。
如果遇到 Cannot find package,先确认当前路径下确实有 package.json 和 node_modules,再重新运行 npm install。
5. 用 MCP Inspector 调用 hello
MCP Inspector 是官方提供的本地调试工具。它会启动我们的命令,连接 Server,再把 Tool 列表和调用结果展示在网页里。
在 my_mcp 根目录运行:
npx @modelcontextprotocol/inspector npx tsx src/index.ts第一次运行需要下载 Inspector,稍等片刻后浏览器会打开本地页面。点击 Connect,再进入 Tools。
此时左侧应该出现 hello:
选择 hello,在 name 中输入“盾山”。输入完成后应该是这样:
点击 Execute Tool,结果区域会显示:
到这里,我们已经完成了第一次真实调用。Inspector 先询问 Server 有哪些 Tool,再根据 inputSchema 生成输入框;点击按钮后,它把参数交给 hello 的 handler,最后显示 handler 返回的 content。
如果 Tools 中没有 hello,先回到启动 Inspector 的终端看报错。最常见的原因是命令运行位置不在 my_mcp 根目录,或者 src/index.ts 没有保存。
Inspector 启动时会在终端和本地 URL 中放入临时 Auth Token。它用于保护本地调试服务,不要把完整 URL 或 Token 发到公开文章和群聊里。
6. 再加两个 Tool
hello 已经证明基础链路能工作。接下来在同一个 Server 里加入:
add:接收两个数字,返回它们的和;get_current_time:接收一个时区,返回当地时间。
把 src/index.ts 替换为下面的完整版本:
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";
function createServer(): McpServer {
const server = new McpServer({
name: "my-first-mcp",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "向一个人打招呼",
inputSchema: z.object({
name: z.string().describe("要打招呼的人的名字"),
}),
},
async ({ name }) => {
return {
content: [
{
type: "text",
text: `你好,${name}!`,
},
],
};
},
);
server.registerTool(
"get_current_time",
{
description: "获取指定时区的当前时间",
inputSchema: z.object({
timezone: z
.string()
.optional()
.describe("时区,例如 Asia/Shanghai;不填写则使用电脑系统时区"),
}),
},
async ({ timezone }) => {
try {
const formatter = new Intl.DateTimeFormat("zh-CN", {
timeZone: timezone,
dateStyle: "full",
timeStyle: "long",
});
const currentTime = formatter.format(new Date());
return {
content: [
{
type: "text",
text: currentTime,
},
],
};
} catch {
return {
content: [
{
type: "text",
text: `无法识别这个时区:${timezone}`,
},
],
isError: true,
};
}
},
);
server.registerTool(
"add",
{
description: "计算两个数字的和",
inputSchema: z.object({
a: z.number().describe("第一个数字"),
b: z.number().describe("第二个数字"),
}),
},
async ({ a, b }) => {
const result = a + b;
return {
content: [
{
type: "text",
text: `${a} + ${b} = ${result}`,
},
],
};
},
);
return server;
}
void serveStdio(createServer);
console.error("My MCP server running on stdio");这里没有引入新的 MCP 写法。两个 Tool 仍然由名称、说明、参数 Schema、handler 和返回结果组成。
时间 Tool 多做了一层错误处理。Asia/Shanghai 是合法时区;如果传入不存在的名称,handler 会返回 isError: true,让客户端知道这次调用失败了。
7. 再用 Inspector 检查
修改代码后,停止旧的 Inspector,再重新运行:
npx @modelcontextprotocol/inspector npx tsx src/index.ts进入 Tools,先调用 add:
a = 5
b = 6预期结果是:
5 + 6 = 11再调用 get_current_time:
timezone = Asia/Shanghai你会看到当时的上海日期和时间。不同 Node.js 版本可能把时区后缀显示为“中国标准时间”或 GMT+8,两者都正常。
打开 Inspector 右侧的 Protocol,还能看到一次 Tool 调用的原始消息:
这时再认识两个协议动作会轻松很多:
tools/list:Client 询问 Server 提供哪些 Tool;tools/call:Client 指定 Tool 名称和参数,要求 Server 执行。
截图中的 Parameters 是发给 add 的参数,Response 是 Server 返回的结果。界面显示 LEGACY 时也不用改代码:v2 的 serveStdio 默认兼容旧版 Client,Inspector 会根据连接协商协议版本。
下面这张图把目前的关系收在一起:
Tool 是 hello、add 和 get_current_time 这些具体能力。MCP Server 负责登记并执行它们,AI Client 负责发现和调用。
8. 接入 CodeBuddy
Inspector 适合开发阶段手动测试。代码确认没问题后,可以把同一个 Server 交给真正的 AI Client。
本文用 CodeBuddy 演示。其他支持本地 stdio MCP 的客户端也能使用同样的 Server,只是配置入口和 JSON 文件位置可能不同。
先停止 Inspector。然后在 CodeBuddy 中打开设置,进入 MCP,点击“配置 MCP”:
在 PowerShell 中取得入口文件的绝对路径:
(Resolve-Path .\src\index.ts).Path把得到的路径填入 MCP 配置。下面使用一个示例路径,请换成你自己的实际结果:
{
"mcpServers": {
"my-first-mcp": {
"command": "npx",
"args": ["tsx", "C:\\你的路径\\my_mcp\\src\\index.ts"]
}
}
}Windows 的 JSON 路径要写双反斜杠,例如 C:\\Users\\...。当然,这一步完全可以让 CodeBuddy 帮你做,和它说就行。
保存后重启或重新连接 MCP。CodeBuddy 的 MCP 列表里出现 my-first-mcp,并且状态正常,就说明它已经启动了我们的 TypeScript 程序。
如果连接失败,先在同一个项目目录运行 npx tsx src/index.ts。这条命令能启动时,再检查配置中的绝对路径;它本身也报错时,先修代码或依赖。
9. 让 AI 自己选择 Tool
现在给 CodeBuddy 一条普通的自然语言要求:
用我的 MCP 帮我算一下 56 + 78,
再用我的 MCP 查看现在上海几点这次不需要手动点 add 或 get_current_time。CodeBuddy 读取 Tool 的名称、description 和 inputSchema,判断该用哪个 Tool,再填入参数发起调用。
实际结果如下:
CodeBuddy 先调用 add,传入 56 和 78,得到 134;随后调用 get_current_time,传入 Asia/Shanghai,再把两次返回内容整理成回答。
这也说明 description 不是写给人看的装饰。AI Client 正是根据它理解 Tool 的用途。名称和说明写得含糊,客户端就更容易选错。
10. 常见问题
npm start 后为什么一直不退出?
stdio Server 正在等待 Client 请求。看到启动日志且没有报错,就可以按 Ctrl+C 停止,再让 Inspector 或 CodeBuddy 启动它。
Inspector 为什么看不到新 Tool?
先保存 src/index.ts,停止旧进程,再重新执行 Inspector 命令。Inspector 启动的是一个新进程,不会自动读取旧进程之外的修改。
为什么不能用 console.log?
stdout 是 stdio 的协议通道。console.log() 会把普通文字混进 JSON-RPC 消息,客户端可能因此解析失败。日志使用 console.error()。
CodeBuddy 能连接,AI 却不调用 Tool?
先在 Inspector 里确认 Tool 能列出、参数能提交。然后检查 description 是否准确说明用途。连接成功只能证明 Server 已启动,AI 是否选择 Tool 还取决于任务、描述和当前客户端行为。
11. 做完以后,我们学会了什么?
这次完成了三件事:
- 创建一个通过 stdio 工作的 MCP Server;
- 注册并用 Inspector 调试三个 Tool;
- 把 Server 接入真实 AI Client,让 AI 根据要求选择 Tool。
现在回头看,MCP 的作用很具体:它给 AI Client 一套统一方式,用来发现外部能力、传入参数并取得结果。
今天的 handler 只做了问候、加法和时间查询。把其中的代码换成读取文件、调用 API 或查询数据库,MCP Server 的基本结构仍然一样。
下一步,可以试着写一个真正会读取本地文件的 Tool。