# apple-notes-mcp [![npm version](https://img.shields.io/npm/v/@disco_trooper/apple-notes-mcp)](https://www.npmjs.com/package/@disco_trooper/apple-notes-mcp) [![npm downloads](https://img.shields.io/npm/dm/@disco_trooper/apple-notes-mcp)](https://www.npmjs.com/package/@disco_trooper/apple-notes-mcp) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![macOS](https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=white)](https://www.apple.com/macos/) [![Bun](https://img.shields.io/badge/Bun-000000?logo=bun&logoColor=white)](https://bun.sh) [![Claude](https://img.shields.io/badge/Claude-MCP-blueviolet)](https://modelcontextprotocol.io) MCP server for Apple Notes with semantic search and CRUD operations. Claude searches, reads, creates, updates, and manages your Apple Notes through natural language. ## Features - **Chunk-Based Search** - Long notes split into chunks for accurate matching - **Query Caching** - 60x faster repeated searches - **Knowledge Graph** - Tags, links, and related notes discovery - **Hybrid Search** - Vector + keyword search with Reciprocal Rank Fusion - **Semantic Search** - Find notes by meaning, not keywords - **Full CRUD** - Create, read, update, delete, and move notes - **Incremental Indexing** - Re-embed only changed notes - **Background Index Jobs** - Async full/incremental indexing with progress polling - **Dual Embedding** - Local HuggingFace or OpenRouter API ## What's New in 1.8.1 - **Faster `list-notes` folder filtering** - `list-notes` now queries only the requested folder instead of scanning all notes first - **Duplicate folder name correctness** - Folder filtering now aggregates matching folders across accounts - **Large vault performance** - Folder-scoped listing is significantly faster on larger note libraries ## Installation ### npm (recommended) ```bash npm install -g @disco_trooper/apple-notes-mcp apple-notes-mcp ``` The setup wizard guides you through: 1. Choosing your embedding provider (local or OpenRouter) 2. Configuring API keys if needed 3. Setting up Claude Code integration 4. Indexing your notes ### From source ```bash git clone https://github.com/disco-trooper/apple-notes-mcp.git cd apple-notes-mcp bun install bun run start ``` ## Requirements - macOS (uses Apple Notes via JXA) - [Bun](https://bun.sh) runtime - Apple Notes app with notes ## Quick Start Run the command after installation: ```bash apple-notes-mcp ``` The setup wizard starts automatically on first run. Restart Claude Code after setup to use the MCP tools. ## Configuration Configuration stored in `~/.apple-notes-mcp/.env`: | Variable | Description | Default | |----------|-------------|---------| | `OPENROUTER_API_KEY` | OpenRouter API key (enables cloud embeddings) | - | | `EMBEDDING_MODEL` | Model name (local or OpenRouter) | `Xenova/multilingual-e5-small` | | `EMBEDDING_DIMS` | Embedding dimensions | `4096` | | `READONLY_MODE` | Block all write operations | `false` | | `INDEX_TTL` | Auto-refresh-on-search interval in seconds (disabled when unset) | - | | `SEARCH_REFRESH_TIMEOUT_MS` | Max time search waits for refresh before using stale index | `2000` | | `INDEX_JOB_RETENTION_SECONDS` | How long completed/failed index jobs remain queryable | `3600` | | `EMBEDDING_BATCH_SIZE` | Batch size for embedding generation | `50` | | `DEBUG` | Enable debug logging | `false` | ### Search Auto-Refresh Policy - `search-notes` does **not** force refresh on every request. - If `INDEX_TTL` is unset, auto-refresh is disabled and search uses the current index. - If `INDEX_TTL` is set, refresh runs only after TTL expiration. - If refresh fails or takes longer than `SEARCH_REFRESH_TIMEOUT_MS`, search falls back to stale index results instead of timing out. To reconfigure: ```bash apple-notes-mcp setup # or from source: bun run setup ``` ### Embedding Providers **Local (default)**: Uses HuggingFace Transformers with `Xenova/multilingual-e5-small`. Free, runs locally, ~200MB download. **OpenRouter**: Uses cloud API. Fast, requires no local resources, needs API key from [openrouter.ai](https://openrouter.ai). See [docs/models.md](docs/models.md) for model comparison. ## Tools ### Search & Discovery #### `search-notes` Hybrid vector + fulltext search. ``` query: "meeting notes from last week" folder: "Work" # optional, filter by folder limit: 10 # default: 20 mode: "hybrid" # hybrid, keyword, or semantic include_content: false # include full content vs preview ``` #### `list-notes` List notes with sorting and filtering. Without parameters, shows index statistics. ``` sort_by: "modified" # created, modified, or title (default: modified) order: "desc" # asc or desc (default: desc) limit: 10 # max notes to return (1-100) folder: "Work" # filter by folder (case-insensitive) ``` When `folder` is provided, the server fetches only matching folders from Apple Notes. This keeps folder-scoped requests fast even when your vault has hundreds of notes. **Examples:** - Get 5 newest notes: `{ sort_by: "created", order: "desc", limit: 5 }` - Recently modified: `{ sort_by: "modified", limit: 10 }` - Alphabetical in folder: `{ sort_by: "title", order: "asc", folder: "Projects" }` #### `list-folders` List all Apple Notes folders. #### `get-note` Get note content by title. ``` title: "My Note" # or "Work/My Note" for disambiguation include_html: false # include raw HTML (default: false) ``` #### `get-tables` Extract structured table data from a note. ``` title: "My Note" ``` Returns: ```json { "tableCount": 2, "tables": [{ "index": 0, "rows": [["Header1", "Header2"], ["Val1", "Val2"]], "formatting": [[{"bold": true}, {"bold": true}], ...] }] } ``` ### Indexing #### `index-notes` Index notes for semantic search. ``` mode: "incremental" # incremental (default) or full force: false # force reindex even if TTL hasn't expired background: false # optional; defaults to false (synchronous mode) ``` Use `mode: "full"` to create the chunk index for better long-note search. First full index takes longer as it generates chunks, but subsequent searches run fast. For large vaults, prefer background indexing: #### `start-index-job` ``` mode: "full" # full or incremental ``` Returns a job snapshot with `id`, `status`, and `progress`. Progress updates in smaller steps across fetch, embed, and persist phases. #### `get-index-job` ``` job_id: "" ``` Poll until status is `completed`, `failed`, or `cancelled`. You may see `cancelling` as a transitional status. #### `list-index-jobs` ``` limit: 10 # optional, 1-50 ``` #### `cancel-index-job` ``` job_id: "" ``` Requests best-effort cancellation for a running job. Cancellation is cooperative: - A long-running step must reach a cancellation checkpoint. - Partial work may remain. - Start a new job after the current one reaches `cancelled`. #### `reindex-note` Re-index a single note after manual edits. ``` title: "My Note" ``` ### CRUD Operations #### `create-note` Create a note in Apple Notes. ``` title: "New Note" content: "# Heading\n\nMarkdown content..." folder: "Work" # optional, defaults to Notes ``` After create, update, delete, or move, the server auto-syncs vector and chunk indexes in best-effort mode. If sync partly fails, the tool response includes an `index sync warning`. Run `reindex-note` or `index-notes`. #### `update-note` Update an existing note. ``` title: "My Note" content: "Updated markdown content..." reindex: true # re-embed after update (default: true) ``` #### `delete-note` Delete a note (requires confirmation). ``` title: "My Note" confirm: true # must be true to delete ``` #### `move-note` Move a note to another folder. ``` title: "My Note" folder: "Archive" ``` #### `batch-delete` Delete multiple notes at once. ``` titles: ["Note 1", "Note 2"] # OR folder: "Old Project" confirm: true # required for safety ``` #### `batch-move` Move multiple notes to a target folder. ``` titles: ["Note 1", "Note 2"] # OR sourceFolder: "Old" targetFolder: "Archive" # required ``` ### Index Management #### `purge-index` Clear all indexed data. Use when switching embedding models or to fix corrupted index. ``` confirm: true # required for safety ``` After purging, run `index-notes` to rebuild. ### Knowledge Graph #### `list-tags` List all tags with occurrence counts. #### `search-by-tag` Find notes with a specific tag. ``` tag: "project" folder: "Work" # optional limit: 20 # default: 20 ``` #### `related-notes` Find notes related to a source note. ``` title: "My Note" types: ["tag", "link", "similar"] # default: all limit: 10 # default: 10 ``` #### `export-graph` Export knowledge graph for visualization. ``` format: "json" # json or graphml folder: "Work" # optional filter ``` **Supported Formats:** - `json` - For custom visualization (D3.js, web apps) - `graphml` - For professional tools (Gephi, yEd, Cytoscape) ## Claude Code Setup ### Automatic (recommended) The setup wizard automatically adds apple-notes-mcp to Claude Code. Run `apple-notes-mcp` after installation. ### Manual Add to `~/.claude.json`: For npm installation: ```json { "mcpServers": { "apple-notes": { "command": "apple-notes-mcp", "args": [], "env": {} } } } ``` For source installation: ```json { "mcpServers": { "apple-notes": { "command": "bun", "args": ["run", "/path/to/apple-notes-mcp/src/index.ts"], "env": {} } } } ``` ## Usage Examples After setup, use natural language with Claude: - "Search my notes for project ideas" - "Create a note called 'Meeting Notes' in the Work folder" - "What's in my note about vacation plans?" - "Move the 'Old Project' note to Archive" - "Index my notes" (after adding notes in Apple Notes) ## Troubleshooting ### "Note not found" Use full path format `Folder/Note Title` when multiple notes share the same name. ### Slow first search Local embeddings download the model on first use (~200MB). Subsequent searches run fast. ### "READONLY_MODE is enabled" Set `READONLY_MODE=false` in `.env` to enable write operations. ### Notes missing from search Run `index-notes` to update the search index. Use `mode: full` if incremental misses changes. ### "iCloud account not available" / `Can't get account "iCloud"` This error comes from a different Apple Notes MCP implementation that uses tool `search_notes` and argument `Keywords`. This project uses: - tool: `search-notes` - argument: `query` If your client calls `search_notes` with `Keywords`, point your MCP config to `apple-notes-mcp` and restart the client. ### JXA errors Ensure Apple Notes runs and contains notes. Grant automation permissions when prompted. ### "JSON Parse error: Unexpected identifier undefined" This usually means the indexing process ran out of memory. Try: 1. Close other applications to free memory 2. Set `EMBEDDING_BATCH_SIZE=25` in `.env` to reduce memory usage 3. Restart Apple Notes app 4. Run `index-notes` again ### Skipped notes during indexing Some notes may be skipped if they are: - **Locked** - Unlock them in Apple Notes if you want them indexed - **Syncing** - Wait for iCloud sync to complete, then reindex - **Corrupted** - Try copying content to a new note and deleting the old one The indexer will report which notes were skipped and continue with the rest. ## Development ```bash # Type check bun run check # Run tests bun run test # Run with coverage bun run test:coverage # Run with debug logging DEBUG=true bun run start # Watch mode bun run dev ``` ## Contributing PRs welcome! Please: - Run `bun run check` before submitting - Add tests for new functionality - Update documentation as needed ## License MIT