本教程带你用官方 TypeScript SDK(@modelcontextprotocol/sdk)从零构建一个日志分析 MCP 服务器。学完后你能:注册带类型校验的工具、用 structuredContent 返回结构化结果、用 ResourceTemplate 暴露日志片段资源、用 isError 让模型自我纠错,最后用一条 claude mcp add 命令把它接入 Claude Code,实现"自然语言问日志"。
适用人群:写过 Node.js/TypeScript、想给 Claude Code 加自定义工具的开发者。预计 60–90 分钟可跑通。
fetch 和 node:crypto)一个反直觉的坑:MCP 服务器跑在 stdio 模式时,stdout 是协议通道,任何
console.log都会污染 JSON-RPC 流导致连接崩溃。全文日志一律走console.error(stderr)。这是 TS/JS 实现里最高频的翻车点。
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 生成配置文件。
常见问题:
Cannot find module .../mcp.js:多半是 moduleResolution 没设成 NodeNext,或自己写 import 时漏了 .js 扩展名(TS 的 ESM 要求带 .js,即便源文件是 .ts)。tsx 装不上:可改用 ts-node,但 tsx 对 ESM 更省心。新建 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 重复。写清调用顺序("先检索再读片段")能显著提升模型调用准确率。
用 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 里的自然语言摘要。
常见问题:
structuredContent 报类型错误:把 interface SearchHit 改成 type SearchHit,或写成 structuredContent: { ...result } 展开。description 里给一个具体例子(如 '5\d\d'),把 isError 文案写清楚,模型会据此改写正则。资源(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: "拒绝访问根目录外的文件" }] };
}
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) 按错误类型归类,给出最可能的根因与建议。` },
},
],
})
);
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 高效得多。
在项目目录执行:
# 用 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 是否清晰到模型知道"何时该用它"。模糊的描述是工具"隐身"的头号原因。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 的价值。
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 模型自我纠正]
2026-07-28(SEP-2577)后进入弃用期,官方建议 stdio 服务器改用 stderr 日志、远程服务器改用 OpenTelemetry。本教程全程 console.error 即符合这一方向。resource_link 引用而非把内容全嵌进来,让客户端按需拉取,能省大量 token。_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 步)。每步给一份可运行的种子代码,让学员的精力集中在改写工具逻辑而非调环境。
simpleStreamableHttp.ts:从 stdio 一键迁移到远程 HTTP。@modelcontextprotocol/inspector:图形化调试任意 MCP 服务器,排查连接与协议问题。[!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 等场景全覆盖。
