<!-- title: MCP Server TypeScript全实战：三原语到发布 -->

# MCP Server TypeScript全实战：三原语到发布

**学习目标**：掌握 MCP（Model Context Protocol）TypeScript SDK 的完整开发流程，从理解 Tools / Resources / Prompts 三大原语开始，逐步构建一个具备真实功能的 MCP Server，最终配置到 Claude Code 并发布到 npm。

**适用人群**：有 TypeScript/Node.js 基础，希望为 Claude Code 创建自定义工具扩展的开发者。

**预计时长**：4-6 小时（含实战项目）

---

## 一、MCP 架构全景：你到底在构建什么

MCP（Model Context Protocol）是 Anthropic 推出的开放标准协议，定义了 AI 模型与外部系统之间的通信规范。截至 2026 年 5 月，MCP SDK 月下载量已超过 9700 万，公开注册的 MCP Server 超过 10,000 个。

理解 MCP 架构的关键是区分三个角色和两层协议：

```
┌──────────────┐    JSON-RPC     ┌──────────────┐
│  MCP Client  │◄──────────────►│  MCP Server  │
│  (Claude Code│    stdio/HTTP   │  (你开发的    │
│   / Desktop) │                 │   工具服务)   │
└──────────────┘                 └──────────────┘
       │                                │
       │ LLM 调用工具                    │ 访问外部系统
       ▼                                ▼
┌──────────────┐                 ┌──────────────┐
│   AI Model   │                 │  数据库/API/  │
│  (Claude等)  │                 │  文件系统等   │
└──────────────┘                 └──────────────┘
```

### 1.1 MCP Server 三大原语

MCP Server 通过三个核心原语（Primitives）向 Client 暴露能力：

| 原语 | 控制方 | 用途 | 典型场景 |
|------|--------|------|----------|
| **Tools** | 模型控制 | 执行操作、调用 API | 查询数据库、发送请求、计算 |
| **Resources** | 应用控制 | 提供数据上下文 | 读取文件、获取配置、加载数据 |
| **Prompts** | 用户控制 | 提供模板化交互 | 预设提示词、工作流模板 |

**控制方**的区别至关重要：
- **Tools** 由 LLM 决定何时调用（模型自主）
- **Resources** 由用户或应用显式请求（人为驱动）
- **Prompts** 由用户选择并填充参数后提交（模板化交互）

### 1.2 传输机制

MCP 支持两种传输方式：

| 传输方式 | 适用场景 | Claude Code 支持 |
|----------|----------|-----------------|
| **stdio** | 本地进程通信 | 原生支持 |
| **Streamable HTTP** | 远程/网络通信 | 支持 |

> 注意：SSE 传输已在 MCP 规范最新版本中被标记为废弃（deprecated），推荐使用 Streamable HTTP 替代。

```mermaid
flowchart TB
    subgraph Local["本地模式 (stdio)"]
        CC1["Claude Code"] -->|stdin/stdout| MS1["MCP Server 进程"]
    end

    subgraph Remote["远程模式 (Streamable HTTP)"]
        CC2["Claude Code"] -->|HTTP POST| PROXY["HTTP Server"]
        PROXY -->|JSON-RPC| MS2["MCP Server"]
    end

    subgraph Dev["开发调试"]
        MI["MCP Inspector"] -->|stdio/HTTP| MS3["MCP Server"]
    end
```

---

## 二、环境准备

### 2.1 前置条件

```bash
# 确认 Node.js >= 18
node --version   # v18+ 或 v20+ 推荐

# 确认 Claude Code 已安装
claude --version
```

### 2.2 创建项目

```bash
# 创建项目目录
mkdir mcp-weather-server && cd mcp-weather-server

# 初始化 TypeScript 项目
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

# 创建 TypeScript 配置
cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}
EOF

# 创建源码目录
mkdir src
```

### 2.3 配置 package.json

