<!-- title: 用TS官方SDK从零造MCP工具 -->

## 概述

本教程带你用 Anthropic 官方的 TypeScript SDK，从空白目录开始，完整造出一个可在 Claude Code 里直接调用的 MCP 服务器。学完你将掌握：MCP 的「宿主—客户端—服务器」架构与 stdio 传输、用 Zod 定义工具入参契约、`server.tool()` 高层 API、错误回包规范、MCP Inspector 调试闭环，以及 `claude mcp add` 的三种作用域。适合已写过 Node.js/TypeScript、想给 AI 接入「私有数据或私有动作」的后端与全栈工程师，全程零 API Key、可离线跑通。

## 前置条件

- Node.js 20+（LTS）与 npm/pnpm 任一
- 会读 TypeScript 基本语法（泛型、联合类型即可，不需要高级）
- 已装好 Claude Code CLI，并能在终端跑 `claude --version`
- 一颗想搞清楚「MCP 到底替我解决了什么」的耐心

---

## 一、先想清楚：MCP 到底解决了什么

很多人对 MCP 的第一印象是「又一个插件系统」。这个理解只对了一半。真正的痛点是：**每个 AI 客户端都在重复造同一套集成**——接 GitHub 要写一遍，接数据库要写一遍，换了客户端还得重写。MCP（Model Context Protocol）用一份开放的 JSON-RPC 协议，把「AI 想用的上下文」标准化成了三类原语：

- **Tools（工具）**：模型可以主动调用的动作，比如「发起 HTTP 请求」「查一行 SQL」。这是 90% 场景会用到的原语。
- **Resources（资源）**：只读的数据源，用 URI 标识，比如 `file:///config.yaml`。适合让模型按需拉取文档。
- **Prompts（提示词模板）**：预置的可复用提示，用户可从菜单里挑。

整个生态的角色分工如下：

```mermaid
flowchart LR
    subgraph Host["宿主 Host（如 Claude Code）"]
        Client["MCP Client<br/>每个服务器一个"]
    end
    Server["MCP Server<br/>你的进程"]
    Client <-->|"JSON-RPC 2.0"| Server
    Server --> T["Tools"]
    Server --> R["Resources"]
    Server --> P["Prompts"]
    T --> World[("文件系统 / 数据库 / API")]
    R --> World
```

关键认知：**宿主和服务器是两个独立进程**，靠一条「传输通道」通信。本地最常用的是 **stdio**（标准输入输出）——宿主把请求写到你的进程 stdin，你把响应写到 stdout。远程则用 **Streamable HTTP**。记住一句口诀：**stdout 是协议通道，stderr 是日志通道，两者绝不能混**。这条规则后面会救你的命。

## 二、十分钟搭起项目骨架

新建一个目录，初始化 TypeScript 工程：

```bash
mkdir format-forge-mcp && cd format-forge-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
```

`@modelcontextprotocol/sdk` 是官方 SDK。注意截至 2026 年，它的 **v2 分支处于 beta**，API 还在演进；本教程用的是稳定且广泛兼容的高层 `McpServer` API，v1/v2 都能跑。

把 `tsconfig.json` 关键项调好（模块用 NodeNext，否则子路径导入会报错）：

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

在 `package.json` 里补上脚本和 bin 入口：

```json
{
  "type": "module",
  "bin": { "format-forge": "dist/index.js" },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js"
  }
}
```

新建 `src/index.ts`，写一个能启动、会报上自己名字的最小服务器：

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

const server = new McpServer({
  name: "format-forge",
  version: "1.0.0",
});

await server.connect(new StdioServerTransport());
```

跑一下 `npm run dev`，它会「卡住」不输出——**这是正常的**，因为它正等着 stdin 上来 JSON-RPC 请求。`Ctrl+C` 退出即可。能卡住不死，说明骨架通了。

## 三、定义第一个工具：用 Zod 立契约

MCP 工具的灵魂是**入参契约**。SDK 用 Zod schema 生成 JSON Schema 暴露给模型，模型据此决定怎么填参数。一个写得好的 schema，能让模型「看一眼就知道该传什么」。

加一个 `format_json` 工具——校验并美化 JSON：

```typescript
import { z } from "zod";

