MCP-APPLE-NOTES
6,800× faster MCP server for Apple Notes. Fully local, semantic hybrid search.
Every other Apple Notes MCP server uses JXA (AppleScript automation) to fetch notes one at a time. Fine for 50 notes; at 1,800 it takes ~49 minutes, and on macOS Sequoia it just stops working — processes without a bundle ID get silently denied Automation permission, so JXA-based servers return empty results with no error. This fork reads the Notes SQLite database directly, decodes the protobuf blobs, and indexes 1,800 notes in under 5 seconds. Fetch alone goes from ~49 min to ~430 ms.
Stack
- Bun + TypeScript
- Single runtime, no transpile step; cold start fast enough that re-index-on-search feels free.
- SQLite (direct read)
- Open
NoteStore.sqliteread-only out of the macOS group container. No JXA, no permission dialog past Full Disk Access. - protobuf
- Apple stores note text as gzipped protobuf in
ZICNOTEDATA.zdata. Decompress, decode against the reverse-engineered schema, walk the document tree to plain text. all-MiniLM-L6-v2- On-device 384-dim sentence embeddings via transformers.js. Small, fast, good enough for personal corpora.
- BM25 + RRF
- Lexical and semantic searches fused by Reciprocal Rank Fusion — robust to score-scale mismatches between the two indices.
- Re-rank:
RRF × title × recency - Title hits get a multiplier; recency factor decays by edit time. Notes I touched today rank ahead of identical matches from 2018.
- MCP stdio transport
- Drops into Claude Desktop's config like any other server. UI surface for indexing progress is part of the MCP response.
Process
I forked the upstream JXA-based server because it broke on Sequoia. Debugging took longer than the rewrite — JXA isn't returning an error, it's returning an empty array, and the macOS log shows nothing helpful. Tracked it down to TCC silently denying Automation permission to any process without a bundle ID; Bun, Node, and most MCP runtimes have no bundle ID.
Switching to direct SQLite means giving the runtime Full Disk Access, but that's a one-time setting toggle instead of a permission Apple won't grant. The Notes SQLite schema is reasonably stable across macOS versions; the trap is that note bodies aren't text columns, they're gzipped protobuf. Reverse-engineering the proto schema took an evening with protoc --decode_raw and a few notes whose contents I already knew.
The interesting search question is fusion. Pure semantic gives you "fuzzy" matches but ranks the exact term you typed below something tangentially related. Pure BM25 misses everything that uses synonyms. RRF dodges the score-calibration problem entirely — you only use ranks — and the RRF × title × recency reranker gets the obvious-match-on-top behavior people actually expect from search.
Auto re-indexing happens on every search request, gated by a 1ms mtime check on the SQLite file. If nothing changed, nothing re-indexes. If something did, only the changed notes get re-embedded — chunked 1500 chars at a time so a long note doesn't dominate its own results. Folder paths are preserved end-to-end so the MCP tool surface can take a path-prefix filter.