Read-first access to a local notmuch email database (search, read threads and messages, list scopes, prepare replies). Never sends mail.
mcp-server-notmuch demonstrates strong naming conventions, detailed descriptions, and comprehensive parameter documentation. All 16 tools follow verb_noun patterns (mail_search, mail_read_thread, etc.) and include descriptions ranging from 60 - 250 characters. Input schemas are fully specified with proper JSON types (string, integer, array) and nullable markers. However, output schemas are not formally documented, responses are described narratively but lack explicit response structure definitions. Error handling is minimal: tools reference ValueError but do not provide actionable recovery guidance. The server correctly uses tool annotations (readOnlyHint=true for 12/16 tools) per current spec. Composition is strong: tools chain well (mail_search → mail_read_thread, mail_find_attachments → mail_read_attachment), and most parameters accept human-friendly identifiers (e.g., message_id with or without 'id:' prefix, thread_id with or without 'thread:' prefix). The design is read-first with explicit write-tier registration (allow_drafts, allow_tags flags), reducing blast radius.
Prepare a forward, with the original message as a quoted block and no pre-filled recipients (the caller chooses them).
Prepare a reply or reply-all, with quoted body and derived headers.
Create a draft message in the configured maildir, or abort with a detailed error.
Export a notmuch query as a Gource-format custom log file for temporal/evolutionary visualization.
Search for messages with attachments matching a type filter, returning message IDs and a summary of each matching attachment.
Search for all unique email addresses matching a query, grouped by frequency and last occurrence, or list all addresses ever seen in the mailbox.
List all available scopes (saved searches) and their queries.
Output schemas are not formally documented. Tools include narrative descriptions of what is returned (e.g., 'Return every message in a thread') but lack explicit JSON schema definitions of response structure. LLMs cannot reliably parse unstructured output or plan downstream tool calls without knowing returned field types and names.
Error handling provides no recovery guidance. ValueError exceptions reference invalid values but do not suggest corrective actions. For example, mail_search raises 'response_format must be concise or detailed' but does not indicate what to do next or whether retry is safe.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-22 | C | 62 | 2026-07-28+ | v2 |
Return any unsent mail pending reply or action: messages with no response from the user yet, or messages from the user awaiting the recipient's reply.
Return the decoded content of a single attachment by ID (a MIME part identifier, available from mail_find_attachments or mail_read_message).
Return a single message, with full headers and full body.
Return every message in a thread, oldest first, with '--- [N/M] ---' separators. Use this once mail_search has identified a thread_id and you need the conversation in order. For a single message, use mail_read_message instead. For a long thread, consider calling mail_thread_overview first: it shows one line per message (date, size, from, subject changes) so you can pick the one or two messages worth reading in full here, instead of pulling the whole thread.
Find related threads by subject or author, for thread drift / tangents.
Decode and save a single attachment to the configured export directory.
Search notmuch and return a paged summary, at thread or message granularity. Use granularity="threads" for "what conversations are there about Y": each row is one thread, dated by its newest (or oldest) matched message, with an authors column that can mix matched and unmatched participants (see the legend in the output). Use granularity="messages" for "the last mail from X" or "how many mails did X write": each row is one matching message with its own date, from and subject. Do not use this to fetch full bodies (use mail_read_message / mail_read_thread) or to turn a person's name into addresses first (use mail_list_addresses; from:<name> also matches notification mail that merely mentions the name).
Add or remove tags on one or more messages (matched by a query). Returns the count of messages modified.
One line per message: index, date, size, from, and a marker when the subject changes from the thread's root subject. Use this before mail_read_thread on any thread you have not looked at yet, especially a long one: it costs a fraction of the tokens and lets you decide which one or two messages to actually read in full, instead of pulling all eighty. Do not use it in place of mail_read_thread when you already know you need the full content.
mail_related_threads description is too brief (45 characters). It does not explain WHEN to use this tool vs. mail_search, what 'drift / tangents' means, or what criteria ('subject' vs 'author') are appropriate for different scenarios.
mail_list_scopes description is under-specified. It does not explain what 'scopes' are, why an agent would call this tool, or what structure the returned data has. The description reads like a method signature rather than user-facing intent.
mail_tag and mail_create_draft lack explicit idempotency documentation. mail_tag accepts a query and modifies N messages matching it; if called twice with the same query, it may silently re-tag already-tagged messages. Agents need to know whether this is idempotent or has side effects on retries.
No confirmation or dry-run pattern for destructive operations. mail_create_draft and mail_save_attachment write to the file system; mail_tag modifies email state. Agents can accidentally create unwanted drafts or tag thousands of messages. Consider adding a dry_run parameter to estimate scope before executing.