首页 › tutorials › MCP三原语开发:从Tool到npm发布全实战

MCP三原语开发:从Tool到npm发布全实战

概述

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三原语:何时用哪个?

绝大多数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 只读上下文

二、项目初始化

2.1 创建项目结构

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"] — 只发布编译产物,减小包体积

2.2 安装依赖

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

当前SDK版本为 1.29.0,支持完整的MCP规范。

2.3 TypeScript配置

创建 tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

2.4 项目目录结构

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开发:动作与副作用

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开发:只读数据暴露

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开发:提示词模板

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开发要点:

七、组装服务器入口

创建 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);
});

关键细节:

  1. Shebang行 #!/usr/bin/env node — 发布后npm能正确执行
  2. Stdio传输 — 本地MCP服务器使用stdin/stdout通信,性能最好
  3. 禁止console.log — stdout被MCP协议占用,调试信息只能用 process.stderr.write
  4. 模块化注册 — 每种原语独立文件,便于维护和测试

编译并验证:

npx tsc
# 无错误输出则编译成功

八、配置Claude Code挂载

8.1 方式一:命令行添加(推荐)

# 开发模式:直接用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

8.2 方式二:配置文件

在项目根目录创建 .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"
    }
  }
}

8.3 验证挂载

启动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调试

MCP Inspector是官方提供的可视化调试工具,独立于Claude Code,用于直接测试MCP服务器。

9.1 启动Inspector

# 方式一:通过package.json的script
npm run inspect

# 方式二:直接运行
npx @modelcontextprotocol/inspector npx tsx src/index.ts

Inspector会自动启动一个本地Web界面(默认 http://localhost:6274),打开浏览器即可操作。

9.2 Inspector调试流程

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格式是否正确"]

9.3 测试用例

测试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:

  1. 点击 "Resources" 标签
  2. 点击 projects://summary
  3. 预期返回项目列表的文本摘要

测试Prompt — daily-standup:

  1. 点击 "Prompts" 标签
  2. 选择 daily-standup
  3. 填入参数:{ "projectId": "proj-001", "reporter": "Alice" }
  4. 预期返回包含Alice已完成和待办任务的站会模板

9.4 常见问题排查

问题 原因 解决
Inspector连接失败 服务器启动报错 检查 process.stderr.write 的错误日志
Tool返回空结果 handler中throw了异常 查看Inspector的错误面板
Resource不显示 URI格式错误 确保URI是合法格式,动态模板用 new URL()
Prompt参数缺失 Zod schema定义有误 检查Inspector自动生成的表单字段
中文乱码 编码问题 确保 tsconfig.json 的 target 为 ES2022

十、npm发布与分发

10.1 发布前准备

在 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/

10.2 编译与验证

# 编译TypeScript
npx tsc

# 验证编译产物
ls -la dist/
# 应看到 index.js, tools/project.js, resources/project.js, prompts/review.js 等

# 本地测试运行
node dist/index.js
# 应无错误输出(服务器在等待stdin输入)

10.3 发布到npm

# 登录npm(首次)
npm login

# 发布
npm publish

# 如果包名已被占用,使用scope
npm publish --access public
# 包名需改为 @yourname/mcp-project-server

10.4 发布后验证

# 全局测试
npx -y mcp-project-server

# 在Claude Code中配置
claude mcp add project-server -- npx -y mcp-project-server

# 验证
claude
> /mcp

10.5 版本更新流程

# 修改代码后
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

十三、最佳实践与反模式

13.1 命名规范

原语 命名风格 正例 反例
Tool snake_case动词短语 create_project projectCreate / 新建项目
Resource URI 路径式 projects://{id}/detail getProjectDetail
Prompt kebab-case daily-standup Daily Standup

13.2 描述编写原则

Tool的 description 直接影响Claude的调用决策:

# 好的描述:包含触发条件和行为
"列出所有项目及其状态概览,当用户询问项目情况时使用"

# 差的描述:模糊不清
"获取数据"

13.3 错误处理模式

// 推荐:使用 isError 标记
if (!project) {
  return {
    content: [{ type: "text", text: `错误:项目 ${id} 不存在` }],
    isError: true,  // 让Claude知道这是错误,不是正常结果
  };
}

// 反模式:throw Error
// throw new Error("Project not found");
// 这会导致MCP协议错误,Claude无法理解具体原因

13.4 性能考量

场景 建议
Resource内容较大 分页返回,或只返回摘要
Tool调用耗时 考虑流式返回(progress notification)
内存数据 注意泄漏,长时间运行的stdio进程需清理
外部API调用 添加超时和重试逻辑

13.5 反模式清单

反模式 问题 正确做法
用 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编码

十四、从Stdio迁移到Streamable HTTP

当MCP服务器需要被多个客户端共享或远程部署时,需要从Stdio迁移到Streamable HTTP传输。

14.1 为什么需要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

14.2 HTTP模式代码变更

修改 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");
});

14.3 Claude Code配置HTTP模式

{
  "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分钟 各组演示,互评

参考来源

  1. MCP TypeScript SDK — GitHub — 官方TypeScript SDK源码及API文档
  2. Build an MCP Server — Model Context Protocol — MCP官方服务器构建指南
  3. MCP TypeScript SDK Server API Reference — McpServer类的完整API文档
  4. Connect Claude Code to tools via MCP — Claude Code MCP配置官方文档
  5. MCP Inspector — GitHub — MCP可视化调试工具
  6. Streamable HTTP vs SSE for MCP Servers — SSE废弃及Streamable HTTP迁移指南
  7. Publish MCP Server to npm — RapidDev — MCP服务器npm发布教程

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入