apple-notes-mcp 2.5.9 → 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 +28 -35
- 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
|
@@ -38656,7 +38656,7 @@ var StdioServerTransport = class {
|
|
|
38656
38656
|
};
|
|
38657
38657
|
|
|
38658
38658
|
// src/utils/applescript.ts
|
|
38659
|
-
import {
|
|
38659
|
+
import { execFileSync } from "child_process";
|
|
38660
38660
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
38661
38661
|
var DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
38662
38662
|
function envPositiveNumber(name) {
|
|
@@ -38693,13 +38693,10 @@ function debugLog(message, data) {
|
|
|
38693
38693
|
console.error(`[DEBUG ${timestamp}] ${message}`);
|
|
38694
38694
|
}
|
|
38695
38695
|
}
|
|
38696
|
-
function escapeForShell(script) {
|
|
38697
|
-
return script.replace(/'/g, "'\\''");
|
|
38698
|
-
}
|
|
38699
38696
|
function isTimeoutError(error2) {
|
|
38700
38697
|
if (error2 instanceof Error) {
|
|
38701
38698
|
const execError = error2;
|
|
38702
|
-
return execError.killed === true || execError.signal === "SIGTERM";
|
|
38699
|
+
return execError.code === "ETIMEDOUT" || execError.killed === true || execError.signal === "SIGKILL" || execError.signal === "SIGTERM";
|
|
38703
38700
|
}
|
|
38704
38701
|
return false;
|
|
38705
38702
|
}
|
|
@@ -38714,19 +38711,13 @@ function isRetryableError(errorMessage) {
|
|
|
38714
38711
|
return RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
38715
38712
|
}
|
|
38716
38713
|
function sleep(ms) {
|
|
38717
|
-
|
|
38718
|
-
const result = spawnSync("sleep", [seconds.toString()], { stdio: "ignore" });
|
|
38719
|
-
if (result.error) {
|
|
38720
|
-
const end = Date.now() + ms;
|
|
38721
|
-
while (Date.now() < end) {
|
|
38722
|
-
}
|
|
38723
|
-
}
|
|
38714
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
38724
38715
|
}
|
|
38725
38716
|
var ERROR_MAPPINGS = [
|
|
38726
38717
|
// Permission errors
|
|
38727
38718
|
{
|
|
38728
38719
|
pattern: /not authorized|not permitted|access.*denied/i,
|
|
38729
|
-
message: "Permission denied. Grant automation access in System
|
|
38720
|
+
message: "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation."
|
|
38730
38721
|
},
|
|
38731
38722
|
// Application not running
|
|
38732
38723
|
{
|
|
@@ -38812,8 +38803,7 @@ function executeAppleScript(script, options = {}) {
|
|
|
38812
38803
|
error: "Cannot execute empty AppleScript"
|
|
38813
38804
|
};
|
|
38814
38805
|
}
|
|
38815
|
-
const preparedScript =
|
|
38816
|
-
const command = `osascript -e '${preparedScript}'`;
|
|
38806
|
+
const preparedScript = wrapWithTimeout(script.trim(), timeoutMs);
|
|
38817
38807
|
debugLog("Executing AppleScript", {
|
|
38818
38808
|
scriptPreview: script.trim().substring(0, 200) + (script.length > 200 ? "..." : ""),
|
|
38819
38809
|
timeout: timeoutMs,
|
|
@@ -38824,7 +38814,8 @@ function executeAppleScript(script, options = {}) {
|
|
|
38824
38814
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
38825
38815
|
const attemptStart = Date.now();
|
|
38826
38816
|
try {
|
|
38827
|
-
const output =
|
|
38817
|
+
const output = execFileSync("osascript", ["-"], {
|
|
38818
|
+
input: preparedScript,
|
|
38828
38819
|
encoding: "utf8",
|
|
38829
38820
|
timeout: timeoutMs,
|
|
38830
38821
|
// SIGKILL (not the default SIGTERM): a wedged osascript blocked on an
|
|
@@ -38833,9 +38824,7 @@ function executeAppleScript(script, options = {}) {
|
|
|
38833
38824
|
killSignal: "SIGKILL",
|
|
38834
38825
|
// Raise the output cap above Node's 1 MB default so large exports /
|
|
38835
38826
|
// long notes aren't truncated into an ENOBUFS failure. (#16)
|
|
38836
|
-
maxBuffer: getMaxBuffer()
|
|
38837
|
-
// Capture stderr separately to get error details
|
|
38838
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
38827
|
+
maxBuffer: getMaxBuffer()
|
|
38839
38828
|
});
|
|
38840
38829
|
const duration3 = Date.now() - attemptStart;
|
|
38841
38830
|
debugLog("AppleScript succeeded", {
|
|
@@ -38901,7 +38890,7 @@ function executeAppleScript(script, options = {}) {
|
|
|
38901
38890
|
}
|
|
38902
38891
|
|
|
38903
38892
|
// src/utils/checklistParser.ts
|
|
38904
|
-
import { execFileSync } from "child_process";
|
|
38893
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
38905
38894
|
import * as zlib from "zlib";
|
|
38906
38895
|
import * as fs from "fs";
|
|
38907
38896
|
import * as path from "path";
|
|
@@ -38986,6 +38975,10 @@ function embeddedMessage(field) {
|
|
|
38986
38975
|
return decodeMessage(bytes);
|
|
38987
38976
|
}
|
|
38988
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
|
+
|
|
38989
38982
|
// src/utils/checklistParser.ts
|
|
38990
38983
|
var CHECKLIST_STYLE_TYPE = 103;
|
|
38991
38984
|
var NOTES_DB_PATH = path.join(
|
|
@@ -38995,7 +38988,7 @@ var NOTES_DB_PATH = path.join(
|
|
|
38995
38988
|
function hasFullDiskAccess() {
|
|
38996
38989
|
try {
|
|
38997
38990
|
if (!fs.existsSync(NOTES_DB_PATH)) return false;
|
|
38998
|
-
|
|
38991
|
+
execFileSync2("sqlite3", ["-readonly", NOTES_DB_PATH, "SELECT 1;"], {
|
|
38999
38992
|
encoding: "utf8",
|
|
39000
38993
|
timeout: 3e3,
|
|
39001
38994
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -39014,7 +39007,7 @@ function queryNoteData(noteId) {
|
|
|
39014
39007
|
const pk = pkMatch[1];
|
|
39015
39008
|
const query = `SELECT hex(nd.ZDATA) FROM ZICNOTEDATA nd JOIN ZICCLOUDSYNCINGOBJECT n ON nd.ZNOTE = n.Z_PK WHERE n.Z_PK = ${pk};`;
|
|
39016
39009
|
try {
|
|
39017
|
-
const result =
|
|
39010
|
+
const result = execFileSync2("sqlite3", ["-readonly", NOTES_DB_PATH, query], {
|
|
39018
39011
|
encoding: "utf8",
|
|
39019
39012
|
timeout: 5e3,
|
|
39020
39013
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -39106,7 +39099,7 @@ function getChecklistItems(noteId) {
|
|
|
39106
39099
|
return {
|
|
39107
39100
|
items: null,
|
|
39108
39101
|
error: "no_fda",
|
|
39109
|
-
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.`
|
|
39110
39103
|
};
|
|
39111
39104
|
}
|
|
39112
39105
|
if (!hexData) {
|
|
@@ -40540,7 +40533,7 @@ var AppleNotesManager = class {
|
|
|
40540
40533
|
message: "Notes.app is accessible"
|
|
40541
40534
|
});
|
|
40542
40535
|
} else {
|
|
40543
|
-
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)" : "";
|
|
40544
40537
|
checks.push({
|
|
40545
40538
|
name: "notes_app",
|
|
40546
40539
|
passed: false,
|
|
@@ -40560,7 +40553,7 @@ var AppleNotesManager = class {
|
|
|
40560
40553
|
checks.push({
|
|
40561
40554
|
name: "permissions",
|
|
40562
40555
|
passed: !isPermError,
|
|
40563
|
-
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}`
|
|
40564
40557
|
});
|
|
40565
40558
|
if (isPermError) {
|
|
40566
40559
|
return { healthy: false, checks };
|
|
@@ -41366,7 +41359,7 @@ var AppleNotesManager = class {
|
|
|
41366
41359
|
};
|
|
41367
41360
|
|
|
41368
41361
|
// src/utils/syncDetection.ts
|
|
41369
|
-
import { execFileSync as
|
|
41362
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
41370
41363
|
import * as fs2 from "fs";
|
|
41371
41364
|
import * as path2 from "path";
|
|
41372
41365
|
import * as os2 from "os";
|
|
@@ -41407,7 +41400,7 @@ function getSyncStatus(useCache = true) {
|
|
|
41407
41400
|
WHERE ZCURRENTLOCALVERSION > ZLATESTVERSIONSYNCEDTOCLOUD
|
|
41408
41401
|
AND ZLATESTVERSIONSYNCEDTOCLOUD IS NOT NULL;
|
|
41409
41402
|
`;
|
|
41410
|
-
const result =
|
|
41403
|
+
const result = execFileSync3(
|
|
41411
41404
|
"sqlite3",
|
|
41412
41405
|
["-readonly", NOTES_DB_PATH2, query.replace(/\n/g, " ")],
|
|
41413
41406
|
{
|
|
@@ -41466,7 +41459,7 @@ function withSyncAwarenessSync(operation, fn) {
|
|
|
41466
41459
|
}
|
|
41467
41460
|
|
|
41468
41461
|
// src/utils/noteMetadata.ts
|
|
41469
|
-
import { execFileSync as
|
|
41462
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
41470
41463
|
import * as fs3 from "fs";
|
|
41471
41464
|
import * as path3 from "path";
|
|
41472
41465
|
import * as os3 from "os";
|
|
@@ -41474,7 +41467,7 @@ var NOTES_DB_PATH3 = path3.join(
|
|
|
41474
41467
|
os3.homedir(),
|
|
41475
41468
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
41476
41469
|
);
|
|
41477
|
-
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.`;
|
|
41478
41471
|
var COLUMN_MAP = [
|
|
41479
41472
|
{ key: "pinned", column: "ZISPINNED", type: "bool" },
|
|
41480
41473
|
{ key: "hasChecklist", column: "ZHASCHECKLIST", type: "bool" },
|
|
@@ -41487,7 +41480,7 @@ var COLUMN_MAP = [
|
|
|
41487
41480
|
{ key: "smartFolderQuery", column: "ZSMARTFOLDERQUERYJSON", type: "text" }
|
|
41488
41481
|
];
|
|
41489
41482
|
function runSqlite(query) {
|
|
41490
|
-
return
|
|
41483
|
+
return execFileSync4("sqlite3", ["-readonly", NOTES_DB_PATH3, query], {
|
|
41491
41484
|
encoding: "utf8",
|
|
41492
41485
|
timeout: 5e3,
|
|
41493
41486
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -41626,7 +41619,7 @@ function strippedImagesWarning(stripped) {
|
|
|
41626
41619
|
}
|
|
41627
41620
|
|
|
41628
41621
|
// src/tools/doctor.ts
|
|
41629
|
-
import { spawnSync
|
|
41622
|
+
import { spawnSync } from "child_process";
|
|
41630
41623
|
function runDoctor(manager) {
|
|
41631
41624
|
const checks = [];
|
|
41632
41625
|
const hc = manager.healthCheck();
|
|
@@ -41655,7 +41648,7 @@ function runDoctor(manager) {
|
|
|
41655
41648
|
checks.push({
|
|
41656
41649
|
name: "Full Disk Access",
|
|
41657
41650
|
status: fda ? "ok" : "warn",
|
|
41658
|
-
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}`
|
|
41659
41652
|
});
|
|
41660
41653
|
checks.push(checkNodeRuntimeSignature());
|
|
41661
41654
|
const healthy = !checks.some((c) => c.status === "fail");
|
|
@@ -41664,7 +41657,7 @@ function runDoctor(manager) {
|
|
|
41664
41657
|
function checkNodeRuntimeSignature() {
|
|
41665
41658
|
const name = "Node runtime signature";
|
|
41666
41659
|
try {
|
|
41667
|
-
const r =
|
|
41660
|
+
const r = spawnSync("codesign", ["-dvvv", process.execPath], { encoding: "utf8" });
|
|
41668
41661
|
const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
41669
41662
|
if (r.error || !out.trim()) {
|
|
41670
41663
|
return {
|
|
@@ -41678,7 +41671,7 @@ function checkNodeRuntimeSignature() {
|
|
|
41678
41671
|
return {
|
|
41679
41672
|
name,
|
|
41680
41673
|
status: "warn",
|
|
41681
|
-
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}`
|
|
41682
41675
|
};
|
|
41683
41676
|
}
|
|
41684
41677
|
const team = /^TeamIdentifier=(.+)$/m.exec(out)?.[1];
|
|
@@ -42682,7 +42675,7 @@ server.registerTool(
|
|
|
42682
42675
|
return ` ${icon} ${c.name}: ${c.message}`;
|
|
42683
42676
|
}).join("\n");
|
|
42684
42677
|
const fdaAvailable = hasFullDiskAccess();
|
|
42685
|
-
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.`;
|
|
42686
42679
|
return successResponse(`${statusIcon} ${statusText}
|
|
42687
42680
|
|
|
42688
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
|
],
|