apple-notes-mcp 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/build/index.js +507 -139
- package/build/services/__fixtures__/notesNormalizedHtml.js +32 -0
- package/build/services/appleNotesManager.js +132 -0
- package/build/services/appleNotesManager.test.js +109 -0
- package/build/services/notesHtmlMarkdown.test.js +55 -0
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -108,23 +108,33 @@ const folderNameSchema = {
|
|
|
108
108
|
// Note Tools
|
|
109
109
|
// =============================================================================
|
|
110
110
|
// --- create-note ---
|
|
111
|
-
server.
|
|
112
|
-
title:
|
|
113
|
-
|
|
114
|
-
.string()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.string()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
111
|
+
server.registerTool("create-note", {
|
|
112
|
+
description: "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).",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
title: z.string().min(1, "Title is required"),
|
|
115
|
+
content: z
|
|
116
|
+
.string()
|
|
117
|
+
.min(1, "Content is required")
|
|
118
|
+
.describe('Note body. AppleScript cannot create true Apple Notes checklists — `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items. To produce a checklist, create the note with a plain `<ul>` or `- ` list and convert it in Notes.app with ⇧⌘L.'),
|
|
119
|
+
format: z
|
|
120
|
+
.enum(["plaintext", "html"])
|
|
121
|
+
.optional()
|
|
122
|
+
.default("plaintext")
|
|
123
|
+
.describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
124
|
+
tags: z.array(z.string()).optional().describe("Tags for organization"),
|
|
125
|
+
folder: z
|
|
126
|
+
.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe("Folder to create the note in (supports nested paths like 'Work/Clients')"),
|
|
129
|
+
account: z.string().optional().describe("Account name (defaults to iCloud)"),
|
|
130
|
+
},
|
|
131
|
+
outputSchema: {
|
|
132
|
+
ok: z.boolean().optional(),
|
|
133
|
+
id: z.string().optional(),
|
|
134
|
+
title: z.string().optional(),
|
|
135
|
+
folder: z.string().optional(),
|
|
136
|
+
account: z.string().optional(),
|
|
137
|
+
},
|
|
128
138
|
}, withErrorHandling(({ title, content, format = "plaintext", tags = [], folder, account }) => {
|
|
129
139
|
const note = notesManager.createNote(title, content, tags, folder, account, format);
|
|
130
140
|
if (!note) {
|
|
@@ -140,16 +150,23 @@ server.tool("create-note", "Use when: the user wants to create a brand-new Apple
|
|
|
140
150
|
});
|
|
141
151
|
}, "Error creating note"));
|
|
142
152
|
// --- search-notes ---
|
|
143
|
-
server.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.string()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
server.registerTool("search-notes", {
|
|
154
|
+
description: "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.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
query: z.string().min(1, "Search query is required"),
|
|
157
|
+
searchContent: z.boolean().optional().describe("Search note content instead of titles"),
|
|
158
|
+
account: z.string().optional().describe("Account to search in"),
|
|
159
|
+
folder: z.string().optional().describe("Limit search to a specific folder"),
|
|
160
|
+
modifiedSince: z
|
|
161
|
+
.string()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("ISO 8601 date string to filter notes modified on or after this date (e.g., '2025-01-01'). Useful for searching only recent notes in large collections."),
|
|
164
|
+
limit: z.number().int().positive().optional().describe("Maximum number of results to return"),
|
|
165
|
+
},
|
|
166
|
+
outputSchema: {
|
|
167
|
+
notes: z.array(z.object({}).passthrough()).optional(),
|
|
168
|
+
count: z.number().optional(),
|
|
169
|
+
},
|
|
153
170
|
}, withErrorHandling(({ query, searchContent = false, account, folder, modifiedSince, limit }) => {
|
|
154
171
|
// Use sync-aware wrapper for this read operation
|
|
155
172
|
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("search-notes", () => notesManager.searchNotes(query, searchContent, account, folder, modifiedSince, limit));
|
|
@@ -185,13 +202,21 @@ server.tool("search-notes", "Use when: finding notes by a keyword in the title (
|
|
|
185
202
|
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
186
203
|
}, "Error searching notes"));
|
|
187
204
|
// --- get-note-content ---
|
|
188
|
-
server.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
.string()
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
server.registerTool("get-note-content", {
|
|
206
|
+
description: "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.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
209
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
210
|
+
account: z
|
|
211
|
+
.string()
|
|
212
|
+
.optional()
|
|
213
|
+
.describe("Account name (defaults to iCloud, ignored if id is provided)"),
|
|
214
|
+
},
|
|
215
|
+
outputSchema: {
|
|
216
|
+
title: z.string().optional(),
|
|
217
|
+
content: z.string().optional(),
|
|
218
|
+
hashtags: z.array(z.string()).optional(),
|
|
219
|
+
},
|
|
195
220
|
}, withErrorHandling(({ id, title, account }) => {
|
|
196
221
|
// Prefer ID-based lookup if provided
|
|
197
222
|
if (id) {
|
|
@@ -229,9 +254,68 @@ server.tool("get-note-content", "Use when: reading the full body text of one kno
|
|
|
229
254
|
const hashtags = parseHashtags(content);
|
|
230
255
|
return successResponse(content, { title, content, hashtags });
|
|
231
256
|
}, "Error retrieving note content"));
|
|
257
|
+
// --- get-note-plaintext ---
|
|
258
|
+
server.registerTool("get-note-plaintext", {
|
|
259
|
+
description: "Use when: reading one note's body as plain text with no HTML, by id (preferred) or title.\nReturns: the note's plaintext exactly as Notes exposes it.\nDo not use when: you need the HTML body (get-note-content) or Markdown with checklist state (get-note-markdown).\nNote: this reads the note's native plaintext property, so it skips the HTML-to-text conversion; password-protected notes must be unlocked in Notes.app first.",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
262
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
263
|
+
account: z
|
|
264
|
+
.string()
|
|
265
|
+
.optional()
|
|
266
|
+
.describe("Account name (defaults to iCloud, ignored if id is provided)"),
|
|
267
|
+
},
|
|
268
|
+
outputSchema: {
|
|
269
|
+
title: z.string().optional(),
|
|
270
|
+
plaintext: z.string().optional(),
|
|
271
|
+
},
|
|
272
|
+
}, withErrorHandling(({ id, title, account }) => {
|
|
273
|
+
// Prefer ID-based lookup if provided
|
|
274
|
+
if (id) {
|
|
275
|
+
const note = notesManager.getNoteById(id);
|
|
276
|
+
if (!note) {
|
|
277
|
+
return errorResponse(`Note with ID "${id}" not found`);
|
|
278
|
+
}
|
|
279
|
+
if (note.passwordProtected) {
|
|
280
|
+
return errorResponse(`Note "${note.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`);
|
|
281
|
+
}
|
|
282
|
+
const plaintext = notesManager.getNotePlaintextById(id);
|
|
283
|
+
if (!plaintext) {
|
|
284
|
+
return errorResponse(`Failed to read plaintext of note "${note.title}"`);
|
|
285
|
+
}
|
|
286
|
+
return successResponse(plaintext, { title: note.title, plaintext });
|
|
287
|
+
}
|
|
288
|
+
// Fall back to title-based lookup
|
|
289
|
+
if (!title) {
|
|
290
|
+
return errorResponse("Either 'id' or 'title' is required");
|
|
291
|
+
}
|
|
292
|
+
const note = notesManager.getNoteDetails(title, account);
|
|
293
|
+
if (!note) {
|
|
294
|
+
return errorResponse(`Note "${title}" not found`);
|
|
295
|
+
}
|
|
296
|
+
if (note.passwordProtected) {
|
|
297
|
+
return errorResponse(`Note "${title}" is password-protected and cannot be read. Unlock it in Notes.app first.`);
|
|
298
|
+
}
|
|
299
|
+
const plaintext = notesManager.getNotePlaintext(title, account);
|
|
300
|
+
if (!plaintext) {
|
|
301
|
+
return errorResponse(`Failed to read plaintext of note "${title}"`);
|
|
302
|
+
}
|
|
303
|
+
return successResponse(plaintext, { title, plaintext });
|
|
304
|
+
}, "Error retrieving note plaintext"));
|
|
232
305
|
// --- get-note-by-id ---
|
|
233
|
-
server.
|
|
234
|
-
id:
|
|
306
|
+
server.registerTool("get-note-by-id", {
|
|
307
|
+
description: "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).",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
id: z.string().min(1, "Note ID is required"),
|
|
310
|
+
},
|
|
311
|
+
outputSchema: {
|
|
312
|
+
id: z.string().optional(),
|
|
313
|
+
title: z.string().optional(),
|
|
314
|
+
created: z.string().optional(),
|
|
315
|
+
modified: z.string().optional(),
|
|
316
|
+
shared: z.boolean().optional(),
|
|
317
|
+
passwordProtected: z.boolean().optional(),
|
|
318
|
+
},
|
|
235
319
|
}, withErrorHandling(({ id }) => {
|
|
236
320
|
const note = notesManager.getNoteById(id);
|
|
237
321
|
if (!note) {
|
|
@@ -249,7 +333,19 @@ server.tool("get-note-by-id", "Use when: you have a note id and need its metadat
|
|
|
249
333
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
250
334
|
}, "Error retrieving note"));
|
|
251
335
|
// --- get-note-details ---
|
|
252
|
-
server.
|
|
336
|
+
server.registerTool("get-note-details", {
|
|
337
|
+
description: "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.",
|
|
338
|
+
inputSchema: noteTitleSchema,
|
|
339
|
+
outputSchema: {
|
|
340
|
+
id: z.string().optional(),
|
|
341
|
+
title: z.string().optional(),
|
|
342
|
+
created: z.string().optional(),
|
|
343
|
+
modified: z.string().optional(),
|
|
344
|
+
shared: z.boolean().optional(),
|
|
345
|
+
passwordProtected: z.boolean().optional(),
|
|
346
|
+
account: z.string().optional(),
|
|
347
|
+
},
|
|
348
|
+
}, withErrorHandling(({ title, account }) => {
|
|
253
349
|
const note = notesManager.getNoteDetails(title, account);
|
|
254
350
|
if (!note) {
|
|
255
351
|
return errorResponse(`Note "${title}" not found`);
|
|
@@ -267,12 +363,19 @@ server.tool("get-note-details", "Use when: you have a note title (not an id) and
|
|
|
267
363
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
268
364
|
}, "Error retrieving note details"));
|
|
269
365
|
// --- show-note ---
|
|
270
|
-
server.
|
|
271
|
-
id:
|
|
272
|
-
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
|
|
366
|
+
server.registerTool("show-note", {
|
|
367
|
+
description: "Use when: the user wants to reveal a known note in Notes.app by id.\nReturns: confirmation that Notes.app accepted the show command.\nDo not use when: you only need note content (get-note-content) or metadata (get-note-by-id).\nNote: this opens or focuses the Notes UI.",
|
|
368
|
+
inputSchema: {
|
|
369
|
+
id: z.string().min(1, "Note ID is required"),
|
|
370
|
+
separately: z
|
|
371
|
+
.boolean()
|
|
372
|
+
.optional()
|
|
373
|
+
.describe("Open in a separate note window when supported by Notes.app"),
|
|
374
|
+
},
|
|
375
|
+
outputSchema: {
|
|
376
|
+
id: z.string().optional(),
|
|
377
|
+
separately: z.boolean().optional(),
|
|
378
|
+
},
|
|
276
379
|
}, withErrorHandling(({ id, separately = false }) => {
|
|
277
380
|
const success = notesManager.showNoteById(id, separately);
|
|
278
381
|
if (!success) {
|
|
@@ -280,24 +383,75 @@ server.tool("show-note", "Use when: the user wants to reveal a known note in Not
|
|
|
280
383
|
}
|
|
281
384
|
return successResponse(`Shown note with ID "${id}" in Notes.app`, { id, separately });
|
|
282
385
|
}, "Error showing note"));
|
|
386
|
+
// --- show-folder ---
|
|
387
|
+
server.registerTool("show-folder", {
|
|
388
|
+
description: "Use when: the user wants to reveal a known folder in Notes.app by id.\nReturns: confirmation that Notes.app accepted the show command.\nDo not use when: you only need the folder list (list-folders).\nNote: this opens or focuses the Notes UI. Get the id from list-folders.",
|
|
389
|
+
inputSchema: {
|
|
390
|
+
id: z.string().min(1, "Folder ID is required"),
|
|
391
|
+
separately: z
|
|
392
|
+
.boolean()
|
|
393
|
+
.optional()
|
|
394
|
+
.describe("Open in a separate window when supported by Notes.app"),
|
|
395
|
+
},
|
|
396
|
+
outputSchema: {
|
|
397
|
+
id: z.string().optional(),
|
|
398
|
+
separately: z.boolean().optional(),
|
|
399
|
+
},
|
|
400
|
+
}, withErrorHandling(({ id, separately = false }) => {
|
|
401
|
+
const success = notesManager.showFolderById(id, separately);
|
|
402
|
+
if (!success) {
|
|
403
|
+
return errorResponse(`Failed to show folder with ID "${id}"`);
|
|
404
|
+
}
|
|
405
|
+
return successResponse(`Shown folder with ID "${id}" in Notes.app`, { id, separately });
|
|
406
|
+
}, "Error showing folder"));
|
|
407
|
+
// --- show-account ---
|
|
408
|
+
server.registerTool("show-account", {
|
|
409
|
+
description: "Use when: the user wants to reveal a known account in Notes.app by id.\nReturns: confirmation that Notes.app accepted the show command.\nDo not use when: you only need the account list (list-accounts).\nNote: this opens or focuses the Notes UI. Get the id from list-accounts.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
id: z.string().min(1, "Account ID is required"),
|
|
412
|
+
separately: z
|
|
413
|
+
.boolean()
|
|
414
|
+
.optional()
|
|
415
|
+
.describe("Open in a separate window when supported by Notes.app"),
|
|
416
|
+
},
|
|
417
|
+
outputSchema: {
|
|
418
|
+
id: z.string().optional(),
|
|
419
|
+
separately: z.boolean().optional(),
|
|
420
|
+
},
|
|
421
|
+
}, withErrorHandling(({ id, separately = false }) => {
|
|
422
|
+
const success = notesManager.showAccountById(id, separately);
|
|
423
|
+
if (!success) {
|
|
424
|
+
return errorResponse(`Failed to show account with ID "${id}"`);
|
|
425
|
+
}
|
|
426
|
+
return successResponse(`Shown account with ID "${id}" in Notes.app`, { id, separately });
|
|
427
|
+
}, "Error showing account"));
|
|
283
428
|
// --- update-note ---
|
|
284
|
-
server.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
.string()
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
429
|
+
server.registerTool("update-note", {
|
|
430
|
+
description: "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, and run list-attachments first when the note may hold files, images, scans, PDFs, or audio, since a full-body replace can drop embedded attachments. Edits to shared notes are immediately visible to all collaborators.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
433
|
+
title: z.string().optional().describe("Current note title (use id instead when available)"),
|
|
434
|
+
newTitle: z.string().optional().describe("New title for the note"),
|
|
435
|
+
newContent: z
|
|
436
|
+
.string()
|
|
437
|
+
.min(1, "New content is required")
|
|
438
|
+
.describe("New note body. AppleScript cannot produce true Apple Notes checklists; checkbox inputs and `- [ ]` markdown do not render as checkable items. Use a plain list and convert in Notes.app with ⇧⌘L."),
|
|
439
|
+
format: z
|
|
440
|
+
.enum(["plaintext", "html"])
|
|
441
|
+
.optional()
|
|
442
|
+
.default("plaintext")
|
|
443
|
+
.describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
444
|
+
account: z
|
|
445
|
+
.string()
|
|
446
|
+
.optional()
|
|
447
|
+
.describe("Account containing the note (ignored if id is provided)"),
|
|
448
|
+
},
|
|
449
|
+
outputSchema: {
|
|
450
|
+
ok: z.boolean().optional(),
|
|
451
|
+
id: z.string().optional(),
|
|
452
|
+
title: z.string().optional(),
|
|
453
|
+
shared: z.boolean().optional(),
|
|
454
|
+
},
|
|
301
455
|
}, withErrorHandling(({ id, title, newTitle, newContent, format = "plaintext", account }) => {
|
|
302
456
|
// Prefer ID-based update if provided
|
|
303
457
|
if (id) {
|
|
@@ -355,13 +509,22 @@ server.tool("update-note", "Use when: changing the title and/or replacing the bo
|
|
|
355
509
|
});
|
|
356
510
|
}, "Error updating note"));
|
|
357
511
|
// --- delete-note ---
|
|
358
|
-
server.
|
|
359
|
-
id:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
.string()
|
|
363
|
-
|
|
364
|
-
|
|
512
|
+
server.registerTool("delete-note", {
|
|
513
|
+
description: "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.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
516
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
517
|
+
account: z
|
|
518
|
+
.string()
|
|
519
|
+
.optional()
|
|
520
|
+
.describe("Account name (defaults to iCloud, ignored if id is provided)"),
|
|
521
|
+
},
|
|
522
|
+
outputSchema: {
|
|
523
|
+
ok: z.boolean().optional(),
|
|
524
|
+
id: z.string().optional(),
|
|
525
|
+
title: z.string().optional(),
|
|
526
|
+
wasShared: z.boolean().optional(),
|
|
527
|
+
},
|
|
365
528
|
}, withErrorHandling(({ id, title, account }) => {
|
|
366
529
|
// Prefer ID-based deletion if provided
|
|
367
530
|
if (id) {
|
|
@@ -409,11 +572,20 @@ server.tool("delete-note", "Use when: permanently deleting a single note, by id
|
|
|
409
572
|
});
|
|
410
573
|
}, "Error deleting note"));
|
|
411
574
|
// --- move-note ---
|
|
412
|
-
server.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
575
|
+
server.registerTool("move-note", {
|
|
576
|
+
description: "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).",
|
|
577
|
+
inputSchema: {
|
|
578
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
579
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
580
|
+
folder: z.string().min(1, "Destination folder is required"),
|
|
581
|
+
account: z.string().optional().describe("Account containing the note/folder"),
|
|
582
|
+
},
|
|
583
|
+
outputSchema: {
|
|
584
|
+
ok: z.boolean().optional(),
|
|
585
|
+
id: z.string().optional(),
|
|
586
|
+
title: z.string().optional(),
|
|
587
|
+
folder: z.string().optional(),
|
|
588
|
+
},
|
|
417
589
|
}, withErrorHandling(({ id, title, folder, account }) => {
|
|
418
590
|
// Prefer ID-based move if provided
|
|
419
591
|
if (id) {
|
|
@@ -453,14 +625,21 @@ server.tool("move-note", "Use when: moving one note to a different folder, by id
|
|
|
453
625
|
});
|
|
454
626
|
}, "Error moving note"));
|
|
455
627
|
// --- list-notes ---
|
|
456
|
-
server.
|
|
457
|
-
account:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
.string()
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
628
|
+
server.registerTool("list-notes", {
|
|
629
|
+
description: "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.",
|
|
630
|
+
inputSchema: {
|
|
631
|
+
account: z.string().optional().describe("Account to list notes from"),
|
|
632
|
+
folder: z.string().optional().describe("Filter to specific folder"),
|
|
633
|
+
modifiedSince: z
|
|
634
|
+
.string()
|
|
635
|
+
.optional()
|
|
636
|
+
.describe("ISO 8601 date string to filter notes modified on or after this date (e.g., '2025-01-01'). Useful for listing only recent notes in large collections."),
|
|
637
|
+
limit: z.number().int().positive().optional().describe("Maximum number of notes to return"),
|
|
638
|
+
},
|
|
639
|
+
outputSchema: {
|
|
640
|
+
notes: z.array(z.string()).optional(),
|
|
641
|
+
count: z.number().optional(),
|
|
642
|
+
},
|
|
464
643
|
}, withErrorHandling(({ account, folder, modifiedSince, limit }) => {
|
|
465
644
|
// Use sync-aware wrapper for this read operation
|
|
466
645
|
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("list-notes", () => notesManager.listNotes(account, folder, modifiedSince, limit));
|
|
@@ -488,7 +667,14 @@ server.tool("list-notes", "Use when: enumerating notes in an account or folder;
|
|
|
488
667
|
return successResponse(`Found ${notes.length} notes${location}${acct}${dateInfo}${limitInfo}:\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
489
668
|
}, "Error listing notes"));
|
|
490
669
|
// --- get-selected-notes ---
|
|
491
|
-
server.
|
|
670
|
+
server.registerTool("get-selected-notes", {
|
|
671
|
+
description: "Use when: the user asks what note(s) are currently selected in Notes.app.\nReturns: selected note metadata with ids for follow-up operations.\nDo not use when: searching all notes (search-notes) or listing a folder (list-notes).\nNote: reads Notes.app UI selection; it may be empty if Notes is closed or no note is selected.",
|
|
672
|
+
inputSchema: {},
|
|
673
|
+
outputSchema: {
|
|
674
|
+
notes: z.array(z.object({}).passthrough()).optional(),
|
|
675
|
+
count: z.number().optional(),
|
|
676
|
+
},
|
|
677
|
+
}, withErrorHandling(() => {
|
|
492
678
|
const notes = notesManager.getSelectedNotes();
|
|
493
679
|
if (notes.length === 0) {
|
|
494
680
|
return successResponse("No notes are currently selected in Notes.app", {
|
|
@@ -503,8 +689,15 @@ server.tool("get-selected-notes", "Use when: the user asks what note(s) are curr
|
|
|
503
689
|
// Folder Tools
|
|
504
690
|
// =============================================================================
|
|
505
691
|
// --- list-folders ---
|
|
506
|
-
server.
|
|
507
|
-
account:
|
|
692
|
+
server.registerTool("list-folders", {
|
|
693
|
+
description: "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.",
|
|
694
|
+
inputSchema: {
|
|
695
|
+
account: z.string().optional().describe("Account to list folders from"),
|
|
696
|
+
},
|
|
697
|
+
outputSchema: {
|
|
698
|
+
folders: z.array(z.object({}).passthrough()).optional(),
|
|
699
|
+
count: z.number().optional(),
|
|
700
|
+
},
|
|
508
701
|
}, withErrorHandling(({ account }) => {
|
|
509
702
|
// Use sync-aware wrapper for this read operation
|
|
510
703
|
const { result: folders, syncBefore, syncInterference, } = withSyncAwarenessSync("list-folders", () => notesManager.listFolders(account));
|
|
@@ -528,12 +721,19 @@ server.tool("list-folders", "Use when: listing all folders, with full nested pat
|
|
|
528
721
|
});
|
|
529
722
|
}, "Error listing folders"));
|
|
530
723
|
// --- create-folder ---
|
|
531
|
-
server.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
724
|
+
server.registerTool("create-folder", {
|
|
725
|
+
description: "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).",
|
|
726
|
+
inputSchema: {
|
|
727
|
+
name: z
|
|
728
|
+
.string()
|
|
729
|
+
.min(1, "Folder name is required")
|
|
730
|
+
.describe('Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'),
|
|
731
|
+
account: z.string().optional().describe("Account name (defaults to iCloud)"),
|
|
732
|
+
},
|
|
733
|
+
outputSchema: {
|
|
734
|
+
ok: z.boolean().optional(),
|
|
735
|
+
folder: z.string().optional(),
|
|
736
|
+
},
|
|
537
737
|
}, withErrorHandling(({ name, account }) => {
|
|
538
738
|
const folder = notesManager.createFolder(name, account);
|
|
539
739
|
if (!folder) {
|
|
@@ -545,7 +745,14 @@ server.tool("create-folder", "Use when: creating a folder, including nested path
|
|
|
545
745
|
});
|
|
546
746
|
}, "Error creating folder"));
|
|
547
747
|
// --- delete-folder ---
|
|
548
|
-
server.
|
|
748
|
+
server.registerTool("delete-folder", {
|
|
749
|
+
description: "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.",
|
|
750
|
+
inputSchema: folderNameSchema,
|
|
751
|
+
outputSchema: {
|
|
752
|
+
ok: z.boolean().optional(),
|
|
753
|
+
folder: z.string().optional(),
|
|
754
|
+
},
|
|
755
|
+
}, withErrorHandling(({ name, account }) => {
|
|
549
756
|
const success = notesManager.deleteFolder(name, account);
|
|
550
757
|
if (!success) {
|
|
551
758
|
return errorResponse(`Failed to delete folder "${name}". Folder may not exist or may contain notes.`);
|
|
@@ -556,7 +763,14 @@ server.tool("delete-folder", "Use when: deleting an existing folder by name or n
|
|
|
556
763
|
// Account Tools
|
|
557
764
|
// =============================================================================
|
|
558
765
|
// --- list-accounts ---
|
|
559
|
-
server.
|
|
766
|
+
server.registerTool("list-accounts", {
|
|
767
|
+
description: "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).",
|
|
768
|
+
inputSchema: {},
|
|
769
|
+
outputSchema: {
|
|
770
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
771
|
+
count: z.number().optional(),
|
|
772
|
+
},
|
|
773
|
+
}, withErrorHandling(() => {
|
|
560
774
|
const accounts = notesManager.listAccounts();
|
|
561
775
|
if (accounts.length === 0) {
|
|
562
776
|
return successResponse("No Notes accounts found", { accounts: [], count: 0 });
|
|
@@ -574,7 +788,14 @@ server.tool("list-accounts", "Use when: discovering which Notes accounts exist (
|
|
|
574
788
|
});
|
|
575
789
|
}, "Error listing accounts"));
|
|
576
790
|
// --- get-default-location ---
|
|
577
|
-
server.
|
|
791
|
+
server.registerTool("get-default-location", {
|
|
792
|
+
description: "Use when: discovering where Notes.app will create new notes by default.\nReturns: default account and default folder metadata.\nDo not use when: you already have an explicit account/folder target.",
|
|
793
|
+
inputSchema: {},
|
|
794
|
+
outputSchema: {
|
|
795
|
+
account: z.object({}).passthrough().optional(),
|
|
796
|
+
folder: z.object({}).passthrough().optional(),
|
|
797
|
+
},
|
|
798
|
+
}, withErrorHandling(() => {
|
|
578
799
|
const location = notesManager.getDefaultLocation();
|
|
579
800
|
const message = `Default account: ${location.account.name} [id: ${location.account.id}]\n` +
|
|
580
801
|
`Default folder: ${location.folder.name} [id: ${location.folder.id}]`;
|
|
@@ -584,7 +805,14 @@ server.tool("get-default-location", "Use when: discovering where Notes.app will
|
|
|
584
805
|
// Collaboration Tools
|
|
585
806
|
// =============================================================================
|
|
586
807
|
// --- list-shared-notes ---
|
|
587
|
-
server.
|
|
808
|
+
server.registerTool("list-shared-notes", {
|
|
809
|
+
description: "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.",
|
|
810
|
+
inputSchema: {},
|
|
811
|
+
outputSchema: {
|
|
812
|
+
notes: z.array(z.object({}).passthrough()).optional(),
|
|
813
|
+
count: z.number().optional(),
|
|
814
|
+
},
|
|
815
|
+
}, withErrorHandling(() => {
|
|
588
816
|
const sharedNotes = notesManager.listSharedNotes();
|
|
589
817
|
if (sharedNotes.length === 0) {
|
|
590
818
|
return successResponse("No shared notes found. You have no notes shared with collaborators.", { notes: [], count: 0 });
|
|
@@ -602,7 +830,18 @@ server.tool("list-shared-notes", "Use when: finding notes shared with collaborat
|
|
|
602
830
|
// Diagnostics Tools
|
|
603
831
|
// =============================================================================
|
|
604
832
|
// --- get-sync-status ---
|
|
605
|
-
server.
|
|
833
|
+
server.registerTool("get-sync-status", {
|
|
834
|
+
description: "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.",
|
|
835
|
+
inputSchema: {},
|
|
836
|
+
outputSchema: {
|
|
837
|
+
syncDetected: z.boolean().optional(),
|
|
838
|
+
pendingUpload: z.number().optional(),
|
|
839
|
+
secondsSinceLastChange: z.number().optional(),
|
|
840
|
+
recentActivity: z.boolean().optional(),
|
|
841
|
+
warning: z.string().optional(),
|
|
842
|
+
error: z.string().optional(),
|
|
843
|
+
},
|
|
844
|
+
}, withErrorHandling(() => {
|
|
606
845
|
const status = getSyncStatus();
|
|
607
846
|
if (status.error) {
|
|
608
847
|
return successResponse(`⚠️ Sync status unknown: ${status.error}`, { ...status });
|
|
@@ -628,7 +867,15 @@ server.tool("get-sync-status", "Use when: checking whether iCloud sync is in pro
|
|
|
628
867
|
return successResponse(lines.join("\n"), { ...status });
|
|
629
868
|
}, "Error checking sync status"));
|
|
630
869
|
// --- health-check ---
|
|
631
|
-
server.
|
|
870
|
+
server.registerTool("health-check", {
|
|
871
|
+
description: "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.",
|
|
872
|
+
inputSchema: {},
|
|
873
|
+
outputSchema: {
|
|
874
|
+
healthy: z.boolean().optional(),
|
|
875
|
+
checks: z.array(z.object({}).passthrough()).optional(),
|
|
876
|
+
fullDiskAccess: z.boolean().optional(),
|
|
877
|
+
},
|
|
878
|
+
}, withErrorHandling(() => {
|
|
632
879
|
const result = notesManager.healthCheck();
|
|
633
880
|
const statusIcon = result.healthy ? "✓" : "✗";
|
|
634
881
|
const statusText = result.healthy ? "All checks passed" : "Issues detected";
|
|
@@ -650,14 +897,30 @@ server.tool("health-check", "Use when: a quick check that Notes.app is reachable
|
|
|
650
897
|
});
|
|
651
898
|
}, "Error running health check"));
|
|
652
899
|
// --- doctor ---
|
|
653
|
-
server.
|
|
900
|
+
server.registerTool("doctor", {
|
|
901
|
+
description: "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.",
|
|
902
|
+
inputSchema: {},
|
|
903
|
+
outputSchema: {
|
|
904
|
+
healthy: z.boolean().optional(),
|
|
905
|
+
checks: z.array(z.object({}).passthrough()).optional(),
|
|
906
|
+
},
|
|
907
|
+
}, withErrorHandling(() => {
|
|
654
908
|
// Richer than health-check: Notes.app permission, account state, and Full
|
|
655
909
|
// Disk Access with actionable messages + structuredContent (#22).
|
|
656
910
|
const report = runDoctor(notesManager);
|
|
657
911
|
return successResponse(formatDoctorReport(report), { ...report });
|
|
658
912
|
}, "Error running doctor"));
|
|
659
913
|
// --- get-notes-stats ---
|
|
660
|
-
server.
|
|
914
|
+
server.registerTool("get-notes-stats", {
|
|
915
|
+
description: "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.",
|
|
916
|
+
inputSchema: {},
|
|
917
|
+
outputSchema: {
|
|
918
|
+
totalNotes: z.number().optional(),
|
|
919
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
920
|
+
recentlyModified: z.object({}).passthrough().optional(),
|
|
921
|
+
coverage: z.object({}).passthrough().optional(),
|
|
922
|
+
},
|
|
923
|
+
}, withErrorHandling(() => {
|
|
661
924
|
const stats = notesManager.getNotesStats();
|
|
662
925
|
// Format the output
|
|
663
926
|
const lines = [];
|
|
@@ -693,13 +956,20 @@ server.tool("get-notes-stats", "Use when: summarizing the library — total note
|
|
|
693
956
|
return successResponse(lines.join("\n"), { ...stats });
|
|
694
957
|
}, "Error getting notes statistics"));
|
|
695
958
|
// --- list-attachments ---
|
|
696
|
-
server.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
.string()
|
|
701
|
-
|
|
702
|
-
|
|
959
|
+
server.registerTool("list-attachments", {
|
|
960
|
+
description: "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).",
|
|
961
|
+
inputSchema: {
|
|
962
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
963
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
964
|
+
account: z
|
|
965
|
+
.string()
|
|
966
|
+
.optional()
|
|
967
|
+
.describe("Account containing the note (ignored if id is provided)"),
|
|
968
|
+
},
|
|
969
|
+
outputSchema: {
|
|
970
|
+
attachments: z.array(z.object({}).passthrough()).optional(),
|
|
971
|
+
count: z.number().optional(),
|
|
972
|
+
},
|
|
703
973
|
}, withErrorHandling(({ id, title, account }) => {
|
|
704
974
|
// Prefer ID-based lookup if provided
|
|
705
975
|
if (id) {
|
|
@@ -733,8 +1003,17 @@ server.tool("list-attachments", "Use when: listing the attachments of one note,
|
|
|
733
1003
|
return successResponse(`Found ${attachments.length} attachment(s) in "${title}":\n${attachmentList}`, { attachments, count: attachments.length });
|
|
734
1004
|
}, "Error listing attachments"));
|
|
735
1005
|
// --- batch-delete-notes ---
|
|
736
|
-
server.
|
|
737
|
-
|
|
1006
|
+
server.registerTool("batch-delete-notes", {
|
|
1007
|
+
description: "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.",
|
|
1008
|
+
inputSchema: {
|
|
1009
|
+
ids: z.array(z.string()).describe("Array of note IDs to delete"),
|
|
1010
|
+
},
|
|
1011
|
+
outputSchema: {
|
|
1012
|
+
ok: z.boolean().optional(),
|
|
1013
|
+
succeeded: z.number().optional(),
|
|
1014
|
+
failed: z.number().optional(),
|
|
1015
|
+
results: z.array(z.object({}).passthrough()).optional(),
|
|
1016
|
+
},
|
|
738
1017
|
}, withErrorHandling(({ ids }) => {
|
|
739
1018
|
if (ids.length === 0) {
|
|
740
1019
|
return errorResponse("No note IDs provided");
|
|
@@ -759,13 +1038,23 @@ server.tool("batch-delete-notes", "Use when: permanently deleting multiple notes
|
|
|
759
1038
|
: errorResponse(lines.join("\n"));
|
|
760
1039
|
}, "Error performing batch delete"));
|
|
761
1040
|
// --- batch-move-notes ---
|
|
762
|
-
server.
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
.string()
|
|
767
|
-
|
|
768
|
-
|
|
1041
|
+
server.registerTool("batch-move-notes", {
|
|
1042
|
+
description: "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).",
|
|
1043
|
+
inputSchema: {
|
|
1044
|
+
ids: z.array(z.string()).describe("Array of note IDs to move"),
|
|
1045
|
+
folder: z.string().describe("Destination folder name"),
|
|
1046
|
+
account: z
|
|
1047
|
+
.string()
|
|
1048
|
+
.optional()
|
|
1049
|
+
.describe("Account containing the destination folder (defaults to iCloud)"),
|
|
1050
|
+
},
|
|
1051
|
+
outputSchema: {
|
|
1052
|
+
ok: z.boolean().optional(),
|
|
1053
|
+
folder: z.string().optional(),
|
|
1054
|
+
succeeded: z.number().optional(),
|
|
1055
|
+
failed: z.number().optional(),
|
|
1056
|
+
results: z.array(z.object({}).passthrough()).optional(),
|
|
1057
|
+
},
|
|
769
1058
|
}, withErrorHandling(({ ids, folder, account }) => {
|
|
770
1059
|
if (ids.length === 0) {
|
|
771
1060
|
return errorResponse("No note IDs provided");
|
|
@@ -791,16 +1080,27 @@ server.tool("batch-move-notes", "Use when: moving multiple notes by id into one
|
|
|
791
1080
|
: errorResponse(lines.join("\n"));
|
|
792
1081
|
}, "Error performing batch move"));
|
|
793
1082
|
// --- save-attachment ---
|
|
794
|
-
server.
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1083
|
+
server.registerTool("save-attachment", {
|
|
1084
|
+
description: "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.",
|
|
1085
|
+
inputSchema: {
|
|
1086
|
+
noteId: z
|
|
1087
|
+
.string()
|
|
1088
|
+
.min(1, "noteId is required")
|
|
1089
|
+
.describe("CoreData note id (from search/list)"),
|
|
1090
|
+
attachmentId: z
|
|
1091
|
+
.string()
|
|
1092
|
+
.min(1, "attachmentId is required")
|
|
1093
|
+
.describe("Attachment id (from list-attachments)"),
|
|
1094
|
+
savePath: z
|
|
1095
|
+
.string()
|
|
1096
|
+
.min(1, "savePath is required")
|
|
1097
|
+
.describe("Absolute destination file path (must be under home, temp, or /Volumes)"),
|
|
1098
|
+
},
|
|
1099
|
+
outputSchema: {
|
|
1100
|
+
savedPath: z.string().optional(),
|
|
1101
|
+
name: z.string().optional(),
|
|
1102
|
+
contentType: z.string().optional(),
|
|
1103
|
+
},
|
|
804
1104
|
}, withErrorHandling(({ noteId, attachmentId, savePath }) => {
|
|
805
1105
|
const r = notesManager.saveAttachmentById(noteId, attachmentId, savePath);
|
|
806
1106
|
if (!r.success) {
|
|
@@ -813,12 +1113,24 @@ server.tool("save-attachment", "Use when: writing one note attachment to a file
|
|
|
813
1113
|
});
|
|
814
1114
|
}, "Error saving attachment"));
|
|
815
1115
|
// --- fetch-attachment ---
|
|
816
|
-
server.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1116
|
+
server.registerTool("fetch-attachment", {
|
|
1117
|
+
description: "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.",
|
|
1118
|
+
inputSchema: {
|
|
1119
|
+
noteId: z
|
|
1120
|
+
.string()
|
|
1121
|
+
.min(1, "noteId is required")
|
|
1122
|
+
.describe("CoreData note id (from search/list)"),
|
|
1123
|
+
attachmentId: z
|
|
1124
|
+
.string()
|
|
1125
|
+
.min(1, "attachmentId is required")
|
|
1126
|
+
.describe("Attachment id (from list-attachments)"),
|
|
1127
|
+
},
|
|
1128
|
+
outputSchema: {
|
|
1129
|
+
name: z.string().optional(),
|
|
1130
|
+
contentType: z.string().optional(),
|
|
1131
|
+
bytes: z.number().optional(),
|
|
1132
|
+
base64: z.string().optional(),
|
|
1133
|
+
},
|
|
822
1134
|
}, withErrorHandling(({ noteId, attachmentId }) => {
|
|
823
1135
|
const r = notesManager.getAttachmentBase64ById(noteId, attachmentId);
|
|
824
1136
|
if (!r.success || !r.base64) {
|
|
@@ -826,8 +1138,50 @@ server.tool("fetch-attachment", "Use when: retrieving one note attachment's byte
|
|
|
826
1138
|
}
|
|
827
1139
|
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 });
|
|
828
1140
|
}, "Error fetching attachment"));
|
|
1141
|
+
// --- show-attachment ---
|
|
1142
|
+
server.registerTool("show-attachment", {
|
|
1143
|
+
description: "Use when: the user wants to reveal one note attachment in Notes.app.\nReturns: confirmation that Notes.app revealed the attachment.\nDo not use when: you want the bytes (fetch-attachment) or a file on disk (save-attachment).\nNote: this opens or focuses the Notes UI. Get the ids from list-attachments first.",
|
|
1144
|
+
inputSchema: {
|
|
1145
|
+
noteId: z
|
|
1146
|
+
.string()
|
|
1147
|
+
.min(1, "noteId is required")
|
|
1148
|
+
.describe("CoreData note id (from search/list)"),
|
|
1149
|
+
attachmentId: z
|
|
1150
|
+
.string()
|
|
1151
|
+
.min(1, "attachmentId is required")
|
|
1152
|
+
.describe("Attachment id (from list-attachments)"),
|
|
1153
|
+
separately: z
|
|
1154
|
+
.boolean()
|
|
1155
|
+
.optional()
|
|
1156
|
+
.describe("Open in a separate window when supported by Notes.app"),
|
|
1157
|
+
},
|
|
1158
|
+
outputSchema: {
|
|
1159
|
+
noteId: z.string().optional(),
|
|
1160
|
+
attachmentId: z.string().optional(),
|
|
1161
|
+
separately: z.boolean().optional(),
|
|
1162
|
+
},
|
|
1163
|
+
}, withErrorHandling(({ noteId, attachmentId, separately = false }) => {
|
|
1164
|
+
const success = notesManager.showAttachmentById(noteId, attachmentId, separately);
|
|
1165
|
+
if (!success) {
|
|
1166
|
+
return errorResponse(`Failed to show attachment "${attachmentId}" on note "${noteId}"`);
|
|
1167
|
+
}
|
|
1168
|
+
return successResponse(`Shown attachment "${attachmentId}" in Notes.app`, {
|
|
1169
|
+
noteId,
|
|
1170
|
+
attachmentId,
|
|
1171
|
+
separately,
|
|
1172
|
+
});
|
|
1173
|
+
}, "Error showing attachment"));
|
|
829
1174
|
// --- export-notes-json ---
|
|
830
|
-
server.
|
|
1175
|
+
server.registerTool("export-notes-json", {
|
|
1176
|
+
description: "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.",
|
|
1177
|
+
inputSchema: {},
|
|
1178
|
+
outputSchema: {
|
|
1179
|
+
exportDate: z.string().optional(),
|
|
1180
|
+
version: z.string().optional(),
|
|
1181
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
1182
|
+
summary: z.object({}).passthrough().optional(),
|
|
1183
|
+
},
|
|
1184
|
+
}, withErrorHandling(() => {
|
|
831
1185
|
const exportData = notesManager.exportNotesAsJson();
|
|
832
1186
|
const { summary } = exportData;
|
|
833
1187
|
return {
|
|
@@ -845,13 +1199,19 @@ server.tool("export-notes-json", "Use when: exporting the entire notes library a
|
|
|
845
1199
|
};
|
|
846
1200
|
}, "Error exporting notes"));
|
|
847
1201
|
// --- get-note-markdown ---
|
|
848
|
-
server.
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
.string()
|
|
853
|
-
|
|
854
|
-
|
|
1202
|
+
server.registerTool("get-note-markdown", {
|
|
1203
|
+
description: "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.",
|
|
1204
|
+
inputSchema: {
|
|
1205
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
1206
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
1207
|
+
account: z
|
|
1208
|
+
.string()
|
|
1209
|
+
.optional()
|
|
1210
|
+
.describe("Account containing the note (ignored if id is provided)"),
|
|
1211
|
+
},
|
|
1212
|
+
outputSchema: {
|
|
1213
|
+
markdown: z.string().optional(),
|
|
1214
|
+
},
|
|
855
1215
|
}, withErrorHandling(({ id, title, account }) => {
|
|
856
1216
|
// Prefer ID-based lookup if provided
|
|
857
1217
|
if (id) {
|
|
@@ -872,8 +1232,16 @@ server.tool("get-note-markdown", "Use when: reading a note as Markdown, with che
|
|
|
872
1232
|
return successResponse(markdown, { markdown });
|
|
873
1233
|
}, "Error getting note as markdown"));
|
|
874
1234
|
// --- get-checklist-state ---
|
|
875
|
-
server.
|
|
876
|
-
|
|
1235
|
+
server.registerTool("get-checklist-state", {
|
|
1236
|
+
description: "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.",
|
|
1237
|
+
inputSchema: {
|
|
1238
|
+
id: z.string().min(1, "Note ID is required. Use search-notes to find the note ID first."),
|
|
1239
|
+
},
|
|
1240
|
+
outputSchema: {
|
|
1241
|
+
items: z.array(z.object({}).passthrough()).optional(),
|
|
1242
|
+
checked: z.number().optional(),
|
|
1243
|
+
total: z.number().optional(),
|
|
1244
|
+
},
|
|
877
1245
|
}, withErrorHandling(({ id }) => {
|
|
878
1246
|
// Verify the note exists and is accessible
|
|
879
1247
|
const note = notesManager.getNoteById(id);
|