A Model Context Protocol (MCP) server that enables AI assistants like Claude to read, create, search, and manage notes in Apple Notes on macOS.
This server acts as a bridge between AI assistants and Apple Notes. Once configured, you can ask Claude (or any MCP-compatible AI) to:
- "Save this conversation as a note called 'Meeting Summary'"
- "Find all my notes about the project deadline"
- "Read my shopping list note"
- "Move my draft notes to the Archive folder"
- "What notes do I have in my Work folder?"
The AI assistant communicates with this server, which then uses AppleScript to interact with the Notes app on your Mac. All data stays local on your machine.
If you're using Claude Code (in Terminal or VS Code), just ask Claude to install it:
Install the sweetrb/apple-notes-mcp MCP server so you can help me manage my Apple Notes
Claude will handle the installation and configuration automatically.
Or register it yourself with one deterministic command:
claude mcp add apple-notes -s user -- npx -y apple-notes-mcpInstall as a Claude Code plugin for automatic configuration and enhanced AI behavior:
/plugin marketplace add sweetrb/apple-notes-mcp
/plugin install apple-notesThis method also installs a skill that teaches Claude when and how to use Apple Notes effectively.
On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click OK. Optionally, grant Full Disk Access to the app that launches the server to enable the checklist-state and note-metadata features; see the Full Disk Access Setup Guide. Everything else works without it.
The same plugin is available for Codex. Add the marketplace and install the plugin:
codex plugin marketplace add sweetrb/apple-notes-mcp
codex plugin add apple-notes@apple-notes-mcpThe Codex plugin runs the published apple-notes-mcp server through npx and ships the same Apple Notes skill, so behavior matches the Claude Code plugin.
Configuration for two more hosts is included — each registers the same apple-notes MCP server (npx -y apple-notes-mcp):
- Hermes Agent (NousResearch) — Hermes has no plugin/marketplace drop-in. Add the server with
hermes mcp add apple-notes --command npx --args -y apple-notes-mcp, or merge.hermes-plugin/config.yamlinto~/.hermes/config.yaml. Details:.hermes-plugin/README.md. - Antigravity (Google) — add the server entry from
.antigravity-plugin/mcp_config.jsonto~/.gemini/config/mcp_config.json(or via Antigravity's MCP settings).
1. Install the server:
npm install -g apple-notes-mcp2. Add to Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"apple-notes": {
"command": "npx",
"args": ["-y", "apple-notes-mcp"]
}
}
}3. Restart Claude Desktop and start using natural language:
"Create a note called 'Ideas' with my brainstorming thoughts"
On first use, macOS will ask for permission to automate Notes.app. Click "OK" to allow.
- macOS - Apple Notes and AppleScript are macOS-only
- Node.js 20+ - Required for the MCP server
- Apple Notes - Must have at least one account configured (iCloud, Gmail, etc.)
| Feature | Description |
|---|---|
| Create Notes | Create notes with titles, content, and optional folder/account targeting |
| Search Notes | Find notes by title or search within note content |
| Read Notes | Retrieve note content and metadata |
| Update Notes | Modify existing notes (title and/or content) |
| Delete Notes | Remove notes (moves to Recently Deleted) |
| Move Notes | Organize notes into folders (supports nested paths) |
| Folder Management | Create, list, and delete folders with full hierarchical path support |
| Multi-Account | Work with iCloud, Gmail, Exchange, or any configured account, including account IDs and default folders |
| Batch Operations | Delete or move multiple notes at once |
| Checklist State | Read checklist done/undone state directly from the Notes database (requires Full Disk Access) |
| Export | Export all notes as JSON or get individual notes as Markdown |
| Attachments | List attachments, save them to disk, or fetch their bytes as base64 |
| Notes.app UI State | Reveal a note in Notes.app or read the current Notes.app selection |
| Sync Awareness | Detect iCloud sync in progress, warn about incomplete results |
| Collaboration | Detect shared notes, warn before modifying |
| Diagnostics | health-check plus a richer doctor (reachability, automation permission, accounts, Full Disk Access), sync status, and statistics |
Read/list/get tools also return structured JSON (structuredContent) alongside the text, so agents can consume results without parsing prose.
Resources expose read-only context the client can attach without a tool call:
notes://accounts, notes://folders, notes://stats, and the
notes://note/{id} template (returns the note as Markdown). Prompts package
common workflows: find-note, weekly-review, new-meeting-note.
A few Notes UI features are not exposed to AppleScript and therefore cannot be supported. See docs/APPLESCRIPT-LIMITATIONS.md for the investigation and verification behind each:
- Pinned notes — Notes has no scriptable
pinnedproperty via AppleScript. Pin state can now be read with the BETAget-note-metadatatool (from the NoteStore database), but it still cannot be set programmatically. - Note-to-note links — there is no
applenotes://deep link or link property; the only stable handle is thex-coredata://note id.
This section documents all available tools. AI agents should use these tool names and parameters exactly as specified.
Creates a new note in Apple Notes.
| Parameter | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | The title of the note. Automatically prepended as <h1> — do NOT include the title in content |
content |
string | Yes | The body content of the note (do not repeat the title here) |
tags |
string[] | No | Returned-only metadata — NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline #hashtags in content instead (Notes.app turns those into real tags) |
folder |
string | No | Folder to create the note in. Supports nested paths like "Work/Clients". Defaults to account root |
account |
string | No | Account name (defaults to iCloud) |
format |
string | No | Content format: "plaintext" (default) or "html". In both formats, the title is automatically prepended as <h1>. In plaintext mode, newlines become <br>, tabs become <br>, and backslashes are preserved as HTML entities |
Example (tagged with inline hashtags):
{
"title": "Meeting Notes",
"content": "Discussed Q4 roadmap and budget allocation\n\n#work #meetings"
}Example - Create in a specific folder:
{
"title": "Client Meeting",
"content": "Discussed project timeline",
"folder": "Work/Clients"
}Example - HTML formatting:
{
"title": "Status Report",
"content": "<h2>Summary</h2><p>All tasks <b>on track</b>.</p><ul><li>Feature A: complete</li><li>Feature B: in progress</li></ul>",
"format": "html"
}Note: The title is automatically prepended as
<h1>in both plaintext and HTML formats. Do not include a<h1>title tag in thecontentparameter, or the title will appear twice.
Returns: Confirmation message with note title and ID. Save the ID for subsequent operations like update-note, delete-note, etc.
Searches for notes by title or content.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | Text to search for |
searchContent |
boolean | No | If true, searches note body; if false (default), searches titles only |
account |
string | No | Account to search in (defaults to iCloud) |
folder |
string | No | Limit search to a specific folder (supports nested paths like "Work/Clients") |
modifiedSince |
string | No | ISO 8601 date string to filter notes modified on or after this date (e.g., "2025-01-01") |
limit |
number | No | Maximum number of results to return |
Example - Search titles:
{
"query": "meeting"
}Example - Search content:
{
"query": "budget allocation",
"searchContent": true
}Example - Search recent notes with limit:
{
"query": "todo",
"searchContent": true,
"modifiedSince": "2025-01-01",
"limit": 10
}Returns: List of matching notes with titles, folder names, and IDs. Use the returned ID for subsequent operations like get-note-content, update-note, etc.
Retrieves the full content of a specific note.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | No | Note ID (preferred - more reliable than title) |
title |
string | No | Note title (use id instead when available) |
account |
string | No | Account containing the note (defaults to iCloud, ignored if id is provided) |
Note: Either id or title must be provided. Using id is recommended as it's unique and avoids issues with duplicate titles.
Example - Using ID (recommended):
{
"id": "x-coredata://ABC123/ICNote/p456"
}Example - Using title:
{
"title": "Shopping List"
}Returns: The HTML content of the note, or error if not found. The
structuredContent also includes hashtags — any inline #hashtag tags parsed
from the body. Apple Notes tags are inline hashtags, not a scriptable property;
see docs/APPLESCRIPT-LIMITATIONS.md. Smart Folders are not scriptable.
Retrieves a note's body as plain text, with no HTML markup.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | No | Note ID (preferred - more reliable than title) |
title |
string | No | Note title (use id instead when available) |
account |
string | No | Account containing the note (defaults to iCloud, ignored if id is provided) |
Note: Either id or title must be provided. This reads the note's native plaintext property, so it skips the HTML-to-text conversion that get-note-content plus a Markdown pass would do. Use get-note-content when you need the HTML, or get-note-markdown when you want Markdown with checklist state.
Returns: The plain-text content of the note in structuredContent.plaintext, or error if not found.
Retrieves metadata about a note (without full content).
| Parameter | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Exact title of the note |
account |
string | No | Account containing the note (defaults to iCloud) |
Example:
{
"title": "Project Plan"
}Returns: JSON with note metadata:
{
"id": "x-coredata://...",
"title": "Project Plan",
"created": "2025-01-15T10:30:00.000Z",
"modified": "2025-01-20T14:22:00.000Z",
"shared": false,
"passwordProtected": false,
"account": "iCloud"
}Retrieves a note using its unique CoreData identifier.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The CoreData URL identifier (e.g., x-coredata://...) |
Returns: JSON with note metadata, or error if not found.
Reveals a note in Notes.app using its unique CoreData identifier.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The CoreData URL identifier (e.g., x-coredata://...) |
separately |
boolean | No | Open in a separate note window when supported by Notes.app |
Returns: Confirmation that Notes.app accepted the show command.
Updates an existing note's content and/or title.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | No | Note ID (preferred - more reliable than title) |
title |
string | No | Current title of the note to update (use id instead when available) |
newTitle |
string | No | New title (if changing the title; ignored when format is "html") |
newContent |
string | Yes | New content for the note body |
account |
string | No | Account containing the note (defaults to iCloud, ignored if id is provided) |
format |
string | No | Content format: "plaintext" (default) or "html". When "html", content replaces the entire note body as raw HTML and newTitle is ignored (the first HTML element serves as the title) |
Note: Either id or title must be provided. Using id is recommended.
Example - Using ID (recommended):
{
"id": "x-coredata://ABC123/ICNote/p456",
"newContent": "Updated content here"
}Example - Update content only:
{
"title": "Shopping List",
"newContent": "- Milk\n- Eggs\n- Bread\n- Butter"
}Example - Update title and content:
{
"title": "Draft",
"newTitle": "Final Version",
"newContent": "This is the completed document."
}Example - Update with HTML formatting:
{
"id": "x-coredata://ABC123/ICNote/p456",
"newContent": "<p>New findings with <b>bold</b> emphasis.</p><pre><code>console.log('hello');</code></pre>",
"format": "html"
}Returns: Confirmation message, or error if note not found.
Note: newContent replaces the entire note body — it is not appended. To preserve existing content, read it first (e.g. with get-note-content) and include it in newContent.
Attachments: A full-body replace can drop embedded files, images, scans, PDFs, or audio. When a note may hold attachments, run list-attachments first, and either save them with save-attachment or build a new note rather than overwriting. See the skill's Attachment-Safe Updates guidance.
Deletes a note (moves to Recently Deleted in Notes.app).
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | No | Note ID (preferred - more reliable than title) |
title |
string | No | Exact title of the note to delete (use id instead when available) |
account |
string | No | Account containing the note (defaults to iCloud, ignored if id is provided) |
Note: Either id or title must be provided. Using id is recommended.
Example - Using ID (recommended):
{
"id": "x-coredata://ABC123/ICNote/p456"
}Example - Using title:
{
"title": "Old Draft"
}Returns: Confirmation message, or error if note not found.
search-notes / list-notes first to confirm the exact id(s) being deleted.
Moves a note to a different folder. The note is relocated in place via Notes.app's native move, so its id, creation date, and all embedded attachments (files, images, scans, PDFs, audio) are preserved. The destination folder must already exist — create it first with create-folder.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | No | Note ID (preferred - more reliable than title) |
title |
string | No | Title of the note to move (use id instead when available) |
folder |
string | Yes | Destination folder name or nested path (e.g., "Work/Clients") |
account |
string | No | Account containing the note (defaults to iCloud, ignored if id is provided) |
Note: Either id or title must be provided. Using id is recommended.
Example - Using ID (recommended):
{
"id": "x-coredata://ABC123/ICNote/p456",
"folder": "Archive"
}Example - Using title:
{
"title": "Completed Task",
"folder": "Archive"
}Returns: Confirmation message, or error if note or folder not found.
Lists all notes, optionally filtered by folder, date, and limit.
| Parameter | Type | Required | Description |
|---|---|---|---|
account |
string | No | Account to list notes from (defaults to iCloud) |
folder |
string | No | Filter to notes in this folder only (supports nested paths like "Work/Clients") |
modifiedSince |
string | No | ISO 8601 date string to filter notes modified on or after this date (e.g., "2025-01-01") |
limit |
number | No | Maximum number of notes to return |
Example - All notes:
{}Example - Notes in a folder:
{
"folder": "Work"
}Example - Recent notes with limit:
{
"modifiedSince": "2025-06-01",
"limit": 20
}Returns: List of note titles.
Reads the currently selected note(s) from the Notes.app UI.
Parameters: None
Returns: Selected note metadata, including IDs for follow-up operations. Returns an empty list when Notes.app has no selected note.
Lists all folders in an account with full hierarchical paths.
| Parameter | Type | Required | Description |
|---|---|---|---|
account |
string | No | Account to list folders from (defaults to iCloud) |
Example:
{}Returns: List of folders with IDs, paths, account names, and shared state. Nested folders are shown as full paths (e.g., Work/Clients/Omnia). Duplicate folder names are disambiguated by their full path. Literal slashes in folder names are escaped as \/ (e.g., Spain\/Portugal 2023).
Creates a new folder.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Name for the new folder |
account |
string | No | Account to create folder in (defaults to iCloud) |
Example:
{
"name": "Work Projects"
}Returns: Confirmation message, or error if folder already exists.
Deletes a folder.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Name or path of the folder to delete (supports nested paths like "Work/Old") |
account |
string | No | Account containing the folder (defaults to iCloud) |
Example:
{
"name": "Old Projects"
}Returns: Confirmation message, or error if folder not found or not empty.
list-folders first to confirm the exact folder path being deleted.
Reveals a folder in Notes.app using its unique CoreData identifier.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The folder's CoreData identifier (from list-folders) |
separately |
boolean | No | Open in a separate window when supported by Notes.app |
Returns: Confirmation that Notes.app accepted the show command.
Lists all configured Notes accounts.
Parameters: None
Example:
{}Returns: List of accounts with names, IDs, upgraded state, and default folder metadata.
Returns the default account and folder Notes.app uses for newly created notes.
Parameters: None
Returns: Default account and folder metadata, including IDs and shared state.
Reveals an account in Notes.app using its unique CoreData identifier.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The account's CoreData identifier (from list-accounts) |
separately |
boolean | No | Open in a separate window when supported by Notes.app |
Returns: Confirmation that Notes.app accepted the show command.
Deletes multiple notes at once by ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
ids |
string[] | Yes | Array of note IDs to delete |
Returns: Summary of successes and failures.
search-notes / list-notes first to confirm the exact ids being deleted.
Moves multiple notes to a folder.
| Parameter | Type | Required | Description |
|---|---|---|---|
ids |
string[] | Yes | Array of note IDs to move |
folder |
string | Yes | Destination folder name or nested path (e.g., "Work/Clients") |
account |
string | No | Account containing the folder |
Returns: Summary of successes and failures.
Exports all notes as a JSON structure.
Parameters: None
Returns: Complete JSON export with all accounts, folders, and notes including metadata.
Gets a note's content as Markdown instead of HTML. If the note contains checklists and Full Disk Access is granted, checklist items are automatically annotated with [x] (done) or [ ] (undone).
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | No | Note ID (preferred) |
title |
string | No | Note title |
account |
string | No | Account containing the note |
Returns: Note content converted to Markdown format. Checklist items include [x]/[ ] prefixes when database access is available.
Reads checklist done/undone state for a note. This bypasses the AppleScript limitation where body of note strips checklist state, by reading directly from the NoteStore SQLite database.
Requires: Full Disk Access for the MCP host process (see Full Disk Access Setup).
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Note ID (use search-notes to find it first) |
Example:
{
"id": "x-coredata://ABC123/ICNote/p456"
}Returns: Checklist items with done/undone state and progress count:
Checklist for "Shopping List" (2/4 done):
[x] Buy milk
[x] Get bread
[ ] Pick up laundry
[ ] Call dentist
Reads note metadata that AppleScript cannot expose, by querying the NoteStore SQLite database directly: pinned state, checklist flags, trash/recovery state, the preview snippet, and the password hint. The available fields vary by macOS version.
Requires: Full Disk Access for the MCP host process (see Full Disk Access Setup).
BETA: the NoteStore schema changes between macOS releases, so some fields can be absent on older or newer systems. The database is only ever read, never written.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Note ID (use search-notes to find it first) |
Returns: A metadata object in structuredContent holding any of pinned, hasChecklist, hasChecklistInProgress, recoveringFromTrash, passwordProtected, passwordHint, snippet, widgetSnippet, and smartFolderQuery. Unlike most read tools, it also resolves trashed notes that AppleScript can no longer find.
Lists attachments in a note.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | No | Note ID (preferred) |
title |
string | No | Note title |
account |
string | No | Account containing the note |
Returns: List of attachments with IDs, names, content identifiers, URLs when available, created/modified dates, and shared state.
Saves a note attachment to disk.
| Parameter | Type | Required | Description |
|---|---|---|---|
noteId |
string | Yes | CoreData note ID (from search-notes/list-notes) |
attachmentId |
string | Yes | Attachment ID (from list-attachments) |
savePath |
string | Yes | Absolute destination file path. Must be under your home directory, a temp directory, or /Volumes |
Returns: Confirmation with the saved path, name, and content type (also in structuredContent).
Returns a note attachment's bytes as base64, without writing to disk (the read counterpart to save-attachment).
| Parameter | Type | Required | Description |
|---|---|---|---|
noteId |
string | Yes | CoreData note ID (from search-notes/list-notes) |
attachmentId |
string | Yes | Attachment ID (from list-attachments) |
Returns: The attachment name, content type, byte count, and base64 payload in structuredContent.base64.
Reveals one note attachment in Notes.app. Attachments are elements of a note, so this takes both the note id and the attachment id (the same pair used by save-attachment / fetch-attachment).
| Parameter | Type | Required | Description |
|---|---|---|---|
noteId |
string | Yes | CoreData note ID (from search-notes/list-notes) |
attachmentId |
string | Yes | Attachment ID (from list-attachments) |
separately |
boolean | No | Open in a separate window when supported by Notes.app |
Returns: Confirmation that Notes.app revealed the attachment.
Verifies Notes.app connectivity and permissions.
Parameters: None
Returns: Status of all health checks (app installed, permissions, account access).
Run a full setup diagnostic: Notes.app reachability, the Automation permission, configured accounts, and Full Disk Access — each reported as ok / warn / fail with an actionable message. This is the richer counterpart to health-check; reach for it first when something isn't working.
Parameters: None
Returns: A per-check report (structuredContent carries the raw {healthy, checks[]}). The Full Disk Access check tells you whether checklist-state features will work — see Full Disk Access Setup.
Gets comprehensive statistics about your notes.
Parameters: None
Returns: Total counts, per-account breakdown, folder statistics, and recently modified counts.
The structuredContent also includes a coverage object — { complete, scanned, covered, warnings[] }. If complete is false, one or more accounts (or the recent-activity scan) could not be read and the counts reflect only the scopes that succeeded; the text output adds a "
Checks iCloud sync status.
Parameters: None
Returns: Whether sync is active, pending uploads, and last activity time.
Lists all notes shared with collaborators.
Parameters: None
Returns: List of shared notes with warnings about collaboration.
User: "Create a note called 'Todo' with my tasks for today"
AI: [calls create-note with title="Todo", content="Tasks for today..."]
"I've created a note called 'Todo' with your tasks."
User: "What notes do I have?"
AI: [calls list-notes]
"You have 15 notes: Todo, Shopping List, Meeting Notes..."
User: "Show me the Shopping List"
AI: [calls get-note-content with title="Shopping List"]
"Here's your shopping list: - Milk - Eggs - Bread..."
By default, all operations use iCloud. To work with other accounts:
User: "What accounts do I have?"
AI: [calls list-accounts]
"You have 3 accounts: iCloud, Gmail, Exchange"
User: "List notes in my Gmail account"
AI: [calls list-notes with account="Gmail"]
"Your Gmail account has 5 notes..."
User: "Create a folder called 'Archive'"
AI: [calls create-folder with name="Archive"]
"Created folder 'Archive'"
User: "Move my old meeting notes to Archive"
AI: [calls move-note with title="Old Meeting Notes", folder="Archive"]
"Moved 'Old Meeting Notes' to 'Archive'"
User: "What folders do I have?"
AI: [calls list-folders]
"You have 5 folders: Work, Work/Clients, Work/Clients/Omnia, Archive, Recipes"
User: "Create a note in Work/Clients about Acme Corp"
AI: [calls create-note with title="Acme Corp", content="...", folder="Work/Clients"]
"Created 'Acme Corp' in Work/Clients"
npm install -g apple-notes-mcpgit clone https://github.com/sweetrb/apple-notes-mcp.git
cd apple-notes-mcpThe repo ships a prebuilt, dependency-free build/index.js, so a bare clone runs with nothing but Node installed. pnpm install and pnpm run build are only needed when you change the source (development uses pnpm, not npm).
You can also install straight from the git repo with npm install -g github:sweetrb/apple-notes-mcp (building from source requires pnpm), but the published npm package above is the recommended path.
If installed from source, use this configuration:
{
"mcpServers": {
"apple-notes": {
"command": "node",
"args": ["/path/to/apple-notes-mcp/build/index.js"]
}
}
}This repo ships a .mcp.json at its root so that, when you run claude from inside a clone, the server is registered automatically as a project-scope server — no manual config needed. Just launch Claude Code from the repo directory and approve the server when prompted (the bundled build/index.js is committed, so no build step is required).
The entrypoint is written as:
"args": ["${CLAUDE_PROJECT_DIR:-.}/build/index.js"]CLAUDE_PROJECT_DIR is the variable Claude Code injects into a project/user-scoped server's environment, and it resolves to the repo root. You must launch claude from inside the repo for this to work — the bare . fallback is only a last resort and is not reliable, because it resolves against the launching process's working directory, not the repo.
Why not
${CLAUDE_PLUGIN_ROOT}?CLAUDE_PLUGIN_ROOTis set only for marketplace plugin installs, never for a project-scope clone, so it can't drive the clone workflow. Conversely, a plugin install can't useCLAUDE_PROJECT_DIR(in a plugin, that points at the user's project, not the plugin's own directory). Claude Code does not support nested defaults like${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}, so a single entrypoint string cannot serve both contexts. The two distribution paths are therefore decoupled: the plugin carries its own MCP config in.claude-plugin/plugin.json(using${CLAUDE_PLUGIN_ROOT}), while the root.mcp.jsonis dedicated to the clone workflow (using${CLAUDE_PROJECT_DIR:-.}). Becauseplugin.jsondeclares its ownmcpServers, the plugin does not also auto-load the root.mcp.json, so there is no double-registration.
Heads-up on scope precedence: project-scope (
.mcp.json) outranks user-scope. If you also have anapple-notesentry registered at user scope (e.g. an absolute path in~/.claude.json), the project-scope entry wins and the user-scope one is ignored entirely. Pick one — for local development on this repo, the project-scope.mcp.jsonis the intended source. To pin a specific local build instead, register it at local scope (claude mcp add apple-notes -s local -- node /abs/path/build/index.js), which outranks project scope.
All configuration is optional — the server works out of the box. Override behavior with these variables (set them in your MCP client's env block, or via the config file below):
| Variable | Default | Description |
|---|---|---|
APPLE_NOTES_MCP_MAX_BUFFER |
67108864 (64 MB) |
Max bytes captured from a single AppleScript invocation. Raise it if a very large export/list is truncated; lower it to cap memory. |
APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES |
26214400 (25 MB) |
Max size of an attachment that fetch-attachment will base64-encode inline. Larger attachments are rejected with an error pointing at save-attachment (which streams to disk and has no such limit). Raise it to fetch bigger attachments inline; lower it to cap memory. |
APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES |
262144 (256 KB) |
Per-image cap on the base64 payload kept inline in a get-note-content response. Inline images over the cap are replaced with placeholders (with a warning appended) so an image-heavy note cannot exceed the MCP client's message limit and drop the connection; export the real files with save-attachment or fetch-attachment. Raise it to keep bigger images inline. |
APPLE_NOTES_MCP_CONFIG_FILE |
~/Library/Application Support/apple-notes-mcp/config.json |
Path to the JSON config file (see below). |
APPLE_NOTES_MCP_TIMEOUT_MS |
30000 (30 s) |
Per-call AppleScript timeout. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call timeoutMs options still win. |
APPLE_NOTES_MCP_MAX_RETRIES |
2 |
Total attempts for an AppleScript call that fails with a transient error (Notes.app busy / not responding / lost connection / timeout). 2 means one retry; set 1 to fail fast with no retries. Non-transient errors (e.g. "note not found") never retry. |
APPLE_NOTES_MCP_RETRY_DELAY_MS |
1000 (1 s) |
Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
DEBUG / VERBOSE |
unset | Set either to enable verbose diagnostic logging to stderr. |
Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed
environment and ignore the env block in their server config, so there's no way
to pass APPLE_NOTES_MCP_* settings through it. In that case, put them in a JSON
file the host doesn't manage — APPLE_NOTES_MCP_CONFIG_FILE, or by default
~/Library/Application Support/apple-notes-mcp/config.json:
{
"APPLE_NOTES_MCP_MAX_BUFFER": "134217728",
"DEBUG": "1"
}The server reads it at startup and merges values into the environment without
overriding anything already set there (so an explicit env still wins). This
is the recommended way to configure the server under Claude Desktop. Apple Notes
MCP stores no secrets, but as a general rule keep only non-secret config here.
The get-checklist-state tool and checklist annotations in get-note-markdown read directly from the Apple Notes SQLite database. This requires Full Disk Access for the process running the MCP server.
📘 For the full why-and-how walkthrough (which app to grant, verifying with
doctor, graceful degradation), see the Full Disk Access Setup Guide. The summary below is the quick version.
- Open System Settings (or System Preferences on older macOS)
- Go to Privacy & Security > Full Disk Access
- Click the + button
- Add the application that hosts the MCP server:
- Claude Desktop: Add
/Applications/Claude.app - Terminal: Add
/Applications/Utilities/Terminal.app - VS Code: Add
/Applications/Visual Studio Code.app - iTerm: Add
/Applications/iTerm.app
- Claude Desktop: Add
- Restart the application after granting access
All other tools work normally without Full Disk Access. Only checklist state features are affected:
get-checklist-statewill return an error explaining that database access is neededget-note-markdownwill return plain list items without[x]/[ ]annotations (graceful fallback)
- Local only - All operations happen locally via AppleScript. No data is sent to external servers.
- Permission required - macOS will prompt for automation permission on first use.
- Password-protected notes - Notes with passwords cannot be read or modified via this server.
- No credential storage - The server doesn't store any passwords or authentication tokens.
| Limitation | Reason |
|---|---|
| macOS only | Apple Notes and AppleScript are macOS-specific |
| Batch ops run per-note | batch-delete-notes / batch-move-notes apply each note individually rather than as one bulk operation — AppleScript has no bulk equivalent to IMAP's UID STORE/MOVE. This is deliberate: it preserves per-note success/failure reporting. (#26) |
| No pinned notes | Pin status is not exposed via AppleScript (#28) |
| Limited rich formatting | Use format: "html" on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
| Title matching | Most operations require exact title matches |
| Checklist state | Requires Full Disk Access to read done/undone state from the database |
| Checklist creation | Not supported. AppleScript's body of note setter strips <input type="checkbox"> and ignores any checklist-styling CSS class. Apple Notes stores checklist items as a protobuf paragraph style (style_type=103) that AppleScript doesn't expose, and the SQLite database is read-only. See Creating Checklists below for the workaround. |
A few capabilities are deliberately deferred to a future release, tracked as open issues:
- Pinned-note support (#28) — Apple doesn't expose pin status via AppleScript.
- Tags / hashtags (#29).
- Note links (#30).
- Local integration-test suite (#31).
There is no programmatic way to create a true Apple Notes checklist via AppleScript — and therefore no way via this MCP server. This is an Apple limitation, not a bug.
When a note is created or updated via AppleScript:
| You send | What Notes.app actually renders |
|---|---|
<input type="checkbox"> Item |
Item (the <input> tag is stripped) |
<ul class="checklist"><li>Item</li></ul> |
A plain bulleted list — the checklist class is dropped |
Markdown - [ ] Item (in plaintext mode) |
The literal text - [ ] Item |
Apple Notes stores checklists as a paragraph style (style_type=103) inside a gzipped protobuf blob in the NoteStore.sqlite database. AppleScript's note body interface does not expose paragraph styles, and writing directly to the live database is unsafe.
Workarounds:
- Create the note with bulleted list items, then convert manually in Notes.app. Select the items and press ⇧⌘L (or Format → Checklist). This converts the list in place and the resulting checklist will be readable by
get-checklist-stateand annotated byget-note-markdown. - Use the Apple Shortcuts app to script the checklist creation, since Shortcuts can manipulate Notes content at a higher level than AppleScript.
- Read-only checklist support is fully implemented — once a checklist exists (created manually or by another app),
get-checklist-stateandget-note-markdownwill read its done/undone state correctly (with Full Disk Access).
If you need to track todos programmatically and don't strictly need them rendered as Apple Notes checklist UI, plain markdown-style - [ ] item / - [x] item lines in a plaintext note are a reasonable alternative — they are searchable, human-readable, and can be parsed by downstream tooling.
When sending content containing backslashes (\) to this MCP server, you must escape them as \\ in the JSON parameters.
Why: The MCP protocol uses JSON for parameter passing. In JSON, a single backslash is an escape character. To include a literal backslash in content, it must be escaped as \\.
Example - Shell command with escaped path:
{
"title": "Install Script",
"content": "cp ~/Library/Mobile\\\\ Documents/file.txt ~/.config/"
}The \\\\ in JSON becomes \\ in the actual string, which represents a single \ in the note.
Common patterns requiring escaping:
- Shell escaped spaces:
Mobile\ Documents→Mobile\\\\ Documentsin JSON - Windows paths:
C:\Users\→C:\\\\Users\\\\in JSON - Regex patterns:
\d+→\\\\d+in JSON
If you see errors when creating/updating notes with backslashes, double-check that backslashes are properly escaped in the JSON payload.
- Ensure Notes.app is not frozen
- Try opening Notes.app manually
- Restart the MCP server
- macOS needs automation permission
- Go to System Settings > Privacy & Security > Automation
- Ensure your terminal/Claude has permission to control Notes
- Note titles must match exactly (case-sensitive)
- Check if the note is in a different account
- Use
list-notesto see available notes
- Content containing
\characters requires JSON escaping - Use
\\to represent each literal backslash - See "Backslash Escaping" section under Known Limitations
- Repeatedly updating a note (especially with HTML content) can accumulate whitespace artifacts —
<div><br></div>tags that persist between sections even after you remove them from your content - Apple Notes' internal HTML processing preserves empty divs from previous edits, so the gaps are baked into the note's internal representation and cannot be fixed through further updates
- Fix: delete the note with
delete-noteand create a fresh one withcreate-note
- Launch
claudefrom inside the repo directory soCLAUDE_PROJECT_DIRresolves to the repo root (the bare.fallback is unreliable — it points at the launching process's working directory) - If you've been editing the source, rerun
pnpm run build— the entrypoint is${CLAUDE_PROJECT_DIR:-.}/build/index.js, and the committed bundle only reflects your changes after a rebuild - Run
claude mcp listto check for a conflictingapple-notesentry at another scope (project-scope outranks user-scope, but local-scope outranks project-scope) - Approve the pending project-scope server when Claude Code prompts you
Development uses pnpm (see packageManager in package.json):
pnpm install # Install dependencies
pnpm run build # Typecheck, then bundle src/index.ts into build/index.js (esbuild)
pnpm test # Run unit test suite (mocked AppleScript)
pnpm run test:integration # Run integration tests against real Notes.app
pnpm run test:all # Unit + integration
pnpm run lint # Check code style
pnpm run format # Format codeThe integration suite (test/integration.test.ts) drives the real
AppleNotesManager → AppleScript → Notes.app stack — creating, reading,
searching, and deleting throwaway notes. Its live tests self-skip when no
writable Notes account is available (e.g. CI), so it is safe to run anywhere;
the pure path-safety and hashtag tests always run.
Rob Sweet - President, Superior Technologies Research
A software consulting, contracting, and development company.
- Email: rob@superiortech.io
- GitHub: @sweetrb
MIT License - see LICENSE for details.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Part of a family of macOS MCP servers:
- apple-mail-mcp — MCP server for Apple Mail (read, search, send, and organize email)
- apple-numbers-mcp — MCP server for Apple Numbers (read and write .numbers spreadsheets)
- apple-photos-mcp — MCP server for Apple Photos (query metadata and export originals)
If macOS keeps re-prompting for Full Disk Access or Automation for node (often after a brew upgrade), the cause is almost always an ad-hoc-signed Node (typically Homebrew's): its code signature (cdhash) changes on every update, so macOS TCC treats each new build as a brand-new binary and silently drops the grants you already made. The fix is to run this server under an official, Developer-ID-signed Node at a stable path — its signing identity stays the same across updates, so you grant the permission once and it persists. The doctor tool detects the ad-hoc-signature case and the full walkthrough is in docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md.
