Skip to main content

mcp-server-notmuch

mcp-server-notmuch logo

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 notmuch CLI.
  • 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) 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.
  • 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_gource writes.

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] 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.

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; 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.

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

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.

Security model

This is a mail server handed to an LLM; message content is not trusted the way your own instructions are.

  1. Prompt injection. Message bodies, attachment text, calendar summaries, and thread overview lines are third-party content, not instructions. 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.
  2. 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), and mail_create_draft/mail_update_draft cannot be made to write outside the configured maildir with any combination of arguments.
  3. 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.
  4. 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).
  5. Never silently unfiltered. A 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.
  6. Never silently truncated. This is a local, fast mailbox: result rows, message bodies and attachment text are returned in full unless a tool call passes an explicit 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.

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcp_server_notmuch-3.0.0.tar.gz (350.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_server_notmuch-3.0.0-py3-none-any.whl (48.4 kB view details)

Uploaded Python 3

File details

Details for the file mcp_server_notmuch-3.0.0.tar.gz.

File metadata

  • Download URL: mcp_server_notmuch-3.0.0.tar.gz
  • Upload date:
  • Size: 350.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mcp_server_notmuch-3.0.0.tar.gz
Algorithm Hash digest
SHA256 6775da7488d68ca33929fbf7555dca2cc4d2497014b6dcfd3681005afe64ac12
MD5 c64cb02fba598da3fe112dfc098fc653
BLAKE2b-256 e64f0fc4ca2d92fc3992eba7b7dbabdd1f409aeff9720d89829a62bc20d153cb

See more details on using hashes here.

File details

Details for the file mcp_server_notmuch-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: mcp_server_notmuch-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 48.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mcp_server_notmuch-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c291ddfcba23a56a7a9656e0081c1c8d498f9f893cb6802536b4fcf098a9fdb6
MD5 136df3304d6c632057af08e48f00288f
BLAKE2b-256 b55be3da4dc4577a12ddb4098bdebe91308e366cd67e5c54ab722760e5305af9

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

3.0.0 This release

2 files

2.0.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page