Skip to main content
Glama
disco-trooper

apple-notes-mcp

README.md
# 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.9.0

- **Large library indexing** - Fetch reads note properties in bulk batches, so indexing thousands of notes finishes instead of stalling
- **Resilient fetch** - Locked or syncing notes are retried per batch and skipped with a report instead of failing the run
- **Cancellable reads** - Long Notes reads time out (`JXA_TIMEOUT_MS`) and cancellation actually stops the fetch

## 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` |
| `NOTES_FETCH_BATCH_SIZE` | Batch size for Notes fetch | `100` |
| `JXA_TIMEOUT_MS` | Max time for a single Notes read in milliseconds | `120000` |
| `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: "<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: "<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