```json
{
  "name": "mcp-weather-server",
  "version": "1.0.0",
  "description": "MCP Server for weather data queries",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "mcp-weather-server": "dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "start": "node dist/index.js",
    "inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
  },
  "files": ["dist"]
}
```

**关键点说明**：
- `"type": "module"` — 使用 ES Module，MCP SDK 要求 ESM
- `"bin"` 字段 — 使包可通过 `npx` 执行，也用于 npm 发布
- `"inspect"` 脚本 — 使用 MCP Inspector 进行可视化调试

---

## 三、实战一：构建 Tool 原语 — 天气查询工具

Tool 是 MCP Server 最核心的原语，LLM 会根据用户意图自主决定调用哪个 Tool。每个 Tool 需要声明名称、描述和参数 Schema。

### 3.1 最小可运行 Server

创建 `src/index.ts`：

```typescript
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 创建 MCP Server 实例
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
});

// ========== Tool 1: 获取当前天气 ==========
server.tool(
  "get_current_weather",          // 工具名称
  "获取指定城市的当前天气信息",      // 工具描述（LLM 靠这个决定是否调用）
  {
    city: z.string().describe("城市名称，如'北京'、'Shanghai'"),
    unit: z.enum(["celsius", "fahrenheit"]).optional()
      .describe("温度单位，默认摄氏度"),
  },
  async ({ city, unit }) => {
    // 调用天气 API（这里用模拟数据演示）
    const weather = await fetchWeather(city, unit ?? "celsius");

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(weather, null, 2),
        },
      ],
    };
  }
);

// ========== Tool 2: 天气预报 ==========
server.tool(
  "get_weather_forecast",
  "获取指定城市未来几天的天气预报",
  {
    city: z.string().describe("城市名称"),
    days: z.number().min(1).max(7).optional()
      .describe("预报天数，1-7天，默认3天"),
  },
  async ({ city, days }) => {
    const forecast = await fetchForecast(city, days ?? 3);

    return {
      content: [
        {
          type: "text",
          text: formatForecast(forecast),
        },
      ],
    };
  }
);

// ========== 启动 Server ==========
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  // Server 启动后通过 stdin/stdout 与 Client 通信
  // 不需要 console.log — stdout 被用于协议通信
}

main().catch((error) => {
  // 错误输出到 stderr，避免干扰 MCP 协议
  process.stderr.write(`Fatal error: ${error}\n`);
  process.exit(1);
});

// ========== 模拟数据层（实际项目替换为真实 API）==========
async function fetchWeather(city: string, unit: string) {
  // 实际项目中调用 OpenWeatherMap / 和风天气 等 API
  return {
    city,
    temperature: unit === "fahrenheit" ? 82 : 28,
    unit: unit === "fahrenheit" ? "°F" : "°C",
    condition: "晴朗",
    humidity: 45,
    wind_speed: "12 km/h",
    updated_at: new Date().toISOString(),
  };
}

async function fetchForecast(city: string, days: number) {
  const conditions = ["晴", "多云", "小雨", "阴", "雷阵雨"];
  return Array.from({ length: days }, (_, i) => ({
    date: new Date(Date.now() + i * 86400000)
      .toISOString().split("T")[0],
    high: Math.round(25 + Math.random() * 10),
    low: Math.round(15 + Math.random() * 8),
    condition: conditions[Math.floor(Math.random() * conditions.length)],
  }));
}

function formatForecast(forecast: any[]): string {
  return forecast
    .map((d) => `${d.date}: ${d.condition}, ${d.low}°C ~ ${d.high}°C`)
    .join("\n");
}
```

### 3.2 核心概念解析

**`server.tool()` 四参数模式**：

```typescript
server.tool(
  string,      // 工具名称 — 必须唯一，推荐 snake_case
  string,      // 描述 — LLM 根据描述决定是否调用，务必清晰准确
  ZodSchema,   // 参数 Schema — 用 Zod 定义，自动生成 JSON Schema
  Handler,     // 异步处理函数 — 接收解析后的参数，返回 content 数组
);
```

