apple-notes-mcp 2.0.1 → 2.1.1
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 +64 -9
- package/build/index.js +40 -28
- package/build/services/appleNotesManager.js +260 -64
- package/build/services/appleNotesManager.test.js +187 -139
- package/build/utils/hashtags.js +56 -0
- package/build/utils/hashtags.test.js +45 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that e
|
|
|
6
6
|
[](https://github.com/sweetrb/apple-notes-mcp/actions/workflows/ci.yml)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
+
<p align="center">
|
|
10
|
+
<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">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
9
13
|
## What is This?
|
|
10
14
|
|
|
11
15
|
This server acts as a bridge between AI assistants and Apple Notes. Once configured, you can ask Claude (or any MCP-compatible AI) to:
|
|
@@ -41,6 +45,21 @@ Install as a Claude Code plugin for automatic configuration and enhanced AI beha
|
|
|
41
45
|
|
|
42
46
|
This method also installs a **skill** that teaches Claude when and how to use Apple Notes effectively.
|
|
43
47
|
|
|
48
|
+
### Using the Codex Marketplace
|
|
49
|
+
|
|
50
|
+
The same plugin is available for Codex. Add the marketplace and install the plugin:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
codex plugin marketplace add sweetrb/apple-notes-mcp
|
|
54
|
+
codex plugin add apple-notes@apple-notes-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The Codex plugin runs the published `apple-notes-mcp` server through `npx` and ships the same Apple Notes skill, so behavior matches the Claude Code plugin.
|
|
58
|
+
|
|
59
|
+
### Other Hosts (Hermes, Antigravity)
|
|
60
|
+
|
|
61
|
+
Plugin packaging for the Hermes and Antigravity hosts is also included (`.hermes-plugin/` and `.antigravity-plugin/`). Each registers the same `apple-notes` MCP server (launched via `npx -y apple-notes-mcp`) and bundles the Apple Notes skill, so behavior matches the Claude Code and Codex plugins. Install them through each host's plugin/marketplace mechanism pointed at this repository.
|
|
62
|
+
|
|
44
63
|
### Manual Installation
|
|
45
64
|
|
|
46
65
|
**1. Install the server:**
|
|
@@ -102,6 +121,15 @@ Resources expose read-only context the client can attach without a tool call:
|
|
|
102
121
|
`notes://note/{id}` template (returns the note as Markdown). Prompts package
|
|
103
122
|
common workflows: `find-note`, `weekly-review`, `new-meeting-note`.
|
|
104
123
|
|
|
124
|
+
### Known limitations
|
|
125
|
+
|
|
126
|
+
A few Notes UI features are not exposed to AppleScript and therefore cannot be
|
|
127
|
+
supported. See **[docs/APPLESCRIPT-LIMITATIONS.md](docs/APPLESCRIPT-LIMITATIONS.md)**
|
|
128
|
+
for the investigation and verification behind each:
|
|
129
|
+
|
|
130
|
+
- **Pinned notes** — Notes has no scriptable `pinned` property, so pin state can be neither read nor set.
|
|
131
|
+
- **Note-to-note links** — there is no `applenotes://` deep link or link property; the only stable handle is the `x-coredata://` note id.
|
|
132
|
+
|
|
105
133
|
---
|
|
106
134
|
|
|
107
135
|
## Tool Reference
|
|
@@ -224,7 +252,10 @@ Retrieves the full content of a specific note.
|
|
|
224
252
|
}
|
|
225
253
|
```
|
|
226
254
|
|
|
227
|
-
**Returns:** The HTML content of the note, or error if not found.
|
|
255
|
+
**Returns:** The HTML content of the note, or error if not found. The
|
|
256
|
+
`structuredContent` also includes `hashtags` — any inline `#hashtag` tags parsed
|
|
257
|
+
from the body. Apple Notes tags are inline hashtags, not a scriptable property;
|
|
258
|
+
see [docs/APPLESCRIPT-LIMITATIONS.md](../docs/APPLESCRIPT-LIMITATIONS.md#tags--hashtags-29). Smart Folders are not scriptable.
|
|
228
259
|
|
|
229
260
|
---
|
|
230
261
|
|
|
@@ -322,6 +353,8 @@ Updates an existing note's content and/or title.
|
|
|
322
353
|
|
|
323
354
|
**Returns:** Confirmation message, or error if note not found.
|
|
324
355
|
|
|
356
|
+
**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`.
|
|
357
|
+
|
|
325
358
|
---
|
|
326
359
|
|
|
327
360
|
#### `delete-note`
|
|
@@ -352,6 +385,8 @@ Deletes a note (moves to Recently Deleted in Notes.app).
|
|
|
352
385
|
|
|
353
386
|
**Returns:** Confirmation message, or error if note not found.
|
|
354
387
|
|
|
388
|
+
**⚠️ Safety:** Irreversible from the agent's side — requires explicit user confirmation before calling. Prefer `search-notes` / `list-notes` first to confirm the exact id(s) being deleted.
|
|
389
|
+
|
|
355
390
|
---
|
|
356
391
|
|
|
357
392
|
#### `move-note`
|
|
@@ -481,6 +516,8 @@ Deletes a folder.
|
|
|
481
516
|
|
|
482
517
|
**Returns:** Confirmation message, or error if folder not found or not empty.
|
|
483
518
|
|
|
519
|
+
**⚠️ Safety:** Irreversible — requires explicit user confirmation before calling. Prefer `list-folders` first to confirm the exact folder path being deleted.
|
|
520
|
+
|
|
484
521
|
---
|
|
485
522
|
|
|
486
523
|
### Account Operations
|
|
@@ -512,6 +549,8 @@ Deletes multiple notes at once by ID.
|
|
|
512
549
|
|
|
513
550
|
**Returns:** Summary of successes and failures.
|
|
514
551
|
|
|
552
|
+
**⚠️ Safety:** Irreversible — requires explicit user confirmation before calling. Prefer `search-notes` / `list-notes` first to confirm the exact ids being deleted.
|
|
553
|
+
|
|
515
554
|
---
|
|
516
555
|
|
|
517
556
|
#### `batch-move-notes`
|
|
@@ -653,6 +692,8 @@ Gets comprehensive statistics about your notes.
|
|
|
653
692
|
|
|
654
693
|
**Returns:** Total counts, per-account breakdown, folder statistics, and recently modified counts.
|
|
655
694
|
|
|
695
|
+
The `structuredContent` also includes a `coverage` object — `{ complete, scanned, covered, warnings[] }`. If `complete` is `false`, one or more accounts (or the recent-activity scan) could not be read and the counts reflect only the scopes that succeeded; the text output adds a "⚠️ Partial results" line. This lets you tell a genuinely empty library apart from a partial failure.
|
|
696
|
+
|
|
656
697
|
---
|
|
657
698
|
|
|
658
699
|
#### `get-sync-status`
|
|
@@ -946,13 +987,21 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
946
987
|
## Development
|
|
947
988
|
|
|
948
989
|
```bash
|
|
949
|
-
npm install
|
|
950
|
-
npm run build
|
|
951
|
-
npm test
|
|
952
|
-
npm run
|
|
953
|
-
npm run
|
|
990
|
+
npm install # Install dependencies
|
|
991
|
+
npm run build # Compile TypeScript
|
|
992
|
+
npm test # Run unit test suite (mocked AppleScript)
|
|
993
|
+
npm run test:integration # Run integration tests against real Notes.app
|
|
994
|
+
npm run test:all # Unit + integration
|
|
995
|
+
npm run lint # Check code style
|
|
996
|
+
npm run format # Format code
|
|
954
997
|
```
|
|
955
998
|
|
|
999
|
+
The integration suite (`test/integration.test.ts`) drives the real
|
|
1000
|
+
`AppleNotesManager → AppleScript → Notes.app` stack — creating, reading,
|
|
1001
|
+
searching, and deleting throwaway notes. Its live tests self-skip when no
|
|
1002
|
+
writable Notes account is available (e.g. CI), so it is safe to run anywhere;
|
|
1003
|
+
the pure path-safety and hashtag tests always run.
|
|
1004
|
+
|
|
956
1005
|
---
|
|
957
1006
|
|
|
958
1007
|
## Author
|
|
@@ -974,6 +1023,12 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
|
|
|
974
1023
|
|
|
975
1024
|
## Related Projects
|
|
976
1025
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
- [apple-
|
|
1026
|
+
Part of a family of macOS MCP servers:
|
|
1027
|
+
|
|
1028
|
+
- [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp) — MCP server for Apple Mail (read, search, send, and organize email)
|
|
1029
|
+
- [apple-numbers-mcp](https://github.com/sweetrb/apple-numbers-mcp) — MCP server for Apple Numbers (read and write .numbers spreadsheets)
|
|
1030
|
+
- [apple-photos-mcp](https://github.com/sweetrb/apple-photos-mcp) — MCP server for Apple Photos (query metadata and export originals)
|
|
1031
|
+
|
|
1032
|
+
## Recurring macOS permission prompts
|
|
1033
|
+
|
|
1034
|
+
If macOS keeps re-prompting for Full Disk Access or Automation for `node` (often after a `brew upgrade`), see [docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md](docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md) — the fix is to run this server under the official, Developer-ID-signed Node so the grant survives Node updates.
|
package/build/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { AppleNotesManager } from "./services/appleNotesManager.js";
|
|
|
27
27
|
import { getSyncStatus, withSyncAwarenessSync } from "./utils/syncDetection.js";
|
|
28
28
|
import { getChecklistItems, hasFullDiskAccess } from "./utils/checklistParser.js";
|
|
29
29
|
import { detectChecklistAttempt } from "./utils/contentWarnings.js";
|
|
30
|
+
import { parseHashtags } from "./utils/hashtags.js";
|
|
30
31
|
import { runDoctor, formatDoctorReport } from "./tools/doctor.js";
|
|
31
32
|
import { loadFileConfig } from "./services/fileConfig.js";
|
|
32
33
|
import { registerResourcesAndPrompts } from "./tools/resourcesAndPrompts.js";
|
|
@@ -107,7 +108,7 @@ const folderNameSchema = {
|
|
|
107
108
|
// Note Tools
|
|
108
109
|
// =============================================================================
|
|
109
110
|
// --- create-note ---
|
|
110
|
-
server.tool("create-note", {
|
|
111
|
+
server.tool("create-note", "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id — reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field).", {
|
|
111
112
|
title: z.string().min(1, "Title is required"),
|
|
112
113
|
content: z
|
|
113
114
|
.string()
|
|
@@ -133,7 +134,7 @@ server.tool("create-note", {
|
|
|
133
134
|
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`);
|
|
134
135
|
}, "Error creating note"));
|
|
135
136
|
// --- search-notes ---
|
|
136
|
-
server.tool("search-notes", {
|
|
137
|
+
server.tool("search-notes", "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id.\nDo not use when: you already have a note id (use get-note-content) or want every note (use list-notes).\nPrefer this first to obtain ids for subsequent read/update/delete/move calls.", {
|
|
137
138
|
query: z.string().min(1, "Search query is required"),
|
|
138
139
|
searchContent: z.boolean().optional().describe("Search note content instead of titles"),
|
|
139
140
|
account: z.string().optional().describe("Account to search in"),
|
|
@@ -178,7 +179,7 @@ server.tool("search-notes", {
|
|
|
178
179
|
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
179
180
|
}, "Error searching notes"));
|
|
180
181
|
// --- get-note-content ---
|
|
181
|
-
server.tool("get-note-content", {
|
|
182
|
+
server.tool("get-note-content", "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the note's content plus parsed hashtags.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.", {
|
|
182
183
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
183
184
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
184
185
|
account: z
|
|
@@ -200,7 +201,8 @@ server.tool("get-note-content", {
|
|
|
200
201
|
if (!content) {
|
|
201
202
|
return errorResponse(`Failed to read content of note "${note.title}"`);
|
|
202
203
|
}
|
|
203
|
-
|
|
204
|
+
const hashtags = parseHashtags(content);
|
|
205
|
+
return successResponse(content, { title: note.title, content, hashtags });
|
|
204
206
|
}
|
|
205
207
|
// Fall back to title-based lookup
|
|
206
208
|
if (!title) {
|
|
@@ -218,10 +220,11 @@ server.tool("get-note-content", {
|
|
|
218
220
|
if (!content) {
|
|
219
221
|
return errorResponse(`Failed to read content of note "${title}"`);
|
|
220
222
|
}
|
|
221
|
-
|
|
223
|
+
const hashtags = parseHashtags(content);
|
|
224
|
+
return successResponse(content, { title, content, hashtags });
|
|
222
225
|
}, "Error retrieving note content"));
|
|
223
226
|
// --- get-note-by-id ---
|
|
224
|
-
server.tool("get-note-by-id", {
|
|
227
|
+
server.tool("get-note-by-id", "Use when: you have a note id and need its metadata only.\nReturns: id, title, created, modified, shared, passwordProtected.\nDo not use when: you need the body text (get-note-content) or only have a title (get-note-details).", {
|
|
225
228
|
id: z.string().min(1, "Note ID is required"),
|
|
226
229
|
}, withErrorHandling(({ id }) => {
|
|
227
230
|
const note = notesManager.getNoteById(id);
|
|
@@ -240,7 +243,7 @@ server.tool("get-note-by-id", {
|
|
|
240
243
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
241
244
|
}, "Error retrieving note"));
|
|
242
245
|
// --- get-note-details ---
|
|
243
|
-
server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, account }) => {
|
|
246
|
+
server.tool("get-note-details", "Use when: you have a note title (not an id) and need its metadata.\nReturns: id, title, created, modified, shared, passwordProtected, account.\nDo not use when: you have an id (get-note-by-id) or need the body text (get-note-content).\nUse the returned id for reliable follow-up operations.", noteTitleSchema, withErrorHandling(({ title, account }) => {
|
|
244
247
|
const note = notesManager.getNoteDetails(title, account);
|
|
245
248
|
if (!note) {
|
|
246
249
|
return errorResponse(`Note "${title}" not found`);
|
|
@@ -258,7 +261,7 @@ server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, acc
|
|
|
258
261
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
259
262
|
}, "Error retrieving note details"));
|
|
260
263
|
// --- update-note ---
|
|
261
|
-
server.tool("update-note", {
|
|
264
|
+
server.tool("update-note", "Use when: changing the title and/or replacing the body of an existing note, by id (preferred) or title.\nReturns: confirmation; warns when the note is shared.\nDo not use when: creating a new note (create-note).\nSafety: newContent REPLACES the entire body — it does not append. Read the note first if you need to preserve existing text. Edits to shared notes are immediately visible to all collaborators.", {
|
|
262
265
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
263
266
|
title: z.string().optional().describe("Current note title (use id instead when available)"),
|
|
264
267
|
newTitle: z.string().optional().describe("New title for the note"),
|
|
@@ -323,7 +326,7 @@ server.tool("update-note", {
|
|
|
323
326
|
return successResponse(`Note updated: "${finalTitle}"${sharedWarning}${checklistWarning}`);
|
|
324
327
|
}, "Error updating note"));
|
|
325
328
|
// --- delete-note ---
|
|
326
|
-
server.tool("delete-note", {
|
|
329
|
+
server.tool("delete-note", "Use when: permanently deleting a single note, by id (preferred) or title.\nReturns: confirmation; warns when the note was shared.\nDo not use when: deleting many notes (batch-delete-notes) or just relocating one (move-note).\nSafety: requires explicit user confirmation before deleting. Prefer search-notes/list-notes first to show the affected note id and title. Deleting a shared note removes collaborator access.", {
|
|
327
330
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
328
331
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
329
332
|
account: z
|
|
@@ -368,7 +371,7 @@ server.tool("delete-note", {
|
|
|
368
371
|
return successResponse(`Note deleted: "${title}"${sharedWarning}`);
|
|
369
372
|
}, "Error deleting note"));
|
|
370
373
|
// --- move-note ---
|
|
371
|
-
server.tool("move-note", {
|
|
374
|
+
server.tool("move-note", "Use when: moving one note to a different folder, by id (preferred) or title.\nReturns: confirmation of the note and destination folder.\nDo not use when: moving many notes (batch-move-notes).\nNote: implemented as copy-then-delete; the destination folder must already exist (create-folder).", {
|
|
372
375
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
373
376
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
374
377
|
folder: z.string().min(1, "Destination folder is required"),
|
|
@@ -403,7 +406,7 @@ server.tool("move-note", {
|
|
|
403
406
|
return successResponse(`Note moved: "${title}" -> "${folder}"`);
|
|
404
407
|
}, "Error moving note"));
|
|
405
408
|
// --- list-notes ---
|
|
406
|
-
server.tool("list-notes", {
|
|
409
|
+
server.tool("list-notes", "Use when: enumerating notes in an account or folder; supports modifiedSince and limit for large collections.\nReturns: note titles only (no content or ids).\nDo not use when: you need content (get-note-content) or ids for follow-up edits (use search-notes).\nNote: warns if iCloud sync is active and results may be partial.", {
|
|
407
410
|
account: z.string().optional().describe("Account to list notes from"),
|
|
408
411
|
folder: z.string().optional().describe("Filter to specific folder"),
|
|
409
412
|
modifiedSince: z
|
|
@@ -441,7 +444,7 @@ server.tool("list-notes", {
|
|
|
441
444
|
// Folder Tools
|
|
442
445
|
// =============================================================================
|
|
443
446
|
// --- list-folders ---
|
|
444
|
-
server.tool("list-folders", {
|
|
447
|
+
server.tool("list-folders", "Use when: listing all folders, with full nested paths, for an account.\nReturns: folder names/paths.\nDo not use when: listing notes (list-notes).\nNote: warns if iCloud sync is active.", {
|
|
445
448
|
account: z.string().optional().describe("Account to list folders from"),
|
|
446
449
|
}, withErrorHandling(({ account }) => {
|
|
447
450
|
// Use sync-aware wrapper for this read operation
|
|
@@ -466,7 +469,7 @@ server.tool("list-folders", {
|
|
|
466
469
|
});
|
|
467
470
|
}, "Error listing folders"));
|
|
468
471
|
// --- create-folder ---
|
|
469
|
-
server.tool("create-folder", {
|
|
472
|
+
server.tool("create-folder", "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nDo not use when: creating a note (create-note).", {
|
|
470
473
|
name: z
|
|
471
474
|
.string()
|
|
472
475
|
.min(1, "Folder name is required")
|
|
@@ -480,7 +483,7 @@ server.tool("create-folder", {
|
|
|
480
483
|
return successResponse(`Folder created: "${folder.name}"`);
|
|
481
484
|
}, "Error creating folder"));
|
|
482
485
|
// --- delete-folder ---
|
|
483
|
-
server.tool("delete-folder", folderNameSchema, withErrorHandling(({ name, account }) => {
|
|
486
|
+
server.tool("delete-folder", "Use when: deleting an existing folder by name or nested path.\nReturns: confirmation.\nDo not use when: deleting a note (delete-note).\nSafety: requires explicit user confirmation. Deletion fails if the folder still contains notes — list or move those notes first.", folderNameSchema, withErrorHandling(({ name, account }) => {
|
|
484
487
|
const success = notesManager.deleteFolder(name, account);
|
|
485
488
|
if (!success) {
|
|
486
489
|
return errorResponse(`Failed to delete folder "${name}". Folder may not exist or may contain notes.`);
|
|
@@ -491,7 +494,7 @@ server.tool("delete-folder", folderNameSchema, withErrorHandling(({ name, accoun
|
|
|
491
494
|
// Account Tools
|
|
492
495
|
// =============================================================================
|
|
493
496
|
// --- list-accounts ---
|
|
494
|
-
server.tool("list-accounts", {}, withErrorHandling(() => {
|
|
497
|
+
server.tool("list-accounts", "Use when: discovering which Notes accounts exist (iCloud, Gmail, Exchange, etc.) before targeting one.\nReturns: account names.\nDo not use when: you already know the account, or are working by note id (ids are account-independent).", {}, withErrorHandling(() => {
|
|
495
498
|
const accounts = notesManager.listAccounts();
|
|
496
499
|
if (accounts.length === 0) {
|
|
497
500
|
return successResponse("No Notes accounts found", { accounts: [], count: 0 });
|
|
@@ -506,7 +509,7 @@ server.tool("list-accounts", {}, withErrorHandling(() => {
|
|
|
506
509
|
// Collaboration Tools
|
|
507
510
|
// =============================================================================
|
|
508
511
|
// --- list-shared-notes ---
|
|
509
|
-
server.tool("list-shared-notes", {}, withErrorHandling(() => {
|
|
512
|
+
server.tool("list-shared-notes", "Use when: finding notes shared with collaborators.\nReturns: shared notes with title, account, and id.\nDo not use when: searching all notes (search-notes).\nNote: edits or deletes to these notes affect all collaborators.", {}, withErrorHandling(() => {
|
|
510
513
|
const sharedNotes = notesManager.listSharedNotes();
|
|
511
514
|
if (sharedNotes.length === 0) {
|
|
512
515
|
return successResponse("No shared notes found. You have no notes shared with collaborators.", { notes: [], count: 0 });
|
|
@@ -524,7 +527,7 @@ server.tool("list-shared-notes", {}, withErrorHandling(() => {
|
|
|
524
527
|
// Diagnostics Tools
|
|
525
528
|
// =============================================================================
|
|
526
529
|
// --- get-sync-status ---
|
|
527
|
-
server.tool("get-sync-status", {}, withErrorHandling(() => {
|
|
530
|
+
server.tool("get-sync-status", "Use when: checking whether iCloud sync is in progress before trusting read results.\nReturns: sync active/idle, pending upload count, and seconds since last change.\nDo not use when: you need note data — this is a read-only diagnostics tool.", {}, withErrorHandling(() => {
|
|
528
531
|
const status = getSyncStatus();
|
|
529
532
|
if (status.error) {
|
|
530
533
|
return successResponse(`⚠️ Sync status unknown: ${status.error}`, { ...status });
|
|
@@ -550,7 +553,7 @@ server.tool("get-sync-status", {}, withErrorHandling(() => {
|
|
|
550
553
|
return successResponse(lines.join("\n"), { ...status });
|
|
551
554
|
}, "Error checking sync status"));
|
|
552
555
|
// --- health-check ---
|
|
553
|
-
server.tool("health-check", {}, withErrorHandling(() => {
|
|
556
|
+
server.tool("health-check", "Use when: a quick check that Notes.app is reachable and (optionally) Full Disk Access is granted for checklist features.\nReturns: pass/fail per check.\nDo not use when: you need detailed, actionable setup diagnostics (use doctor).\nRead-only.", {}, withErrorHandling(() => {
|
|
554
557
|
const result = notesManager.healthCheck();
|
|
555
558
|
const statusIcon = result.healthy ? "✓" : "✗";
|
|
556
559
|
const statusText = result.healthy ? "All checks passed" : "Issues detected";
|
|
@@ -568,14 +571,14 @@ server.tool("health-check", {}, withErrorHandling(() => {
|
|
|
568
571
|
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}\n${fdaLine}`);
|
|
569
572
|
}, "Error running health check"));
|
|
570
573
|
// --- doctor ---
|
|
571
|
-
server.tool("doctor", {}, withErrorHandling(() => {
|
|
574
|
+
server.tool("doctor", "Use when: diagnosing setup problems (Notes.app automation permission, account state, Full Disk Access) with actionable guidance.\nReturns: a detailed report plus structured fields.\nDo not use when: you just need a quick pass/fail (health-check).\nRead-only.", {}, withErrorHandling(() => {
|
|
572
575
|
// Richer than health-check: Notes.app permission, account state, and Full
|
|
573
576
|
// Disk Access with actionable messages + structuredContent (#22).
|
|
574
577
|
const report = runDoctor(notesManager);
|
|
575
578
|
return successResponse(formatDoctorReport(report), { ...report });
|
|
576
579
|
}, "Error running doctor"));
|
|
577
580
|
// --- get-notes-stats ---
|
|
578
|
-
server.tool("get-notes-stats", {}, withErrorHandling(() => {
|
|
581
|
+
server.tool("get-notes-stats", "Use when: summarizing the library — total notes, per-account/folder counts, and recent activity.\nReturns: aggregate statistics; flags partial coverage when some scopes were unreadable.\nDo not use when: you need individual notes (list-notes/search-notes).\nRead-only.", {}, withErrorHandling(() => {
|
|
579
582
|
const stats = notesManager.getNotesStats();
|
|
580
583
|
// Format the output
|
|
581
584
|
const lines = [];
|
|
@@ -599,10 +602,19 @@ server.tool("get-notes-stats", {}, withErrorHandling(() => {
|
|
|
599
602
|
lines.push(` Last 24 hours: ${stats.recentlyModified.last24h}`);
|
|
600
603
|
lines.push(` Last 7 days: ${stats.recentlyModified.last7d}`);
|
|
601
604
|
lines.push(` Last 30 days: ${stats.recentlyModified.last30d}`);
|
|
605
|
+
// Partial-coverage diagnostics (#19): if some scopes couldn't be read, say so
|
|
606
|
+
// explicitly so the numbers above aren't mistaken for a complete picture.
|
|
607
|
+
if (!stats.coverage.complete) {
|
|
608
|
+
lines.push(``);
|
|
609
|
+
lines.push(`⚠️ Partial results: read ${stats.coverage.covered}/${stats.coverage.scanned} scopes. Counts above exclude:`);
|
|
610
|
+
for (const w of stats.coverage.warnings) {
|
|
611
|
+
lines.push(` - ${w.scope}: ${w.reason}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
602
614
|
return successResponse(lines.join("\n"), { ...stats });
|
|
603
615
|
}, "Error getting notes statistics"));
|
|
604
616
|
// --- list-attachments ---
|
|
605
|
-
server.tool("list-attachments", {
|
|
617
|
+
server.tool("list-attachments", "Use when: listing the attachments of one note, by id (preferred) or title.\nReturns: each attachment's name, content type, and id (use with save-attachment/fetch-attachment).\nDo not use when: you want the attachment bytes (fetch-attachment) or a file on disk (save-attachment).", {
|
|
606
618
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
607
619
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
608
620
|
account: z
|
|
@@ -642,7 +654,7 @@ server.tool("list-attachments", {
|
|
|
642
654
|
return successResponse(`Found ${attachments.length} attachment(s) in "${title}":\n${attachmentList}`, { attachments, count: attachments.length });
|
|
643
655
|
}, "Error listing attachments"));
|
|
644
656
|
// --- batch-delete-notes ---
|
|
645
|
-
server.tool("batch-delete-notes", {
|
|
657
|
+
server.tool("batch-delete-notes", "Use when: permanently deleting multiple notes by id in one call.\nReturns: per-id success/failure counts.\nDo not use when: deleting a single note (delete-note).\nSafety: requires explicit user confirmation; this is destructive and not undoable. Prefer search-notes/list-notes first to confirm the exact ids being deleted.", {
|
|
646
658
|
ids: z.array(z.string()).describe("Array of note IDs to delete"),
|
|
647
659
|
}, withErrorHandling(({ ids }) => {
|
|
648
660
|
if (ids.length === 0) {
|
|
@@ -661,7 +673,7 @@ server.tool("batch-delete-notes", {
|
|
|
661
673
|
return succeeded > 0 ? successResponse(lines.join("\n")) : errorResponse(lines.join("\n"));
|
|
662
674
|
}, "Error performing batch delete"));
|
|
663
675
|
// --- batch-move-notes ---
|
|
664
|
-
server.tool("batch-move-notes", {
|
|
676
|
+
server.tool("batch-move-notes", "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts.\nDo not use when: moving a single note (move-note).\nNote: the destination folder must already exist (create-folder).", {
|
|
665
677
|
ids: z.array(z.string()).describe("Array of note IDs to move"),
|
|
666
678
|
folder: z.string().describe("Destination folder name"),
|
|
667
679
|
account: z
|
|
@@ -685,7 +697,7 @@ server.tool("batch-move-notes", {
|
|
|
685
697
|
return succeeded > 0 ? successResponse(lines.join("\n")) : errorResponse(lines.join("\n"));
|
|
686
698
|
}, "Error performing batch move"));
|
|
687
699
|
// --- save-attachment ---
|
|
688
|
-
server.tool("save-attachment", {
|
|
700
|
+
server.tool("save-attachment", "Use when: writing one note attachment to a file on disk.\nReturns: the saved path.\nDo not use when: you want the bytes in-memory as base64 (fetch-attachment).\nSafety: writes a file; savePath must be absolute and under the home directory, a temp dir, or /Volumes. Get the ids from list-attachments first.", {
|
|
689
701
|
noteId: z.string().min(1, "noteId is required").describe("CoreData note id (from search/list)"),
|
|
690
702
|
attachmentId: z
|
|
691
703
|
.string()
|
|
@@ -707,7 +719,7 @@ server.tool("save-attachment", {
|
|
|
707
719
|
});
|
|
708
720
|
}, "Error saving attachment"));
|
|
709
721
|
// --- fetch-attachment ---
|
|
710
|
-
server.tool("fetch-attachment", {
|
|
722
|
+
server.tool("fetch-attachment", "Use when: retrieving one note attachment's bytes inline as base64 (no file written).\nReturns: name, content type, byte count, and base64 data.\nDo not use when: you want it saved to disk (save-attachment).\nNote: get the ids from list-attachments first.", {
|
|
711
723
|
noteId: z.string().min(1, "noteId is required").describe("CoreData note id (from search/list)"),
|
|
712
724
|
attachmentId: z
|
|
713
725
|
.string()
|
|
@@ -721,7 +733,7 @@ server.tool("fetch-attachment", {
|
|
|
721
733
|
return successResponse(`Fetched "${r.name ?? "attachment"}" (${r.contentType ?? "unknown type"}, ${r.bytes ?? 0} bytes) as base64.`, { name: r.name, contentType: r.contentType, bytes: r.bytes, base64: r.base64 });
|
|
722
734
|
}, "Error fetching attachment"));
|
|
723
735
|
// --- export-notes-json ---
|
|
724
|
-
server.tool("export-notes-json", {}, withErrorHandling(() => {
|
|
736
|
+
server.tool("export-notes-json", "Use when: exporting the entire notes library as structured JSON for backup or bulk processing.\nReturns: a summary plus the full JSON of all notes, folders, and accounts.\nDo not use when: you need a single note (get-note-content) — this reads everything and can be large.\nRead-only.", {}, withErrorHandling(() => {
|
|
725
737
|
const exportData = notesManager.exportNotesAsJson();
|
|
726
738
|
const { summary } = exportData;
|
|
727
739
|
return {
|
|
@@ -739,7 +751,7 @@ server.tool("export-notes-json", {}, withErrorHandling(() => {
|
|
|
739
751
|
};
|
|
740
752
|
}, "Error exporting notes"));
|
|
741
753
|
// --- get-note-markdown ---
|
|
742
|
-
server.tool("get-note-markdown", {
|
|
754
|
+
server.tool("get-note-markdown", "Use when: reading a note as Markdown, with checklist items annotated [x]/[ ] when Full Disk Access is granted.\nReturns: the note's Markdown.\nDo not use when: you need the raw HTML/plaintext body (get-note-content) or only metadata (get-note-details).\nNote: falls back to plain lists (no checkmarks) without Full Disk Access.", {
|
|
743
755
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
744
756
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
745
757
|
account: z
|
|
@@ -766,7 +778,7 @@ server.tool("get-note-markdown", {
|
|
|
766
778
|
return successResponse(markdown, { markdown });
|
|
767
779
|
}, "Error getting note as markdown"));
|
|
768
780
|
// --- get-checklist-state ---
|
|
769
|
-
server.tool("get-checklist-state", {
|
|
781
|
+
server.tool("get-checklist-state", "Use when: reading the checked/unchecked state of a note's checklist items, by id.\nReturns: each item's text and done state plus checked/total counts.\nDo not use when: you only have a title (get the id via search-notes first) or want the full body text (get-note-content).\nNote: requires Full Disk Access; reads the NoteStore database directly.", {
|
|
770
782
|
id: z.string().min(1, "Note ID is required. Use search-notes to find the note ID first."),
|
|
771
783
|
}, withErrorHandling(({ id }) => {
|
|
772
784
|
// Verify the note exists and is accessible
|