loop-breaker
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@loop-breakercheck thread.json against my draft reply — should it send?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
loop-breaker

Stop your agent replying to an autoresponder, a bounce, or another bot.
npx @askeleven/loop-breaker thread.json --reply reply.txtAn agent that answers every message will answer an out-of-office. The out-of-office answers back. So does the agent. Point two agents at each other, or one agent at a helpdesk that acknowledges every email with a new ticket number, and they will talk all night, politely, at your expense and in your name. Nothing errors. Every message is accepted and delivered. loop-breaker is the check that runs before each reply: a CLI for pipelines, an MCP server for agents, and a library for everything else. It returns a verdict, the facts behind it, and the reason for every problem it finds.
Any channel: email, SMS, chat, webhooks. Zero dependencies. No signup, no account, no network.
What it checks
Before a reply, on the thread you pass in:
Check | Verdict |
Automatic reply: out-of-office, autoresponder, read receipt, "driving with Focus on" | Fail |
Delivery failure: a bounce from the mail system | Fail |
Bulk or list mail: List-Id, Precedence: bulk, "Reply STOP to opt out" | Fail |
No-reply sender: | Fail |
They asked you to stop: STOP, unsubscribe, "please stop emailing me" | Fail |
Same message again: they sent the same words twice, give or take a ticket number | Fail |
Your message echoed back: what you sent, returned to you | Fail |
You already sent this: your reply repeats one you sent on the thread | Fail |
Too many messages on the thread: default over 5 in 60 minutes | Fail |
Possibly an automatic reply: "We've received your request; ticket #4821 has been created" | Review |
Replies faster than typing: two or more replies that arrived at over 3 words a second | Review |
With the optional ledger, across every thread with the same counterparty:
Check | Verdict |
They asked you to stop on any thread or channel, at any time, until they write again | Fail |
Same message again: word for word what they sent on another thread | Fail |
Nobody there is reading: every reply from them in the last 7 days was automated | Fail |
Too many messages to them: default over 10 in 24 hours, across threads | Fail |
Keep means nothing was found. Review means a person should decide. Fail means do not reply.
How it tells a person from a machine
Email headers decide it when they are there. Auto-Submitted (RFC 3834), X-Autoreply,
Precedence, List-Id, X-Failed-Recipients, a multipart/report body and a null
Return-Path are only set by software, so they are certain. So are senders such as
mailer-daemon@ and noreply@.
Without headers, as on SMS and chat, it reads the words the sender wrote, not the quoted message under them. "I am out of the office until Monday" is likely automated. "We've received your request" is only possible, because people write that too, so it asks for review rather than failing. "I'm out of office next week, can we start on the 10th?" is a person, and is answered.
Then it watches how the other side behaves. People do not send the same message twice, repeat yours back, or answer 60 words in two seconds. Machines do.
Related MCP server: Meta Business MCP
What it does not do
It does not send, delete or change anything. It says whether to reply. The ledger only grows when you record to it.
It does not decide what to say instead. When it fails a reply to an out-of-office, the right move might be a follow-up after the return date. That is your agent's call, or a person's.
It does not read every language. Headers work everywhere. The wording rules are English, with out-of-office subject lines in French, German, Spanish, Italian and Portuguese.
It cannot see what it is not given. A loop that runs across threads needs the ledger; a reply that arrives seconds later needs times on the messages.
Example
$ npx @askeleven/loop-breaker thread.json
FAIL Do not reply
Thread 4 messages, 2 in, 2 out
Last message Automatic reply (possible)
"Thank you for your message. Our team will get back"
Sent 3 in the last 60 minutes (limit 5)
FAIL
Same message again
The last message is the same as an earlier one from them, with at most
a detail changed. People do not send the same message twice; machines
answering each message do. Do not reply.
You already sent this
Your reply is the same as a message you already sent on this thread.
Saying it again is the loop. Do not send it; if they have not
answered, a person should decide what happens next.
REVIEW
Possibly an automatic reply
...
Replies faster than typing
2 replies arrived faster than a person can type them, counting from
your message before each (over 3 words a second). The other side is
probably software. A person should look before this goes on.One message on its own, such as a raw email, is classified:
$ npx @askeleven/loop-breaker reply.eml
AUTOMATED Automatic reply, certain
header Auto-Submitted: auto-replied (automatic reply, certain)
subject Subject: Automatic reply: Your quote (automatic reply, likely)
text "I am out of the office until Monday 20 October with limited access" (automatic reply, likely)The thread
A JSON list of messages, oldest first, or an object with messages, and optionally
reply and counterparty:
{
"messages": [
{ "direction": "out", "at": "2026-10-16T14:40:00Z", "text": "Hi, following up on pricing." },
{ "direction": "in", "at": "2026-10-16T14:40:02Z", "raw": "From: ...\nAuto-Submitted: auto-replied\n\n..." }
],
"reply": "Thanks for getting back to me!",
"counterparty": "dana@acme.com"
}Each message has direction (in from them, out from you) and any of at (ISO 8601
with an offset), text, from, subject, headers, or raw, a whole email, from which
the rest is read. Pass whatever your channel has: SMS needs only direction, at and
text. When every message has a time they are put in order; without times the timing
checks are skipped, and the result says so.
Options
--reply <text|file> The message you are about to send.
--ledger <file> JSONL ledger, for checks across threads.
--counterparty <id> Who the thread is with: an address, number or id.
--similarity <0-1> Messages this alike count as the same. Default 0.85.
--max-sends <n> Most messages you send on a thread within --window. Default 5.
--window <minutes> Default 60.
--max-per-day <n> Most messages to the counterparty in 24 hours. Default 10.
--now <time> Check as of this time. Default now.
--json Machine-readable output.
--no-colour Plain text.Exit code 0 means OK to reply, 1 means do not reply (it failed, or a person should
review it), 2 means it could not run. So it gates a pipeline:
npx @askeleven/loop-breaker thread.json --reply reply.txt && ./send reply.txtThe ledger
A loop that opens a new thread every time, an autoresponse that arrives on five threads, an opt-out sent by SMS to an agent that also emails: none of these are in the thread you are replying to. The ledger is an append-only JSONL file with one line per message sent or received, which every check can read.
npx @askeleven/loop-breaker record --ledger loops.jsonl --counterparty dana@acme.com --direction in reply.eml
npx @askeleven/loop-breaker record --ledger loops.jsonl --counterparty dana@acme.com --direction out
npx @askeleven/loop-breaker thread.json --ledger loops.jsonl --counterparty dana@acme.comIt never stores what a message said. A line holds who, when, which way, what kind of message it was, whether it was an opt-out, an optional thread id, and a 16-character hash of the normalized words, which is enough to spot the same message again and nothing more:
{"ts":"2026-10-16T14:01:05.000Z","counterparty":"dana@acme.com","direction":"in","thread":"t1","fp":"9f2c41d07ab3e815","kind":"auto_reply"}Each record is one append of one line, so several processes can share a ledger.
For agents: MCP server
npx @askeleven/loop-breaker mcp --ledger loops.jsonlIt serves check_thread (the thread and your reply, with a counterparty for the ledger
checks) and classify_message (one message). With --ledger it also serves
record_message, and tells the model to record every message sent and received. The
server tells the model never to reply when the verdict is fail, to stop on every thread
after an opt-out, to show review reasons to a person, and not to reword a repeated reply
to get past the check: the loop is the problem, not the wording.
Claude Code
claude mcp add loop-breaker -- npx -y @askeleven/loop-breaker mcp --ledger ~/.loop-breaker.jsonlClaude Desktop, Cursor, and other clients that take a JSON config:
{
"mcpServers": {
"loop-breaker": {
"command": "npx",
"args": ["-y", "@askeleven/loop-breaker", "mcp", "--ledger", "/path/to/loops.jsonl"]
}
}
}As a library
import { checkThread, readLedger, recordMessage } from '@askeleven/loop-breaker'
const { events } = await readLedger('loops.jsonl')
const result = checkThread(thread, { reply, counterparty: 'dana@acme.com', history: events })
if (result.verdict === 'keep') {
await send(reply)
await recordMessage('loops.jsonl', { counterparty: 'dana@acme.com', direction: 'out', message: reply })
}checkThread(messages, options) takes reply, counterparty, history, similarity,
maxSends, windowMinutes, wordsPerSecond, maxSendsPerDay, lookbackHours, and now
(a clock, for tests). Every reason has a stable code (auto_reply, bounce, opt_out,
repeated_inbound and so on) to match on, and a detail to show a person.
classifyMessage(message) returns the kind (person, auto_reply, bounce, bulk,
no_reply), the confidence, whether it is an optOut, and the signals behind it.
parseEmail(raw), similarity(a, b) and isOptOut(text) are exported too.
Privacy
Nothing leaves your machine. There is no network code. The ledger holds no message text.
Requirements
Node 20 or newer. No dependencies.
Why we built it
AskEleven runs AI employees that email and text candidates and customers for small businesses. An agent that answers every message on its own needs to know which messages were not written by anyone. This is that check, run before each reply.
Contributing
Issues and pull requests welcome, especially:
Autoresponders it misses, with the headers and the text (anonymised). These matter most.
People it mistakes for machines. A normal message a person sent that it fails.
Out-of-office wording in other languages.
MCP clients where the server does not work as documented.
Run the tests with npm test. They use a fixed clock and no network, so they are offline
and fast.
License
MIT. See LICENSE.
Related MCP Connectors
Email intelligence for agents: MX, SPF/DKIM/DMARC, disposable/role checks, and a 0-100 score.
Check an email before your AI agent sends it: allow, review or block with reasons.
Authenticated email gateway for AI agents — per-agent inboxes, HITL approval, SPF/DKIM verified.
Authenticated email gateway for AI agents — per-agent inboxes, HITL approval, SPF/DKIM verified.
Related MCP Servers
- AlicenseAqualityDmaintenance▎ Check an email before an AI agent sends it: send / ask a human / block — with machine reason codes and prompt-injection detection.22MIT
- FlicenseNot gradedqualityDmaintenanceCompliance intelligence layer for AI agents sending WhatsApp Business messages. Prevents account suspensions by validating Meta's rules (care windows, opt-outs, rate limits) in real-time before every send.1-

EVIDIQ Envelopeofficial
FlicenseNot gradedqualityBmaintenanceEnables agents to verify the authenticity of inbound email messages via SPF, DKIM, DMARC, and ARC checks, detect sender spoofing and malicious links/attachments, and obtain signed, verifiable attestations of the verdict.1-- FlicenseNot gradedqualityBmaintenanceEnables AI agents to score their outgoing responses against groundedness and prompt-injection risks mid-turn, returning allow, warn, or block verdicts before the response reaches the user.17 npm-