First: this is a genuinely well-built server — the append-only audit log, the rate limiter, and input validation put it ahead of most MCP servers I've read. This issue is about the one gap that safety net doesn't cover.
The scenario. An agent calls send_email. The SMTP conversation is slow; the MCP client times out and retries — or the agent itself decides the call failed and tries again. The recipient gets two identical emails. With bulk or scheduled sends, the same shape amplifies.
Why nothing catches it today (all from the current code):
send_email's schema (src/tools/send.tool.ts) has no parameter a caller could use to mark two attempts as the same send — no message id, no client token.
smtp.service.ts calls transport.sendMail({...}) without a messageId, so nodemailer generates a fresh random Message-ID per attempt (its documented default). Two attempts at the same email are, on the wire, two unrelated messages.
- The audit log (
src/safety/audit.ts) is append-only and written after the send — it records the duplicate faithfully but can't prevent it. (And it redacts body, correctly, which means it also can't be used for exact-payload matching after the fact.)
SMTP itself has no idempotency mechanism, so there's no server on the other side that can rescue this — the guard has to live client-side, which is exactly where this repo already keeps its other safety machinery.
Two fixes that fit the existing design, smaller first:
- Accept an optional
messageId param on send_email/reply_email/forward_email and pass it through to sendMail. A caller that supplies a stable id gets: major receivers (Gmail-class) deduplicating an identical Message-ID arriving twice, and at minimum a traceable identity across attempts in your audit log. No behavior change for callers who omit it.
- (Larger, optional) a pre-send check against the audit log — same account + recipients + subject within a short window → refuse without an explicit override. Your log already captures everything needed except the intent to consult it before sending.
I'm happy to send a PR for the first one — it's small and I'd keep it to exactly that scope.
Disclosure: I work on open-source retry-safety tooling (once-kernel, effectfence), which is how I came to be reading send paths in MCP servers. This issue stands on its own either way — the fix above needs none of my code.
First: this is a genuinely well-built server — the append-only audit log, the rate limiter, and input validation put it ahead of most MCP servers I've read. This issue is about the one gap that safety net doesn't cover.
The scenario. An agent calls
send_email. The SMTP conversation is slow; the MCP client times out and retries — or the agent itself decides the call failed and tries again. The recipient gets two identical emails. Withbulkor scheduled sends, the same shape amplifies.Why nothing catches it today (all from the current code):
send_email's schema (src/tools/send.tool.ts) has no parameter a caller could use to mark two attempts as the same send — no message id, no client token.smtp.service.tscallstransport.sendMail({...})without amessageId, so nodemailer generates a fresh random Message-ID per attempt (its documented default). Two attempts at the same email are, on the wire, two unrelated messages.src/safety/audit.ts) is append-only and written after the send — it records the duplicate faithfully but can't prevent it. (And it redactsbody, correctly, which means it also can't be used for exact-payload matching after the fact.)SMTP itself has no idempotency mechanism, so there's no server on the other side that can rescue this — the guard has to live client-side, which is exactly where this repo already keeps its other safety machinery.
Two fixes that fit the existing design, smaller first:
messageIdparam onsend_email/reply_email/forward_emailand pass it through tosendMail. A caller that supplies a stable id gets: major receivers (Gmail-class) deduplicating an identical Message-ID arriving twice, and at minimum a traceable identity across attempts in your audit log. No behavior change for callers who omit it.I'm happy to send a PR for the first one — it's small and I'd keep it to exactly that scope.
Disclosure: I work on open-source retry-safety tooling (once-kernel, effectfence), which is how I came to be reading send paths in MCP servers. This issue stands on its own either way — the fix above needs none of my code.