server.tool(
  "format_json",                                    // ① 工具名
  "Validate and pretty-print a JSON string. Use when you have minified or pasted JSON that may be malformed.",  // ② 给模型看的描述
  {
    input: z.string().min(1).describe("The raw JSON string to format"),
    indent: z
      .number().int().min(0).max(8)
      .default(2)
      .describe("Number of indentation spaces, 0 for compact"),
  },
  async ({ input, indent }) => {                    // ③ 处理函数
    try {
      const parsed = JSON.parse(input);
      return {
        content: [
          { type: "text", text: JSON.stringify(parsed, null, indent) },
        ],
      };
    } catch (err) {
      return {
        isError: true,
        content: [
          { type: "text", text: `Invalid JSON: ${(err as Error).message}` },
        ],
      };
    }
  }
);
```

四个要点逐条解释：

1. **工具名要动词起头、用蛇形**。`format_json` 远比 `formatter` 或 `json` 好——后者模型不知道该不该调。
2. **描述写清楚「何时该用」**。模型依据这段文字做工具选择决策，写「Use when…」比写功能列表有效得多。
3. **每个字段都要 `.describe()`**。Zod 会把这些描述编进 JSON Schema，模型的命中率高度依赖它。
4. **成功与失败都返回 `content` 数组**。失败时加 `isError: true`，告诉客户端「这是工具业务错误，不是协议崩溃」。

## 四、扩展工具：三条设计原则

第一个工具跑通后，照葫芦画瓢再加两个。但加之前先记住三条原则，能省掉后面 80% 的返工。

**原则一：宁可拆细，不要造「瑞士军刀」。** 一个 `convert(input, from, to, options)` 看着通用，但模型要在描述里读四种格式 × 两种方向的所有组合，容易填错。拆成 `json_to_csv`、`json_to_yaml` 这种动词明确的工具，每个 schema 更窄、命中更准。

**原则二：用枚举收口取值空间。** 凡是有限选项，就别给 `string`：

```typescript
server.tool(
  "json_to_yaml",
  "Convert a JSON object string to YAML.",
  {
    json: z.string().describe("A valid JSON object string"),
    flow: z
      .enum(["block", "flow"])
      .default("block")
      .describe("YAML output style"),
  },
  async ({ json }) => { /* ... */ }
);
```

**原则三：结构化输出比纯文本更可信。** 当返回的数据模型还要二次处理时，用 `structuredContent` 让客户端拿到结构化对象，而不是从一段文本里正则抠：

```typescript
async ({ json }) => {
  const data = JSON.parse(json);
  const keys = Object.keys(data);
  return {
    content: [{ type: "text", text: `Found ${keys.length} keys` }],
    structuredContent: { keyCount: keys.length, keys }, // 机器友好
  };
}
```

## 五、错误处理：让工具「说得清人话」

工具迟早会失败。MCP 给了你三个不同的失败通道，混用会让模型抓狂。记住这张对照表：

| 失败类型 | 正确做法 | 模型看到的效果 |
|---|---|---|
| 参数不合法 | 什么都不做，让 Zod 拦 | 客户端直接回 `InvalidParams`，模型自动重试 |
| 工具业务失败（如 JSON 解析错） | `return { isError: true, content }` | 模型读到错误文本，决定改参数还是换工具 |
| 服务器崩溃 | 抛异常 / 进程退出 | 客户端收到错误，但你丢了这次会话的连接 |

最常见的错误是**把业务失败当成异常抛**。一抛异常，进程可能崩；即便不崩，模型拿到的也是栈轨迹，毫无信息量。正确写法永远是：在 handler 内 `try/catch`，然后**把错误翻译成一句人话**塞进 `content`：

```typescript
async ({ json }) => {
  let data: unknown;
  try {
    data = JSON.parse(json);
  } catch (err) {
    return {
      isError: true,
      content: [{
        type: "text",
        text: `无法解析 JSON：${(err as Error).message}。请检查引号、逗号是否成对，再传一次。`,
      }],
    };
  }
  // ...正常路径
}
```

注意「翻译」这件事——`Unexpected token < in JSON` 对模型是噪音，「你的输入像是 HTML 而非 JSON」才是可行动的提示。

## 六、用 MCP Inspector 闭环测试

写完先别急着接 Claude Code。官方的 **MCP Inspector** 是一个独立的交互式调试客户端，能让你脱离大模型、手动点按测试每个工具：

```bash
npx @modelcontextprotocol/inspector npm run dev
```

它会启动一个本地 Web UI（默认 `http://localhost:6274`），左侧能看到服务器暴露的所有工具、入参 schema，右侧可以填值、点 `Run Tool` 看原始 JSON-RPC 响应。

