apple-notes-mcp 1.4.3 → 2.0.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 +97 -6
- package/build/index.js +96 -42
- package/build/services/appleNotesManager.js +272 -142
- package/build/services/appleNotesManager.test.js +196 -68
- package/build/services/attachmentSave.test.js +85 -0
- package/build/services/fileConfig.js +51 -0
- package/build/services/fileConfig.test.js +48 -0
- package/build/tools/doctor.js +50 -0
- package/build/tools/doctor.test.js +42 -0
- package/build/tools/resourcesAndPrompts.js +70 -0
- package/build/tools/resourcesAndPrompts.test.js +63 -0
- package/build/utils/applescript.js +47 -3
- package/build/utils/applescript.test.js +29 -1
- package/build/utils/attachmentFs.js +59 -0
- package/build/utils/attachmentFs.test.js +46 -0
- package/build/utils/jxa.js +17 -0
- package/build/utils/jxa.test.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,12 +86,21 @@ On first use, macOS will ask for permission to automate Notes.app. Click "OK" to
|
|
|
86
86
|
| **Folder Management** | Create, list, and delete folders with full hierarchical path support |
|
|
87
87
|
| **Multi-Account** | Work with iCloud, Gmail, Exchange, or any configured account |
|
|
88
88
|
| **Batch Operations** | Delete or move multiple notes at once |
|
|
89
|
-
| **Checklist State** | Read checklist done/undone state directly from the Notes database |
|
|
89
|
+
| **Checklist State** | Read checklist done/undone state directly from the Notes database (requires Full Disk Access) |
|
|
90
90
|
| **Export** | Export all notes as JSON or get individual notes as Markdown |
|
|
91
|
-
| **Attachments** | List attachments
|
|
91
|
+
| **Attachments** | List attachments, save them to disk, or fetch their bytes as base64 |
|
|
92
92
|
| **Sync Awareness** | Detect iCloud sync in progress, warn about incomplete results |
|
|
93
93
|
| **Collaboration** | Detect shared notes, warn before modifying |
|
|
94
|
-
| **Diagnostics** |
|
|
94
|
+
| **Diagnostics** | `health-check` plus a richer `doctor` (reachability, automation permission, accounts, Full Disk Access), sync status, and statistics |
|
|
95
|
+
|
|
96
|
+
Read/list/get tools also return **structured JSON** (`structuredContent`) alongside the text, so agents can consume results without parsing prose.
|
|
97
|
+
|
|
98
|
+
### MCP resources & prompts
|
|
99
|
+
|
|
100
|
+
Resources expose read-only context the client can attach without a tool call:
|
|
101
|
+
`notes://accounts`, `notes://folders`, `notes://stats`, and the
|
|
102
|
+
`notes://note/{id}` template (returns the note as Markdown). Prompts package
|
|
103
|
+
common workflows: `find-note`, `weekly-review`, `new-meeting-note`.
|
|
95
104
|
|
|
96
105
|
---
|
|
97
106
|
|
|
@@ -587,6 +596,33 @@ Lists attachments in a note.
|
|
|
587
596
|
|
|
588
597
|
---
|
|
589
598
|
|
|
599
|
+
#### `save-attachment`
|
|
600
|
+
|
|
601
|
+
Saves a note attachment to disk.
|
|
602
|
+
|
|
603
|
+
| Parameter | Type | Required | Description |
|
|
604
|
+
|-----------|------|----------|-------------|
|
|
605
|
+
| `noteId` | string | Yes | CoreData note ID (from `search-notes`/`list-notes`) |
|
|
606
|
+
| `attachmentId` | string | Yes | Attachment ID (from `list-attachments`) |
|
|
607
|
+
| `savePath` | string | Yes | Absolute destination file path. Must be under your home directory, a temp directory, or `/Volumes` |
|
|
608
|
+
|
|
609
|
+
**Returns:** Confirmation with the saved path, name, and content type (also in `structuredContent`).
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
#### `fetch-attachment`
|
|
614
|
+
|
|
615
|
+
Returns a note attachment's bytes as base64, without writing to disk (the read counterpart to `save-attachment`).
|
|
616
|
+
|
|
617
|
+
| Parameter | Type | Required | Description |
|
|
618
|
+
|-----------|------|----------|-------------|
|
|
619
|
+
| `noteId` | string | Yes | CoreData note ID (from `search-notes`/`list-notes`) |
|
|
620
|
+
| `attachmentId` | string | Yes | Attachment ID (from `list-attachments`) |
|
|
621
|
+
|
|
622
|
+
**Returns:** The attachment name, content type, byte count, and base64 payload in `structuredContent.base64`.
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
590
626
|
### Diagnostics
|
|
591
627
|
|
|
592
628
|
#### `health-check`
|
|
@@ -599,6 +635,16 @@ Verifies Notes.app connectivity and permissions.
|
|
|
599
635
|
|
|
600
636
|
---
|
|
601
637
|
|
|
638
|
+
#### `doctor`
|
|
639
|
+
|
|
640
|
+
Run a full setup diagnostic: Notes.app reachability, the Automation permission, configured accounts, and Full Disk Access — each reported as ok / warn / fail with an actionable message. This is the richer counterpart to `health-check`; reach for it first when something isn't working.
|
|
641
|
+
|
|
642
|
+
**Parameters:** None
|
|
643
|
+
|
|
644
|
+
**Returns:** A per-check report (`structuredContent` carries the raw `{healthy, checks[]}`). The Full Disk Access check tells you whether checklist-state features will work — see [Full Disk Access Setup](docs/FULL-DISK-ACCESS.md).
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
602
648
|
#### `get-notes-stats`
|
|
603
649
|
|
|
604
650
|
Gets comprehensive statistics about your notes.
|
|
@@ -730,10 +776,46 @@ The entrypoint is written as:
|
|
|
730
776
|
|
|
731
777
|
---
|
|
732
778
|
|
|
779
|
+
## Configuration
|
|
780
|
+
|
|
781
|
+
### Environment variables
|
|
782
|
+
|
|
783
|
+
All configuration is optional — the server works out of the box. Override behavior with these variables (set them in your MCP client's `env` block, or via the [config file](#configuration-file-when-the-host-strips-env) below):
|
|
784
|
+
|
|
785
|
+
| Variable | Default | Description |
|
|
786
|
+
|----------|---------|-------------|
|
|
787
|
+
| `APPLE_NOTES_MCP_MAX_BUFFER` | `67108864` (64 MB) | Max bytes captured from a single AppleScript invocation. Raise it if a very large export/list is truncated; lower it to cap memory. |
|
|
788
|
+
| `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
|
|
789
|
+
| `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
|
|
790
|
+
|
|
791
|
+
### Configuration file (when the host strips `env`)
|
|
792
|
+
|
|
793
|
+
Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed
|
|
794
|
+
environment and ignore the `env` block in their server config, so there's no way
|
|
795
|
+
to pass `APPLE_NOTES_MCP_*` settings through it. In that case, put them in a JSON
|
|
796
|
+
file the host doesn't manage — `APPLE_NOTES_MCP_CONFIG_FILE`, or by default
|
|
797
|
+
`~/Library/Application Support/apple-notes-mcp/config.json`:
|
|
798
|
+
|
|
799
|
+
```json
|
|
800
|
+
{
|
|
801
|
+
"APPLE_NOTES_MCP_MAX_BUFFER": "134217728",
|
|
802
|
+
"DEBUG": "1"
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
The server reads it at startup and merges values into the environment **without
|
|
807
|
+
overriding** anything already set there (so an explicit `env` still wins). This
|
|
808
|
+
is the recommended way to configure the server under Claude Desktop. Apple Notes
|
|
809
|
+
MCP stores no secrets, but as a general rule keep only non-secret config here.
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
733
813
|
## Full Disk Access for Checklist Features
|
|
734
814
|
|
|
735
815
|
The `get-checklist-state` tool and checklist annotations in `get-note-markdown` read directly from the Apple Notes SQLite database. This requires **Full Disk Access** for the process running the MCP server.
|
|
736
816
|
|
|
817
|
+
> 📘 **For the full why-and-how walkthrough (which app to grant, verifying with `doctor`, graceful degradation), see the [Full Disk Access Setup Guide](docs/FULL-DISK-ACCESS.md).** The summary below is the quick version.
|
|
818
|
+
|
|
737
819
|
### How to Grant Full Disk Access
|
|
738
820
|
|
|
739
821
|
1. Open **System Settings** (or System Preferences on older macOS)
|
|
@@ -768,13 +850,22 @@ All other tools work normally without Full Disk Access. Only checklist state fea
|
|
|
768
850
|
| Limitation | Reason |
|
|
769
851
|
|------------|--------|
|
|
770
852
|
| macOS only | Apple Notes and AppleScript are macOS-specific |
|
|
771
|
-
|
|
|
772
|
-
| No pinned notes | Pin status is not exposed via AppleScript |
|
|
853
|
+
| Batch ops run per-note | `batch-delete-notes` / `batch-move-notes` apply each note individually rather than as one bulk operation — AppleScript has no bulk equivalent to IMAP's `UID STORE`/`MOVE`. This is deliberate: it preserves per-note success/failure reporting. ([#26](https://github.com/sweetrb/apple-notes-mcp/issues/26)) |
|
|
854
|
+
| No pinned notes | Pin status is not exposed via AppleScript ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)) |
|
|
773
855
|
| Limited rich formatting | Use `format: "html"` on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
|
|
774
856
|
| Title matching | Most operations require exact title matches |
|
|
775
|
-
| Checklist state | Requires Full Disk Access to read done/undone state from the database |
|
|
857
|
+
| Checklist state | Requires [Full Disk Access](docs/FULL-DISK-ACCESS.md) to read done/undone state from the database |
|
|
776
858
|
| Checklist **creation** | Not supported. AppleScript's `body of note` setter strips `<input type="checkbox">` and ignores any checklist-styling CSS class. Apple Notes stores checklist items as a protobuf paragraph style (`style_type=103`) that AppleScript doesn't expose, and the SQLite database is read-only. See [Creating Checklists](#creating-checklists) below for the workaround. |
|
|
777
859
|
|
|
860
|
+
### Roadmap
|
|
861
|
+
|
|
862
|
+
A few capabilities are deliberately deferred to a future release, tracked as open issues:
|
|
863
|
+
|
|
864
|
+
- **Pinned-note support** ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)) — Apple doesn't expose pin status via AppleScript.
|
|
865
|
+
- **Tags / hashtags** ([#29](https://github.com/sweetrb/apple-notes-mcp/issues/29)).
|
|
866
|
+
- **Note links** ([#30](https://github.com/sweetrb/apple-notes-mcp/issues/30)).
|
|
867
|
+
- **Local integration-test suite** ([#31](https://github.com/sweetrb/apple-notes-mcp/issues/31)).
|
|
868
|
+
|
|
778
869
|
### Creating Checklists
|
|
779
870
|
|
|
780
871
|
**There is no programmatic way to create a true Apple Notes checklist via AppleScript** — and therefore no way via this MCP server. This is an Apple limitation, not a bug.
|
package/build/index.js
CHANGED
|
@@ -27,6 +27,12 @@ import { AppleNotesManager } from "./services/appleNotesManager.js";
|
|
|
27
27
|
import { getSyncStatus, withSyncAwarenessSync } from "./utils/syncDetection.js";
|
|
28
28
|
import { getChecklistItems, hasFullDiskAccess } from "./utils/checklistParser.js";
|
|
29
29
|
import { detectChecklistAttempt } from "./utils/contentWarnings.js";
|
|
30
|
+
import { runDoctor, formatDoctorReport } from "./tools/doctor.js";
|
|
31
|
+
import { loadFileConfig } from "./services/fileConfig.js";
|
|
32
|
+
import { registerResourcesAndPrompts } from "./tools/resourcesAndPrompts.js";
|
|
33
|
+
// Load file-based config FIRST (#24) — before anything reads APPLE_NOTES_MCP_*.
|
|
34
|
+
// Lets users configure the server when the host app strips the MCP env block.
|
|
35
|
+
loadFileConfig();
|
|
30
36
|
// Read version from package.json to keep it in sync
|
|
31
37
|
const require = createRequire(import.meta.url);
|
|
32
38
|
const { version } = require("../package.json");
|
|
@@ -46,25 +52,19 @@ const server = new McpServer({
|
|
|
46
52
|
* Handles all AppleScript execution and note operations.
|
|
47
53
|
*/
|
|
48
54
|
const notesManager = new AppleNotesManager();
|
|
49
|
-
// =============================================================================
|
|
50
|
-
// Response Helpers
|
|
51
|
-
// =============================================================================
|
|
52
55
|
/**
|
|
53
|
-
* Creates a successful MCP tool response.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* @returns Formatted MCP response object
|
|
56
|
+
* Creates a successful MCP tool response. Pass `structured` to attach typed JSON
|
|
57
|
+
* (`structuredContent`) alongside the human-readable text so agents can consume
|
|
58
|
+
* results without parsing prose (#21).
|
|
57
59
|
*/
|
|
58
|
-
function successResponse(message) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
function successResponse(message, structured) {
|
|
61
|
+
const res = { content: [{ type: "text", text: message }] };
|
|
62
|
+
if (structured)
|
|
63
|
+
res.structuredContent = structured;
|
|
64
|
+
return res;
|
|
62
65
|
}
|
|
63
66
|
/**
|
|
64
67
|
* Creates an error MCP tool response.
|
|
65
|
-
*
|
|
66
|
-
* @param message - The error message to display
|
|
67
|
-
* @returns Formatted MCP error response object
|
|
68
68
|
*/
|
|
69
69
|
function errorResponse(message) {
|
|
70
70
|
return {
|
|
@@ -74,10 +74,6 @@ function errorResponse(message) {
|
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Wraps a tool handler with consistent error handling.
|
|
77
|
-
*
|
|
78
|
-
* @param handler - The async function to execute
|
|
79
|
-
* @param errorPrefix - Prefix for error messages (e.g., "Error creating note")
|
|
80
|
-
* @returns Wrapped handler with try/catch
|
|
81
77
|
*/
|
|
82
78
|
function withErrorHandling(handler, errorPrefix) {
|
|
83
79
|
return async (params) => {
|
|
@@ -164,7 +160,7 @@ server.tool("search-notes", {
|
|
|
164
160
|
}
|
|
165
161
|
const syncNote = syncWarnings.length > 0 ? `\n\n${syncWarnings.join(" ")}` : "";
|
|
166
162
|
if (notes.length === 0) {
|
|
167
|
-
return successResponse(`No notes found matching "${query}" in ${searchType}${folderInfo}${dateInfo}${syncNote}
|
|
163
|
+
return successResponse(`No notes found matching "${query}" in ${searchType}${folderInfo}${dateInfo}${syncNote}`, { notes: [], count: 0 });
|
|
168
164
|
}
|
|
169
165
|
// Format each note with ID and folder info, highlighting Recently Deleted
|
|
170
166
|
const noteList = notes
|
|
@@ -179,7 +175,7 @@ server.tool("search-notes", {
|
|
|
179
175
|
return ` - ${n.title}${idSuffix}`;
|
|
180
176
|
})
|
|
181
177
|
.join("\n");
|
|
182
|
-
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):\n${noteList}${syncNote}
|
|
178
|
+
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
183
179
|
}, "Error searching notes"));
|
|
184
180
|
// --- get-note-content ---
|
|
185
181
|
server.tool("get-note-content", {
|
|
@@ -204,7 +200,7 @@ server.tool("get-note-content", {
|
|
|
204
200
|
if (!content) {
|
|
205
201
|
return errorResponse(`Failed to read content of note "${note.title}"`);
|
|
206
202
|
}
|
|
207
|
-
return successResponse(content);
|
|
203
|
+
return successResponse(content, { title: note.title, content });
|
|
208
204
|
}
|
|
209
205
|
// Fall back to title-based lookup
|
|
210
206
|
if (!title) {
|
|
@@ -222,7 +218,7 @@ server.tool("get-note-content", {
|
|
|
222
218
|
if (!content) {
|
|
223
219
|
return errorResponse(`Failed to read content of note "${title}"`);
|
|
224
220
|
}
|
|
225
|
-
return successResponse(content);
|
|
221
|
+
return successResponse(content, { title, content });
|
|
226
222
|
}, "Error retrieving note content"));
|
|
227
223
|
// --- get-note-by-id ---
|
|
228
224
|
server.tool("get-note-by-id", {
|
|
@@ -241,7 +237,7 @@ server.tool("get-note-by-id", {
|
|
|
241
237
|
shared: note.shared,
|
|
242
238
|
passwordProtected: note.passwordProtected,
|
|
243
239
|
};
|
|
244
|
-
return successResponse(JSON.stringify(metadata, null, 2));
|
|
240
|
+
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
245
241
|
}, "Error retrieving note"));
|
|
246
242
|
// --- get-note-details ---
|
|
247
243
|
server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, account }) => {
|
|
@@ -259,7 +255,7 @@ server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, acc
|
|
|
259
255
|
passwordProtected: note.passwordProtected,
|
|
260
256
|
account: note.account,
|
|
261
257
|
};
|
|
262
|
-
return successResponse(JSON.stringify(metadata, null, 2));
|
|
258
|
+
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
263
259
|
}, "Error retrieving note details"));
|
|
264
260
|
// --- update-note ---
|
|
265
261
|
server.tool("update-note", {
|
|
@@ -433,10 +429,13 @@ server.tool("list-notes", {
|
|
|
433
429
|
}
|
|
434
430
|
const syncNote = syncWarnings.length > 0 ? `\n\n${syncWarnings.join(" ")}` : "";
|
|
435
431
|
if (notes.length === 0) {
|
|
436
|
-
return successResponse(`No notes found${location}${acct}${dateInfo}${syncNote}
|
|
432
|
+
return successResponse(`No notes found${location}${acct}${dateInfo}${syncNote}`, {
|
|
433
|
+
notes: [],
|
|
434
|
+
count: 0,
|
|
435
|
+
});
|
|
437
436
|
}
|
|
438
437
|
const noteList = notes.map((t) => ` - ${t}`).join("\n");
|
|
439
|
-
return successResponse(`Found ${notes.length} notes${location}${acct}${dateInfo}${limitInfo}:\n${noteList}${syncNote}
|
|
438
|
+
return successResponse(`Found ${notes.length} notes${location}${acct}${dateInfo}${limitInfo}:\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
440
439
|
}, "Error listing notes"));
|
|
441
440
|
// =============================================================================
|
|
442
441
|
// Folder Tools
|
|
@@ -458,10 +457,13 @@ server.tool("list-folders", {
|
|
|
458
457
|
}
|
|
459
458
|
const syncNote = syncWarnings.length > 0 ? `\n\n${syncWarnings.join(" ")}` : "";
|
|
460
459
|
if (folders.length === 0) {
|
|
461
|
-
return successResponse(`No folders found${acct}${syncNote}
|
|
460
|
+
return successResponse(`No folders found${acct}${syncNote}`, { folders: [], count: 0 });
|
|
462
461
|
}
|
|
463
462
|
const folderList = folders.map((f) => ` - ${f.name}`).join("\n");
|
|
464
|
-
return successResponse(`Found ${folders.length} folders${acct}:\n${folderList}${syncNote}
|
|
463
|
+
return successResponse(`Found ${folders.length} folders${acct}:\n${folderList}${syncNote}`, {
|
|
464
|
+
folders,
|
|
465
|
+
count: folders.length,
|
|
466
|
+
});
|
|
465
467
|
}, "Error listing folders"));
|
|
466
468
|
// --- create-folder ---
|
|
467
469
|
server.tool("create-folder", {
|
|
@@ -492,10 +494,13 @@ server.tool("delete-folder", folderNameSchema, withErrorHandling(({ name, accoun
|
|
|
492
494
|
server.tool("list-accounts", {}, withErrorHandling(() => {
|
|
493
495
|
const accounts = notesManager.listAccounts();
|
|
494
496
|
if (accounts.length === 0) {
|
|
495
|
-
return successResponse("No Notes accounts found");
|
|
497
|
+
return successResponse("No Notes accounts found", { accounts: [], count: 0 });
|
|
496
498
|
}
|
|
497
499
|
const accountList = accounts.map((a) => ` - ${a.name}`).join("\n");
|
|
498
|
-
return successResponse(`Found ${accounts.length} accounts:\n${accountList}
|
|
500
|
+
return successResponse(`Found ${accounts.length} accounts:\n${accountList}`, {
|
|
501
|
+
accounts,
|
|
502
|
+
count: accounts.length,
|
|
503
|
+
});
|
|
499
504
|
}, "Error listing accounts"));
|
|
500
505
|
// =============================================================================
|
|
501
506
|
// Collaboration Tools
|
|
@@ -504,7 +509,7 @@ server.tool("list-accounts", {}, withErrorHandling(() => {
|
|
|
504
509
|
server.tool("list-shared-notes", {}, withErrorHandling(() => {
|
|
505
510
|
const sharedNotes = notesManager.listSharedNotes();
|
|
506
511
|
if (sharedNotes.length === 0) {
|
|
507
|
-
return successResponse("No shared notes found. You have no notes shared with collaborators.");
|
|
512
|
+
return successResponse("No shared notes found. You have no notes shared with collaborators.", { notes: [], count: 0 });
|
|
508
513
|
}
|
|
509
514
|
const noteList = sharedNotes
|
|
510
515
|
.map((n) => {
|
|
@@ -513,7 +518,7 @@ server.tool("list-shared-notes", {}, withErrorHandling(() => {
|
|
|
513
518
|
})
|
|
514
519
|
.join("\n");
|
|
515
520
|
return successResponse(`Found ${sharedNotes.length} shared note(s):\n${noteList}\n\n` +
|
|
516
|
-
`⚠️ Changes to shared notes are visible to all collaborators
|
|
521
|
+
`⚠️ Changes to shared notes are visible to all collaborators.`, { notes: sharedNotes, count: sharedNotes.length });
|
|
517
522
|
}, "Error listing shared notes"));
|
|
518
523
|
// =============================================================================
|
|
519
524
|
// Diagnostics Tools
|
|
@@ -522,7 +527,7 @@ server.tool("list-shared-notes", {}, withErrorHandling(() => {
|
|
|
522
527
|
server.tool("get-sync-status", {}, withErrorHandling(() => {
|
|
523
528
|
const status = getSyncStatus();
|
|
524
529
|
if (status.error) {
|
|
525
|
-
return successResponse(`⚠️ Sync status unknown: ${status.error}
|
|
530
|
+
return successResponse(`⚠️ Sync status unknown: ${status.error}`, { ...status });
|
|
526
531
|
}
|
|
527
532
|
const lines = [];
|
|
528
533
|
if (status.syncDetected) {
|
|
@@ -542,7 +547,7 @@ server.tool("get-sync-status", {}, withErrorHandling(() => {
|
|
|
542
547
|
lines.push("");
|
|
543
548
|
lines.push(` Last activity: ${status.secondsSinceLastChange}s ago`);
|
|
544
549
|
}
|
|
545
|
-
return successResponse(lines.join("\n"));
|
|
550
|
+
return successResponse(lines.join("\n"), { ...status });
|
|
546
551
|
}, "Error checking sync status"));
|
|
547
552
|
// --- health-check ---
|
|
548
553
|
server.tool("health-check", {}, withErrorHandling(() => {
|
|
@@ -562,6 +567,13 @@ server.tool("health-check", {}, withErrorHandling(() => {
|
|
|
562
567
|
: " ⓘ 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.";
|
|
563
568
|
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}\n${fdaLine}`);
|
|
564
569
|
}, "Error running health check"));
|
|
570
|
+
// --- doctor ---
|
|
571
|
+
server.tool("doctor", {}, withErrorHandling(() => {
|
|
572
|
+
// Richer than health-check: Notes.app permission, account state, and Full
|
|
573
|
+
// Disk Access with actionable messages + structuredContent (#22).
|
|
574
|
+
const report = runDoctor(notesManager);
|
|
575
|
+
return successResponse(formatDoctorReport(report), { ...report });
|
|
576
|
+
}, "Error running doctor"));
|
|
565
577
|
// --- get-notes-stats ---
|
|
566
578
|
server.tool("get-notes-stats", {}, withErrorHandling(() => {
|
|
567
579
|
const stats = notesManager.getNotesStats();
|
|
@@ -587,7 +599,7 @@ server.tool("get-notes-stats", {}, withErrorHandling(() => {
|
|
|
587
599
|
lines.push(` Last 24 hours: ${stats.recentlyModified.last24h}`);
|
|
588
600
|
lines.push(` Last 7 days: ${stats.recentlyModified.last7d}`);
|
|
589
601
|
lines.push(` Last 30 days: ${stats.recentlyModified.last30d}`);
|
|
590
|
-
return successResponse(lines.join("\n"));
|
|
602
|
+
return successResponse(lines.join("\n"), { ...stats });
|
|
591
603
|
}, "Error getting notes statistics"));
|
|
592
604
|
// --- list-attachments ---
|
|
593
605
|
server.tool("list-attachments", {
|
|
@@ -606,10 +618,13 @@ server.tool("list-attachments", {
|
|
|
606
618
|
}
|
|
607
619
|
const attachments = notesManager.listAttachmentsById(id);
|
|
608
620
|
if (attachments.length === 0) {
|
|
609
|
-
return successResponse(`Note "${note.title}" has no attachments
|
|
621
|
+
return successResponse(`Note "${note.title}" has no attachments`, {
|
|
622
|
+
attachments: [],
|
|
623
|
+
count: 0,
|
|
624
|
+
});
|
|
610
625
|
}
|
|
611
626
|
const attachmentList = attachments.map((a) => ` - ${a.name} (${a.contentType})`).join("\n");
|
|
612
|
-
return successResponse(`Found ${attachments.length} attachment(s) in "${note.title}":\n${attachmentList}
|
|
627
|
+
return successResponse(`Found ${attachments.length} attachment(s) in "${note.title}":\n${attachmentList}`, { attachments, count: attachments.length });
|
|
613
628
|
}
|
|
614
629
|
// Fall back to title-based lookup
|
|
615
630
|
if (!title) {
|
|
@@ -621,10 +636,10 @@ server.tool("list-attachments", {
|
|
|
621
636
|
}
|
|
622
637
|
const attachments = notesManager.listAttachments(title, account);
|
|
623
638
|
if (attachments.length === 0) {
|
|
624
|
-
return successResponse(`Note "${title}" has no attachments
|
|
639
|
+
return successResponse(`Note "${title}" has no attachments`, { attachments: [], count: 0 });
|
|
625
640
|
}
|
|
626
641
|
const attachmentList = attachments.map((a) => ` - ${a.name} (${a.contentType})`).join("\n");
|
|
627
|
-
return successResponse(`Found ${attachments.length} attachment(s) in "${title}":\n${attachmentList}
|
|
642
|
+
return successResponse(`Found ${attachments.length} attachment(s) in "${title}":\n${attachmentList}`, { attachments, count: attachments.length });
|
|
628
643
|
}, "Error listing attachments"));
|
|
629
644
|
// --- batch-delete-notes ---
|
|
630
645
|
server.tool("batch-delete-notes", {
|
|
@@ -669,6 +684,42 @@ server.tool("batch-move-notes", {
|
|
|
669
684
|
}
|
|
670
685
|
return succeeded > 0 ? successResponse(lines.join("\n")) : errorResponse(lines.join("\n"));
|
|
671
686
|
}, "Error performing batch move"));
|
|
687
|
+
// --- save-attachment ---
|
|
688
|
+
server.tool("save-attachment", {
|
|
689
|
+
noteId: z.string().min(1, "noteId is required").describe("CoreData note id (from search/list)"),
|
|
690
|
+
attachmentId: z
|
|
691
|
+
.string()
|
|
692
|
+
.min(1, "attachmentId is required")
|
|
693
|
+
.describe("Attachment id (from list-attachments)"),
|
|
694
|
+
savePath: z
|
|
695
|
+
.string()
|
|
696
|
+
.min(1, "savePath is required")
|
|
697
|
+
.describe("Absolute destination file path (must be under home, temp, or /Volumes)"),
|
|
698
|
+
}, withErrorHandling(({ noteId, attachmentId, savePath }) => {
|
|
699
|
+
const r = notesManager.saveAttachmentById(noteId, attachmentId, savePath);
|
|
700
|
+
if (!r.success) {
|
|
701
|
+
return errorResponse(`Failed to save attachment: ${r.error ?? "unknown error"}`);
|
|
702
|
+
}
|
|
703
|
+
return successResponse(`Saved "${r.name ?? "attachment"}" to ${r.savedPath}`, {
|
|
704
|
+
savedPath: r.savedPath,
|
|
705
|
+
name: r.name,
|
|
706
|
+
contentType: r.contentType,
|
|
707
|
+
});
|
|
708
|
+
}, "Error saving attachment"));
|
|
709
|
+
// --- fetch-attachment ---
|
|
710
|
+
server.tool("fetch-attachment", {
|
|
711
|
+
noteId: z.string().min(1, "noteId is required").describe("CoreData note id (from search/list)"),
|
|
712
|
+
attachmentId: z
|
|
713
|
+
.string()
|
|
714
|
+
.min(1, "attachmentId is required")
|
|
715
|
+
.describe("Attachment id (from list-attachments)"),
|
|
716
|
+
}, withErrorHandling(({ noteId, attachmentId }) => {
|
|
717
|
+
const r = notesManager.getAttachmentBase64ById(noteId, attachmentId);
|
|
718
|
+
if (!r.success || !r.base64) {
|
|
719
|
+
return errorResponse(`Failed to fetch attachment: ${r.error ?? "unknown error"}`);
|
|
720
|
+
}
|
|
721
|
+
return successResponse(`Fetched "${r.name ?? "attachment"}" (${r.contentType ?? "unknown type"}, ${r.bytes ?? 0} bytes) as base64.`, { name: r.name, contentType: r.contentType, bytes: r.bytes, base64: r.base64 });
|
|
722
|
+
}, "Error fetching attachment"));
|
|
672
723
|
// --- export-notes-json ---
|
|
673
724
|
server.tool("export-notes-json", {}, withErrorHandling(() => {
|
|
674
725
|
const exportData = notesManager.exportNotesAsJson();
|
|
@@ -684,6 +735,7 @@ server.tool("export-notes-json", {}, withErrorHandling(() => {
|
|
|
684
735
|
text: JSON.stringify(exportData, null, 2),
|
|
685
736
|
},
|
|
686
737
|
],
|
|
738
|
+
structuredContent: { ...exportData },
|
|
687
739
|
};
|
|
688
740
|
}, "Error exporting notes"));
|
|
689
741
|
// --- get-note-markdown ---
|
|
@@ -701,7 +753,7 @@ server.tool("get-note-markdown", {
|
|
|
701
753
|
if (!markdown) {
|
|
702
754
|
return errorResponse(`Note with ID "${id}" not found or has no content`);
|
|
703
755
|
}
|
|
704
|
-
return successResponse(markdown);
|
|
756
|
+
return successResponse(markdown, { markdown });
|
|
705
757
|
}
|
|
706
758
|
// Fall back to title-based lookup
|
|
707
759
|
if (!title) {
|
|
@@ -711,7 +763,7 @@ server.tool("get-note-markdown", {
|
|
|
711
763
|
if (!markdown) {
|
|
712
764
|
return errorResponse(`Note "${title}" not found or has no content. Use search-notes to find notes, then use the note's ID for reliable operations.`);
|
|
713
765
|
}
|
|
714
|
-
return successResponse(markdown);
|
|
766
|
+
return successResponse(markdown, { markdown });
|
|
715
767
|
}, "Error getting note as markdown"));
|
|
716
768
|
// --- get-checklist-state ---
|
|
717
769
|
server.tool("get-checklist-state", {
|
|
@@ -733,7 +785,7 @@ server.tool("get-checklist-state", {
|
|
|
733
785
|
.map((item) => `${item.done ? "[x]" : "[ ]"} ${item.text}`)
|
|
734
786
|
.join("\n");
|
|
735
787
|
const checked = result.items.filter((i) => i.done).length;
|
|
736
|
-
return successResponse(`Checklist for "${note.title}" (${checked}/${result.items.length} done):\n${summary}
|
|
788
|
+
return successResponse(`Checklist for "${note.title}" (${checked}/${result.items.length} done):\n${summary}`, { items: result.items, checked, total: result.items.length });
|
|
737
789
|
}, "Error reading checklist state"));
|
|
738
790
|
// =============================================================================
|
|
739
791
|
// Server Startup
|
|
@@ -744,5 +796,7 @@ server.tool("get-checklist-state", {
|
|
|
744
796
|
* The server uses stdio transport for communication with MCP clients.
|
|
745
797
|
* This is the standard transport for CLI-based MCP servers.
|
|
746
798
|
*/
|
|
799
|
+
// Register read-only resources and workflow prompts (#23).
|
|
800
|
+
registerResourcesAndPrompts(server, notesManager);
|
|
747
801
|
const transport = new StdioServerTransport();
|
|
748
802
|
await server.connect(transport);
|