当MCP Tool需要执行超过30秒的复杂操作——批量代码重构、大规模数据分析、远程部署流水线——你会遇到三大问题:客户端超时断开、用户无法感知进度、意外中断无法恢复。本教程从MCP通知机制入手,逐步构建支持进度推送、优雅取消和异步Task恢复的生产级长任务MCP Server,最终交付一个可运行的代码审计引擎。
适用人群:已有MCP Server基础开发经验,需要处理耗时操作的TypeScript开发者。
| 条件 | 说明 |
|---|---|
| Node.js >= 18 | 运行时环境 |
@modelcontextprotocol/sdk >= 1.20 |
支持通知与Task的SDK版本 |
| Claude Code CLI | 用于集成测试 |
| 基础MCP知识 | 了解Tool/Resource/Prompt三原语 |
# 快速环境验证
node -v # v18+
npm ls @modelcontextprotocol/sdk # 确认版本
claude --version # Claude Code CLI
MCP协议基于JSON-RPC 2.0,默认是请求-响应模式。一次Tool调用如果耗时5分钟,客户端只能干等。通知(Notification)机制打破了这种沉默——Server可以主动向Client推送消息,且不期望回复。
sequenceDiagram
participant Client as Claude Code
participant Server as MCP Server
Client->>Server: tools/call {name: "analyze_codebase", progressToken: "p1"}
Note over Server: 开始分析...
Server-->>Client: notifications/progress {progressToken: "p1", progress: 20, total: 100}
Note over Client: 显示进度 20%
Note over Server: 解析AST...
Server-->>Client: notifications/progress {progress: 50, total: 100}
Note over Client: 显示进度 50%
Note over Server: 生成报告...
Server-->>Client: notifications/progress {progress: 90, total: 100}
Server->>Client: Tool Result {content: "分析完成"}
MCP规范定义了三类通知方向,每类包含具体类型:
| 方向 | 通知类型 | 用途 |
|---|---|---|
| Server→Client | notifications/progress |
工具执行进度 |
| Server→Client | notifications/resource/updated |
资源内容变更 |
| Server→Client | notifications/tools/list_changed |
工具列表变化 |
| Client→Server | notifications/cancelled |
取消正在执行的请求 |
| 双向 | notifications/message(日志) |
调试日志输出 |
创建项目结构:
mkdir mcp-longtask && cd mcp-longtask
npm init -y
npm install @modelcontextprotocol/sdk zod
核心文件 server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "longtask-demo",
version: "1.0.0",
});
// 工具一:带进度通知的批量处理
server.tool(
"batch_process",
"批量处理数据文件,实时返回进度",
{
fileCount: z.number().min(1).max(100).describe("待处理文件数量"),
operation: z.enum(["analyze", "transform", "validate"]).describe("操作类型"),
},
{
// 声明此工具支持长时间运行,可被取消
destructiveHint: false,
openWorldHint: false,
},
async ({ fileCount, operation }, { sendNotification, signal }) => {
const results: string[] = [];
for (let i = 1; i <= fileCount; i++) {
// 检查取消信号
if (signal?.aborted) {
return {
content: [{
type: "text" as const,
text: `已取消:处理了 ${i - 1}/${fileCount} 个文件。已完成的结果:\n${results.join("\n")}`,
}],
isError: true,
};
}
// 模拟处理单个文件
await new Promise((resolve) => setTimeout(resolve, 800));
results.push(`文件 ${i}: ${operation} 完成`);
// 发送进度通知
if (sendNotification) {
await sendNotification({
method: "notifications/progress",
params: {
progressToken: "batch-process-1", // 与请求中的 progressToken 对应
progress: i,
total: fileCount,
},
});
}
}
return {
content: [{
type: "text" as const,
text: `全部完成!共处理 ${fileCount} 个文件。\n${results.join("\n")}`,
}],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
package.json 添加启动脚本:
{
"name": "mcp-longtask",
"type": "module",
"scripts": {
"start": "npx tsx server.ts",
"build": "npx tsx server.ts"
}
}
在项目根目录创建 .mcp.json:
{
"mcpServers": {
"longtask": {
"command": "npx",
"args": ["tsx", "server.ts"],
"cwd": "."
}
}
}
启动Claude Code后输入:
请使用 batch_process 工具处理 10 个文件,执行 analyze 操作
预期结果:Claude Code在等待过程中会显示进度百分比,从10%逐步递增到100%,最终返回完整处理报告。
| 问题 | 原因 | 解决 |
|---|---|---|
| 看不到进度通知 | sendNotification 未传入或为空 |
检查SDK版本 >= 1.20 |
| 进度Token不匹配 | Token未与客户端请求关联 | 使用请求中传入的 progressToken |
| 工具执行超时被杀 | 超过Claude Code默认超时 | 拆分为更小的子任务 |
MCP的取消机制依赖 AbortSignal,从Claude Code客户端一直穿透到你的Tool实现:
flowchart LR
A[用户点击取消] --> B[Claude Code发送<br>notifications/cancelled]
B --> C[SDK拦截并触发<br>AbortSignal.abort]
C --> D[Tool中检查<br>signal.aborted]
D --> E[清理资源<br>返回部分结果]
在 server.ts 中追加:
// 工具二:可中断的代码扫描器
server.tool(
"code_scan",
"深度扫描代码库,支持中断并返回已扫描结果",
{
directory: z.string().describe("扫描目录路径"),
rules: z.array(z.string()).describe("检查规则列表,如 security, performance, style"),
},
async ({ directory, rules }, { sendNotification, signal }) => {
const findings: Array<{ file: string; rule: string; severity: string; message: string }> = [];
// 模拟文件列表
const files = Array.from({ length: 20 }, (_, i) => `src/module-${i + 1}.ts`);
let scannedCount = 0;
for (const file of files) {
// 优雅退出检查点
if (signal?.aborted) {
// 发送日志通知告知中断
if (sendNotification) {
await sendNotification({
method: "notifications/message",
params: {
level: "warning",
data: `扫描中断于第 ${scannedCount} 个文件,返回已完成结果`,
},
});
}
return {
content: [
{
type: "text" as const,
text: `⚠️ 扫描被中断\n\n已完成: ${scannedCount}/${files.length} 个文件\n发现: ${findings.length} 个问题\n\n部分结果:\n${JSON.stringify(findings, null, 2)}`,
},
],
isError: false, // 中断不算错误,返回可用结果
};
}
// 模拟对每个文件应用每条规则
for (const rule of rules) {
if (Math.random() > 0.7) {
findings.push({
file,
rule,
severity: Math.random() > 0.5 ? "high" : "medium",
message: `在 ${file} 中发现 ${rule} 相关问题`,
});
}
}
scannedCount++;
// 发送进度
if (sendNotification) {
await sendNotification({
method: "notifications/progress",
params: {
progressToken: "code-scan-1",
progress: scannedCount,
total: files.length,
},
});
}
await new Promise((resolve) => setTimeout(resolve, 300));
}
// 生成汇总报告
const summary = {
totalFiles: files.length,
totalFindings: findings.length,
bySeverity: {
high: findings.filter(f => f.severity === "high").length,
medium: findings.filter(f => f.severity === "medium").length,
},
byRule: rules.reduce((acc, rule) => {
acc[rule] = findings.filter(f => f.rule === rule).length;
return acc;
}, {} as Record<string, number>),
};
return {
content: [{
type: "text" as const,
text: `✅ 扫描完成\n\n${JSON.stringify(summary, null, 2)}\n\n详细发现:\n${JSON.stringify(findings, null, 2)}`,
}],
};
}
);
| 策略 | 实现方式 | 适用场景 |
|---|---|---|
| 快照恢复 | Tool返回中间状态,下次调用传入继续 | 文件处理、数据转换 |
| 检查点持久化 | 写入临时文件/数据库,自动从断点续传 | 大规模分析、编译任务 |
| 分页分批 | 将大任务拆为固定大小的批次,按页执行 | API遍历、日志扫描 |
检查点持久化示例:
import { writeFileSync, readFileSync, existsSync } from "fs";
const CHECKPOINT_FILE = "/tmp/mcp-scan-checkpoint.json";
interface ScanCheckpoint {
lastFileIndex: number;
timestamp: number;
findings: Array<{ file: string; rule: string; severity: string; message: string }>;
}
function saveCheckpoint(data: ScanCheckpoint) {
writeFileSync(CHECKPOINT_FILE, JSON.stringify(data));
}
function loadCheckpoint(): ScanCheckpoint | null {
if (!existsSync(CHECKPOINT_FILE)) return null;
return JSON.parse(readFileSync(CHECKPOINT_FILE, "utf-8"));
}
// 工具三:支持断点续传的扫描器
server.tool(
"resumable_scan",
"支持断点续传的代码扫描器",
{
directory: z.string().describe("目标目录"),
resume: z.boolean().default(false).describe("是否从上次断点继续"),
},
async ({ directory, resume }, { sendNotification, signal }) => {
const files = Array.from({ length: 50 }, (_, i) => `src/deep/module-${String(i).padStart(3, "0")}.ts`);
let checkpoint = resume ? loadCheckpoint() : null;
let startIndex = checkpoint?.lastFileIndex ?? 0;
let findings = checkpoint?.findings ?? [];
if (checkpoint) {
if (sendNotification) {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `从第 ${startIndex + 1} 个文件恢复,已有 ${findings.length} 个发现`,
},
});
}
}
for (let i = startIndex; i < files.length; i++) {
if (signal?.aborted) {
// 保存检查点
saveCheckpoint({ lastFileIndex: i, timestamp: Date.now(), findings });
return {
content: [{
type: "text" as const,
text: `⏸ 已暂停于第 ${i + 1}/${files.length} 个文件。检查点已保存。\n再次调用 resumable_scan 并设置 resume=true 可继续。`,
}],
};
}
// 模拟扫描
if (Math.random() > 0.8) {
findings.push({
file: files[i],
rule: "security",
severity: "high",
message: `潜在安全问题: ${files[i]}`,
});
}
// 每10个文件保存一次检查点
if ((i + 1) % 10 === 0) {
saveCheckpoint({ lastFileIndex: i + 1, timestamp: Date.now(), findings });
}
if (sendNotification) {
await sendNotification({
method: "notifications/progress",
params: {
progressToken: "resumable-scan",
progress: i + 1,
total: files.length,
},
});
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
// 清理检查点文件
try { require("fs").unlinkSync(CHECKPOINT_FILE); } catch {}
return {
content: [{
type: "text" as const,
text: `✅ 全量扫描完成\n文件: ${files.length}\n发现: ${findings.length} 个问题`,
}],
};
}
);
MCP的Resource默认是静态数据。但通过订阅机制,Server可以在数据变化时主动推送更新:
sequenceDiagram
participant CC as Claude Code
participant S as MCP Server
CC->>S: resources/subscribe {uri: "build://status"}
S-->>CC: 订阅确认
Note over S: 构建状态变更...
S-->>CC: notifications/resources/updated {uri: "build://status"}
CC->>S: resources/read {uri: "build://status"}
S->>CC: 返回最新构建状态
Note over S: 构建完成...
S-->>CC: notifications/resources/updated {uri: "build://status"}
CC->>S: resources/read {uri: "build://status"}
S->>CC: 构建成功 ✓
CC->>S: resources/unsubscribe {uri: "build://status"}
在 server.ts 中添加:
// 模拟构建状态存储
interface BuildStatus {
id: string;
project: string;
status: "queued" | "running" | "success" | "failed";
progress: number;
startedAt: string;
logs: string[];
}
const buildStore = new Map<string, BuildStatus>();
// 添加定期更新的构建状态资源
server.resource(
"build-status",
"build://status",
async (uri) => {
const statuses = Array.from(buildStore.values());
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(statuses, null, 2),
}],
};
}
);
// 单个构建资源
server.resource(
"build-detail",
new z.ZodType<string, z.ZodStringDef>().optional().describe("build://status/{buildId}"),
// 使用模板URI
async (uri) => {
const buildId = uri.href.replace("build://status/", "");
const status = buildStore.get(buildId);
if (!status) {
return {
contents: [{
uri: uri.href,
text: `构建 ${buildId} 不存在`,
}],
};
}
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(status, null, 2),
}],
};
}
);
// 工具四:触发构建并监控
server.tool(
"trigger_build",
"触发项目构建,实时推送状态更新",
{
project: z.string().describe("项目名称"),
command: z.string().describe("构建命令"),
},
async ({ project, command }, { sendNotification }) => {
const buildId = `build-${Date.now()}`;
const status: BuildStatus = {
id: buildId,
project,
status: "running",
progress: 0,
startedAt: new Date().toISOString(),
logs: [`开始构建: ${command}`],
};
buildStore.set(buildId, status);
// 模拟构建步骤
const steps = ["依赖安装", "编译", "测试", "打包", "部署"];
for (let i = 0; i < steps.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
status.progress = Math.round(((i + 1) / steps.length) * 100);
status.logs.push(`[${steps[i]}] 完成 (${status.progress}%)`);
// 推送资源更新通知
if (sendNotification) {
await sendNotification({
method: "notifications/resources/updated",
params: {
uri: `build://status/${buildId}`,
},
});
}
}
status.status = "success";
status.logs.push("构建完成 ✓");
return {
content: [{
type: "text" as const,
text: `构建 ${buildId} 已完成\n项目: ${project}\n状态: 成功\n日志:\n${status.logs.map(l => ` - ${l}`).join("\n")}`,
}],
};
}
);
MCP Tasks是协议规范2025-11-25版本引入的异步任务原语,专为长时间运行的操作设计:
flowchart TB
subgraph 传统Tool调用
T1[Client调用Tool] --> T2[同步等待]
T2 --> T3[超时风险]
end
subgraph MCP Tasks模式
M1[Client发起Task] --> M2[Server返回taskToken]
M2 --> M3[Client轮询状态]
M3 --> M4{Task完成?}
M4 -->|否| M3
M4 -->|是| M5[获取结果]
end
style T3 fill:#ff6b6b,color:#fff
style M5 fill:#51cf66,color:#fff
关键差异:
| 特性 | 普通Tool | MCP Task |
|---|---|---|
| 执行模式 | 同步阻塞 | 异步轮询 |
| 超时风险 | 高(客户端等待) | 无(立即返回token) |
| 状态追踪 | 仅进度通知 | 完整生命周期 |
| 中断恢复 | 需自行实现 | 协议原生支持 |
| 适用时长 | < 30秒 | 分钟到小时级 |
stateDiagram-v2
[*] --> pending: 创建Task
pending --> running: 开始执行
running --> completed: 正常完成
running --> failed: 执行出错
running --> cancelled: 用户取消
running --> input_required: 需要用户输入
input_required --> running: 用户提供输入
completed --> [*]
failed --> [*]
cancelled --> [*]
input_required 状态是MCP Tasks的精髓——Server可以在执行过程中暂停,向用户请求额外信息后继续。这实现了人机协作的异步工作流。
在 server.ts 中添加完整的Task管理系统:
// Task管理系统
interface McpTask {
id: string;
status: "pending" | "running" | "completed" | "failed" | "cancelled" | "input_required";
progress: number;
total: number | null;
result: unknown;
error: string | null;
createdAt: number;
updatedAt: number;
metadata: Record<string, unknown>;
}
const taskStore = new Map<string, McpTask>();
let taskIdCounter = 0;
function createTask(metadata: Record<string, unknown> = {}): McpTask {
const task: McpTask = {
id: `task-${++taskIdCounter}`,
status: "pending",
progress: 0,
total: null,
result: null,
error: null,
createdAt: Date.now(),
updatedAt: Date.now(),
metadata,
};
taskStore.set(task.id, task);
return task;
}
function updateTask(id: string, updates: Partial<McpTask>) {
const task = taskStore.get(id);
if (!task) throw new Error(`Task ${id} 不存在`);
Object.assign(task, updates, { updatedAt: Date.now() });
taskStore.set(id, task);
return task;
}
// 工具五:启动异步代码审计Task
server.tool(
"async_audit",
"启动异步代码审计任务,立即返回Task ID,可后续查询进度",
{
target: z.string().describe("审计目标(目录或文件)"),
depth: z.enum(["quick", "standard", "deep"]).default("standard").describe("审计深度"),
categories: z.array(z.enum(["security", "performance", "maintainability", "style"]))
.default(["security", "performance"])
.describe("审计类别"),
},
async ({ target, depth, categories }) => {
const task = createTask({ target, depth, categories });
// 异步启动后台任务
runAuditInBackground(task.id, target, depth, categories);
return {
content: [{
type: "text" as const,
text: `✅ 审计任务已创建\n\nTask ID: ${task.id}\n目标: ${target}\n深度: ${depth}\n类别: ${categories.join(", ")}\n\n使用 task_status 工具查询进度,使用 task_result 获取结果。`,
}],
};
}
);
async function runAuditInBackground(
taskId: string,
target: string,
depth: string,
categories: string[]
) {
const task = taskStore.get(taskId)!;
// 根据深度确定扫描步骤数
const stepsPerCategory = { quick: 5, standard: 12, deep: 25 } as const;
const totalSteps = categories.length * stepsPerCategory[depth as keyof typeof stepsPerCategory];
updateTask(taskId, { status: "running", total: totalSteps, progress: 0 });
const findings: Array<{ category: string; severity: string; file: string; message: string }> = [];
try {
let step = 0;
for (const category of categories) {
const steps = stepsPerCategory[depth as keyof typeof stepsPerCategory];
for (let i = 0; i < steps; i++) {
step++;
// 检查取消状态
const currentTask = taskStore.get(taskId)!;
if (currentTask.status === "cancelled") {
return; // 已被外部取消
}
// 模拟扫描逻辑
await new Promise((resolve) => setTimeout(resolve, 200));
if (Math.random() > 0.75) {
findings.push({
category,
severity: ["critical", "high", "medium", "low"][Math.floor(Math.random() * 4)],
file: `${target}/module-${Math.floor(Math.random() * 50)}.ts`,
message: `${category} 相关: 发现潜在${["SQL注入", "内存泄漏", "循环依赖", "硬编码密钥", "未处理异常"][Math.floor(Math.random() * 5)]}问题`,
});
}
updateTask(taskId, { progress: step });
}
}
updateTask(taskId, {
status: "completed",
progress: totalSteps,
result: {
target,
depth,
findings,
summary: {
total: findings.length,
critical: findings.filter(f => f.severity === "critical").length,
high: findings.filter(f => f.severity === "high").length,
medium: findings.filter(f => f.severity === "medium").length,
low: findings.filter(f => f.severity === "low").length,
},
completedAt: new Date().toISOString(),
},
});
} catch (error) {
updateTask(taskId, {
status: "failed",
error: error instanceof Error ? error.message : String(error),
});
}
}
// 工具六:查询Task状态
server.tool(
"task_status",
"查询异步任务的当前状态和进度",
{
taskId: z.string().describe("任务ID"),
},
async ({ taskId }) => {
const task = taskStore.get(taskId);
if (!task) {
return {
content: [{ type: "text" as const, text: `Task ${taskId} 不存在` }],
isError: true,
};
}
const elapsed = Math.round((Date.now() - task.createdAt) / 1000);
const progressPercent = task.total ? Math.round((task.progress / task.total) * 100) : 0;
const progressBar = "█".repeat(Math.floor(progressPercent / 5)) + "░".repeat(20 - Math.floor(progressPercent / 5));
return {
content: [{
type: "text" as const,
text: `📋 Task: ${task.id}\n状态: ${task.status}\n进度: ${progressBar} ${progressPercent}%\n耗时: ${elapsed}秒\n${task.error ? `错误: ${task.error}` : ""}`,
}],
};
}
);
// 工具七:获取Task结果
server.tool(
"task_result",
"获取已完成异步任务的完整结果",
{
taskId: z.string().describe("任务ID"),
},
async ({ taskId }) => {
const task = taskStore.get(taskId);
if (!task) {
return {
content: [{ type: "text" as const, text: `Task ${taskId} 不存在` }],
isError: true,
};
}
if (task.status !== "completed") {
return {
content: [{
type: "text" as const,
text: `Task ${taskId} 当前状态为 ${task.status},尚未完成。请使用 task_status 查询进度。`,
}],
isError: true,
};
}
return {
content: [{
type: "text" as const,
text: `📊 审计报告 — ${task.id}\n\n${JSON.stringify(task.result, null, 2)}`,
}],
};
}
);
// 工具八:取消Task
server.tool(
"task_cancel",
"取消正在运行的异步任务",
{
taskId: z.string().describe("要取消的任务ID"),
},
async ({ taskId }) => {
const task = taskStore.get(taskId);
if (!task) {
return {
content: [{ type: "text" as const, text: `Task ${taskId} 不存在` }],
isError: true,
};
}
if (task.status !== "running" && task.status !== "pending") {
return {
content: [{
type: "text" as const,
text: `Task ${taskId} 状态为 ${task.status},无法取消。只能取消 pending 或 running 状态的任务。`,
}],
};
}
updateTask(taskId, { status: "cancelled" });
return {
content: [{
type: "text" as const,
text: `✅ Task ${taskId} 已取消`,
}],
};
}
);
将前面所有章节的内容整合为一个完整的MCP Server,支持以下工作流:
flowchart TB
subgraph "Claude Code客户端"
U[用户发起审计请求] --> R{选择模式?}
R -->|快速| S1[batch_process<br>同步+进度]
R -->|标准| S2[code_scan<br>可中断+日志]
R -->|深度| S3[async_audit<br>异步Task]
end
subgraph "MCP Server"
S1 --> P1[进度通知<br>notifications/progress]
S2 --> P2[取消处理<br>AbortSignal + 检查点]
S3 --> P3[后台引擎<br>Task生命周期管理]
P3 --> TS[task_status<br>task_result<br>task_cancel]
end
subgraph "数据层"
P3 --> FS[文件系统检查点]
P3 --> MEM[内存Task存储]
end
style S1 fill:#74c0fc
style S2 fill:#ffa94d
style S3 fill:#69db7c
以下是完整的、可直接运行的服务器代码。将前面章节的所有代码片段合并为一个文件:
// server.ts - 生产级代码审计MCP Server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { writeFileSync, readFileSync, existsSync, unlinkSync } from "fs";
// ==================== Task管理系统 ====================
interface McpTask {
id: string;
status: "pending" | "running" | "completed" | "failed" | "cancelled";
progress: number;
total: number | null;
result: unknown;
error: string | null;
createdAt: number;
updatedAt: number;
}
const taskStore = new Map<string, McpTask>();
let taskIdCounter = 0;
function createTask(): McpTask {
const task: McpTask = {
id: `audit-${++taskIdCounter}-${Date.now().toString(36)}`,
status: "pending",
progress: 0,
total: null,
result: null,
error: null,
createdAt: Date.now(),
updatedAt: Date.now(),
};
taskStore.set(task.id, task);
return task;
}
// ==================== 检查点管理 ====================
const CHECKPOINT_DIR = "/tmp/mcp-audit";
function saveCheckpoint(taskId: string, data: object) {
try {
writeFileSync(`${CHECKPOINT_DIR}-${taskId}.json`, JSON.stringify(data));
} catch { /* 静默失败 */ }
}
function loadCheckpoint(taskId: string): object | null {
try {
if (!existsSync(`${CHECKPOINT_DIR}-${taskId}.json`)) return null;
return JSON.parse(readFileSync(`${CHECKPOINT_DIR}-${taskId}.json`, "utf-8"));
} catch { return null; }
}
// ==================== MCP Server ====================
const server = new McpServer({
name: "code-audit-engine",
version: "2.0.0",
});
// ----- 工具1: 同步快速扫描(带进度) -----
server.tool(
"quick_scan",
"快速同步扫描,带实时进度反馈,适合小型项目",
{ path: z.string(), rules: z.array(z.string()).default(["security"]) },
async ({ path, rules }, { sendNotification }) => {
const files = Array.from({ length: 15 }, (_, i) => `${path}/file-${i}.ts`);
const issues: string[] = [];
for (let i = 0; i < files.length; i++) {
await new Promise(r => setTimeout(r, 400));
if (Math.random() > 0.7) issues.push(`[${rules[0]}] ${files[i]}: 发现问题`);
await sendNotification?.({
method: "notifications/progress",
params: { progressToken: "qs", progress: i + 1, total: files.length },
});
}
return {
content: [{ type: "text" as const, text: `扫描 ${files.length} 文件,发现 ${issues.length} 问题\n${issues.join("\n")}` }],
};
}
);
// ----- 工具2: 可中断深度扫描 -----
server.tool(
"deep_scan",
"深度扫描,支持中断恢复。中断后可使用 resume_scan 继续",
{
path: z.string(),
fileCount: z.number().default(50),
},
async ({ path, fileCount }, { sendNotification, signal }) => {
const files = Array.from({ length: fileCount }, (_, i) => `${path}/module-${i}.ts`);
const findings: Array<{ file: string; type: string; msg: string }> = [];
for (let i = 0; i < files.length; i++) {
if (signal?.aborted) {
saveCheckpoint("deep-scan", { lastIndex: i, findings, totalFiles: fileCount });
return {
content: [{
type: "text" as const,
text: `⏸ 已暂停于 ${i}/${fileCount}。使用 resume_scan 继续扫描。`,
}],
};
}
await new Promise(r => setTimeout(r, 150));
if (Math.random() > 0.75) {
findings.push({
file: files[i],
type: ["security", "perf", "style"][Math.floor(Math.random() * 3)],
msg: "需要人工审查",
});
}
await sendNotification?.({
method: "notifications/progress",
params: { progressToken: "ds", progress: i + 1, total: fileCount },
});
}
return {
content: [{
type: "text" as const,
text: `✅ 深度扫描完成\n扫描: ${fileCount} 文件\n发现: ${findings.length} 问题\n${JSON.stringify(findings, null, 2)}`,
}],
};
}
);
// ----- 工具3: 断点续传 -----
server.tool(
"resume_scan",
"从上次中断处继续深度扫描",
{},
async (_, { sendNotification, signal }) => {
const checkpoint = loadCheckpoint("deep-scan") as {
lastIndex: number; findings: Array<{ file: string; type: string; msg: string }>; totalFiles: number;
} | null;
if (!checkpoint) {
return { content: [{ type: "text" as const, text: "没有找到检查点,请先运行 deep_scan" }], isError: true };
}
const { lastIndex, findings, totalFiles } = checkpoint;
const files = Array.from({ length: totalFiles }, (_, i) => `src/module-${i}.ts`);
for (let i = lastIndex; i < totalFiles; i++) {
if (signal?.aborted) {
saveCheckpoint("deep-scan", { lastIndex: i, findings, totalFiles });
return {
content: [{ type: "text" as const, text: `⏸ 再次暂停于 ${i}/${totalFiles}` }],
};
}
await new Promise(r => setTimeout(r, 150));
if (Math.random() > 0.75) {
findings.push({
file: files[i],
type: "security",
msg: "续扫发现",
});
}
await sendNotification?.({
method: "notifications/progress",
params: { progressToken: "rs", progress: i + 1 - lastIndex, total: totalFiles - lastIndex },
});
}
// 清理检查点
try { unlinkSync(`${CHECKPOINT_DIR}-deep-scan.json`); } catch {}
return {
content: [{
type: "text" as const,
text: `✅ 续传扫描完成(从第 ${lastIndex} 个文件恢复)\n总计: ${findings.length} 个问题`,
}],
};
}
);
// ----- 工具4: 异步审计Task -----
server.tool(
"start_audit",
"启动异步审计任务,立即返回Task ID。适用于大型项目",
{
project: z.string(),
depth: z.enum(["quick", "standard", "deep"]).default("standard"),
},
async ({ project, depth }) => {
const task = createTask();
runAuditAsync(task.id, project, depth);
return {
content: [{
type: "text" as const,
text: `🚀 审计已启动\n\nTask ID: ${task.id}\n项目: ${project}\n深度: ${depth}\n\n→ 使用 check_task "${task.id}" 查看进度\n→ 使用 cancel_task "${task.id}" 取消`,
}],
};
}
);
async function runAuditAsync(taskId: string, project: string, depth: string) {
const stepMap = { quick: 10, standard: 30, deep: 60 };
const total = stepMap[depth as keyof typeof stepMap] ?? 30;
const task = taskStore.get(taskId)!;
Object.assign(task, { status: "running", total });
const findings: Array<{ severity: string; file: string; message: string }> = [];
try {
for (let i = 1; i <= total; i++) {
if (taskStore.get(taskId)!.status === "cancelled") return;
await new Promise(r => setTimeout(r, 300));
if (Math.random() > 0.7) {
findings.push({
severity: ["critical", "high", "medium", "low"][Math.floor(Math.random() * 4)],
file: `${project}/src/${["api", "db", "auth", "utils"][Math.floor(Math.random() * 4)]}/${["handler", "service", "model", "helper"][Math.floor(Math.random() * 4)]}.ts`,
message: ["硬编码凭证", "未参数化SQL查询", "缺少输入验证", "循环依赖", "未捕获Promise异常"][Math.floor(Math.random() * 5)],
});
}
Object.assign(task, { progress: i, updatedAt: Date.now() });
}
Object.assign(task, {
status: "completed",
result: {
project, depth,
totalFindings: findings.length,
critical: findings.filter(f => f.severity === "critical").length,
high: findings.filter(f => f.severity === "high").length,
findings: findings.slice(0, 20),
completedAt: new Date().toISOString(),
},
});
} catch (e) {
Object.assign(task, { status: "failed", error: String(e) });
}
}
// ----- 工具5: 查询Task -----
server.tool(
"check_task",
"查询异步任务状态",
{ taskId: z.string() },
async ({ taskId }) => {
const task = taskStore.get(taskId);
if (!task) return { content: [{ type: "text" as const, text: `Task ${taskId} 不存在` }], isError: true };
const pct = task.total ? Math.round((task.progress / task.total) * 100) : 0;
const bar = "█".repeat(Math.floor(pct / 5)) + "░".repeat(20 - Math.floor(pct / 5));
return {
content: [{
type: "text" as const,
text: `${bar} ${pct}%\n状态: ${task.status}\n${task.status === "completed" ? JSON.stringify(task.result, null, 2) : task.error ?? ""}`,
}],
};
}
);
// ----- 工具6: 取消Task -----
server.tool(
"cancel_task",
"取消异步任务",
{ taskId: z.string() },
async ({ taskId }) => {
const task = taskStore.get(taskId);
if (!task) return { content: [{ type: "text" as const, text: "不存在" }], isError: true };
if (task.status !== "running") return { content: [{ type: "text" as const, text: `状态 ${task.status} 不可取消` }] };
Object.assign(task, { status: "cancelled" });
return { content: [{ type: "text" as const, text: `✅ ${taskId} 已取消` }] };
}
);
// ==================== 启动 ====================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[code-audit-engine] MCP Server 已启动");
}
main().catch(console.error);
启动Claude Code后,按以下对话测试全部功能:
# 测试1:同步快速扫描
用 quick_scan 扫描 ./src 目录,检查 security 规则
# 测试2:可中断深度扫描
用 deep_scan 扫描 ./project 目录,50个文件
(等待进度到一半时按 Ctrl+C 中断)
# 测试3:断点续传
用 resume_scan 继续刚才中断的扫描
# 测试4:异步审计
用 start_audit 启动对 ./large-project 的 deep 深度审计
(记下返回的 Task ID,如 audit-1-m5x7k)
# 测试5:查询进度
用 check_task 查询 audit-1-m5x7k 的进度
# 测试6:取消任务
用 cancel_task 取消 audit-1-m5x7k
stdio传输仅限本地进程间通信。当MCP Server需要部署到远程服务器、被多个客户端共享时,需要HTTP传输。MCP规范从2025-03-26版本起,推荐Streamable HTTP替代已废弃的SSE传输。
// transport.ts - 双传输支持
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamablehttp.js";
export async function startWithTransport(server: any, mode: "stdio" | "http") {
if (mode === "stdio") {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[Server] stdio 模式启动");
} else {
const express = await import("express");
const app = express.default();
app.use(express.default.json());
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式
});
app.post("/mcp", async (req: any, res: any) => {
await transport.handleRequest(req, res, req.body);
});
app.get("/mcp", async (req: any, res: any) => {
// SSE 端点用于服务器推送通知
await transport.handleRequest(req, res);
});
await server.connect(transport);
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.error(`[Server] Streamable HTTP 模式启动: http://localhost:${PORT}/mcp`);
});
}
}
远程配置 .mcp.json:
{
"mcpServers": {
"remote-audit": {
"url": "http://your-server:3001/mcp",
"transport": "streamable-http"
}
}
}
长任务Server最怕资源耗尽。添加简单的并发限制:
const MAX_CONCURRENT_TASKS = 5;
const activeTasks = new Set<string>();
function canStartTask(): boolean {
return activeTasks.size < MAX_CONCURRENT_TASKS;
}
function registerTask(taskId: string) {
activeTasks.add(taskId);
}
function unregisterTask(taskId: string) {
activeTasks.delete(taskId);
// 清理超过1小时的已完成任务
const oneHourAgo = Date.now() - 3600000;
for (const [id, task] of taskStore) {
if (task.updatedAt < oneHourAgo && (task.status === "completed" || task.status === "failed" || task.status === "cancelled")) {
taskStore.delete(id);
}
}
}
// 定期报告内存使用
setInterval(() => {
const mem = process.memoryUsage();
const mb = (bytes: number) => `${Math.round(bytes / 1024 / 1024)}MB`;
console.error(`[监控] RSS: ${mb(mem.rss)} | Heap: ${mb(mem.heapUsed)}/${mb(mem.heapTotal)} | Tasks: ${taskStore.size}`);
}, 30000);
// 进程级异常捕获,防止崩溃
process.on("uncaughtException", (err) => {
console.error("[致命错误]", err.message);
// 将所有运行中的任务标记为失败
for (const [id, task] of taskStore) {
if (task.status === "running") {
Object.assign(task, { status: "failed", error: "服务器异常中断" });
}
}
});
process.on("unhandledRejection", (reason) => {
console.error("[未处理Promise拒绝]", reason);
});
flowchart LR
subgraph "用户决策"
A[发起扫描请求] --> B{任务规模?}
end
B -->|小型<15文件| C[quick_scan<br>同步+进度通知]
B -->|中型15-50文件| D[deep_scan<br>可中断+检查点]
B -->|大型>50文件| E[start_audit<br>异步Task引擎]
C --> F[实时进度条]
D --> G{是否中断?}
G -->|否| H[完整报告]
G -->|是| I[保存检查点]
I --> J[resume_scan<br>断点续传]
J --> H
E --> K[立即返回TaskID]
K --> L[check_task<br>轮询状态]
L --> M{完成?}
M -->|否| L
M -->|是| N[完整审计报告]
style C fill:#74c0fc
style D fill:#ffa94d
style E fill:#69db7c
style H fill:#b2f2bb
style N fill:#b2f2bb
[!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 等场景全覆盖。
