Fast MCP server for Apple Mail with FTS5 search index
This project has been archived.
The maintainers of this project have marked this project as archived. No new releases are expected.
Project description
JXA Mail MCP
A fast MCP (Model Context Protocol) server for Apple Mail, using optimized JXA (JavaScript for Automation) scripts with batch property fetching for 87x faster performance, plus an optional FTS5 search index for 700-3500x faster body search (~2ms vs ~7s).
Features
Email Tools (5 total)
| Tool | Purpose | Parameters |
|---|---|---|
list_accounts() |
List email accounts | - |
list_mailboxes(account?) |
List mailboxes | account (optional) |
get_emails(...) |
Unified email listing | account?, mailbox?, filter?, limit? |
get_email(id) |
Get single email with content | message_id |
search(query, ...) |
Unified search with FTS5 | query, scope?, limit? |
Unified get_emails() Filters
get_emails() # All emails (default)
get_emails(filter="unread") # Unread emails only
get_emails(filter="flagged") # Flagged emails only
get_emails(filter="today") # Emails received today
get_emails(filter="this_week") # Emails from last 7 days
Unified search() Scopes
search("invoice") # Search everywhere (uses FTS5)
search("john@example.com", scope="sender") # Sender only
search("meeting notes", scope="subject") # Subject only
search("deadline", scope="body") # Body content only
Installation
No installation required
Use pipx run to run directly from PyPI:
pipx run jxa-mail-mcp
With pipx (optional)
For faster startup, install globally:
pipx install jxa-mail-mcp
From source
Requires Python 3.13+ and uv:
git clone https://github.com/imdinu/jxa-mail-mcp
cd jxa-mail-mcp
uv sync
Quick Start
1. Add to Claude Code
{
"mcpServers": {
"mail": {
"command": "jxa-mail-mcp"
}
}
}
2. Build the Search Index (Optional but Recommended)
For instant body search (~2ms instead of ~7s), build the FTS5 index:
# Grant Full Disk Access to Terminal first:
# System Settings → Privacy & Security → Full Disk Access → Add Terminal
jxa-mail-mcp index --verbose
# → Indexed 22,696 emails in 1m 7.6s
# → Database size: 130.5 MB
3. Use with Claude
Once configured, you can search emails, get today's messages, find unread emails, and more through natural conversation.
CLI Commands
jxa-mail-mcp # Run MCP server (default)
jxa-mail-mcp serve # Run MCP server explicitly
jxa-mail-mcp --watch # Run with real-time index updates
jxa-mail-mcp index # Build search index from disk
jxa-mail-mcp status # Show index statistics
jxa-mail-mcp rebuild # Force rebuild index
Real-Time Index Updates
Use --watch to automatically update the index when new emails arrive:
jxa-mail-mcp --watch
# or
jxa-mail-mcp serve --watch
The file watcher monitors ~/Library/Mail/V10/ for .emlx changes and updates the index in real-time. Requires Full Disk Access.
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
JXA_MAIL_DEFAULT_ACCOUNT |
First account | Default email account |
JXA_MAIL_DEFAULT_MAILBOX |
INBOX |
Default mailbox |
JXA_MAIL_INDEX_PATH |
~/.jxa-mail-mcp/index.db |
Index database location |
JXA_MAIL_INDEX_MAX_EMAILS |
5000 |
Max emails per mailbox to index |
JXA_MAIL_INDEX_STALENESS_HOURS |
24 |
Hours before index is stale |
Claude Code Config
{
"mcpServers": {
"mail": {
"command": "jxa-mail-mcp",
"env": {
"JXA_MAIL_DEFAULT_ACCOUNT": "Work"
}
}
}
}
FTS5 Search Index
The FTS5 index makes search() ~100x faster by pre-indexing email content.
How It Works
- Build from disk:
jxa-mail-mcp indexreads.emlxfiles directly (~30x faster than JXA) - Startup sync: Index is synced with disk when server starts (fast, <5s)
- Real-time updates:
--watchflag enables file watcher for automatic updates - Fast search: Queries use SQLite FTS5 with BM25 ranking
Requirements
Building the index requires Full Disk Access for Terminal:
- Open System Settings
- Go to Privacy & Security → Full Disk Access
- Add and enable Terminal.app (or your terminal emulator)
- Restart terminal
The MCP server itself does NOT need Full Disk Access (uses disk sync).
Performance Comparison
| Operation | Without Index | With Index | Speedup |
|---|---|---|---|
| Body search | ~7,000ms | ~2-10ms | 700-3500x |
| Startup sync | 60s timeout | <5s | 12x |
| Initial index build | N/A | ~1-2 min | One-time |
| Index size | N/A | ~6 KB/email | - |
Real-World Benchmarks (22,696 emails)
| Query | Results | Time |
|---|---|---|
| "invoice" | 20 | 2.5ms |
| "meeting tomorrow" | 20 | 1.3ms |
| "password reset" | 20 | 0.6ms |
| "shipping confirmation" | 10 | 4.1ms |
Architecture
src/jxa_mail_mcp/
├── __init__.py # CLI entry point
├── cli.py # CLI commands (index, status, rebuild)
├── server.py # FastMCP server and MCP tools (5 tools)
├── config.py # Environment variable configuration
├── builders.py # QueryBuilder for constructing JXA scripts
├── executor.py # Async JXA script execution utilities
├── index/ # FTS5 search index module
│ ├── __init__.py # Exports IndexManager
│ ├── schema.py # SQLite schema, migrations (v3)
│ ├── manager.py # IndexManager class
│ ├── disk.py # Direct .emlx file reading + inventory
│ ├── sync.py # Disk-based state reconciliation
│ ├── search.py # FTS5 search functions
│ └── watcher.py # Real-time file watcher
└── jxa/
├── __init__.py # Exports MAIL_CORE_JS
└── mail_core.js # Shared JXA utilities library
Design Principles
- Disk-first sync: Fast filesystem scanning instead of slow JXA queries
- Consolidated tools: 5 focused tools instead of 13 redundant ones
- Builder pattern:
QueryBuilderconstructs optimized JXA scripts - Hybrid indexing: Disk reading for speed, state reconciliation for sync
- Async execution: All JXA calls use
asyncio.create_subprocess_exec - Type safety: Python type hints and TypedDict for clear API contracts
Hybrid Access Pattern
| Access Method | Use Case | Latency | When Used |
|---|---|---|---|
| JXA (Live) | Real-time ops, small queries | ~100-300ms | get_email(), list_mailboxes() |
| FTS5 (Cached) | Body search, complex filtering | ~2-10ms | search() |
| Disk (Batch) | Initial indexing, sync | ~15ms/100 emails | jxa-mail-mcp index, startup |
Performance
Batch Property Fetching (87x faster)
Naive AppleScript/JXA iteration is extremely slow because each property access triggers a separate Apple Event IPC round-trip. We use batch property fetching instead:
// FAST: ~0.6 seconds (87x faster than per-message iteration)
const msgs = inbox.messages;
const senders = msgs.sender(); // Single IPC call returns array
const subjects = msgs.subject(); // Single IPC call returns array
Benchmark Results
| Method | Time | Speedup |
|---|---|---|
| AppleScript (per-message) | 54.1s | 1x |
| JXA (per-message) | 53.9s | 1x |
| JXA (batch fetching) | 0.62s | 87x |
Disk-First Sync (12x faster)
| Sync Method | Time | Status |
|---|---|---|
| JXA date-based (old) | 60s timeout | ❌ |
| Disk state reconciliation | <5s | ✅ |
Development
uv sync
uv run ruff check src/
uv run ruff format src/
# Run unit tests
uv run pytest
# Run tests with verbose output
uv run pytest -v
# Manual test
uv run python -c "
import asyncio
from jxa_mail_mcp.server import list_accounts, get_emails
print('Accounts:', len(asyncio.run(list_accounts())))
print('Emails:', len(asyncio.run(get_emails(filter='today'))))
"
# Test index
uv run python -c "
from jxa_mail_mcp.index import IndexManager
m = IndexManager.get_instance()
if m.has_index():
stats = m.get_stats()
print(f'Indexed: {stats.email_count} emails')
"
Security
Implemented Protections
| Threat | Mitigation | Location |
|---|---|---|
| SQL Injection | Parameterized queries | search.py, sync.py |
| JXA Injection | json.dumps() serialization |
sync.py, executor.py |
| FTS5 Query Injection | Special character escaping | search.py |
| XSS via HTML Emails | BeautifulSoup HTML parsing | disk.py |
| DoS via Large Files | 25 MB file size limit | disk.py |
| Path Traversal | Path validation in watcher | watcher.py |
| Data Exposure | Database created with 0600 permissions | schema.py |
Known Issues
XMLParsedAsHTMLWarning during indexing
When running jxa-mail-mcp index, you may see warnings like:
XMLParsedAsHTMLWarning: It looks like you're using an HTML parser to parse an XML document.
This is harmless - BeautifulSoup's HTML parser handles the XML plist metadata in .emlx files adequately for text extraction. The warning can be suppressed by installing lxml:
pip install lxml
FTS5 search ignores account/mailbox filters
Body search via search() currently searches all indexed emails regardless of account/mailbox parameters. This is because the disk indexer stores account UUIDs from folder paths, while JXA returns friendly names (e.g., "iCloud"). The mismatch prevents filtering.
Impact: Search results may include emails from all accounts, not just the specified one.
Troubleshooting
ModuleNotFoundError after install
If you get ModuleNotFoundError: No module named 'jxa_mail_mcp' even though
the package is installed, reset the virtual environment:
rm -rf .venv
uv sync --upgrade
Full Disk Access denied
The jxa-mail-mcp index command requires Full Disk Access to read Mail.app's
data files. Grant access in:
System Settings → Privacy & Security → Full Disk Access → Add Terminal
Then restart your terminal.
Migration from v0.3.x
v0.4.0 introduces breaking changes to consolidate the API:
Removed Tools → Replacements
| Old Tool | New Usage |
|---|---|
get_todays_emails() |
get_emails(filter="today") |
get_unread_emails() |
get_emails(filter="unread") |
get_flagged_emails() |
get_emails(filter="flagged") |
search_emails() |
search() |
fuzzy_search_emails() |
search() |
search_email_bodies() |
search() |
index_status() |
CLI: jxa-mail-mcp status |
sync_index() |
Automatic at startup |
rebuild_index() |
CLI: jxa-mail-mcp rebuild |
Schema Migration
After upgrading, rebuild the index to populate the new emlx_path column:
jxa-mail-mcp rebuild
Removed CLI Flag
The --no-sync flag has been removed since disk-based sync is now fast (<5s).
License
GPL-3.0-or-later
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jxa_mail_mcp-0.4.0.tar.gz.
File metadata
- Download URL: jxa_mail_mcp-0.4.0.tar.gz
- Upload date:
- Size: 51.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73fba451aaafc52ea99dfc9bf945aea3050f90626dfb1a10af5f4c85eb933443
|
|
| MD5 |
2012d177165fd8595da39319b5e4efab
|
|
| BLAKE2b-256 |
f4c097d507c77c602b5fadfa3a9098fed360c984238771b3217b5808cbe464e4
|
File details
Details for the file jxa_mail_mcp-0.4.0-py3-none-any.whl.
File metadata
- Download URL: jxa_mail_mcp-0.4.0-py3-none-any.whl
- Upload date:
- Size: 59.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c32aa02c0b603d7115a35d53d4b0a29503e48ae733751b84d30755558b2639f
|
|
| MD5 |
6502c6c4da73a3061b26ef2f892d3e20
|
|
| BLAKE2b-256 |
7fc8a3b3b844035b5e2ce122cccecd098989a7c5597ba7f6aab7ab219549d20e
|