<!-- title: 用TS SDK造MCP：调试到接入闭环 -->

## 概述

本教程面向想亲手构建 MCP（Model Context Protocol）工具并接入 Claude Code 的开发者。我们将使用官方 **TypeScript SDK**（`@modelcontextprotocol/sdk`）从零搭建一个"开发者瑞士军刀"MCP 服务器，借助 **MCP Inspector** 进行可视化调试，最后通过 `claude mcp add` 与项目级 `.mcp.json` 把它接入 Claude Code，跑通"开发→调试→接入"的完整闭环。全文配套一个离线可运行的多工具项目，无需任何 API Key。

---

## 一、为什么用 TypeScript SDK

MCP 生态里已有 Go、Python、Rust、Java 等多套官方/社区 SDK，但 **TypeScript SDK** 始终是受众最广、迭代最快、示例最全的一套，原因有三：

1. **与 Claude Code 同源**：Claude Code 本身是 Node/TS 生态，stdio 子进程通信最顺畅，跨平台路径处理与 Node 内置模块（`crypto`、`fs`、`child_process`）开箱即用。
2. **高层 `McpServer` API 极简**：注册一个工具只需 `server.tool(name, desc, schema, handler)` 四个参数，配合 **Zod** 声明入参，自动生成 JSON Schema 给客户端。
3. **Inspector 一键调试**：官方提供的 `@modelcontextprotocol/inspector` 是 Web UI，能实时查看工具列表、JSON-RPC 报文与调用结果，把"黑盒子进程"变成可视化面板。

> 与已有的 Go SDK、FastMCP（Python）教程不同，本篇聚焦 **TS SDK 的开发循环 + Inspector 调试 + Claude Code 接入**，这条链路是前序教程尚未覆盖的。

---

## 二、前置条件

| 项目 | 要求 | 说明 |
|------|------|------|
| Node.js | ≥ 20 LTS | TS SDK 与 Inspector 都依赖较新 Node；建议 20.x 或 22.x |
| 包管理器 | npm / pnpm / bun 任一 | 示例统一用 npm |
| TypeScript | 5.4+ | 通过 `typescript` devDependency 安装 |
| Claude Code | 已安装并登录 | 用于最后的接入环节 |
| 操作系统 | macOS / Linux / Windows | 跨平台；示例命令以 Linux/macOS 为准 |

环境自检命令：

```bash
node -v        # 应输出 v20.x 或更高
npm -v
claude --version
```

如尚未安装 Claude Code，可执行 `npm install -g @anthropic-ai/claude-code`（或按官方推荐的原生安装方式）。

---

## 三、第 1 步：脚手架与依赖

新建项目目录并初始化：

```bash
mkdir mcp-devtools && cd mcp-devtools
npm init -y
```

安装核心依赖：

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

> `tsx` 让我们跳过编译直接运行 `.ts`，开发期迭代极快；正式接入时再用 `tsc` 编译成 JS。

初始化 TS 配置：

```bash
npx tsc --init
```

编辑 `tsconfig.json`，关键字段如下（ESM 模块，NodeNext 解析）：

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

在 `package.json` 中补上 `"type": "module"` 与脚本：

```json
{
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node build/index.js",
    "inspect": "npx @modelcontextprotocol/inspector node build/index.js"
  }
}
```

**预期结果**：目录中出现 `tsconfig.json`，`node_modules` 内可见 `@modelcontextprotocol/sdk`。

**常见问题**：

- *报错 `ERR_UNKNOWN_FILE_EXTENSION .ts`*：忘了在 `package.json` 写 `"type": "module"`，或 `moduleResolution` 没设成 `NodeNext`。
- *导入路径报错*：TS SDK 在包内使用 `.js` 后缀导出，例如 `@modelcontextprotocol/sdk/server/mcp.js`——这是 ESM 规范要求的，**不要**改成 `.ts`。

---

## 四、第 2 步：用高层 API 注册第一个工具