**Zod Schema 到 JSON Schema 的自动映射**：

```typescript
// Zod 定义                    → JSON Schema（自动生成）
z.string()                     → { type: "string" }
z.string().describe("城市")    → { type: "string", description: "城市" }
z.number().min(1).max(7)       → { type: "number", minimum: 1, maximum: 7 }
z.enum(["a", "b"])             → { enum: ["a", "b"] }
z.string().optional()          → 属性不加入 required 数组
z.array(z.string())            → { type: "array", items: { type: "string" } }
```

**Handler 返回值结构**：

```typescript
// 文本内容
return {
  content: [{ type: "text", text: "结果文本" }]
};

// 图片内容（Tool 特有）
return {
  content: [{
    type: "image",
    data: base64EncodedImage,
    mimeType: "image/png"
  }]
};

// 错误（不抛异常，而是返回 isError 标记）
return {
  content: [{ type: "text", text: "城市不存在: xxx" }],
  isError: true,
};
```

### 3.3 编译与验证

```bash
# 编译 TypeScript
npm run build

# 使用 MCP Inspector 可视化测试
npm run inspect
```

Inspector 启动后会在浏览器中打开，你可以：
1. 查看注册的 Tool 列表
2. 手动填入参数并调用 Tool
3. 查看请求/响应的完整 JSON-RPC 消息

### 3.4 注册到 Claude Code

```bash
# 方式一：使用 claude mcp add 命令（推荐）
claude mcp add weather-server -- node /path/to/mcp-weather-server/dist/index.js

# 方式二：使用 JSON 配置
claude mcp add-json weather-server '{
  "type": "stdio",
  "command": "node",
  "args": ["/path/to/mcp-weather-server/dist/index.js"]
}'

# 验证配置
claude mcp list

# 在 Claude Code 中测试
claude
> 北京今天天气怎么样？
# Claude 会自动识别并调用 get_current_weather 工具
```

**常见问题**：

| 问题 | 原因 | 解决方案 |
|------|------|----------|
| `spawn ENOENT` | 命令路径错误 | 使用绝对路径 |
| Server 启动后无响应 | `console.log` 干扰协议 | 改用 `process.stderr.write` |
| 工具未显示 | 配置文件格式错误 | 检查 JSON 语法 |
| Tool 调用超时 | Handler 卡住 | 确保异步操作有超时控制 |

---

## 四、实战二：Resource 原语 — 提供上下文数据

Resources 向 Client 暴露可读取的数据资源。与 Tool 不同，Resource 由应用/用户主动请求，LLM 不会自主调用。适合提供配置文件、数据字典、模板等上下文信息。

### 4.1 静态 Resource

```typescript
// ========== Resource: 支持的城市列表 ==========
server.resource(
  "supported-cities",              // 资源名称
  "weather://cities/supported",    // URI（MCP 使用自定义 URI Scheme）
  async (uri) => {
    const cities = [
      { name: "北京", code: "BJ", lat: 39.9, lon: 116.4 },
      { name: "上海", code: "SH", lat: 31.2, lon: 121.5 },
      { name: "广州", code: "GZ", lat: 23.1, lon: 113.3 },
      { name: "Shenzhen", code: "SZ", lat: 22.5, lon: 114.1 },
    ];

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(cities, null, 2),
        },
      ],
    };
  }
);
```

### 4.2 动态 Resource Template

Resource Template 使用 URI 模板支持参数化访问：

