<!-- title: TS构建日志分析MCP服务器 -->

## 概述

本教程带你用官方 TypeScript SDK（`@modelcontextprotocol/sdk`）从零构建一个**日志分析 MCP 服务器**。学完后你能：注册带类型校验的工具、用 `structuredContent` 返回结构化结果、用 `ResourceTemplate` 暴露日志片段资源、用 `isError` 让模型自我纠错，最后用一条 `claude mcp add` 命令把它接入 Claude Code，实现"自然语言问日志"。

**适用人群**：写过 Node.js/TypeScript、想给 Claude Code 加自定义工具的开发者。预计 60–90 分钟可跑通。

## 前置条件

- Node.js 18+（需原生 `fetch` 和 `node:crypto`）
- 已安装 Claude Code CLI 并能正常启动
- 一份本地日志文件用于测试（本文用 nginx 风格的访问日志举例）
- 基础的命令行与 TypeScript 语法知识

> 一个反直觉的坑：MCP 服务器跑在 stdio 模式时，**stdout 是协议通道，任何 `console.log` 都会污染 JSON-RPC 流导致连接崩溃**。全文日志一律走 `console.error`（stderr）。这是 TS/JS 实现里最高频的翻车点。

## 正文

### 第 1 步：初始化项目

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

编辑生成的 `tsconfig.json`，把模块解析设为 NodeNext（ESM 必需），否则 `.js` 后缀的导入会解析失败：

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

**预期结果**：`node_modules` 出现 `@modelcontextprotocol/sdk`，`tsc --init` 生成配置文件。

**常见问题**：

- 报错 `Cannot find module .../mcp.js`：多半是 `moduleResolution` 没设成 `NodeNext`，或自己写 import 时漏了 `.js` 扩展名（TS 的 ESM 要求带 `.js`，即便源文件是 `.ts`）。
- `tsx` 装不上：可改用 `ts-node`，但 `tsx` 对 ESM 更省心。

### 第 2 步：搭建服务器骨架

新建 `src/server.ts`：

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