新建 `src/index.ts`。先写一个最小的 stdio 服务器，注册一个 `uuid-generate` 工具：

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

// 1. 创建服务器实例，声明名称与版本（客户端会在握手时读到）
const server = new McpServer({
  name: "mcp-devtools",
  version: "0.1.0",
});

// 2. 注册工具：名称 / 描述 / Zod 入参 schema / 处理函数
server.tool(
  "uuid-generate",
  "生成一个或多个 UUID v4，默认返回 1 个",
  {
    count: z.number().int().min(1).max(100).default(1).describe("要生成的数量"),
  },
  async ({ count }) => {
    const uuids = Array.from({ length: count }, () => randomUUID());
    return {
      content: [
        { type: "text", text: uuids.join("\n") },
      ],
    };
  }
);

// 3. 绑定 stdio 传输层并启动
const transport = new StdioServerTransport();
await server.connect(transport);
```

启动开发版：

```bash
npm run dev
```

**预期结果**：进程不会退出，静默等待 stdio 上的 JSON-RPC 报文。这是正常的——stdio MCP 服务器就是一个"哑终端"，等待客户端发指令。

**关键点解析**：

- `server.tool(...)` 的第三个参数是 **Zod schema 对象**，SDK 会自动把它转成 JSON Schema 暴露给客户端（Claude Code 由此知道该工具接受哪些参数）。
- 返回值必须包含 `content` 数组，每个元素带 `type`。2025-06-18 规范下文本用 `"text"`，图片用 `"image"`，并支持 `"resource"`（按 URI 暴露只读数据，本篇略）。
- **不要**用 `console.log` 打印调试信息！stdio 通道上的任何非 JSON-RPC 输出都会破坏协议。调试日志请走 `console.error`（stderr 不污染通道）。

**常见问题**：

- *工具没被 Claude Code 发现*：八成是 `console.log` 污染了 stdout。把日志全部改到 `console.error`。
- *Zod 的 `.default()` 让参数变成可选*：客户端可不传，SDK 会填默认值。

---

## 五、第 3 步：补全多工具（哈希与编解码）

把"瑞士军刀"补成三件套：`uuid-generate`、`hash-text`、`encode-decode`。在 `server.tool(...)` 注册块之后继续追加（保持同一个 `server` 实例）：

```typescript
import { createHash } from "node:crypto";

// 工具 2：哈希
server.tool(
  "hash-text",
  "计算文本的哈希摘要，支持 sha256 / sha512 / md5",
  {
    text: z.string().min(1).describe("待哈希的文本"),
    algorithm: z
      .enum(["sha256", "sha512", "md5"])
      .default("sha256")
      .describe("哈希算法"),
  },
  async ({ text, algorithm }) => {
    const digest = createHash(algorithm).update(text).digest("hex");
    return {
      content: [{ type: "text", text: `${algorithm}: ${digest}` }],
    };
  }
);

// 工具 3：编解码
server.tool(
  "encode-decode",
  "Base64 / URL 的编码与解码",
  {
    mode: z
      .enum(["base64-encode", "base64-decode", "url-encode", "url-decode"])
      .describe("操作模式"),
    input: z.string().describe("输入字符串"),
  },
  async ({ mode, input }) => {
    let output: string;
    try {
      switch (mode) {
        case "base64-encode":
          output = Buffer.from(input, "utf8").toString("base64");
          break;
        case "base64-decode":
          output = Buffer.from(input, "base64").toString("utf8");
          break;
        case "url-encode":
          output = encodeURIComponent(input);
          break;
        case "url-decode":
          output = decodeURIComponent(input);
          break;
      }
      return { content: [{ type: "text", text: output }] };
    } catch (err) {
      // 出错时返回 isError，客户端会把它呈现给模型而非崩溃
      return {
        isError: true,
        content: [
          { type: "text", text: `解码失败：输入不是合法的 ${mode} 内容` },
        ],
      };
    }
  }
);
```

**预期结果**：服务器现在暴露 3 个工具。注意 `hash-text` 用了 `z.enum(...)`——SDK 会把它编译成 JSON Schema 的 `enum`，模型只能从限定值里选，避免幻觉出 `sha1024` 这种不存在的东西。

**设计要点**：

- **错误也要返回结构化内容**：MCP 工具不要抛异常给客户端，而是返回 `isError: true` + 文本说明。这样模型能看到错误原因并自行纠正（例如换个模式重试）。
- **入参用 enum 优于自由字符串**：限定取值既约束模型，也让 Inspector 的下拉框更友好。

---

## 六、第 4 步：用 MCP Inspector 可视化调试

这是 TS SDK 开发循环里最省时的一环。先编译一次：

```bash
npm run build      # 产出 build/index.js
```

启动 Inspector（它会在本地起一个 Web 面板，并自动拉起你的服务器作为子进程）：

```bash
npm run inspect
# 等价于：npx @modelcontextprotocol/inspector node build/index.js
```

终端会打印一行类似 `MCP Inspector is up and running at http://localhost:6274/` 的地址，浏览器打开它。

