Skip to main content

MCP server exposing a local notmuch email database, read-first

Project description

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

  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.

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.

Project details


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-0.1.0.tar.gz (344.9 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-0.1.0-py3-none-any.whl (47.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: mcp_server_notmuch-0.1.0.tar.gz
  • Upload date:
  • Size: 344.9 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-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f229d9c257e88448f15b31c654500ae39f1b38c22de76fc87d9730e91715e18b
MD5 6c5a96c49f94404a8d3dd0e160af1581
BLAKE2b-256 f44b7f6dca268717c0798579ed6e41c4985884a9a93a69f98d109d094e8a0d64

See more details on using hashes here.

File details

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

File metadata

  • Download URL: mcp_server_notmuch-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 47.2 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-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6c89e772f44178baa24cba8c704aacc36833ebf5ad9517c86ba3aee46212bf78
MD5 47e3fcbddde23d184d307a3940443e97
BLAKE2b-256 fba88371caebb643af66494090658d7c157ed0dd5babf578a5fa018686a44ba9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page