Notmuch
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.
README
mcp-server-notmuch
<p align="center"> <img src="logo.jpg" alt="mcp-server-notmuch logo" width="200"/> </p>
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.
What it does
- Searches and reads your mail (threads, single messages, attachments,
calendar invites, office documents, images) via the real
notmuchCLI. - Understands scopes: a named, pre-configured notmuch query (e.g. "personal mail" vs. "mailing lists") that every search is confined to unless you ask otherwise.
- Answers "what's still unanswered" and "who owes me a reply" (
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). - Optionally composes and revises plain-text drafts (
--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).
What it does not do
- It does not send mail. Ever.
- It does not modify your mail in any way unless you pass
--allow-tags(tagging) or--allow-drafts(writing a new file into a drafts maildir). Neither flag lets it touch existing messages' content. - It does not read or write outside the notmuch database, the configured
drafts maildir, and (only with
--allow-export) the configured export directory. - It does not require or use the notmuch2 Python bindings, so no compiler is needed to install it.
Install
From PyPI (once published)
$ uvx --prerelease=allow mcp-server-notmuch --help
From source
$ git clone https://github.com/hgn/mcp-server-notmuch
$ cd mcp-server-notmuch
$ uv pip install --prerelease=allow -e .
$ mcp-server-notmuch --help
The --prerelease=allow is required because this project pins a
pre-release of the mcp SDK (see SDK version below); it is
not optional.
System requirements
notmuch(the CLI, not just the library) onPATHor pointed to vianotmuch.binaryin the config.poppler-utils(pdftotext) to read PDF attachments. Without it,mail_read_attachmenton a PDF names the missing package.pandocorlibreofficeto read office documents (doc/docx/odt/rtf). Without either, the error names both options.- Optionally,
Pillow(pip install 'mcp-server-notmuch[image]') to let oversized image attachments be downscaled instead of refused. - Optionally, Gource to actually play back the log
mail_export_gourcewrites.
MCP client configuration
Claude Code
Read-only (the default: search and read tools only):
$ claude mcp add notmuch -- uvx --prerelease=allow mcp-server-notmuch
With drafts enabled (also allows revising/tagging as needed):
$ claude mcp add notmuch -- uvx --prerelease=allow mcp-server-notmuch --allow-drafts
Or by hand in .mcp.json:
{
"mcpServers": {
"notmuch": {
"command": "uvx",
"args": ["--prerelease=allow", "mcp-server-notmuch", "--allow-drafts"]
}
}
}
Claude Desktop
Add to claude_desktop_config.json (read-only default):
{
"mcpServers": {
"notmuch": {
"command": "uvx",
"args": ["--prerelease=allow", "mcp-server-notmuch"]
}
}
}
With drafts enabled:
{
"mcpServers": {
"notmuch": {
"command": "uvx",
"args": ["--prerelease=allow", "mcp-server-notmuch", "--allow-drafts"]
}
}
}
Configuration
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] |
default_limit |
20 |
Rows returned when a tool call omits limit. |
max_limit |
100 |
Hard ceiling on limit, whatever is requested. |
|
max_body_chars |
40000 |
Message body truncation point. | |
max_attachment_chars |
100000 |
Attachment/office/calendar text truncation point. | |
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".
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.
Tiers and tools
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. |
Read tier (always on)
| Tool | Purpose |
|---|---|
mail_search |
Search threads or messages, paged, with a truncation notice. |
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. |
Draft tier (--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. |
Tag tier (--allow-tags)
| Tool | Purpose |
|---|---|
mail_tag |
Add/remove tags on every message matching a query. |
Export tier (--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:
$ gource --log-format custom mail.gource -s 0.5 --key
limit (default 100000) matters: feeding Gource every mailing-list message
you've ever received produces an unwatchable animation, so scope the query
first.
Security model
This is a mail server handed to an LLM; message content is not trusted the way your own instructions are.
- Prompt injection. Message bodies, attachment text, calendar
summaries, and thread overview lines are third-party content, not
instructions.
render.pywraps 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. - Path confinement. The drafts maildir (
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), andmail_create_draft/mail_update_draftcannot be made to write outside the configured maildir with any combination of arguments. - No shell, ever. Every subprocess call is
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. - No content in diagnostics. Errors and progress go to stderr and are content-free (a byte count or a file path, never a message body).
- Never silently unfiltered. A
scopeargument that AND-composes with a query is always explicit; there is no hidden "search everything" fallback unless a scope literally namedallis configured.
notmuch query syntax
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.
SDK version
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.
Development
$ make # fmt + lint + test
$ make test # pytest (skips cleanly if notmuch is not installed)
$ make lint # ruff format --check + ruff check
$ make help # list all targets
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.
License
MIT, see LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.