```typescript
// ========== Resource Template: 城市天气历史 ==========
server.resource(
  "city-weather-history",
  new ResourceTemplate("weather://cities/{city}/history", { list: undefined }),
  async (uri, { city }) => {
    // {city} 从 URI 中自动提取
    const history = await getCityHistory(city);

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(history, null, 2),
        },
      ],
    };
  }
);

// 配合 list 回调，让 Client 能发现所有可用资源
server.resource(
  "city-detail",
  new ResourceTemplate("weather://cities/{city}/detail", {
    list: async () => {
      const cities = ["beijing", "shanghai", "guangzhou", "shenzhen"];
      return {
        resources: cities.map((city) => ({
          name: `${city} 详情`,
          uri: `weather://cities/${city}/detail`,
          mimeType: "application/json",
        })),
      };
    },
  }),
  async (uri, { city }) => {
    const detail = await getCityDetail(city);
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(detail, null, 2),
        },
      ],
    };
  }
);
```

### 4.3 Resource 订阅与通知

当资源数据变化时，Server 可以主动通知 Client：

```typescript
// 声明支持订阅
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
}, {
  capabilities: {
    resources: {
      subscribe: true,  // 启用订阅能力
      listChanged: true, // 启用资源列表变更通知
    },
  },
});

// 在数据变更时发送通知
async function onWeatherUpdate(city: string) {
  // 通知所有订阅了该资源的 Client
  await server.notification({
    method: "notifications/resources/updated",
    params: {
      uri: `weather://cities/${city}/current`,
    },
  });
}

// 资源列表变更时通知
async function onNewCityAdded() {
  await server.notification({
    method: "notifications/resources/list_changed",
  });
}
```

> **注意**：Claude Code 目前对 Resource 订阅的支持有限，建议优先使用 Tool 来提供动态数据。

---

## 五、实战三：Prompt 原语 — 模板化交互

Prompts 是用户控制的模板化交互方式。用户从可用 Prompt 列表中选择一个，填写参数后提交给 LLM。

```typescript
// ========== Prompt: 天气分析报告 ==========
server.prompt(
  "weather-analysis",              // Prompt 名称
  "生成城市天气分析报告，包含趋势预测和建议",  // 描述
  {
    city: z.string().describe("目标城市"),
    period: z.enum(["daily", "weekly", "monthly"])
      .describe("分析周期"),
    focus: z.string().optional()
      .describe("关注重点，如'出行'、'农业'、'能源'"),
  },
  async ({ city, period, focus }) => {
    const weatherData = await fetchWeather(city, "celsius");
    const forecast = await fetchForecast(city, 7);

    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: [
              `请为 ${city} 生成一份${period === "daily" ? "每日" : period === "weekly" ? "每周" : "每月"}天气分析报告。`,
              ``,
              `## 当前天气`,
              `- 温度: ${weatherData.temperature}°C`,
              `- 天气状况: ${weatherData.condition}`,
              `- 湿度: ${weatherData.humidity}%`,
              `- 风速: ${weatherData.wind_speed}`,
              ``,
              `## 未来预报`,
              forecast.map((f: any) => `- ${f.date}: ${f.condition}, ${f.low}°C ~ ${f.high}°C`).join("\n"),
              focus ? `\n## 特别关注\n请重点从"${focus}"角度分析影响。` : "",
              ``,
              `请包含以下内容：`,
              `1. 天气趋势总结`,
              `2. 对日常生活的影响和建议`,
              `3. ${focus ? `${focus}相关的具体建议` : "出行建议"}`,
            ].join("\n"),
          },
        },
      ],
    };
  }
);

