Semantic search + connection-discovery across your own Apple Notes (bridges, synthesis).
MCP Apple Notes
The io.github.connerkward/mcp-apple-notes server provides semantic search and connection-discovery across Apple Notes. It traverses an accumulated collection of notes using hybrid semantic + BM25 search, bridge connections (including “Swanson-ABC bridges”), entity threads, related-notes, and grounded synthesis across previously written content.
🛠️ Key Features
Semantic + BM25 hybrid search
Connection-discovery via non-obvious bridge links
Entity threads and related-notes exploration
Cited, grounded synthesis over authored Apple Notes
Uses embeddings, search, BM25, clustering, and bridges on-device
Synthesis generation calls an LLM (“only synthesis generation calls an L”)
🚀 Use Cases
Find relevant notes using semantic and lexical matching
Discover relationships between concepts via bridge connections
Follow entity threads across notes
Generate grounded summaries that reference prior writing
⚡ Developer Benefits
Aligns outputs to previously written Apple Notes content
Supports hybrid search for more robust retrieval
Keeps most computation on-device; synthesis relies on model calls
⚠️ Limitations
On-device embeddings/search/clustering; synthesis generation requires an LLM call
Functionality described here is limited to Apple Notes content traversal and synthesis
mcp-apple-notes is an MCP server for semantic search and connection-discovery across your own Apple Notes — hybrid search, Swanson-ABC bridges, entity threads, and cited synthesis over everything you've written.
It traverses your accumulated mass of notes: semantic + BM25 hybrid search, non-obvious bridge connections, entity threads, related-notes, and grounded synthesis across everything you've ever written in Apple Notes. Embeddings, search, BM25, clustering, and bridges run on-device; only synthesis generation calls an LLM (local via LM Studio/Ollama, or cloud via OpenAI — your choice). Built on the Model Context Protocol (MCP).
Forked from RafalWilinski/mcp-apple-notes — this fork reads the SQLite store + protobuf directly and adds bridges, entities, feed, and synthesis.
This registers the MCP server and bundles the apple-notes-search agent skill. Or install the MCP server manually — see Installation below (requires macOS + bun + Full Disk Access).
Other MCP Notes servers break at scale. They use JXA (AppleScript automation) to read notes one-by-one — fine for 50 notes, unusable at 500+. At 1,800 notes, JXA takes ~49 minutes just to fetch content. On macOS Sequoia it's worse: Apple silently denies Automation permission to processes without a bundle ID, so JXA-based servers fail entirely. This fork reads the SQLite database directly, decodes the protobuf blobs for real note text, and indexes 1,800 notes in under 5 seconds.
📝 BM25 full-text search combined via Reciprocal Rank Fusion
🧠 Re-ranking: RRF × title_boost × recency_factor
📂 Full folder hierarchy — filter by any path segment
🕐 Auto re-index: every search runs ~1ms change detection and, if notes changed, kicks ONE background index job (single-flight) — search itself never blocks on indexing
✂️ 1500-char chunking for long notes
🍎 Direct SQLite + protobuf decode — real note text, not garbled HTML
🏃♂️ On-device — embeddings, search, BM25, clustering, and bridges run locally with no API keys; only optional synthesis generation calls an LLM (local LM Studio/Ollama, or cloud OpenAI — your choice)
Local web app — not just search, but connections & synthesis
Run the server directly and open the browser UI — no MCP client needed:
bash
bun index.ts # → http://localhost:3741/ (also serves /mcp)
Six modes, building from retrieval toward sensemaking:
Feed (default) — one ranked, evidence-first stream mixing three connection kinds: bridges (A ↔ C via B, with the connecting phrase mined from B as plain-text evidence), abstraction pairs (two notes whose LLM-consolidated abstractions from ~/.mcp-apple-notes/consolidated.jsonl make the same underlying move), and entity threads ("X notes across Y folders mention Z", from the entity graph). Every item shows its evidence in plain text. 👍/👎 on any item tunes the ranking: a tiny online logistic regression (pure TS, no deps) takes a few SGD steps per vote; votes append to ~/.mcp-apple-notes/votes.jsonl (with the item's feature vector) and replay on boot, so the ranking survives restarts. Diversity constraints: ≤2 consecutive items of one kind, no note more than twice per page of 20; "show more" paginates. Endpoints: GET /api/feed?offset=&limit=, POST /api/vote{id, vote: 1|-1}.
Search — the hybrid semantic + BM25 search, in a paper-themed UI with query highlighting and folder filter.
Map — a topic map of every note: spherical k-means clusters (TF-IDF labels) over the embeddings, projected to 2D with PCA. Hover a note for the neighbor-lens — lines to its nearest-by-meaning notes, which often cross clusters (the connection-finding payload). Endpoint: GET /api/clusters?k=.
Synthesize — "what do I think about X across everything I've written?" Query-expansion → relevance-gated retrieval (no recency bias) → MMR diversification → an LLM writes a grounded answer with inline [n] citations back to the source notes; provenance is post-checked. Endpoint: GET /api/synthesize?q=.
Bridges — Swanson-ABC discovery over your own notes: pairs (A, C) that are not directly similar but are both strongly similar to a shared bridge note B — connections the corpus only makes through an intermediary. Pure arithmetic over the embeddings (score = sim(A,B)·sim(B,C)·(1−sim(A,C)); cross-folder, no shared tags/wikilinks; thresholds auto-relax; hub-penalized selection so no note dominates the list). No LLM, $0. Endpoint: GET /api/bridges?limit=40.
Entities — "where else do I talk about Mercedes?" Ranked entity chips (people, orgs, concepts) → the notes that mention them, by mention weight. Reads an optional sqlite graph db (see below). Endpoints: GET /api/entities?q=&limit=, GET /api/entity-notes?entity=.
Precomputed disk caches (instant bridges & map)
Bridge mining (~2 min of embedding) and clustering are computed once and persisted, keyed by a corpus fingerprint (note count + max modification date):
~/.mcp-apple-notes/bridges_cache.json — the full mined bridge pool
~/.mcp-apple-notes/clusters_cache.json — cluster output per k
Fresh cache → served from disk instantly (<300ms). Stale (notes changed) → the stale copy is served instantly with a "recomputing…" pill in the UI while ONE background job (single-flight) refreshes it. Absent → computed inline once.
Enabling synthesis (needs an LLM)
Embeddings/search/clustering/bridges/entities are fully local. Only synthesis generation needs an LLM. Point at a local OpenAI-compatible server to keep notes private:
bash
# LM Studio / Ollama (zero API cost, notes stay local):
SYNTH_BASE_URL=http://localhost:1234/v1 SYNTH_MODEL=<loaded-model> OPENAI_API_KEY=local bun index.ts
# …or real OpenAI: set a funded OPENAI_API_KEY (defaults to gpt-4o-mini).
The entity graph (optional)
The entity layer reads ~/.mcp-apple-notes/layered_graph.db (override with LAYERED_DB) — a plain sqlite file produced by the companion exp-notes-indexing benchmark harness (layered_graph.py). No extra dependencies (bun ships bun:sqlite); if the file is absent the entity tools and tab simply report how to generate it.
Installation
Clone and install:
bash
git clone https://github.com/connerkward/mcp-apple-notes
cd mcp-apple-notes
bun install
Add bun (~/.bun/bin/bun) to Full Disk Access in System Settings.
Restart Claude Desktop and ask: "Index my Apple Notes".
Claude Code / CLI registration
json
// .mcp.json (project) or `claude mcp add apple-notes -- bun /path/to/mcp-apple-notes/index.ts --stdio`{"mcpServers":{"apple-notes":{"command":"bun","args":["/path/to/mcp-apple-notes/index.ts","--stdio"]}}}
The same bridges/entities tools power the web UI tabs at the local app (bun index.ts → http://localhost:3741/); the entity graph db is optional, generated by the exp-notes-indexing benchmark harness.
Exact substring search (like Apple Notes built-in); optional folder, date range
get-note
Full note by title; fuzzy fallback on no exact match
list-notes
Notes sorted by recency; optional folder, date range, limit
list-folders
All folders with note counts
list-tags
All #hashtags across notes, sorted by frequency
search-by-tag
Notes containing a specific hashtag
related-notes
Related notes via shared tags, [[wikilinks]], and vector similarity
bridge-notes
Swanson-ABC bridges: non-similar note pairs connected via a shared intermediary; optional folder, limit
feed
The ranked evidence-first connection feed (bridges + abstraction pairs + entity threads) as JSON; optional limit
entity-notes
Notes mentioning an entity (e.g. "Mercedes"), by mention weight — needs the optional entity graph db
list-entities
Entities ranked by mention count; optional substring query, limit
get-tables
Extract pipe/tab-separated tables from a note
create-note
Create a note
update-note
Edit an existing note
check-changes
Check if notes changed since last index (without triggering re-index)
index-health
Sync status, last indexed time, note count
Search & Ranking
Every search detects changes (~1ms). If notes changed, ONE background incremental index job is kicked (single-flight — duplicate triggers are dropped) and the search returns immediately from the current index; results catch up when the job lands. The last-indexed watermark persists to ~/.mcp-apple-notes/index_state.json across restarts. Change detection compares the per-title set of modification dates (Apple Notes titles are not unique — "TODO" ×10 — and a title→single-date map can never converge for duplicates).
Temporal queries (recent, latest, today) automatically shift to a 1-day recency half-life at 70% weight. Normal queries use 90-day half-life at 10% so relevance stays primary.
Permissions error → ensure bun (~/.bun/bin/bun) has Full Disk Access in System Settings → Privacy & Security.
🧭 ckw-skills — part of Conner K. Ward's collection of Claude Code skills & MCP servers.
Install
Configuration
Environment variables
OPENAI_API_KEYsecret
Only required for synthesis. Set to 'local' when pointing SYNTH_BASE_URL at a local OpenAI-compatible server (LM Studio / Ollama); set to a funded key for real OpenAI. Search, bridges, entities run on-device with no key.
SYNTH_BASE_URL
Optional OpenAI-compatible base URL for synthesis (e.g. http://localhost:1234/v1 for LM Studio). Keeps notes on-device.
SYNTH_MODEL
Optional model id for synthesis when SYNTH_BASE_URL is set (defaults to gpt-4o-mini for real OpenAI).