上篇 MCP 协议详解 讲了 MCP 是什么、Client/Server 架构、以及它为什么是「AI 工具的 USB 接口」。那篇偏理论,这篇我们撸起袖子写代码——从零构建一个能跑、能用的 MCP Server。

最终效果:Claude Code 通过 MCP 协议调用你写的 Server,读取本地数据、执行自定义工具。

前置准备

开始前确保环境就绪:

  • Node.js 18+(MCP SDK 要求 ESM)
  • Claude Code 或支持 MCP 的客户端(用于最终测试)
  • 一个空目录,比如 my-first-mcp-server
1
2
mkdir my-first-mcp-server && cd my-first-mcp-server
npm init -y

修改 package.json,加上 "type": "module"(MCP SDK 纯 ESM):

1
2
3
4
5
6
7
8
9
{
"name": "my-first-mcp-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc"
}
}

安装依赖:

1
2
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

项目结构

1
2
3
4
5
6
7
8
my-first-mcp-server/
├── package.json
├── tsconfig.json
└── src/
├── index.ts # 入口,创建 Server 并启动
└── tools/
├── file-tools.ts # 文件操作工具
└── weather.ts # 天气查询工具(模拟)

Hello World:最简 MCP Server

先从最小的可运行版本开始。不用管工具和资源,先让 Server 跑起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
name: "my-first-mcp-server",
version: "1.0.0",
});

const transport = new StdioServerTransport();
await server.connect(transport);

console.error("MCP Server is running on stdio");

StdioServerTransport 通过标准输入输出通信——这是 MCP 最常用的传输方式,不需要网络端口,完全在本地机器上运行。

console.error 而不是 console.log 是因为 stdout 是 MCP 协议的通信通道,普通日志必须走 stderr。

运行测试——Server 启动后等待 JSON-RPC 消息,你手动输入不会被 echo 回来,这是正常的:

1
npx tsx src/index.ts

Ctrl+C 退出。能跑就行。

定义第一个 Tool

Tool 是 MCP Server 的核心——它暴露函数给 AI 调用。我们来写一个计算两个日期之间相差天数的工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/index.ts (在 server 初始化后添加)
import { z } from "zod";

server.tool(
"date-diff",
"计算两个日期之间相差的天数",
{
start: z.string().describe("起始日期,格式 YYYY-MM-DD"),
end: z.string().describe("结束日期,格式 YYYY-MM-DD"),
},
async ({ start, end }) => {
const startDate = new Date(start);
const endDate = new Date(end);
const diffMs = endDate.getTime() - startDate.getTime();
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));

return {
content: [
{
type: "text",
text: `${start}${end} 相差 ${diffDays} 天`,
},
],
};
}
);

关键点:

  • 第一个参数是 tool name,AI 用它来识别和调用
  • 第二个参数是 description,AI 读这个来决定何时调用
  • zod schema 定义了参数类型,MCP SDK 用它做参数校验
  • 返回值必须是 { content: [{ type: "text", text: "..." }] } 格式

Description 写得好不好直接影响 AI 调用准确率。写清楚它做什么、输入什么、输出什么。

添加文件读取工具

更实用的例子——让 AI 能读取本地文件目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// src/tools/file-tools.ts
import { z } from "zod";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";

export function registerFileTools(server: import("@modelcontextprotocol/sdk/server/mcp.js").McpServer) {
server.tool(
"list-files",
"列出指定目录下的所有文件",
{
path: z.string().describe("目录路径(绝对路径或相对于当前目录的路径)"),
},
async ({ path }) => {
const files = await readdir(path);
const fileList = files.map((f, i) => `${i + 1}. ${f}`).join("\n");

return {
content: [
{
type: "text",
text: fileList || "目录为空",
},
],
};
}
);

server.tool(
"read-file",
"读取指定文件的内容",
{
path: z.string().describe("文件路径"),
},
async ({ path }) => {
const content = await readFile(path, "utf-8");
const preview = content.slice(0, 5000);

return {
content: [
{
type: "text",
text: preview + (content.length > 5000 ? "\n...(内容过长,已截断)" : ""),
},
],
};
}
);
}

index.ts 中注册:

1
2
3
import { registerFileTools } from "./tools/file-tools.js";

registerFileTools(server);

写完后让 AI 能浏览文件系统——但注意安全:生产环境一定要限制可访问路径,否则相当于给 AI 开了文件系统的后门。

添加外部 API 工具

模拟一个天气查询——实际项目中替换成真实 API 调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/tools/weather.ts
import { z } from "zod";

export function registerWeatherTool(server: any) {
server.tool(
"get-weather",
"查询指定城市的天气信息",
{
city: z.string().describe("城市名称,如 Beijing、Shanghai"),
},
async ({ city }: { city: string }) => {
// 模拟数据——实际项目替换为 API 调用
const weatherData: Record<string, string> = {
beijing: "北京:晴,25°C,湿度 40%,风力 2 级",
shanghai: "上海:多云,28°C,湿度 65%,风力 3 级",
};

const key = city.toLowerCase();
const result = weatherData[key] || `${city}:暂无天气数据`;

return {
content: [{ type: "text", text: result }],
};
}
);
}