Inspector 的价值在于：**它让你看到模型将看到的东西**——schema 长什么样、错误怎么回的、`structuredContent` 对不对。把每个工具都点一遍，确认契约无误，再进下一步。这是把「连不上 / 调不对」的排查成本前置消灭掉的关键一环。

```mermaid
sequenceDiagram
    participant U as 你
    participant I as MCP Inspector (Client)
    participant S as 你的 Server
    U->>I: 填参数，点 Run Tool
    I->>S: tools/call { name, args }
    S->>S: Zod 校验入参
    S-->>I: content / isError
    I-->>U: 渲染结果
    Note over U,S: 全程不消耗大模型 token
```

## 七、接入 Claude Code：三种作用域

服务器测好了，正式挂到 Claude Code。核心命令是 `claude mcp add`，关键是理解三种**作用域（scope）**，它决定了配置写到哪里、谁能共享：

```bash
# 1) local（默认）：仅当前项目 + 当前机器，写进本地缓存，最私密
claude mcp add format-forge -- node /绝对路径/dist/index.js

# 2) project：写进项目根的 .mcp.json，可随 git 提交，团队共享
claude mcp add format-forge --scope project -- node ./dist/index.js

# 3) user：跨你本机所有项目可用，适合通用工具
claude mcp add format-forge --scope user -- node /绝对路径/dist/index.js
```

注意 `--` 之后才是真正启动你进程的命令。`node` 后面**务必用绝对路径**（project scope 下相对路径也可，但需团队成员机器结构一致）。验证连接：

```bash
claude mcp list          # 看所有服务器与连接状态
claude mcp get format-forge  # 看单个服务器详情
```

在交互式 REPL 里输入 `/mcp`，能看到每个服务器的连接状态、暴露的工具数。如果工具没出现，多半是 schema 编译失败或进程启动即崩。

团队协作时，`.mcp.json` 的标准长这样（可提交进仓库）：

```json
{
  "mcpServers": {
    "format-forge": {
      "command": "node",
      "args": ["./dist/index.js"],
      "env": { "LOG_LEVEL": "info" }
    }
  }
}
```

队友拉下代码后，Claude Code 首次发现 `.mcp.json` 会提示授权加载。

## 八、调试实战：服务「连不上」的五大原因

按概率从高到低，逐个排除：

**① stdout 被污染。** 你在代码里随手 `console.log("hello")` 或第三方库往 stdout 打日志，把 JSON-RPC 帧破坏了。**铁律：服务器里所有日志一律走 `console.error`（stderr）或专用 logger**，stdout 只准回协议帧。这是「StdioServer 进程连上就挂」的头号原因。

**② 启动路径用了相对路径。** Claude Code 在不同 cwd 下启动你的进程，`./dist/index.js` 可能找不到。固定用绝对路径，或先 `npm run build` 再指向产物。

**③ 没构建就用 ts 源文件。** `command: "node"` 却指向 `.ts` 文件，直接报错。要么 `command: "npx"` + `args: ["tsx", "src/index.ts"]`（开发期），要么先 build 指向 `dist`。

**④ Zod schema 编译失败但没报。** 比如用了运行时不存在的校验、或 schema 引用了函数。用 Inspector 先过一遍能抓到。

**⑤ 进程读 stdin 是阻塞读、却没正确处理 EOF。** 用官方 `StdioServerTransport` 不会有这问题；只有手写底层 `Server` + `setRequestHandler` 时才踩。新手优先用高层 `McpServer`。

一条万能排查命令——把服务器当普通进程直接喂一帧，看它怎么回：

```bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node dist/index.js
```

能回一帧 `initialize` 结果，说明协议层没问题；卡住或乱码，就回到上面五条。

## 九、完整实战：format-forge-mcp 全量代码

把前面拼起来，这是一个三个工具、带日志、可即跑的完整 `src/index.ts`：

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

const log = (...a: unknown[]) => console.error("[format-forge]", ...a);

const server = new McpServer({
  name: "format-forge",
  version: "1.0.0",
});

