Skip to main content
Glama
hgn

Notmuch

by hgn

mcp-server-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.

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

Related MCP server: notmuchproxy

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.

Install Server
A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hgn/mcp-server-notmuch'

If you have feedback or need assistance with the MCP directory API, please join our Discord server