<!-- title: 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威力需要理解三个原语的分工。

```mermaid
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 创建项目结构

```bash
mkdir mcp-project-server && cd mcp-project-server
npm init -y
```

修改 `package.json` 关键字段：

```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 安装依赖

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

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

### 2.3 TypeScript配置

创建 `tsconfig.json`：

```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`，使用内存存储模拟数据库：

```typescript
// 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`：

```typescript
// 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`：

```typescript
// 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的关键区别：**

```mermaid
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`：

```typescript
// 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开发要点：**

- Prompt返回的是 `messages` 数组，不是 `content`，这是和Tool/Resource的关键区别
- 参数同样使用Zod定义，SDK自动生成JSON Schema
- Prompt适合提供"交互模板"，让用户以最少输入获得最优输出
- 一个Prompt可以返回多条message（多轮对话模板），但单条user message最常见

## 七、组装服务器入口

创建 `src/index.ts`：

```typescript
#!/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. **模块化注册** — 每种原语独立文件，便于维护和测试

编译并验证：

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

## 八、配置Claude Code挂载

### 8.1 方式一：命令行添加（推荐）

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

```json
{
  "mcpServers": {
    "project-server": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"],
      "type": "stdio"
    }
  }
}
```

或在用户级配置 `~/.claude.json` 中添加（全局生效）：

```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

```bash
# 方式一：通过package.json的script
npm run inspect

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

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

### 9.2 Inspector调试流程

```mermaid
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` 中添加发布必需字段：

```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 编译与验证

```bash
# 编译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

```bash
# 登录npm（首次）
npm login

# 发布
npm publish

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

### 10.4 发布后验证

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

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

# 验证
claude
> /mcp
```

### 10.5 版本更新流程

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

## 十二、完整架构图

```mermaid
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 错误处理模式

```typescript
// 推荐：使用 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传输

```mermaid
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`：

```typescript
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模式

```json
{
  "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](https://github.com/modelcontextprotocol/typescript-sdk) — 官方TypeScript SDK源码及API文档
2. [Build an MCP Server — Model Context Protocol](https://modelcontextprotocol.io/docs/develop/build-server) — MCP官方服务器构建指南
3. [MCP TypeScript SDK Server API Reference](https://ts.sdk.modelcontextprotocol.io/documents/server.html) — McpServer类的完整API文档
4. [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp) — Claude Code MCP配置官方文档
5. [MCP Inspector — GitHub](https://github.com/modelcontextprotocol/inspector) — MCP可视化调试工具
6. [Streamable HTTP vs SSE for MCP Servers](https://toolradar.com/blog/streamable-http-vs-sse) — SSE废弃及Streamable HTTP迁移指南
7. [Publish MCP Server to npm — RapidDev](https://www.rapidevelopers.com/mcp-tutorial/how-to-publish-mcp-server-to-npm) — 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
