先理解:审计和二次确认是写操作上线前提
后台系统的写操作可能影响真实业务,例如修改订单状态、触发补偿、导出敏感数据。MCP Server 一旦开放这类能力,就必须记录谁在什么时候请求了什么,以及高风险动作是否经过确认。
本篇实现审计日志和二次确认。高风险 Tool 不直接执行,只生成确认请求。
审计日志记录什么
至少记录调用时间、用户角色、工具名、参数摘要、结果状态、风险级别。不要记录完整敏感数据,避免审计日志本身变成泄露源。
二次确认不是形式主义
二次确认让 AI 从“直接执行者”变成“操作建议者”。人类确认后再执行,能显著降低误操作风险。
本篇解决生产风险:谁让 AI 做了什么、参数是什么、结果是什么,都必须可追溯。
审计模块
src/audit.ts:
ts
import { mkdirSync, appendFileSync } from "node:fs"
export function audit(event: {
userId: string
tool: string
args: unknown
result: "success" | "blocked" | "error"
reason?: string
}) {
mkdirSync("logs", { recursive: true })
appendFileSync("logs/audit.jsonl", JSON.stringify({
...event,
ts: new Date().toISOString(),
}) + "\n")
}接入普通 Tool
在 handler 中包一层:
ts
const ctx = currentContext()
try {
requireRole(ctx, ["readonly", "operator", "admin"])
const order = await getOrder(orderId)
audit({ userId: ctx.userId, tool: "get_order", args: { orderId }, result: "success" })
return { content: [{ type: "text", text: JSON.stringify(order, null, 2) }] }
} catch (err) {
audit({ userId: ctx.userId, tool: "get_order", args: { orderId }, result: "error", reason: String(err) })
throw err
}高风险动作只生成确认请求
退款不要做成直接执行 Tool,而是准备确认请求:
ts
server.tool(
"prepare_refund_request",
"准备退款申请,不直接执行退款。返回人工确认所需信息",
{ orderId: z.string(), amount: z.number(), reason: z.string() },
async ({ orderId, amount, reason }) => {
const ctx = currentContext()
requireRole(ctx, ["operator", "admin"])
audit({
userId: ctx.userId,
tool: "prepare_refund_request",
args: { orderId, amount, reason },
result: "blocked",
reason: "requires_manual_confirmation",
})
return {
content: [{
type: "text",
text: JSON.stringify({
status: "requires_manual_confirmation",
orderId,
amount,
reason,
}, null, 2),
}],
}
},
)验收
调用任意 Tool 后检查:
bash
tail -n 5 logs/audit.jsonl每条日志应包含:
userIdtoolargsresultts
高风险动作必须返回 requires_manual_confirmation,不能直接修改后台状态。

