本教程面向已熟悉 TypeScript 开发、想用 MCP 官方参考实现(@modelcontextprotocol/sdk)打造自有工具链的工程师。Go、Rust、Python 的 MCP 服务器教程已不鲜见,但 TypeScript SDK 是 Anthropic 维护的参考实现——协议变更最先落地、API 最完整、社区生态最大(月下载量近亿)。我们将从零构建一个 project-snapshot 服务器:用 Resources 暴露项目元数据,用 Tools 执行只读分析,用 Prompts 内置可复用提示,最后打包到 npm、通过 npx 一行接入 Claude Code,并支持 stdio 与 Streamable HTTP 双传输形态。
node -v 应 ≥ 20)type: "module")官方 TypeScript SDK 提供两种风格的 API,理解它们的分工是写出干净服务器的前提。
Server 类(@modelcontextprotocol/sdk/server/index.js):直接映射协议消息,需要你手写 setRequestHandler 注册 "tools/call"、"resources/read" 等处理器,并自行把 Zod schema 转成 JSON Schema。控制力最强,代码也最冗长。McpServer 类(@modelcontextprotocol/sdk/server/mcp.js):在 Server 之上封装出 server.tool() / server.resource() / server.prompt() 三个声明式方法,自动把 Zod schema 编译成 JSON Schema,自动处理协议握手、能力声明、错误包装。绝大多数场景应从 McpServer 起步。一句话原则:用高层 API 表达"我提供什么",由 SDK 负责协议细节。本教程全程使用高层 API,仅在第九节打包时简要说明何时需要退回低层。
官方提供一键脚手架,能生成带 bin 入口、TypeScript 配置、Inspector 联调脚本的最小工程。
npx @modelcontextprotocol/create-mcp-server@latest project-snapshot
cd project-snapshot
npm install
若想完全手动搭建,最小目录结构如下:
project-snapshot/
├── src/
│ └── index.ts # 服务器入口(含 shebang)
├── package.json
├── tsconfig.json
└── README.md
package.json 的关键字段——type: "module" 决定能否用 import,bin 决定能否被 npx 直接执行:
{
"name": "project-snapshot",
"version": "1.0.0",
"type": "module",
"bin": { "project-snapshot": "dist/index.js" },
"files": ["dist"],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/index.js",
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
"zod": "^3.25.0"
},
"devDependencies": {
"typescript": "^5.9.0",
"@types/node": "^22.0.0"
}
}
tsconfig.json 用 Node16 模块解析,确保 .js 扩展名导入能正确解析:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
常见问题:导入路径必须写全扩展名(
from ".../server/mcp.js"),即便源文件是.ts。这是因为NodeNext模块解析按字面量解析导入路径。漏掉.js会报ERR_MODULE_NOT_FOUND。
Resources 是 MCP 的只读数据原语,适合暴露"模型可能想读的上下文"。server.resource() 接受名称、URI(字符串或 ResourceTemplate)、回调。
#!/usr/bin/env node
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { readFileSync } from "node:fs";
const server = new McpServer({
name: "project-snapshot",
version: "1.0.0",
});
// 静态 URI:项目清单
server.resource("manifest", "file:///package.json", async (uri) => {
const text = readFileSync("package.json", "utf8");
return { contents: [{ uri: uri.href, mimeType: "application/json", text }] };
});
// 动态模板:带参数的 README
const readmeTpl = new ResourceTemplate("snapshot://readme/{project}", { list: undefined });
server.resource("readme", readmeTpl, async (uri, { project }) => {
const text = readFileSync("README.md", "utf8");
return {
contents: [{ uri: uri.href, mimeType: "text/markdown", text: `## ${project}\n\n${text}` }],
};
});
预期结果:Claude Code 里输入 /mcp 可看到 project-snapshot 暴露的资源列表;问"读一下这个项目的 package.json",模型会通过 resources/read 拉取而非自己用 Bash 读——区别在于这是协议化的、带 MIME 类型与 URI 寻址的访问,客户端可做权限边界控制。
Tools 是"带副作用的动作",是 MCP 最常用的原语。server.tool(name, description, schema, handler):传入 Zod schema,SDK 自动生成 JSON Schema 暴露给模型。
import { execSync } from "node:child_process";
import { z } from "zod";
// 无参数工具:schema 可省略
server.tool("list_scripts", "列出 package.json 中的 npm scripts", async () => {
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
const scripts = Object.entries(pkg.scripts ?? {}) as [string, string][];
const text = scripts.length
? scripts.map(([k, v]) => `- **${k}**: \`${v}\``).join("\n")
: "(未定义任何 script)";
return { content: [{ type: "text", text }] };
});
// 带参数工具:Zod 描述输入契约
server.tool(
"count_todos",
"统计源码中的 TODO/FIXME 数量并返回示例",
{ path: z.string().default(".").describe("搜索根目录") },
async ({ path }) => {
try {
const out = execSync(`grep -rIn "TODO\\|FIXME" ${path} || true`, { encoding: "utf8" });
const lines = out.split("\n").filter(Boolean);
const preview = lines.slice(0, 20).join("\n");
return {
content: [{ type: "text", text: `共 ${lines.length} 处:\n${preview}` }],
};
} catch (e) {
return { content: [{ type: "text", text: (e as Error).message }], isError: true };
}
},
);
server.tool(
"recent_commits",
"查看最近 N 条 git 提交",
{ n: z.number().int().min(1).max(50).default(10) },
async ({ n }) => {
const out = execSync(
`git log -n ${n} --pretty=format:"%h %ad %s" --date=short 2>/dev/null`,
{ encoding: "utf8" },
);
return { content: [{ type: "text", text: out || "(非 git 仓库)" }] };
},
);
关键细节:
{ content: [{ type: "text" | "image" | "resource", ... }] } 结构,与协议 CallToolResult 对齐。isError: true,不要抛异常——抛异常会被 SDK 转成传输层错误,客户端可能直接断连;结构化错误让模型有机会重试或换工具。.describe() 写进 schema 的注释会成为模型的"参数说明书",直接影响调用准确率。package.json 同级的 Claude Code 权限层加护栏(本例全是只读,无需额外护栏)。Prompts 把"团队沉淀的提示词"固化成服务器资产,模型可按名称加载、参数化填充。server.prompt(name, schema, handler) 返回标准 messages 结构。
server.prompt(
"release_notes",
"根据最近提交生成面向用户的发布说明",
{ since: z.string().describe("起始 tag 或日期,例如 v1.0.0") },
async ({ since }) => {
const log = execSync(`git log ${since}..HEAD --pretty=format:"- %s" 2>/dev/null`, {
encoding: "utf8",
});
return {
messages: [
{
role: "user",
content: {
type: "text",
text:
"把以下提交整理成面向终端用户的发布说明,按「新功能 / 问题修复 / 其他」" +
"分类,语气友好、避免技术黑话:\n\n" + (log || "(无新提交)"),
},
},
],
};
},
);
预期结果:在 Claude Code 中可调用 @project-snapshot release_notes since:v1.2.0,模型直接拿到填充好的提示并继续生成。与把提示写进 Skill 的区别:Prompt 由服务器统一版本管理、可参数化、跨客户端(Claude Desktop / Code / Cursor)通用。
MCP 的传输层与业务逻辑完全解耦——同一个 McpServer 可挂到不同 transport。stdio 是 npm/npx 分发的默认形态(Claude Code 启动子进程、通过标准输入输出通信);Streamable HTTP 用于远程部署(多客户端共享、可加 OAuth2、可水平扩展)。
stdio 形态(默认):
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const transport = new StdioServerTransport();
await server.connect(transport);
Streamable HTTP 形态(用 Express 暴露 /mcp 端点):
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
if (process.argv.includes("--http")) {
const app = express();
app.post("/mcp", async (req, res) => {
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(t);
await t.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.error("HTTP MCP on :3000/mcp"));
} else {
await server.connect(new StdioServerTransport());
}
常见问题: - stdio 模式下不要
console.log:标准输出是协议通道,任何日志都会污染 JSON-RPC 帧,导致客户端解析失败。用console.error(走 stderr)或 SDK 的 logging facility(server.sendLoggingMessage)。 - Streamable HTTP 无状态模式(sessionIdGenerator: undefined)适合短连接与无状态部署;需要会话保持时实现自定义sessionIdGenerator返回唯一 ID。 - 远程 HTTP 服务器务必加认证(参考已覆盖的 OAuth2 远程服务器教程),否则等于把工具能力暴露在公网。
构建出产物后,用 claude mcp add 注册。Claude Code 有三种配置 scope,决定配置写到哪里、谁能共享:
# 1) local(默认):仅你本人、当前项目可见,写进用户级项目设置
claude mcp add project-snapshot -- node /绝对路径/dist/index.js
# 2) project:写入仓库根 .mcp.json,随 git 提交,团队共享
claude mcp add project-snapshot --scope project -- npx -y project-snapshot
# 3) user:写入 ~/.claude.json,你在所有项目都能用
claude mcp add project-snapshot --scope user -- npx -y project-snapshot
# 验证与管理
claude mcp list
claude mcp get project-snapshot
claude mcp remove project-snapshot
-- 的作用:把其后所有参数原样作为启动命令,避免 Claude Code 的 CLI flag 与服务器参数混淆。stdio 服务器尤其要注意。
--scope project 生成的 .mcp.json(可提交到 VCS,团队成员 clone 后 Claude Code 首次启动会提示授权):
{
"mcpServers": {
"project-snapshot": {
"command": "npx",
"args": ["-y", "project-snapshot"],
"env": {}
}
}
}
预期结果:注册成功后,在 Claude Code 内输入 /mcp 看到 project-snapshot: connected,工具以 mcp__project-snapshot__count_todos 的命名空间暴露。直接说"统计一下 TODO"即可触发。
Inspector 是官方可视化调试器,能脱离任何客户端直接驱动你的服务器、观察每个请求/响应。
npm run inspect
# 等价于:npx @modelcontextprotocol/inspector node dist/index.js
浏览器打开后可:手动调用每个 Tool 并查看 CallToolResult 原始 JSON;列出/读取 Resources;加载 Prompt 并查看填充后的 messages;切换 transport(stdio / Streamable HTTP)。强烈建议在接入 Claude Code 之前先用 Inspector 把每个原语跑通——客户端侧的报错往往信息不透明,Inspector 能精确隔离"是我的服务器错了,还是客户端配置错了"。
把服务器发布到 npm 后,任何用户无需 clone 代码,一行命令即可加载。关键在于 bin 字段 + shebang + files 白名单。
# 1) 确认入口可执行
chmod +x src/index.ts # shebang 需要文件可执行权限(构建产物 dist/index.js 同理)
npm run build # 产出 dist/
# 2) 本地验证 npx 能跑通(用 pack 模拟发布)
npm pack # 生成 project-snapshot-1.0.0.tgz
npx ./project-snapshot-1.0.0.tgz # 应启动 stdio 服务器(阻塞等待输入即正常)
# 3) 登录并发布
npm login
npm publish # 首次发布;后续用 npm version patch && npm publish
# 4) 任意机器一行接入 Claude Code
claude mcp add project-snapshot -- npx -y project-snapshot
发布检查清单:
- package.json 的 name 全局唯一;version 走 SemVer;repository / homepage / description 写全(npm 搜索与 MCP 目录收录都依赖这些)。
- files: ["dist"] 确保只发布构建产物,不泄露 src / 测试。
- bin 指向 dist/index.js,且该文件首行是 #!/usr/bin/env node。
- 若用了原生依赖,声明 engines: { node: ">=20" } 并提供 optionalDependencies 平台包。
把前面碎片整合成一个可直接 npm run build && node dist/index.js 启动的完整文件。新建 src/index.ts:
#!/usr/bin/env node
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFileSync } from "node:fs";
import { execSync } from "node:child_process";
import { z } from "zod";
const server = new McpServer({ name: "project-snapshot", version: "1.0.0" });
// ---------- Resources ----------
server.resource("manifest", "file:///package.json", async (uri) => ({
contents: [
{ uri: uri.href, mimeType: "application/json", text: readFileSync("package.json", "utf8") },
],
}));
const readmeTpl = new ResourceTemplate("snapshot://readme/{project}", { list: undefined });
server.resource("readme", readmeTpl, async (uri, { project }) => ({
contents: [
{ uri: uri.href, mimeType: "text/markdown", text: `## ${project}\n\n` + readFileSync("README.md", "utf8") },
],
}));
// ---------- Tools ----------
server.tool("list_scripts", "列出 npm scripts", async () => {
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
const s = Object.entries(pkg.scripts ?? {}) as [string, string][];
return { content: [{ type: "text", text: s.length ? s.map(([k, v]) => `- ${k}: ${v}`).join("\n") : "(无)" }] };
});
server.tool(
"count_todos",
"统计 TODO/FIXME",
{ path: z.string().default(".") },
async ({ path }) => {
try {
const out = execSync(`grep -rIn "TODO\\|FIXME" ${path} || true`, { encoding: "utf8" });
const lines = out.split("\n").filter(Boolean);
return { content: [{ type: "text", text: `共 ${lines.length} 处\n` + lines.slice(0, 20).join("\n") }] };
} catch (e) {
return { content: [{ type: "text", text: (e as Error).message }], isError: true };
}
},
);
server.tool(
"recent_commits",
"最近 N 条提交",
{ n: z.number().int().min(1).max(50).default(10) },
async ({ n }) => ({
content: [
{ type: "text", text: execSync(`git log -n ${n} --pretty=format:"%h %ad %s" --date=short 2>/dev/null`, { encoding: "utf8" }) || "(非 git 仓库)" },
],
}),
);
// ---------- Prompts ----------
server.prompt(
"release_notes",
{ since: z.string().describe("起始 tag/日期,如 v1.0.0") },
async ({ since }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text:
"把以下提交整理成面向用户的发布说明,按「新功能/修复/其他」分类,语气友好:\n\n" +
execSync(`git log ${since}..HEAD --pretty=format:"- %s" 2>/dev/null`, { encoding: "utf8" }) || "(无新提交)",
},
},
],
}),
);
// ---------- 传输层 ----------
await server.connect(new StdioServerTransport());
构建并接入:
npm install && npm run build
claude mcp add project-snapshot -- node "$(pwd)/dist/index.js"
# 在仓库目录内启动 claude,输入:统计一下当前项目的 TODO,并列出最近 5 条提交
flowchart TD
A[npx create-mcp-server 脚手架] --> B[配置 package.json / tsconfig]
B --> C[McpServer 声明三原语]
C --> C1[Resources 只读元数据]
C --> C2[Tools 只读分析]
C --> C3[Prompts 提示模板]
C1 & C2 & C3 --> D{选择传输层}
D -->|本地分发| E[StdioServerTransport]
D -->|远程部署| F[StreamableHTTPServerTransport]
E --> G[npm run inspect 全链路调试]
F --> G
G --> H[npm pack 本地验证]
H --> I[npm publish 发布]
I --> J[claude mcp add -- npx -y 接入]
J --> K[Claude Code 内 /mcp 验证可用]
ERR_MODULE_NOT_FOUND: .../mcp.js:tsconfig 未用 Node16 模块解析,或导入路径漏了 .js 扩展名。console.log。改成 console.error 或 server.sendLoggingMessage({ level: "info", data: "..." })。claude mcp get <name> 显示 connected;若显示 failed,用 claude --mcp-debug 启动看 stderr;Inspector 单独跑 node dist/index.js 验证服务器本身无误。server.tool 的第三个参数(描述在前、schema 在中);.default() 提供的参数可省略。npx -y 接入很慢:首次会下载包。固定版本 npx -y [email protected] 可避免每次拉取 latest 的网络开销。npx project-snapshot 报权限拒绝:dist/index.js 缺可执行位,chmod +x dist/index.js 后重新 npm publish。.mcp.json、Tool Search):https://code.claude.com/docs/en/mcp本教程适合"AI 工程化 / 开发者工具"方向的进阶训练营,难度定位为中阶(需 TypeScript 与命令行基础)。建议教学时长 3–4 小时:1 小时讲两层 API 与三原语模型,1.5 小时动手实现 + Inspector 调试,1 小时打包发布与团队 .mcp.json 共享演练。可作为"工具生态构建"模块的核心实验,前后衔接"封装现有 API"(基础)与"OAuth2 远程服务器"(高级)。若学员以 Python/Go 为主,本教程的协议理解与工程化流程仍可直接迁移,仅需替换 SDK 语法。
claude mcp add 三种 scope、.mcp.json 共享、-- 参数分隔官方说明[!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 等场景全覆盖。
