apple-notes-mcp 2.12.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -4
- package/build/index.js +1763 -73
- package/docs/markdown-templates.md +83 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 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.
|
|
4
4
|
|
|
5
|
-
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, HTML, or JSON, read audio transcripts and drawings, and run a query language across your whole library.
|
|
5
|
+
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.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/apple-notes-mcp)
|
|
8
8
|
[](https://www.npmjs.com/package/apple-notes-mcp)
|
|
@@ -704,8 +704,10 @@ stable code in brackets, such as `[encrypted]` or `[no-full-disk-access]`.
|
|
|
704
704
|
|
|
705
705
|
#### `list-note-paragraphs`
|
|
706
706
|
|
|
707
|
-
Lists one note's non-empty paragraphs in body order, read
|
|
708
|
-
|
|
707
|
+
Lists one note's non-empty paragraphs in body order, read from the NoteStore
|
|
708
|
+
database. It never changes Notes. With `recordAnchors` it also writes the
|
|
709
|
+
local anchor registry, so the tool is not marked read-only in its MCP
|
|
710
|
+
annotations. Each paragraph has `blockIndex` (its index in
|
|
709
711
|
`get-note-blocks`, where empty paragraphs also count), `text`, `style`,
|
|
710
712
|
`styleType`, `paragraphId` (the UUID Notes stores on the paragraph's first
|
|
711
713
|
text run), and `paragraphIdStatus`:
|
|
@@ -733,6 +735,7 @@ IDs.
|
|
|
733
735
|
| `linkableOnly` | boolean | No | Return only paragraphs with a `url` |
|
|
734
736
|
| `offset` | number | No | First paragraph to return (default 0). Use `page.nextOffset` |
|
|
735
737
|
| `limit` | number | No | Maximum paragraphs per page (default 500, max 5000) |
|
|
738
|
+
| `recordAnchors` | boolean | No | Also record a [paragraph anchor](#paragraph-anchors) 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 |
|
|
736
739
|
|
|
737
740
|
---
|
|
738
741
|
|
|
@@ -756,7 +759,9 @@ Otherwise the result is an error whose `structuredContent` carries the usual
|
|
|
756
759
|
`no-match`, `ambiguous-paragraph`, `occurrence-out-of-range`, `ambiguous-note`
|
|
757
760
|
(several notes have that title; the message lists their folders), `not-found`,
|
|
758
761
|
`encrypted`, or `no-body`. The tool never creates or changes a paragraph ID,
|
|
759
|
-
and an edit in Notes can later replace the ID and break the link.
|
|
762
|
+
and an edit in Notes can later replace the ID and break the link. With
|
|
763
|
+
`recordAnchor` it also writes the local anchor registry, so the tool is not
|
|
764
|
+
marked read-only in its MCP annotations.
|
|
760
765
|
|
|
761
766
|
**Requires:** Full Disk Access.
|
|
762
767
|
|
|
@@ -767,6 +772,117 @@ and an edit in Notes can later replace the ID and break the link.
|
|
|
767
772
|
| `match` | string | One of `contains`, `match`, `blockIndex` | The whole paragraph |
|
|
768
773
|
| `blockIndex` | number | One of `contains`, `match`, `blockIndex` | The paragraph's `blockIndex` |
|
|
769
774
|
| `occurrence` | number | No | Which match to use when several paragraphs match |
|
|
775
|
+
| `recordAnchor` | boolean | No | Also record a [paragraph anchor](#paragraph-anchors) for the linked paragraph and return its `anchorId`. Writes only the local anchor registry |
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
#### Paragraph anchors
|
|
780
|
+
|
|
781
|
+
A paragraph link breaks when Notes replaces or copies the paragraph's ID. A
|
|
782
|
+
paragraph anchor records enough about the paragraph to find it again: the
|
|
783
|
+
note's Notes UUID, the paragraph ID and its status, the paragraph's normalized
|
|
784
|
+
text and a fingerprint of it, the fingerprints of the paragraphs before and
|
|
785
|
+
after it, its `blockIndex`, and when it was recorded. Anchors are kept in one
|
|
786
|
+
local file, `~/Library/Application Support/apple-notes-mcp/paragraph-anchors.json`
|
|
787
|
+
(or the absolute path in `APPLE_NOTES_MCP_ANCHOR_FILE`). It is created with
|
|
788
|
+
mode 0600 in a 0700 directory, replaced atomically under a lock file, and
|
|
789
|
+
holds the text of every anchored paragraph. A symlinked, unreadable or
|
|
790
|
+
corrupt registry is refused and left as it is. The anchor tools never change
|
|
791
|
+
Notes.
|
|
792
|
+
|
|
793
|
+
Resolving an anchor tries three steps in order, and every step fails closed:
|
|
794
|
+
|
|
795
|
+
1. **Paragraph ID.** A paragraph whose ID is still unique in the note is the
|
|
796
|
+
anchored paragraph, even if its text was edited or it moved. A link to that
|
|
797
|
+
ID opens exactly that paragraph.
|
|
798
|
+
2. **Exact text.** The same normalized text. When several paragraphs have it,
|
|
799
|
+
only the one whose neighbours still match is chosen; a tie is `ambiguous`.
|
|
800
|
+
3. **Text and neighbours.** Similar text between both recorded neighbours, or
|
|
801
|
+
nearly identical text beside one of them. Two close candidates are
|
|
802
|
+
`ambiguous`; one below `minConfidence` is `low-confidence`.
|
|
803
|
+
|
|
804
|
+
A match whose ID is now shared or missing is `needs-reminting`: the paragraph
|
|
805
|
+
is found (the result names its block), but no safe link exists until it gets a
|
|
806
|
+
new ID. Public automation cannot set a paragraph ID, so this server only
|
|
807
|
+
reports it and returns no `url`.
|
|
808
|
+
|
|
809
|
+
To share anchored links outside Notes, see the
|
|
810
|
+
[paragraph anchor resolver](#paragraph-anchor-resolver-opt-in).
|
|
811
|
+
|
|
812
|
+
#### `create-paragraph-anchor`
|
|
813
|
+
|
|
814
|
+
Records an anchor for one paragraph, selected exactly as in
|
|
815
|
+
`get-paragraph-link` (note by `id` or `title`/`folder`; paragraph by one of
|
|
816
|
+
`contains`, `match`, `blockIndex`, plus `occurrence`). Unlike
|
|
817
|
+
`get-paragraph-link`, a paragraph whose ID is shared or missing can be
|
|
818
|
+
anchored. Recording the same paragraph again (same note, ID, text and block)
|
|
819
|
+
returns the existing anchor with `created: false`.
|
|
820
|
+
|
|
821
|
+
Returns `anchor`, `created`, and `url` when the paragraph currently has a
|
|
822
|
+
unique ID.
|
|
823
|
+
|
|
824
|
+
**Requires:** Full Disk Access. Writes only the anchor registry.
|
|
825
|
+
|
|
826
|
+
| Parameter | Type | Required | Description |
|
|
827
|
+
|-----------|------|----------|-------------|
|
|
828
|
+
| `id`, `title`, `folder` | string | One of `id`, `title` | Note selector, as in `list-note-paragraphs` |
|
|
829
|
+
| `contains`, `match`, `blockIndex`, `occurrence` | | One of `contains`, `match`, `blockIndex` | Paragraph selector, as in `get-paragraph-link` |
|
|
830
|
+
|
|
831
|
+
#### `resolve-paragraph-anchor`
|
|
832
|
+
|
|
833
|
+
Finds an anchored paragraph in the note as it is now. The result has `status`
|
|
834
|
+
(`resolved`, `needs-reminting`, `ambiguous`, `low-confidence`, `not-found`,
|
|
835
|
+
`note-not-found`, `note-deleted` for a note in Recently Deleted, or
|
|
836
|
+
`note-unreadable` for a locked note), `method` (`paragraph-id`, `exact-text`,
|
|
837
|
+
`text-and-neighbours`), `confidence` (0 to 1), `match` (`blockIndex`, `text`,
|
|
838
|
+
`paragraphId`, `paragraphIdStatus`), `changes` (`textChanged`,
|
|
839
|
+
`blockIndexChanged`, `paragraphIdChanged`), and `message`. `url`, the current
|
|
840
|
+
`applenotes://` link, is present only when `status` is `resolved`.
|
|
841
|
+
|
|
842
|
+
**Requires:** Full Disk Access.
|
|
843
|
+
|
|
844
|
+
| Parameter | Type | Required | Description |
|
|
845
|
+
|-----------|------|----------|-------------|
|
|
846
|
+
| `anchorId` | string | Yes | The anchor (`pa_` and 24 hex digits) |
|
|
847
|
+
| `minConfidence` | number | No | Lowest confidence accepted as a match (default 0.6) |
|
|
848
|
+
| `refresh` | boolean | No | After 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` |
|
|
849
|
+
|
|
850
|
+
#### `list-paragraph-anchors`
|
|
851
|
+
|
|
852
|
+
Lists recorded anchors in the order recorded, with `total`, the `registry`
|
|
853
|
+
path and `page` info. Reads only the registry.
|
|
854
|
+
|
|
855
|
+
| Parameter | Type | Required | Description |
|
|
856
|
+
|-----------|------|----------|-------------|
|
|
857
|
+
| `noteIdentifier` | string | No | Only anchors in the note with this Notes UUID |
|
|
858
|
+
| `offset` | number | No | First anchor to return (default 0) |
|
|
859
|
+
| `limit` | number | No | Maximum anchors (default 100, max 1000) |
|
|
860
|
+
|
|
861
|
+
#### `get-paragraph-anchor`
|
|
862
|
+
|
|
863
|
+
Returns one stored anchor as recorded. Reads only the registry.
|
|
864
|
+
|
|
865
|
+
| Parameter | Type | Required | Description |
|
|
866
|
+
|-----------|------|----------|-------------|
|
|
867
|
+
| `anchorId` | string | Yes | The anchor |
|
|
868
|
+
|
|
869
|
+
#### `prune-paragraph-anchors`
|
|
870
|
+
|
|
871
|
+
Removes anchors that no longer resolve, or the anchors you list. It is a dry
|
|
872
|
+
run unless `dryRun` is `false`. Without `anchorIds` it resolves every anchor
|
|
873
|
+
in scope and treats `not-found` and `note-not-found` as stale. A note in
|
|
874
|
+
Recently Deleted (`note-deleted`) is kept by default because it can be
|
|
875
|
+
restored. Returns `stale` (`anchorId`, `status`, `message`), `examined`, and
|
|
876
|
+
`removed`.
|
|
877
|
+
|
|
878
|
+
**Requires:** Full Disk Access unless `anchorIds` is given.
|
|
879
|
+
|
|
880
|
+
| Parameter | Type | Required | Description |
|
|
881
|
+
|-----------|------|----------|-------------|
|
|
882
|
+
| `anchorIds` | string[] | No | Remove exactly these anchors instead of resolving |
|
|
883
|
+
| `noteIdentifier` | string | No | Only anchors in the note with this Notes UUID |
|
|
884
|
+
| `statuses` | string[] | No | Statuses treated as stale (default `not-found`, `note-not-found`) |
|
|
885
|
+
| `dryRun` | boolean | No | Report without removing (default `true`) |
|
|
770
886
|
|
|
771
887
|
---
|
|
772
888
|
|
|
@@ -1950,6 +2066,21 @@ be deleted.
|
|
|
1950
2066
|
|
|
1951
2067
|
**Returns:** `name`, `path`, and `deleted: true`.
|
|
1952
2068
|
|
|
2069
|
+
#### Template editor (command line)
|
|
2070
|
+
|
|
2071
|
+
`apple-notes-mcp templates edit [name]` starts a local web editor for
|
|
2072
|
+
templates: edit the JSON on one side and see it validated and rendered
|
|
2073
|
+
against built-in sample notes on the other, then save it to the library
|
|
2074
|
+
(create-only unless you tick "replace"). It prints one address with a
|
|
2075
|
+
per-run token, listens on `127.0.0.1` only, refuses cross-origin requests,
|
|
2076
|
+
and stops on Ctrl-C or after 30 idle minutes (`--idle-minutes`). It reads a
|
|
2077
|
+
real note only when you pass `--note <id>`, and then read-only. `--tailnet`
|
|
2078
|
+
listens on this Mac's Tailscale address instead, so another device on your
|
|
2079
|
+
tailnet can open it; it never changes Tailscale or firewall settings. With
|
|
2080
|
+
`--tailnet`, `--note` exposes the full note body to anyone who has the URL. See
|
|
2081
|
+
[docs/markdown-templates.md](docs/markdown-templates.md#editing-in-a-browser)
|
|
2082
|
+
for options and exactly what it exposes.
|
|
2083
|
+
|
|
1953
2084
|
---
|
|
1954
2085
|
|
|
1955
2086
|
#### `get-checklist-state`
|
|
@@ -2813,6 +2944,8 @@ All configuration is optional — the server works out of the box. Override beha
|
|
|
2813
2944
|
| `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
|
|
2814
2945
|
| `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (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. |
|
|
2815
2946
|
| `APPLE_NOTES_MCP_TEMPLATE_DIR` | `~/Library/Application Support/apple-notes-mcp/templates` | Absolute directory of the saved Markdown template library (`save-markdown-template` and friends). |
|
|
2947
|
+
| `APPLE_NOTES_MCP_ANCHOR_FILE` | `~/Library/Application Support/apple-notes-mcp/paragraph-anchors.json` | Absolute path of the [paragraph anchor](#paragraph-anchors) registry. |
|
|
2948
|
+
| `APPLE_NOTES_MCP_ANCHORS_TOKEN` | unset | Token for `apple-notes-mcp anchors serve` (at least 32 characters). Unset, the resolver makes a new random token each run. |
|
|
2816
2949
|
| `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` | `8388608` (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. |
|
|
2817
2950
|
| `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#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`](#get-note-structure) also omits note text larger than this. A [`list-note-paragraphs`](#list-note-paragraphs) or [`list-note-links`](#list-note-links) page also stops early to stay under it. |
|
|
2818
2951
|
| `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Maximum 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. |
|
|
@@ -2940,6 +3073,48 @@ The helper never opens the Notes database and never writes under the Notes group
|
|
|
2940
3073
|
|
|
2941
3074
|
---
|
|
2942
3075
|
|
|
3076
|
+
## Paragraph anchor resolver (opt-in)
|
|
3077
|
+
|
|
3078
|
+
An `applenotes://` paragraph link only opens on an Apple device, and it breaks
|
|
3079
|
+
when the paragraph's ID changes. For links shared outside Notes, you can run a
|
|
3080
|
+
small local resolver that looks up a [paragraph anchor](#paragraph-anchors)
|
|
3081
|
+
when the link is opened and redirects to the paragraph's current link:
|
|
3082
|
+
|
|
3083
|
+
```bash
|
|
3084
|
+
apple-notes-mcp anchors serve # http://127.0.0.1:<random port>
|
|
3085
|
+
apple-notes-mcp anchors serve --port 8765 # a fixed port
|
|
3086
|
+
apple-notes-mcp anchors serve --tailnet # this Mac's Tailscale address instead
|
|
3087
|
+
```
|
|
3088
|
+
|
|
3089
|
+
It serves one route, `GET /a/<anchor-id>?token=<token>`. A `resolved` anchor
|
|
3090
|
+
answers with a 302 redirect to the `applenotes://` link. Any other status gets
|
|
3091
|
+
a short plain-text answer and no redirect: 409 for `ambiguous`,
|
|
3092
|
+
`low-confidence`, `needs-reminting` and `note-unreadable`, and 404 for a gone
|
|
3093
|
+
paragraph or note.
|
|
3094
|
+
|
|
3095
|
+
- **Off by default.** Only this command starts it; the MCP server never does.
|
|
3096
|
+
It runs in the foreground until you press Ctrl-C.
|
|
3097
|
+
- **Loopback by default.** It binds `127.0.0.1`. `--tailnet` binds the first
|
|
3098
|
+
`100.64.0.0/10` address on this Mac (a Tailscale address) instead, and fails
|
|
3099
|
+
if there is none. It never runs `tailscale` and never changes Tailscale,
|
|
3100
|
+
firewall or system settings.
|
|
3101
|
+
- **Token required.** Every request needs the token, as `?token=` or
|
|
3102
|
+
`Authorization: Bearer`. It is `APPLE_NOTES_MCP_ANCHORS_TOKEN` when set (at
|
|
3103
|
+
least 32 characters; use this for links that must survive a restart), or a
|
|
3104
|
+
random token printed once at startup. Tokens are compared in constant time
|
|
3105
|
+
and never logged; request logs on stderr omit the query string. After 20
|
|
3106
|
+
failed token checks in a minute from one peer address, that peer gets 429
|
|
3107
|
+
until the minute passes. The count is per peer, so one client guessing
|
|
3108
|
+
tokens cannot lock out another, and requests refused for their method or
|
|
3109
|
+
`Host` header never count.
|
|
3110
|
+
- **Host check.** The `Host` header must name the bound address, which blocks
|
|
3111
|
+
DNS rebinding. Only `GET` and `HEAD` are served, with `Cache-Control:
|
|
3112
|
+
no-store` and `Referrer-Policy: no-referrer`.
|
|
3113
|
+
- It needs Full Disk Access for the terminal or process that runs it, and it
|
|
3114
|
+
reads the same registry as the anchor tools.
|
|
3115
|
+
|
|
3116
|
+
---
|
|
3117
|
+
|
|
2943
3118
|
## Security and Privacy
|
|
2944
3119
|
|
|
2945
3120
|
- **Local only** - All operations happen locally, through AppleScript, the packaged Shortcuts, read-only reads of the Notes database, and helpers you build on your own Mac. The server makes no network requests, and `transcribe-note-audio` uses on-device speech recognition only. Whatever your MCP client does with the results is governed by that client.
|