apple-notes-mcp 2.1.4 → 2.3.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 +4 -1
- package/build/index.js +451 -151
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,10 @@ The Codex plugin runs the published `apple-notes-mcp` server through `npx` and s
|
|
|
62
62
|
|
|
63
63
|
### Other Hosts (Hermes, Antigravity)
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Configuration for two more hosts is included — each registers the same `apple-notes` MCP server (`npx -y apple-notes-mcp`):
|
|
66
|
+
|
|
67
|
+
- **[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).
|
|
68
|
+
- **[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).
|
|
66
69
|
|
|
67
70
|
### Manual Installation
|
|
68
71
|
|
package/build/index.js
CHANGED
|
@@ -108,42 +108,65 @@ 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) {
|
|
131
141
|
return errorResponse(`Failed to create note "${title}". Check that Notes.app is configured and accessible.`);
|
|
132
142
|
}
|
|
133
143
|
const checklistWarning = detectChecklistAttempt(content) ?? "";
|
|
134
|
-
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}
|
|
144
|
+
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`, {
|
|
145
|
+
ok: true,
|
|
146
|
+
id: note.id,
|
|
147
|
+
title: note.title,
|
|
148
|
+
folder,
|
|
149
|
+
account,
|
|
150
|
+
});
|
|
135
151
|
}, "Error creating note"));
|
|
136
152
|
// --- search-notes ---
|
|
137
|
-
server.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.string()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
},
|
|
147
170
|
}, withErrorHandling(({ query, searchContent = false, account, folder, modifiedSince, limit }) => {
|
|
148
171
|
// Use sync-aware wrapper for this read operation
|
|
149
172
|
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("search-notes", () => notesManager.searchNotes(query, searchContent, account, folder, modifiedSince, limit));
|
|
@@ -179,13 +202,21 @@ server.tool("search-notes", "Use when: finding notes by a keyword in the title (
|
|
|
179
202
|
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
180
203
|
}, "Error searching notes"));
|
|
181
204
|
// --- get-note-content ---
|
|
182
|
-
server.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.string()
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
},
|
|
189
220
|
}, withErrorHandling(({ id, title, account }) => {
|
|
190
221
|
// Prefer ID-based lookup if provided
|
|
191
222
|
if (id) {
|
|
@@ -224,8 +255,19 @@ server.tool("get-note-content", "Use when: reading the full body text of one kno
|
|
|
224
255
|
return successResponse(content, { title, content, hashtags });
|
|
225
256
|
}, "Error retrieving note content"));
|
|
226
257
|
// --- get-note-by-id ---
|
|
227
|
-
server.
|
|
228
|
-
id:
|
|
258
|
+
server.registerTool("get-note-by-id", {
|
|
259
|
+
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).",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
id: z.string().min(1, "Note ID is required"),
|
|
262
|
+
},
|
|
263
|
+
outputSchema: {
|
|
264
|
+
id: z.string().optional(),
|
|
265
|
+
title: z.string().optional(),
|
|
266
|
+
created: z.string().optional(),
|
|
267
|
+
modified: z.string().optional(),
|
|
268
|
+
shared: z.boolean().optional(),
|
|
269
|
+
passwordProtected: z.boolean().optional(),
|
|
270
|
+
},
|
|
229
271
|
}, withErrorHandling(({ id }) => {
|
|
230
272
|
const note = notesManager.getNoteById(id);
|
|
231
273
|
if (!note) {
|
|
@@ -243,7 +285,19 @@ server.tool("get-note-by-id", "Use when: you have a note id and need its metadat
|
|
|
243
285
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
244
286
|
}, "Error retrieving note"));
|
|
245
287
|
// --- get-note-details ---
|
|
246
|
-
server.
|
|
288
|
+
server.registerTool("get-note-details", {
|
|
289
|
+
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.",
|
|
290
|
+
inputSchema: noteTitleSchema,
|
|
291
|
+
outputSchema: {
|
|
292
|
+
id: z.string().optional(),
|
|
293
|
+
title: z.string().optional(),
|
|
294
|
+
created: z.string().optional(),
|
|
295
|
+
modified: z.string().optional(),
|
|
296
|
+
shared: z.boolean().optional(),
|
|
297
|
+
passwordProtected: z.boolean().optional(),
|
|
298
|
+
account: z.string().optional(),
|
|
299
|
+
},
|
|
300
|
+
}, withErrorHandling(({ title, account }) => {
|
|
247
301
|
const note = notesManager.getNoteDetails(title, account);
|
|
248
302
|
if (!note) {
|
|
249
303
|
return errorResponse(`Note "${title}" not found`);
|
|
@@ -261,12 +315,19 @@ server.tool("get-note-details", "Use when: you have a note title (not an id) and
|
|
|
261
315
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
262
316
|
}, "Error retrieving note details"));
|
|
263
317
|
// --- show-note ---
|
|
264
|
-
server.
|
|
265
|
-
id:
|
|
266
|
-
|
|
267
|
-
.
|
|
268
|
-
|
|
269
|
-
|
|
318
|
+
server.registerTool("show-note", {
|
|
319
|
+
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.",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
id: z.string().min(1, "Note ID is required"),
|
|
322
|
+
separately: z
|
|
323
|
+
.boolean()
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("Open in a separate note window when supported by Notes.app"),
|
|
326
|
+
},
|
|
327
|
+
outputSchema: {
|
|
328
|
+
id: z.string().optional(),
|
|
329
|
+
separately: z.boolean().optional(),
|
|
330
|
+
},
|
|
270
331
|
}, withErrorHandling(({ id, separately = false }) => {
|
|
271
332
|
const success = notesManager.showNoteById(id, separately);
|
|
272
333
|
if (!success) {
|
|
@@ -275,23 +336,32 @@ server.tool("show-note", "Use when: the user wants to reveal a known note in Not
|
|
|
275
336
|
return successResponse(`Shown note with ID "${id}" in Notes.app`, { id, separately });
|
|
276
337
|
}, "Error showing note"));
|
|
277
338
|
// --- update-note ---
|
|
278
|
-
server.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
.string()
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
339
|
+
server.registerTool("update-note", {
|
|
340
|
+
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. Edits to shared notes are immediately visible to all collaborators.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
343
|
+
title: z.string().optional().describe("Current note title (use id instead when available)"),
|
|
344
|
+
newTitle: z.string().optional().describe("New title for the note"),
|
|
345
|
+
newContent: z
|
|
346
|
+
.string()
|
|
347
|
+
.min(1, "New content is required")
|
|
348
|
+
.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."),
|
|
349
|
+
format: z
|
|
350
|
+
.enum(["plaintext", "html"])
|
|
351
|
+
.optional()
|
|
352
|
+
.default("plaintext")
|
|
353
|
+
.describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
354
|
+
account: z
|
|
355
|
+
.string()
|
|
356
|
+
.optional()
|
|
357
|
+
.describe("Account containing the note (ignored if id is provided)"),
|
|
358
|
+
},
|
|
359
|
+
outputSchema: {
|
|
360
|
+
ok: z.boolean().optional(),
|
|
361
|
+
id: z.string().optional(),
|
|
362
|
+
title: z.string().optional(),
|
|
363
|
+
shared: z.boolean().optional(),
|
|
364
|
+
},
|
|
295
365
|
}, withErrorHandling(({ id, title, newTitle, newContent, format = "plaintext", account }) => {
|
|
296
366
|
// Prefer ID-based update if provided
|
|
297
367
|
if (id) {
|
|
@@ -313,7 +383,12 @@ server.tool("update-note", "Use when: changing the title and/or replacing the bo
|
|
|
313
383
|
? "\n\n⚠️ This note is shared with collaborators. Your changes will be visible to them."
|
|
314
384
|
: "";
|
|
315
385
|
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
316
|
-
return successResponse(`Note updated: "${displayTitle}"${sharedWarning}${checklistWarning}
|
|
386
|
+
return successResponse(`Note updated: "${displayTitle}"${sharedWarning}${checklistWarning}`, {
|
|
387
|
+
ok: true,
|
|
388
|
+
id,
|
|
389
|
+
title: displayTitle,
|
|
390
|
+
shared: note.shared ?? false,
|
|
391
|
+
});
|
|
317
392
|
}
|
|
318
393
|
// Fall back to title-based update
|
|
319
394
|
if (!title) {
|
|
@@ -337,16 +412,29 @@ server.tool("update-note", "Use when: changing the title and/or replacing the bo
|
|
|
337
412
|
? "\n\n⚠️ This note is shared with collaborators. Your changes will be visible to them."
|
|
338
413
|
: "";
|
|
339
414
|
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
340
|
-
return successResponse(`Note updated: "${finalTitle}"${sharedWarning}${checklistWarning}
|
|
415
|
+
return successResponse(`Note updated: "${finalTitle}"${sharedWarning}${checklistWarning}`, {
|
|
416
|
+
ok: true,
|
|
417
|
+
title: finalTitle,
|
|
418
|
+
shared: note.shared ?? false,
|
|
419
|
+
});
|
|
341
420
|
}, "Error updating note"));
|
|
342
421
|
// --- delete-note ---
|
|
343
|
-
server.
|
|
344
|
-
id:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
.string()
|
|
348
|
-
|
|
349
|
-
|
|
422
|
+
server.registerTool("delete-note", {
|
|
423
|
+
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.",
|
|
424
|
+
inputSchema: {
|
|
425
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
426
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
427
|
+
account: z
|
|
428
|
+
.string()
|
|
429
|
+
.optional()
|
|
430
|
+
.describe("Account name (defaults to iCloud, ignored if id is provided)"),
|
|
431
|
+
},
|
|
432
|
+
outputSchema: {
|
|
433
|
+
ok: z.boolean().optional(),
|
|
434
|
+
id: z.string().optional(),
|
|
435
|
+
title: z.string().optional(),
|
|
436
|
+
wasShared: z.boolean().optional(),
|
|
437
|
+
},
|
|
350
438
|
}, withErrorHandling(({ id, title, account }) => {
|
|
351
439
|
// Prefer ID-based deletion if provided
|
|
352
440
|
if (id) {
|
|
@@ -363,7 +451,12 @@ server.tool("delete-note", "Use when: permanently deleting a single note, by id
|
|
|
363
451
|
const sharedWarning = note.shared
|
|
364
452
|
? "\n\n⚠️ This note was shared with collaborators. They will no longer have access."
|
|
365
453
|
: "";
|
|
366
|
-
return successResponse(`Note deleted: "${note.title}"${sharedWarning}
|
|
454
|
+
return successResponse(`Note deleted: "${note.title}"${sharedWarning}`, {
|
|
455
|
+
ok: true,
|
|
456
|
+
id,
|
|
457
|
+
title: note.title,
|
|
458
|
+
wasShared: note.shared ?? false,
|
|
459
|
+
});
|
|
367
460
|
}
|
|
368
461
|
// Fall back to title-based deletion
|
|
369
462
|
if (!title) {
|
|
@@ -382,14 +475,27 @@ server.tool("delete-note", "Use when: permanently deleting a single note, by id
|
|
|
382
475
|
const sharedWarning = note.shared
|
|
383
476
|
? "\n\n⚠️ This note was shared with collaborators. They will no longer have access."
|
|
384
477
|
: "";
|
|
385
|
-
return successResponse(`Note deleted: "${title}"${sharedWarning}
|
|
478
|
+
return successResponse(`Note deleted: "${title}"${sharedWarning}`, {
|
|
479
|
+
ok: true,
|
|
480
|
+
title,
|
|
481
|
+
wasShared: note.shared ?? false,
|
|
482
|
+
});
|
|
386
483
|
}, "Error deleting note"));
|
|
387
484
|
// --- move-note ---
|
|
388
|
-
server.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
485
|
+
server.registerTool("move-note", {
|
|
486
|
+
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).",
|
|
487
|
+
inputSchema: {
|
|
488
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
489
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
490
|
+
folder: z.string().min(1, "Destination folder is required"),
|
|
491
|
+
account: z.string().optional().describe("Account containing the note/folder"),
|
|
492
|
+
},
|
|
493
|
+
outputSchema: {
|
|
494
|
+
ok: z.boolean().optional(),
|
|
495
|
+
id: z.string().optional(),
|
|
496
|
+
title: z.string().optional(),
|
|
497
|
+
folder: z.string().optional(),
|
|
498
|
+
},
|
|
393
499
|
}, withErrorHandling(({ id, title, folder, account }) => {
|
|
394
500
|
// Prefer ID-based move if provided
|
|
395
501
|
if (id) {
|
|
@@ -402,7 +508,12 @@ server.tool("move-note", "Use when: moving one note to a different folder, by id
|
|
|
402
508
|
if (!success) {
|
|
403
509
|
return errorResponse(`Failed to move note "${note.title}" to folder "${folder}". Folder may not exist.`);
|
|
404
510
|
}
|
|
405
|
-
return successResponse(`Note moved: "${note.title}" -> "${folder}"
|
|
511
|
+
return successResponse(`Note moved: "${note.title}" -> "${folder}"`, {
|
|
512
|
+
ok: true,
|
|
513
|
+
id,
|
|
514
|
+
title: note.title,
|
|
515
|
+
folder,
|
|
516
|
+
});
|
|
406
517
|
}
|
|
407
518
|
// Fall back to title-based move
|
|
408
519
|
if (!title) {
|
|
@@ -417,17 +528,28 @@ server.tool("move-note", "Use when: moving one note to a different folder, by id
|
|
|
417
528
|
if (!success) {
|
|
418
529
|
return errorResponse(`Failed to move note "${title}" to folder "${folder}". Folder may not exist.`);
|
|
419
530
|
}
|
|
420
|
-
return successResponse(`Note moved: "${title}" -> "${folder}"
|
|
531
|
+
return successResponse(`Note moved: "${title}" -> "${folder}"`, {
|
|
532
|
+
ok: true,
|
|
533
|
+
title,
|
|
534
|
+
folder,
|
|
535
|
+
});
|
|
421
536
|
}, "Error moving note"));
|
|
422
537
|
// --- list-notes ---
|
|
423
|
-
server.
|
|
424
|
-
account:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.string()
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
538
|
+
server.registerTool("list-notes", {
|
|
539
|
+
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.",
|
|
540
|
+
inputSchema: {
|
|
541
|
+
account: z.string().optional().describe("Account to list notes from"),
|
|
542
|
+
folder: z.string().optional().describe("Filter to specific folder"),
|
|
543
|
+
modifiedSince: z
|
|
544
|
+
.string()
|
|
545
|
+
.optional()
|
|
546
|
+
.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."),
|
|
547
|
+
limit: z.number().int().positive().optional().describe("Maximum number of notes to return"),
|
|
548
|
+
},
|
|
549
|
+
outputSchema: {
|
|
550
|
+
notes: z.array(z.string()).optional(),
|
|
551
|
+
count: z.number().optional(),
|
|
552
|
+
},
|
|
431
553
|
}, withErrorHandling(({ account, folder, modifiedSince, limit }) => {
|
|
432
554
|
// Use sync-aware wrapper for this read operation
|
|
433
555
|
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("list-notes", () => notesManager.listNotes(account, folder, modifiedSince, limit));
|
|
@@ -455,7 +577,14 @@ server.tool("list-notes", "Use when: enumerating notes in an account or folder;
|
|
|
455
577
|
return successResponse(`Found ${notes.length} notes${location}${acct}${dateInfo}${limitInfo}:\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
456
578
|
}, "Error listing notes"));
|
|
457
579
|
// --- get-selected-notes ---
|
|
458
|
-
server.
|
|
580
|
+
server.registerTool("get-selected-notes", {
|
|
581
|
+
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.",
|
|
582
|
+
inputSchema: {},
|
|
583
|
+
outputSchema: {
|
|
584
|
+
notes: z.array(z.object({}).passthrough()).optional(),
|
|
585
|
+
count: z.number().optional(),
|
|
586
|
+
},
|
|
587
|
+
}, withErrorHandling(() => {
|
|
459
588
|
const notes = notesManager.getSelectedNotes();
|
|
460
589
|
if (notes.length === 0) {
|
|
461
590
|
return successResponse("No notes are currently selected in Notes.app", {
|
|
@@ -470,8 +599,15 @@ server.tool("get-selected-notes", "Use when: the user asks what note(s) are curr
|
|
|
470
599
|
// Folder Tools
|
|
471
600
|
// =============================================================================
|
|
472
601
|
// --- list-folders ---
|
|
473
|
-
server.
|
|
474
|
-
account:
|
|
602
|
+
server.registerTool("list-folders", {
|
|
603
|
+
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.",
|
|
604
|
+
inputSchema: {
|
|
605
|
+
account: z.string().optional().describe("Account to list folders from"),
|
|
606
|
+
},
|
|
607
|
+
outputSchema: {
|
|
608
|
+
folders: z.array(z.object({}).passthrough()).optional(),
|
|
609
|
+
count: z.number().optional(),
|
|
610
|
+
},
|
|
475
611
|
}, withErrorHandling(({ account }) => {
|
|
476
612
|
// Use sync-aware wrapper for this read operation
|
|
477
613
|
const { result: folders, syncBefore, syncInterference, } = withSyncAwarenessSync("list-folders", () => notesManager.listFolders(account));
|
|
@@ -495,32 +631,56 @@ server.tool("list-folders", "Use when: listing all folders, with full nested pat
|
|
|
495
631
|
});
|
|
496
632
|
}, "Error listing folders"));
|
|
497
633
|
// --- create-folder ---
|
|
498
|
-
server.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
634
|
+
server.registerTool("create-folder", {
|
|
635
|
+
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).",
|
|
636
|
+
inputSchema: {
|
|
637
|
+
name: z
|
|
638
|
+
.string()
|
|
639
|
+
.min(1, "Folder name is required")
|
|
640
|
+
.describe('Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'),
|
|
641
|
+
account: z.string().optional().describe("Account name (defaults to iCloud)"),
|
|
642
|
+
},
|
|
643
|
+
outputSchema: {
|
|
644
|
+
ok: z.boolean().optional(),
|
|
645
|
+
folder: z.string().optional(),
|
|
646
|
+
},
|
|
504
647
|
}, withErrorHandling(({ name, account }) => {
|
|
505
648
|
const folder = notesManager.createFolder(name, account);
|
|
506
649
|
if (!folder) {
|
|
507
650
|
return errorResponse(`Failed to create folder "${name}".`);
|
|
508
651
|
}
|
|
509
|
-
return successResponse(`Folder created: "${folder.name}"
|
|
652
|
+
return successResponse(`Folder created: "${folder.name}"`, {
|
|
653
|
+
ok: true,
|
|
654
|
+
folder: folder.name,
|
|
655
|
+
});
|
|
510
656
|
}, "Error creating folder"));
|
|
511
657
|
// --- delete-folder ---
|
|
512
|
-
server.
|
|
658
|
+
server.registerTool("delete-folder", {
|
|
659
|
+
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.",
|
|
660
|
+
inputSchema: folderNameSchema,
|
|
661
|
+
outputSchema: {
|
|
662
|
+
ok: z.boolean().optional(),
|
|
663
|
+
folder: z.string().optional(),
|
|
664
|
+
},
|
|
665
|
+
}, withErrorHandling(({ name, account }) => {
|
|
513
666
|
const success = notesManager.deleteFolder(name, account);
|
|
514
667
|
if (!success) {
|
|
515
668
|
return errorResponse(`Failed to delete folder "${name}". Folder may not exist or may contain notes.`);
|
|
516
669
|
}
|
|
517
|
-
return successResponse(`Folder deleted: "${name}"
|
|
670
|
+
return successResponse(`Folder deleted: "${name}"`, { ok: true, folder: name });
|
|
518
671
|
}, "Error deleting folder"));
|
|
519
672
|
// =============================================================================
|
|
520
673
|
// Account Tools
|
|
521
674
|
// =============================================================================
|
|
522
675
|
// --- list-accounts ---
|
|
523
|
-
server.
|
|
676
|
+
server.registerTool("list-accounts", {
|
|
677
|
+
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).",
|
|
678
|
+
inputSchema: {},
|
|
679
|
+
outputSchema: {
|
|
680
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
681
|
+
count: z.number().optional(),
|
|
682
|
+
},
|
|
683
|
+
}, withErrorHandling(() => {
|
|
524
684
|
const accounts = notesManager.listAccounts();
|
|
525
685
|
if (accounts.length === 0) {
|
|
526
686
|
return successResponse("No Notes accounts found", { accounts: [], count: 0 });
|
|
@@ -538,7 +698,14 @@ server.tool("list-accounts", "Use when: discovering which Notes accounts exist (
|
|
|
538
698
|
});
|
|
539
699
|
}, "Error listing accounts"));
|
|
540
700
|
// --- get-default-location ---
|
|
541
|
-
server.
|
|
701
|
+
server.registerTool("get-default-location", {
|
|
702
|
+
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.",
|
|
703
|
+
inputSchema: {},
|
|
704
|
+
outputSchema: {
|
|
705
|
+
account: z.object({}).passthrough().optional(),
|
|
706
|
+
folder: z.object({}).passthrough().optional(),
|
|
707
|
+
},
|
|
708
|
+
}, withErrorHandling(() => {
|
|
542
709
|
const location = notesManager.getDefaultLocation();
|
|
543
710
|
const message = `Default account: ${location.account.name} [id: ${location.account.id}]\n` +
|
|
544
711
|
`Default folder: ${location.folder.name} [id: ${location.folder.id}]`;
|
|
@@ -548,7 +715,14 @@ server.tool("get-default-location", "Use when: discovering where Notes.app will
|
|
|
548
715
|
// Collaboration Tools
|
|
549
716
|
// =============================================================================
|
|
550
717
|
// --- list-shared-notes ---
|
|
551
|
-
server.
|
|
718
|
+
server.registerTool("list-shared-notes", {
|
|
719
|
+
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.",
|
|
720
|
+
inputSchema: {},
|
|
721
|
+
outputSchema: {
|
|
722
|
+
notes: z.array(z.object({}).passthrough()).optional(),
|
|
723
|
+
count: z.number().optional(),
|
|
724
|
+
},
|
|
725
|
+
}, withErrorHandling(() => {
|
|
552
726
|
const sharedNotes = notesManager.listSharedNotes();
|
|
553
727
|
if (sharedNotes.length === 0) {
|
|
554
728
|
return successResponse("No shared notes found. You have no notes shared with collaborators.", { notes: [], count: 0 });
|
|
@@ -566,7 +740,18 @@ server.tool("list-shared-notes", "Use when: finding notes shared with collaborat
|
|
|
566
740
|
// Diagnostics Tools
|
|
567
741
|
// =============================================================================
|
|
568
742
|
// --- get-sync-status ---
|
|
569
|
-
server.
|
|
743
|
+
server.registerTool("get-sync-status", {
|
|
744
|
+
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.",
|
|
745
|
+
inputSchema: {},
|
|
746
|
+
outputSchema: {
|
|
747
|
+
syncDetected: z.boolean().optional(),
|
|
748
|
+
pendingUpload: z.number().optional(),
|
|
749
|
+
secondsSinceLastChange: z.number().optional(),
|
|
750
|
+
recentActivity: z.boolean().optional(),
|
|
751
|
+
warning: z.string().optional(),
|
|
752
|
+
error: z.string().optional(),
|
|
753
|
+
},
|
|
754
|
+
}, withErrorHandling(() => {
|
|
570
755
|
const status = getSyncStatus();
|
|
571
756
|
if (status.error) {
|
|
572
757
|
return successResponse(`⚠️ Sync status unknown: ${status.error}`, { ...status });
|
|
@@ -592,7 +777,15 @@ server.tool("get-sync-status", "Use when: checking whether iCloud sync is in pro
|
|
|
592
777
|
return successResponse(lines.join("\n"), { ...status });
|
|
593
778
|
}, "Error checking sync status"));
|
|
594
779
|
// --- health-check ---
|
|
595
|
-
server.
|
|
780
|
+
server.registerTool("health-check", {
|
|
781
|
+
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.",
|
|
782
|
+
inputSchema: {},
|
|
783
|
+
outputSchema: {
|
|
784
|
+
healthy: z.boolean().optional(),
|
|
785
|
+
checks: z.array(z.object({}).passthrough()).optional(),
|
|
786
|
+
fullDiskAccess: z.boolean().optional(),
|
|
787
|
+
},
|
|
788
|
+
}, withErrorHandling(() => {
|
|
596
789
|
const result = notesManager.healthCheck();
|
|
597
790
|
const statusIcon = result.healthy ? "✓" : "✗";
|
|
598
791
|
const statusText = result.healthy ? "All checks passed" : "Issues detected";
|
|
@@ -607,17 +800,37 @@ server.tool("health-check", "Use when: a quick check that Notes.app is reachable
|
|
|
607
800
|
const fdaLine = fdaAvailable
|
|
608
801
|
? " ✓ full_disk_access: Granted (checklist features available)"
|
|
609
802
|
: " ⓘ full_disk_access: Not granted (optional — needed for get-checklist-state and checklist annotations in get-note-markdown). Grant in System Settings > Privacy & Security > Full Disk Access.";
|
|
610
|
-
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}\n${fdaLine}
|
|
803
|
+
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}\n${fdaLine}`, {
|
|
804
|
+
healthy: result.healthy,
|
|
805
|
+
checks: result.checks,
|
|
806
|
+
fullDiskAccess: fdaAvailable,
|
|
807
|
+
});
|
|
611
808
|
}, "Error running health check"));
|
|
612
809
|
// --- doctor ---
|
|
613
|
-
server.
|
|
810
|
+
server.registerTool("doctor", {
|
|
811
|
+
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.",
|
|
812
|
+
inputSchema: {},
|
|
813
|
+
outputSchema: {
|
|
814
|
+
healthy: z.boolean().optional(),
|
|
815
|
+
checks: z.array(z.object({}).passthrough()).optional(),
|
|
816
|
+
},
|
|
817
|
+
}, withErrorHandling(() => {
|
|
614
818
|
// Richer than health-check: Notes.app permission, account state, and Full
|
|
615
819
|
// Disk Access with actionable messages + structuredContent (#22).
|
|
616
820
|
const report = runDoctor(notesManager);
|
|
617
821
|
return successResponse(formatDoctorReport(report), { ...report });
|
|
618
822
|
}, "Error running doctor"));
|
|
619
823
|
// --- get-notes-stats ---
|
|
620
|
-
server.
|
|
824
|
+
server.registerTool("get-notes-stats", {
|
|
825
|
+
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.",
|
|
826
|
+
inputSchema: {},
|
|
827
|
+
outputSchema: {
|
|
828
|
+
totalNotes: z.number().optional(),
|
|
829
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
830
|
+
recentlyModified: z.object({}).passthrough().optional(),
|
|
831
|
+
coverage: z.object({}).passthrough().optional(),
|
|
832
|
+
},
|
|
833
|
+
}, withErrorHandling(() => {
|
|
621
834
|
const stats = notesManager.getNotesStats();
|
|
622
835
|
// Format the output
|
|
623
836
|
const lines = [];
|
|
@@ -653,13 +866,20 @@ server.tool("get-notes-stats", "Use when: summarizing the library — total note
|
|
|
653
866
|
return successResponse(lines.join("\n"), { ...stats });
|
|
654
867
|
}, "Error getting notes statistics"));
|
|
655
868
|
// --- list-attachments ---
|
|
656
|
-
server.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
.string()
|
|
661
|
-
|
|
662
|
-
|
|
869
|
+
server.registerTool("list-attachments", {
|
|
870
|
+
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).",
|
|
871
|
+
inputSchema: {
|
|
872
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
873
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
874
|
+
account: z
|
|
875
|
+
.string()
|
|
876
|
+
.optional()
|
|
877
|
+
.describe("Account containing the note (ignored if id is provided)"),
|
|
878
|
+
},
|
|
879
|
+
outputSchema: {
|
|
880
|
+
attachments: z.array(z.object({}).passthrough()).optional(),
|
|
881
|
+
count: z.number().optional(),
|
|
882
|
+
},
|
|
663
883
|
}, withErrorHandling(({ id, title, account }) => {
|
|
664
884
|
// Prefer ID-based lookup if provided
|
|
665
885
|
if (id) {
|
|
@@ -693,8 +913,17 @@ server.tool("list-attachments", "Use when: listing the attachments of one note,
|
|
|
693
913
|
return successResponse(`Found ${attachments.length} attachment(s) in "${title}":\n${attachmentList}`, { attachments, count: attachments.length });
|
|
694
914
|
}, "Error listing attachments"));
|
|
695
915
|
// --- batch-delete-notes ---
|
|
696
|
-
server.
|
|
697
|
-
|
|
916
|
+
server.registerTool("batch-delete-notes", {
|
|
917
|
+
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.",
|
|
918
|
+
inputSchema: {
|
|
919
|
+
ids: z.array(z.string()).describe("Array of note IDs to delete"),
|
|
920
|
+
},
|
|
921
|
+
outputSchema: {
|
|
922
|
+
ok: z.boolean().optional(),
|
|
923
|
+
succeeded: z.number().optional(),
|
|
924
|
+
failed: z.number().optional(),
|
|
925
|
+
results: z.array(z.object({}).passthrough()).optional(),
|
|
926
|
+
},
|
|
698
927
|
}, withErrorHandling(({ ids }) => {
|
|
699
928
|
if (ids.length === 0) {
|
|
700
929
|
return errorResponse("No note IDs provided");
|
|
@@ -709,16 +938,33 @@ server.tool("batch-delete-notes", "Use when: permanently deleting multiple notes
|
|
|
709
938
|
lines.push(` - ${result.id}: ${result.error}`);
|
|
710
939
|
}
|
|
711
940
|
}
|
|
712
|
-
return succeeded > 0
|
|
941
|
+
return succeeded > 0
|
|
942
|
+
? successResponse(lines.join("\n"), {
|
|
943
|
+
ok: failed === 0,
|
|
944
|
+
succeeded,
|
|
945
|
+
failed,
|
|
946
|
+
results,
|
|
947
|
+
})
|
|
948
|
+
: errorResponse(lines.join("\n"));
|
|
713
949
|
}, "Error performing batch delete"));
|
|
714
950
|
// --- batch-move-notes ---
|
|
715
|
-
server.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
.string()
|
|
720
|
-
|
|
721
|
-
|
|
951
|
+
server.registerTool("batch-move-notes", {
|
|
952
|
+
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).",
|
|
953
|
+
inputSchema: {
|
|
954
|
+
ids: z.array(z.string()).describe("Array of note IDs to move"),
|
|
955
|
+
folder: z.string().describe("Destination folder name"),
|
|
956
|
+
account: z
|
|
957
|
+
.string()
|
|
958
|
+
.optional()
|
|
959
|
+
.describe("Account containing the destination folder (defaults to iCloud)"),
|
|
960
|
+
},
|
|
961
|
+
outputSchema: {
|
|
962
|
+
ok: z.boolean().optional(),
|
|
963
|
+
folder: z.string().optional(),
|
|
964
|
+
succeeded: z.number().optional(),
|
|
965
|
+
failed: z.number().optional(),
|
|
966
|
+
results: z.array(z.object({}).passthrough()).optional(),
|
|
967
|
+
},
|
|
722
968
|
}, withErrorHandling(({ ids, folder, account }) => {
|
|
723
969
|
if (ids.length === 0) {
|
|
724
970
|
return errorResponse("No note IDs provided");
|
|
@@ -733,19 +979,38 @@ server.tool("batch-move-notes", "Use when: moving multiple notes by id into one
|
|
|
733
979
|
lines.push(` - ${result.id}: ${result.error}`);
|
|
734
980
|
}
|
|
735
981
|
}
|
|
736
|
-
return succeeded > 0
|
|
982
|
+
return succeeded > 0
|
|
983
|
+
? successResponse(lines.join("\n"), {
|
|
984
|
+
ok: failed === 0,
|
|
985
|
+
folder,
|
|
986
|
+
succeeded,
|
|
987
|
+
failed,
|
|
988
|
+
results,
|
|
989
|
+
})
|
|
990
|
+
: errorResponse(lines.join("\n"));
|
|
737
991
|
}, "Error performing batch move"));
|
|
738
992
|
// --- save-attachment ---
|
|
739
|
-
server.
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
993
|
+
server.registerTool("save-attachment", {
|
|
994
|
+
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.",
|
|
995
|
+
inputSchema: {
|
|
996
|
+
noteId: z
|
|
997
|
+
.string()
|
|
998
|
+
.min(1, "noteId is required")
|
|
999
|
+
.describe("CoreData note id (from search/list)"),
|
|
1000
|
+
attachmentId: z
|
|
1001
|
+
.string()
|
|
1002
|
+
.min(1, "attachmentId is required")
|
|
1003
|
+
.describe("Attachment id (from list-attachments)"),
|
|
1004
|
+
savePath: z
|
|
1005
|
+
.string()
|
|
1006
|
+
.min(1, "savePath is required")
|
|
1007
|
+
.describe("Absolute destination file path (must be under home, temp, or /Volumes)"),
|
|
1008
|
+
},
|
|
1009
|
+
outputSchema: {
|
|
1010
|
+
savedPath: z.string().optional(),
|
|
1011
|
+
name: z.string().optional(),
|
|
1012
|
+
contentType: z.string().optional(),
|
|
1013
|
+
},
|
|
749
1014
|
}, withErrorHandling(({ noteId, attachmentId, savePath }) => {
|
|
750
1015
|
const r = notesManager.saveAttachmentById(noteId, attachmentId, savePath);
|
|
751
1016
|
if (!r.success) {
|
|
@@ -758,12 +1023,24 @@ server.tool("save-attachment", "Use when: writing one note attachment to a file
|
|
|
758
1023
|
});
|
|
759
1024
|
}, "Error saving attachment"));
|
|
760
1025
|
// --- fetch-attachment ---
|
|
761
|
-
server.
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1026
|
+
server.registerTool("fetch-attachment", {
|
|
1027
|
+
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.",
|
|
1028
|
+
inputSchema: {
|
|
1029
|
+
noteId: z
|
|
1030
|
+
.string()
|
|
1031
|
+
.min(1, "noteId is required")
|
|
1032
|
+
.describe("CoreData note id (from search/list)"),
|
|
1033
|
+
attachmentId: z
|
|
1034
|
+
.string()
|
|
1035
|
+
.min(1, "attachmentId is required")
|
|
1036
|
+
.describe("Attachment id (from list-attachments)"),
|
|
1037
|
+
},
|
|
1038
|
+
outputSchema: {
|
|
1039
|
+
name: z.string().optional(),
|
|
1040
|
+
contentType: z.string().optional(),
|
|
1041
|
+
bytes: z.number().optional(),
|
|
1042
|
+
base64: z.string().optional(),
|
|
1043
|
+
},
|
|
767
1044
|
}, withErrorHandling(({ noteId, attachmentId }) => {
|
|
768
1045
|
const r = notesManager.getAttachmentBase64ById(noteId, attachmentId);
|
|
769
1046
|
if (!r.success || !r.base64) {
|
|
@@ -772,7 +1049,16 @@ server.tool("fetch-attachment", "Use when: retrieving one note attachment's byte
|
|
|
772
1049
|
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 });
|
|
773
1050
|
}, "Error fetching attachment"));
|
|
774
1051
|
// --- export-notes-json ---
|
|
775
|
-
server.
|
|
1052
|
+
server.registerTool("export-notes-json", {
|
|
1053
|
+
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.",
|
|
1054
|
+
inputSchema: {},
|
|
1055
|
+
outputSchema: {
|
|
1056
|
+
exportDate: z.string().optional(),
|
|
1057
|
+
version: z.string().optional(),
|
|
1058
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
1059
|
+
summary: z.object({}).passthrough().optional(),
|
|
1060
|
+
},
|
|
1061
|
+
}, withErrorHandling(() => {
|
|
776
1062
|
const exportData = notesManager.exportNotesAsJson();
|
|
777
1063
|
const { summary } = exportData;
|
|
778
1064
|
return {
|
|
@@ -790,13 +1076,19 @@ server.tool("export-notes-json", "Use when: exporting the entire notes library a
|
|
|
790
1076
|
};
|
|
791
1077
|
}, "Error exporting notes"));
|
|
792
1078
|
// --- get-note-markdown ---
|
|
793
|
-
server.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
.string()
|
|
798
|
-
|
|
799
|
-
|
|
1079
|
+
server.registerTool("get-note-markdown", {
|
|
1080
|
+
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.",
|
|
1081
|
+
inputSchema: {
|
|
1082
|
+
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
1083
|
+
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
1084
|
+
account: z
|
|
1085
|
+
.string()
|
|
1086
|
+
.optional()
|
|
1087
|
+
.describe("Account containing the note (ignored if id is provided)"),
|
|
1088
|
+
},
|
|
1089
|
+
outputSchema: {
|
|
1090
|
+
markdown: z.string().optional(),
|
|
1091
|
+
},
|
|
800
1092
|
}, withErrorHandling(({ id, title, account }) => {
|
|
801
1093
|
// Prefer ID-based lookup if provided
|
|
802
1094
|
if (id) {
|
|
@@ -817,8 +1109,16 @@ server.tool("get-note-markdown", "Use when: reading a note as Markdown, with che
|
|
|
817
1109
|
return successResponse(markdown, { markdown });
|
|
818
1110
|
}, "Error getting note as markdown"));
|
|
819
1111
|
// --- get-checklist-state ---
|
|
820
|
-
server.
|
|
821
|
-
|
|
1112
|
+
server.registerTool("get-checklist-state", {
|
|
1113
|
+
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.",
|
|
1114
|
+
inputSchema: {
|
|
1115
|
+
id: z.string().min(1, "Note ID is required. Use search-notes to find the note ID first."),
|
|
1116
|
+
},
|
|
1117
|
+
outputSchema: {
|
|
1118
|
+
items: z.array(z.object({}).passthrough()).optional(),
|
|
1119
|
+
checked: z.number().optional(),
|
|
1120
|
+
total: z.number().optional(),
|
|
1121
|
+
},
|
|
822
1122
|
}, withErrorHandling(({ id }) => {
|
|
823
1123
|
// Verify the note exists and is accessible
|
|
824
1124
|
const note = notesManager.getNoteById(id);
|
package/package.json
CHANGED