首页 › tutorials › TS构建日志分析MCP服务器

概述

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

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

前置条件

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

正文

第 1 步:初始化项目

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 后缀的导入会解析失败:

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

预期结果:node_modules 出现 @modelcontextprotocol/sdk,tsc --init 生成配置文件。

常见问题:

第 2 步:搭建服务器骨架

新建 src/server.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 相对动态语言的最大优势:类型在编译期和运行期双重保证。

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 里的自然语言摘要。

常见问题:

第 4 步:用 ResourceTemplate 暴露日志片段

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

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)和逃逸的符号链接。生产环境务必补上这段:

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 比让模型自己规划更稳。

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 步:编译与本地冒烟测试

npx tsx src/server.ts   # 直接跑 TS(开发期最快)
# 或编译后跑:
npx tsc && node dist/server.js

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

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

在项目目录执行:

# 用 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,最后产出归因报告。

常见问题:

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

假设线上某服务持续返回 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 的价值。

工作流程图

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 模型自我纠正]

进阶与生产化建议

训练营适用性评估

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

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

参考来源

  1. Building MCP servers — TypeScript SDK 官方指南 — McpServer、registerTool、ResourceTemplate、logging/progress 的权威 API 说明。
  2. MCP TypeScript SDK 文档站 — 涵盖 transports、resource_link、completions 与 v2 包结构。
  3. Build MCP Server with TypeScript: Complete Tutorial (2026) — 强调 NodeNext 配置、Zod 校验、stderr 日志等实战要点。
  4. @modelcontextprotocol/sdk — npm — 安装与版本信息。
  5. MCP 架构与协议概念 — 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学通信知识星球海报-扫码加入