// ========== Prompt: 对比分析 ==========
server.prompt(
  "city-comparison",
  "对比两个城市的天气状况",
  {
    city1: z.string().describe("第一个城市"),
    city2: z.string().describe("第二个城市"),
    aspect: z.enum(["temperature", "comfort", "travel"])
      .describe("对比维度"),
  },
  async ({ city1, city2, aspect }) => {
    const [w1, w2] = await Promise.all([
      fetchWeather(city1, "celsius"),
      fetchWeather(city2, "celsius"),
    ]);

    const aspectLabel = {
      temperature: "温度",
      comfort: "舒适度",
      travel: "出行适宜度",
    }[aspect];

    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `对比 ${city1} 和 ${city2} 的${aspectLabel}：\n\n`
              + `${city1}: ${w1.temperature}°C, ${w1.condition}\n`
              + `${city2}: ${w2.temperature}°C, ${w2.condition}\n\n`
              + `请从${aspectLabel}角度进行详细对比分析。`,
          },
        },
      ],
    };
  }
);
```

**Prompt vs Tool 的选择策略**：

```
                    由谁决定调用？
                         │
            ┌────────────┼────────────┐
            ▼            ▼            ▼
         LLM自主      用户选择      应用请求
         Tool         Prompt       Resource
            │            │            │
            ▼            ▼            ▼
     执行操作/计算    模板化交互    读取数据
     查询API         生成报告      获取配置
     修改状态        预设工作流    加载文件
```

---

## 六、进阶：Tool 返回图片与进度通知

### 6.1 返回图片

Tool 可以直接返回图片内容：

```typescript
server.tool(
  "weather_chart",
  "生成城市天气图表",
  {
    city: z.string().describe("城市名称"),
    chart_type: z.enum(["temperature", "humidity", "wind"])
      .describe("图表类型"),
  },
  async ({ city, chart_type }) => {
    // 生成图表（使用 chart.js + canvas 等库）
    const imageBuffer = await generateChart(city, chart_type);

    return {
      content: [
        {
          type: "image",
          data: imageBuffer.toString("base64"),
          mimeType: "image/png",
        },
      ],
    };
  }
);
```

### 6.2 进度通知

长时间操作可以通过进度通知让 Client 感知进度：

```typescript
server.tool(
  "batch_weather_report",
  "批量生成多个城市的天气报告",
  {
    cities: z.array(z.string()).describe("城市列表"),
  },
  async ({ cities }, { signal, meta }) => {
    const results: string[] = [];

    for (let i = 0; i < cities.length; i++) {
      // 检查是否被取消
      signal?.throwIfAborted();

      const report = await generateReport(cities[i]);
      results.push(report);

      // 发送进度通知
      await server.notification({
        method: "notifications/progress",
        params: {
          progress: i + 1,
          total: cities.length,
          progressToken: meta?.progressToken,
        },
      });
    }

    return {
      content: [{ type: "text", text: results.join("\n---\n") }],
    };
  }
);
```

---

## 七、完整项目：综合天气 MCP Server

将以上所有原语组合成一个完整项目。创建 `src/index.ts`：

```typescript
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer(
  { name: "weather-server", version: "1.0.0" },
  {
    capabilities: {
      resources: { subscribe: true, listChanged: true },
    },
  }
);

// ━━━━━━━ Tools ━━━━━━━

server.tool("get_current_weather", "获取指定城市的当前天气信息", {
  city: z.string().describe("城市名称"),
  unit: z.enum(["celsius", "fahrenheit"]).optional().describe("温度单位"),
}, async ({ city, unit }) => {
  const u = unit ?? "celsius";
  const data = await fetchWeather(city, u);
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
});

server.tool("get_weather_forecast", "获取未来天气预报", {
  city: z.string().describe("城市名称"),
  days: z.number().min(1).max(7).optional().describe("预报天数"),
}, async ({ city, days }) => {
  const data = await fetchForecast(city, days ?? 3);
  const text = data
    .map((d: any) => `${d.date}: ${d.condition}, ${d.low}°C ~ ${d.high}°C`)
    .join("\n");
  return { content: [{ type: "text", text }] };
});

server.tool("compare_weather", "对比两个城市的天气", {
  city1: z.string().describe("城市1"),
  city2: z.string().describe("城市2"),
}, async ({ city1, city2 }) => {
  const [w1, w2] = await Promise.all([
    fetchWeather(city1, "celsius"),
    fetchWeather(city2, "celsius"),
  ]);
  const comparison = [
    `| 指标 | ${city1} | ${city2} |`,
    `|------|---------|---------|`,
    `| 温度 | ${w1.temperature}°C | ${w2.temperature}°C |`,
    `| 天气 | ${w1.condition} | ${w2.condition} |`,
    `| 湿度 | ${w1.humidity}% | ${w2.humidity}% |`,
    `| 风速 | ${w1.wind_speed} | ${w2.wind_speed} |`,
  ].join("\n");
  return { content: [{ type: "text", text: comparison }] };
});

