The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the MCP Server Notmuch listing page.
An MCP server that exposes a local notmuch email database to an LLM client such as Claude. It is read-first: searching, reading, and understanding mail is always available; writing anything (drafts, tags, exported files) requires an explicit opt-in flag and is confined to clearly bounded locations.
It never sends mail. There is no send capability anywhere in this codebase, in any mode, with any flag. Drafts are written to a local maildir for you to review and send yourself in a real mail client.
notmuch CLI.mail_pending),
"has this come up before" (mail_related_threads), and gives a
token-cheap overview of a long thread before you read all of it
(mail_thread_overview).--allow-drafts),
tags messages (--allow-tags), or exports attachments and a
Gource visualization of your mailbox history to a
directory you name (--allow-export DIR).--allow-tags
(tagging) or --allow-drafts (writing a new file into a drafts maildir).
Neither flag lets it touch existing messages' content.--allow-export) the configured export
directory.The --prerelease=allow is required because this project pins a
pre-release of the mcp SDK (see SDK version below); it is
not optional.
notmuch (the CLI, not just the library) on PATH or pointed to via
notmuch.binary in the config.poppler-utils (pdftotext) to read PDF attachments. Without it,
mail_read_attachment on a PDF names the missing package.pandoc or libreoffice to read office documents (doc/docx/odt/rtf).
Without either, the error names both options.Pillow (pip install 'mcp-server-notmuch[image]') to let
oversized image attachments be downscaled instead of refused.mail_export_gource writes.Read-only (the default: search and read tools only):
With drafts enabled (also allows revising/tagging as needed):
Or by hand in .mcp.json:
Add to claude_desktop_config.json (read-only default):
With drafts enabled:
The server reads $XDG_CONFIG_HOME/mcp-server-notmuch/config.toml
(~/.config/mcp-server-notmuch/config.toml if XDG_CONFIG_HOME is unset),
or a path given with --config. A missing file is not an error: the server
falls back to the system notmuch binary and a single built-in scope
all with an empty query. Once a file exists, [scopes] is authoritative
and all is no longer implied.
See config.example.toml for a fully commented
reference file. Summary of every key:
| Section | Key | Default | Meaning |
|---|---|---|---|
[notmuch] | binary | "notmuch" | Path or bare name of the notmuch binary. |
config | notmuch's own default | Path passed as NOTMUCH_CONFIG. | |
[limits] | max_body_chars | unset (unlimited) | Message body truncation point; unset means full body every time. |
max_attachment_chars | unset (unlimited) | Attachment/office/calendar text truncation point; unset means full text every time. | |
max_image_bytes | 5242880 | Image size ceiling; downscaled with Pillow if larger, else refused. | |
[scopes] | default | (required once [scopes] exists) | Scope used when a tool call omits scope. |
[scopes.<name>] | query | — | A notmuch query ANDed with every search using this scope. |
description | "" | Shown by mail_list_scopes. | |
[drafts] | maildir | unset | Root of a maildir (cur/, new/, tmp/) for mail_create_draft. |
from | unset | From: header on every draft. | |
signature | unset | Plain-text signature file, appended on request. | |
wrap_columns | 72 | Hard-wrap width for drafted plain text. | |
max_total_attachment_bytes | 26214400 (25 MiB) | Ceiling on draft attachments' combined size. | |
[identity] | addresses | notmuch's user.primary_email/user.other_email | Your own address(es); used to exclude yourself from reply-all and to detect mail_pending direction="waiting". |
mail_create_draft/mail_update_draft refuse to run unless both
drafts.maildir and drafts.from are set. mail_pending direction="waiting" needs [identity] addresses (or a readable notmuch
user.primary_email) to know which address is "you".
Nothing is truncated by default. This is a local, fast mailbox, not a
rate-limited API: mail_search, mail_pending, mail_related_threads,
mail_list_addresses, mail_find_attachments and mail_export_gource all
return every matching row unless a tool call passes an explicit limit,
and message bodies/attachment text come back in full unless
limits.max_body_chars/limits.max_attachment_chars are set. Whenever an
explicit limit or char cap does cut something, the output says so:
"Showing N of TOTAL ..." plus a hint to omit limit to see everything.
There is no silent, invisible ceiling anywhere in this server.
Scope resolution: a tool's scope argument names a configured scope; its
query is ANDed with the caller's query, both sides parenthesized
((scope_query) and (user_query)), so an or on either side cannot leak
past the other. An unknown scope name is an error listing the configured
scopes; scope="all" is never silently unfiltered unless you define a
scope literally named all.
Four tiers. The read tier is always registered. The other three are registered only when their flag is passed — there is no "registered but refused" state, an unauthorized tool is simply absent from the tool list a client sees.
| Flag | Registers |
|---|---|
| (none) | Read tier: search, read, list, prepare — nothing is written. |
--allow-drafts | Draft tier: compose and revise local plain-text drafts. |
--allow-tags | Tag tier: add/remove tags on existing messages. |
--allow-export DIR | Export tier: write attachments/a Gource log into DIR. |
| Tool | Purpose |
|---|---|
mail_search | Search threads or messages, paged; returns everything unless limit caps it. |
mail_read_thread | Every message in a thread, oldest first. |
mail_thread_overview | One line per message (date/size/from), tree or flat layout, before reading a long thread in full. |
mail_related_threads | Heuristic "has this come up before" (subject + participant overlap). |
mail_pending | Threads you owe a reply on, or threads you're waiting on a reply to. |
mail_read_message | A single message's headers and body. |
mail_count | Cheap message/thread count for a query. |
mail_list_addresses | Resolve a name to the real address(es) behind it. |
mail_list_attachments | List one message's attachments. |
mail_read_attachment | Read one attachment: text, PDF, image, office document, or calendar invite. |
mail_find_attachments | Find attachments across a whole search (e.g. "all PDFs from 2025"). |
mail_list_scopes | List the configured scopes. |
mail_prepare_reply | Derive reply/reply-all/forward headers and quoted/forwarded body; writes nothing. |
--allow-drafts)| Tool | Purpose |
|---|---|
mail_create_draft | Compose a plain-text draft (optionally with attachments) into the configured maildir. |
mail_update_draft | Revise an existing draft in place; only the given fields change. |
--allow-tags)| Tool | Purpose |
|---|---|
mail_tag | Add/remove tags on every message matching a query. |
--allow-export DIR)| Tool | Purpose |
|---|---|
mail_save_attachment | Save one attachment's raw bytes into DIR. |
mail_export_gource | Write a Gource custom log of mailbox history into DIR. |
mail_export_gource writes one line per message,
timestamp\|username\|type\|path\|colour, sorted oldest first (Gource
requires this). path is folder/normalized-subject, so a whole reply
chain lands at one point in the tree; colour is a stable hash of the
folder name, so a folder keeps its colour across repeated exports. Play it
back with:
All matching messages are included by default; limit matters when the
query is broad, since feeding Gource every mailing-list message you've
ever received produces an unwatchable animation, so scope the query first
or set limit explicitly.
This is a mail server handed to an LLM; message content is not trusted the way your own instructions are.
render.py wraps every one of them in explicit
-----BEGIN/END UNTRUSTED EMAIL CONTENT----- markers with a notice that
nothing inside should be treated as a command, so no individual tool can
forget to do this.compose.py) and the export
directory (export.py) each resolve the target path and verify it is
still inside the configured root afterward. This catches a literal ..
and a symlink pointing outside the root (Path.resolve() follows
symlinks), and mail_create_draft/mail_update_draft cannot be made to
write outside the configured maildir with any combination of arguments.subprocess.run([...], shell=False) with an argv list; queries are passed as a single argv
element, never interpolated into a shell string or a notmuch query
string beyond normal AND/OR composition.scope argument that AND-composes
with a query is always explicit; there is no hidden "search everything"
fallback unless a scope literally named all is configured.limit or the config sets an explicit
limits.max_body_chars/limits.max_attachment_chars. Whenever one of
those does cut something, the output states the true total and how to
see the rest, never a silent cut.query/scope arguments accept full notmuch search syntax: from:,
to:, subject:, tag:, date: ranges, boolean and/or/not, and
more. See notmuch-search-terms(7) (man notmuch-search-terms) for the
complete reference.
Targets MCP spec 2026-07-28 and pins mcp==2.0.0b2, a pre-release of
the Python SDK built for that spec. Once the spec and a matching stable SDK
release ship, this pin moves to the stable release; until then, every
install (uv pip install, uvx) needs --prerelease=allow.
Tests build a small crafted maildir and run real notmuch commands against
it; nothing touches your real mail. CI runs on Python 3.11, 3.12 and 3.13
with notmuch installed via apt.
MIT, see LICENSE.