在面板里完成以下验证：

1. **连接**：左侧 *Server Status* 应显示 `Connected`。若失败，检查 *Server Command* 是否是 `node`、*Args* 是否是 `build/index.js`。
2. **Tools 标签页**：应看到 `encode-decode`、`hash-text`、`uuid-generate` 三个工具，每个工具下方有 SDK 根据 Zod 自动生成的参数表单。
3. **单工具调用**：点开 `hash-text`，填 `text = hello`、`algorithm = sha256`，点 *Run Tool*，右侧应返回 `sha256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824`。
4. **Notifications / 原始报文**：切换到 *Notifications* 或 *Protocol Log* 标签，能看到完整的 JSON-RPC `tools/list` 与 `tools/call` 请求/响应，是排查 schema 问题的金标准。

**为什么这一步值钱**：没有 Inspector 时，你只能靠 Claude Code 反馈"工具没出现"，但分不清是 schema 错、stdout 污染、还是握手失败。Inspector 把整条链路摊开在眼前。

**常见问题**：

- *Inspector 里工具数为 0*：说明 `tools/list` 返回了空——多半是 `server.tool()` 的 Zod 对象写得不对，或返回值缺 `content`。
- *面板显示 `Disconnected` 后立刻退出*：服务器启动即崩溃。直接 `node build/index.js` 单独跑一次，看 stderr 报错。

---

## 七、第 5 步：接入 Claude Code

调试通过后，把服务器正式挂进 Claude Code。Claude Code 支持 **三种作用域**：

| 作用域 | 标志 | 配置位置 | 适用场景 |
|--------|------|----------|----------|
| `local`（默认） | `-s local` | `~/.claude.json`（项目私有） | 只在当前项目、当前机器用 |
| `project` | `-s project` | 项目根 `.mcp.json`（可提交 Git） | 团队共享，新人 clone 即用 |
| `user` | `-s user` | `~/.claude.json`（全局） | 所有项目都可用 |

### 方式 A：命令行添加（local 作用域）

```bash
# 先 build，保证 node 能跑的是编译产物
npm run build

# 注册（注意 -- 之后才是真正的启动命令）
claude mcp add devtools -s local -- node /绝对路径/mcp-devtools/build/index.js
```

验证：

```bash
claude mcp list          # 应列出 devtools
claude mcp get devtools  # 查看详细配置与连接状态
```

进入任意项目目录，启动 `claude`，输入：

```
帮我生成 5 个 UUID，再把 "hello world" 做 base64 编码
```

模型会自主调用 `uuid-generate(count:5)` 与 `encode-decode(mode:base64-encode,...)`，并在回复里贴出结果。

### 方式 B：项目级 `.mcp.json`（团队共享，推荐）

在项目根放一份 `.mcp.json`，团队成员 clone 后 Claude Code 会**自动提示**是否信任并加载：

```json
{
  "mcpServers": {
    "devtools": {
      "command": "node",
      "args": ["/绝对路径/mcp-devtools/build/index.js"]
    }
  }
}
```