// ━━━━━━━ Resources ━━━━━━━

server.resource("supported-cities", "weather://config/cities",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: "application/json",
      text: JSON.stringify([
        { name: "北京", code: "BJ" },
        { name: "上海", code: "SH" },
        { name: "广州", code: "GZ" },
      ], null, 2),
    }],
  })
);

server.resource("city-weather",
  new ResourceTemplate("weather://cities/{city}/current", {
    list: async () => ({
      resources: ["beijing", "shanghai", "guangzhou"].map((c) => ({
        name: `${c} 实时天气`,
        uri: `weather://cities/${c}/current`,
      })),
    }),
  }),
  async (uri, { city }) => {
    const data = await fetchWeather(city, "celsius");
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(data, null, 2),
      }],
    };
  }
);

// ━━━━━━━ Prompts ━━━━━━━

server.prompt("weather-analysis", "生成城市天气分析报告", {
  city: z.string().describe("目标城市"),
  period: z.enum(["daily", "weekly", "monthly"]).describe("分析周期"),
}, async ({ city, period }) => {
  const weather = await fetchWeather(city, "celsius");
  return {
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `请为${city}生成${period === "daily" ? "每日" : period === "weekly" ? "每周" : "每月"}天气分析。\n`
          + `当前: ${weather.temperature}°C, ${weather.condition}, 湿度${weather.humidity}%`,
      },
    }],
  };
});

// ━━━━━━━ 启动 ━━━━━━━

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((err) => {
  process.stderr.write(`Fatal: ${err}\n`);
  process.exit(1);
});

// ━━━━━━━ 数据层 ━━━━━━━

async function fetchWeather(city: string, unit: string) {
  return {
    city,
    temperature: unit === "fahrenheit" ? 82 : 28,
    unit: unit === "fahrenheit" ? "°F" : "°C",
    condition: "晴朗",
    humidity: 45,
    wind_speed: "12 km/h",
    updated_at: new Date().toISOString(),
  };
}

async function fetchForecast(city: string, days: number) {
  const conditions = ["晴", "多云", "小雨", "阴"];
  return Array.from({ length: days }, (_, i) => ({
    date: new Date(Date.now() + i * 86400000).toISOString().split("T")[0],
    high: Math.round(25 + Math.random() * 10),
    low: Math.round(15 + Math.random() * 8),
    condition: conditions[Math.floor(Math.random() * conditions.length)],
  }));
}
```

### 完整工作流程

```mermaid
flowchart LR
    subgraph 开发阶段
        A["npm init + 安装依赖"] --> B["编写 MCP Server 代码"]
        B --> C["tsc 编译"]
        C --> D["MCP Inspector 调试"]
    end

    subgraph 集成阶段
        D --> E["claude mcp add"]
        E --> F["Claude Code 中测试"]
        F --> G{"功能正确?"}
        G -->|否| B
        G -->|是| H["项目级 .mcp.json"]
    end

    subgraph 发布阶段
        H --> I["npm publish"]
        I --> J["MCP Registry 注册"]
        J --> K["社区使用"]
    end
```

---

## 八、项目级配置与团队共享

### 8.1 项目级 `.mcp.json`

在项目根目录创建 `.mcp.json`，团队成员自动共享 MCP 配置：

```json
{
  "mcpServers": {
    "weather-server": {
      "type": "stdio",
      "command": "node",
      "args": ["./mcp-servers/weather/dist/index.js"]
    }
  }
}
```

### 8.2 用户级配置 `~/.claude.json`

适用于个人全局使用的 MCP Server：

```bash
# 全局配置（所有项目可用）
claude mcp add --scope user weather-server -- node ~/mcp-servers/weather/dist/index.js
```

### 8.3 配置作用域优先级

```
项目级 .mcp.json > 用户级 ~/.claude.json > 系统级配置
     (高优先)                           (低优先)