const server = new McpServer(
  { name: "logscope", version: "0.1.0" },
  {
    // instructions 会被客户端拼进系统提示，告诉模型整体用法
    instructions:
      "这是一个本地日志分析服务器。先调用 search_logs 检索，再按需用 log_excerpt 读取上下文片段。结果最多 50 行。"
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[logscope] stdio server started"); // 注意：stderr，不是 stdout
```

**预期结果**：直接 `npx tsx src/server.ts` 运行会"卡住"等待 stdio 输入，这很正常——它在等客户端握手。`Ctrl+C` 退出即可。

**关键点**：`instructions` 是跨工具的"使用说明书"，别和单个工具的 `description` 重复。写清调用顺序（"先检索再读片段"）能显著提升模型调用准确率。

### 第 3 步：注册第一个工具——结构化检索

用 Zod 做 inputSchema 校验，是 TS 相对动态语言的最大优势：类型在编译期和运行期双重保证。

```ts
import * as z from "zod";

// 用 type alias（不要用 interface），否则缺索引签名，无法赋给 structuredContent
type SearchHit = {
  file: string;
  line: number;
  level: "ERROR" | "WARN" | "INFO";
  message: string;
};

server.registerTool(
  "search_logs",
  {
    title: "检索日志",
    description: "按正则检索本地日志，返回匹配行。只读、无副作用。",
    inputSchema: {
      pattern: z.string().describe("正则表达式，如 '5\\d\\d' 匹配 5xx 状态码"),
      file: z.string().describe("日志文件路径"),
      limit: z.number().int().min(1).max(200).default(50),
    },
    outputSchema: { hits: z.array(z.object({ file: z.string(), line: z.number(), level: z.string(), message: z.string() })) },
    annotations: {
      readOnlyHint: true,        // 只读：客户端可放心自动调用
      openWorldHint: false,      // 不访问外部网络
    },
  },
  async ({ pattern, file, limit }) => {
    try {
      const { readFileSync } = await import("node:fs");
      const text = readFileSync(file, "utf8");
      const re = new RegExp(pattern);
      const hits: SearchHit[] = [];
      text.split("\n").forEach((raw, i) => {
        if (re.test(raw)) {
          const level = /ERROR/i.test(raw) ? "ERROR" : /WARN/i.test(raw) ? "WARN" : "INFO";
          hits.push({ file, line: i + 1, level, message: raw.trim() });
        }
      });
      return {
        content: [{ type: "text" as const, text: `命中 ${Math.min(hits.length, limit)} / ${hits.length} 条` }],
        structuredContent: { hits: hits.slice(0, limit) },
      };
    } catch (err) {
      // isError=true 的错误会被模型看到并能自我纠正；抛异常则被吞成协议错误
      return {
        isError: true,
        content: [{ type: "text" as const, text: `检索失败：${err instanceof Error ? err.message : String(err)}` }],
      };
    }
  }
);
```

**预期结果**：传入 `{"pattern":"500","file":"/var/log/nginx/access.log","limit":10}`，返回 JSON 结构化的命中行数组。模型既能读 `structuredContent` 拿到机器可读数据，也能读 `content` 里的自然语言摘要。

**常见问题**：

- `structuredContent` 报类型错误：把 `interface SearchHit` 改成 `type SearchHit`，或写成 `structuredContent: { ...result }` 展开。
- 正则报"无效模式"被模型反复尝试：在 `description` 里给一个具体例子（如 `'5\d\d'`），把 `isError` 文案写清楚，模型会据此改写正则。

### 第 4 步：用 ResourceTemplate 暴露日志片段

资源（resource）是**应用端控制**的只读数据，适合"按需取大段内容"。我们用 URI 模板 `log://{file}/{start}-{end}` 让客户端按行号区间取片段，避免一次塞进上下文。

```ts
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

server.registerResource(
  "log_excerpt",
  new ResourceTemplate("log://{file}/{start}-{end}", { list: undefined }),
  {
    title: "日志片段",
    description: "按行号区间读取日志片段",
    mimeType: "text/plain",
  },
  async (uri, { file, start, end }) => {
    const { readFileSync } = await import("node:fs");
    const s = Number(start), e = Number(end);
    const lines = readFileSync(String(file), "utf8").split("\n").slice(s - 1, e);
    return {
      contents: [{ uri: uri.href, mimeType: "text/plain", text: lines.map((l, i) => `${s + i}\t${l}`).join("\n") }],
    };
  }
);
```

**安全要点**：当资源底层是文件系统时，规范**强制要求**清洗路径。务必解析绝对路径并校验它仍在允许的根目录内，拒绝 `..`（含编码形式 `%2e%2e`）和逃逸的符号链接。生产环境务必补上这段：

```ts
import { resolve } from "node:path";
const ROOT = resolve(process.env.LOG_ROOT ?? "./logs");
const safe = resolve(String(file));
if (!safe.startsWith(ROOT + "/") && safe !== ROOT) {
  return { isError: true, content: [{ type: "text", text: "拒绝访问根目录外的文件" }] };
}
```

### 第 5 步：用 Prompt 固化"根因分析"交互

Prompt 是用户显式触发的" canned 交互模板"。当某个排查流程是固定的（先取 5xx → 读上下文 → 归类原因），用 prompt 比让模型自己规划更稳。

```ts
server.registerPrompt(
  "root_cause",
  {
    title: "5xx 根因分析",
    description: "针对某个文件做 5xx 错误的根因分析",
    argsSchema: { file: z.string().describe("日志文件路径") },
  },
  ({ file }) => ({
    messages: [
      {
        role: "user" as const,
        content: { type: "text" as const, text:
          `请对日志文件 ${file} 做根因分析：1) 用 search_logs 找出所有 5xx 状态码行；2) 对每条用 log_excerpt 读取前后 5 行上下文；3) 按错误类型归类，给出最可能的根因与建议。` },
      },
    ],
  })
);
```

### 第 6 步：编译与本地冒烟测试

```bash
npx tsx src/server.ts   # 直接跑 TS（开发期最快）
# 或编译后跑：
npx tsc && node dist/server.js
```

冒烟测试不需要 Claude Code，直接用 JSON-RPC 喂一行握手请求验证不崩：

```bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}' | npx tsx src/server.ts
```

**预期结果**：stderr 打印启动日志，stdout 返回一段带 `serverInfo` 的 `initialize` 响应 JSON。若 stdout 里混入了非 JSON 文本，就是有 `console.log` 漏网了。

**进阶**：用官方 Inspector 做全链路调试——`npx @modelcontextprotocol/inspector npx tsx src/server.ts`，在浏览器里点按调用工具、查看资源，比手撸 JSON 高效得多。

### 第 7 步：接入 Claude Code

在项目目录执行：

```bash
# 用 stdio 模式注册；-- 分隔符后是启动命令
claude mcp add logscope -- npx -y tsx /绝对路径/src/server.ts

# 验证已注册
claude mcp list
```

启动 Claude Code，输入 `/mcp` 应能看到 `logscope` 处于 connected。然后试问：

> "分析 ./logs/app.log 里所有的 5xx 错误，给出根因。"

模型会自动按 `instructions` 的顺序调用 `search_logs` → `log_excerpt`，或直接触发 `root_cause` prompt，最后产出归因报告。

**常见问题**：

- `/mcp` 显示 failed：90% 是启动命令路径问题。改成绝对路径；或先 `npx tsx` 单独确认能起来。
- 工具没被调用：检查 `description` 是否清晰到模型知道"何时该用它"。模糊的描述是工具"隐身"的头号原因。
- 路径找不到：Claude Code 启动 MCP 子进程的工作目录可能不是你预期目录，日志路径最好用绝对路径或通过环境变量 `LOG_ROOT` 传入。