> 也可写成更可移植的 `npx` 形式：`"command": "npx"`，`"args": ["tsx", "/path/src/index.ts"]`，免去编译步骤，但每次启动稍慢。

**预期结果**：`claude` 启动时顶部状态栏出现 `devtools` 标记；`/mcp` 命令可查看连接详情与工具数。

**常见问题**：

- *`claude mcp list` 显示 `failed to connect`*：90% 是路径写成了相对路径，或 `node build/index.js` 的工作目录不对。改用绝对路径最稳。
- *`.mcp.json` 没被加载*：Claude Code 出于安全要求**手动确认信任**。首次启动会弹窗，选 *Trust*；之后可在 `/mcp` 里管理信任状态。
- *改了代码但行为没变*：你改的是 `src/*.ts`，但 Claude Code 跑的是 `build/*.js`——记得重新 `npm run build`。

---

## 八、实战案例：完整工作流串讲

把上述步骤串成一条可复现的流水线。假设你刚加了一个 `hash-text` 的 `blake2b` 选项想验证：

```bash
# 1) 改代码（src/index.ts 里给 enum 加 "blake2b"，并用 createHash 支持）
# 2) 编译
npm run build
# 3) Inspector 里连上，调一次 hash-text(algorithm=blake2b) 看输出
npm run inspect
# 4) 满意后退出 Inspector，Claude Code 已经指向 build/index.js，无需重注册
claude
> 用 blake2b 算一下 "claude code" 的哈希
```

注意第 4 步：**MCP 服务器进程在每次会话启动时由 Claude Code 拉起**，因此只要 `build/index.js` 更新了，新会话自然用上新代码，无需重启 Claude Code 本身、也无需重新 `claude mcp add`。

### 端到端架构与数据流

```mermaid
flowchart LR
    U([用户提问]) --> CC[Claude Code 宿主]
    CC -->|tools/call JSON-RPC| SP[stdio 子进程<br/>build/index.js]
    SP --> T1[uuid-generate]
    SP --> T2[hash-text]
    SP --> T3[encode-decode]
    T2 -->|crypto.createHash| N((Node 内置模块))
    T1 --> N
    T3 -->|Buffer/encodeURIComponent| N
    SP -->|tools/result| CC
    CC --> U

    subgraph 开发调试
      IN[MCP Inspector Web UI] -.拉起并观察.-> SP
      IN -.JSON-RPC 报文.-> SP
    end
```

### 协议握手时序

理解握手有助于排查"工具不出现"类问题：

```mermaid
sequenceDiagram
    participant CC as Claude Code（客户端）
    participant S as 你的 MCP 服务器（stdio）
    CC->>S: initialize {name, version, capabilities}
    S-->>CC: InitializeResult {serverInfo, capabilities}
    CC->>S: initialized（通知）
    CC->>S: tools/list
    S-->>CC: [uuid-generate, hash-text, encode-decode]
    CC->>S: tools/call {name: "hash-text", arguments:{text,algorithm}}
    S-->>CC: content:[{type:text, text:"sha256:..."}]
```

握手失败、或 `tools/list` 返回异常，都会导致工具在 Claude Code 中"隐身"——对照时序图，定位是哪一跳出问题。

---

## 九、进阶：从 stdio 升级到 Streamable HTTP

stdio 适合本地工具；若要把服务器部署到远端（云函数、容器），需用 **Streamable HTTP** 传输（2025-06-18 规范中取代了旧的纯 SSE 传输）。最小改法：

```typescript
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "node:http";

const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });

// 复用同一个 server 实例
await server.connect(httpTransport);

const httpServer = createServer(async (req, res) => {
  // 单一端点 /mcp 同时承载 POST（请求）与 GET（可选的服务器推送）
  if (req.url === "/mcp") {
    await httpTransport.handleRequest(req, res);
  } else {
    res.writeHead(404).end();
  }
});

httpServer.listen(3000, () => console.error("MCP HTTP on :3000/mcp"));
```

