TL;DR: 使用 Node.js + 官方 TypeScript SDK,你可以在 5 分钟内构建一个功能完整的 MCP Server,并在 Claude Desktop 中直接调用它。本文提供从项目初始化到工具注册、本地调试、客户端对接的完整流程,附带每一步的可运行代码。

核心要点

  • 5 分钟上手:从 npm init 到 Claude Desktop 成功调用,整个流程不超过 5 分钟
  • 零协议知识要求:官方 SDK 封装了 JSON-RPC 2.0 通信细节,你只需关注业务逻辑
  • stdio 传输:本教程使用最简单的 stdio 模式,无需网络配置,Claude Desktop 原生支持
  • 完整可运行:每段代码都经过验证,复制即用,不是伪代码
  • 调试友好:包含 MCP Inspector 工具的使用方法和常见报错排查方案

如果你还不了解 MCP 协议的整体架构,建议先阅读 MCP 协议深度解析

前置准备

开始之前,确保你的开发环境满足以下要求:

依赖 最低版本 检查命令
Node.js 18.0+ node --version
npm 9.0+ npm --version
TypeScript 5.0+ 全局安装或项目内安装均可

本教程的所有代码基于 @modelcontextprotocol/sdk 官方 TypeScript SDK 最新版本。确认 Node.js 版本后,我们直接开始。

第一步:初始化项目

创建一个新的 Node.js 项目并安装 MCP SDK:

bash
mkdir my-first-mcp-server
cd my-first-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

这里安装了三个关键依赖:

  • @modelcontextprotocol/sdk:MCP 官方 SDK,处理所有协议层通信
  • zod:运行时 Schema 验证库,用于定义 Tool 的输入参数类型
  • typescript:编译器,SDK 的类型推断需要它

初始化 TypeScript 配置:

bash
npx tsc --init

修改生成的 tsconfig.json,确保以下关键配置:

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

同时更新 package.json,添加 type 字段和构建脚本:

json
{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

项目骨架搭建完成。整个过程不到 1 分钟。

第二步:编写 MCP Server 核心代码

src/index.ts 中编写完整的 Server 代码。我们从最小可用版本开始——注册一个 Tool,让 AI Agent 能够调用它。

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

// 1. 创建 Server 实例
const server = new McpServer({
  name: "my-first-mcp-server",
  version: "1.0.0",
});

// 2. 注册一个 Tool
server.tool(
  "get-weather",
  "获取指定城市的当前天气信息",
  {
    city: z.string().describe("城市名称,例如 Beijing、Tokyo、New York"),
  },
  async ({ city }) => {
    // 这里用模拟数据演示,实际项目中替换为真实 API 调用
    const weatherData: Record<string, { temp: number; condition: string }> = {
      Beijing: { temp: 22, condition: "晴" },
      Tokyo: { temp: 18, condition: "多云" },
      "New York": { temp: 15, condition: "小雨" },
    };

    const weather = weatherData[city];

    if (!weather) {
      return {
        content: [
          {
            type: "text" as const,
            text: `未找到城市 "${city}" 的天气数据。支持的城市:${Object.keys(weatherData).join("、")}`,
          },
        ],
      };
    }

    return {
      content: [
        {
          type: "text" as const,
          text: `${city} 当前天气:${weather.condition},温度 ${weather.temp}°C`,
        },
      ],
    };
  }
);

// 3. 启动 Server
const transport = new StdioServerTransport();
await server.connect(transport);

这段代码做了三件事:

  1. 创建 McpServer 实例:指定 Server 名称和版本号,这些信息会在客户端的 initialize 握手中使用
  2. 注册 Tool:通过 server.tool() 声明一个名为 get-weather 的工具。第二个参数是自然语言描述——LLM 会根据这段描述决定何时调用该工具。第三个参数用 zod 定义参数的 JSON Schema,SDK 会自动将其转换为 MCP 协议要求的 inputSchema 格式
  3. 连接传输层:使用 StdioServerTransport 通过标准输入/输出与客户端通信

关于 Tool 描述的编写技巧

Tool 的 description 字段直接影响 LLMTool Use 决策。好的描述应该:

  • 明确说明工具的功能("获取天气"而不是"天气工具")
  • 包含关键约束("需要传入城市名称")
  • 避免歧义(如果有多个类似工具,描述中应体现差异)

这与 Function Calling 中函数描述的编写原则一致——Prompt Engineering 的基本功在这里同样适用。

第三步:编译并本地测试

编译 TypeScript 代码:

bash
npx tsc

编译成功后,dist/index.js 就是可运行的 Server。先在终端手动验证它能正常启动:

bash
node dist/index.js

如果没有任何输出且进程没有退出,说明 Server 已在等待 stdin 输入——这是 stdio 模式的正常行为。按 Ctrl+C 退出。

使用 MCP Inspector 调试

手动构造 JSON-RPC 消息来测试 Server 既繁琐又容易出错。MCP 官方提供了一个可视化调试工具——MCP Inspector:

bash
npx @modelcontextprotocol/inspector node dist/index.js

Inspector 启动后会在浏览器中打开一个调试界面,你可以:

  • 查看 Server 注册的所有 Tools、Resources 和 Prompts
  • 手动输入参数调用 Tool 并查看返回结果
  • 查看完整的 JSON-RPC 消息收发日志

在 Inspector 的 Tools 面板中,你应该能看到 get-weather 工具。填入 city: "Beijing" 点击调用,预期返回:

json
{
  "content": [
    {
      "type": "text",
      "text": "Beijing 当前天气:晴,温度 22°C"
    }
  ]
}

调试确认无误后,进入下一步。

第四步:对接 Claude Desktop

Claude Desktop 是目前最成熟的 MCP 客户端之一。配置方法如下:

1. 找到配置文件

macOS 的配置文件路径:

code
~/Library/Application Support/Claude/claude_desktop_config.json

Windows 的配置文件路径:

code
%APPDATA%\Claude\claude_desktop_config.json

如果文件不存在,手动创建即可。

2. 添加 Server 配置

在配置文件中注册你的 MCP Server:

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

注意两个关键点:

  • commandargs 中的路径必须是绝对路径。相对路径会导致 Claude Desktop 无法找到可执行文件
  • 如果你使用 nvm 管理 Node.js 版本,command 应指向 nvm 环境下的 node 绝对路径(通过 which node 查看)

3. 重启 Claude Desktop

完全退出 Claude Desktop 后重新打开(macOS 上需要从 Dock 栏右键退出,仅关闭窗口不会重新加载配置)。

重启后,在 Claude 的对话输入框旁边应该能看到一个工具图标(锤子形状),点击展开可以看到 get-weather 工具已注册。

4. 测试对话

在 Claude Desktop 中输入:

北京今天天气怎么样?

Claude 会识别出这个问题可以通过 get-weather 工具回答,自动调用你的 MCP Server,并将结果整合到回复中。整个 Tool Use 流程对用户完全透明。

扩展:添加更多 Tools

一个 MCP Server 可以注册任意数量的 Tool。我们继续添加两个实用工具来演示更复杂的场景。

带有多参数的 Tool

typescript
server.tool(
  "calculate-bmi",
  "根据身高和体重计算 BMI 指数",
  {
    height: z.number().describe("身高,单位厘米"),
    weight: z.number().describe("体重,单位千克"),
  },
  async ({ height, weight }) => {
    const heightInMeters = height / 100;
    const bmi = weight / (heightInMeters * heightInMeters);
    const bmiFixed = bmi.toFixed(1);

    let category: string;
    if (bmi < 18.5) category = "偏瘦";
    else if (bmi < 24) category = "正常";
    else if (bmi < 28) category = "偏胖";
    else category = "肥胖";

    return {
      content: [
        {
          type: "text" as const,
          text: `BMI: ${bmiFixed}${category})\n身高: ${height}cm / 体重: ${weight}kg`,
        },
      ],
    };
  }
);

返回结构化数据的 Tool

typescript
server.tool(
  "generate-uuid",
  "生成一个或多个 UUID v4",
  {
    count: z.number().min(1).max(50).default(1).describe("生成数量,1-50"),
  },
  async ({ count }) => {
    const uuids = Array.from({ length: count }, () => crypto.randomUUID());

    return {
      content: [
        {
          type: "text" as const,
          text: uuids.join("\n"),
        },
      ],
    };
  }
);

注意 count 参数使用了 z.number().min(1).max(50).default(1)——zod 的链式校验会自动转换为 JSON Schema 中的 minimummaximumdefault 约束。SDK 在收到客户端请求时会自动执行输入验证,不合法的参数会返回标准的 JSON-RPC 错误响应。

添加 Resources 和 Prompts

除了 Tools,MCP 协议还定义了另外两个核心原语。

Resource:暴露数据给 AI

Resource 让 AI Agent 能够读取你的 Server 中的数据,类似于一个只读的数据接口:

typescript
server.resource(
  "server-info",
  "info://server",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          name: "my-first-mcp-server",
          version: "1.0.0",
          tools: ["get-weather", "calculate-bmi", "generate-uuid"],
          uptime: process.uptime(),
        }),
      },
    ],
  })
);

Prompt:预置对话模板

Prompt 是可复用的对话模板,客户端可以让用户选择并填充参数:

typescript
server.prompt(
  "weather-report",
  { city: z.string() },
  ({ city }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `请查询 ${city} 的天气,并用简洁的方式报告当前温度和天气状况。`,
        },
      },
    ],
  })
);