// 工具 1：美化 JSON
server.tool(
  "format_json",
  "Validate and pretty-print a JSON string. Use on minified or pasted JSON.",
  {
    input: z.string().min(1).describe("Raw JSON string"),
    indent: z.number().int().min(0).max(8).default(2).describe("Indent spaces"),
  },
  async ({ input, indent }) => {
    try {
      const parsed = JSON.parse(input);
      log("formatted json, keys=", typeof parsed === "object" ? Object.keys(parsed as object).length : "scalar");
      return { content: [{ type: "text", text: JSON.stringify(parsed, null, indent) }] };
    } catch (err) {
      return { isError: true, content: [{ type: "text", text: `Invalid JSON: ${(err as Error).message}` }] };
    }
  }
);

// 工具 2：JSON 对象数组 → CSV
server.tool(
  "json_to_csv",
  "Convert a JSON array of flat objects into CSV. All objects should share the same keys.",
  {
    json: z.string().describe('A JSON array string, e.g. [{"a":1,"b":2}]'),
  },
  async ({ json }) => {
    let arr: unknown;
    try {
      arr = JSON.parse(json);
    } catch (err) {
      return { isError: true, content: [{ type: "text", text: `Invalid JSON: ${(err as Error).message}` }] };
    }
    if (!Array.isArray(arr)) {
      return { isError: true, content: [{ type: "text", text: "Input must be a JSON array." }] };
    }
    const rows = arr as Record<string, unknown>[];
    if (rows.length === 0) return { content: [{ type: "text", text: "" }] };

    const headers = Object.keys(rows[0]);
    const esc = (v: unknown) => {
      const s = v == null ? "" : String(v);
      return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
    };
    const csv = [headers.join(","), ...rows.map(r => headers.map(h => esc(r[h])).join(","))].join("\n");
    return {
      content: [{ type: "text", text: csv }],
      structuredContent: { rows: rows.length, columns: headers.length },
    };
  }
);

// 工具 3：点路径取值
server.tool(
  "json_path_get",
  "Read a value from a JSON object using a dot path, e.g. 'user.address.city'. Returns the value or an error if the path does not exist.",
  {
    json: z.string().describe("A JSON object string"),
    path: z.string().describe("Dot-separated path, e.g. 'a.b.0.c'"),
  },
  async ({ json, path }) => {
    let cur: unknown;
    try {
      cur = JSON.parse(json);
    } catch (err) {
      return { isError: true, content: [{ type: "text", text: `Invalid JSON: ${(err as Error).message}` }] };
    }
    for (const seg of path.split(".")) {
      if (cur == null) return { isError: true, content: [{ type: "text", text: `Path '${path}' not found (hit null at '${seg}').` }] };
      cur = (cur as Record<string, unknown>)[seg];
    }
    return {
      content: [{ type: "text", text: cur === undefined ? "(undefined)" : JSON.stringify(cur) }],
      structuredContent: { found: cur !== undefined, value: cur },
    };
  }
);

await server.connect(new StdioServerTransport());
log("format-forge mcp ready");
```

构建并接入：

```bash
npm run build
claude mcp add format-forge --scope user -- node "$(pwd)/dist/index.js"
claude mcp list
```

然后在 Claude Code 里直接对它说话：「把这段 `{"users":[{"id":1,"name":"Ada"},{"id":2,"name":"Alan"}]}` 转成 CSV」，模型会自动挑 `json_to_csv`。整套开发到验证的工作流：

```mermaid
flowchart TD
    A["写 src/index.ts<br/>server.tool(...)"] --> B["tsx 本地启动<br/>确认不崩"]
    B --> C["Inspector 点测<br/>逐个验 schema"]
    C --> D{契约 OK?}
    D -- 否 --> A
    D -- 是 --> E["npm run build"]
    E --> F["claude mcp add<br/>选 scope"]
    F --> G["claude mcp list<br/>看连接"]
    G --> H["REPL /mcp 看工具<br/>自然语言试调"]
    H --> I{调对?}
    I -- 否 --> J["查 stdout 污染<br/>/路径/构建"]
    J --> A
    I -- 是 --> K["✅ 上线"]
```

## 延伸阅读

- **MCP 官方文档** — 传输协议、生命周期、工具设计：`https://modelcontextprotocol.io/docs`
- **TypeScript SDK 仓库** — v2 beta 与高层 API 全量示例：`https://github.com/modelcontextprotocol/typescript-sdk`
- **MCP Inspector 指南** — 交互调试与配置导出：`https://modelcontextprotocol.io/docs/tools/inspector`
- **Claude Code 接 MCP** — 作用域、`.mcp.json`、命令参考：`https://code.claude.com/docs/en/mcp`
- 远程化方向（流式 HTTP 上云）属于进阶，可另立专题深入。

