Apple Notes MCP

by sweetrb• 174•• Scanned•Claim this listing•Request removal
Otheraiappleapple notesapple shortcutsapplescript

MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants

Apple Notes MCP is an open-source mcp server by sweetrb, written mainly in TypeScript, with 174 GitHub stars and the MIT license. Its code was last updated Oct 30, 2026. The latest npm release is apple-notes-mcp 2.16.8, published Nov 12, 2026, with 7,035 downloads last week.

Apple Notes MCP Server

A Model Context Protocol (MCP) server that lets Claude (Claude Code and Claude Desktop), Codex, and other MCP clients read, search, create, edit, organize, and export notes in Apple Notes on macOS. It runs locally on your Mac and talks to Notes through AppleScript, Apple Shortcuts, and read-only access to the Notes database.

Beyond creating and editing notes, it manages folders and accounts, native tags, checklists, tables, pinned notes, links, and attachments. It can also export notes as Markdown (through templates you can edit in a local web editor), HTML (with classic drawings as SVG), or JSON, read audio transcripts and drawings, link to single paragraphs with anchors that survive edits, and run a query language across your whole library.

npm version npm downloads node CI OpenSSF Scorecard platform: macOS License: MIT MCP

<p align="center"> <img src="https://raw.githubusercontent.com/sweetrb/apple-notes-mcp/main/codex/assets/screenshot.png" alt="Apple Notes MCP — create, search, and organize Apple Notes from Codex, Claude, and other AI assistants" width="680"> </p>

Contents

What is This?

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. Some features also use packaged Apple Shortcuts (native tags, checklists, tables, pinning) or read the Notes database read-only (queries, checklist state, transcripts). Notes operations and speech recognition run locally. If you explicitly enable downloadAssets: true, the public helper may download speech models; what your MCP client does with tool results is up to that client.

<a id="quick-start"></a>

Setup on your Mac

Apple Notes MCP runs on the Mac that holds your Notes library. Your assistant starts a local MCP server over stdio, and the server uses AppleScript, signed Apple Shortcuts, and read-only database access. Choose one client setup below, then complete the macOS permissions and optional feature setup you need.

Version boundary: these instructions describe v2.15.1, including the optional permission broker. Normal client registration starts an in-process server unless a matching broker is already installed and answering. Broker installation is a separate opt-in step.

Manual client entries, setup commands, and the source checkout below select v2.15.1. Run setup from the same release as the server your client actually launches. The Claude Code plugin bundles its server; the Codex plugin manifest launches an unpinned npm package even when its marketplace is pinned. For reproducible Codex setup, use the manual registration below instead of the plugin. When upgrading, update the selected version consistently in the client and setup commands.

Run each setup target separately and use the exact options shown. Version 2.15.1 does not reject all unknown or mixed setup flags; a misspelled target can fall through to Shortcut setup and open import windows.

Requirements

  • macOS with Notes.app and at least one configured Notes account. Features depend on the installed macOS version and account type; inspect get-capabilities before relying on optional features.
  • Node.js 20 or newer. The npx examples also need npm, normally included with Node.js.
  • A client that can launch a local stdio MCP server on this Mac. The configurations below cover Claude Code, Claude Desktop, Codex, Hermes, and Antigravity.
  • Automation permission to control Notes.app for AppleScript operations. Database-backed operations also need Full Disk Access for the process macOS holds responsible for the server.
  • Apple Shortcuts for native writes such as native tags, checklists, tables, pinning, and rich append. The optional native Markdown creation route needs macOS 26 or newer and an iCloud account.
  • An Apple developer toolchain only if you choose to build an optional native helper or permissions window. The public helper builds with older SDKs too; only a macOS 26 or newer SDK compiles in its SpeechAnalyzer route (see macOS compatibility).

macOS compatibility

What works depends on the macOS release. get-capabilities reports this for the Mac it runs on (features.<name>.minimumMacOSVersion, osSupported, and a reason such as requires_macos_13), so check it there rather than relying on this table alone. The server is developed and tested on current macOS; rows for older releases come from each feature's declared minimum and Apple's API availability, not from testing on those releases.

