学习目标:从零构建一个生产级MCP Server,使用最新TypeScript SDK v2(@modelcontextprotocol/server),覆盖Tools、Resources、Prompts三大原语,完成本地调试后接入Claude Code,实现端到端联调。
适用人群:有TypeScript基础的前端/全栈开发者,想为Claude Code、Claude Desktop、Cursor等AI客户端构建自定义工具扩展。
Model Context Protocol(MCP)是Anthropic推出的开放协议,用标准化方式让AI模型连接外部工具和数据源。截至2026年5月,MCP SDK月下载量已突破9700万,公开Server实现超过10000个。Claude、Cursor、Windsurf、OpenAI等主流AI平台均原生支持。
MCP架构由三层角色构成:
graph TB
subgraph Host["MCP Host(宿主)"]
A["Claude Code"]
B["Claude Desktop"]
C["Cursor IDE"]
D["Windsurf"]
end
subgraph Client["MCP Client(客户端)"]
E["内置于Host中<br/>管理连接生命周期"]
end
subgraph Server["MCP Server(你的Server)"]
F["Tools<br/>可调用的函数"]
G["Resources<br/>可读取的数据"]
H["Prompts<br/>预置的提示模板"]
end
Host -->|"JSON-RPC 2.0"| Client
Client -->|"stdio / HTTP"| Server
Server --> F
Server --> G
Server --> H
三种传输模式: - stdio:Server作为Host的子进程,通过stdin/stdout通信。适合本地工具。 - HTTP(Streamable HTTP):推荐用于远程Server。基于标准HTTP请求/响应。 - SSE(已废弃):旧版远程传输,已被HTTP替代。
| 工具 | 最低版本 | 推荐版本 |
|---|---|---|
| Node.js | v18+ | v22+ |
| npm | v8+ | v10+ |
| TypeScript | 5.x | 5.8+ |
| Claude Code | 最新版 | npm install -g @anthropic-ai/claude-code |
node --version # v22.x.x 或更高
npm --version # v10.x.x 或更高
claude --version # 确认Claude Code已安装
创建项目目录并配置ESM模块系统。"type": "module"不可省略——MCP SDK是纯ESM包,缺少此配置会导致运行时import失败。
mkdir markdown-toolkit && cd markdown-toolkit
手动创建package.json:
{
"name": "markdown-toolkit",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "npx tsc --watch",
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/server": "^2.0.0",
"zod": "^3.25.0"
},
"devDependencies": {
"typescript": "^5.8.3",
"@types/node": "^22.0.0"
}
}
SDK版本说明:2026年Q1,TypeScript SDK v2已发布稳定版。v1.x(包名
@modelcontextprotocol/sdk)继续维护6个月。新项目建议直接使用v2。v2的包拆分为@modelcontextprotocol/server和@modelcontextprotocol/client。
安装依赖:
npm install
预期输出:
added 120 packages in 2s
found 0 vulnerabilities
创建tsconfig.json。module和moduleResolution必须设为"Node16"——旧版"node"无法正确解析SDK内部的.js扩展名import。
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
创建源码目录:
mkdir src
我们要构建一个Markdown工具箱Server,提供以下能力:
| 原语 | 名称 | 功能 |
|---|---|---|
| Tool | word_count |
统计Markdown文本的字数、段落数、标题数 |
| Tool | extract_toc |
从Markdown提取目录结构 |
| Tool | check_links |
检查Markdown中的链接格式 |
| Resource | templates://readme |
提供标准README模板 |
| Prompt | review_markdown |
生成Markdown审查提示词 |
创建src/index.ts:
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
// 初始化Server
const server = new McpServer({
name: "markdown-toolkit",
version: "1.0.0",
});
// ========== Tools ==========
// Tool 1: 字数统计
server.registerTool(
"word_count",
{
description:
"统计Markdown文本的字数、段落数、标题数。" +
"返回结构化的统计数据,适合文档分析。",
inputSchema: z.object({
markdown: z.string().describe("要分析的Markdown文本"),
}),
},
async ({ markdown }) => {
const lines = markdown.split("\n");
const headings = lines.filter((l) => l.startsWith("#"));
const paragraphs = lines.filter(
(l) => l.trim() !== "" && !l.startsWith("#")
);
const words = markdown
.replace(/[#*_`>\[\]()!-]/g, " ")
.split(/\s+/)
.filter(Boolean);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
totalWords: words.length,
totalLines: lines.length,
headings: headings.length,
headingList: headings.map((h) => h.trim()),
paragraphs: paragraphs.length,
},
null,
2
),
},
],
};
}
);
// Tool 2: 提取目录
server.registerTool(
"extract_toc",
{
description:
"从Markdown文本中提取层级目录结构。" +
"返回带缩进的目录树,含标题级别和锚点链接。",
inputSchema: z.object({
markdown: z.string().describe("要提取目录的Markdown文本"),
maxDepth: z
.number()
.min(1)
.max(6)
.optional()
.describe("最大标题深度,默认6(包含所有级别)"),
}),
},
async ({ markdown, maxDepth }) => {
const depth = maxDepth ?? 6;
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
const toc: string[] = [];
let match;
while ((match = headingRegex.exec(markdown)) !== null) {
const level = match[1].length;
if (level > depth) continue;
const title = match[2].trim();
const anchor = title
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, "")
.replace(/\s+/g, "-");
const indent = " ".repeat(level - 1);
toc.push(`${indent}- [${title}](#${anchor})`);
}
if (toc.length === 0) {
return {
content: [
{ type: "text", text: "未找到标题。请确保文本包含 # 标题。" },
],
};
}
return {
content: [
{
type: "text",
text: `## 目录\n\n${toc.join("\n")}\n\n共 ${toc.length} 个标题。`,
},
],
};
}
);
// Tool 3: 链接检查
server.registerTool(
"check_links",
{
description:
"检查Markdown文本中的链接格式是否规范。" +
"检测空链接、重复锚点、格式异常等问题。",
inputSchema: z.object({
markdown: z.string().describe("要检查链接的Markdown文本"),
}),
},
async ({ markdown }) => {
const linkRegex = /\[([^\]]*)\]\(([^)]*)\)/g;
const issues: string[] = [];
const anchors = new Set<string>();
let match;
let linkCount = 0;
while ((match = linkRegex.exec(markdown)) !== null) {
linkCount++;
const text = match[1];
const href = match[2];
if (!text.trim()) {
issues.push(`链接 #${linkCount}: 链接文本为空 → ${match[0]}`);
}
if (!href.trim()) {
issues.push(`链接 #${linkCount}: href为空 → ${match[0]}`);
}
if (href.startsWith("#")) {
if (anchors.has(href)) {
issues.push(`链接 #${linkCount}: 重复锚点 → ${href}`);
}
anchors.add(href);
}
if (href.includes(" ") && !href.startsWith("http")) {
issues.push(`链接 #${linkCount}: href包含空格 → ${match[0]}`);
}
}
const result = {
totalLinks: linkCount,
issuesFound: issues.length,
issues: issues.length > 0 ? issues : ["所有链接格式正常 ✓"],
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
// ========== Resources ==========
server.resource(
"readme-template",
"templates://readme",
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text: `# 项目名称
> 一句话描述你的项目
## 功能特性
- 特性一
- 特性二
- 特性三
## 快速开始
\`\`\`bash
npm install your-package
\`\`\`
## 使用方法
\`\`\`typescript
import { something } from 'your-package';
\`\`\`
## 贡献指南
欢迎提交 Issue 和 Pull Request。
## 许可证
MIT License
`,
},
],
})
);
// ========== Prompts ==========
server.prompt(
"review_markdown",
"审查Markdown文档的质量和规范性",
{ document: z.string().describe("要审查的Markdown文档内容") },
async ({ document }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `请审查以下Markdown文档,从以下维度评分(1-10):
1. **结构层次**:标题层级是否合理,是否缺少必要章节
2. **链接完整性**:所有链接是否有效,锚点是否正确
3. **代码示例**:代码块是否有语言标注,示例是否可运行
4. **格式规范**:列表缩进、空行使用、表格格式
5. **可读性**:段落长度、语言风格、专业术语使用
文档内容:
${document}
请给出具体评分和改进建议。`,
},
},
],
})
);
// ========== 启动Server ==========
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[markdown-toolkit] MCP Server已启动(stdio模式)");
}
main().catch((err) => {
console.error("Server启动失败:", err);
process.exit(1);
});
1. registerTool签名(v2新API):
server.registerTool(name, { description, inputSchema }, handler)
v2使用Zod schema替代v1的JSON Schema对象。SDK自动将Zod转换为标准JSON Schema供客户端消费。
2. 日志安全:
所有日志必须写入stderr(console.error)。stdout专用于JSON-RPC消息,任何写入都会破坏协议。
3. Top-level await:
"type": "module"启用了ESM,支持顶层await,无需包装到async IIFE中。
npx tsc
预期:无输出,退出码0。检查dist/index.js已生成。
ls dist/
# index.js index.js.map(如果启用了sourcemap)
创建test.mjs进行协议级测试:
// test.mjs
import { spawn } from "child_process";
const proc = spawn("node", ["dist/index.js"], {
stdio: ["pipe", "pipe", "pipe"],
});
const messages = [
{
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "test-client", version: "1.0" },
},
},
{ jsonrpc: "2.0", method: "notifications/initialized", params: {} },
{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
{
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: {
name: "word_count",
arguments: { markdown: "# 标题\n\nHello World from MCP\n\n## 二级标题\n\nAnother paragraph" },
},
},
{
jsonrpc: "2.0",
id: 4,
method: "resources/list",
params: {},
},
];
let output = "";
proc.stdout.on("data", (d) => {
output += d;
});
messages.forEach((m) => proc.stdin.write(JSON.stringify(m) + "\n"));
setTimeout(() => {
proc.kill();
output
.trim()
.split("\n")
.forEach((line) => {
try {
const { id, result } = JSON.parse(line);
console.log(`[${id}]`, JSON.stringify(result).slice(0, 150));
} catch {}
});
}, 2000);
运行测试:
node test.mjs
预期输出:
[1] {"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true},"resources":{"listChanged":true},"prompts":{"listChanged":true}},"serverInfo":{"name":"markdown-toolkit","version":"1.0.0"}}
[2] {"tools":[{"name":"word_count","description":"统计Markdown文本的字数、段落数、标题数..."},{"name":"extract_toc",...},{"name":"check_links",...}]}
[3] {"content":[{"type":"text","text":"{\n \"totalWords\": 8,\n \"totalLines\": 7,\n \"headings\": 2,..."}]}
[4] {"resources":[{"uri":"templates://readme","name":"readme-template","mimeType":"text/markdown"}]}
| 错误 | 原因 | 解决 |
|---|---|---|
Cannot find module '@modelcontextprotocol/server' |
缺少"type": "module" |
package.json中添加"type": "module" |
Cannot find module ... .js |
moduleResolution配置错误 |
tsconfig.json设为"Node16" |
Unexpected token import |
Node.js版本过低 | 升级到v18+ |
| 运行后无响应 | console.log写入了stdout |
改用console.error |
MCP Inspector是官方提供的可视化调试工具,可以在浏览器中交互式测试Server的每个能力。
npx @modelcontextprotocol/inspector node dist/index.js
Inspector会启动一个本地Web服务(默认http://localhost:6274),自动连接你的Server。
Inspector提供的调试能力:
graph LR
subgraph Inspector["MCP Inspector UI"]
A["Tools面板<br/>列出所有Tool<br/>在线填写参数<br/>查看返回结果"]
B["Resources面板<br/>浏览可用资源<br/>预览资源内容"]
C["Prompts面板<br/>查看提示模板<br/>测试参数渲染"]
D["Connection面板<br/>查看握手信息<br/>协议版本<br/>能力协商"]
end
subgraph Server["你的MCP Server"]
E["stdio进程"]
end
Inspector -->|"JSON-RPC"| Server
在Inspector中你可以:
1. 点击Tools标签,选择word_count
2. 输入参数{"markdown": "# Test\n\nHello"}
3. 点击Run,查看完整的JSON-RPC请求和响应
4. 验证返回的统计数据是否正确
这一步能独立验证Server逻辑正确性,无需依赖任何AI客户端。
使用claude mcp add命令注册Server:
claude mcp add --transport stdio markdown-toolkit -- node /absolute/path/to/markdown-toolkit/dist/index.js
验证注册成功:
claude mcp list
预期输出包含:
markdown-toolkit: node /absolute/path/to/markdown-toolkit/dist/index.js (stdio)
对于团队协作项目,建议使用.mcp.json文件进行项目级配置:
在项目根目录创建.mcp.json:
{
"mcpServers": {
"markdown-toolkit": {
"type": "stdio",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/dist/index.js"]
}
}
}
作用域层级(从高到低):
1. Local scope:claude mcp add --scope local,仅当前会话
2. Project scope:.mcp.json文件,团队共享
3. User scope:~/.claude.json,全局配置
Claude Code会自动设置CLAUDE_PROJECT_DIR环境变量指向项目根目录,Server内部可通过process.env.CLAUDE_PROJECT_DIR获取。
启动Claude Code并测试:
claude
在对话中尝试以下指令:
> 统计以下Markdown的字数:# 标题\n\n这是测试段落
> (Claude会调用word_count工具)
> 帮我提取这个文档的目录结构
> (Claude会调用extract_toc工具)
> 检查我的README中的链接格式
> (Claude会调用check_links工具)
Claude Code会自动发现已注册的MCP Server,在需要时调用对应的工具,并将结果整合到回答中。
Resources通过@引用语法使用。在Claude Code中:
> @markdown-toolkit 读取README模板
Claude会列出Server暴露的资源,选择templates://readme即可获取标准README模板内容。
Prompts被注册为斜杠命令。在Claude Code中:
> /markdown-toolkit:review_markdown
Claude会提示输入document参数,然后使用预设的审查提示词模板生成结构化的审查请求。
当Server需要部署到服务器供多人使用时,使用HTTP传输替代stdio。
npm install @modelcontextprotocol/node
创建src/http-server.ts:
import { McpServer } from "@modelcontextprotocol/server";
import { createServer } from "http";
import { randomUUID } from "crypto";
import { z } from "zod";
const server = new McpServer({
name: "markdown-toolkit",
version: "1.0.0",
});
// 注册相同的Tools(复用逻辑)
server.registerTool(
"word_count",
{
description: "统计Markdown文本的字数、段落数、标题数",
inputSchema: z.object({
markdown: z.string().describe("要分析的Markdown文本"),
}),
},
async ({ markdown }) => {
const words = markdown
.replace(/[#*_`>\[\]()!-]/g, " ")
.split(/\s+/)
.filter(Boolean);
return {
content: [
{ type: "text", text: JSON.stringify({ totalWords: words.length }) },
],
};
}
);
// HTTP Transport处理
const httpServer = createServer(async (req, res) => {
if (req.method === "POST") {
const body = await new Promise<string>((resolve) => {
let data = "";
req.on("data", (chunk) => (data += chunk));
req.on("end", () => resolve(data));
});
const message = JSON.parse(body);
const response = await server.handleMessage(message, {
sessionId: randomUUID(),
});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(response));
}
});
const PORT = process.env.PORT || 3001;
httpServer.listen(PORT, () => {
console.log(`MCP Server running on http://localhost:${PORT}/mcp`);
});
claude mcp add --transport http markdown-toolkit-remote http://your-server:3001/mcp
如果需要认证:
claude mcp add --transport http \
--header "Authorization: Bearer your-token" \
markdown-toolkit-remote \
https://api.your-domain.com/mcp
flowchart TD
A["创建项目<br/>npm init + type:module"] --> B["安装SDK<br/>@modelcontextprotocol/server"]
B --> C["实现Tools/Resources/Prompts<br/>registerTool / resource / prompt"]
C --> D["编译TypeScript<br/>npx tsc"]
D --> E["本地JSON-RPC测试<br/>test.mjs"]
E --> F{"测试通过?"}
F -->|No| C
F -->|Yes| G["MCP Inspector<br/>可视化调试"]
G --> H{"功能正确?"}
H -->|No| C
H -->|Yes| I["注册到Claude Code<br/>claude mcp add"]
I --> J["集成测试<br/>Claude Code对话调用"]
J --> K{"联调通过?"}
K -->|No| C
K -->|Yes| L["选择部署方式"]
L --> M["stdio模式<br/>本地/团队使用"]
L --> N["HTTP模式<br/>远程/多用户"]
M --> O["配置.mcp.json<br/>项目级共享"]
N --> P["部署到服务器<br/>claude mcp add --transport http"]
// v2 API(推荐)
server.registerTool(
name: string,
options: {
description: string;
inputSchema: ZodSchema; // 使用Zod,SDK自动转JSON Schema
},
handler: (args) => Promise<{
content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }>;
isError?: boolean;
}>
);
server.resource(
name: string,
uri: string, // 如 "templates://readme"
handler: (uri: URL) => Promise<{
contents: Array<{
uri: string;
text?: string;
blob?: string;
mimeType?: string;
}>;
}>
);
server.prompt(
name: string,
description: string,
argsSchema: Record<string, ZodSchema>,
handler: (args) => Promise<{
messages: Array<{
role: "user" | "assistant";
content: { type: "text"; text: string };
}>;
}>
);
return {
content: [
{ type: "text", text: "分析结果如下:" },
{
type: "image",
data: base64EncodedImage,
mimeType: "image/png",
},
],
};
server.registerTool(
"safe_tool",
{ description: "带错误处理的Tool", inputSchema: z.object({ url: z.string().url() }) },
async ({ url }) => {
try {
const data = await fetchData(url);
return { content: [{ type: "text", text: JSON.stringify(data) }] };
} catch (error) {
return {
content: [
{
type: "text",
text: `错误:${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true, // 标记为错误响应
};
}
}
);
Tool的description字段直接决定LLM的调用准确度。好的描述应包含:
// ❌ 差的描述
{ description: "统计字数" }
// ✅ 好的描述
{
description:
"统计Markdown文本的字数、段落数、标题数。" +
"返回结构化的统计数据,适合文档分析。" +
"输入为原始Markdown文本,输出为JSON格式的统计结果。"
}
// .mcp.json中使用环境变量
{
"mcpServers": {
"my-api-server": {
"type": "stdio",
"command": "node",
"args": ["dist/index.js"],
"env": {
"API_KEY": "${MY_API_KEY}",
"PROJECT_DIR": "${CLAUDE_PROJECT_DIR:-.}"
}
}
}
}
CLAUDE_PROJECT_DIR是Claude Code自动注入的项目根目录变量。在.mcp.json中使用时需要添加默认值:-.,因为变量展开发生在Server进程中而非Claude Code自身。
// 对于耗时操作,考虑缓存
const cache = new Map<string, { data: unknown; expiry: number }>();
function withCache<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {
const cached = cache.get(key);
if (cached && Date.now() < cached.expiry) {
return Promise.resolve(cached.data as T);
}
return fn().then((data) => {
cache.set(key, { data, expiry: Date.now() + ttl });
return data;
});
}
| 症状 | 诊断方法 | 解决方案 |
|---|---|---|
| Claude Code中看不到Tool | claude mcp list检查注册 |
确认路径绝对且正确 |
| Tool调用超时 | 检查handler是否有死循环 | 添加超时保护 |
| Server启动后立即退出 | node dist/index.js手动运行 |
检查import是否正确 |
| 中文参数乱码 | 确认文件编码为UTF-8 | tsc会保持UTF-8 |
| Claude Code连接被拒 | 检查stdio/HTTP配置匹配 | stdio用command+args,HTTP用URL |
registerTool is not a function |
SDK版本为v1 | v1用server.tool(),v2用server.registerTool() |
| 维度 | 评估 |
|---|---|
| 适合训练营类型 | AI工具开发营、前端进阶营、LLM应用开发营 |
| 学员基础要求 | TypeScript基础、Node.js使用经验、命令行操作 |
| 预计教学时长 | 4-5小时(含动手实践) |
| 实验环境需求 | Node.js 22+、Claude Code账号、网络访问 |
| 难度曲线 | Step 1-4(1.5h基础)→ Step 5-6(2h核心)→ Step 7(1h进阶) |
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