```

---

## 九、MCP Inspector 调试实战

MCP Inspector 是官方可视化调试工具，支持测试所有三种原语。

### 9.1 启动 Inspector

```bash
# 方式一：通过 npm scripts（已在 package.json 中配置）
npm run inspect

# 方式二：直接使用 npx
npx @modelcontextprotocol/inspector node dist/index.js

# 方式三：带环境变量
npx @modelcontextprotocol/inspector \
  -e API_KEY=your_key \
  node dist/index.js
```

### 9.2 Inspector 使用流程

1. **连接**：Inspector 自动启动 Server 并建立 stdio 连接
2. **Tools 标签页**：列出所有注册的 Tool，点击后可填入参数并调用
3. **Resources 标签页**：列出所有静态和动态 Resource，支持浏览 URI
4. **Prompts 标签页**：列出所有 Prompt 模板，可填入参数并预览生成结果
5. **History 面板**：查看完整的 JSON-RPC 请求/响应日志

### 9.3 导出配置

Inspector 提供一键导出功能，生成 Claude Code / Claude Desktop 兼容的配置：

```json
{
  "mcpServers": {
    "weather-server": {
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"]
    }
  }
}
```

---

## 十、发布到 npm 与 MCP Registry

### 10.1 准备发布

```bash
# 确保入口文件有 shebang
head -1 dist/index.js
# 应输出: #!/usr/bin/env node

# 确保构建产物是最新的
npm run build

# 检查 package.json 字段
npm pack --dry-run
```

### 10.2 发布到 npm

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

# 发布
npm publish --access public

# 发布后，用户可以这样使用：
# claude mcp add weather-server -- npx mcp-weather-server
```

### 10.3 注册到 MCP Registry

MCP Registry 是官方的 Server 发现平台，只存储元数据（不存代码）：

```bash
# 安装 MCP CLI
npm install -g @modelcontextprotocol/cli

# 发布到 Registry
mcp publish
```

在项目根目录创建 `mcp-metadata.json`：

```json
{
  "name": "weather-server",
  "description": "天气查询 MCP Server，支持全球城市天气、预报和对比",
  "repository": "https://github.com/yourname/mcp-weather-server",
  "npmPackage": "mcp-weather-server",
  "categories": ["weather", "utilities"],
  "tools": [
    {
      "name": "get_current_weather",
      "description": "获取当前天气"
    },
    {
      "name": "get_weather_forecast",
      "description": "获取天气预报"
    },
    {
      "name": "compare_weather",
      "description": "对比城市天气"
    }
  ]
}
```

---

## 十一、安全最佳实践

### 11.1 输入验证

```typescript
// 使用 Zod 进行严格验证
server.tool("search_weather", "搜索天气记录", {
  query: z.string().min(1).max(200)
    .regex(/^[\w\s\u4e00-\u9fff,.-]+$/)  // 只允许安全字符
    .describe("搜索关键词"),
}, async ({ query }) => {
  // ...
});
```

### 11.2 错误处理

```typescript
server.tool("risky_operation", "需要安全处理的操作", {
  param: z.string(),
}, async ({ param }) => {
  try {
    const result = await callExternalAPI(param);
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (error) {
    // 返回错误信息，而非抛出异常
    return {
      content: [{
        type: "text",
        text: `操作失败: ${error instanceof Error ? error.message : "未知错误"}`,
      }],
      isError: true,
    };
  }
});
```

### 11.3 不要在 Tool 描述中暴露内部实现

```typescript
// 差：暴露了 API 端点
server.tool("get_user", "调用 /api/v2/users/{id} 获取用户", ...);

// 好：描述功能而非实现
server.tool("get_user", "获取指定用户的基本信息", ...);
```