FeaturemacOS 12macOS 13–15macOS 26 and later
AppleScript core: create, read, search, update, move and delete notes and folders (applescriptCore)YesYesYes
Notes database reads with Full Disk Access: queries, structure, checklist state, exports, attachments (fullDiskAccessReads)YesYesYes
get-note-link without Full Disk AccessYes, AppleScript fallbackYes, AppleScript fallbackNo, needs Full Disk Access
Listing bridge Shortcuts (doctor, setup --check, shortcutsBridges)Yes, by name (12's shortcuts list cannot show identifiers)YesYes
Native Tags and Background Operations bridges: native tags, checklists, tables, pinning, note links, append-native (nativeTagsBridge, backgroundOperationsBridge)No: they call Notes App Intents actions, which macOS 12 does not haveMinimum declared; untestedYes
Markdown note creation through the Markdown bridge (markdownNoteBridge)NoNoYes, iCloud account only
Public native helper build (setup --public-helper)Builds with the SDK that release's Xcode or Command Line Tools provide; not tested on macOS 12 itselfBuildsBuilds
Classic drawing decode (get-note-drawings, needs the helper and Full Disk Access)YesYesYes
On-device transcription (transcribe-note-audio, needs the helper and Full Disk Access)SFSpeechRecognizer: needs a Speech Recognition grant for the app that launches the server, and an on-device model for the localeSame as macOS 12SpeechAnalyzer, no grant needed, when the helper was built with a macOS 26 or newer SDK; otherwise as on macOS 12
Stored audio transcripts (get-audio-transcripts)With Full Disk AccessWith Full Disk AccessWith Full Disk Access

Before macOS 13, setup does not open the Tags or Background bridge for import, and setup and setup --check report both as skipped with requires_macos_13.

The public helper compiles its SpeechAnalyzer route only when the SDK provides it. With an older SDK the same helper builds without it and transcribes through SFSpeechRecognizer on every release, even on macOS 26 and later. Node.js 20 or newer is required everywhere.

On macOS 12 the privacy panes are under System Preferences → Security & Privacy → Privacy, not System Settings. macOS never prompts for Full Disk Access on any release: nothing will ask, and you add the app or executable yourself (see which process needs it).

Python is not an Apple Notes MCP runtime dependency. The package includes its compiled JavaScript server and signed Shortcut files. Python is used by developer scripts that regenerate those workflows, not by normal installation or MCP tool calls.

You do not need an Apple ID password, API key, or NotesCTL installation to configure this server. A client's own login and tool-approval policy are separate from macOS privacy permissions.

1. Choose one client registration

Use either a plugin or a manual MCP entry for the same client. Registering both can load duplicate servers or leave you using a different package version than expected. Adding a client entry does not grant macOS permissions or import Shortcuts.

Claude Code

For the plugin, run these commands inside a Claude Code session:

/plugin marketplace add sweetrb/apple-notes-mcp#v2.15.1
/plugin install apple-notes@apple-notes-mcp

The plugin bundles the server and the Apple Notes skill. For a manual MCP registration instead, run this in your terminal:

claude mcp add apple-notes -s user -- npx -y apple-notes-mcp@latest

Check the connection with /mcp in Claude Code. claude mcp get apple-notes inspects the manually registered server. Project-scoped .mcp.json entries require workspace/server approval and can override a user-scoped entry.

Claude Desktop

Open Settings → Developer → Edit Config. Merge the following server entry into ~/Library/Application Support/Claude/claude_desktop_config.json, preserving any existing servers:

{
  "mcpServers": {
    "apple-notes": {
      "command": "npx",
      "args": ["-y", "apple-notes-mcp@latest"]
    }
  }
}

Fully quit and reopen Claude Desktop. This npx route does not require a preceding global npm installation. If the desktop app cannot find npx, use an absolute executable path or the fixed Node configuration below. The desktop app may have a different PATH from your interactive shell.

Codex

For the plugin and its Apple Notes skill:

codex plugin marketplace add sweetrb/apple-notes-mcp --ref v2.15.1
codex plugin add apple-notes@apple-notes-mcp

For a manual MCP registration instead:

codex mcp add apple-notes -- npx -y apple-notes-mcp@latest

The equivalent entry in ~/.codex/config.toml is:

[mcp_servers.apple-notes]
command = "npx"
args = ["-y", "apple-notes-mcp@latest"]

If codex is not on your terminal's PATH, use the desktop client's Settings → MCP servers → Add server: choose STDIO, enter npx with arguments -y and apple-notes-mcp@latest, save, and restart the server.

Restart the relevant local Codex session or client. Use /mcp in the CLI or the client's MCP settings to inspect the connection. codex mcp list lists manually configured servers. The desktop client, CLI, and IDE extension share the Codex configuration; select local execution on the Mac containing your Notes library.

The plugin's server entry runs the published npm package through unpinned npx, despite the marketplace ref above. Its server may therefore differ from v2.15.1. Use the manual entry to pin the runtime for these instructions, or run setup from the release matching the plugin's actual server. This is a different distribution path from the Claude Code plugin's bundled server.

Hermes

Register a local stdio server:

hermes mcp add apple-notes --command npx --args -y apple-notes-mcp@latest

Or merge this entry into ~/.hermes/config.yaml:

mcp_servers:
  apple-notes:
    command: npx
    args: ["-y", "apple-notes-mcp@latest"]

Restart the Hermes session. This repository supplies an MCP configuration route for Hermes; see its CLI commands and MCP configuration reference.

Antigravity

In the IDE, open MCP Servers → Manage MCP Servers → View raw config. Merge the same mcpServers JSON entry shown for Claude Desktop into the global ~/.gemini/config/mcp_config.json, or use the workspace configuration at .agents/mcp_config.json. Refresh or restart the client so it loads the server. Google's MCP guide describes the current menu and configuration paths.

The repository's .antigravity-plugin/mcp_config.json supplies an unpinned stdio server entry; the JSON above pins the audited release. Use command and args; this package does not provide a remote serverUrl.

Gemini Spark (macOS Local MCP)

In Gemini macOS builds that offer Local MCP, open Connected Apps, click + Custom, and select Local MCP in "Set up a custom connected app". Fill in the form with:

FieldValue
Server NameApple Notes
DescriptionRead, search, create, edit, organize, and export Apple Notes on this Mac.
Commandnpx -y apple-notes-mcp@latest

You can print these instructions from the installed server:

npx -y apple-notes-mcp setup --gemini-spark

The command only prints the guide. It exits before loading server configuration, checking Notes, or running the other setup commands, and it doesn't register the app or change permissions. Exit 0 means the guide was printed, not that Gemini connected. Run it by itself; combining it with other setup options exits 2.

Keep Sandbox settings at Default initially, then click Next and review the discovered tools and permission requests before connecting. If Gemini can't launch the server, inspect Show Logs and check whether npx is available to the app. For a stable local installation, npm install -g apple-notes-mcp provides the apple-notes-mcp command; you can also use absolute paths to your Node executable and the installed build/index.js if the app can't find them. Keep that installation available, and update the command if you move it or replace Node.

Gemini's sandbox and macOS permissions are separate. Review any additional filesystem access the launcher needs; Full Access isn't a required setup step. Automation of Notes.app enables the AppleScript tools, while database tools need the runtime's Full Disk Access. Shortcut operations need the packaged bridge setup above. After connecting, ask Gemini to run get-capabilities for tool availability, then doctor for Notes access diagnostics before attempting a write. A successful terminal check doesn't verify the permissions of Gemini's process.

This integration uses the Mac app's Local MCP command interface and the server's existing stdio transport. Availability depends on your Gemini build. Google's custom apps documentation currently describes URL-based setup in the web app; it doesn't document this local form. Cloud MCP, Gemini web/mobile, and a hosted HTTP endpoint aren't supported by this server. These instructions apply to Gemini Spark, separately from Gemini CLI and Antigravity.

Other MCP clients and remote execution

A local MCP client can use the command npx with arguments -y and apple-notes-mcp@latest. Follow that client's stdio configuration format: JSON mcpServers, TOML, and YAML are not interchangeable.

Apple Notes MCP has no built-in HTTP, SSE, WebSocket, or OAuth MCP endpoint. A cloud-only client cannot access your Mac's Notes library merely by installing the npm package in a Linux container or entering a URL. A separately managed transport bridge or client feature that delegates tool execution to your Mac needs its own configuration and security review; it is not part of this package's supported setup. GUI reveal and clipboard tools also need your logged-in desktop session.

The optional anchors serve command is a paragraph-link resolver, and templates edit is a template editor. Neither is a remote MCP server.

<a id="full-disk-access"></a> <a id="permissions-check"></a>

2. Verify the server and macOS access

In the configured client, ask it to run get-capabilities and then doctor. get-capabilities inspects feature availability without opening Notes or running the bridges. doctor also sends a read-only Apple event to Notes and may trigger the macOS Automation prompt. Run these from the actual client because a successful terminal check does not establish that a different desktop app has the same permissions.

Access or componentWhat it enablesHow to complete it
Automation of Notes.appAppleScript reads and writesAllow the macOS prompt for the responsible app to control Notes. Inspect Privacy & Security → Automation if access was denied.
Full Disk AccessDatabase reads, rich-content inspection and safety checks, query tools, native object reads, and database-backed exportsmacOS never prompts for it. Add the responsible executable/app under Privacy & Security → Full Disk Access (macOS 12: System Preferences → Security & Privacy → Privacy), fully quit and relaunch the host, then rerun doctor.
Shortcut installation and consentNative tag, checklist, table, pin, link, and append routesImport the bridges and complete their foreground consent steps below.
Public helper and, where required, Speech RecognitionClassic drawing decoding and on-device audio transcriptionBuild the optional helper; follow the permission result from doctor or the tool.

Which process needs Full Disk Access? macOS never shows a Full Disk Access prompt, so nothing will ask; you add the entry yourself. For a brokered session, grant the signed Apple Notes MCP Broker.app access; doctor reports whether that session uses it. Without the broker, Claude Desktop needs the exact Node executable printed by doctor; granting Claude.app alone does not reach the server in that launch path. Add that exact path: a Node found some other way (which node in a shell, another install, or a Homebrew symlink) can be a different binary. For a terminal-launched client, the terminal app is normally the responsible app; for an editor-launched server, check that editor. Most other desktop clients stay responsible for the servers they launch, so grant the client app itself (and the Node path from doctor if that alone does not work). Use the actual running client's diagnostics instead of assuming that all clients share grants.

Client sandbox access is a separate boundary. A sandboxed client must be able to launch the executable and reach its files; a brokered client also needs access to the broker's local Unix socket. Full Disk Access and Automation grants do not expand the client's sandbox. Diagnose a launch or socket-access refusal in the client before changing macOS privacy permissions.

To inspect setup from a terminal without opening permission panes or requesting Automation access:

npx -y apple-notes-mcp@latest setup --permissions --check --json

The Automation result is intentionally unknown in this mode. When you are at the Mac and want to check it, this command sends a read-only Apple event and may show the macOS consent prompt:

npx -y apple-notes-mcp@latest setup --permissions --once --probe-automation

For an interactive checklist that also opens the relevant settings panes:

npx -y apple-notes-mcp@latest setup --permissions --open --probe-automation

The command checks permissions; it does not grant them. --check and SSH sessions suppress the Automation probe even when --probe-automation is supplied. Without the probe, exit status 0 does not establish Automation access.

Full Disk Access is optional if you only need operations that can safely run through AppleScript, but many capabilities and safety checks depend on it. It can also be required by guards on existing-note writes, including rich-content and revision verification; it is not merely an enhancement for read tools. Do not interpret an unavailable database-backed feature as an empty result. In particular, sync results without database visibility do not prove that iCloud sync is idle. Use the tool reference and get-capabilities for individual requirements, and see the Full Disk Access guide.

3. Enable the native Shortcut features you need

Run explicit setup on your Mac:

npx -y apple-notes-mcp@latest setup

Approve Add Shortcut for the missing signed workflows that macOS opens:

  • Notes MCP Tags
  • Notes MCP Background v6
  • Notes MCP Markdown, offered only on macOS 26 or newer and optional for native Markdown creation

Releases before 2.16.1 installed these as Apple Notes MCP - Native Tags, Apple Notes MCP - Background Operations v6 and Apple Notes MCP - Create Markdown Note. Those still work. To switch to the short names, run setup after deleting the old ones in Shortcuts.app. Delete Apple Notes MCP - Background Operations v5 in any case; the server no longer uses it.

Then check installation:

npx -y apple-notes-mcp@latest setup --check

The first two bridges determine this check's readiness; the optional Markdown bridge is reported separately. Keep only one installed copy of each named bridge. The server refuses duplicate bridge names.

After installation or upgrade, run each installed bridge once in the foreground in Shortcuts.app and answer its access prompts. Choose Always Allow if you want that bridge to run unattended. For the Markdown bridge, use the first-run request in shortcuts/README.md; running it without input stops before it asks Notes for access. That request creates a disposable check note.

An installed bridge is not proof of consent: shortcuts list, setup --check, and doctor cannot observe the first-run consent state. A background native write can stall on an unanswered prompt. After a timeout or an indeterminate write result, read the exact note before retrying because the write may already have happened.

4. Build optional helpers

These commands are separate from registering the MCP client. If xcrun swiftc or clang is unavailable, install Apple's Command Line Tools with xcode-select --install before building a helper. The public helper compiles its macOS 26 SpeechAnalyzer/SpeechTranscriber route only when the SDK declares those APIs; with an older SDK it builds with the SFSpeechRecognizer route only. A build against the macOS 15.4 SDK targeting macOS 12 compiles; running it on macOS 12 itself is untested.

Public native helper

<a id="public-native-helper"></a>

Needed for get-note-drawings and transcribe-note-audio. Brokered sessions do not load external helpers; set APPLE_NOTES_MCP_BROKER=off for this client and restart it before using these tools:

npx -y apple-notes-mcp@latest setup --public-helper
npx -y apple-notes-mcp@latest setup --public-helper --check

It compiles locally, signs ad hoc, verifies its handshake, and records source and binary checksums. The server refuses stale or modified helpers; rerun setup after a relevant package upgrade. This helper uses public Apple frameworks and does not open or write the Notes database. The server still needs Full Disk Access to read the drawing/audio assets it passes to the helper.

On older macOS versions, speech transcription needs Speech Recognition access for the responsible host. The helper never requests this silently. On macOS 26 and newer, the SpeechAnalyzer route has been observed to work without a prior grant; explicit denial or restriction still blocks it. Inspect the returned capability/permission status on your Mac.

Private helper setup

Optional read-only NotesShared state inspection, disabled by default. Brokered sessions do not load this helper; set APPLE_NOTES_MCP_BROKER=off for this client and restart it first:

npx -y apple-notes-mcp@latest setup --native-helper
npx -y apple-notes-mcp@latest setup --native-helper --check

To enable it, set the string environment value APPLE_NOTES_MCP_ENABLE_PRIVATE=1 for the server and restart the client. Inspect native-helper-status before using native-note-state. This released helper opens the store read-only and cannot provide deep native writes. Apple's private API may change with macOS updates; the remaining server features do not require this helper.

Optional checklist window

Optional graphical presentation of the checklist:

npx -y apple-notes-mcp@latest setup --permissions-window
npx -y apple-notes-mcp@latest setup --permissions --window

It displays the command's checks and opens settings on request. It does not become the permission owner or replace the client process.

5. Confirm the capabilities you intend to use

After restarting the client, run doctor and get-capabilities again. Confirm that the specific routes you need are available, not just that the server connected. Private helper status, public helper readiness, Markdown creation, and the native-write bridges are separate checks.

Use read-only operations first, such as listing accounts/folders and reading a note you select. If you want to exercise a write route, use a dedicated disposable note and verify the result by its exact ID. A successful plain note creation does not validate native tables, checklists, or Markdown creation; test the feature you plan to use.

Keep the server's revision and scope checks enabled. Updates, appends, and deletes use exact IDs and content hashes; native routes may also require a distinctive existing scopeText. Full-body updates refuse rich notes they cannot preserve. Follow the returned error and capability information instead of falling back to a lower-level write.

Features

FeatureDescription
Create NotesCreate notes from plaintext, HTML, or Markdown, with optional folder/account targeting (Markdown creation uses an optional Shortcut on macOS 26 or later)
Search NotesFind notes by title or search within note content
Query Languagequery-notes combines text, folder, account, tag, attachment, checklist, flag, word-count, and date conditions with AND/OR/NOT, read from the Notes database (requires Full Disk Access)
Read NotesRetrieve note content as HTML, plain text, or Markdown, plus metadata that AppleScript does not expose (pinned, Quick Note, locked, Recently Deleted)
Note StructureDecode a note into typed paragraph blocks, list its paragraphs and links, and get a direct link to one paragraph (requires Full Disk Access)
Update NotesReplace, append to, or prepend to an existing note, guarded by a content hash so a note that changed since it was read is never overwritten
Delete NotesRemove notes (moves to Recently Deleted)
Move NotesOrganize notes into folders (supports nested paths)
Folder ManagementCreate, list, rename, and delete folders with full hierarchical path support; read the folder tree with note counts and Smart Folder rules
Multi-AccountWork with iCloud, Gmail, Exchange, or any configured account, including account IDs and default folders
Batch OperationsDelete or move multiple notes at once
Native TagsList, add, remove, and replace real Notes tags (not just #hashtag text)
ChecklistsRead checklist done/undone state from the Notes database, and append real checklist items through a Shortcut
TablesRead native tables as Markdown and JSON, and append new native tables
PinningRead pinned state and pin or unpin a note
LinksGet a note's notes:// deep link, insert web, mail, or note-to-note links, and list the links in a note or folder
ExportExport notes as paginated JSON, or render a note or folder as Markdown (with reusable templates) or standalone HTML
AttachmentsAdd files to notes (from a path or the pasteboard), list attachments with their on-disk paths, find a note's lead image, save or batch-export them, or fetch their bytes as base64
Audio and DrawingsRead the transcripts Notes stored for recordings, transcribe audio on-device, and decode drawings to strokes and SVG
Incremental Synclist-recent-notes pages through notes by modification time with an exact cursor
Notes.app UI StateReveal a note, folder, account, or attachment in Notes.app, or read the current Notes.app selection
Sync AwarenessDetect iCloud sync in progress, warn about incomplete results
CollaborationDetect shared notes, warn before modifying
Diagnosticshealth-check plus a richer doctor (server version, reachability, automation permission, accounts, Full Disk Access), sync status, statistics, and get-capabilities for the Shortcut bridges

Read/list/get tools also return structured JSON (structuredContent) alongside the text, so agents can consume results without parsing prose.

Some clients (Claude Desktop among them) pass the model only a result's text and drop structuredContent. So every result that has structuredContent also ends with one more text block holding the same data as a single JSON line:

structuredContent: {"id":"x-coredata://…/ICNote/p123","contentHash":"sha256:…","writable":true,…}

That line carries every token a follow-up call needs: the contentHash revision token for expectedContentHash, new ids, nativeTags, writable, page.nextOffset, and an error's code, committed and indeterminate. A long value the text above already shows in full, such as the body from get-note-content, appears as "[shown in full above]" instead of twice. If the line would still exceed 16 KB, fields larger than 1 KB (usually a list already printed above) are left out and named in _omitted. Tools whose text already is that JSON get no extra block. structuredContent itself is unchanged.

MCP resources & prompts

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.

AppleScript limitations

A few Notes UI features are not exposed to AppleScript. Some are recovered by reading Notes' own database instead; the rest genuinely cannot be supported. See docs/APPLESCRIPT-LIMITATIONS.md for the investigation and verification behind each:

  • Pinned notes — Notes has no scriptable pinned property via AppleScript. Pin state is read from the NoteStore database by the BETA get-note-metadata tool and list-special-notes, and set through the Background Operations Shortcut by set-note-pinned.
  • Note-to-note links — AppleScript exposes no link property or link element. Links are instead read from the NoteStore database by list-note-links, and inserted by insert-note-link (through a Shortcut) or insert-link. A shareable notes://showNote?identifier=<uuid> deep link is available via get-note-link.

Tool Reference

This section documents all available tools. AI agents should use these tool names and parameters exactly as specified.

Identifier forms

Every tool that takes a note id (id, noteId, ids, linkedNoteId, or the id inside a batch entry) accepts any of three forms of the same note:

FormExample shapeNeeds Full Disk Access
AppleScript id (canonical)x-coredata://<store-uuid>/ICNote/p123No
Notes UUID (identifier)8-4-4-4-12 hex digits, as in notes://showNote?identifier= linksYes
Numeric Core Data key123 (the digits after p)Yes

The server turns a UUID or numeric key into the canonical id before the tool runs, reading the Notes database read-only. A numeric key or UUID resolves only to a note, never to a folder or attachment. Without Full Disk Access those two forms fail with an error that says so, while x-coredata ids keep working as before. Folder-id inputs (show-folder, get-folder-by-id, rename-folder) accept a folder's UUID or numeric key the same way, resolving only to folders.

When Full Disk Access is granted, list and read tools also return stable identifiers next to each id: notes carry identifier, folderIdentifier, and accountIdentifier; folders carry identifier, parentIdentifier (nested folders only), and accountIdentifier; accounts carry identifier. These fields come from one batched read-only query per call and are omitted when the database cannot be read. Tools that return them: search-notes, list-notes, get-selected-notes, list-shared-notes, get-note-content, get-note-by-id, get-note-details, list-folders, get-folder-by-id, list-accounts, and get-default-location.

Error results

A failed call returns isError: true with the same human-readable text as before, plus structuredContent carrying a stable machine-readable code (also repeated in the trailing structuredContent: text line, for clients that drop structuredContent). Branch on code, not on the prose, which may be reworded.

codeMeaning
not_foundThe note, folder, account, attachment, or checklist does not exist
ambiguousMore than one item matched; use an exact id
permission_deniedmacOS refused Automation access to Notes.app
full_disk_access_missingThe Notes database is not readable; grant Full Disk Access
shortcut_not_installedA native-write bridge Shortcut is not installed exactly once
timeout_indeterminateThe operation timed out; for a write, the outcome is unknown
verification_failedThe write ran, but exact-ID readback did not confirm it
revision_conflictThe note changed since it was read
validation_errorThe request was rejected before anything ran
unsupportedNot supported for this note or in this mode, such as a locked note
notes_unavailableNotes.app is not running, busy, or not responding
operation_failedThe server could not classify the failure; read the text

Two optional booleans describe a write's outcome when it is known. indeterminate: true means the outcome is uncertain: read the target by exact id before any retry, and never retry blindly. committed: true means the write took effect even though its verification failed; committed: false means nothing was written (for example a revision_conflict). An absent flag means unknown. An argument that fails the input schema is rejected before the tool runs, with code: "validation_error" and committed: false. The exception is a Notes UUID or numeric key that could not be resolved to an id: it carries not_found, or full_disk_access_missing when the database is unreadable.

Note Operations

create-note

Creates a new note in Apple Notes.

ParameterTypeRequiredDescription
titlestringYesThe title of the note. Automatically prepended as <h1> — do NOT include the title in content
contentstringOne of content/contentPathThe body content of the note (do not repeat the title here)
contentPathstringOne of content/contentPathAbsolute path of a local UTF-8 file to use as the body instead of content. Allowed in the same places save-attachment may write (home, temp, /Volumes), except hidden paths (any component starting with ., such as ~/.ssh or a project .env) and ~/Library other than iCloud Drive (~/Library/Mobile Documents) and cloud storage folders (~/Library/CloudStorage), which can hold credentials; set APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1 to allow those. Symbolic links, non-regular files, invalid UTF-8 and files over 1 MiB are refused before anything is written. A leading byte-order mark is dropped
tagsstring[]NoReturned-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. Inline #hashtags in content stay searchable text and are returned as hashtags, but they do not become native Notes tags; add real tags afterwards with add-native-tags. Refused with format: "markdown"
folderstringNoFolder to create the note in. Supports nested paths like "Work/Clients". The folder must already exist — create it first with create-folder. A smart folder is refused (see move-note). Defaults to account root
accountstringNoAccount name (defaults to Notes.app's default account; matched exactly or by a unique prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see list-accounts
formatstringNoContent format: "plaintext" (default), "html", or "markdown". In all formats, the title is automatically prepended as the note's title line. In plaintext mode, newlines become <br>, tabs become <br>, and backslashes are preserved as HTML entities. "markdown" produces real Title/Heading/Subheading styles through a Shortcut; see Markdown notes
markdownRoutestringNoWith format: "markdown" only: "shortcut" (default) or "html". See Markdown through HTML
timeoutSecondsnumberNoWhole seconds, 1–120, for each Notes.app automation step this call runs; overrides APPLE_NOTES_MCP_TIMEOUT_MS for this call only. A timed-out write is uncertain, not failed: read the note by id before any retry. Also accepted by get-note-content, update-note, append-to-note, delete-note and move-note

Example (with inline textual 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 the content parameter, or the title will appear twice.

Known limitation: with "plaintext" or "html", create-note sets the note body directly via AppleScript's body property, which does not apply real Notes paragraph styles for interior content — an <h2>/<h3> tag or a <span style="font-size: …px"> heading span in content renders as plain bold, styled text, not an actual Heading or Subheading (#172). Use format: "markdown" for real headings in a new note, or append-native's format: "markdown" on a note that already exists.

Markdown notes

format: "markdown" creates the note with Notes' own Markdown importer (the Create Note action's "Interpret as Markdown" option, macOS 26+), run through the packaged Notes MCP Markdown Shortcut. #, ## and ### become real Title, Heading and Subheading styles, and the note starts with the title line, with no seed line.

{
  "title": "Project Plan",
  "content": "## Goals\n\n- Ship the beta\n- Collect feedback\n\n### Links\n\n[Tracker](https://example.com/tracker)",
  "format": "markdown",
  "folder": "Work"
}
  • Notes interprets Markdown only in an iCloud account. The note is created in the iCloud account's default folder, then moved to folder in that account. folder must already exist and is checked before anything is created. If it exists only in another account, the note is still created and verified in the iCloud default folder, and the error names that account and the note's id so you can create the folder there and move-note it instead of creating the note again. account is refused with this format.

  • tags are refused with this format. Create the note without them, then add native tags to the returned id with add-native-tags.

  • content accepts the same bounded subset as append-native's Markdown: #/##/### headings, flat lists, **bold**, *italic* and inline links. It also refuses Markdown that Notes would rewrite and the server could not verify: _ emphasis (underscores inside a word, as in snake_case, and inside a link destination, as in [docs](https://example.com/_next/static), are fine), backslash escapes, character references such as &amp;, === lines and rule lines other than the --- divider described below, indented headings or list items, 1) lists, closing #s, and formatting inside link labels. Content that needs one of these literally, such as a /_next path or a literal \*, has no Markdown form here: use format: "html" for that note (or append-native with format: "html" on an existing one), which keeps the characters but not the Heading and Subheading styles. Markdown punctuation in title is escaped, so the title stays literal.

  • Notes' importer also maps these block constructs to native styles. They are gated separately, and get-capabilities reports them as create-note-markdown-blocks. Each mapping below was live-verified on macOS 27.2 by reading the created note's stored styles.

    MarkdownNative result
    - [ ] item / - [x] itemChecklist item, unchecked / checked
    > text (consecutive lines form one quote)Body paragraph with a block quote
    A fence of bare ``` lines (no language)Monospaced paragraphs; the code text is kept literally
    --- on its own line, after a blank lineDivider line
    `inline code`Highlighted text, not monospace

    Constructs Notes would not render faithfully, or that this server cannot yet verify, are refused before anything is created: ~~~ fences, a language after the opening fence, nested (>>) or indented quotes, lists or headings inside a quote, a quote followed directly by text (Markdown would join that text to the quote), --- directly under text (Markdown would make that text a heading), ***/___/---- rules, [X], * [ ] or + [ ] items, checklist items directly next to ordinary list items, inline code padded with spaces or inside a link label, tables, and ~~strikethrough~~. The readback checks the block-quote text, the Monospaced text, each checklist item's text and done state, the divider count, and the highlighted text. These mappings apply to the default Shortcut route only; see Markdown through HTML for markdownRoute: "html".

  • append-native's format: "markdown" refuses all of the constructs above: its Shortcuts converter is a different one, which renders a quote and a fenced block as plain body text, - [ ] as a bullet with literal brackets, and inline code as plain text, and drops ---.

  • The server finds the new note among the notes added to the default folder during the run by verifying each one's visible text, heading levels and links by exact-ID readback, and moves it only after exactly one verifies. On any uncertain result it names the note (or says to search for the title) and never retries.

  • It is gated like the other native operations; get-capabilities reports it as create-note-markdown. The Create Markdown Note Shortcut is optional and needed only for this format (macOS 26+); install and approve it as described in shortcuts/README.md.

  • A first line that is exactly # <title> (same case and spacing as title) is removed together with one blank line after it, because the title is supplied separately; the response then carries strippedDuplicateTitle: true. A different first heading stays in the body. This applies to both Markdown routes, and Markdown that holds only that heading is refused.

Markdown through HTML

markdownRoute: "html" imports the same bounded Markdown subset without the Shortcut: the server converts it to HTML and creates the note through AppleScript, like format: "html". It works in any account, accepts tags, and needs no Shortcut, but headings get the plain bold styling described in the known limitation above rather than real Heading and Subheading styles.

On this route, bullet task items (- [ ] item, - [x] item) become ordinary list rows that start with a visible ☐ or ☑ character, and the response reports how many as taskItemsRendered. They are text, not native checkable checklist items. Use this route as the glyph fallback when the Shortcut is not installed or the note is outside iCloud. Block quotes, fenced code and inline code, which the Shortcut route maps natively, are refused on this route, and a --- line stays literal text rather than becoming a divider.

{
  "title": "Weekly Review",
  "contentPath": "/Users/me/Documents/weekly-review.md",
  "format": "markdown",
  "markdownRoute": "html",
  "folder": "Work"
}

Returns: Confirmation message with note title and ID. Save the ID for subsequent operations like update-note, delete-note, etc.


search-notes

Searches for notes by title or content.

ParameterTypeRequiredDescription
querystringYesText to search for
searchContentbooleanNoIf true, searches note content (title line included); if false (default), searches titles only. With Full Disk Access, content search reads the Notes database (well under a second, the 5000 most recently modified notes, Recently Deleted excluded); without it, it falls back to AppleScript, which scans every body and can time out on a broad term
accountstringNoAccount to search in (defaults to Notes.app's default account; exact or unique-prefix match)
folderstringNoLimit search to a specific folder (supports nested paths like "Work/Clients")
modifiedSincestringNoISO 8601 date string to filter notes modified on or after this date (e.g., "2025-01-01")
limitnumberNoMaximum number of results to return. Defaults to 50 — a broad query reads several properties per match via AppleScript (~200ms/note), so an unbounded search over hundreds of matches can exceed Notes' 30s timeout and return an error instead of results. Pass a higher value to see more; the applied limit (and whether it truncated the results) is disclosed in the response.
includeWordCountbooleanNoAdd wordCount to each result (null when the note is locked or its body unreadable). A database content search already has the text. Otherwise the bodies are read in one batched read-only database query, never one AppleScript call per note; that needs Full Disk Access and also adds matchedIn. Without it, the response carries wordCountUnavailable and the results are unchanged.

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. A content search also returns source ("database" or "applescript"), and scanTruncated when the database path left older notes unsearched.

When the note text came from the database (a database content search, or any search with includeWordCount), each result also carries matchedIn: ["title"], ["body"], or ["title", "body"], saying where the query text occurs. The body is the text after the first line. matchedIn is absent for locked notes and for AppleScript searches without includeWordCount. The text output appends the same details to each line, for example · matched in title, body · 245 words.

Example - Content search with word counts:

{
  "query": "budget",
  "searchContent": true,
  "includeWordCount": true
}

query-notes

Finds notes with a boolean query expression evaluated against the NoteStore database, read-only. Because it does not go through AppleScript, a query over several hundred notes typically returns in well under a second, and one call can match titles and bodies together.

Requires: Full Disk Access for the MCP host process (see Full Disk Access Setup). Without it, use search-notes.

ParameterTypeRequiredDescription
querystringYesQuery expression (syntax below), at most 2000 characters
limitnumberNoMaximum notes to return. Defaults to 50, maximum 500. The response reports the total match count.
scanLimitnumberNoHow many of the most recently modified notes to examine. Defaults to 500, maximum 10000. The response says when older notes were left unscanned. A large scan decodes more bodies, so it takes longer.
includeDeletedbooleanNoAlso scan notes in Recently Deleted, notes pending deletion, and folderless notes. Defaults to false.
includeWordCountbooleanNoAdd wordCount to each returned note, the same count words: filters on (null when locked or unreadable). Free when the query already reads bodies; a metadata-only query (for example pinned) reads just the returned notes' bodies in one extra read-only query. Defaults to false.

Syntax:

FormMatches
budget, "quarterly budget"Title or body contains the word or phrase (case-insensitive substring)
title:x, body:x, text:xTitle only, body only (text after the first line), or either
folder:Work, folder:"Work/Clients"The note's own folder, by name or full path, case-insensitive (notes in subfolders are not included); a literal / in a name can be written \/ as in list-folders
account:iCloudAccount name, case-insensitive
tag:financeNative Notes tag (with or without #); textual hashtags are ordinary words
has:link, has:attachment, has:checklist, has:drawing, has:image, has:video, has:audio, has:pdf, has:table, has:scan, has:url, has:map, has:tagThe note body contains that kind of object. has:url is a link preview card (it also counts as has:link, as inline links do); has:map is a map attachment
checklist:open, checklist:doneAt least one unchecked item; or items present and all checked
pinned, locked, shared, quicknote (or is:pinned …)Note flags; shared includes notes in a shared folder, and quicknote is a note created as a Quick Note (the same flag list-special-notes kind=quick-notes reads)
words:>250Word count, with =, >, >=, <, <=
created:>=2026-07-01, modified:<2026-09-01Dates as YYYY-MM-DD in local time, with the same operators; = means that whole day
a b, a AND b, a OR b, NOT a, -a, ( … )AND is implicit and binds tighter than OR

Operators are case-insensitive. Quote an operator or flag word to search it literally, for example "and" or "pinned". Queries are capped at 256 tokens and 64 levels of nesting. An unknown field such as titel:x is an error rather than a silent text search; quote it to search the literal text.

Password-protected notes match on title and metadata only. Their bodies are encrypted, so a body predicate is unknown for them rather than false: neither body:x nor -body:x matches them, though -body:x OR pinned matches a pinned one. Notes whose body cannot be decoded are treated the same way. Their snippets are always empty.

Example - Open to-dos in a folder:

{
  "query": "folder:\"Work Projects\" has:checklist -checklist:done"
}

Example - Invoices or finance-tagged notes since July, scanning more history:

{
  "query": "(title:invoice OR tag:finance) modified:>=2026-07-01",
  "scanLimit": 2000,
  "limit": 20
}

Returns: Matching notes, most recently modified first, each with id, title, folder, account, modified, created, and a snippet centred on the first matched phrase. Each note also has matchedIn when the query has a positive text term: ["title"], ["body"], or both, saying where those terms occur (a title: term is only looked for in the title, a body: term only in the body). It is absent for locked or undecodable notes, and an empty list means the note matched through a non-text branch such as pinned OR x. The ids are the same x-coredata://…/ICNote/p… form every other tool accepts. structuredContent also reports matched (total matches), scanned, eligible, scanTruncated, truncated, and unreadable (bodies that could not be decoded). A malformed query returns an error naming the problem and its position.


get-note-content

Retrieves the full content of a specific note.

ParameterTypeRequiredDescription
idstringNoNote ID (preferred - more reliable than title)
titlestringNoNote title (use id instead when available)
accountstringNoAccount containing the note (defaults to Notes.app's default account; exact or unique-prefix match, ignored if id is provided)
timeoutSecondsnumberNoWhole seconds, 1–120, for the body read; overrides APPLE_NOTES_MCP_TIMEOUT_MS for this call only

Note: Either id or title must be provided. Using id is recommended as it's unique and avoids issues with duplicate titles.

Busy Notes.app: with an id and Full Disk Access, the note's existence and password flag are read from the NoteStore database (as get-note-structure does), so only the body read goes through Notes.app. A timeout is always reported as timeout_indeterminate, never as not_found, even when an automatic retry then fails differently; and when Notes.app cannot resolve an id the database still holds, the error is notes_unavailable. Retry in a moment, or use get-note-structure for a read that does not need Notes.app.

Large images: Notes.app returns images inside the body as base64, so a note holding a very large image (tens of MB) can take longer to read than the timeout allows. When the read times out or overflows the output buffer, the error says so and, with Full Disk Access, names the attachments of 5 MB or more. Retry with a larger timeoutSeconds; delete-note accepts the same argument, and it needs a successful read to verify the note before deleting it.

Example - Using ID (recommended):

{
  "id": "x-coredata://ABC123/ICNote/p456"
}

Example - Using title:

{
  "title": "Shopping List"
}

Returns: The HTML content of the note, its exact id, and a contentHash. Pass that hash back as expectedContentHash for a later update, append, or delete; the write is rejected if the note's body or rich metadata changed after this read. With Full Disk Access, embedded URLs omitted by AppleScript are restored and returned in links. The response also reports actual nativeTags, richContentComplete, and writable. Textual hashtags remain a separate field and are not proof that Notes registered native tags. writable is also false when the note uses formatting that AppleScript's HTML does not carry (superscript, subscript, non-left paragraph alignment, or highlight): a full-body update-note would silently drop it, so it is refused and the warning names the formatting. append-to-note with scopeText still works through the native path, which verifies existing formatting.

⚠️ The returned body can be lossy — do not write it back verbatim. Inline base64 images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with [inline image omitted: …] text placeholders so an image-heavy note cannot blow the MCP message limit. structuredContent reports this as strippedImages (count) and truncated (boolean). When either is set, passing this body to an unguarded full-body writer would replace the real images with placeholder text. This server's update-note refuses attachment-bearing notes, and append-to-note routes them to native end-append with scopeText, which never rewrites the existing body; make any other edit in Notes.app.


get-native-objects

Reads native object identities and ranges, checklist item IDs and state, actual native tags, and native table data from one exact note ID. Table output includes stable row and column identifiers. tableCellsComplete is false when Notes metadata cannot be decoded completely. This tool is read-only and requires Full Disk Access.

ParameterTypeRequiredDescription
idstringYesExact note ID, in any identifier form

get-note-tables

Reads a page of native tables in one exact note, in body order, from the NoteStore database. Each table is returned as GitHub-flavored Markdown and as JSON rows with stable rowIds and columnIds. Notes tables have no header row, so the Markdown uses the first row as the header. Pipes are escaped as \|, backslashes are doubled, and line breaks inside a cell become <br>.

ParameterTypeRequiredDescription
idstringYesExact note ID (x-coredata://…/ICNote/pNNN)
offsetintegerNoZero-based position of the first table (default 0); use page.nextOffset
limitintegerNoMaximum tables per page (default and maximum 500)
maxBytesintegerNoFull serialized response budget in UTF-8 bytes (default 4 MiB, minimum 1024 bytes, maximum 8 MiB)

The byte budget includes both the text response and structured data, including repeated Markdown and JSON escaping. Tables are returned whole. tableCount and page.totalAvailable count all tables in the note; page.returned counts the entries in this response. While page.hasMore is true, call again with offset: page.nextOffset and the same limit and maxBytes. Each call reads the note again, so finish paging before editing it.

If one table exceeds an otherwise empty page, its entry contains identifying metadata, complete: false, contentOmitted: true, and a reason, without partial rows or row/column IDs. The next offset advances past that entry. To retrieve its content, retry that offset with a larger maxBytes, or export the note to a file. If even the metadata exceeds the budget, the tool returns a validation error. page.stoppedAtSizeLimit reports a byte-budget stop.

tableCellsComplete is false when any table or cell could not be decoded, or when the current page omits a table's content for its byte budget. A cell holding an embedded object is null in rows, listed in incompleteCells, and shown as [undecoded cell] in Markdown. A table whose data cannot be decoded at all has complete: false, a reason, and no rows. Cell text is never guessed. Links and styling inside cells are not rendered. A note without tables returns an empty tables list. This tool is read-only, requires Full Disk Access, and refuses password-protected notes.


get-note-blocks

Decodes one note's body into typed blocks, read-only, from the NoteStore database. Each block is one paragraph with its style (title, heading, subheading, body, monospaced, bulleted, dashed, numbered, checklist, or unknown with the raw styleType), indent, alignment, blockQuote, checklist id/done, and paragraphUuid when stored. Each block lists its inline runs with bold, italic, underline, strikethrough, superscript, subscript, color, highlight, link (plus linkSafe), font, and attachment, and its attachment markers in body order. Offsets and lengths count UTF-16 code units. summary counts styles and inline attributes for the whole note. undecodedFields lists stored field numbers the decoder deliberately does not interpret.

Requires: Full Disk Access. Password-protected notes are refused.

ParameterTypeRequiredDescription
idstringYesExact note ID
offsetnumberNoFirst block to return (default 0). Use page.nextOffset
limitnumberNoMaximum blocks per page (default 500, max 5000)

A page also stops early to stay under APPLE_NOTES_MCP_BLOCKS_MAX_BYTES (default 4 MB). paragraphUuid is not unique: Notes copies it when a paragraph is split. Link URLs are returned as stored, and linkSafe is false for schemes other than http(s), notes:, applenotes: and mailto:. Errors carry a stable code in brackets, such as [encrypted] or [no-full-disk-access].


list-note-paragraphs

Lists one note's non-empty paragraphs in body order, read from the NoteStore database. It never changes Notes. With recordAnchors it also writes the local anchor registry, so the tool is not marked read-only in its MCP annotations. Each paragraph has blockIndex (its index in get-note-blocks, where empty paragraphs also count), text, style, styleType, paragraphId (the UUID Notes stores on the paragraph's first text run), and paragraphIdStatus:

  • unique: no other paragraph in the note carries this ID. The paragraph also gets url, a direct applenotes://showNote?identifier=<note>&paragraphID=<paragraph> link that opens Notes at that paragraph.
  • shared: other paragraphs carry the same ID (sharedWith counts them). Notes copies the ID when a paragraph is split, so this is common for body text. No url is given, because it could open the wrong paragraph.
  • missing: no ID is stored.

mixedParagraphIds: true marks a paragraph whose text runs carry more than one ID; only the first-run ID is used. Titles and headings usually have unique IDs.

Requires: Full Disk Access. Password-protected notes are refused.

ParameterTypeRequiredDescription
idstringOne of id, titleExact note ID, or the note's Notes UUID
titlestringOne of id, titleExact note title; must match one note. Notes in Recently Deleted are ignored, as in list-notes
folderstringNoWith title: the folder's name or full path as list-folders shows it, such as Work/Clients
linkableOnlybooleanNoReturn only paragraphs with a url
offsetnumberNoFirst paragraph to return (default 0). Use page.nextOffset
limitnumberNoMaximum paragraphs per page (default 500, max 5000)
recordAnchorsbooleanNoAlso record a paragraph anchor for each returned paragraph (at most 500 per call); each row gains anchorId and the result gains anchorsRecorded (the new ones). Writes only the local anchor registry

get-paragraph-link

Returns a direct link to one paragraph of a note: applenotes://showNote?identifier=<note>&paragraphID=<paragraph>. It selects the note like list-note-paragraphs and the paragraph by exactly one of:

  • contains: a snippet of the paragraph. Case, runs of spaces, and Unicode width are ignored.
  • match: the whole paragraph, compared the same way.
  • blockIndex: from list-note-paragraphs.

When contains or match hits several paragraphs, pass occurrence (1-based) or a longer snippet.

The link is returned only when the paragraph's ID is unique in the note. Otherwise the result is an error whose structuredContent carries the usual code plus a reason: paragraph-id-shared, paragraph-id-missing, no-match, ambiguous-paragraph, occurrence-out-of-range, ambiguous-note (several notes have that title; the message lists their folders), not-found, encrypted, or no-body. The tool never creates or changes a paragraph ID, and an edit in Notes can later replace the ID and break the link. With recordAnchor it also writes the local anchor registry, so the tool is not marked read-only in its MCP annotations.

Requires: Full Disk Access.

ParameterTypeRequiredDescription
id, title, folderstringOne of id, titleNote selector, as in list-note-paragraphs
containsstringOne of contains, match, blockIndexSnippet of the paragraph
matchstringOne of contains, match, blockIndexThe whole paragraph
blockIndexnumberOne of contains, match, blockIndexThe paragraph's blockIndex
occurrencenumberNoWhich match to use when several paragraphs match
recordAnchorbooleanNoAlso record a paragraph anchor for the linked paragraph and return its anchorId. Writes only the local anchor registry

Paragraph anchors

A paragraph link breaks when Notes replaces or copies the paragraph's ID. A paragraph anchor records enough about the paragraph to find it again: the note's Notes UUID, the paragraph ID and its status, the paragraph's normalized text and a fingerprint of it, the fingerprints of the paragraphs before and after it, its blockIndex, and when it was recorded. Anchors are kept in one local file, ~/Library/Application Support/apple-notes-mcp/paragraph-anchors.json (or the absolute path in APPLE_NOTES_MCP_ANCHOR_FILE). It is created with mode 0600 in a 0700 directory, replaced atomically under a lock file, and holds the text of every anchored paragraph. A symlinked, unreadable or corrupt registry is refused and left as it is. The anchor tools never change Notes.

Resolving an anchor tries three steps in order, and every step fails closed:

  1. Paragraph ID. A paragraph whose ID is still unique in the note is the anchored paragraph, even if its text was edited or it moved. A link to that ID opens exactly that paragraph.
  2. Exact text. The same normalized text. When several paragraphs have it, only the one whose neighbours still match is chosen; a tie is ambiguous.
  3. Text and neighbours. Similar text between both recorded neighbours, or nearly identical text beside one of them. Two close candidates are ambiguous; one below minConfidence is low-confidence.

A match whose ID is now shared or missing is needs-reminting: the paragraph is found (the result names its block), but no safe link exists until it gets a new ID. Public automation cannot set a paragraph ID, so this server only reports it and returns no url.

To share anchored links outside Notes, see the paragraph anchor resolver.

create-paragraph-anchor

Records an anchor for one paragraph, selected exactly as in get-paragraph-link (note by id or title/folder; paragraph by one of contains, match, blockIndex, plus occurrence). Unlike get-paragraph-link, a paragraph whose ID is shared or missing can be anchored. Recording the same paragraph again (same note, ID, text and block) returns the existing anchor with created: false.

Returns anchor, created, and url when the paragraph currently has a unique ID.

Requires: Full Disk Access. Writes only the anchor registry.

ParameterTypeRequiredDescription
id, title, folderstringOne of id, titleNote selector, as in list-note-paragraphs
contains, match, blockIndex, occurrenceOne of contains, match, blockIndexParagraph selector, as in get-paragraph-link

resolve-paragraph-anchor

Finds an anchored paragraph in the note as it is now. The result has status (resolved, needs-reminting, ambiguous, low-confidence, not-found, note-not-found, note-deleted for a note in Recently Deleted, or note-unreadable for a locked note), method (paragraph-id, exact-text, text-and-neighbours), confidence (0 to 1), match (blockIndex, text, paragraphId, paragraphIdStatus), changes (textChanged, blockIndexChanged, paragraphIdChanged), and message. url, the current applenotes:// link, is present only when status is resolved.

Requires: Full Disk Access.

ParameterTypeRequiredDescription
anchorIdstringYesThe anchor (pa_ and 24 hex digits)
minConfidencenumberNoLowest confidence accepted as a match (default 0.6)
refreshbooleanNoAfter a match with confidence 0.8 or more, store the paragraph as it is now (text, neighbours, block, ID), so later edits are tracked from here. Reports refreshed or refreshSkipped

list-paragraph-anchors

Lists recorded anchors in the order recorded, with total, the registry path and page info. Reads only the registry.

ParameterTypeRequiredDescription
noteIdentifierstringNoOnly anchors in the note with this Notes UUID
offsetnumberNoFirst anchor to return (default 0)
limitnumberNoMaximum anchors (default 100, max 1000)

get-paragraph-anchor

Returns one stored anchor as recorded. Reads only the registry.

ParameterTypeRequiredDescription
anchorIdstringYesThe anchor

prune-paragraph-anchors

Removes anchors that no longer resolve, or the anchors you list. It is a dry run unless dryRun is false. Without anchorIds it resolves every anchor in scope and treats not-found and note-not-found as stale. A note in Recently Deleted (note-deleted) is kept by default because it can be restored. Returns stale (anchorId, status, message), examined, and removed.

Requires: Full Disk Access unless anchorIds is given.

ParameterTypeRequiredDescription
anchorIdsstring[]NoRemove exactly these anchors instead of resolving
noteIdentifierstringNoOnly anchors in the note with this Notes UUID
statusesstring[]NoStatuses treated as stale (default not-found, note-not-found)
dryRunbooleanNoReport without removing (default true)

get-note-structure

Returns a read-only overview of one note from the NoteStore database, in one call:

  • text (decoded body), textLength, wordCount, charCount (Unicode characters, attachment placeholders excluded), and blockSummary (the get-note-blocks summary counts).
  • links, each with a kind: inline (a hyperlink on text), card (a rich link preview attachment, with attachmentId and previewPath), note (a native link chip to another note), or section (a native link chip to a heading or paragraph, with section). Notes deep links also carry targetNote and paragraphId. linkCounts totals them.
  • tags (native tags in body order) and attachments, listed the way list-attachments with includePaths lists them: the same kind (image, scan, drawing, pdf, audio, video, url, table, other), the same body order, and the same previewPath, plus the card title/url, fileSize, and body position. Gallery items and recording parts are nested under children; attachmentCount counts top-level attachments only.
  • deepLink, isShared (the note or any enclosing folder is shared), isLocked, isPinned, inRecentlyDeleted, lastViewed, checklistTotal, checklistDone, hasDrawing (classic sketches and Paper drawings), and firstImage (the same lead visual list-attachments returns with firstImage, plus its attachment id).

lastViewed is an ISO date, or null with lastViewedStatus set to never-viewed, not-recorded, malformed, or unsupported (the column does not exist on this macOS version). A password-protected note returns its metadata and attachment rows with bodyDecoded: false and the body-derived fields null.

Requires: Full Disk Access.

ParameterTypeRequiredDescription
idstringYesExact note ID: the x-coredata:// id, or the note's Notes UUID or numeric key (see Identifier forms)
includeTextbooleanNoInclude the decoded text (default true). Text larger than APPLE_NOTES_MCP_BLOCKS_MAX_BYTES is omitted with textOmitted: true

Link URLs are returned as stored; check linkSafe before emitting one into HTML.


list-note-links

Lists links, read-only from the NoteStore database, in one note (id) or across a folder, an account, or the whole library (no selector). Each link has a kind:

KindWhat it isWhere Notes stores it
inlineA hyperlink on textInside the note body
cardA rich link previewAn attachment row with the URL and title
noteA native link chip to another noteAn inline-attachment row with a Notes deep link
sectionA native link chip to a heading or paragraphSame, with paragraphID in the deep link

Each row has url, text (label), linkSafe, targetNote and paragraphId for Notes deep links, section for section chips, and for cards attachmentId and previewPath (Notes' largest cached preview image, found the same way list-attachments finds it, or null when Notes has none). Each row also names its source: noteId, noteIdentifier, noteTitle, noteModified, folder, folderPath (as list-folders prints it), account, and accountIdentifier. When bodies were decoded, start and blockIndex give the link's position, and inBody: false marks a card or chip row with no marker left in the body.

Inline links need every body in scope decompressed and decoded, so a folder, account, or library scan includes them only with includeInline: true (slower). A single note always includes them. A folder scope includes its subfolders unless includeSubfolders is false. Scans skip Recently Deleted and folderless notes; a note requested by id is read wherever it is. Links come newest-modified note first, in body order within a note.

Requires: Full Disk Access.

ParameterTypeRequiredDescription
idstringNoOne exact note ID (x-coredata id, Notes UUID, or numeric key). Do not combine with account or folder
accountstringNoAccount name (exact or unique-prefix match, as in the other tools)
folderstringNoFolder name (any depth, must be unique) or path from the top level as list-folders prints it, such as Work/Clients; escape a literal slash as \/
includeSubfoldersbooleanNoWith folder, also list notes in its subfolders (default true)
includeInlinebooleanNoDecode bodies for inline links (default true for id, false otherwise)
kindsstring[]NoOnly these kinds: inline, card, note, section
offsetnumberNoFirst link to return (default 0). Use page.nextOffset
limitnumberNoMaximum links per page (default 200, max 2000)

The response also reports scope, counts per kind, notesInScope, notesWithoutBody (locked, empty or undecodable bodies when inline links were requested), and page. A page stops early to stay under APPLE_NOTES_MCP_BLOCKS_MAX_BYTES. An ambiguous folder name is refused with a message that lists the matching paths.


list-native-tags

Lists actual native Notes tags. This differs from textual hashtag search. It is read-only and requires Full Disk Access.

  • Folder mode (pass folder, optionally account): maps each tag used in that folder to its matching note IDs. The response reports complete: false and per-note errors when some native metadata is unavailable.
  • Inventory mode (omit folder): an account-wide inventory with counts, read-only from the database. account narrows it to one account; omit it to count every account. Each inventory entry has tag, noteCount (distinct notes, Recently Deleted excluded), per-account counts in accounts, and any other spellings Notes treats as the same tag. Tags with no remaining notes are listed with noteCount: 0. A tag counts for a note only while the note body still references it. Locked or unreadable bodies are counted from the tag objects alone and reported through unverifiedNotes and complete: false.
ParameterTypeRequiredDescription
folderstringNoFolder to list (nested paths supported). Omit for the inventory
accountstringNoAccount name, exact or unique prefix

native-tags-status

Checks whether exactly one configured Native Tags Shortcut is installed. An installed workflow may still need macOS permission on its first execution, and installation does not show whether that consent was given: run it once in the foreground in Shortcuts.app and choose Always Allow after install or upgrade (see Troubleshooting).


add-native-tags

Adds actual native Notes tag objects to one exact note using id, a fresh expectedContentHash, a distinctive existing scopeText, and tags. The operation verifies the note's original text, links, and native objects after the Shortcut runs. It refuses ambiguous title-and-scope matches and never retries an uncertain write. Install the signed workflow as described in shortcuts/README.md.

ParameterTypeRequiredDescription
idstringYesExact note ID, in any identifier form
expectedContentHashstringYescontentHash from a fresh read of the note
scopeTextstringYesDistinctive phrase already in the note, 12–500 characters
tagsstring[]Yes1–100 tag names to add

get-note-plaintext

Retrieves a note's body as plain text, with no HTML markup.

ParameterTypeRequiredDescription
idstringNoNote ID (preferred - more reliable than title)
titlestringNoNote title (use id instead when available)
accountstringNoAccount containing the note (defaults to Notes.app's default account; exact or unique-prefix match, 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.


get-note-details

Retrieves metadata about a note (without full content).

ParameterTypeRequiredDescription
titlestringYesExact title of the note
accountstringNoAccount containing the note (defaults to Notes.app's default account; exact or unique-prefix match)

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"
}

get-note-by-id

Retrieves a note using its unique CoreData identifier.

ParameterTypeRequiredDescription
idstringYesThe CoreData URL identifier (e.g., x-coredata://...), or the note's Notes UUID or numeric key (see Identifier forms)

Returns: JSON with note metadata, plus identifier, folderIdentifier, and accountIdentifier when Full Disk Access is granted, or error if not found.


show-note

Reveals a note in Notes.app using its unique CoreData identifier.

ParameterTypeRequiredDescription
idstringYesThe CoreData URL identifier (e.g., x-coredata://...), or the note's Notes UUID or numeric key
separatelybooleanNoOpen in a separate note window when supported by Notes.app

Returns: Confirmation that Notes.app accepted the show command.


update-note

Updates an existing note's content and/or title.

ParameterTypeRequiredDescription
idstringYesExact CoreData note ID returned by a read or search
expectedContentHashstringYescontentHash from the exact note version being replaced
newTitlestringNoNew title (if changing the title; ignored when format is "html")
newContentstringYesNew content for the note body
formatstringNoContent 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)
allowLinkChangesbooleanNoSet to true only when intentionally changing or removing existing links
ifFolderId, ifAncestorFolderId, forbiddenAncestorFolderIdsstring, string, string[]NoFolder preconditions; see Folder scope guards
timeoutSecondsnumberNoPer-call timeout, 1–120 seconds, as for create-note. A timed-out write is uncertain: read the note by id before any retry

Title-only updates are rejected because Apple Notes titles are not unique.

Example - Using ID (recommended):

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "newContent": "Updated content here"
}

Example - Update with HTML formatting:

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "newContent": "<p>New findings with <b>bold</b> emphasis.</p><pre><code>console.log('hello');</code></pre>",
  "format": "html"
}

Returns: Confirmation with the exact ID, post-save contentHash, and verifiedVisibleText: true, or an error if the note changed before saving. Apple Notes normalizes HTML, so this proves the visible text after saving, not byte-identical rich formatting.

Note: newContent replaces the entire note body — it is not appended. To add to a note, prefer append-to-note, which does the read-and-concatenate for you and always round-trips the body as HTML. If you do read-modify-write by hand, note that get-note-content replaces oversized inline images with text placeholders (see get-note-content) — writing that body back bakes the placeholders in.

Rich-content safety: update-note refuses to replace a note when its rich metadata is unavailable or it contains attachments, native tags, inline objects, or checklists that AppleScript cannot preserve. Existing link destinations must remain present unless allowLinkChanges is explicitly set.


delete-note

Deletes a note (moves to Recently Deleted in Notes.app).

ParameterTypeRequiredDescription
idstringYesExact CoreData note ID returned by a read or search
expectedContentHashstringYescontentHash from the exact note version being deleted
ifFolderId, ifAncestorFolderId, forbiddenAncestorFolderIdsstring, string, string[]NoFolder preconditions; see Folder scope guards
timeoutSecondsnumberNoPer-call timeout, 1–120 seconds, as for create-note. A timed-out write is uncertain: read the note by id before any retry
guardNoteIdstringNoA second note (usually a verified copy) that must still be intact; needs Full Disk Access
expectedGuardContentHashstringWith guardNoteIdcontentHash of the guard note from get-note-content
requireActiveNoteIdstringNoA second note that must still exist, be unlocked, stay outside Recently Deleted, and not be a Quick Note; its content is not fingerprinted. Needs Full Disk Access

Title-only deletion is rejected. If the note changed after the supplied hash was read, deletion is also rejected.

Notes with large images. Notes.app returns a note's body with each inline image embedded as base64, so a note holding one 40 MB image has a body of about 110 MB. Body reads accept up to 512 MB of output, and delete-note compares a body longer than 5 MB against a private temporary file rather than embedding it in the AppleScript. The comparison still covers the whole body, and the file is removed when the delete finishes.

Copy-then-retire. To delete an original only while its copy is still good, read both notes, verify the copy, and pass the copy as guardNoteId with its contentHash as expectedGuardContentHash. The copy's revision is re-read just before the delete, and its body, lock state, and folder are checked again inside the delete AppleScript, with the same fail-closed Recently Deleted test as the note being deleted. The guard note must not be a Quick Note, a flag only the database holds, so the guard needs Full Disk Access. A copy the database has not saved yet passes on the live checks alone. The pair is still not one transaction: the rich revision (which also covers checklists and attachments) is a pre-check, and the in-script check covers the body and state Notes.app exposes. requireActiveNoteId is the narrower form for a destination you wrote yourself rather than copied.

Example - Using ID (recommended):

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}

Returns: Confirmation message, or error if note not found. The delete and a check of the note's original folder run in one AppleScript: if Notes.app accepts the delete but the note is still listed in that folder, the call reports that nothing was deleted instead of claiming success.

A note that is already in Recently Deleted is refused, because deleting it there removes it permanently. The folder is read live from Notes.app in the same AppleScript; the Recently Deleted folder is recognised by its database id (with Full Disk Access) or by its English name. The check fails closed: a note whose folder Notes.app reports as something other than a folder (as it does for a note trashed earlier in the same Notes session) is refused the same way, and a note whose folder cannot be read at all is refused with a message to retry. To remove such a note for good, do it in Notes.app.

⚠️ Safety: Irreversible from the agent's side — requires explicit user confirmation before calling. Prefer search-notes / list-notes first to confirm the exact id(s) being deleted.


move-note

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.

A smart folder is never a destination: it only gathers notes by its rules, and Notes.app would move the note to Recently Deleted or store a created note where no folder shows it. create-note, create-note-with-attachment, move-note, batch-move-notes, and create-folder refuse one before writing anything, with code: "unsupported", committed: false, and reason: "smart_folder_destination". An ordinary folder that shares a smart folder's name is still found. Detection reads the NoteStore database, so it needs Full Disk Access; without it, destinations resolve as before.

ParameterTypeRequiredDescription
idstringYesExact CoreData note ID returned by a read or search
folderstringYesDestination folder name or nested path (e.g., "Work/Clients")
accountstringNoAccount whose folder is the destination (default: the default account)
ifFolderId, ifAncestorFolderId, forbiddenAncestorFolderIdsstring, string, string[]NoFolder preconditions; see Folder scope guards
timeoutSecondsnumberNoPer-call timeout, 1–120 seconds, as for create-note. A timed-out write is uncertain: read the note by id before any retry

Title-only moves are rejected.

Example - Using ID (recommended):

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "folder": "Archive"
}

Returns: Confirmation only after the same note ID is read back and its actual destination folder ID matches the requested folder.


Folder scope guards

update-note, append-to-note, delete-note, move-note, and batch-move-notes accept three optional folder preconditions. Use exact folder ids from list-folders.

  • ifFolderId: the note must currently be in exactly this folder.
  • ifAncestorFolderId: the note must be inside this folder or any of its subfolders.
  • forbiddenAncestorFolderIds (up to 50): the note must not be inside any of these folders or their subfolders. For move-note and batch-move-notes, the destination must not be either.

The checks read Notes.app's live folders inside the same AppleScript as the write, immediately before it. If the folder observed by the check fails a precondition, the write is refused with Scope guard failed: …. The check and write are separate Notes operations; another actor can move the note between them. A native append to a protected note runs through Shortcuts, so its precheck runs in a separate AppleScript before the append.

For batch-move-notes, the same preconditions apply to every supplied note ID. Each note is checked independently immediately before its move. A scope mismatch leaves that note unchanged and appears in its failure result; the remaining items continue. A batch can therefore move some notes and refuse others.

Example - retire a note only while it is still in the inbox:

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "ifFolderId": "x-coredata://ABC123/ICFolder/p12"
}

append-to-note

Appends or prepends content to an existing note without replacing it. Always reads and writes as HTML, preserving all existing rich formatting.

ParameterTypeRequiredDescription
idstringYesExact CoreData note ID returned by a read or search
expectedContentHashstringYescontentHash from the exact note version being extended
contentstringYesText to append to the note body
positionstringNo"after" (default) appends to the end; "before" inserts directly below the note's title line, so the title stays first
separatorstringNoString placed between existing content and new content (default: two newlines → <div><br></div> in HTML)
formatstringNoFormat of the content being appended: "plaintext" (default) or "html"
scopeTextstringNative-object notes onlyUnique existing phrase, as for append-native
ifFolderId, ifAncestorFolderId, forbiddenAncestorFolderIdsstring, string, string[]NoFolder preconditions; see Folder scope guards
timeoutSecondsnumberNoPer-call timeout, 1–120 seconds, as for create-note. A timed-out write is uncertain: read the note by id before any retry

Title-only appends are rejected.

Example - Append plaintext:

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "content": "New item added today"
}

Example - Prepend HTML:

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "content": "<div><b>Status:</b> done</div>",
  "format": "html",
  "position": "before"
}

Returns: Confirmation with the exact ID, post-save contentHash, and verifiedVisibleText: true. Apple Notes normalizes HTML, so this proves the visible text after saving, not byte-identical rich formatting. Warns when the note is shared with collaborators.

Safety: The append is rejected if the note changed since it was read, rich metadata is unavailable, or the note contains attachments. Existing link destinations are verified after saving.

Notes containing native objects (a table, a checklist, native tags) cannot be spliced, so they are routed to the native end-append bridge — see append-native. That path additionally requires scopeText, keeps the default blank-line separator and position: "after", and accepts a fixed HTML subset rather than anything Notes.app can render:

Native append
Elements<a> <b> <br> <code> <del> <div> <em> <h1> <h2> <h3> <i> <li> <ol> <p> <s> <span> <strong> <tt> <u> <ul>
Attributeshref on <a> (https:, http:, notes:, applenotes:, mailto: only) and a font-size style on <span>, e.g. <span style="font-size: 18px"> — the form Notes itself stores a heading as
Refusedevery other element and attribute, by name, naming the accepted subset; a table, in whole or in part (<table> <thead> <tbody> <tr> <td> <th>) — use create-table instead; comments, doctype and processing instructions

Ordinary notes take the guarded HTML path and are not restricted to that subset.


insert-link

Adds one web, mail or Notes link to an exact note as its own paragraph, then proves it from the link runs Notes actually stored. Use mode: "raw" to show the URL itself, or mode: "hyperlink" with a label to show text that links to the URL. For a link to another note by id, use insert-note-link, which looks up that note's real deep link.

ParameterTypeRequiredDescription
idstringYesExact CoreData note ID
expectedContentHashstringYescontentHash from the exact note version being extended
urlstringYesAbsolute http(s) URL with a host, or a mailto:, notes:// or applenotes: link. No spaces, <, > or "
modestringNo"raw" (default) shows the URL; "hyperlink" shows label
labelstringHyperlink onlyVisible text for mode: "hyperlink"; refused in raw mode
linkedbooleanNoRaw mode only. true (default) stores a real link on the URL text. false writes plain text with no stored link
positionstringNo"end" (default) or "after-title" (first paragraph under the title)
blankLinebooleanNoLeave a blank line between existing text and the link paragraph (default true)
scopeTextstringNative-object notes onlyUnique existing phrase, as for append-native

Example - Hyperlink under the title:

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "url": "https://example.com/report",
  "mode": "hyperlink",
  "label": "Quarterly report",
  "position": "after-title"
}

Returns: route ("applescript" for ordinary notes, "native" for notes with native objects), linkStored, storedUrl (the destination read back from the note, for example https://example.com/ for a bare origin), and the new contentHash.

Safety: The same guards as append-to-note: a fresh expectedContentHash, the attachment block, and every existing link must survive. A linked insert must add exactly one stored link with the requested label and destination; if the text lands but that proof fails, the error says the write happened so it is not repeated.

Limits:

  • Plain URL text is not linked by Notes when written this way. With linked: false, the note stores no link and readers of the body see ordinary text; Notes.app may still underline the URL on screen through its own data detection. The result reports linkStored: false.
  • Notes with native objects (a table, a checklist, native tags) take the native end-append path, so only position: "end" with blankLine: true works there.
  • The link always gets its own paragraph. Placing it at the end of one existing paragraph, or inside the text, is not available.
  • Rich URL preview cards (the link tile Notes makes when you paste a URL) are not produced. No public automation route creates one: the Shortcuts Notes actions write text, and AppleScript's body has no card markup.
  • To start a new note with a link, use create-note with format: "html" and an <a href> in content; the link is stored the same way.

get-note-link

Returns the notes://showNote?identifier=<uuid> deep-link URL for a note. The URL opens the note in Notes.app on iOS and macOS and can be stored in Reminders tasks or shared links.

ParameterTypeRequiredDescription
idstringNoNote ID (preferred - more reliable than title)
titlestringNoNote title (use id instead when available)
accountstringNoAccount containing the note (defaults to Notes.app's default account; exact or unique-prefix match, ignored if id is provided)

Note: Either id or title must be provided. Using id is recommended. Password-protected notes cannot be linked.

Example:

{
  "id": "x-coredata://ABC123/ICNote/p456"
}

Returns: notes://showNote?identifier=<uuid> URL string, plus the note id and title.

Note: Requires Full Disk Access for the process that runs the server so the Notes SQLite database is readable. On macOS 12–15 the tool also falls back to the AppleScript note link property. Run the doctor tool to verify access.


list-notes

Lists all notes, optionally filtered by folder, date, and limit.

ParameterTypeRequiredDescription
accountstringNoAccount to list notes from (defaults to Notes.app's default account; exact or unique-prefix match)
folderstringNoFilter to notes in this folder only (supports nested paths like "Work/Clients")
modifiedSincestringNoISO 8601 date string to filter notes modified on or after this date (e.g., "2025-01-01")
limitnumberNoMaximum number of notes to return
includeRecentlyDeletedbooleanNoAlso list notes in Recently Deleted, each flagged inRecentlyDeleted: true (default false)

Example - All notes:

{}

Example - Notes in a folder:

{
  "folder": "Work"
}

Example - Recent notes with limit:

{
  "modifiedSince": "2025-06-01",
  "limit": 20
}

Returns: List of notes as {title, id} pairs — notes: Array<{title, id}>, plus count. The human-readable line is - <title> [id: <id>]. Notes in Recently Deleted are left out by default and counted in excludedRecentlyDeleted (present only when nonzero); with includeRecentlyDeleted: true they are listed with inRecentlyDeleted: true and a [RECENTLY DELETED] marker. Notes.app's own listing includes them, so the Recently Deleted folder is recognised by its database id (with Full Disk Access) or by its English name. With Full Disk Access each entry also carries identifier, folderIdentifier, and accountIdentifier (see Identifier forms).

Use the returned id for any follow-up read/update/move/delete rather than re-resolving the title: titles are not unique, and a by-title lookup resolves a duplicated title to the same one note every time, silently skipping the others.

Changed in 2.7.0: notes was previously string[] (titles only). Callers that treated the array as strings must now read .title.

For date order, incremental sync cursors, Recently Deleted, or word counts, use list-recent-notes.


list-recent-notes

Lists notes from the NoteStore database (read-only) by stored modification time, with an exact cursor for incremental sync. With since, it pages through changes oldest first; without it, it shows the newest notes first. Unlike list-notes, it can include Recently Deleted and can count words.

Requires: Full Disk Access for the MCP host process (see Full Disk Access Setup).

ParameterTypeRequiredDescription
accountstringNoOnly this account (exact or unique-prefix name). Omit for every account
folderstringNoOnly notes directly in this folder: a full path in list-folders syntax, or a unique folder name
sincestringNoReturn notes after this point, oldest first. A modifiedCheckpoint cursor (from a row or nextSince), or an ISO 8601 date (local midnight) or date-time (local time unless it carries Z or an offset), meaning modified strictly after it
limitnumberNoMaximum rows, 1–1000 (default 50)
includeDeletedbooleanNoAlso return notes in Recently Deleted, notes awaiting deletion, and folderless notes (default false)
wordCountsbooleanNoDecode each body and add wordCount and charCount (default false)
bodyPreviewbooleanNoAdd bodyPreview (up to 180 characters) and textDecoded (default false)

Returns: notes, each with id, identifier, title, folder, account, created, modified, modifiedCheckpoint, pinned, locked, inRecentlyDeleted, and markedForDeletion. Also count, limit, order (oldest-first or newest-first), saturated, and nextSince.

  • Cursors. modifiedCheckpoint is an opaque token (cdts1:, 16 hex digits, :, the note's database key). It carries the exact stored timestamp bits, so it never loses precision the way an ISO string can, and the key orders notes that share one timestamp. Pass it back as since.
  • Syncing. Call with since, store nextSince, and pass it as the next since. nextSince is the last returned row's cursor, or the incoming boundary when nothing matched, so every call advances. saturated is true when count equals limit: more changes may follow, so call again right away. When it is false, you are caught up. Notes that share a timestamp are split across pages by key, so none is skipped or repeated.
  • First sync. Start with since: "1970-01-01" and page the same way. A library larger than the 1000-row maximum is reached in full.
  • Browsing. Without since, rows come newest first. nextSince is then the newest row's cursor, set only when the call returned every matching note (saturated is false); otherwise it is null.
  • Late edits. A modification-date cursor can miss an edit that iCloud delivers later from another device with an older timestamp, for example after that device was offline. The row then sorts before the cursor. Run a full pass from the start now and then to catch these.
  • Deletions. Deleted notes are invisible unless includeDeleted is true. With it, notes in Recently Deleted and notes awaiting deletion appear, flagged, when their modification date is after the cursor. A note purged from the database leaves no row, so compare ids against a full pass to detect it.
  • Word counts. A word is a whitespace-separated token containing a letter or digit, except that Chinese, Japanese, Thai, Lao, Khmer and Myanmar text, which has no spaces between words, is split at word boundaries (the same count query-notes uses for words: and wordCount); charCount counts Unicode code points. Attachment markers are not counted. Both are null for locked notes and bodies that are not downloaded or cannot be decoded, and 0 for a body known to be empty.
  • Previews. With wordCounts, bodyPreview comes from the decoded body and textDecoded is true. Otherwise it is the stored snippet. Locked notes never get a preview.
  • Folderless notes (abandoned Quick Note drafts Notes.app never shows) and Recently Deleted appear only with includeDeleted. Notes without a stored modification date never match a since query.

Example - incremental sync:

{
  "since": "cdts1:41c7e0ef438fcd6f:4312",
  "limit": 200
}

get-selected-notes

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.


Folder Operations

list-folders

Lists all folders in an account with full hierarchical paths.

ParameterTypeRequiredDescription
accountstringNoAccount to list folders from (defaults to Notes.app's default account; exact or unique-prefix match)

Example:

{}

Returns: List of folders with IDs, paths, account names, and shared state, plus identifier, parentIdentifier, and accountIdentifier when Full Disk Access is granted. 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). Smart folders are listed too, because Notes' AppleScript lists them like ordinary folders; with Full Disk Access they carry smartFolder: true (and (smart folder) in the text list). A smart folder cannot hold notes or folders.


list-smart-folders

Lists every Smart Folder with the rules that define it, read from the NoteStore database. Requires Full Disk Access.

ParameterTypeRequiredDescription
includeMatchingNotesbooleanNoAlso list the notes each smart folder currently shows (default false)
limitnumberNoMaximum matching notes per folder, 1–500 (default 50). matchingNoteCount is always the total

Example:

{
  "includeMatchingNotes": true,
  "limit": 20
}

Returns: For each smart folder: name, id, identifier, account, accountId, accountIdentifier, parent, parentId, and parentIdentifier (the parent fields are null at the account root). Its rules are decoded as match ("all", "any", or "none") and filters. Each filter has a type (the stored rule key, such as folder, tag, checklist, or creationDateRelativeRange), its stored value, excluded for an Exclude rule, and a readable description. Folder filters add the folder's name and folderId, and a nested rule group is a filter of type group with its own match and filters. Notes stores each query inside an outer {"deleted": false} wrapper that keeps Recently Deleted out. query is the stored query with that wrapper removed, includesRecentlyDeleted reports the wrapper's value, and rawQuery is the stored JSON verbatim. A rule this server does not recognize is kept as a filter of type unknown, and fullyDecoded is then false.

With includeMatchingNotes, each folder also carries matchingNoteCount and matchingNotes (title and id). These come from Notes.app itself, which evaluates the folder's rules, so they need Automation permission. The server does not re-evaluate the rules on its own. A folder whose notes cannot be read carries matchingNotesError instead.


list-folder-tree

Returns the folder hierarchy with note counts for each account, read from the NoteStore database in one pass.

Requires: Full Disk Access for the MCP host process (see Full Disk Access Setup).

ParameterTypeRequiredDescription
accountstringNoOnly this account (exact or unique-prefix name). Omit for every account
includeDeletedbooleanNoInclude folders marked for deletion (flagged markedForDeletion) and folders whose account no longer exists (default false)

Returns: accounts, each with account, identifier, noteCount, and folders. Each folder node has id, identifier, name, path (in list-folders syntax), kind (folder, smart, or trash), noteCount (notes directly inside), totalNoteCount (including subfolders), and children. Regular folders sort by name, then smart folders, then Recently Deleted. An account's noteCount sums its regular folders. Smart folders report 0 because their contents are a saved search. Also returns folderCount.


create-folder

Creates a new folder, including a whole nested hierarchy in one call.

ParameterTypeRequiredDescription
namestringYesFolder name, or a nested path separated by / (e.g. "Retro Tech/PC/CPUs"). Every intermediate folder is created; segments that already exist are skipped. A segment that names a smart folder is refused before anything is created
accountstringNoAccount to create folder in (defaults to Notes.app's default account; exact or unique-prefix match)

Example:

{
  "name": "Work Projects"
}

Example - Create a nested hierarchy:

{
  "name": "Work/Clients/Omnia"
}

Returns: Confirmation message. The call is idempotent — an already-existing folder (or path segment) is skipped rather than treated as an error, so it is safe to call before every create-note that targets a folder.

Existence is decided by folder id, not by name: a folder deleted earlier in the same Notes session no longer counts as existing, and the call succeeds only once the created folder is confirmed by its id. Otherwise it reports failure.


get-folder-by-id

Reads one exact folder's current name, parent ID, and account ID (accountId, plus isRoot, true when the folder sits at the account root). Use these values with rename-folder or delete-folder-by-id; this avoids relying on ambiguous folder names or paths. The id may also be the folder's Notes UUID or numeric key, and the result adds identifier, parentIdentifier, and accountIdentifier when Full Disk Access is granted.


rename-folder

Renames an existing folder in place using its exact id, expectedName, expectedParentId, and newName. The operation preserves the folder ID, notes, and descendants. It refuses stale metadata and a conflicting sibling name.


delete-folder-by-id

Deletes one exact, empty, ordinary folder through a plan-then-apply handshake. Read the folder first with get-folder-by-id, call once with dryRun: true, then repeat the same guards with dryRun: false and the returned revision.

ParameterTypeRequiredDescription
idstringYesExact folder id (x-coredata://…/ICFolder/pN), or the folder's Notes UUID or numeric key
expectedNamestringYesCurrent folder name (not a path), matched case-sensitively
expectedAccountIdstringYesOwning account id (x-coredata://…/ICAccount/pN)
expectedParentIdstringOne of these twoCurrent parent folder id (same forms as id)
expectedRoottrueOne of these twoThe folder sits at the account root
dryRunbooleanYestrue plans; false applies
expectedRevisionstringOn applyThe revision from the dry run

Example (plan, then apply with the returned revision):

{
  "id": "x-coredata://ABC/ICFolder/p42",
  "expectedName": "Old Projects",
  "expectedAccountId": "x-coredata://ABC/ICAccount/p3",
  "expectedRoot": true,
  "dryRun": true
}

Returns: status: "planned" with wouldDelete, identifier, name, accountId, parentId, folderType: 0, zero childFolderCount and noteCount, and revision; on apply, status: "deleted", committed: true, verified: true (Notes.app no longer resolves the id), and storeTombstoned.

Safety: it always refuses Recently Deleted, smart folders, the account's default and other system folders, shared folders (or folders inside a shared folder), and any folder that still holds notes or subfolders. There is no override. It is not atomic: the guard is a pre-check followed by an AppleScript delete. The name, parent, account, sharing, and emptiness checks repeat inside the delete script, but the folder type and stable identifier come from the local Notes database just before it. It needs Full Disk Access and fails closed without it.

Errors carry the standard code: revision_conflict (a Conflict: message; read and plan again), unsupported (a Refused: message), verification_failed with indeterminate: true (read the folder before any retry), and full_disk_access_missing.


delete-folder

Deletes a folder.

ParameterTypeRequiredDescription
namestringYesName or path of the folder to delete (supports nested paths like "Work/Old")
accountstringNoAccount containing the folder (defaults to Notes.app's default account; exact or unique-prefix match)

Example:

{
  "name": "Old Projects"
}

Returns: Confirmation message, or error if folder not found or not empty.

⚠️ Safety: Irreversible — requires explicit user confirmation before calling. Prefer list-folders first to confirm the exact folder path being deleted.


show-folder

Reveals a folder in Notes.app using its unique CoreData identifier.

ParameterTypeRequiredDescription
idstringYesThe folder's CoreData identifier (from list-folders), or its Notes UUID or numeric key
separatelybooleanNoOpen in a separate window when supported by Notes.app

Returns: Confirmation that Notes.app accepted the show command.


Account Operations

list-accounts

Lists all configured Notes accounts.

Parameters: None

Example:

{}

Returns: List of accounts with names, IDs, upgraded state, and default folder metadata, plus each account's identifier (Notes UUID) when Full Disk Access is granted.


get-default-location

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.


show-account

Reveals an account in Notes.app using its unique CoreData identifier.

ParameterTypeRequiredDescription
idstringYesThe account's CoreData identifier (from list-accounts)
separatelybooleanNoOpen in a separate window when supported by Notes.app

Returns: Confirmation that Notes.app accepted the show command.


Batch Operations

batch-delete-notes

Deletes multiple notes at once by ID.

ParameterTypeRequiredDescription
notesobject[]YesArray of {id, expectedContentHash} snapshots to delete (max 500 per request)

Returns: Summary of successes and failures. A note already in Recently Deleted fails without being deleted, as in delete-note.

⚠️ Safety: Irreversible — requires explicit user confirmation before calling. Prefer search-notes / list-notes first to confirm the exact ids being deleted.


batch-move-notes

Moves multiple notes to a folder.

ParameterTypeRequiredDescription
idsstring[]YesArray of note IDs to move (max 500 per request)
folderstringYesDestination folder name or nested path (e.g., "Work/Clients"). Must already exist — create it with create-folder. A smart folder is refused for the whole call before any note moves
accountstringNoAccount containing the folder
ifFolderId, ifAncestorFolderId, forbiddenAncestorFolderIdsstring, string, string[]NoShared folder preconditions applied independently to each note; see Folder scope guards. Forbidden folders also apply to the destination

Returns: Summary of successes and failures. Each success is reported only after the note's actual container folder ID matches the destination folder ID. If every note fails, the call returns its existing error response; the exact note IDs and reasons remain in the failure text.

Failure rows carry committed: false, indeterminate: false only when that item was refused before its move. Move/readback failures and an unknown whole-script outcome carry indeterminate: true, with committed omitted. If every item fails, the batch reports committed: false only when every row is a proven pre-move refusal. Otherwise its error result is indeterminate; read each exact note ID before retrying, even if another row has a scope refusal.

Example - move only notes that are still in the reviewed inbox:

{
  "ids": ["x-coredata://ABC123/ICNote/p456", "x-coredata://ABC123/ICNote/p457"],
  "folder": "Archive",
  "ifFolderId": "x-coredata://ABC123/ICFolder/p12"
}

The source-folder check runs immediately before each move in the same AppleScript. If the check finds that a note is no longer in that inbox, its result reports Scope guard failed: … while a note still there can move. The check does not lock the note against another actor moving it before the move. The batch is not a transaction, and successful moves are not rolled back after a refusal.


Export Operations

export-notes-json

Exports notes as JSON — metadata, HTML content, and plaintext, grouped by account and folder — one page at a time. A whole library rarely fits in one MCP message (note bodies embed images as base64), so each call returns a page and says where the next one starts.

ParameterTypeRequiredDescription
offsetnumberNo0-based position to start from, counting notes in account → folder → note order (default 0). Pass the previous page's page.nextOffset
limitnumberNoMaximum notes in this page (default 50, max 500). A page holds fewer when it reaches the response size limit
modifiedSincestringNoISO 8601 date string; export only notes modified on or after this date (e.g., "2025-01-01"). Keep the same value while paging

Example - First page:

{}

Example - A later page of an incremental backup:

{
  "offset": 50,
  "modifiedSince": "2025-06-01"
}

Returns: exportDate, version, accounts (every account and folder, holding the notes that fall in this page), summary (totalNotes in this page, totalFolders, totalAccounts), and page:

FieldDescription
offset / limitThe window that was applied
totalAvailableNotes in the library, after modifiedSince
returnedNotes in this page
nextOffsetWhere the next page starts; absent on the last page
hasMoretrue until the last page — call again with offset set to nextOffset
stoppedAtSizeLimittrue when the page closed early to stay under the response size limit

Size limit: each response stays under APPLE_NOTES_MCP_EXPORT_MAX_BYTES (default 8 MB), below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger without passing on any error text. A note too large to fit on its own is still returned: its oversized inline images are replaced with placeholders (strippedImages), or failing that its HTML body, and if necessary its plaintext, is left empty with contentOmitted: true. Read such a note with get-note-content, and its files with list-attachments / save-attachment. Lower limit if your MCP client caps tool output below that size.

Paging: positions are worked out on every call, so notes created, deleted, or moved between calls can shift a page. Page through promptly, or use modifiedSince for incremental backups.


get-note-markdown

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).

ParameterTypeRequiredDescription
idstringNoNote ID (preferred)
titlestringNoNote title
accountstringNoAccount containing the note

Returns: Note content converted to Markdown format. Checklist items include [x]/[ ] prefixes when database access is available.


export-notes-markdown

Exports one note, or the notes of one folder, as a single Markdown document rendered from the decoded note body (the same block model as get-note-blocks). Titles render as #, headings as ##, subheadings as ###. Bulleted, dashed and numbered lists keep their indent (four spaces per level), checklists render as - [x]/- [ ], block quotes as >, and monospaced paragraphs as fenced code. Inline runs keep bold, italic, strikethrough, underline (<u>), highlight (==), superscript and subscript (<sup>/<sub>), and links whose scheme is http(s), notes:, applenotes: or mailto:. Tables render as GitHub tables. Attachments stay in body order: with assetsDir they link to copies of their files (images inline, drawings through Notes' fallback image, scans as their PDF), and without it they render as labeled placeholders such as \[Image: name\]. An attachment whose file cannot be found renders as \[Image unavailable: name\]. Body text that Markdown would read as block syntax (a leading # to ######, >, -, +, *, 1. or 1), a --- line, a code fence or a table row) is backslash-escaped, so a plain paragraph such as ## Notes stays a paragraph, and wrap never starts a continuation line with such a marker. This tool leaves get-note-markdown unchanged.

A folder export is one presentation document with notes separated by ---. It is not a backup or restore format; use export-notes-json for that.

Requires: Full Disk Access. Password-protected notes are skipped in a folder export (listed in skipped with code encrypted) and refused for a single note.

ParameterTypeRequiredDescription
idstringOne of id/folderExact note ID
folderstringOne of id/folderFolder path, as for list-notes
accountstringNoAccount holding folder
limitnumberNoMaximum notes read from the folder (default 100, max 1000)
outputPathstringNoAbsolute file to create. Create-only: an existing file (or symlink) is refused with [output_exists]
assetsDirstringNoAbsolute directory for attachment copies. Existing files are never replaced; a taken name gets -2, -3, ...
wrapnumberNoHard-wrap prose at this many columns. Code, tables and headings are never wrapped
templatestringNoRender through a template: standard-markdown, obsidian, or a saved template's name. Exclusive with templateFile
templateFilestringNoRender through the JSON template in this .json file (at most 256 KiB), read under the same rules as create-note's contentPath: hidden paths (such as ~/.docker or a project .env) and ~/Library other than iCloud Drive and ~/Library/CloudStorage are refused, checked again after resolving symbolic links and letter case, unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1. Errors name the file, never its contents

Both paths follow the save-attachment rules (absolute, under the home directory, a temp directory, or /Volumes, no symlink escapes) and may not point inside the Notes library container. templateFile is a read, so it also follows contentPath's private-location rule. With outputPath, asset links are relative to the document's directory; without it they are absolute paths.

Returns: without outputPath, the Markdown itself (refused with [too-large] above half of APPLE_NOTES_MCP_EXPORT_MAX_BYTES, because the document travels in both the text and structured result). With outputPath, a receipt: format, count, bytes, output, and assets (dir, files). Both carry stats (attachments, placed, placeholders, unavailable, tables, unreadableTables, unreferenced) and skipped. Nothing already written is deleted if a later step fails.

Templates: template or templateFile renders through a portable JSON template that sets how every block style, inline format, attachment, per-note header and footer (for YAML front matter with title, dates, folder, tags and id), and the note separator are written. The built-in standard-markdown reproduces the default output; obsidian adds front matter and copies attachments into <file>.assets beside outputPath. Templated asset copies get stable content-hashed names and are reused on a repeat export. An invalid template is refused with [invalid-template] and one JSON path per problem, before any note is read. A templated receipt adds template, warnings (such as missing_asset) and assetFiles. See docs/markdown-templates.md for the schema, every rule and placeholder, and examples.


export-notes-html

Exports one note, or the notes of one folder, as one standalone HTML file rendered from the decoded note body. It uses the same block model and attachment handling as export-notes-markdown. Headings become h1-h3, lists nest by indent as ul/ol, checklists show disabled checkboxes, block quotes and monospaced paragraphs become blockquote and pre, and inline runs keep bold, italic, underline, strikethrough, highlight, superscript, subscript, text color and safe links. Tables are semantic <table> elements with a header row. Images, drawings (Notes' fallback image by default, SVG with vectorDrawings: true; see below), scans (PDF with preview), audio, video, files and link cards (title, domain and preview thumbnail) appear in body order. Attachments with no body marker are appended in creation order. An attachment with no usable source renders a visible [Image unavailable: name] marker. The document contains no script, no file: URL and no Notes library path.

A folder export is one presentation document with notes separated by <hr class="note-separator">. It is not a backup or restore format.

By default every drawing keeps Notes' own fallback image, a fixed-size PNG. With vectorDrawings: true, classic PencilKit drawings (com.apple.drawing.2 and com.apple.drawing) are rendered as SVG instead, decoded through the public native helper exactly as get-note-drawings does. The option is off by default because decoding runs the helper and costs extra time per note. The SVG is embedded as a data URL, or written to the sidecar directory with embedAssets: false, and it is subject to the same size limits as any other asset. It stays sharp at any zoom. Paper drawings (com.apple.paper) have no public decoder and keep Notes' fallback image or preview. If the helper is not built (apple-notes-mcp setup --public-helper), a drawing does not decode, the helper stopped at its stroke limit, or the SVG is too large, that drawing falls back to the PNG; the export never fails for it. The SVG traces each stroke's points with its color and mean width, so pencil grain and marker blending are approximated.

Requires: Full Disk Access. Password-protected notes are skipped in a folder export and refused for a single note.

ParameterTypeRequiredDescription
idstringOne of id/folderExact note ID
folderstringOne of id/folderFolder path, as for list-notes
accountstringNoAccount holding folder
limitnumberNoMaximum notes read from the folder (default 100, max 1000)
outputPathstringYesAbsolute HTML file to create. Create-only: an existing file is refused with [output_exists]
embedAssetsbooleanNoEmbed assets as data URLs (default true). Each asset is capped at 10 MiB and a document at 256 MiB of embedded assets; a larger one renders as unavailable with a hint to use embedAssets: false
assetsDirstringNoWith embedAssets: false, the sidecar directory (default <output stem>.assets beside the file). Existing files are never replaced; a taken name gets -2, -3, ...
vectorDrawingsbooleanNoRender classic drawings as SVG through the public native helper (default false). When false or omitted, every drawing keeps Notes' PNG and the helper never runs

The HTML is always written to a file, because an embedded document is too large for an MCP message. Paths follow the save-attachment rules and may not point inside the Notes library container. Sidecar URLs are relative to the HTML file, so the file and its .assets directory can be moved together.

Returns: format, count, bytes, output, either embedded (assets embedded) or assets (dir, files), stats, and skipped. When the notes contain classic drawings and vectorDrawings is true, vectorDrawings reports rendered (drawings placed as SVG), fallback (drawings left as PNG), and fallbackReasons, a count per code such as helper_not_installed, undecodable, truncated, timeout, or too-large. After one helper timeout, or when the helper is not usable, the remaining drawings in the export are not decoded. Nothing already written is deleted if a later step fails.


list-markdown-templates

Lists the Markdown export templates: the built-ins (standard-markdown, obsidian) and every saved template in the library, with its display name, description, size and modification date. Unreadable, invalid or unsafe files in the library are skipped and counted in skipped. Takes no parameters.

Returns: builtins, templates, skipped, and dir (the library directory).


show-markdown-template

Returns one template in its portable form: a built-in, or a saved template exactly as stored (overrides only). Use it as the starting point for a new template.

ParameterTypeRequiredDescription
namestringYesA built-in or saved template name
expandedbooleanNoAlso return expanded, with every rule filled in from its base

Returns: name, source (builtin or saved), template, and optionally expanded.


validate-markdown-template

Checks a template without saving it. Pass exactly one of name, template or templateFile.

ParameterTypeRequiredDescription
namestringOne of threeA saved or built-in template
templateobject or stringOne of threeThe template as a JSON object or JSON text
templateFilestringOne of threeAbsolute path of a JSON template file (home, a temp directory, or /Volumes; at most 256 KiB), read under the same rules as create-note's contentPath: hidden paths (such as ~/.docker or a project .env) and ~/Library other than iCloud Drive and ~/Library/CloudStorage are refused, checked again after resolving symbolic links and letter case, unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1

Returns: valid, and errors as {path, message} pairs such as $.rules["inline.bold"].after: is required when mode is "wrap". An invalid template is a normal result, not a tool error.


save-markdown-template

Validates a template and stores it in the library so export-notes-markdown can use it by template name.

ParameterTypeRequiredDescription
namestringYesLowercase a-z, 0-9, - and _, 1-64 characters, starting with a letter or digit. Built-in names are reserved
templateobject or stringOne of template/templateFileThe template
templateFilestringOne of template/templateFileA JSON template file, under the same rules as validate-markdown-template's
forcebooleanNoReplace an existing template of this name (default false)

Saving is create-only: an existing name is refused with [template-exists] unless force is true. An invalid template is refused with [invalid-template] and one JSON path per problem. The file is written to a temporary name and then moved into place, with mode 0600 in a 0700 directory. The library is ~/Library/Application Support/apple-notes-mcp/templates, or APPLE_NOTES_MCP_TEMPLATE_DIR. A symlinked library or template file is refused.

Returns: name, path, bytes, and replaced.


delete-markdown-template

Removes one saved template file from the library. Built-in templates cannot be deleted.

ParameterTypeRequiredDescription
namestringYesThe saved template to delete

Returns: name, path, and deleted: true.

Template editor (command line)

apple-notes-mcp templates edit [name] starts a local web editor for templates: edit the JSON on one side and see it validated and rendered against built-in sample notes on the other, then save it to the library (create-only unless you tick "replace"). It prints one address with a per-run token, listens on 127.0.0.1 only, refuses cross-origin requests, and stops on Ctrl-C or after 30 idle minutes (--idle-minutes). It reads a real note only when you pass --note <id>, and then read-only. --tailnet listens on this Mac's Tailscale address instead, so another device on your tailnet can open it; it never changes Tailscale or firewall settings. With --tailnet, --note exposes the full note body to anyone who has the URL. See docs/markdown-templates.md for options and exactly what it exposes.


get-checklist-state

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).

ParameterTypeRequiredDescription
idstringYesNote 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

get-note-metadata (BETA)

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.

ParameterTypeRequiredDescription
idstringYesNote 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.


list-special-notes

Lists a set of notes that AppleScript cannot enumerate: pinned notes, Quick Notes, notes in Recently Deleted, or password-protected notes. It reads the NoteStore database read-only and returns metadata only, never body text.

Requires: Full Disk Access for the MCP host process (see Full Disk Access Setup).

ParameterTypeRequiredDescription
kindstringYespinned, quick-notes, recently-deleted, or locked
accountstringNoOnly this account (exact or unique-prefix name). Omit for every account
limitnumberNoMaximum rows, 1–1000 (default 100)

Returns: notes, newest first, each with id, identifier (Notes UUID), title, folder (path in list-folders syntax, or null), account, created, modified, and the flags pinned, locked, quickNote, inRecentlyDeleted, and markedForDeletion. Unlocked rows add the stored snippet. The locked listing adds passwordHint when one is set. Also returns count, total (matches before limit), limit, and supported.

  • pinned and quick-notes cover notes in folders outside Recently Deleted. Folderless Quick Note drafts, which Notes.app never shows, are left out.
  • recently-deleted lists notes in each account's Recently Deleted folder and skips tombstones already waiting to sync away.
  • locked lists every password-protected note, including trashed and folderless ones, so a misplaced locked note can still be found. Their flags and folder say where each one is.
  • supported: false means this macOS version's database has no column for that kind (for example, Quick Notes before macOS 12).

get-audio-transcripts

Reads the transcript, and the summary when one exists, that Notes already computed for the audio recordings in a note. It does not transcribe anything itself. Notes keeps each recording's transcript as word-level mergeable data on the audio attachment's database row, and this tool decodes it read-only.

Requires: Full Disk Access for the MCP host process (see Full Disk Access Setup). Password-protected notes are refused.

ParameterTypeRequiredDescription
idstringYesNote ID (use search-notes to find it first)
includeSegmentsbooleanNoAlso return word-level segments: text, start and duration in seconds, and speaker. Default false
maxSegmentsnumberNoCap on segments per attachment when includeSegments is set (default 2000, at most 20000)

Example:

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "includeSegments": true,
  "maxSegments": 500
}

Returns: structuredContent.attachments holds one entry per top-level audio attachment, in the order the recordings appear in the note. Each entry has:

  • attachmentId (usable with save-attachment), identifier, typeUti, and durationSeconds when known
  • status: ok (a transcript is stored), none (no transcript is stored, for example a recording Notes has not transcribed, or an audio file attached from elsewhere), or undecodable with a reason
  • text (the words joined into readable text), wordCount, fragmentCount, speakers, summary and topLineSummary when stored, and needsTranscription when the database records it
  • segments, only when includeSegments is set, with segmentsTruncated when the cap cut them short

A recording extended with more takes has several fragments. Their transcripts are joined in stored order, separated by a blank line, and each segment carries a fragment index. Recordings with more than one fragment have been verified against synthetic fixtures only. A response that would exceed APPLE_NOTES_MCP_EXPORT_MAX_BYTES (default 8 MB) drops segments first and then shortens text, marking truncated, segmentsTruncated and textTruncated. When the note body cannot be parsed, attachments come back in database order with bodyOrder: false.


get-note-drawings

Decodes a note's classic PencilKit drawings (com.apple.drawing.2 and the older com.apple.drawing attachments) into strokes and SVG. The PencilKit bytes are read read-only from the NoteStore database and decoded by Apple's public PKDrawing(data:) in the public native helper. Modern Paper sketches (com.apple.paper) are a different format and are not decoded here.

Requires: Full Disk Access for the MCP host process, and the public native helper built once with apple-notes-mcp setup --public-helper.

ParameterTypeRequiredDescription
idstringYesNote ID (use search-notes to find it first)
formatstringNo"json" (default) returns strokes, "svg" returns a standalone SVG document per drawing, "both" returns both
includePointsbooleanNoInclude per-point x, y, width, opacity, and force in JSON strokes (default true)

Returns: status (ok, partial, error, or none when the note has no classic drawing), drawingCount, and one entry per drawing with attachmentId, identifier, typeUti, status (ok, or error with a code such as no_data, undecodable, or timeout), strokeCount, bounds, truncated, strokes (each with inkType, sRGB color with alpha, mean width, pointCount, bounds, and points), and svg. Points are already in drawing coordinates. Ink removed with the pixel eraser is left out: a partly erased stroke comes back as one entry per visible piece, each marked masked: true, and hiddenStrokeCount counts strokes erased completely. pointsTruncated: true marks a stroke cut short by the helper's point limit. When a response would exceed APPLE_NOTES_MCP_EXPORT_MAX_BYTES, points are dropped and pointsOmitted is set, then SVG documents are dropped and svgOmitted is set; if it still does not fit, the call fails with an error instead. The SVG draws one path per stroke; it is a faithful outline, not a pixel-exact copy of PencilKit's ink textures.


transcribe-note-audio

Transcribes a note's voice recordings and audio attachments now, on this Mac, with Apple's Speech framework through the public native helper. Recognition is on-device only: SpeechAnalyzer on macOS 26 and later, or SFSpeechRecognizer with on-device recognition required on older systems (a locale without on-device support is refused, never sent to a server). The audio files are opened read-only where Notes keeps them.

Requires: Full Disk Access for the MCP host process, and the public native helper built once with apple-notes-mcp setup --public-helper.

ParameterTypeRequiredDescription
idstringYesNote ID (use search-notes to find it first)
localestringNoBCP-47 language of the speech, such as "en-US" (default), "it-IT", or "fr-FR"
attachmentIdstringNoOnly transcribe this audio attachment (an x-coredata://…/ICAttachment/pN id)
includeTextbooleanNoInclude transcript text (default true); false returns statuses and word counts only
downloadAssetsbooleanNoLet macOS download the language's on-device speech model when it is missing (default false: the call returns asset_unavailable at once)
maxSecondsintegerNoTotal time budget for the call, 30 to 3600 seconds (default 900)

Returns: Overall status and, per audio attachment, status, durationSeconds, wordCount, transcript, and takes (a Notes recording can hold several takes; each is transcribed and the texts are joined in stored order). Statuses:

  • ok: the whole recording was transcribed.
  • partial: some text came back, but a take failed or the helper stopped at its deadline (code: "incomplete").
  • error: nothing was transcribed; code says why (asset_unavailable when the audio file is not on this Mac or the language's speech model is not installed, permission_required, unsupported_locale, unsupported_audio, time_limit, ...).
  • indeterminate: the helper did not answer in time, so the outcome is unknown and a retry may succeed.
  • none (overall only): the note has no audio.

Each take gets a deadline of 1.5 times its length plus a minute (at most 30 minutes), shortened to what is left of maxSeconds. A take that cannot start before the budget runs out reports code: "time_limit". The helper runs as a separate process without blocking the server, and cancelling the request stops it. On macOS 27 a 16-minute recording took about 22 seconds. Some MCP clients stop waiting for a tool after a fixed time, so transcribe long recordings one at a time with attachmentId. Transcripts longer than APPLE_NOTES_MCP_EXPORT_MAX_BYTES are shortened and marked transcriptTruncated.

Speech models: the call never starts a download on its own. When the language's on-device model is not installed, it returns asset_unavailable right away. Pass downloadAssets: true to let macOS download it (a one-time download); if it is still downloading when the call ends, try again shortly.

Speech Recognition permission: the server never shows the Speech Recognition prompt, because nobody may be watching an MCP server to answer it. The helper reads the current authorization first. On older macOS, the SFSpeechRecognizer path needs that access, and macOS attributes the grant to the app that launches the MCP server (Claude Desktop, Codex, Terminal, and so on), not to the helper. Without it the call returns code: "permission_required"; allow the app under System Settings > Privacy & Security > Speech Recognition. If the app has never asked for Speech Recognition access, macOS does not list it there yet, and the call returns code: "permission_not_requested" instead: use macOS 26 or later, or run the server from an app that already has the access. On macOS 26 and later, SpeechAnalyzer transcribed files without any grant in testing (authorization stayed "not determined"), so only an explicit refusal (denied or restricted) stops it.


add-attachment

Adds one nonempty local file of at most 64 MiB to an exact note using id, the latest expectedContentHash, and an absolute path. The server never retries the insertion. It verifies that existing rich content survived and compares the size and streamed SHA-256 of Notes' saved copy with the source before reporting success, with no read cap below the 64 MiB write limit.

ParameterTypeRequiredDescription
idstringYesExact CoreData note ID
expectedContentHashstringYescontentHash from the note version being extended
pathstringYesAbsolute path of the local file. Read under the same rules as create-note's contentPath: home, temp or /Volumes only; hidden paths (any component starting with ., such as ~/.ssh or a project .env) and ~/Library other than iCloud Drive (~/Library/Mobile Documents) and ~/Library/CloudStorage are refused, checked again after resolving symbolic links and letter case, unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1. Symbolic links and anything but a regular file (a FIFO, a device) are refused
filenamestringNoName the attachment gets in Notes instead of the source file's name. One path component that keeps the source file's extension, with no slash, colon, backslash, control character, leading dot or surrounding spaces. Notes names a file attachment after the file it receives, so the server gives its private temporary copy this name

Returns: attachmentId, bytes, the name Notes reports, and the new contentHash. With filename, filenameVerified says whether Notes reports that exact name. A mismatch is a warning (filenameWarning), not a failure, because the attachment and its bytes are already verified.

On macOS 27, Notes' AppleScript does not list PDF attachments, so it cannot see a PDF this tool just added. When that happens and the server has Full Disk Access, it verifies through the read-only NoteStore database instead: success requires exactly one new attachment row on the note whose media file matches the source bytes, whose type fits the file's extension, and, when the decoded note body lists attachments, that the body references. The check repeats for about two seconds while Notes' database catches up, and contentHash is read after it finishes. The result carries verifiedBy: "database". Without Full Disk Access a PDF attach reports "insertion outcome uncertain"; read the note before retrying, because the attachment was probably created.

When the Notes MCP Background v6 bridge is installed, the tool inserts the file with Notes' own Add File to Note action, which places it in the note body once, and the result carries insertedBy: "shortcut". The bridge finds the note by its exact title plus one line of its text that no other note with that title contains; when no such line exists, or the bridge's search refuses, nothing has run and the tool falls back to AppleScript (insertedBy: "applescript"). A bridge run that fails midway is verified, never retried through AppleScript.

Notes.app's AppleScript insertion places every new attachment in the note body twice (#321): one attachment, shown two times. On the AppleScript path the tool removes the extra copy in the same call and checks that the note lists the attachment once. A PDF on macOS 27 cannot be listed, so its extra copy cannot be removed; when the database shows the PDF twice, the tool returns an error naming the attachment. Delete the extra copy in Notes.app and do not attach the file again. Install the v6 bridge to avoid this.


create-note-with-attachment

Creates a note and attaches one local file to it in a single call. It checks the file and filename before creating anything, creates the note through Notes.app the way create-note does in plaintext, then attaches with the same verification as add-attachment.

ParameterTypeRequiredDescription
titlestringYesOne-line note title
contentstringNoPlain-text body placed above the attachment
folderstringNoExisting folder or nested path; create it first with create-folder
accountstringNoAccount name; defaults to Notes.app's default account
pathstringYesAbsolute path of the local file (at most 64 MiB), under the same read rules as add-attachment; a refused path creates no note
filenamestringNoAttachment name override, as in add-attachment
{
  "title": "Signed lease",
  "folder": "Home",
  "path": "/Users/me/Downloads/scan-0042.pdf",
  "filename": "Lease 2026.pdf"
}

Returns: the new note's id with noteCreated: true and the add-attachment result. If the attachment step fails after the note exists but before the file is inserted, the error names the new note's id: attach to it with add-attachment instead of calling this tool again, which would create a second note. If insertion started and could not be verified, the error is code: "verification_failed" with indeterminate: true: the file may already be in the note, so read it with list-attachments before attaching again.


add-attachment-from-pasteboard

Attaches whatever image, PDF, or file is on the pasteboard (a screenshot, "Copy Image", or a file copied in Finder) to an exact note. The pasteboard is read once through AppKit's public NSPasteboard API (via JXA, no native build) and its bytes are frozen into a private temporary file before anything touches the note. The pasteboard itself is never written. The note and expectedContentHash are checked before the pasteboard is read, so a request with a wrong id, a stale revision, or a malformed filename never captures your clipboard. The frozen file then goes through add-attachment's checks.

ParameterTypeRequiredDescription
idstringYesNote ID
expectedContentHashstringYesThe note's current contentHash (from get-note-content)
filenamestringNoAttachment name. Defaults to the copied file's name, or Pasted image.png / Pasted document.pdf for image or PDF data. Without an extension, the pasted type's extension is added; with one, it must match the pasted type. Otherwise the same rules as add-attachment
allowPasteAlertbooleanNoDefault false. Read the pasteboard even when macOS will show its paste alert (see Paste privacy below). Never overrides a Deny setting

Returns: the add-attachment result plus source (kind: file or data, the pasteboard type, and the default filename). A copied file wins over image data; among data types PDF is preferred (a copied PDF usually comes with a raster preview of itself), then PNG, JPEG, HEIC, GIF, and TIFF. Several copied files or several image or PDF items are refused rather than attaching only the first.

Paste privacy (macOS 15.4 and later): macOS can show an alert asking whether to allow a paste when a process reads the general pasteboard without a user paste. Before reading anything, the tool checks AppKit's NSPasteboard.accessBehavior. It reads only when the value is alwaysAllow, or when you pass allowPasteAlert: true and the value is default or ask (macOS then shows its alert, and the call waits for your answer up to the automation timeout). Otherwise it reads nothing and returns code: "permission_denied" with pasteboardCode: "pasteboard_access_denied" and accessBehavior (default, ask, alwaysDeny, or unknown). alwaysDeny is always refused; change it in System Settings, where macOS lists an app after its first paste alert. On macOS before 15.4 the property does not exist and the pasteboard is read as before.

Errors carry a pasteboardCode next to the shared code, and committed: false because nothing was written:

pasteboardCodecodeCause
pasteboard_access_deniedpermission_deniedReading would show the macOS paste alert, or pasting is denied (see above)
pasteboard_emptyvalidation_errorNothing is on the pasteboard
unsupported_contentvalidation_errorNo PNG, JPEG, HEIC, GIF, TIFF, PDF, or copied file (text belongs in append-to-note); the message and types list the pasteboard types found
multiple_filesvalidation_errorMore than one copied file; count says how many. Copy one file, or use add-attachment per file
multiple_itemsvalidation_errorMore than one image or PDF item (not files); count says how many. Copy one, or save each and use add-attachment
too_largevalidation_errorMore than 64 MiB
file_unreadablevalidation_errorThe copied file is a symlink, empty, over 64 MiB, or unreadable
pasteboard_changedoperation_failedThe pasteboard changed while it was being read; try again
pasteboard_timeoutoperation_failedThe read timed out (for example an unanswered paste alert)
pasteboard_unavailableoperation_failedThe MCP host is not running in your logged-in GUI session (for example over SSH)
write_failedoperation_failedThe temporary copy could not be written

The pasteboard read uses the same timeout as other automation steps (APPLE_NOTES_MCP_TIMEOUT_MS, 30 seconds by default).

Known limitation (#236): on macOS 27, Notes' AppleScript dictionary does not list PDF attachments, so a PDF (pasted or from add-attachment) is inserted but cannot be verified and the call reports "insertion outcome uncertain". Read the note before retrying.

For testing, APPLE_NOTES_MCP_PASTEBOARD_NAME makes the tool read a private named pasteboard instead of the general one, so a test never touches your clipboard.


list-attachments

Lists attachments in a note.

ParameterTypeRequiredDescription
idstringNoNote ID (preferred)
titlestringNoNote title
accountstringNoAccount containing the note
includePathsbooleanNoAlso report where each attachment's files are on disk (needs id and Full Disk Access)
firstImagebooleanNoReturn only the note's lead visual instead of the list (needs id and Full Disk Access)

Returns: List of attachments with IDs, names, content identifiers, URLs when available, created/modified dates, and shared state.

With includePaths, each attachment also carries identifier, uti, kind (image, scan, drawing, pdf, audio, video, url, table, other), bodyIndex, and three path fields read from NoteStore and the Notes data folder (read-only):

  • assetPaths: the attachment's own files, best first (the media file, or Notes' fallback image or PDF rendering).
  • previewPath: Notes' largest rendered thumbnail, chosen by pixel area from the Previews entries named <identifier>-<scale>-<W>x<H>-<appearance>. It is always the image file, including when the entry is a directory holding <n>_<uuid>/Preview.png. null when there is none.
  • paths: assetPaths followed by previewPath.

Container attachments (a scan gallery) list their child rows under children. If the database cannot be read, the list is still returned with a pathsError.

With firstImage, the result is {firstImage, orderSource}. firstImage is the first image in the note's body order, even when its asset has not downloaded (path: null, possibly with a previewPath). A scan or drawing stands in only when the note has no image, and a gallery's pages are considered at the gallery's position (parentIdentifier, galleryIndex). orderSource is body, or creation when the body did not record attachment order. firstImage is null when the note has no visual.

⚠️ Safety: A lookup failure is reported as an error, never as an empty list — so an empty result reliably means the note has no attachments and is safe to replace wholesale. Treat an error as "unknown", not "none". Treat returned paths as local data: copy files out with export-attachments instead of passing raw paths on.


save-attachment

Saves a note attachment to disk.

ParameterTypeRequiredDescription
noteIdstringYesCoreData note ID (from search-notes/list-notes)
attachmentIdstringYesAttachment ID (from list-attachments)
savePathstringYesAbsolute destination file path. Must be under your home directory, a temp directory, or /Volumes, and never inside the Notes data folder (~/Library/Group Containers/group.com.apple.notes), however it is reached (symlink or different letter case)

Returns: Confirmation with the saved path, name, and content type (also in structuredContent).


list-paper-attachments

Lists the Paper drawings (com.apple.paper) and classic drawings (com.apple.drawing, com.apple.drawing.2) in one note, and whether Notes has a rendered image of each. Read-only; needs Full Disk Access and does not open Notes.app.

ParameterTypeRequiredDescription
idstringYesExact CoreData note ID

Returns: Per drawing: attachmentId, identifier, uti, kind (paper or drawing), handwritingSummary (the handwriting text Notes recognized, or null when it stored none), bundlePresent (the Paper data bundle is on disk), fallbackImagePath (Notes' full rendering), fallbackImageStale (true when Notes recorded a newer rendering than the one on disk, so the image may not show the latest strokes; export-paper-image then reports stale: true), previewPath (its largest thumbnail), and raster {source, format, width, height}: the validated image export-paper-image would copy, or null.

Strokes are not decoded. Notes' Paper bundle has no public reader, so the image is Notes' own rendering.


export-paper-image

Saves Notes' rendered image of one Paper drawing or classic drawing to a new file.

ParameterTypeRequiredDescription
noteIdstringYesExact CoreData note ID
savePathstringYesAbsolute path for the new file. It must end in the image's extension (.png, or .jpg/.jpeg for a JPEG rendering)
attachmentIdstringNoThe drawing to export (attachmentId or identifier from list-paper-attachments). Required when the note has more than one

Returns: savedPath, format, width, height, bytes, source (fallback for Notes' full rendering, preview for its largest thumbnail when no full rendering exists), and the drawing's attachmentId, identifier, kind, and handwritingSummary.

⚠️ Safety: Writes one new file, readable only by you (mode 0600). savePath follows the save-attachment allowlist (home, temp, or /Volumes), may not be inside the Notes data folder, and must not exist yet. The image header (PNG signature and IHDR, or JPEG frame) is checked before copying and the written file is checked again; a file that fails is removed.


analyze-svg

Analyzes one local SVG file and reports whether it can be represented as editable monoline strokes (a list of strokes, each a solid color, a width, and a polyline) and what that conversion would approximate or drop. It is a standalone preflight: it reads only the file, opens no Notes data, and makes no network request.

ParameterTypeRequiredDescription
pathstringYesAbsolute path of one .svg file: a regular file, not a symbolic link, at most 1 MiB of UTF-8, in home, temp, or /Volumes, read under the same rules as create-note's contentPath: hidden paths (such as ~/.docker or a project .env) and ~/Library other than iCloud Drive and ~/Library/CloudStorage are refused, checked again after resolving symbolic links and letter case, unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1. A file whose first element is not <svg> is refused without quoting any of it
includeDrawingbooleanNoAlso return the normalized drawing. Off by default because it can be large

Returns: classification, importable, defaultWriteAllowed, requiredLosses, issues (each with a code, the loss it causes or null, an element location such as svg/g[2]/path[1]#id, and a message), viewport, counts (elements, references, path segments, output strokes and points, and the geometryWork, dashWork, and scanWork budgets as {used, max}), source (sha256, bytes), drawingBytes, and analysisDigest.

ClassificationMeaning
safeThe output needs no reported approximation or omission. defaultWriteAllowed is true
lossyThe output needs geometry-approximation or paint-approximation
unsupportedVisible content would be dropped (drop-content), or nothing drawable remains (importable: false)

The loss modes:

  • geometry-approximation: non-round caps or joins are drawn round, a non-uniformly scaled stroke gets one width, or centerlines are clipped at a viewport. Curves are flattened to within 0.25 px, which is not reported as a loss.
  • paint-approximation: solid fills become overlapping strokes, group opacity is applied to each stroke, and blend modes or a non-default paint order are ignored.
  • drop-content: text, images, gradient and pattern paint, filters, masks, clip paths, markers, <switch>, and unknown elements are omitted.

Supported input: path, rect, circle, ellipse, line, polyline, polygon; g, a, nested svg; local defs, symbol, and use; transforms; viewBox and preserveAspectRatio; solid named, hex, rgb()/rgba(), and hsl() colors, currentColor, opacity, fill rules, and dashes, from attributes or the style attribute. analysisDigest is a SHA-256 over the canonical analysis and normalized drawing, so equal digests mean the same result.

⚠️ Safety: Read-only. Active or unsafe input is refused as an error with code: "validation_error" and an svgCode: svg_unsafe for scripts, event-handler attributes, <style> elements, animation, embedded objects, DOCTYPE or entity declarations, processing instructions, and any external reference (only local #id references and embedded raster images are allowed); svg_invalid for malformed XML; svg_reference_invalid for cyclic or duplicate-id references; svg_complexity_limit or svg_geometry_invalid when a limit is exceeded (16,384 elements, 100,000 path segments, 4,096 output strokes, 100,000 points, 5,000,000 fill scan checks); svg_file_invalid for a path or file that cannot be read.


export-attachments

Copies a note's attachment files into a directory without opening Notes.app.

ParameterTypeRequiredDescription
noteIdstringYesExact CoreData note ID
exportDirstringYesAbsolute destination directory, created if missing. Same allowlist as save-attachment (home, temp, or /Volumes), and never inside the Notes data folder
firstImageOnlybooleanNoExport only the lead visual that list-attachments firstImage reports

Returns: exportDir, counts (exported, previews, fallbacks, skipped, failed), and per attachment its attachmentId, identifier, kind, exportedTo, and exportedKind: "asset" for the real file, "fallback" for Notes' own full rendering of it (a drawing's PNG or a scan's PDF, named with that format's extension), "preview" when the asset never downloaded and only Notes' thumbnail was on disk, or null when nothing was on disk. inBody: false marks an attachment the note body no longer shows, and stale: true marks a rendering taken from an older generation because the one Notes recorded is missing; attachments inside a container Notes has deleted are not exported. A preview is never chosen over an available asset, and it is named <name>-preview.<ext> so it is not mistaken for the original. A scan gallery with no file of its own exports its pages.

⚠️ Safety: Existing files are never replaced. A name that is taken gets -2, -3, and so on. Reads NoteStore and the Notes data folder read-only and needs Full Disk Access.


fetch-attachment

Returns a note attachment's bytes as base64, without writing to disk (the read counterpart to save-attachment).

ParameterTypeRequiredDescription
noteIdstringYesCoreData note ID (from search-notes/list-notes)
attachmentIdstringYesAttachment ID (from list-attachments)

Returns: The attachment name, content type, byte count, and base64 payload in structuredContent.base64.


show-attachment

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).

ParameterTypeRequiredDescription
noteIdstringYesCoreData note ID (from search-notes/list-notes)
attachmentIdstringYesAttachment ID (from list-attachments)
separatelybooleanNoOpen in a separate window when supported by Notes.app

Returns: Confirmation that Notes.app revealed the attachment.


Diagnostics

health-check

Verifies Notes.app connectivity and permissions.

Parameters: None

Returns: Status of all health checks (app installed, permissions, account access).


doctor

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.

The report ends with the same feature matrix as get-capabilities (structuredContent.runtimeOS and structuredContent.features). It is informational: an unavailable optional feature never changes healthy.


get-notes-stats

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 "⚠️ Partial results" line. This lets you tell a genuinely empty library apart from a partial failure.


get-sync-status

Checks iCloud sync status.

Parameters: None

Returns: Whether sync is active, pending uploads, and last activity time.


list-shared-notes

Lists all notes shared with collaborators.

Parameters: None

Returns: List of shared notes with warnings about collaboration.


Native background operations

Install the signed workflows once as described in shortcuts/README.md, then run each once in the foreground in Shortcuts.app and choose Always Allow — again after every upgrade — so no first-run consent prompt is left for a background run that cannot display it. Native writes never use UI automation or write directly to the Notes database. Every operation on an existing note requires an exact note ID, a fresh expectedContentHash, and a distinctive existing scopeText so the Shortcut and server can independently resolve the same note. Creating a note from Markdown is create-note with format: "markdown".

Every tool below that edits one note takes these parameters, plus the ones listed under the tool itself:

ParameterTypeRequiredDescription
idstringYesExact note ID: x-coredata id, Notes UUID, or numeric key (see Identifier forms)
expectedContentHashstringYescontentHash (sha256:…) from a fresh read of the note
scopeTextstringYesDistinctive phrase already in the note below its title line, 12–500 characters, that Notes search can find. Prefer plain words without punctuation, hashtags, or paths

When the Shortcut refuses a request (its Find Notes step did not return exactly one note with that exact title and scopeText) or stops with its own action error, and the note reads back unchanged, the tool reports the Shortcut's reason with code: "operation_failed", committed: false and indeterminate: false: nothing was written, so a retry is safe once the cause is fixed. Notes search can lag behind a note created or edited moments ago, so a refusal on a brand-new note may clear after a minute. A timeout, or a failed readback on a note that did change, is still reported as indeterminate. If the Shortcut is not installed, the error is code: "shortcut_not_installed" with committed: false.

get-capabilities

Reports which background operations are implemented, live-verified, installed, and currently available, with a specific reason for each unavailable operation.

It also reports runtimeOS (platform, macOSVersion from sw_vers, darwinRelease) and a features matrix, one entry per feature group: applescriptCore, fullDiskAccessReads, shortcutsBridges, backgroundOperationsBridge, nativeTagsBridge, markdownNoteBridge, paragraphLinks and audioTranscription (both database reads that need Full Disk Access), and the placeholders checklistToggle and smartFolders (creating or editing smart folders), which need a native write helper this server does not ship (requirement native_write_helper). The opt-in private helper below is read-only, so enabling it does not change them: they stay not_implemented. Each entry carries available, osSupported, minimumMacOSVersion, requirements, missing, unverified, tools, and a machine-readable reason that is null when available and otherwise the first that applies:

reasonMeaning
unsupported_platformNot running on macOS
not_implementedThe server has no implementation for this feature yet
unknown_os_versionThe feature has a macOS floor and sw_vers could not be read
requires_macos_<major>macOS is older than minimumMacOSVersion, e.g. requires_macos_26
full_disk_access_missingThe Notes database is not readable by this process
shortcuts_unavailableThe shortcuts command could not be run
shortcut_not_installedA bridge Shortcut is missing or installed more than once

The probe never opens Notes.app or runs a Shortcut, so the Automation permission for Notes.app appears under unverified rather than being guessed; doctor and health-check confirm it by contacting Notes.

append-native

Appends bounded plaintext, semantic HTML, or Markdown while preserving existing native objects. The regular append-to-note tool routes protected notes here when scopeText is supplied. Use a distinctive phrase of plain words without punctuation, hashtags, or paths because Notes search may not resolve them literally. format: "html" accepts the fixed subset tabulated under append-to-note; content outside it is refused by name, with the accepted subset in the error. format: "markdown" accepts the same bounded subset as format: "html"'s Markdown-shaped content (#/##/### headings, flat lists, emphasis, and inline links) but is sent through Notes' own native Markdown importer rather than converted to HTML first, so #/##/### produce real Title/Heading/Subheading — Notes' HTML importer only distinguishes two heading levels and renders ### the same as ##. Markdown that the importer would rewrite, so the appended text could not be verified, is refused before anything is written; the list is under Markdown notes. A transport failure names the Shortcut it was waiting on, so a missing or unapproved bridge is identified rather than guessed at; a timeout also says it may be an unanswered first-run consent prompt and names the Shortcut to run once in the foreground.

ParameterTypeRequiredDescription
contentstringYesContent to append, up to 1,048,576 characters
formatstringNo"plaintext" (default), "html", or "markdown"

create-checklist-item

Appends one real unchecked Notes checklist item and verifies its native identity and text.

ParameterTypeRequiredDescription
textstringYesThe item's text

create-checklist-items

Appends several real unchecked checklist items, 1 to 20, in the order given. It takes the same id, expectedContentHash and scopeText as create-checklist-item, plus items, an array of one-line texts. Each item is one run of the same verified bridge, so the call takes a few seconds per item (a full batch of 20 can run about a minute; if your client times out first, read the note before retrying rather than resending the whole batch) and is gated with create-checklist-item in get-capabilities.

After every run the server checks that exactly one new unchecked item with that text and a new native identity appeared, that every item appended earlier in the call kept its identity and text, and that nothing else in the note changed. The verified revision feeds the next run. A final check confirms the new items are the note's last checklist items in the requested order.

{
  "id": "x-coredata://ABC123/ICNote/p456",
  "expectedContentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "scopeText": "Packing list for the trip",
  "items": ["Passport", "Charger", "Rain jacket"]
}

Returns: items (also as landed), each with its index, native id and text, plus orderVerified and the new contentHash. The first uncertain result stops the call without a retry and returns ok: false with landed (the verified items), stoppedAt (the item's index, text, outcome of "not-written" or "uncertain", and the error), and notAttempted. Every ok: false result is an error result (isError: true) with a code; after an uncertain stop it has indeterminate: true and no contentHash, since the note may have changed. Read the note before retrying, and retry only the items that are not present.

create-table

Appends a native table from rectangular string rows and verifies every decoded cell. It never substitutes a text table. Omit rows for an empty 2 × 2 table, the size Notes itself inserts from Format > Table; its four empty cells are verified the same way.

ParameterTypeRequiredDescription
rowsstring[][]NoCell text, row by row: 1–1,000 rows of 1–100 cells, every row the same length. Omit for an empty 2 × 2 table

set-note-pinned

Sets an explicit pinned state after checking both the expected current state and the note revision. It does not rewrite the body.

ParameterTypeRequiredDescription
expectedPinnedbooleanYesThe pinned state you last read; the call is refused if it differs
pinnedbooleanYesThe pinned state to set

remove-native-tags

Removes specified tags from one exact note while preserving unrelated tags and native objects. It does not delete global tag definitions.

ParameterTypeRequiredDescription
tagsstring[]Yes1–100 tag names to remove

replace-native-tag

Adds and verifies the new tag before removing the old tag on an explicit list of freshly read notes. Stops on the first uncertain result. Smart Folder rules are not changed.

Unlike the other tools in this section, it takes a list of notes instead of one id:

ParameterTypeRequiredDescription
notesobject[]Yes1–100 entries, each with the note's id, expectedContentHash, and scopeText
oldTagstringYesTag to remove after the new one is verified
newTagstringYesTag to add

insert-note-link

Retrieves another note's real deep link and appends it with a static label while preserving the target note's native objects.

ParameterTypeRequiredDescription
linkedNoteIdstringYesID of the note to link to, in any identifier form
labelstringNoVisible link text (default: the linked note's title)

Private helper (opt-in, unsupported Apple API)

An optional, read-only native helper reads note state through Notes' own data model (Apple's private NotesShared framework) instead of AppleScript or Shortcuts. It is off by default and nothing in the rest of the server depends on it. It cannot write: every store it opens is opened read-only, and write support was deliberately deferred by the maintainer until a second writer beside a running Notes.app, CRDT replica identity, and the iCloud upload lag are understood. Private API can break on any macOS update; see TECHNICAL_NOTES.md for the API surface, risks, and safety contract.

Brokered sessions do not load external native helpers. Set APPLE_NOTES_MCP_BROKER=off for this client and restart it, then follow Private helper setup to build and enable it. The server checks the helper's recorded source and binary SHA-256 before every call and refuses a missing, stale, or modified helper with a machine-readable code. Rerun matching-version setup after upgrading apple-notes-mcp.

native-helper-status

Reports whether the helper is enabled, built, and current, and runs its live probe: macOS and Notes versions, whether NotesShared loads, whether every required class, selector, and model property exists, and whether the Notes store opens. Each feature reports available plus a reason code (disabled, helper_not_installed, helper_stale, helper_modified, private_api_unavailable, store_unavailable, …). Read-only; the response carries readOnly: true.

native-note-state

Reads one note's native state by Notes UUID (identifier) or x-coredata id: title, modification date, folder identifier, lock/trash/shared/editable flags, iCloud version counters, and a revision change token (compare two reads to detect a change). Opens the store with Core Data's read-only option.

native-checklist-state

Reads one note's native checklist by Notes UUID (identifier) or x-coredata id; pass exactly one. The id is resolved through an exact read-only database lookup after checking the opt-in and installed hashes. Requires the same opt-in, current helper, and Full Disk Access as native-note-state. Check native-helper-status's readChecklist feature first: its probe checks the checklist getters separately. Locked notes or unavailable native bodies are refused. Ordinary ☐ glyphs and Markdown task text do not count as native checklist items.

Returns identifier, revision, items, total, checked, and syncHostRunning. Each item has its exact native todoIdentifier (32 lowercase hex digits), uuid, index, done, and text. Items follow the first occurrence of each native todo identity, which can cover multiple style runs. text is the line containing its first visible styled character, or the first styled line when it has only line terminators. lineStart, lineLengthUTF16, styledStart, and styledLengthUTF16 are UTF-16 offsets and lengths; line text excludes the terminator. The styled range is an envelope, including intervening characters when the identity occurs in separated runs.

Read the ambiguity flags before treating an entry as a single checklist row: contiguous: false means separated runs share the identity; consistent: false means their done bits disagree; spansLines: true means its styled envelope contains multiple lines after leading/trailing terminators are removed. done comes from the first visible styled character, falling back to the first run. Totals count native identities, including ambiguous ones. The revision is an informational change token. This tool opens the store read-only and cannot toggle, create, or repair checklist items.

Usage Patterns

Basic Workflow

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..."

Working with Accounts

Omit account and an operation targets whichever account Notes.app itself reports as the default — not a hardcoded "iCloud". That matters if your default is a non-iCloud account, if the account name is localized, or if it carries a trailing U+F8FF () character.

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..."

When you do pass account, it is resolved in this order:

  1. Exact name match wins outright.

  2. A unique prefix match resolves — account="robert" finds robert.b.sweet@gmail.com.

  3. An ambiguous prefix is refused, with every candidate named:

    Account "rob" is ambiguous - it matches 2 accounts:
    rob@superiortech.io, robert.b.sweet@gmail.com. Use the full account name.
    

That third rule is deliberate. Silently taking the first prefix match would make delete-note or move-note land in the wrong account and report success. An unresolvable account is reported as such rather than as "note not found", so you are not sent looking for the wrong problem.

Organizing with Folders

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: [searches for the note, then calls move-note with its exact id and 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"

<a id="installation-options"></a>

Installation choices and upgrades

Published package

The examples above use npx -y apple-notes-mcp@latest, so a separate global install is optional. To install a persistent command instead:

npm install -g apple-notes-mcp@latest

You can then run apple-notes-mcp setup --check and the other setup commands directly. @latest makes npx check npm for a newer release each time the client starts the server. To hold a reproducible version instead, pin one in the npx argument, for example apple-notes-mcp@X.Y.Z with a version from the CHANGELOG, and update the pin yourself when you upgrade. Keep client and setup commands on the same package version.

After an upgrade, restart the client, check Shortcuts, complete any renewed foreground Shortcut consent, and rebuild a helper if its source checksum changed. Do not use a permission reset as a routine upgrade step.

Fixed Node executable for desktop clients

If a desktop client cannot resolve npx, or Node updates repeatedly invalidate macOS grants, use a persistent package location and an absolute Node executable path:

command -v node
npm root -g

Use the first result as command and append /apple-notes-mcp/build/index.js to the second result for args. Example placeholders:

{
  "mcpServers": {
    "apple-notes": {
      "command": "/absolute/path/to/node",
      "args": ["/absolute/npm/root/apple-notes-mcp/build/index.js"]
    }
  }
}

Confirm those files exist on your Mac. A Developer-ID-signed Node at a stable resolved path is preferable when permission persistence matters. Ad-hoc signing or moving to a different version-manager path can require new grants. doctor checks the running Node's signature. See docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md for the dedicated-runtime procedure.

Source checkout

git clone --branch v2.15.1 https://github.com/sweetrb/apple-notes-mcp.git
cd apple-notes-mcp

The repository includes the compiled build/index.js, so running that snapshot needs Node but no dependency installation. Configure the client with an absolute Node executable and the absolute path to build/index.js.

For Claude Code launched inside the clone, the root .mcp.json is a project-scope entry using ${CLAUDE_PROJECT_DIR:-.}/build/index.js. Approve it when prompted. The plugin's ${CLAUDE_PLUGIN_ROOT} entry belongs to the plugin installation and is not the clone configuration. Local scope takes precedence over project scope, which takes precedence over user scope; avoid accidentally selecting an older copy.

Only when changing the source, install and build using the repository's pinned pnpm version. First ensure Corepack is installed: Node.js 25 and newer no longer bundle it, and other Node distributions may omit it. If corepack is unavailable, follow the official Corepack installation instructions, then run:

corepack enable
pnpm install --frozen-lockfile
pnpm run build

These are developer commands, not prerequisites for the published package.

<a id="configuration"></a> <a id="configuration-file-when-the-host-strips-env"></a>

Server settings

Use the client's server environment configuration for optional APPLE_NOTES_MCP_* settings. When a host does not propagate the values, the server can also read string values from ~/Library/Application Support/apple-notes-mcp/config.json, or the path in APPLE_NOTES_MCP_CONFIG_FILE:

{
  "APPLE_NOTES_MCP_TIMEOUT_MS": "60000"
}

An explicit nonempty environment value takes precedence over the file. Restart the server after changing settings. Put only non-secret server settings in this file. A settings file can configure the server but cannot grant macOS permissions, import a Shortcut, or install a native helper.

For a brokered session, the client forwards only the broker's allowed numeric settings from this file. The brokered server skips JSON configuration files entirely; editing this file cannot enable helper paths or safety overrides in that server.

Environment variables

These optional settings control server behavior. They do not replace the macOS permissions and component setup. Set them in the client environment or the server settings file. The broker accepts only an explicit allowlist of numeric limits and timeouts; code paths, helper paths, Shortcut overrides, and settings that relax safety checks do not carry over.

VariableDefaultDescription
APPLE_NOTES_MCP_MAX_BUFFER67108864 (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. Reading one note body allows at least 512 MB regardless, since a body embeds its inline images. Output past the cap fails with an error naming this variable, not as a timeout.
APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES26214400 (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_BYTES262144 (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.jsonPath to the JSON settings file.
APPLE_NOTES_MCP_TIMEOUT_MS30000 (30 s)Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call timeoutMs options still win, and a write tool's timeoutSeconds argument overrides it for that call.
APPLE_NOTES_MCP_TEMPLATE_DIR~/Library/Application Support/apple-notes-mcp/templatesAbsolute directory of the saved Markdown template library (save-markdown-template and friends).
APPLE_NOTES_MCP_ANCHOR_FILE~/Library/Application Support/apple-notes-mcp/paragraph-anchors.jsonAbsolute path of the paragraph anchor registry.
APPLE_NOTES_MCP_ANCHORS_TOKENunsetToken for apple-notes-mcp anchors serve (at least 32 characters). Unset, the resolver makes a new random token each run.
APPLE_NOTES_MCP_EXPORT_MAX_BYTES8388608 (8 MB)Largest response export-notes-json sends; a page closes early to stay under it. export-notes-markdown returns inline Markdown up to half of it. The default sits below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger. Raise it only if your MCP client accepts bigger messages.
APPLE_NOTES_MCP_BLOCKS_MAX_BYTES4194304 (4 MB)Largest block payload one get-note-blocks page returns; the page closes early to stay under it, and a single oversized paragraph comes back with textOmitted: true. get-note-structure also omits note text larger than this. A list-note-paragraphs or list-note-links page also stops early to stay under it.
APPLE_NOTES_MCP_MAX_RETRIES2Maximum attempts for a read-only AppleScript call that fails with a transient error (Notes.app busy / not responding / lost connection). 2 means one retry; set 1 to fail fast with no retries. Retries share the single APPLE_NOTES_MCP_TIMEOUT_MS budget rather than each getting a fresh one, and a retry is skipped when under a second of that budget remains — so this is a ceiling, not a guarantee. In particular a call that exhausts the budget with a timeout has no time left to retry by construction. Mutating operations run once because a timeout can occur after Notes.app applied the change. Non-transient errors (e.g. "note not found") never retry.
APPLE_NOTES_MCP_RETRY_DELAY_MS1000 (1 s)Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...).
APPLE_NOTES_MCP_ENABLE_PRIVATEunsetSet to 1 to allow the opt-in private helper. Any other value keeps it off.
APPLE_NOTES_MCP_PRIVATE_HELPER_DIR~/Library/Application Support/apple-notes-mcp/private-helperWhere setup --native-helper installs the helper and its checksum manifest.
APPLE_NOTES_MCP_PRIVATE_HELPER_TIMEOUT_MS20000 (20 s)Per-call timeout for the private helper. The helper is read-only, so a timeout changes nothing in Notes.
APPLE_NOTES_MCP_PRIVATE_STOREunsetTesting only: points the helper at a copy of NoteStore.sqlite. The helper refuses a path that resolves to the live store.
APPLE_NOTES_MCP_PUBLIC_HELPER_DIR~/Library/Application Support/apple-notes-mcp/public-helperWhere setup --public-helper installs the public native helper and its checksum manifest.
APPLE_NOTES_MCP_PUBLIC_HELPER_TIMEOUT_MS30000

Apple Notes MCP alternatives

View more →

Similar mcp servers in Other, ranked by GitHub stars. Compare licence, recent activity and scan status before you choose.

NameStarsLicenceLast updatedScan
Apple Notes MCP (this page)
sweetrb
174MITOct 30, 2026Scanned, no flagged issues
Github MCP Server
github
34kMITOct 30, 2026Scanned, no flagged issues
Fastapi MCP
tadata-org
12kMITNov 24, 2025Scanned, no flagged issues
GhidraMCP
LaurieWired
12kApache-2.0Oct 10, 2026Scanned, no flagged issues
MCP Go
mark3labs
9.2kMITOct 30, 2026Scanned, no flagged issues
Git MCP
idosal
8.5kApache-2.0May 8, 2026Scanned, no flagged issues