apple-notes-mcp 2.5.10 → 2.5.11
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 +46 -32
- package/build/index.js +12 -8
- package/docs/APPLESCRIPT-LIMITATIONS.md +92 -0
- package/docs/FULL-DISK-ACCESS.md +72 -0
- package/docs/JXA_RESEARCH.md +129 -0
- package/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md +109 -0
- package/docs/STABILITY-PERF-AUDIT-2026-06-19.md +151 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that e
|
|
|
12
12
|
[](https://modelcontextprotocol.io)
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<img src="codex/assets/screenshot.png" alt="Apple Notes MCP — create, search, and organize Apple Notes from Codex, Claude, and other AI assistants" width="680">
|
|
15
|
+
<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">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
## What is This?
|
|
@@ -39,6 +39,12 @@ Install the sweetrb/apple-notes-mcp MCP server so you can help me manage my Appl
|
|
|
39
39
|
|
|
40
40
|
Claude will handle the installation and configuration automatically.
|
|
41
41
|
|
|
42
|
+
Or register it yourself with one deterministic command:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude mcp add apple-notes -s user -- npx -y apple-notes-mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
42
48
|
### Using the Plugin Marketplace
|
|
43
49
|
|
|
44
50
|
Install as a Claude Code plugin for automatic configuration and enhanced AI behavior:
|
|
@@ -50,6 +56,8 @@ Install as a Claude Code plugin for automatic configuration and enhanced AI beha
|
|
|
50
56
|
|
|
51
57
|
This method also installs a **skill** that teaches Claude when and how to use Apple Notes effectively.
|
|
52
58
|
|
|
59
|
+
On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click **OK**. Optionally, grant **Full Disk Access** to the app that launches the server to enable the checklist-state and note-metadata features; see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md). Everything else works without it.
|
|
60
|
+
|
|
53
61
|
### Using the Codex Marketplace
|
|
54
62
|
|
|
55
63
|
The same plugin is available for Codex. Add the marketplace and install the plugin:
|
|
@@ -65,14 +73,14 @@ The Codex plugin runs the published `apple-notes-mcp` server through `npx` and s
|
|
|
65
73
|
|
|
66
74
|
Configuration for two more hosts is included — each registers the same `apple-notes` MCP server (`npx -y apple-notes-mcp`):
|
|
67
75
|
|
|
68
|
-
- **[Hermes Agent](https://hermes-agent.nousresearch.com/)** (NousResearch) — Hermes has no plugin/marketplace drop-in. Add the server with `hermes mcp add apple-notes --command npx --args -y apple-notes-mcp`, or merge [`.hermes-plugin/config.yaml`](.hermes-plugin/config.yaml) into `~/.hermes/config.yaml`. Details: [`.hermes-plugin/README.md`](.hermes-plugin/README.md).
|
|
69
|
-
- **[Antigravity](https://antigravity.google/)** (Google) — add the server entry from [`.antigravity-plugin/mcp_config.json`](.antigravity-plugin/mcp_config.json) to `~/.gemini/config/mcp_config.json` (or via Antigravity's MCP settings).
|
|
76
|
+
- **[Hermes Agent](https://hermes-agent.nousresearch.com/)** (NousResearch) — Hermes has no plugin/marketplace drop-in. Add the server with `hermes mcp add apple-notes --command npx --args -y apple-notes-mcp`, or merge [`.hermes-plugin/config.yaml`](https://github.com/sweetrb/apple-notes-mcp/blob/main/.hermes-plugin/config.yaml) into `~/.hermes/config.yaml`. Details: [`.hermes-plugin/README.md`](https://github.com/sweetrb/apple-notes-mcp/blob/main/.hermes-plugin/README.md).
|
|
77
|
+
- **[Antigravity](https://antigravity.google/)** (Google) — add the server entry from [`.antigravity-plugin/mcp_config.json`](https://github.com/sweetrb/apple-notes-mcp/blob/main/.antigravity-plugin/mcp_config.json) to `~/.gemini/config/mcp_config.json` (or via Antigravity's MCP settings).
|
|
70
78
|
|
|
71
|
-
###
|
|
79
|
+
### Using Claude Desktop
|
|
72
80
|
|
|
73
81
|
**1. Install the server:**
|
|
74
82
|
```bash
|
|
75
|
-
npm install -g
|
|
83
|
+
npm install -g apple-notes-mcp
|
|
76
84
|
```
|
|
77
85
|
|
|
78
86
|
**2. Add to Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
@@ -81,7 +89,7 @@ npm install -g github:sweetrb/apple-notes-mcp
|
|
|
81
89
|
"mcpServers": {
|
|
82
90
|
"apple-notes": {
|
|
83
91
|
"command": "npx",
|
|
84
|
-
"args": ["apple-notes-mcp"]
|
|
92
|
+
"args": ["-y", "apple-notes-mcp"]
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
}
|
|
@@ -133,7 +141,7 @@ common workflows: `find-note`, `weekly-review`, `new-meeting-note`.
|
|
|
133
141
|
### Known limitations
|
|
134
142
|
|
|
135
143
|
A few Notes UI features are not exposed to AppleScript and therefore cannot be
|
|
136
|
-
supported. See **[docs/APPLESCRIPT-LIMITATIONS.md](docs/APPLESCRIPT-LIMITATIONS.md)**
|
|
144
|
+
supported. See **[docs/APPLESCRIPT-LIMITATIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/APPLESCRIPT-LIMITATIONS.md)**
|
|
137
145
|
for the investigation and verification behind each:
|
|
138
146
|
|
|
139
147
|
- **Pinned notes** — Notes has no scriptable `pinned` property via AppleScript. Pin state can now be **read** with the BETA `get-note-metadata` tool (from the NoteStore database), but it still cannot be **set** programmatically.
|
|
@@ -155,17 +163,16 @@ Creates a new note in Apple Notes.
|
|
|
155
163
|
|-----------|------|----------|-------------|
|
|
156
164
|
| `title` | string | Yes | The title of the note. Automatically prepended as `<h1>` — do NOT include the title in `content` |
|
|
157
165
|
| `content` | string | Yes | The body content of the note (do not repeat the title here) |
|
|
158
|
-
| `tags` | string[] | No |
|
|
166
|
+
| `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline `#hashtags` in `content` instead (Notes.app turns those into real tags) |
|
|
159
167
|
| `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. Defaults to account root |
|
|
160
168
|
| `account` | string | No | Account name (defaults to iCloud) |
|
|
161
169
|
| `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. In both formats, the title is automatically prepended as `<h1>`. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities |
|
|
162
170
|
|
|
163
|
-
**Example:**
|
|
171
|
+
**Example (tagged with inline hashtags):**
|
|
164
172
|
```json
|
|
165
173
|
{
|
|
166
174
|
"title": "Meeting Notes",
|
|
167
|
-
"content": "Discussed Q4 roadmap and budget allocation"
|
|
168
|
-
"tags": ["work", "meetings"]
|
|
175
|
+
"content": "Discussed Q4 roadmap and budget allocation\n\n#work #meetings"
|
|
169
176
|
}
|
|
170
177
|
```
|
|
171
178
|
|
|
@@ -264,7 +271,7 @@ Retrieves the full content of a specific note.
|
|
|
264
271
|
**Returns:** The HTML content of the note, or error if not found. The
|
|
265
272
|
`structuredContent` also includes `hashtags` — any inline `#hashtag` tags parsed
|
|
266
273
|
from the body. Apple Notes tags are inline hashtags, not a scriptable property;
|
|
267
|
-
see [docs/APPLESCRIPT-LIMITATIONS.md](
|
|
274
|
+
see [docs/APPLESCRIPT-LIMITATIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/APPLESCRIPT-LIMITATIONS.md#tags--hashtags-29). Smart Folders are not scriptable.
|
|
268
275
|
|
|
269
276
|
---
|
|
270
277
|
|
|
@@ -393,7 +400,7 @@ Updates an existing note's content and/or title.
|
|
|
393
400
|
|
|
394
401
|
**Note:** `newContent` **replaces the entire note body** — it is not appended. To preserve existing content, read it first (e.g. with `get-note-content`) and include it in `newContent`.
|
|
395
402
|
|
|
396
|
-
**Attachments:** A full-body replace can drop embedded files, images, scans, PDFs, or audio. When a note may hold attachments, run [`list-attachments`](#list-attachments) first, and either save them with `save-attachment` or build a new note rather than overwriting. See the skill's [Attachment-Safe Updates](skills/apple-notes/SKILL.md#attachment-safe-updates) guidance.
|
|
403
|
+
**Attachments:** A full-body replace can drop embedded files, images, scans, PDFs, or audio. When a note may hold attachments, run [`list-attachments`](#list-attachments) first, and either save them with `save-attachment` or build a new note rather than overwriting. See the skill's [Attachment-Safe Updates](https://github.com/sweetrb/apple-notes-mcp/blob/main/skills/apple-notes/SKILL.md#attachment-safe-updates) guidance.
|
|
397
404
|
|
|
398
405
|
---
|
|
399
406
|
|
|
@@ -460,8 +467,6 @@ Moves a note to a different folder. The note is relocated in place via Notes.app
|
|
|
460
467
|
|
|
461
468
|
**Returns:** Confirmation message, or error if note or folder not found.
|
|
462
469
|
|
|
463
|
-
**Note:** This operation copies the note to the new folder then deletes the original. If the delete fails, the note will exist in both locations.
|
|
464
|
-
|
|
465
470
|
---
|
|
466
471
|
|
|
467
472
|
#### `list-notes`
|
|
@@ -796,7 +801,7 @@ Run a full setup diagnostic: Notes.app reachability, the Automation permission,
|
|
|
796
801
|
|
|
797
802
|
**Parameters:** None
|
|
798
803
|
|
|
799
|
-
**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](docs/FULL-DISK-ACCESS.md).
|
|
804
|
+
**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](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md).
|
|
800
805
|
|
|
801
806
|
---
|
|
802
807
|
|
|
@@ -891,7 +896,7 @@ AI: [calls create-note with title="Acme Corp", content="...", folder="Work/Clien
|
|
|
891
896
|
### npm (Recommended)
|
|
892
897
|
|
|
893
898
|
```bash
|
|
894
|
-
npm install -g
|
|
899
|
+
npm install -g apple-notes-mcp
|
|
895
900
|
```
|
|
896
901
|
|
|
897
902
|
### From Source
|
|
@@ -901,7 +906,9 @@ git clone https://github.com/sweetrb/apple-notes-mcp.git
|
|
|
901
906
|
cd apple-notes-mcp
|
|
902
907
|
```
|
|
903
908
|
|
|
904
|
-
The repo ships a prebuilt, dependency-free `build/index.js`, so a bare clone runs with nothing but Node installed. `
|
|
909
|
+
The repo ships a prebuilt, dependency-free `build/index.js`, so a bare clone runs with nothing but Node installed. `pnpm install` and `pnpm run build` are only needed when you change the source (development uses [pnpm](https://pnpm.io/), not npm).
|
|
910
|
+
|
|
911
|
+
You can also install straight from the git repo with `npm install -g github:sweetrb/apple-notes-mcp` (building from source requires pnpm), but the published npm package above is the recommended path.
|
|
905
912
|
|
|
906
913
|
If installed from source, use this configuration:
|
|
907
914
|
```json
|
|
@@ -976,7 +983,7 @@ MCP stores no secrets, but as a general rule keep only non-secret config here.
|
|
|
976
983
|
|
|
977
984
|
The `get-checklist-state` tool and checklist annotations in `get-note-markdown` read directly from the Apple Notes SQLite database. This requires **Full Disk Access** for the process running the MCP server.
|
|
978
985
|
|
|
979
|
-
> 📘 **For the full why-and-how walkthrough (which app to grant, verifying with `doctor`, graceful degradation), see the [Full Disk Access Setup Guide](docs/FULL-DISK-ACCESS.md).** The summary below is the quick version.
|
|
986
|
+
> 📘 **For the full why-and-how walkthrough (which app to grant, verifying with `doctor`, graceful degradation), see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md).** The summary below is the quick version.
|
|
980
987
|
|
|
981
988
|
### How to Grant Full Disk Access
|
|
982
989
|
|
|
@@ -1016,7 +1023,7 @@ All other tools work normally without Full Disk Access. Only checklist state fea
|
|
|
1016
1023
|
| No pinned notes | Pin status is not exposed via AppleScript ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)) |
|
|
1017
1024
|
| Limited rich formatting | Use `format: "html"` on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
|
|
1018
1025
|
| Title matching | Most operations require exact title matches |
|
|
1019
|
-
| Checklist state | Requires [Full Disk Access](docs/FULL-DISK-ACCESS.md) to read done/undone state from the database |
|
|
1026
|
+
| Checklist state | Requires [Full Disk Access](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md) to read done/undone state from the database |
|
|
1020
1027
|
| Checklist **creation** | Not supported. AppleScript's `body of note` setter strips `<input type="checkbox">` and ignores any checklist-styling CSS class. Apple Notes stores checklist items as a protobuf paragraph style (`style_type=103`) that AppleScript doesn't expose, and the SQLite database is read-only. See [Creating Checklists](#creating-checklists) below for the workaround. |
|
|
1021
1028
|
|
|
1022
1029
|
### Roadmap
|
|
@@ -1084,7 +1091,7 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
1084
1091
|
|
|
1085
1092
|
### "Permission denied"
|
|
1086
1093
|
- macOS needs automation permission
|
|
1087
|
-
- Go to System
|
|
1094
|
+
- Go to System Settings > Privacy & Security > Automation
|
|
1088
1095
|
- Ensure your terminal/Claude has permission to control Notes
|
|
1089
1096
|
|
|
1090
1097
|
### "Note not found"
|
|
@@ -1097,9 +1104,14 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
1097
1104
|
- Use `\\` to represent each literal backslash
|
|
1098
1105
|
- See "Backslash Escaping" section under Known Limitations
|
|
1099
1106
|
|
|
1107
|
+
### Notes accumulate blank lines after repeated updates
|
|
1108
|
+
- Repeatedly updating a note (especially with HTML content) can accumulate whitespace artifacts — `<div><br></div>` tags that persist between sections even after you remove them from your content
|
|
1109
|
+
- Apple Notes' internal HTML processing preserves empty divs from previous edits, so the gaps are baked into the note's internal representation and cannot be fixed through further updates
|
|
1110
|
+
- Fix: delete the note with `delete-note` and create a fresh one with `create-note`
|
|
1111
|
+
|
|
1100
1112
|
### `apple-notes` server fails to connect when run from a clone
|
|
1101
1113
|
- Launch `claude` from **inside the repo directory** so `CLAUDE_PROJECT_DIR` resolves to the repo root (the bare `.` fallback is unreliable — it points at the launching process's working directory)
|
|
1102
|
-
- If you've been editing the source, rerun `
|
|
1114
|
+
- If you've been editing the source, rerun `pnpm run build` — the entrypoint is `${CLAUDE_PROJECT_DIR:-.}/build/index.js`, and the committed bundle only reflects your changes after a rebuild
|
|
1103
1115
|
- Run `claude mcp list` to check for a conflicting `apple-notes` entry at another scope (project-scope outranks user-scope, but local-scope outranks project-scope)
|
|
1104
1116
|
- Approve the pending project-scope server when Claude Code prompts you
|
|
1105
1117
|
|
|
@@ -1107,14 +1119,16 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
1107
1119
|
|
|
1108
1120
|
## Development
|
|
1109
1121
|
|
|
1122
|
+
Development uses [pnpm](https://pnpm.io/) (see `packageManager` in `package.json`):
|
|
1123
|
+
|
|
1110
1124
|
```bash
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1125
|
+
pnpm install # Install dependencies
|
|
1126
|
+
pnpm run build # Typecheck, then bundle src/index.ts into build/index.js (esbuild)
|
|
1127
|
+
pnpm test # Run unit test suite (mocked AppleScript)
|
|
1128
|
+
pnpm run test:integration # Run integration tests against real Notes.app
|
|
1129
|
+
pnpm run test:all # Unit + integration
|
|
1130
|
+
pnpm run lint # Check code style
|
|
1131
|
+
pnpm run format # Format code
|
|
1118
1132
|
```
|
|
1119
1133
|
|
|
1120
1134
|
The integration suite (`test/integration.test.ts`) drives the real
|
|
@@ -1136,11 +1150,11 @@ A software consulting, contracting, and development company.
|
|
|
1136
1150
|
|
|
1137
1151
|
## License
|
|
1138
1152
|
|
|
1139
|
-
MIT License - see [LICENSE](LICENSE) for details.
|
|
1153
|
+
MIT License - see [LICENSE](https://github.com/sweetrb/apple-notes-mcp/blob/main/LICENSE) for details.
|
|
1140
1154
|
|
|
1141
1155
|
## Contributing
|
|
1142
1156
|
|
|
1143
|
-
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
1157
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/CONTRIBUTING.md) for guidelines.
|
|
1144
1158
|
|
|
1145
1159
|
## Related Projects
|
|
1146
1160
|
|
|
@@ -1152,4 +1166,4 @@ Part of a family of macOS MCP servers:
|
|
|
1152
1166
|
|
|
1153
1167
|
## Recurring macOS permission prompts
|
|
1154
1168
|
|
|
1155
|
-
If macOS keeps re-prompting for Full Disk Access or Automation for `node` (often after a `brew upgrade`),
|
|
1169
|
+
If macOS keeps re-prompting for Full Disk Access or Automation for `node` (often after a `brew upgrade`), the cause is almost always an **ad-hoc-signed Node** (typically Homebrew's): its code signature (cdhash) changes on every update, so macOS TCC treats each new build as a brand-new binary and silently drops the grants you already made. The fix is to run this server under an official, **Developer-ID-signed Node at a stable path** — its signing identity stays the same across updates, so you grant the permission once and it persists. The `doctor` tool detects the ad-hoc-signature case and the full walkthrough is in [docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
|
package/build/index.js
CHANGED
|
@@ -38717,7 +38717,7 @@ var ERROR_MAPPINGS = [
|
|
|
38717
38717
|
// Permission errors
|
|
38718
38718
|
{
|
|
38719
38719
|
pattern: /not authorized|not permitted|access.*denied/i,
|
|
38720
|
-
message: "Permission denied. Grant automation access in System
|
|
38720
|
+
message: "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation."
|
|
38721
38721
|
},
|
|
38722
38722
|
// Application not running
|
|
38723
38723
|
{
|
|
@@ -38975,6 +38975,10 @@ function embeddedMessage(field) {
|
|
|
38975
38975
|
return decodeMessage(bytes);
|
|
38976
38976
|
}
|
|
38977
38977
|
|
|
38978
|
+
// src/utils/docsUrls.ts
|
|
38979
|
+
var FULL_DISK_ACCESS_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md";
|
|
38980
|
+
var NODE_RUNTIME_TCC_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md";
|
|
38981
|
+
|
|
38978
38982
|
// src/utils/checklistParser.ts
|
|
38979
38983
|
var CHECKLIST_STYLE_TYPE = 103;
|
|
38980
38984
|
var NOTES_DB_PATH = path.join(
|
|
@@ -39095,7 +39099,7 @@ function getChecklistItems(noteId) {
|
|
|
39095
39099
|
return {
|
|
39096
39100
|
items: null,
|
|
39097
39101
|
error: "no_fda",
|
|
39098
|
-
message:
|
|
39102
|
+
message: `Full Disk Access is required to read checklist state. In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`
|
|
39099
39103
|
};
|
|
39100
39104
|
}
|
|
39101
39105
|
if (!hexData) {
|
|
@@ -40529,7 +40533,7 @@ var AppleNotesManager = class {
|
|
|
40529
40533
|
message: "Notes.app is accessible"
|
|
40530
40534
|
});
|
|
40531
40535
|
} else {
|
|
40532
|
-
const errorHint = appCheck.error?.includes("not authorized") ? " (check Automation permissions in System
|
|
40536
|
+
const errorHint = appCheck.error?.includes("not authorized") ? " (check Automation permissions in System Settings > Privacy & Security > Automation)" : "";
|
|
40533
40537
|
checks.push({
|
|
40534
40538
|
name: "notes_app",
|
|
40535
40539
|
passed: false,
|
|
@@ -40549,7 +40553,7 @@ var AppleNotesManager = class {
|
|
|
40549
40553
|
checks.push({
|
|
40550
40554
|
name: "permissions",
|
|
40551
40555
|
passed: !isPermError,
|
|
40552
|
-
message: isPermError ? "AppleScript permissions denied. Grant access in System
|
|
40556
|
+
message: isPermError ? "AppleScript permissions denied. Grant access in System Settings > Privacy & Security > Automation" : `Permission check returned: ${permCheck.error}`
|
|
40553
40557
|
});
|
|
40554
40558
|
if (isPermError) {
|
|
40555
40559
|
return { healthy: false, checks };
|
|
@@ -41463,7 +41467,7 @@ var NOTES_DB_PATH3 = path3.join(
|
|
|
41463
41467
|
os3.homedir(),
|
|
41464
41468
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
41465
41469
|
);
|
|
41466
|
-
var FDA_MESSAGE =
|
|
41470
|
+
var FDA_MESSAGE = `Full Disk Access is required to read note metadata. In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`;
|
|
41467
41471
|
var COLUMN_MAP = [
|
|
41468
41472
|
{ key: "pinned", column: "ZISPINNED", type: "bool" },
|
|
41469
41473
|
{ key: "hasChecklist", column: "ZHASCHECKLIST", type: "bool" },
|
|
@@ -41644,7 +41648,7 @@ function runDoctor(manager) {
|
|
|
41644
41648
|
checks.push({
|
|
41645
41649
|
name: "Full Disk Access",
|
|
41646
41650
|
status: fda ? "ok" : "warn",
|
|
41647
|
-
detail: fda ? "granted \u2014 checklist features available" :
|
|
41651
|
+
detail: fda ? "granted \u2014 checklist features available" : `not granted \u2014 get-checklist-state and checklist annotations in get-note-markdown won't work. In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it and re-run doctor. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`
|
|
41648
41652
|
});
|
|
41649
41653
|
checks.push(checkNodeRuntimeSignature());
|
|
41650
41654
|
const healthy = !checks.some((c) => c.status === "fail");
|
|
@@ -41667,7 +41671,7 @@ function checkNodeRuntimeSignature() {
|
|
|
41667
41671
|
return {
|
|
41668
41672
|
name,
|
|
41669
41673
|
status: "warn",
|
|
41670
|
-
detail: `${process.execPath} is ad-hoc signed (no Team ID). macOS revokes its Automation and Full Disk Access grants every time the binary changes (e.g. every brew upgrade), which looks like random permission loss. Fix: run the server with a Developer-ID-signed Node at a stable path \u2014 see
|
|
41674
|
+
detail: `${process.execPath} is ad-hoc signed (no Team ID). macOS revokes its Automation and Full Disk Access grants every time the binary changes (e.g. every brew upgrade), which looks like random permission loss. Fix: run the server with a Developer-ID-signed Node at a stable path \u2014 see ${NODE_RUNTIME_TCC_GUIDE_URL}`
|
|
41671
41675
|
};
|
|
41672
41676
|
}
|
|
41673
41677
|
const team = /^TeamIdentifier=(.+)$/m.exec(out)?.[1];
|
|
@@ -42671,7 +42675,7 @@ server.registerTool(
|
|
|
42671
42675
|
return ` ${icon} ${c.name}: ${c.message}`;
|
|
42672
42676
|
}).join("\n");
|
|
42673
42677
|
const fdaAvailable = hasFullDiskAccess();
|
|
42674
|
-
const fdaLine = fdaAvailable ? " \u2713 full_disk_access: Granted (checklist features available)" :
|
|
42678
|
+
const fdaLine = fdaAvailable ? " \u2713 full_disk_access: Granted (checklist features available)" : ` \u24D8 full_disk_access: Not granted (optional \u2014 needed for get-checklist-state and checklist annotations in get-note-markdown). In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`;
|
|
42675
42679
|
return successResponse(`${statusIcon} ${statusText}
|
|
42676
42680
|
|
|
42677
42681
|
${checkLines}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# AppleScript Limitations
|
|
2
|
+
|
|
3
|
+
Apple Notes is automated through its AppleScript dictionary. A few features that
|
|
4
|
+
exist in the Notes UI are simply **not exposed to AppleScript**, so this MCP
|
|
5
|
+
server cannot read or write them no matter how the script is written. This page
|
|
6
|
+
documents what was investigated, how it was verified, and the conclusion, so the
|
|
7
|
+
limitation isn't re-investigated every release.
|
|
8
|
+
|
|
9
|
+
The full set of properties Notes exposes on a `note` is:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
container, class, password protected, modification date, creation date,
|
|
13
|
+
shared, body, id, name, plaintext
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(obtained with `properties of note 1 of account "iCloud"`).
|
|
17
|
+
|
|
18
|
+
## Pinned notes (#28)
|
|
19
|
+
|
|
20
|
+
**Status: not feasible via AppleScript.** The Notes UI lets you pin a note to
|
|
21
|
+
the top of a folder, but the `note` class has no `pinned` property. Asking for
|
|
22
|
+
it raises error `-1700`:
|
|
23
|
+
|
|
24
|
+
```applescript
|
|
25
|
+
tell application "Notes"
|
|
26
|
+
set p to pinned of note 1 of account "iCloud"
|
|
27
|
+
-- error -1700: Can't make pinned of note id "x-coredata://…" into type specifier.
|
|
28
|
+
end tell
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
There is no alternative property, element, or command (`pin`, `pinned`,
|
|
32
|
+
`favorite`, …) in the dictionary. Pinned state lives only in Notes' private
|
|
33
|
+
Core Data store (`NoteStore.sqlite`), which is not part of the scriptable
|
|
34
|
+
surface. Reading it would require parsing the SQLite store directly — brittle
|
|
35
|
+
across macOS releases and outside what an AppleScript-based server should do —
|
|
36
|
+
and there is no supported way to *set* it at all.
|
|
37
|
+
|
|
38
|
+
**Conclusion:** pinned read/write is not supported and will not be added while
|
|
39
|
+
Notes lacks a scriptable property. If a future macOS exposes one, revisit by
|
|
40
|
+
re-running the probe above.
|
|
41
|
+
|
|
42
|
+
## Note-to-note links (#30)
|
|
43
|
+
|
|
44
|
+
**Status: not supported as data; navigation-only.** Apple Notes lets you insert
|
|
45
|
+
a link from one note to another in the UI, but AppleScript exposes no property
|
|
46
|
+
or element for it:
|
|
47
|
+
|
|
48
|
+
- A `note` has no `URL`, `url`, or `link` property — each raises error `-2753`
|
|
49
|
+
(undefined). There is no element that enumerates outgoing/incoming links.
|
|
50
|
+
- There is no readable or constructable `applenotes://` / `notes://` deep link.
|
|
51
|
+
The note's `id` (`x-coredata://…/ICNote/p123`) is the only stable handle, and
|
|
52
|
+
it is a Core Data URI, not a shareable or clickable link.
|
|
53
|
+
|
|
54
|
+
The one related capability that *does* work is the `show` command, which reveals
|
|
55
|
+
a note in the Notes UI by id:
|
|
56
|
+
|
|
57
|
+
```applescript
|
|
58
|
+
tell application "Notes" to show note id "x-coredata://…/ICNote/p123"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This is deliberately **not** wrapped as a tool: it pops the GUI (unhelpful for a
|
|
62
|
+
headless server), and an agent already has the note's content via
|
|
63
|
+
`get-note-content` / `get-note-markdown`. Link relationships between notes
|
|
64
|
+
cannot be read at all, so a "list links in this note" feature is not possible.
|
|
65
|
+
|
|
66
|
+
**Conclusion:** note-to-note link data is not exposed and cannot be surfaced.
|
|
67
|
+
The `id` field already returned by every read tool is the canonical reference;
|
|
68
|
+
use that to address a specific note.
|
|
69
|
+
|
|
70
|
+
## Tags / hashtags (#29)
|
|
71
|
+
|
|
72
|
+
**Status: parsed from the body, not first-class.** Apple Notes "tags" are inline
|
|
73
|
+
`#hashtag` tokens you type into a note's text. They are **not** a scriptable
|
|
74
|
+
property — the `note` class exposes no `tags` element, and the tag relationship
|
|
75
|
+
lives only in Notes' private Core Data store. So the only way to surface a
|
|
76
|
+
note's tags via AppleScript is to read them back out of the body text.
|
|
77
|
+
|
|
78
|
+
This server does that: `get-note-content` parses the body and returns the tags
|
|
79
|
+
as `hashtags` in its `structuredContent` (see `src/utils/hashtags.ts`). The
|
|
80
|
+
rules match Notes' own behaviour — a token is `#` followed by letters/digits/
|
|
81
|
+
underscores containing **at least one letter**, so `#123` is not a tag; tokens
|
|
82
|
+
are de-duplicated case-insensitively.
|
|
83
|
+
|
|
84
|
+
Two related caveats:
|
|
85
|
+
|
|
86
|
+
- The `tags` parameter on `create-note` is an application-level pass-through. It
|
|
87
|
+
is stored on the returned object but Notes does **not** persist it, and it does
|
|
88
|
+
**not** create real `#hashtags` in the body. To make a real tag, put `#tag`
|
|
89
|
+
in the note content.
|
|
90
|
+
- **Smart folders are not scriptable.** Notes' tag-driven Smart Folders cannot be
|
|
91
|
+
created, read, or enumerated via AppleScript; there is no `smart folder` class
|
|
92
|
+
in the dictionary. Only regular folders are scriptable.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Full Disk Access for Checklist Features
|
|
2
|
+
|
|
3
|
+
Apple Notes MCP works almost entirely without any special disk permission. **One
|
|
4
|
+
feature area** needs **Full Disk Access (FDA)** for the process that runs the MCP
|
|
5
|
+
server:
|
|
6
|
+
|
|
7
|
+
- **`get-checklist-state`** — reads a note's checklist done/undone state.
|
|
8
|
+
- **Checklist annotations in `get-note-markdown`** — the `[x]` / `[ ]` prefixes on
|
|
9
|
+
checklist items.
|
|
10
|
+
|
|
11
|
+
Everything else (creating, reading, searching, updating, moving, deleting notes;
|
|
12
|
+
folders, accounts, attachments, stats, export, etc.) works **without** Full Disk
|
|
13
|
+
Access.
|
|
14
|
+
|
|
15
|
+
## Why it's needed
|
|
16
|
+
|
|
17
|
+
Apple Notes stores checklist items as a paragraph style inside a gzipped protobuf
|
|
18
|
+
blob in its SQLite store, `NoteStore.sqlite`. AppleScript's `body of note`
|
|
19
|
+
interface strips that state — it can't tell you whether a checklist item is
|
|
20
|
+
checked. To recover it, the MCP reads the SQLite store directly.
|
|
21
|
+
|
|
22
|
+
That database lives in a macOS-protected directory:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
~/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Reading anything under `~/Library/Group Containers/` requires **Full Disk
|
|
29
|
+
Access** for the host process — without it, macOS denies the read and the MCP
|
|
30
|
+
cannot parse checklist state. (The MCP only ever **reads** this database; it never
|
|
31
|
+
writes to it.)
|
|
32
|
+
|
|
33
|
+
## How to grant Full Disk Access
|
|
34
|
+
|
|
35
|
+
1. Open **System Settings** (or **System Preferences** on older macOS).
|
|
36
|
+
2. Go to **Privacy & Security → Full Disk Access**.
|
|
37
|
+
3. Click the **+** button (you may need to unlock with Touch ID / your password
|
|
38
|
+
first), and add the application that **hosts** the MCP server — i.e. the app
|
|
39
|
+
that actually launches `node`:
|
|
40
|
+
- **Claude Desktop** → `/Applications/Claude.app`
|
|
41
|
+
- **Terminal** (if you run Claude Code from a shell) → `/Applications/Utilities/Terminal.app`
|
|
42
|
+
- **iTerm** → `/Applications/iTerm.app`
|
|
43
|
+
- **VS Code** → `/Applications/Visual Studio Code.app`
|
|
44
|
+
4. Make sure the toggle next to the app is **on**.
|
|
45
|
+
5. **Fully quit and reopen the host app.** macOS only applies the new permission
|
|
46
|
+
to processes started *after* the change — a reload or restart-server is not
|
|
47
|
+
enough; the host application itself must be quit (⌘Q) and relaunched.
|
|
48
|
+
|
|
49
|
+
> **Grant FDA to the right app.** FDA applies to the process that spawns the
|
|
50
|
+
> server, not to `node` or to Notes.app. If you launch Claude Code from iTerm,
|
|
51
|
+
> grant it to iTerm; if you use Claude Desktop, grant it to Claude. Granting it to
|
|
52
|
+
> the wrong app has no effect.
|
|
53
|
+
|
|
54
|
+
## Verifying it worked
|
|
55
|
+
|
|
56
|
+
Run the **`doctor`** tool. It reports a dedicated **Full Disk Access** check as
|
|
57
|
+
`ok` / `warn` / `fail` with the reason, so you can confirm the grant took effect
|
|
58
|
+
without guessing. You can also just call `get-checklist-state` on a note that has
|
|
59
|
+
a checklist — if it returns items with `[x]`/`[ ]` state, FDA is working.
|
|
60
|
+
|
|
61
|
+
## Without Full Disk Access
|
|
62
|
+
|
|
63
|
+
The server degrades gracefully — nothing crashes:
|
|
64
|
+
|
|
65
|
+
- `get-checklist-state` returns a clear error explaining that database access is
|
|
66
|
+
needed (and points here).
|
|
67
|
+
- `get-note-markdown` still returns the note as Markdown, but checklist items
|
|
68
|
+
appear as plain list items without the `[x]`/`[ ]` annotations.
|
|
69
|
+
- **All other tools work normally.**
|
|
70
|
+
|
|
71
|
+
See also: [Known Limitations](../README.md#known-limitations) and
|
|
72
|
+
[Creating Checklists](../README.md#creating-checklists) in the README.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# JXA (JavaScript for Automation) Research
|
|
2
|
+
|
|
3
|
+
This document summarizes research into using JXA as an alternative to AppleScript for interacting with Apple Notes.
|
|
4
|
+
|
|
5
|
+
## Background
|
|
6
|
+
|
|
7
|
+
JXA was introduced in OS X Yosemite (10.10) as an alternative scripting language for the OSA (Open Scripting Architecture) framework. It allows JavaScript to be used instead of AppleScript for automation tasks.
|
|
8
|
+
|
|
9
|
+
**Hypothesis**: JXA might provide advantages over AppleScript:
|
|
10
|
+
- Simpler string escaping (standard JavaScript)
|
|
11
|
+
- Better Unicode handling
|
|
12
|
+
- Familiar syntax for developers
|
|
13
|
+
- Native JSON support
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
Created `src/utils/jxa.ts` with:
|
|
18
|
+
- `executeJXA(script, options)` - Execute JXA via `osascript -l JavaScript`
|
|
19
|
+
- `escapeForJXA(str)` - Standard JavaScript string escaping
|
|
20
|
+
- `buildNotesJXA(code)` - Wrapper for Notes.app context
|
|
21
|
+
|
|
22
|
+
## Test Results
|
|
23
|
+
|
|
24
|
+
### Performance Comparison
|
|
25
|
+
|
|
26
|
+
| Operation | AppleScript | JXA | Ratio |
|
|
27
|
+
|-----------|-------------|-----|-------|
|
|
28
|
+
| List Accounts | 124ms | 152ms | 0.8x |
|
|
29
|
+
| List Folders | 141ms | 153ms | 0.9x |
|
|
30
|
+
| Count Notes | 148ms | 138ms | 1.1x |
|
|
31
|
+
| Get First Note Title | 154ms | 155ms | 1.0x |
|
|
32
|
+
| Get Creation Date | 152ms | 150ms | 1.0x |
|
|
33
|
+
| **Unicode Search** | **155ms** | **4129ms** | **0.04x** |
|
|
34
|
+
| **Text Search** | **311ms** | **4115ms** | **0.08x** |
|
|
35
|
+
|
|
36
|
+
**Overall**: JXA is **0.13x the speed of AppleScript** (7.6x slower)
|
|
37
|
+
|
|
38
|
+
### Why Search is So Slow in JXA
|
|
39
|
+
|
|
40
|
+
AppleScript's `whose` clause is optimized at the OSA/Notes.app level:
|
|
41
|
+
```applescript
|
|
42
|
+
-- Fast: filtered server-side
|
|
43
|
+
notes whose name contains "test"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
JXA must iterate in JavaScript:
|
|
47
|
+
```javascript
|
|
48
|
+
// Slow: fetches all notes, filters client-side
|
|
49
|
+
Notes.notes().filter(n => n.name().includes("test"))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The 347-note test database showed search operations taking 4+ seconds in JXA vs ~300ms in AppleScript.
|
|
53
|
+
|
|
54
|
+
### Output Format Comparison
|
|
55
|
+
|
|
56
|
+
| Aspect | AppleScript | JXA |
|
|
57
|
+
|--------|-------------|-----|
|
|
58
|
+
| **Dates** | Locale-dependent ("Wednesday, December 31, 2025 at 5:33:09 PM") | ISO format with `.toISOString()` |
|
|
59
|
+
| **Arrays** | Comma-separated string | Native JavaScript arrays |
|
|
60
|
+
| **Null handling** | "missing value" | JavaScript null/undefined |
|
|
61
|
+
|
|
62
|
+
### String Escaping Comparison
|
|
63
|
+
|
|
64
|
+
**AppleScript** (complex, multi-layer):
|
|
65
|
+
```typescript
|
|
66
|
+
// Must escape for: shell → AppleScript → HTML
|
|
67
|
+
str.replace(/\\/g, "\") // HTML entity for backslash
|
|
68
|
+
.replace(/"/g, "\\\"") // AppleScript quote escape
|
|
69
|
+
.replace(/'/g, "'\\''") // Shell quote escape
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**JXA** (simpler, standard JavaScript):
|
|
73
|
+
```typescript
|
|
74
|
+
str.replace(/\\/g, "\\\\") // Standard JS escaping
|
|
75
|
+
.replace(/"/g, '\\"')
|
|
76
|
+
.replace(/\n/g, "\\n")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Unicode Handling
|
|
80
|
+
|
|
81
|
+
Both handle Unicode identically - they share the same underlying OSA framework. No advantage either way.
|
|
82
|
+
|
|
83
|
+
### Error Messages
|
|
84
|
+
|
|
85
|
+
Both produce similar error messages from the OSA framework:
|
|
86
|
+
- AppleScript: `execution error: Notes got an error: Can't get note "X". (-1728)`
|
|
87
|
+
- JXA: `execution error: Error: Error: Can't get object. (-1728)`
|
|
88
|
+
|
|
89
|
+
## Recommendation
|
|
90
|
+
|
|
91
|
+
**Do NOT migrate to JXA for Apple Notes operations.**
|
|
92
|
+
|
|
93
|
+
### Reasons
|
|
94
|
+
|
|
95
|
+
1. **Critical Performance Penalty**: Search operations are 10-26x slower due to client-side filtering
|
|
96
|
+
2. **No Significant Escaping Advantage**: While JXA escaping is simpler, our AppleScript escaping is already robust and well-tested
|
|
97
|
+
3. **Same Underlying Limitations**: Both use OSA, so neither can access locked notes, sync state, etc.
|
|
98
|
+
4. **Maintenance Burden**: Would require maintaining two scripting approaches
|
|
99
|
+
|
|
100
|
+
### Limited Use Cases for JXA
|
|
101
|
+
|
|
102
|
+
JXA could be useful for:
|
|
103
|
+
1. **Date formatting**: Returns ISO dates instead of locale-dependent strings (but we can parse AppleScript dates)
|
|
104
|
+
2. **JSON output**: Native `JSON.stringify()` for complex data (but our current parsing works fine)
|
|
105
|
+
3. **One-off scripts**: Developer tools/scripts where performance isn't critical
|
|
106
|
+
|
|
107
|
+
### Conclusion
|
|
108
|
+
|
|
109
|
+
The current AppleScript implementation should remain the primary execution method. The simpler escaping in JXA doesn't justify the 7.6x performance penalty, especially for operations involving filtering or searching.
|
|
110
|
+
|
|
111
|
+
The JXA utilities (`src/utils/jxa.ts`) have been created and tested, and can be used for specific scenarios where its advantages (ISO dates, simpler escaping) outweigh the performance cost.
|
|
112
|
+
|
|
113
|
+
## Files Created
|
|
114
|
+
|
|
115
|
+
- `src/utils/jxa.ts` - JXA execution utilities
|
|
116
|
+
- `src/utils/jxa.test.ts` - Unit tests (16 tests)
|
|
117
|
+
- `scripts/jxa-comparison.ts` - Real-world comparison script
|
|
118
|
+
- `docs/JXA_RESEARCH.md` - This document
|
|
119
|
+
|
|
120
|
+
## Test Environment
|
|
121
|
+
|
|
122
|
+
- macOS Sequoia 26.2.0
|
|
123
|
+
- Notes.app with 347 notes across 4 accounts
|
|
124
|
+
- Node.js with tsx for TypeScript execution
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
*Research completed: January 2025*
|
|
129
|
+
*Branch: feature/jxa-alternative*
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Node runtime & TCC permission stability
|
|
2
|
+
|
|
3
|
+
macOS gates this MCP server's access to your data behind **TCC** permissions —
|
|
4
|
+
**Full Disk Access** (to read app data such as Mail, Notes, or Photos) and
|
|
5
|
+
**Automation / Apple Events** (to drive an app like Mail.app or Notes.app via
|
|
6
|
+
AppleScript). See this repo's Full Disk Access / Automation notes for *which*
|
|
7
|
+
operations need which permission.
|
|
8
|
+
|
|
9
|
+
This page is about a **separate, recurring annoyance**: being asked to approve
|
|
10
|
+
those permissions **over and over**, often right after a routine `brew upgrade`.
|
|
11
|
+
|
|
12
|
+
## Symptom
|
|
13
|
+
|
|
14
|
+
- You granted Full Disk Access (and/or Automation) to "node", but days later
|
|
15
|
+
macOS prompts again — `"node" wants access to ...` or `"node" wants to control
|
|
16
|
+
"Mail"`.
|
|
17
|
+
- System Settings → Privacy & Security → Full Disk Access shows **several
|
|
18
|
+
identical "node" rows**, usually only one enabled.
|
|
19
|
+
- It tends to happen immediately after you update Node.
|
|
20
|
+
|
|
21
|
+
## Cause
|
|
22
|
+
|
|
23
|
+
TCC binds a permission grant to the **code identity of the binary that performs
|
|
24
|
+
the access** — here, the `node` executable that launches the MCP server. For a
|
|
25
|
+
binary that is only **ad-hoc signed** (no Developer ID / Team ID), TCC keys the
|
|
26
|
+
grant to the binary's **cdhash**, a hash of its contents.
|
|
27
|
+
|
|
28
|
+
Homebrew's `node` formula is ad-hoc signed:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ codesign -dvvv "$(which node)" 2>&1 | grep -E 'Signature|TeamIdentifier'
|
|
32
|
+
Signature=adhoc
|
|
33
|
+
TeamIdentifier=not set
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every Node update **replaces the binary**, which **changes the cdhash**, so TCC
|
|
37
|
+
no longer recognizes it as the thing you approved — and re-prompts. The extra
|
|
38
|
+
"node" rows are stale cdhashes from previous versions.
|
|
39
|
+
|
|
40
|
+
By contrast, properly signed apps (Chrome, Slack, …) keep their permissions
|
|
41
|
+
across auto-updates because TCC matches them on a stable **Designated
|
|
42
|
+
Requirement** derived from their Developer ID, not on the cdhash.
|
|
43
|
+
|
|
44
|
+
## Fix: run the MCP under the official, Developer-ID-signed Node
|
|
45
|
+
|
|
46
|
+
Node binaries distributed from **nodejs.org** are signed with a real Developer
|
|
47
|
+
ID (`Node.js Foundation`, Team `HX7739G8FX`), notarized, and self-contained.
|
|
48
|
+
Pointing the MCP server at one gives TCC a **stable** identity to match, so a
|
|
49
|
+
permission you grant **persists across future Node updates**. It also decouples
|
|
50
|
+
the MCP runtime from your Homebrew/dev Node, which can keep updating freely.
|
|
51
|
+
|
|
52
|
+
### Steps (Apple Silicon shown; use `darwin-x64` on Intel)
|
|
53
|
+
|
|
54
|
+
1. Install a current LTS to a stable path (kept off `PATH` so it won't shadow
|
|
55
|
+
your dev Node):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
VER=v24.17.0 ARCH=darwin-arm64
|
|
59
|
+
mkdir -p ~/mcp-runtime && cd ~/mcp-runtime
|
|
60
|
+
curl -O https://nodejs.org/dist/$VER/node-$VER-$ARCH.tar.gz
|
|
61
|
+
curl -O https://nodejs.org/dist/$VER/SHASUMS256.txt
|
|
62
|
+
grep " node-$VER-$ARCH.tar.gz$" SHASUMS256.txt | shasum -a 256 -c - # must print OK
|
|
63
|
+
tar -xzf node-$VER-$ARCH.tar.gz
|
|
64
|
+
ln -sfn node-$VER-$ARCH node-current
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
2. Confirm it's Developer-ID signed:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
codesign -dvvv ~/mcp-runtime/node-current/bin/node 2>&1 | grep -E 'Authority=Developer ID|TeamIdentifier'
|
|
71
|
+
# Authority=Developer ID Application: Node.js Foundation (HX7739G8FX)
|
|
72
|
+
# TeamIdentifier=HX7739G8FX
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. Point this MCP server's launcher at it. For Claude Desktop, edit
|
|
76
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` and set this
|
|
77
|
+
server's `command` to the absolute path:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"apple-notes": {
|
|
83
|
+
"command": "/Users/<you>/mcp-runtime/node-current/bin/node",
|
|
84
|
+
"args": ["/path/to/apple-notes-mcp/build/index.js"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Servers launched via `npx` that don't need Full Disk Access can stay on
|
|
91
|
+
Homebrew Node.
|
|
92
|
+
|
|
93
|
+
4. **Restart your MCP client** so the server relaunches under the new Node.
|
|
94
|
+
|
|
95
|
+
5. **Grant the permissions once** to the new binary:
|
|
96
|
+
- *Full Disk Access*: System Settings → Privacy & Security → Full Disk Access
|
|
97
|
+
→ **+** → ⌘⇧G → paste `~/mcp-runtime/node-current/bin/node`.
|
|
98
|
+
- *Automation*: the first time the server drives an app you'll get a one-time
|
|
99
|
+
`"node" wants to control "<App>"` prompt — click **Allow**.
|
|
100
|
+
|
|
101
|
+
Both grants are keyed to the official Node's stable signature, so you should
|
|
102
|
+
not be asked again — including after future Node updates. You can delete the
|
|
103
|
+
stale "node" rows from the Full Disk Access list.
|
|
104
|
+
|
|
105
|
+
### Updating the dedicated Node later
|
|
106
|
+
|
|
107
|
+
Drop a newer official LTS tarball into `~/mcp-runtime/`, repoint the
|
|
108
|
+
`node-current` symlink, and restart your client. The signing identity is
|
|
109
|
+
unchanged, so existing grants carry over — no re-approval.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# apple-notes-mcp — Stability & Performance Audit
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-19 · **Version audited:** 1.4.4 (`main` @ `bb677d6`)
|
|
4
|
+
**Scope:** full codebase — `services/appleNotesManager.ts` (~2375 LOC), `index.ts` (23 tools), `utils/{applescript,jxa,checklistParser,protobuf,syncDetection}.ts`, CI/publish workflows.
|
|
5
|
+
|
|
6
|
+
This audit measures apple-notes-mcp against the maturity bar set by its sibling
|
|
7
|
+
**apple-mail-mcp (v2.1.1)** — robustness, performance, and the maturity surface
|
|
8
|
+
(structured output, diagnostics, resources/prompts, config). Findings are ranked
|
|
9
|
+
by **impact × likelihood** with rough effort (S/M/L). Many fixes are direct ports
|
|
10
|
+
of code already hardened in apple-mail. Line numbers are against `main` @ `bb677d6`.
|
|
11
|
+
|
|
12
|
+
## Resolution status
|
|
13
|
+
|
|
14
|
+
| # | Finding | Tier | Issue | Status |
|
|
15
|
+
|---|---------|------|-------|--------|
|
|
16
|
+
| H1 | `execSync` has no `maxBuffer` cap | High | [#16](https://github.com/sweetrb/apple-notes-mcp/issues/16) | Open |
|
|
17
|
+
| H2 | No `with timeout` + SIGTERM wedges Notes.app | High | [#17](https://github.com/sweetrb/apple-notes-mcp/issues/17) | Open |
|
|
18
|
+
| H3 | Printable `\|\|\|` / comma delimiters collide with user content | High | [#18](https://github.com/sweetrb/apple-notes-mcp/issues/18) | Open |
|
|
19
|
+
| H4 | Swallowed failures return `[]`/`null`/`0` | High | [#19](https://github.com/sweetrb/apple-notes-mcp/issues/19) | Open |
|
|
20
|
+
| H5 | Unbounded full-library scans, no partial-result signal | High | [#20](https://github.com/sweetrb/apple-notes-mcp/issues/20) | Open |
|
|
21
|
+
| M1 | No `structuredContent` on any tool | Medium | [#21](https://github.com/sweetrb/apple-notes-mcp/issues/21) | Open |
|
|
22
|
+
| M2 | No `doctor` tool (incl. Full Disk Access check) | Medium | [#22](https://github.com/sweetrb/apple-notes-mcp/issues/22) | Open |
|
|
23
|
+
| M3 | No MCP resources or prompts | Medium | [#23](https://github.com/sweetrb/apple-notes-mcp/issues/23) | Open |
|
|
24
|
+
| M4 | No file-based config loader | Medium | [#24](https://github.com/sweetrb/apple-notes-mcp/issues/24) | Open |
|
|
25
|
+
| M5 | Locale-fragile date parsing | Medium | [#25](https://github.com/sweetrb/apple-notes-mcp/issues/25) | Open |
|
|
26
|
+
| M6 | Batch ops are N+1 osascript fan-out | Medium | [#26](https://github.com/sweetrb/apple-notes-mcp/issues/26) | Open |
|
|
27
|
+
| M7 | No `save-attachment` / `fetch-attachment` | Medium | [#27](https://github.com/sweetrb/apple-notes-mcp/issues/27) | Open |
|
|
28
|
+
| L1 | Pinned notes not exposed | Low | [#28](https://github.com/sweetrb/apple-notes-mcp/issues/28) | Open |
|
|
29
|
+
| L2 | Tags/hashtags not surfaced | Low | [#29](https://github.com/sweetrb/apple-notes-mcp/issues/29) | Open |
|
|
30
|
+
| L3 | Note-to-note links not supported | Low | [#30](https://github.com/sweetrb/apple-notes-mcp/issues/30) | Open |
|
|
31
|
+
| L5 | No integration test suite | Low | [#31](https://github.com/sweetrb/apple-notes-mcp/issues/31) | Open |
|
|
32
|
+
| L6 | Full Disk Access guide + commit this audit | Low | [#32](https://github.com/sweetrb/apple-notes-mcp/issues/32) | Open |
|
|
33
|
+
|
|
34
|
+
Target release for the fixes: **2.0.0** (full parity with apple-mail), built on a
|
|
35
|
+
long-lived `v2` branch, one item at a time with tests, then full regression +
|
|
36
|
+
docs + merge.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## High
|
|
41
|
+
|
|
42
|
+
### H1 — `execSync` has no `maxBuffer` cap → ENOBUFS on large output (S)
|
|
43
|
+
`src/utils/applescript.ts:324` and `src/utils/jxa.ts:103` call `execSync` with no
|
|
44
|
+
`maxBuffer`, inheriting Node's 1 MB default. `export-notes-json` (full-library
|
|
45
|
+
JSON), `getRecentlyModifiedCounts` (every note's mod-date across all accounts,
|
|
46
|
+
`appleNotesManager.ts:1838-1851`), `get name of notes`, and `get-note-content` on
|
|
47
|
+
long notes can exceed it. Over the limit `execSync` throws and the catch path
|
|
48
|
+
returns `[]`/`null` — a silent, data-dependent failure. **Fix:** add a 64 MB
|
|
49
|
+
default `maxBuffer` (env-overridable) to both executors (port apple-mail #27).
|
|
50
|
+
|
|
51
|
+
### H2 — No `with timeout` wrap; SIGTERM kill wedges Notes.app (M)
|
|
52
|
+
`applescript.ts` wraps nothing in `with timeout` and relies on `execSync`'s
|
|
53
|
+
default SIGTERM (timeout detection keys on `signal === "SIGTERM"`, line 88).
|
|
54
|
+
Killing osascript with SIGTERM leaves work already dispatched inside the
|
|
55
|
+
single-threaded app running, wedging it for subsequent calls (apple-mail #11).
|
|
56
|
+
**Fix:** port `wrapWithTimeout()` (a script-level `with timeout` set below the
|
|
57
|
+
process timeout) and `killSignal: "SIGKILL"`.
|
|
58
|
+
|
|
59
|
+
### H3 — Printable `|||` / comma delimiters collide with user content (M)
|
|
60
|
+
Output is split on printable tokens a user can type into a note title or folder
|
|
61
|
+
name: `|||` and `|||ITEM|||` (~20 sites in `appleNotesManager.ts`), bare commas in
|
|
62
|
+
`parseCommaSeparatedList` (`:495`, used at `:1242` and `:1656`), and comma
|
|
63
|
+
field-splitting in `parseNotePropertiesOutput` (`:326-345`). A note titled
|
|
64
|
+
"Groceries, etc." splits into two phantom notes. **This is a real
|
|
65
|
+
data-corruption-on-ordinary-input bug.** **Fix:** switch field/record delimiters
|
|
66
|
+
to control chars (`\x1f`/`\x1e`/`\x1d`) and stop comma-splitting list output
|
|
67
|
+
(port apple-mail #30).
|
|
68
|
+
|
|
69
|
+
### H4 — Swallowed failures: error → `return []`/`null`/`0` (M)
|
|
70
|
+
On `result.success === false`, many manager methods log to stderr and return an
|
|
71
|
+
empty/neutral value, so the caller can't distinguish "operation failed" from "no
|
|
72
|
+
notes": search `:808-810`, list `:1212-1216`/`:1239`, folders `:1367-1371`,
|
|
73
|
+
accounts `:1652`, `getRecentlyModifiedCounts` `:1852-1853`, and `getNotesStats`
|
|
74
|
+
(wrong totals if any folder scan fails). **Fix:** surface failures (throw or a
|
|
75
|
+
discriminated result) and add partial-coverage diagnostics (apple-mail #28/#29).
|
|
76
|
+
|
|
77
|
+
### H5 — Unbounded full-library scans, no partial-result signal (M)
|
|
78
|
+
`getRecentlyModifiedCounts` (`:1838-1851`) iterates *every note of every account*
|
|
79
|
+
into one giant string; `getNotesStats` (`:1785-1815`) loops account×folder calling
|
|
80
|
+
`listNotes` (one osascript spawn) per folder. No bound, no coverage diagnostic —
|
|
81
|
+
this both trips H1's buffer and H2's timeout on large libraries. **Fix:** bound
|
|
82
|
+
the scan, batch stats into fewer Apple Events, report coverage (apple-mail #24).
|
|
83
|
+
|
|
84
|
+
## Medium
|
|
85
|
+
|
|
86
|
+
### M1 — No `structuredContent` on any tool (M)
|
|
87
|
+
No tool returns `structuredContent` (grep: none); `export-notes-json` even
|
|
88
|
+
stringifies JSON into a text block (`index.ts:955-961`). **Fix:** add
|
|
89
|
+
`successResponse`/`errorResponse` helpers carrying typed JSON and emit from all
|
|
90
|
+
read/list/get tools (apple-mail A1).
|
|
91
|
+
|
|
92
|
+
### M2 — No `doctor` tool (M)
|
|
93
|
+
Only a thin `health-check` (`index.ts:763`, `appleNotesManager.ts:1687`). **Fix:**
|
|
94
|
+
add a `doctor` checking Notes.app reachability, Automation permission, **Full Disk
|
|
95
|
+
Access** (required for checklist parsing — a common silent-failure source),
|
|
96
|
+
account list, and default-account resolution, with actionable messages +
|
|
97
|
+
`structuredContent` (apple-mail C3).
|
|
98
|
+
|
|
99
|
+
### M3 — No MCP resources or prompts (M)
|
|
100
|
+
No `registerResource`/`registerPrompt`. **Fix:** expose resources
|
|
101
|
+
(`notes://accounts`, `notes://folders`, `notes://note/{id}`) and prompts
|
|
102
|
+
(find-note, create-checklist, weekly-review) (apple-mail D2).
|
|
103
|
+
|
|
104
|
+
### M4 — No file-based config loader (S)
|
|
105
|
+
**Fix:** port `fileConfig` reading
|
|
106
|
+
`~/Library/Application Support/apple-notes-mcp/config.json`, merged without
|
|
107
|
+
overriding existing env, so settings work when the host app strips the MCP `env`
|
|
108
|
+
block (apple-mail 2.1.1) — relevant for the default-account setting.
|
|
109
|
+
|
|
110
|
+
### M5 — Locale-fragile date parsing (S)
|
|
111
|
+
`parseAppleScriptDate` (`:252-266`) feeds AppleScript `(date as text)` into
|
|
112
|
+
`new Date()` and falls back to `new Date()` (now) on failure — breaks under
|
|
113
|
+
non-en-US locales, corrupting created/modified timestamps and recency counts.
|
|
114
|
+
`listNotes`/search already use the locale-safe `whose modification date >=` clause
|
|
115
|
+
(`:1188`, `:774`); the read path doesn't. **Fix:** emit ISO-stable date components
|
|
116
|
+
from AppleScript (or parse explicitly) and never fall back to "now."
|
|
117
|
+
|
|
118
|
+
### M6 — Batch ops are N+1 osascript fan-out (M)
|
|
119
|
+
`batchDeleteNotes` (`:2041`) and `batchMoveNotes` (`:2088`) loop per id issuing
|
|
120
|
+
several separate osascript spawns each. **Fix:** collapse existence/property reads
|
|
121
|
+
and mutations into single scripts where feasible, preserving per-item isolation
|
|
122
|
+
(apple-mail #31).
|
|
123
|
+
|
|
124
|
+
### M7 — No `save-attachment` / `fetch-attachment` (M)
|
|
125
|
+
`list-attachments` (`:1887`) returns metadata only. **Fix:** add `save-attachment`
|
|
126
|
+
(AppleScript `save attachment … in (POSIX file …)`, validated path) and
|
|
127
|
+
`fetch-attachment` (base64), mirroring apple-mail.
|
|
128
|
+
|
|
129
|
+
## Low
|
|
130
|
+
|
|
131
|
+
- **L1 — Pinned notes** ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)): AppleScript `pinned` support is version-dependent; investigate, expose if feasible, else document.
|
|
132
|
+
- **L2 — Tags/hashtags** ([#29](https://github.com/sweetrb/apple-notes-mcp/issues/29)): `create-note`'s `tags` is cosmetic (`:608`); Notes tags are inline `#hashtags` not in the dictionary — parse from body and document; smart folders aren't scriptable.
|
|
133
|
+
- **L3 — Note-to-note links** ([#30](https://github.com/sweetrb/apple-notes-mcp/issues/30)): weak AppleScript support; investigate/document.
|
|
134
|
+
- **L5 — Integration tests** ([#31](https://github.com/sweetrb/apple-notes-mcp/issues/31)): 327 unit tests, no integration suite/CI job; add opt-in tests against a real library (gated).
|
|
135
|
+
- **L6 — FDA guide + audit doc** ([#32](https://github.com/sweetrb/apple-notes-mcp/issues/32)): add a Full-Disk-Access setup guide; commit this audit.
|
|
136
|
+
|
|
137
|
+
## Recommended sequencing
|
|
138
|
+
|
|
139
|
+
1. **Stability core** (H1–H4): port the hardened executor wholesale from apple-mail
|
|
140
|
+
(`maxBuffer` + `with timeout` + `SIGKILL`), control-char delimiters, stop
|
|
141
|
+
swallowing failures. Highest risk reduction per line; bring over apple-mail's
|
|
142
|
+
buffer/timeout/delimiter unit tests.
|
|
143
|
+
2. **Scan safety** (H5, M6): bound and batch the stats/recent scans and the batch
|
|
144
|
+
mutations; add partial-coverage diagnostics.
|
|
145
|
+
3. **Maturity surface** (M1, M2): `structuredContent` everywhere + `doctor` (the
|
|
146
|
+
Full Disk Access check is the high-value Notes-specific piece).
|
|
147
|
+
4. **Config + dates** (M4, M5): small, isolated ports.
|
|
148
|
+
5. **Resources/prompts** (M3) and **attachments** (M7).
|
|
149
|
+
6. **Tests + docs** (L5, L6): integration suite + permissions guide.
|
|
150
|
+
7. **Notes-specific** (L1, L2, L3): verify AppleScript feasibility first; document
|
|
151
|
+
limits where not feasible.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.11",
|
|
4
4
|
"description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"build",
|
|
13
|
+
"docs",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|