---

## 十二、常见问题排查清单

| 症状 | 检查项 | 解决方法 |
|------|--------|----------|
| Server 启动失败 | Node.js 版本 >= 18 | `nvm use 20` |
| `Cannot find module` | ESM 配置 | 确认 `"type": "module"` 和 `.js` 后缀 |
| Tool 参数解析失败 | Zod Schema | 在 Inspector 中验证 Schema |
| stdio 被污染 | console.log | 替换为 `process.stderr.write` |
| Claude Code 看不到工具 | 配置路径 | 使用绝对路径，检查 `claude mcp list` |
| 工具调用返回空 | Handler 逻辑 | 在 Inspector 中单独测试 |
| 类型错误 | TypeScript 配置 | 确认 `moduleResolution: "Node16"` |

```mermaid
flowchart TD
    START["Server 无法工作"] --> Q1{"能启动吗?"}
    Q1 -->|否| Q2{"Node版本?"}
    Q2 -->|< 18| A1["升级 Node.js"]
    Q2 -->|>= 18| Q3{"模块错误?"}
    Q3 -->|是| A2["检查 type:module 和 .js 后缀"]
    Q3 -->|否| A3["检查 import 路径"]

    Q1 -->|能启动| Q4{"Inspector 能调通?"}
    Q4 -->|否| Q5{"Schema 正确?"}
    Q5 -->|否| A4["用 Zod 验证参数"]
    Q5 -->|是| A5["检查 Handler 逻辑"]

    Q4 -->|能| Q6{"Claude Code 可见?"}
    Q6 -->|否| A6["检查 claude mcp list"]
    Q6 -->|是| A7["✅ 正常工作"]
```

---

## 延伸阅读

- [MCP TypeScript SDK 官方文档](https://ts.sdk.modelcontextprotocol.io/) — 完整 API 参考
- [MCP 规范 - Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) — Tools 原语规范定义
- [MCP 传输机制规范](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) — stdio 和 Streamable HTTP 详解
- [Claude Code MCP 配置文档](https://code.claude.com/docs/en/mcp) — Claude Code 集成指南
- [MCP Inspector 仓库](https://github.com/modelcontextprotocol/inspector) — 调试工具源码

## 参考来源

1. [MCP TypeScript SDK - GitHub](https://github.com/modelcontextprotocol/typescript-sdk) — 官方 TypeScript SDK 源码与 README
2. [Build an MCP Server with TypeScript: 2026 Tutorial - dev.to](https://dev.to/jangwook_kim_e31e7291ad98/build-an-mcp-server-with-typescript-2026-tutorial-1ipk) — 2026 年 TypeScript MCP 开发教程
3. [MCP Server TypeScript - MCPize](https://mcpize.com/blog/mcp-server-typescript) — McpServer 类完整使用指南
4. [MCP Developer Guide 2026 - LushBinary](https://lushbinary.com/blog/mcp-model-context-protocol-developer-guide-2026/) — 架构、开发与安全综合指南
5. [MCP Server Packaging & Distribution - ChatForest](https://chatforest.com/guides/mcp-server-packaging-distribution/) — npm 发布与 MCP Registry 注册流程
6. [Connect Claude Code to tools via MCP - Claude Code Docs](https://code.claude.com/docs/en/mcp) — Claude Code MCP 集成官方文档

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| **适合训练营** | AI 工具开发营、MCP 生态开发营、Claude Code 进阶营 |
| **教学时长** | 4-6 小时（含动手实验） |
| **难度等级** | 中级（需要 TypeScript 和 Node.js 基础） |
| **实验环境** | Node.js 20+、Claude Code CLI、MCP Inspector |
| **前置课程** | Claude Code 入门、TypeScript 基础 |
| **后续课程** | MCP 质量工程、MCP Gateway 网关、多 Server 编排 |

> [!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)
