bes-mcp
An MCP server for searching and reading a local SQLite email archive, plus guarded attachment export and reply drafting/sending.
Search stored messages by keyword, sender/recipient, domains, dates, and attachment name, returning snippets or full bodies.
Show one message by stored ID, with bounded body/recipient/attachment fields and new-only filtering.
Retrieve a complete thread by stored ID or RFC822 Message-ID.
Summarize correspondence history for a contact address or domain.
Search attachment metadata and carrying-message summaries; never returns attachment bytes.
Report last sync run state and newest stored-message timestamp.
Export selected stored attachments to a host folder with overwrite, blocked-file-type, and startup-folder safeguards.
Draft an RFC5322 reply-all with recipients/body/attachments and a SHA-256 fingerprint for review.
Send only a reviewed draft when given the matching draft_id and fingerprint, with partial/unknown delivery guidance.
Provides tools to query and search a local SQLite database of archived email messages, including full-text search, message retrieval, threading, attachments, and contact history.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@bes-mcp search my mail archive for the PEN-100 quote thread from alice@example.com"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mailattic
mailattic turns years of Outlook PST archives plus an incremental IMAP sync into one local SQLite database with full-text search. AI agents query it through a CLI, a Python API, or an MCP server.
It was built for one long-running mailbox on a self-hosted mail server that only keeps recent mail.
Quick start
git clone https://github.com/sean3808/mailattic.git
cd mailattic
uv sync
uv run bes init-db --database .\mailattic.db
uv run bes import --database .\mailattic.db --maildir C:\path\to\maildir-account --account-email you@example.com
uv run bes search --database .\mailattic.db --keyword '"PEN-100" AND quote' --sender alice@example.com --limit 10 --json
uv run bes thread 1 --database .\mailattic.db --format jsonYou need Windows 11, Python 3.13, and uv. The import above reads a maildir and does not use Outlook. For PST import, read Requirements first.
Use bes from any directory
Agents in other projects call a global bes. Install it from the clone, then point it at your database:
pwsh -ExecutionPolicy Bypass -File scripts\reinstall-global.ps1
setx MAILATTIC_DB C:\path\to\mailattic.dbThe global bes is a snapshot of the build, so run the script again after you pull new code. Removal and the database path order are in docs/global-install-and-release.md. To register the MCP server with Claude Desktop, add a stdio server to claude_desktop_config.json whose command is uv run --project <repo> python -m mailattic.mcp_server (quit Claude Desktop from the tray before editing the file, then restart it).
Related MCP server: work-assistant
Why query an archive
Most email tools for AI agents are live proxies. The agent asks, the server connects to IMAP or Gmail, and the answer can only come from mail the server still holds. That fails for a quote from five years ago, for a mailbox that was only ever downloaded over POP, and for old mail on a server with a retention limit. The only copy of that mail is an archive on someone's disk, and that archive is what mailattic queries.
Three interfaces
Interface | For | Notes |
CLI | Coding agents and scripts on the same machine |
|
Python API | In-process callers |
|
MCP server | Sandboxed assistants that cannot run a host CLI | Six read tools, plus attachment export, reply draft, new-thread draft, and send |
All three run the same query code, so a date filter or a keyword means the same thing in each.
from pathlib import Path
from mailattic.query import SearchFilters, get_message, search_messages
results = search_messages(
database_path=Path("mailattic.db"),
filters=SearchFilters(keyword="contract", sender="alice@example.com"),
limit=20,
)
message = get_message(Path("mailattic.db"), results[0]["id"])Design choices
Keyword search (SQLite FTS5)
Business mail is searched by things that must match exactly: part numbers, order numbers, names, addresses, dates. A hit is a message that contains the words you searched for, so results are easy to check. The cost is that a query must use words that appear in the mail; there is no semantic recall. The choice was made for a single archive on one machine.
The agent drafts, a person approves, and the tool sends what was approved
A reply is written to a draft file first, and the draft has a SHA-256 fingerprint. An agent sends by passing that fingerprint, so a draft that changed after review is refused. At a real terminal a person can type an authorisation phrase instead. The tool cannot know whether the person was really asked. That part is the agent's duty, and the documentation says so.
Windows first
PST import reads through a running Outlook profile, and the test launcher is PowerShell. Maildir import (bes import --maildir) needs neither Outlook nor Windows-specific tools.
Limits
Developed and tested on Windows 11 only. Cross-platform use and support are not promised.
The real database, attachments, and credentials were never in version control. Addresses in docs and tests are placeholders such as
mailattic.testandexample.com.The commit history starts fresh here. The earlier private history is not included.
The repository ships no data, so you have to import your own. A small de-identified sample database is planned.
One database is one person's mailbox. Sharing one archive so that several mailboxes are searched together, needs new rules for "which addresses are us" and for access control, and none of that is built.
Not in scope: enterprise mail server, eDiscovery, GUI client, push notifications, semantic vector search, attachment OCR, task tracking.
Requirements
Windows 11, Python 3.13,
uvSQLite with FTS5
PST import only: a running Outlook with the target PST already mounted. The importer reads the store and never mounts, unmounts, or changes it. Run the smallest PST first; the run summary and the stop rules are in
docs/cli-reference.md.
Documentation
Question | Go to |
Every subcommand, flag, and query rule |
|
MCP tools and their limits | |
Database tables | |
Global install, release, and scheduling | |
Project rules, architecture map, behaviour contracts | |
Exact meaning of a project term | |
Why a design was chosen |
|
All documents |
Acknowledgements
The development workflow uses Matt Pocock's skills.
License
Available Tools
9 toolsmail_attachment_exportExport attachments to a host folderAIdempotent
Copy stored attachments into a host directory and report where each one landed. attachment_ids are the attachment_id values from mail_attachments (or attachments[].id from mail_show / mail_thread), 1 to 50 per call; a repeated id is reported once. dest_dir must be an absolute host path of at most 200 characters, inside a folder you can read back; it is created if missing. Never overwrites: a file already at the destination is left untouched and reported as skipped-existing. Never exports a file type Windows runs when opened (.exe, .bat, .cmd, .lnk, .vbs, .js, .ps1, .msi, ...): those are reported as skipped-blocked-type and the user must save them from Outlook. A dest_dir inside the Startup folder or an Office startup folder is refused. Each item reports status exported, skipped-existing, skipped-non-exportable (no stored payload, metadata only), skipped-blocked-type, or failed, with the absolute dest_path. Returns paths, never file content.
Inputs: dest_dir (required), attachment_ids (required). Returns: object
mail_attachmentsSearch attachment metadataARead-onlyIdempotent
Return one attachment-metadata result object with attachment rows, totals, and bounded carrying-message summaries. This tool never returns attachment content bytes; use mail_search or mail_show for message content.
Inputs: ext, limit, dedupe, to_date, +6 more. Returns: object
mail_contact_historySummarize correspondence historyARead-onlyIdempotent
Return one aggregated correspondence-summary object for exactly one contact address or one domain. This is not a message search or thread tool; use mail_search, mail_show, or mail_thread for individual messages.
Inputs: domain, contact, date_to, date_from, +2 more. Returns: object
mail_draft_replyDraft a reply to a messageA
Generate an RFC5322 reply-all draft for a stored message and write it to the MCP-dedicated draft folder. message_id is the RFC Message-ID (e.g. 'abc@example.com') from mail_show, not the numeric id from mail_search. Returns its draft_id, content fingerprint (SHA-256), and full draft text for review. The draft is NOT sent. Present the complete draft (Subject, To, Cc, Bcc, body, attachments, draft_id, and fingerprint) to the user for conversational review before calling mail_send.
Inputs: body_text (required), message_id (required), cc, to, +4 more. Returns: object
mail_searchSearch mail messagesARead-onlyIdempotent
Find message candidates by keyword and structured filters, returning a bare list of summary items or full-body items. Use mail_show for one stored id and mail_thread for a whole conversation or an RFC822 Message-ID string. keyword syntax: terms separated by spaces are ANDed; uppercase AND / OR / NOT, parentheses, and double-quoted phrases are honoured, and lowercase and/or/not are ordinary search terms. Results with a keyword are ordered by relevance, not by date, and each item reports match_field (subject, body, or null when the hit is in an indexed field this item does not show, such as recipients or attachment names) and match_in (new or quoted -- whether the hit the snippet shows is in the message's own new content or in its quoted history). snippet is centred on the hit when there is one, on the position covering the most of what was asked for.
Inputs: limit, sender, keyword, to_date, +10 more. Returns: result
mail_sendSend a reviewed draft emailA
Send a reviewed draft from the MCP-dedicated draft folder. Requires the draft_id and the matching SHA-256 fingerprint obtained from mail_draft_reply. Requires MAILATTIC_SMTP_ENV_FILE environment variable to point to a valid SMTP configuration file. Only call this after the user has reviewed the draft and explicitly confirmed to send. status='partial' means SMTP refused some recipients (see refused: address/code/reason): the message WAS sent to the accepted ones, so never call mail_send again for the same draft; create a new draft for the refused recipients only. An error does not always mean the mail was not sent: when it says the outcome is unknown or that the mail may already be delivered, verify delivery status before sending again; never re-draft and resend on your own.
Inputs: draft_id (required), fingerprint (required). Returns: object
mail_showShow one stored messageARead-onlyIdempotent
Return one stored message object by integer stored id, including bounded body and collection fields. Use mail_search to find valid ids and mail_thread when the identifier is an RFC822 Message-ID string or the full conversation is needed.
Inputs: message_id (required), new_only, body_max_chars, recipients_limit, +1 more. Returns: object
mail_sync_statusShow mail sync statusARead-onlyIdempotent
Report the state of the most recent synchronization run together with the timestamp of the newest stored message, in one response. Accepts no parameters and never starts or affects a synchronization run. The four fields -- last_sync_started_at, last_sync_ended_at, last_sync_status (never_synced, running, completed, or failed), and latest_message_at -- are always present; a field whose value is unknown is null rather than omitted. Reports facts only: no freshness verdict, staleness duration, or composite score is computed.
Inputs: no named parameters. Returns: object
mail_threadShow one mail threadARead-onlyIdempotent
Return one conversation object with messages plus total/returned counts, using either a stored id or an RFC822 Message-ID string. Use mail_search to find identifiers and mail_show when you only need one stored message.
Inputs: identifier (required), limit, new_only, full_body, +3 more. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 9 tools
Each tool targets a distinct resource and action: search vs. single-message show vs. whole-thread, plus separate tools for contact aggregation, attachment metadata, attachment export, drafting, sending, and sync status. The descriptions even cross-reference each other (e.g. 'use mail_show for one stored id', 'use mail_search for message content'), eliminating boundary confusion.
All nine tools share a uniform mail_ snake_case prefix with predictable descriptive suffixes (mail_search, mail_show, mail_thread, mail_draft_reply, mail_send, mail_attachment_export). The few noun-only names like mail_attachments and mail_sync_status still fit the same readable convention.
Nine tools is well-scoped for an email server spanning sync status, retrieval, attachments, contacts, and a gated draft/send workflow. Each tool earns its place with no redundant capabilities.
Read/search/thread/attachment/contact coverage plus a safe reply-and-send lifecycle is solid, but the surface only drafts replies and offers no compose-new-message, delete, move/folder, or read-state management. These are workable gaps rather than blocking ones for the stated reading-plus-reply purpose.
Maintenance
Related MCP Connectors
Give AI agents their own email inboxes: list, read and send mail via scoped, revocable OAuth grants.
Governed email for AI agents: a sandbox inbox to read and send from, human approval to go external.
Private, EU-hosted email for AI agents over JMAP: read, search, reply, draft, organize, send.
Connect any IMAP/SMTP mailbox to Claude & ChatGPT: search, read, sort, draft. Sends only with OK.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to manage multiple email accounts with secure credentials, local full-text search, thread-aware replies, and automation.4 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to search and read a local, provider-independent email archive, reconstruct contacts and interactions, and prepare draft responses without sending anything.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to search a local email archive using hybrid keyword and semantic search, and retrieve individual emails with thread context.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to query a local, read-only Apple Mail archive, letting them search messages by keyword, sender, subject, date or account and retrieve summaries, individual message bodies, pending action items and stored daily briefs. All tools are annotated read-only, so assistants can consult mail without sending, deleting, moving or editing anything.MIT