MCP(Model Context Protocol)为AI应用提供标准化的上下文注入协议,其核心包含三个原语:Tool(工具调用)、Resource(数据暴露)、Prompt(提示词模板)。本教程从零构建一个包含全部三原语的TypeScript MCP服务器,经MCP Inspector调试验证后发布到npm,最终在Claude Code中一行命令挂载。适合有TypeScript基础、希望构建可分发MCP工具链的开发者。
| 项目 | 要求 |
|---|---|
| Node.js | >= 18.0 |
| npm | >= 9.0 |
| TypeScript | >= 5.0 |
| Claude Code | 最新版 |
| 基础知识 | TypeScript类型系统、async/await、npm包管理 |
绝大多数MCP教程只讲Tool,但真正发挥MCP威力需要理解三个原语的分工。
graph TB
subgraph "MCP三原语"
T["Tool<br/>工具调用<br/>执行动作、产生副作用"]
R["Resource<br/>数据暴露<br/>提供只读上下文数据"]
P["Prompt<br/>提示词模板<br/>预设交互模式"]
end
T --> |"查询数据库"| T1["返回结构化结果"]
T --> |"调用API"| T2["产生副作用"]
T --> |"文件操作"| T3["读写变更"]
R --> |"代码文件"| R1["实时文件内容"]
R --> |"配置数据"| R2["应用状态"]
R --> |"文档"| R3["知识库"]
P --> |"代码审查"| P1["预设审查清单"]
P --> |"调试助手"| P2["诊断流程模板"]
P --> |"翻译"| P3["多语言格式模板"]
style T fill:#4A90D9,color:#fff
style R fill:#50C878,color:#fff
style P fill:#FF6B6B,color:#fff
选择决策树:
| 场景 | 使用原语 | 原因 |
|---|---|---|
| 查询数据库返回结果 | Tool | 有明确的输入输出,可能产生副作用 |
| 暴露项目配置供AI读取 | Resource | 只读数据,AI按需拉取 |
| 提供代码审查模板 | Prompt | 预设指令模板,用户可填参数 |
| 调用外部API | Tool | 产生副作用 |
| 暴露文件内容 | Resource | 只读上下文 |
mkdir mcp-project-server && cd mcp-project-server
npm init -y
修改 package.json 关键字段:
{
"name": "mcp-project-server",
"version": "1.0.0",
"description": "A project management MCP server with Tools, Resources, and Prompts",
"type": "module",
"main": "dist/index.js",
"bin": {
"mcp-project-server": "dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "npx tsx src/index.ts",
"inspect": "npx @modelcontextprotocol/inspector npx tsx src/index.ts",
"prepublishOnly": "npm run build"
},
"files": ["dist"]
}
关键要点:
- "type": "module" — MCP SDK使用ES模块
- "bin" 字段 — 发布后支持 npx mcp-project-server 直接运行
- "files": ["dist"] — 只发布编译产物,减小包体积
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
当前SDK版本为 1.29.0,支持完整的MCP规范。
创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
mcp-project-server/
├── src/
│ ├── index.ts # 入口:服务器组装与启动
│ ├── tools/ # Tool定义
│ │ └── project.ts
│ ├── resources/ # Resource定义
│ │ └── project.ts
│ ├── prompts/ # Prompt定义
│ │ └── review.ts
│ └── store/ # 内存数据存储
│ └── projects.ts
├── tsconfig.json
├── package.json
└── .gitignore
创建 src/store/projects.ts,使用内存存储模拟数据库:
// src/store/projects.ts
export interface Project {
id: string;
name: string;
status: "active" | "completed" | "paused";
description: string;
tasks: Task[];
createdAt: string;
}
export interface Task {
id: string;
title: string;
done: boolean;
assignee?: string;
}
// 内存数据存储
const projects: Map<string, Project> = new Map();
// 初始化示例数据
projects.set("proj-001", {
id: "proj-001",
name: "MCP Server开发",
status: "active",
description: "构建一个包含Tool、Resource、Prompt的完整MCP服务器",
tasks: [
{ id: "t-1", title: "设计三原语API", done: true, assignee: "Alice" },
{ id: "t-2", title: "实现Tool处理器", done: true, assignee: "Bob" },
{ id: "t-3", title: "实现Resource处理器", done: false, assignee: "Alice" },
{ id: "t-4", title: "编写Prompt模板", done: false, assignee: "Charlie" },
],
createdAt: "2026-05-20T10:00:00Z",
});
export function getAllProjects(): Project[] {
return Array.from(projects.values());
}
export function getProject(id: string): Project | undefined {
return projects.get(id);
}
export function createProject(
name: string,
description: string
): Project {
const id = `proj-${String(projects.size + 1).padStart(3, "0")}`;
const project: Project = {
id,
name,
status: "active",
description,
tasks: [],
createdAt: new Date().toISOString(),
};
projects.set(id, project);
return project;
}
export function addTask(
projectId: string,
title: string,
assignee?: string
): Project | null {
const project = projects.get(projectId);
if (!project) return null;
const taskId = `t-${project.tasks.length + 1}`;
project.tasks.push({ id: taskId, title, done: false, assignee });
return project;
}
export function completeTask(
projectId: string,
taskId: string
): Project | null {
const project = projects.get(projectId);
if (!project) return null;
const task = project.tasks.find((t) => t.id === taskId);
if (!task) return null;
task.done = true;
return project;
}
Tool是MCP中最常用的原语,用于执行操作、产生副作用、返回结构化结果。
创建 src/tools/project.ts:
// src/tools/project.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import {
getAllProjects,
getProject,
createProject,
addTask,
completeTask,
} from "../store/projects.js";
export function registerProjectTools(server: McpServer): void {
// Tool 1: 列出所有项目
server.tool(
"list_projects",
"列出所有项目及其状态概览",
{},
async () => {
const projects = getAllProjects();
const summary = projects.map((p) => ({
id: p.id,
name: p.name,
status: p.status,
tasks: `${p.tasks.filter((t) => t.done).length}/${p.tasks.length} 已完成`,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(summary, null, 2),
},
],
};
}
);
// Tool 2: 查看项目详情
server.tool(
"get_project",
"获取指定项目的完整详情,包含所有任务",
{
projectId: z
.string()
.describe("项目ID,格式为 proj-XXX"),
},
async ({ projectId }) => {
const project = getProject(projectId);
if (!project) {
return {
content: [
{
type: "text",
text: `错误:未找到项目 ${projectId}`,
},
],
isError: true,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(project, null, 2),
},
],
};
}
);
// Tool 3: 创建新项目
server.tool(
"create_project",
"创建一个新项目",
{
name: z.string().min(1).describe("项目名称"),
description: z.string().describe("项目描述"),
},
async ({ name, description }) => {
const project = createProject(name, description);
return {
content: [
{
type: "text",
text: `项目已创建:${project.id} - ${project.name}`,
},
],
};
}
);
// Tool 4: 添加任务
server.tool(
"add_task",
"向指定项目添加新任务",
{
projectId: z.string().describe("目标项目ID"),
title: z.string().min(1).describe("任务标题"),
assignee: z.string().optional().describe("负责人(可选)"),
},
async ({ projectId, title, assignee }) => {
const project = addTask(projectId, title, assignee);
if (!project) {
return {
content: [
{
type: "text",
text: `错误:未找到项目 ${projectId}`,
},
],
isError: true,
};
}
const newTask = project.tasks[project.tasks.length - 1];
return {
content: [
{
type: "text",
text: `任务已添加到 ${project.name}:[${newTask.id}] ${newTask.title}${assignee ? ` → ${assignee}` : ""}`,
},
],
};
}
);
// Tool 5: 完成任务
server.tool(
"complete_task",
"将指定任务标记为已完成",
{
projectId: z.string().describe("项目ID"),
taskId: z.string().describe("任务ID"),
},
async ({ projectId, taskId }) => {
const project = completeTask(projectId, taskId);
if (!project) {
return {
content: [
{
type: "text",
text: `错误:未找到项目 ${projectId} 或任务 ${taskId}`,
},
],
isError: true,
};
}
const doneCount = project.tasks.filter((t) => t.done).length;
return {
content: [
{
type: "text",
text: `任务 ${taskId} 已完成!进度:${doneCount}/${project.tasks.length}`,
},
],
};
}
);
}
Tool开发要点:
| 要点 | 说明 | 示例 |
|---|---|---|
| 参数用Zod定义 | 提供运行时验证 + 自动生成JSON Schema | z.string().min(1) |
| 描述必填 | Claude根据描述决定何时调用此Tool | "列出所有项目及其状态概览" |
| 错误用 isError | 让Claude区分正常结果与错误 | isError: true |
| 返回TextContent | 当前最通用,未来支持Image等 | { type: "text", text: "..." } |
| 空参数传空对象 | 没有参数时必须传 {} |
server.tool("name", "desc", {}, handler) |
Resource为AI提供按需读取的上下文数据,类似于"数据端点"。Claude在需要时会主动请求Resource。
创建 src/resources/project.ts:
// src/resources/project.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getAllProjects, getProject } from "../store/projects.js";
export function registerProjectResources(server: McpServer): void {
// Resource 1: 静态URI — 项目列表摘要
server.resource(
"projects-summary",
"projects://summary",
async (uri) => {
const projects = getAllProjects();
const lines = projects.map(
(p) =>
`- [${p.status.toUpperCase()}] ${p.name} (${p.id}): ` +
`${p.tasks.filter((t) => t.done).length}/${p.tasks.length} tasks`
);
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: `# 项目概览 (${projects.length}个项目)\n\n${lines.join("\n")}`,
},
],
};
}
);
// Resource 2: 动态URI模板 — 单个项目详情
server.resource(
"project-detail",
new URL("projects://{projectId}/detail"),
async (uri, { projectId }) => {
const project = getProject(projectId);
if (!project) {
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: `未找到项目:${projectId}`,
},
],
};
}
const taskList = project.tasks
.map(
(t) =>
` [${t.done ? "x" : " "}] ${t.id}: ${t.title}` +
(t.assignee ? ` @${t.assignee}` : "")
)
.join("\n");
return {
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text:
`# ${project.name}\n\n` +
`**状态**: ${project.status}\n` +
`**创建时间**: ${project.createdAt}\n\n` +
`## 描述\n${project.description}\n\n` +
`## 任务清单\n${taskList}`,
},
],
};
}
);
// Resource 3: 静态URI — 团队工作量统计
server.resource(
"team-workload",
"projects://team-workload",
async (uri) => {
const projects = getAllProjects();
const workload: Record<string, { total: number; done: number }> = {};
for (const p of projects) {
for (const t of p.tasks) {
if (t.assignee) {
if (!workload[t.assignee]) {
workload[t.assignee] = { total: 0, done: 0 };
}
workload[t.assignee].total++;
if (t.done) workload[t.assignee].done++;
}
}
}
const lines = Object.entries(workload).map(
([name, w]) => `- ${name}: ${w.done}/${w.total} 完成`
);
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: `# 团队工作量\n\n${lines.join("\n")}`,
},
],
};
}
);
}
Resource vs Tool的关键区别:
graph LR
subgraph "Resource(数据暴露)"
direction TB
R1["AI按需拉取"]
R2["只读,无副作用"]
R3["URI定位数据"]
R4["返回内容供上下文使用"]
R1 --> R2 --> R3 --> R4
end
subgraph "Tool(工具调用)"
direction TB
T1["AI主动调用"]
T2["可有副作用"]
T3["参数化输入"]
T4["返回结果供下一步决策"]
T1 --> T2 --> T3 --> T4
end
| 维度 | Resource | Tool |
|---|---|---|
| 触发方式 | AI按需读取 | AI主动调用 |
| 副作用 | 无(只读) | 可有 |
| 定位方式 | URI | 函数名 + 参数 |
| MIME类型 | 支持 text、markdown等 | 固定TextContent |
| 典型场景 | 暴露文件、配置、状态 | API调用、数据库写入 |
动态URI模板说明:
new URL("projects://{projectId}/detail") 创建了一个URI模板。当Claude请求 projects://proj-001/detail 时,SDK自动提取 {projectId} 参数并传入handler。注意,使用 new URL() 创建模板对象是SDK 1.x的写法,区别于纯字符串的静态URI。
Prompt为AI提供可参数化的预设指令模板,用户可以传入变量快速生成定制化的提示词。
创建 src/prompts/review.ts:
// src/prompts/review.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { getProject } from "../store/projects.js";
export function registerProjectPrompts(server: McpServer): void {
// Prompt 1: 项目状态报告生成
server.prompt(
"status-report",
"生成指定项目的状态报告模板",
{
projectId: z.string().describe("项目ID"),
format: z
.enum(["markdown", "text"])
.optional()
.describe("输出格式,默认markdown"),
},
async ({ projectId, format }) => {
const project = getProject(projectId);
const projectName = project?.name ?? "未知项目";
const useMarkdown = format !== "text";
const instruction = useMarkdown
? `请为项目「${projectName}」生成一份Markdown格式的状态报告,包含以下章节:
## 项目概览
- 项目名称:${projectName}
- 当前状态:${project?.status ?? "未知"}
- 报告日期:${new Date().toISOString().split("T")[0]}
## 进度摘要
请分析当前任务完成情况并给出百分比和趋势判断。
## 风险与阻塞
请识别未完成任务中的潜在风险点。
## 下一步行动
请为每个未完成任务建议具体的下一步操作。
## 团队分配
请分析团队成员的工作负载是否均衡。`
: `请为项目「${projectName}」生成一份纯文本状态报告,包含:项目名称、当前状态、进度百分比、风险点、下一步行动。保持简洁,每项不超过两行。`;
return {
messages: [
{
role: "user",
content: { type: "text", text: instruction },
},
],
};
}
);
// Prompt 2: 任务分派指令
server.prompt(
"assign-tasks",
"生成任务分派建议",
{
projectId: z.string().describe("项目ID"),
teamMembers: z
.string()
.describe("团队成员,逗号分隔,如:Alice,Bob,Charlie"),
},
async ({ projectId, teamMembers }) => {
const project = getProject(projectId);
const pendingTasks = project?.tasks.filter((t) => !t.done) ?? [];
const members = teamMembers.split(",").map((m) => m.trim());
const taskList = pendingTasks
.map((t) => `- [ ] ${t.title}${t.assignee ? ` (当前: @${t.assignee})` : ""}`)
.join("\n");
return {
messages: [
{
role: "user",
content: {
type: "text",
text:
`请为以下未完成任务做最优分派建议。\n\n` +
`## 团队成员\n${members.map((m) => `- ${m}`).join("\n")}\n\n` +
`## 待分派任务\n${taskList}\n\n` +
`请考虑以下原则:\n` +
`1. 负载均衡:尽量平均分配\n` +
`2. 专业对口:根据任务内容匹配最合适的成员\n` +
`3. 依赖关系:有前后依赖的任务尽量分给同一人\n` +
`4. 输出格式:表格,列为 [任务 | 建议负责人 | 理由]`,
},
},
],
};
}
);
// Prompt 3: 每日站会模板
server.prompt(
"daily-standup",
"生成每日站会报告模板",
{
projectId: z.string().describe("项目ID"),
reporter: z.string().describe("报告人姓名"),
},
async ({ projectId, reporter }) => {
const project = getProject(projectId);
const myTasks = project?.tasks.filter((t) => t.assignee === reporter) ?? [];
const doneTasks = myTasks.filter((t) => t.done);
const pendingTasks = myTasks.filter((t) => !t.done);
return {
messages: [
{
role: "user",
content: {
type: "text",
text:
`请帮 ${reporter} 生成「${project?.name ?? projectId}」的每日站会报告。\n\n` +
`## 昨天完成\n${doneTasks.length > 0 ? doneTasks.map((t) => `- ✅ ${t.title}`).join("\n") : "- 无"}\n\n` +
`## 今天计划\n${pendingTasks.length > 0 ? pendingTasks.map((t) => `- 📋 ${t.title}`).join("\n") : "- 无待办"}\n\n` +
`## 阻塞/风险\n请根据任务状态分析是否存在阻塞点,并给出建议。\n\n` +
`请用简洁的语言整理,适合在3分钟内口头汇报。`,
},
},
],
};
}
);
}
Prompt开发要点:
messages 数组,不是 content,这是和Tool/Resource的关键区别创建 src/index.ts:
#!/usr/bin/env node
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerProjectTools } from "./tools/project.js";
import { registerProjectResources } from "./resources/project.js";
import { registerProjectPrompts } from "./prompts/review.js";
async function main() {
// 创建MCP服务器实例
const server = new McpServer({
name: "mcp-project-server",
version: "1.0.0",
description: "项目管理MCP服务器 — 支持Tool、Resource、Prompt三原语",
});
// 注册三原语
registerProjectTools(server);
registerProjectResources(server);
registerProjectPrompts(server);
// 使用Stdio传输层启动
const transport = new StdioServerTransport();
await server.connect(transport);
// 注意:Stdio模式下不要用console.log
// 所有输出通过MCP协议传输,console.log会污染通信管道
}
main().catch((error) => {
// 错误输出用stderr,不会污染MCP通信
process.stderr.write(`Fatal error: ${error}\n`);
process.exit(1);
});
关键细节:
#!/usr/bin/env node — 发布后npm能正确执行process.stderr.write编译并验证:
npx tsc
# 无错误输出则编译成功
# 开发模式:直接用tsx运行源码
claude mcp add project-server -- npx tsx /path/to/mcp-project-server/src/index.ts
# 发布后:用npx运行npm包
claude mcp add project-server -- npx -y mcp-project-server
在项目根目录创建 .mcp.json:
{
"mcpServers": {
"project-server": {
"command": "npx",
"args": ["tsx", "src/index.ts"],
"type": "stdio"
}
}
}
或在用户级配置 ~/.claude.json 中添加(全局生效):
{
"mcpServers": {
"project-server": {
"command": "npx",
"args": ["-y", "mcp-project-server"],
"type": "stdio"
}
}
}
启动Claude Code后,输入 /mcp 查看已挂载的服务器列表:
> /mcp
● project-server (stdio)
Tools: list_projects, get_project, create_project, add_task, complete_task
Resources: projects-summary, project-detail, team-workload
Prompts: status-report, assign-tasks, daily-standup
看到三原语全部列出,说明挂载成功。
MCP Inspector是官方提供的可视化调试工具,独立于Claude Code,用于直接测试MCP服务器。
# 方式一:通过package.json的script
npm run inspect
# 方式二:直接运行
npx @modelcontextprotocol/inspector npx tsx src/index.ts
Inspector会自动启动一个本地Web界面(默认 http://localhost:6274),打开浏览器即可操作。
flowchart TD
A["启动Inspector"] --> B["连接MCP服务器"]
B --> C["查看Capabilities"]
C --> D{选择原语}
D --> |"Tool"| E["列出所有Tools"]
D --> |"Resource"| F["列出所有Resources"]
D --> |"Prompt"| G["列出所有Prompts"]
E --> H["选择Tool → 填参数 → 执行"]
F --> I["选择Resource → 读取内容"]
G --> J["选择Prompt → 填参数 → 生成"]
H --> K["查看返回结果"]
I --> K
J --> K
K --> L["检查JSON格式是否正确"]
测试Tool — create_project:
在Inspector界面:
1. 点击 "Tools" 标签
2. 选择 create_project
3. 填入参数:
json
{
"name": "AI搜索优化",
"description": "优化搜索算法的召回率和准确率"
}
4. 点击 "Run Tool"
5. 预期结果:
json
{
"content": [
{
"type": "text",
"text": "项目已创建:proj-002 - AI搜索优化"
}
]
}
测试Resource — projects://summary:
projects://summary测试Prompt — daily-standup:
daily-standup{ "projectId": "proj-001", "reporter": "Alice" }| 问题 | 原因 | 解决 |
|---|---|---|
| Inspector连接失败 | 服务器启动报错 | 检查 process.stderr.write 的错误日志 |
| Tool返回空结果 | handler中throw了异常 | 查看Inspector的错误面板 |
| Resource不显示 | URI格式错误 | 确保URI是合法格式,动态模板用 new URL() |
| Prompt参数缺失 | Zod schema定义有误 | 检查Inspector自动生成的表单字段 |
| 中文乱码 | 编码问题 | 确保 tsconfig.json 的 target 为 ES2022 |
在 package.json 中添加发布必需字段:
{
"name": "mcp-project-server",
"version": "1.0.0",
"description": "A project management MCP server with Tools, Resources, and Prompts",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"mcp-project-server": "dist/index.js"
},
"keywords": ["mcp", "model-context-protocol", "claude", "ai-tools"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/mcp-project-server"
},
"engines": {
"node": ">=18.0.0"
}
}
添加 .npmignore:
src/
tsconfig.json
node_modules/
.git/
# 编译TypeScript
npx tsc
# 验证编译产物
ls -la dist/
# 应看到 index.js, tools/project.js, resources/project.js, prompts/review.js 等
# 本地测试运行
node dist/index.js
# 应无错误输出(服务器在等待stdin输入)
# 登录npm(首次)
npm login
# 发布
npm publish
# 如果包名已被占用,使用scope
npm publish --access public
# 包名需改为 @yourname/mcp-project-server
# 全局测试
npx -y mcp-project-server
# 在Claude Code中配置
claude mcp add project-server -- npx -y mcp-project-server
# 验证
claude
> /mcp
# 修改代码后
npx tsc # 重新编译
npm version patch # 1.0.0 → 1.0.1
npm publish # 发布新版本
# Claude Code用户无需任何操作
# npx -y 会自动拉取最新版本
配置完成后,在Claude Code中的典型交互流程:
用户:帮我看看当前有哪些项目
Claude:我来查看项目列表。
[调用 list_projects Tool]
当前有2个项目:
1. **MCP Server开发** (proj-001)
- 状态:active
- 进度:2/4 已完成
2. **AI搜索优化** (proj-002)
- 状态:active
- 进度:0/0 已完成
---
用户:给MCP Server项目加一个任务"编写单元测试",分配给Bob
Claude:
[调用 add_task Tool,参数:{ projectId: "proj-001", title: "编写单元测试", assignee: "Bob" }]
任务已添加到 MCP Server开发:[t-5] 编写单元测试 → Bob
---
用户:生成Alice的每日站会报告
Claude:
[调用 daily-standup Prompt,参数:{ projectId: "proj-001", reporter: "Alice" }]
根据Prompt模板生成的站会报告:
## Alice 每日站会 — MCP Server开发
### 昨天完成
- ✅ 设计三原语API
- ✅ 实现Resource处理器(待确认,task t-3可能未完成)
### 今天计划
- 📋 实现Resource处理器
- 📋 编写Prompt模板
### 阻塞/风险
t-3(实现Resource处理器)已持续较长时间,建议确认是否有技术难点。
---
用户:帮我读取项目概览数据
Claude:
[读取 projects://summary Resource]
# 项目概览 (2个项目)
- [ACTIVE] MCP Server开发 (proj-001): 2/4 tasks
- [ACTIVE] AI搜索优化 (proj-002): 0/0 tasks
graph TB
subgraph "Claude Code"
CC["Claude Code CLI"]
MCP["MCP Client"]
end
subgraph "mcp-project-server"
direction TB
ENTRY["index.ts<br/>服务器入口"]
subgraph "三原语注册"
TOOLS["Tools<br/>5个工具"]
RES["Resources<br/>3个数据端点"]
PROMPTS["Prompts<br/>3个模板"]
end
STORE["内存数据存储<br/>projects Map"]
end
CC -->|"stdin/stdout<br/>Stdio传输"| MCP
MCP -->|"MCP协议"| ENTRY
ENTRY --> TOOLS
ENTRY --> RES
ENTRY --> PROMPTS
TOOLS --> STORE
RES --> STORE
PROMPTS --> STORE
subgraph "工具清单"
T1["list_projects"]
T2["get_project"]
T3["create_project"]
T4["add_task"]
T5["complete_task"]
end
subgraph "数据端点"
R1["projects://summary"]
R2["projects://{id}/detail"]
R3["projects://team-workload"]
end
subgraph "提示词模板"
P1["status-report"]
P2["assign-tasks"]
P3["daily-standup"]
end
TOOLS -.-> T1 & T2 & T3 & T4 & T5
RES -.-> R1 & R2 & R3
PROMPTS -.-> P1 & P2 & P3
style CC fill:#4A90D9,color:#fff
style ENTRY fill:#FF9500,color:#fff
style TOOLS fill:#50C878,color:#fff
style RES fill:#9B59B6,color:#fff
style PROMPTS fill:#E74C3C,color:#fff
style STORE fill:#F39C12,color:#fff
| 原语 | 命名风格 | 正例 | 反例 |
|---|---|---|---|
| Tool | snake_case动词短语 | create_project |
projectCreate / 新建项目 |
| Resource URI | 路径式 | projects://{id}/detail |
getProjectDetail |
| Prompt | kebab-case | daily-standup |
Daily Standup |
Tool的 description 直接影响Claude的调用决策:
# 好的描述:包含触发条件和行为
"列出所有项目及其状态概览,当用户询问项目情况时使用"
# 差的描述:模糊不清
"获取数据"
// 推荐:使用 isError 标记
if (!project) {
return {
content: [{ type: "text", text: `错误:项目 ${id} 不存在` }],
isError: true, // 让Claude知道这是错误,不是正常结果
};
}
// 反模式:throw Error
// throw new Error("Project not found");
// 这会导致MCP协议错误,Claude无法理解具体原因
| 场景 | 建议 |
|---|---|
| Resource内容较大 | 分页返回,或只返回摘要 |
| Tool调用耗时 | 考虑流式返回(progress notification) |
| 内存数据 | 注意泄漏,长时间运行的stdio进程需清理 |
| 外部API调用 | 添加超时和重试逻辑 |
| 反模式 | 问题 | 正确做法 |
|---|---|---|
用 console.log 输出日志 |
污染MCP通信管道 | 用 process.stderr.write |
Tool handler中 throw |
返回协议级错误 | 返回 isError: true 的content |
| Resource中返回二进制 | 不被支持 | 返回base64编码或文件路径 |
| Prompt返回多条message | 增加复杂度 | 保持单条user message |
| 忽略Zod验证 | LLM可能传入非法参数 | 所有参数用Zod定义并加 .describe() |
| Resource URI用中文 | URI规范不支持 | 使用英文路径 + Unicode编码 |
当MCP服务器需要被多个客户端共享或远程部署时,需要从Stdio迁移到Streamable HTTP传输。
graph LR
subgraph "Stdio(本地)"
A["Claude Code"] -->|"stdin/stdout"| B["MCP Server进程"]
end
subgraph "Streamable HTTP(远程)"
C["Claude Code"] -->|"HTTP POST"| D["MCP Server<br/>:3000"]
E["Cursor"] -->|"HTTP POST"| D
F["其他AI客户端"] -->|"HTTP POST"| D
end
style B fill:#50C878,color:#fff
style D fill:#4A90D9,color:#fff
修改 src/index.ts:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import express from "express";
// ... 服务器创建代码相同 ...
const app = express();
app.use(express.json());
const transports: Map<string, StreamableHTTPServerTransport> = new Map();
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.has(sessionId)) {
// 复用已有session
transport = transports.get(sessionId)!;
} else if (!sessionId && isInitializeRequest(req.body)) {
// 新session
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
transports.set(transport.sessionId!, transport);
await server.connect(transport);
} else {
res.status(400).json({ error: "Invalid request" });
return;
}
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => {
process.stderr.write("MCP Server running on http://localhost:3000/mcp\n");
});
{
"mcpServers": {
"project-server": {
"type": "http",
"url": "http://localhost:3000/mcp"
}
}
}
注意:SSE传输已于2026年4月废弃,新项目直接使用Streamable HTTP。
| 维度 | 评估 |
|---|---|
| 适合训练营类型 | AI工具链开发营、MCP生态开发者训练营 |
| 预计教学时长 | 4-6小时(含实操) |
| 难度等级 | 中级(需要TypeScript和npm基础) |
| 前置课程 | TypeScript基础、Claude Code入门 |
| 实操设备要求 | 每人一台有Node.js 18+的开发机 |
| 推荐分组 | 2-3人一组,每组完成一个不同主题的MCP服务器 |
| 成果交付 | 每组发布一个可 npx 运行的MCP服务器到npm |
| 阶段 | 时长 | 内容 |
|---|---|---|
| 理论讲解 | 45分钟 | MCP协议概述、三原语概念、SDK架构 |
| Tool开发 | 60分钟 | 从一个Tool到多个Tool,Zod验证 |
| Resource开发 | 45分钟 | 静态URI、动态模板、MIME类型 |
| Prompt开发 | 30分钟 | 参数化模板、多场景Prompt设计 |
| 调试发布 | 45分钟 | MCP Inspector调试、npm发布流程 |
| 综合实战 | 60分钟 | 分组完成一个完整MCP服务器 |
| 展示评审 | 30分钟 | 各组演示,互评 |
[!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 等场景全覆盖。