## 训练营适用性评估

- **适合训练营类型**：后端 / 全栈 / AI 应用开发营，2–3 天工作坊型课程最佳；也可作为「AI Infra 入门营」的实操模块。
- **前置能力**：需学员具备 TypeScript 基本读写能力与命令行操作习惯。
- **建议教学时长**：讲授 + 跟做约 **3.5 小时**（含三工具编写）；加 Inspector 调试与 Claude Code 接入实练，合计约 **半天（4–5 学时）**。
- **考核建议**：要求学员各自交付一个「至少 2 个工具、含一处 `isError` 业务错误处理、能通过 Inspector 全工具点测」的 MCP 服务器，并当场用自然语言在 Claude Code 中调通。

## 参考来源

1. [MCP TypeScript SDK — GitHub](https://github.com/modelcontextprotocol/typescript-sdk) — 官方 SDK 仓库，含 v2 beta 与高层 `McpServer` API 全量示例
2. [MCP Inspector — Model Context Protocol](https://modelcontextprotocol.io/docs/tools/inspector) — 官方交互式调试客户端使用指南
3. [Connect Claude Code to tools via MCP — Claude Code Docs](https://code.claude.com/docs/en/mcp) — `claude mcp add` 三种作用域与 `.mcp.json` 配置参考
4. [Debugging — Model Context Protocol](https://modelcontextprotocol.io/docs/tools/debugging) — 官方调试分层（Inspector / 日志 / 客户端工具）方法论
5. [Add Custom Tools to TypeScript MCP Servers — MCPcat](https://mcpcat.io/guides/adding-custom-tools-mcp-server-typescript/) — 第三方实战指南，Zod 校验与错误处理补充示例
—— `McpServer` 高层 API 与完整示例。
2. **MCP 规范**：`modelcontextprotocol.io/specification` —— 协议细节、传输方式、生命周期。
3. **MCP 示例库**：`modelcontextprotocol.io/examples` —— 官方维护的参考服务器集合。
4. **Microsoft MCP for Beginners**：`github.com/microsoft/mcp-for-beginners` —— 微软出品的入门课程，含 stdio 服务器章节。
5. **Inspector 文档**：用 `npx @modelcontextprotocol/inspector` 探索协议交互细节。

---

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| **适用训练营类型** | 全栈开发、AI 工程化、DevOps 工具链、企业内部 AI 提效训练营 |
| **学员基础要求** | 中级——需懂 TypeScript 与命令行 |
| **建议教学时长** | 4–6 小时（含动手实操） |
| **可拆分模块** | 骨架搭建(1h) → 单工具实现(2h) → 调试接入(1h) → 自定义扩展(1–2h) |
| **课后挑战** | 学员自选一个外部能力（如 Docker、Jira API、本地日历）仿照本教程造一个 MCP 服务器 |
| **评估方式** | 工具在 Inspector 跑通 + Claude Code 自然语言调用成功返回正确结果 |

> **教学价值**：本教程把"协议理解 → 骨架 → 工具 → 调试 → 接入 → 验证"完整闭环走通，学员掌握的是**方法论**而非单一示例，结业即可独立开发任意 MCP 服务器。

## 参考来源

1. [MCP TypeScript SDK（GitHub）](https://github.com/modelcontextprotocol/typescript-sdk) — 官方 TypeScript SDK 主仓库，含 `McpServer` 高层 API 文档与示例
2. [Build a server — Model Context Protocol](https://modelcontextprotocol.io/docs/develop/build-server) — 官方构建 MCP 服务器指南，讲解传输与原语
3. [Transports — MCP Specification 2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) — 协议规范中 stdio / HTTP 传输机制定义
4. [Microsoft MCP for Beginners — stdio server](https://github.com/microsoft/mcp-for-beginners/blob/main/03-GettingStarted/05-stdio-server/README.md) — 微软入门课程，详解 stdio 服务器原理
5. [Build an MCP Server in Node.js（OneUptime）](https://oneuptime.com/blog/post/2025-12-17-build-mcp-server-nodejs/view) — Node.js 实战教程，覆盖工具注册与资源开发

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