接入 Claude Code 时用 `--transport http`：

```bash
claude mcp add --transport http devtools http://localhost:3000/mcp
```

> 远程 HTTP 服务器若涉及公网暴露，应叠加 OAuth 2.1 鉴权（属另一专题，本篇不展开）。

---

## 十、检查清单与排错速查

完成本教程后，对照下表自检：

- [ ] `node -v` ≥ 20，`npm run build` 无报错
- [ ] `src/index.ts` 中所有日志走 `console.error`，stdout 干净
- [ ] Inspector 里 3 个工具可见且可调用
- [ ] `claude mcp list` 显示 `devtools ✔ connected`
- [ ] 在 `claude` 会话里能成功触发多工具组合调用
- [ ] （可选）`.mcp.json` 已提交，团队成员可一键加载

| 症状 | 可能原因 | 处置 |
|------|----------|------|
| 工具不出现 | stdout 被污染 / schema 非法 | 改 `console.error`；查 Inspector 报文 |
| `failed to connect` | 路径相对 / 工作目录错 | 用绝对路径；单独 `node build/index.js` 试跑 |
| 模型乱传参数 | enum 写成了自由 string | 用 `z.enum` 收窄取值 |
| 改代码无变化 | 没重新 build / 跑的是旧产物 | `npm run build` 后开新会话 |
| 解码类工具崩 | 抛了未捕获异常 | 改用 `isError: true` 返回结构化错误 |

---

## 延伸阅读

- **MCP 官方规范（2025-06-18）**：定义了 Streamable HTTP、工具/资源/提示三大原语、生命周期与错误码，是协议层的权威依据。
- **TypeScript SDK 仓库 `examples/` 目录**：包含 `streamableHttp`、`stdio`、`scheduling` 等多套可直接运行的示例，进阶实现的首选参考。
- **MCP Inspector 文档**：覆盖断点、环境变量注入、传输切换等高级调试技巧。
- **Zod 文档**：深入 schema 组合（`z.object`、`z.union`、`z.discriminatedUnion`），让复杂工具入参既灵活又安全。

---

## 训练营适用性评估

- **适用类型**：MCP 工具开发营、AI 工程化实战营、Claude Code 进阶营、全栈 AI 应用训练营。
- **学员基础**：具备 JavaScript/TypeScript 基本语法与 Node.js 模块概念即可，不要求先验 MCP 知识。
- **教学时长**：
  - 精简版（搭建 + 接入）：**2 课时**（约 90 分钟）。
  - 完整版（含 Inspector 调试 + Streamable HTTP 升级 + 排错演练）：**4 课时**（约 180 分钟），建议留 1 课时做"自选工具"开放练习。
- **延伸作业**：让学员把某个内部 REST API 或常用 CLI 包装成 MCP 工具，并写一份 `.mcp.json` 与 README，完成从"会用"到"会造"的闭环。

---

## 参考来源

1. [modelcontextprotocol/typescript-sdk（官方 TypeScript SDK）](https://github.com/modelcontextprotocol/typescript-sdk) — 官方 SDK 源码、`McpServer` 高层 API 与各传输层实现。
2. [MCP Specification 2025-06-18 — Transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) — stdio 与 Streamable HTTP 两种标准传输的协议定义。
3. [How to build MCP servers with TypeScript SDK — DEV Community](https://dev.to/shadid12/how-to-build-mcp-servers-with-typescript-sdk-1c28) — 从零搭建 TS MCP 服务器的实战教程，含工具注册示例。
4. [Tool Registration and Execution — DeepWiki](https://deepwiki.com/modelcontextprotocol/typescript-sdk/3.2-tool-registration-and-execution) — 对 SDK 工具注册机制、Zod→JSON Schema 编译与执行流程的深入解析。
5. [@modelcontextprotocol/sdk — npm](https://www.npmjs.com/package/@modelcontextprotocol/sdk) — 包安装、版本与发布说明。

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