Tools、Resources、Prompts 这三个原语的协作方式在 MCP 协议进阶实战中有更深入的讲解。

完整项目结构

完成以上步骤后,你的项目结构应如下所示:

code
my-first-mcp-server/
  ├── src/
  │   └── index.ts          # Server 主文件
  ├── dist/
  │   └── index.js           # 编译产物
  ├── package.json
  ├── tsconfig.json
  └── node_modules/

这是一个最小化的结构。随着 Server 复杂度增长,你可以将不同的 Tool 拆分到独立模块中:

code
src/
  ├── index.ts               # Server 入口,注册所有 Tool
  ├── tools/
  │   ├── weather.ts         # 天气相关工具
  │   ├── calculator.ts      # 计算类工具
  │   └── uuid.ts            # UUID 生成
  └── utils/
      └── validators.ts      # 通用校验逻辑

调试与排错

常见报错及解决方案

1. Error: Cannot find module '@modelcontextprotocol/sdk/server/mcp.js'

原因:TypeScript 的 moduleResolution 配置不正确。确保 tsconfig.json 中设置为 "Node16""NodeNext",且 package.json 中有 "type": "module"

2. Claude Desktop 中看不到工具

排查步骤:

  • 检查 claude_desktop_config.json 的 JSON 格式是否合法(多余逗号是常见错误)
  • 确认 args 中的路径是绝对路径
  • 在终端手动运行 node /absolute/path/to/dist/index.js,确认没有报错
  • 完全退出并重启 Claude Desktop

3. Tool 调用返回空结果

检查 Tool handler 函数是否正确返回了 content 数组。MCP 协议要求返回格式必须包含 content 字段,每个 content 项必须有 typetext(或其他支持的 MIME 类型)。

查看日志

Claude Desktop 的 MCP 相关日志路径:

bash
# macOS
tail -f ~/Library/Logs/Claude/mcp*.log

# Windows
# %APPDATA%\Claude\Logs\mcp*.log

日志中可以看到完整的 JSON-RPC 消息交互过程,是排查问题的第一手资料。

生产环境注意事项

本教程构建的是一个学习用的最小 Server。投入生产前,你需要关注以下几点:

错误边界:为每个 Tool handler 添加 try-catch,防止单个工具的异常导致整个 Server 进程崩溃。

输入校验:虽然 zod 提供了基本的类型校验,但对于涉及文件系统、网络请求或数据库操作的 Tool,需要额外的安全校验——防止路径遍历、注入攻击等。

超时控制:长时间运行的 Tool 应设置超时。LLM 客户端通常有请求超时限制,你的 Tool 如果执行时间过长会导致请求失败。

传输层选型:如果你的 Server 需要远程访问或供多个客户端同时使用,应从 stdio 切换到 SSE 传输。关于 SSE 传输层的实现细节,参见 用 Go 从零实现 MCP 协议 SSE 传输层。关于高并发场景下的网关设计,参见 MCP 网关高并发架构设计

性能考量:当 Server 的 Tool 数量增多或调用频率变高时,Node.js 的单线程模型可能成为瓶颈。此时可以考虑 Go 实现——详见 MCP Server 性能对比:Node.js vs Go

下一步学习路线

完成本教程后,你已经掌握了 MCP Server 开发的基础。以下是建议的进阶路线:

  1. 深入协议细节:阅读 MCP 协议进阶实战,了解 Sampling、Roots、协议协商等高级特性
  2. 了解最新规范2025 MCP 规范更新引入了 OAuth 认证和远程 Server 支持,这对生产部署至关重要
  3. 性能优化:通过 Node.js vs Go 性能对比了解不同语言实现的性能特征,为你的场景选择最合适的技术栈
  4. 架构设计:当你需要管理多个 MCP Server 时,MCP 网关高并发架构设计提供了经过验证的网关方案

总结

本文从零开始,用 Node.js 和官方 TypeScript SDK 构建了一个包含天气查询、BMI 计算、UUID 生成三个 Tool 的 MCP Server,并成功对接了 Claude Desktop。整个过程涵盖了 MCP Server 开发的核心流程:

  1. 项目初始化:安装 SDK 和 zod,配置 TypeScript
  2. Tool 注册:使用 server.tool() 声明工具名称、描述、参数 Schema 和处理函数
  3. 本地调试:通过 MCP Inspector 可视化测试
  4. 客户端对接:配置 claude_desktop_config.json 实现 Claude Desktop 集成
  5. 扩展能力:添加 Resources 和 Prompts 丰富 Server 功能

MCP 协议正在成为 AI Agent 连接外部世界的标准方式。掌握 MCP Server 开发,意味着你编写的任何能力都可以被整个 MCP 生态中的客户端复用——这正是 MCP 最大的价值所在。