mail-muncher
An email client for AI agents. Filtered, read-only Gmail delivered to disk as .eml + markdown, and served over MCP.
Install / Use
claude mcp add craigjmidwinter -- npx -y github:craigjmidwinter/mail-muncherIf the server publishes to npm under a different name, use that package instead — check the repo README.
MCP Server
Model Context Protocol server
Quality Score
Category
CommunicationSupported Platforms
Skill content
View source on GitHubmail-muncher
Give a program its own read-only mailbox, filtered down to exactly the mail it asked for, delivered as files on disk.
mail-muncher pulls messages from a mail provider, evaluates each one against
ordered rules, and writes the matches to a directory — byte-faithful .eml,
and optionally a markdown rendering with the headers as YAML frontmatter, the
body as text, and attachments extracted alongside. A rule can take its filter
input from a plain text file that some other program owns, which mail-muncher
re-reads at the start of every cycle. That other program changes one line in
that file, and the very next cycle delivers different mail — no config edit, no
restart, no redeploy.
It reads from any IMAP mailbox — Gmail, Fastmail, iCloud, Proton Bridge, a work account, your own server — or from Gmail's API with a read-only OAuth scope. It runs one-shot for cron, or as a polling daemon, or as a stdio MCP server an agent can query directly. Every mode emits the same machine-readable manifest of what it did, and no mode ever writes to your mailbox.
Two ways to connect a mailbox
Pick one before you install anything. Both are supported, and everything downstream — rules, formats, filenames, the archive layout, the MCP tools — is identical either way.
| | provider: imap | provider: gmail |
| --- | --- | --- |
| Setup time | ~2 min | ~10 min in the Google Cloud Console |
| What you register | nothing | your own Google Cloud project and Desktop-app OAuth client |
| Credential | an app password from your provider's own settings page | an OAuth token, scope gmail.readonly |
| How wide that credential is | a full mail credential. An app password can send and delete | read-only, and nothing else |
| Who enforces read-only | mail-muncher's own code | Google |
| Expiry | none | every 7 days on a Testing-mode consent screen; mail-muncher auth has to be re-run weekly |
| Where the secret lives | wherever your password manager already keeps it: password_cmd is run and its stdout is the password. There is deliberately no password key | token.json, mode 0600, written by mail-muncher auth |
| Which mailboxes | the folders you list in mailboxes:; [INBOX] by default | the whole Gmail account, minus Spam and Trash unless you ask for them |
| Works with | Gmail, Fastmail, iCloud, Proton Bridge, work accounts, self-hosted | Gmail only |
| Extra steps | none. There is no auth command on this path | mail-muncher auth, after docs/gmail-setup.md |
The ~2 min / ~10 min / 7 days above are the same numbers mail-muncher init
and the unconfigured-run guidance print, because they are the numbers that
decide this.
The read-only guarantee is real on both paths, but it is not the same guarantee, and flattening the two would be dishonest.
- Gmail: enforced by Google. The only scope requested is
gmail.readonly. The token that comes back is incapable of sending, deleting, labelling or modifying — not because mail-muncher declines to, but because Google will refuse the call. A bug in this program cannot reach your mailbox. - IMAP: enforced by mail-muncher. IMAP has no read-only credential to ask
for. An app password is a full mail credential; the protocol will happily let
its holder delete a folder. What mail-muncher does instead is refuse to: every
folder is opened with
EXAMINEand neverSELECT, every body is fetched withBODY.PEEK[]and neverBODY[](so mail is never marked read), and there is no code path anywhere in the provider that issuesSTORE,APPENDorEXPUNGE. Both belts are worn because a server is not obliged to protect a client from itself. That is a strong guarantee and an auditable one — it is just this program's guarantee, not your mail provider's.
If you have no specific reason to want the Gmail API, start with IMAP. It works on a Gmail account too, and it is the path the quickstart takes.
The problem
An automated process needs some mail. A job-search tracker wants replies from companies you applied to. A support bot wants messages from one vendor's domain. A research agent wants every newsletter from three publishers, as text it can actually read.
The usual answers are all bad. Hand the process your inbox credentials and it can read (and send, and delete) everything. Give it a mail API integration and you now maintain an OAuth flow, a sync cursor, MIME parsing, and a dedup story inside every process that wants mail. Or hard-code the filter into a config file, and every change to what it wants is a config edit and a redeploy.
mail-muncher splits that in half. It owns the credentials, the incremental sync, the parsing, and the dedup. The consuming program owns a text file listing what it wants and a directory it reads results from — and, if it prefers to ask rather than watch, a handful of MCP tools over that same directory.
The agent workflow
There are two supported shapes, and they compose. Pick by whether your agent runs on a loop of its own or waits to be asked.
- File drop — mail-muncher runs on a schedule and writes files; the agent reads the directory. Nothing calls anything. This is the shape below.
- Tool call — the agent talks to
mail-muncher mcpover MCP and asks questions directly: what am I subscribed to, what arrived, what does this thread say, fetch now. See Shape 2: tool call.
Both read the same archive, and running both at once is normal: a daemon fills the directory while the MCP server answers questions about it.
Shape 1: file drop
The loop is fully decoupled: mail-muncher never calls the agent, and the agent need never call mail-muncher. They share two paths on disk.
1. The agent declares what it wants. Append to a file it owns:
mkdir -p ~/.local/share/agent
cat >> ~/.local/share/agent/domains.txt <<'EOF'
# domains this agent is currently interested in
acme.com
globex.io
EOF
2. mail-muncher subscribes to that declaration. One rule, pointed at the file:
rules:
- name: agent-inbox
match:
from_domains_file: ~/.local/share/agent/domains.txt
dest: ~/mail/agent-inbox
formats: [eml, markdown]
3. Every cycle re-reads the file. Run it from cron, or leave the daemon running:
mail-muncher run # one cycle — the cron entrypoint
mail-muncher daemon --interval 5m # poll forever
4. Matched mail lands in dest as files the agent reads.
~/mail/agent-inbox/
└── 2026/
└── 07/
├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.eml
├── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.md
└── 1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/
└── offer.pdf
The .md is the consumable rendering — parse the frontmatter, feed the body
to a model, open the attachments from the sibling directory:
---
subject: 'Re: Your application for Senior Engineer'
from: Jane Doe <jane@acme.com>
from_address: jane@acme.com
from_addresses: [jane@acme.com]
to: [me@example.com]
to_addresses: [me@example.com]
date: 2026-07-28T09:15:00Z
message_id: <abc123@acme.com>
thread_id: 18fe9c0d1a2b3c4d
thread_id_source: provider
in_reply_to: <application-000@example.com>
account: personal
rule: job-search
labels: [INBOX]
attachments: [offer.pdf]
---
Hi there,
Thanks for applying.
## Attachments
- [offer.pdf](1785230100-a00d5c5e383a1c08-re-your-application-for-senior-engineer.attachments/offer.pdf)
thread_id is on every message and is never empty, so grouping a directory into
conversations is a sort on one field — no reference chains to reassemble.
5. Optionally, take the manifest instead of walking the tree. --json
writes a machine-readable record of the cycle to stdout, one object per account,
while every log line goes to stderr:
mail-muncher run --json 2>/dev/null | jq -r '.stored[].path'
Full contract: docs/manifest.md.
Three properties make this safe to put in an autonomous loop:
- Read-only by construction. Nothing in mail-muncher writes to a mailbox.
On Gmail that is Google's enforcement of the
gmail.readonlyscope; on IMAP it isEXAMINEandBODY.PEEK[]and no write path at all. Either way, whatever consumes the output — and whatever bug it has — cannot send, delete, or modify mail. See the comparison above for which of those two guarantees you are getting. - Idempotent delivery. A message's filename embeds a digest of
account + ":" + message id, so its destination path is a pure function of its identity. A file that is already there means "an earlier cycle stored this", and the sink writes nothing. Re-run, replay after losing state, crash mid-cycle, or overlap two cron invocations: the tree converges, and nothing is processed twice. - Deterministic routing. Rules are ordered and first-match-wins, so each
message is written by exactly one rule. Give each consumer its own rule and
its own
dest, and each gets a private mailbox nothing else writes into.
Delivery is files on disk, and nothing here listens on a network. The contract is the directory, with the manifest as an optional, machine-readable account of what changed.
Shape 2: tool call
mail-muncher mcp is a stdio MCP server over the mail already archived. The
agent asks; nothing is scheduled.
{
"mcpServers": {
"mail-muncher": {
"command": "/usr/local/bin/mail-muncher",
"args": ["mcp", "--config", "/Users/you/.config/mail-muncher/config.yml"]
}
}
}
Five tools:
| Tool | What it answers |
| --- | --- |
| list_rules | What am I collecting, and which senders am I subscribed to right now? Each from_domains_file is re-read on every call. |
| list_messages | What has arrived? Filter by rule, account, thread or date; optionally grouped into conversations. |
| search_messages | Where is the message that mentions X? Substring search over subject, sender, recipients, labels, attachment names and body. |
| read_message | One message in full — metadata, body, attachment names and sizes — and optionally its whole thread in order. |
| sync | Fetch new mail once, returning the same manifest run --json writes. |
It is read-only over mail: no tool sends, deletes, or modifies anything, and
sync — the only tool that changes anything at all — can only add files.
Filesystem access is jailed to the configured rule dest roots, so the config,
any stored credential, and the state directory are unreachable and unnamed.
An unconfigured mcp server starts anyway, and that is deliberate. If a
client launches mail-muncher mcp before there is a config, the server does
not exit — it completes the handshake, registers the same five tool names, and
answers every call with the setup guidance as a tool error, so the agent has
something to relay instead of "server failed to start". If you are wiring th
Truncated for display — read the full file on GitHub.
Related Skills
Agent-Reach
84.2kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
headroom
73.4kCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
ruflo
73.0k🌊 The original agent harness. Deploy intelligent multi-player swarms, coordinate autonomous workflows, and build conversational AI systems. Features adaptive memory, self-learning intelligence, federation, vector RAG integration, and native Claude Code / Codex / Hermes and many more Integrated
CowAgent
47.1kOpen-source super AI assistant & Agent Harness. Plans tasks, runs tools and skills, self-evolves with memory and knowledge. Multi-agent, multi-model, multi-channel. Lightweight, extensible, one-line install.