真实项目中,把模拟数据替换成高德天气 API 或 OpenWeatherMap。关键不变——Tool 定义、参数校验、返回格式。

暴露 Resource

Tool 是「让 AI 做事」,Resource 是「让 AI 看数据」。我们来暴露一个 Resource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server.resource(
"project-info",
"project://info",
{
description: "项目基本信息,包括名称、版本、描述",
},
async () => {
return {
contents: [
{
uri: "project://info",
mimeType: "application/json",
text: JSON.stringify(
{
name: "my-first-mcp-server",
version: "1.0.0",
description: "一个 MCP Server 开发示例",
tools: ["date-diff", "list-files", "read-file", "get-weather"],
resources: ["project://info"],
},
null,
2
),
},
],
};
}
);

Resource 用 URI 标识,AI 用 resources/read 方法获取。对于静态配置、文档、schema 等场景非常合适。

本地测试

写完了,怎么测?两步。

第一步:用 MCP Inspector

1
npx @modelcontextprotocol/inspector tsx src/index.ts

Inspector 打开浏览器页面,你可以:

  • 查看 Server 暴露了哪些 Tools 和 Resources
  • 手动调用 Tool 看返回结果
  • 检查 JSON-RPC 通信日志

第二步:接入 Claude Code

在 Claude Code 的 mcp.json 中注册(位置:项目根目录或 ~/.claude/mcp.json):

1
2
3
4
5
6
7
8
{
"mcpServers": {
"my-tools": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/my-first-mcp-server/src/index.ts"]
}
}
}

重启 Claude Code(如果你用 IntelliJ IDEA,CC GUI 插件 可以让你在 IDE 里直接操作 Claude Code,不用切终端),问一句「你能用 my-tools 帮我做什么」:

1
2
3
4
5
我能使用以下工具:
1. date-diff - 计算两个日期之间相差的天数
2. list-files - 列出指定目录下的所有文件
3. read-file - 读取指定文件的内容
4. get-weather - 查询指定城市的天气信息

然后直接对话调用:帮我算一下 2026-01-01 到 2026-05-22 有几天

文件读取工具要限制路径范围。生产环境中对 path 参数做白名单校验,防止 AI 被 prompt injection 利用去读取 /etc/passwd.env

完整代码

把所有代码拼在一起,完整可运行的 src/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { registerFileTools } from "./tools/file-tools.js";
import { registerWeatherTool } from "./tools/weather.js";

const server = new McpServer({
name: "my-first-mcp-server",
version: "1.0.0",
});

// Tool 1: 日期差值
server.tool(
"date-diff",
"计算两个日期之间相差的天数",
{
start: z.string().describe("起始日期,格式 YYYY-MM-DD"),
end: z.string().describe("结束日期,格式 YYYY-MM-DD"),
},
async ({ start, end }) => {
const diffMs = new Date(end).getTime() - new Date(start).getTime();
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
return {
content: [{ type: "text", text: `${start}${end} 相差 ${diffDays} 天` }],
};
}
);

// Resource: 项目信息
server.resource(
"project-info",
"project://info",
{ description: "项目基本信息" },
async () => ({
contents: [{
uri: "project://info",
mimeType: "application/json",
text: JSON.stringify({ name: "my-first-mcp-server", version: "1.0.0",
tools: ["date-diff", "list-files", "read-file", "get-weather"] }, null, 2),
}],
})
);

// 注册外部工具
registerFileTools(server);
registerWeatherTool(server);

// 启动
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server running on stdio");

进阶方向

这个 Demo 完成了,但 MCP 能做的远不止这些:

  • 多 Transport:除了 stdio,MCP 还支持 HTTP/SSE,适合远程 Server 场景
  • Prompts 模板:Server 可以暴露预定义的 Prompt 模板,让 AI 通过 prompts/get 获取
  • Streamable HTTP:MCP 2025 规范新增,支持真正的流式响应
  • 认证机制:OAuth 2.0 集成,保护敏感工具的访问权限

总结

MCP Server 开发的核心流程:

  1. McpServer 创建实例
  2. server.tool() 定义工具(name + description + zod schema + handler)
  3. server.resource() 暴露数据
  4. StdioServerTransport 通过 stdin/stdout 通信
  5. 在客户端 mcp.json 中注册,AI 就能自动发现和调用

MCP 把 AI 的能力边界从「内置工具」扩展到了「无限自定义工具」。你写的每一行代码都可以变成 AI 的操作能力——这才是 MCP 的真正价值。

从这个角度看,MCP Server 本质上就是 AI Agent 的「技能系统」。如果你对 Agent 开发的完整流程感兴趣,AI Agent 开发完整指南 涵盖了从工具定义到上线的全部环节。