## 实战案例：一次真实的"半夜告警"排查

假设线上某服务持续返回 504，你被叫醒。把网关访问日志下载到 `./logs/gw.log`，在 Claude Code 里问：

> "用 logscope 看 gw.log，最近 1000 行里 504 都集中在哪些上游和时段？"

模型会：① 调 `search_logs`（pattern `504`，limit 50）拿到结构化命中；② 解析出 `upstream` 字段和 `timestamp`，按上游/分钟聚合；③ 对异常突增点调 `log_excerpt` 取上下文；④ 输出"21:03–21:07 upstream-B 超时，疑似其依赖的 DB 连接池打满"的结论与证据行号。

把这段排查沉淀进 `root_cause` prompt 的模板，下次同类告警一句话即可复现。这就是把"专家经验"编码进 MCP 的价值。

## 工作流程图

```mermaid
flowchart TD
    A[Claude Code 用户提问] --> B{解析意图}
    B -->|结构化检索| C[search_logs 工具]
    B -->|固化流程| D[root_cause Prompt]
    D --> C
    C -->|Zod 校验输入| E[读取本地日志]
    E -->|结构化命中| F[structuredContent 返回]
    F --> G{需要上下文?}
    G -->|是| H[log_excerpt 资源模板]
    H -->|按行号区间| I[返回片段]
    I --> J[模型聚合归因]
    G -->|否| J
    J --> K[输出根因报告+证据行号]
    E -.出错.-> L[isError=true 模型自我纠正]
```

## 进阶与生产化建议

- **协议日志别走 MCP logging**：MCP 的 logging 通知在协议版本 `2026-07-28`（SEP-2577）后进入弃用期，官方建议 stdio 服务器改用 **stderr 日志**、远程服务器改用 **OpenTelemetry**。本教程全程 `console.error` 即符合这一方向。
- **资源用 ResourceLink 而非内联**：当结果可能很大时，工具返回 `resource_link` 引用而非把内容全嵌进来，让客户端按需拉取，能省大量 token。
- **长任务报告进度**：扫描 GB 级日志时，若请求 `_meta` 带 `progressToken`，用 `ctx.mcpReq.notify()` 发送 `notifications/progress`，让客户端显示进度条。
- **远程化**：把 `StdioServerTransport` 换成 `StreamableHTTPServerTransport` 即可上云；多租户再叠加 OAuth2。这两条迁移路径官方 SDK 都有示例（`simpleStreamableHttp.ts`）。

## 训练营适用性评估

| 训练营类型 | 适用性 | 教学时长 | 备注 |
|---|---|---|---|
| Claude Code 工具开发进阶营 | ★★★★★ | 4–6 小时 | 完整覆盖三原语+TS 类型安全 |
| 企业内部 AI 提效营 | ★★★★☆ | 3–4 小时 | 侧重日志/运维场景落地 |
| 零基础入门营 | ★★☆☆☆ | 跳过 | 需先补 TS 与命令行基础 |
| 安全/合规专题营 | ★★★★☆ | 2 小时 | 用路径清洗与 annotation 讲护栏 |

建议教学顺序：先跑通骨架（第 1–3 步）建立成就感 → 再加资源/安全（第 4 步） → 最后 prompt 与接入（第 5–7 步）。每步给一份可运行的种子代码，让学员的精力集中在改写工具逻辑而非调环境。

## 参考来源

1. [Building MCP servers — TypeScript SDK 官方指南](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md) — McpServer、registerTool、ResourceTemplate、logging/progress 的权威 API 说明。
2. [MCP TypeScript SDK 文档站](https://ts.sdk.modelcontextprotocol.io/documents/server.html) — 涵盖 transports、resource_link、completions 与 v2 包结构。
3. [Build MCP Server with TypeScript: Complete Tutorial (2026)](https://mcpize.com/blog/mcp-server-typescript) — 强调 NodeNext 配置、Zod 校验、stderr 日志等实战要点。
4. [@modelcontextprotocol/sdk — npm](https://www.npmjs.com/package/@modelcontextprotocol/sdk) — 安装与版本信息。
5. [MCP 架构与协议概念](https://modelcontextprotocol.io/docs/learn/architecture) — tools/resources/prompts 何时使用各的原理解释。

## 延伸阅读

- 官方示例库 `simpleStreamableHttp.ts`：从 stdio 一键迁移到远程 HTTP。
- Anthropic Cookbook 中的 MCP 章节：多个垂直场景（数据库、文件系统、搜索）的可运行样例。
- `@modelcontextprotocol/inspector`：图形化调试任意 MCP 服务器，排查连接与协议问题。
- Model Context Protocol 规范的 Tools / Resources / Prompts 三节：理解协议层语义，写出语义更准的工具描述。

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