apple-notes-mcp 1.1.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/build/index.js ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Apple Notes MCP Server
3
+ *
4
+ * A Model Context Protocol (MCP) server that provides AI assistants
5
+ * with the ability to interact with Apple Notes on macOS.
6
+ *
7
+ * This server exposes tools for:
8
+ * - Creating, reading, updating, and deleting notes
9
+ * - Organizing notes into folders
10
+ * - Searching notes by title or content
11
+ * - Managing multiple accounts (iCloud, Gmail, Exchange, etc.)
12
+ *
13
+ * Architecture:
14
+ * - Tool definitions are declarative (schema + handler)
15
+ * - The AppleNotesManager class handles all AppleScript operations
16
+ * - Error handling is consistent across all tools
17
+ *
18
+ * @module apple-notes-mcp
19
+ * @see https://modelcontextprotocol.io
20
+ */
21
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
+ import { z } from "zod";
24
+ import { AppleNotesManager } from "./services/appleNotesManager.js";
25
+ // =============================================================================
26
+ // Server Initialization
27
+ // =============================================================================
28
+ /**
29
+ * MCP server instance configured for Apple Notes operations.
30
+ */
31
+ const server = new McpServer({
32
+ name: "apple-notes",
33
+ version: "1.1.0",
34
+ description: "MCP server for managing Apple Notes - create, search, update, and organize notes",
35
+ });
36
+ /**
37
+ * Singleton instance of the Apple Notes manager.
38
+ * Handles all AppleScript execution and note operations.
39
+ */
40
+ const notesManager = new AppleNotesManager();
41
+ // =============================================================================
42
+ // Response Helpers
43
+ // =============================================================================
44
+ /**
45
+ * Creates a successful MCP tool response.
46
+ *
47
+ * @param message - The success message to display
48
+ * @returns Formatted MCP response object
49
+ */
50
+ function successResponse(message) {
51
+ return {
52
+ content: [{ type: "text", text: message }],
53
+ };
54
+ }
55
+ /**
56
+ * Creates an error MCP tool response.
57
+ *
58
+ * @param message - The error message to display
59
+ * @returns Formatted MCP error response object
60
+ */
61
+ function errorResponse(message) {
62
+ return {
63
+ content: [{ type: "text", text: message }],
64
+ isError: true,
65
+ };
66
+ }
67
+ /**
68
+ * Wraps a tool handler with consistent error handling.
69
+ *
70
+ * @param handler - The async function to execute
71
+ * @param errorPrefix - Prefix for error messages (e.g., "Error creating note")
72
+ * @returns Wrapped handler with try/catch
73
+ */
74
+ function withErrorHandling(handler, errorPrefix) {
75
+ return async (params) => {
76
+ try {
77
+ return handler(params);
78
+ }
79
+ catch (error) {
80
+ const message = error instanceof Error ? error.message : "Unknown error";
81
+ return errorResponse(`${errorPrefix}: ${message}`);
82
+ }
83
+ };
84
+ }
85
+ // =============================================================================
86
+ // Schema Definitions
87
+ // =============================================================================
88
+ /**
89
+ * Common schema for operations requiring a note title.
90
+ */
91
+ const noteTitleSchema = {
92
+ title: z.string().min(1, "Note title is required"),
93
+ account: z.string().optional().describe("Account name (defaults to iCloud)"),
94
+ };
95
+ /**
96
+ * Common schema for operations requiring a folder name.
97
+ */
98
+ const folderNameSchema = {
99
+ name: z.string().min(1, "Folder name is required"),
100
+ account: z.string().optional().describe("Account name (defaults to iCloud)"),
101
+ };
102
+ // =============================================================================
103
+ // Note Tools
104
+ // =============================================================================
105
+ // --- create-note ---
106
+ server.tool("create-note", {
107
+ title: z.string().min(1, "Title is required"),
108
+ content: z.string().min(1, "Content is required"),
109
+ tags: z.array(z.string()).optional().describe("Tags for organization"),
110
+ }, withErrorHandling(({ title, content, tags = [] }) => {
111
+ const note = notesManager.createNote(title, content, tags);
112
+ if (!note) {
113
+ return errorResponse(`Failed to create note "${title}". Check that Notes.app is configured and accessible.`);
114
+ }
115
+ return successResponse(`Note created: "${note.title}"`);
116
+ }, "Error creating note"));
117
+ // --- search-notes ---
118
+ server.tool("search-notes", {
119
+ query: z.string().min(1, "Search query is required"),
120
+ searchContent: z.boolean().optional().describe("Search note content instead of titles"),
121
+ account: z.string().optional().describe("Account to search in"),
122
+ }, withErrorHandling(({ query, searchContent = false, account }) => {
123
+ const notes = notesManager.searchNotes(query, searchContent, account);
124
+ const searchType = searchContent ? "content" : "titles";
125
+ if (notes.length === 0) {
126
+ return successResponse(`No notes found matching "${query}" in ${searchType}`);
127
+ }
128
+ // Format each note with folder info, highlighting Recently Deleted
129
+ const noteList = notes
130
+ .map((n) => {
131
+ if (n.folder === "Recently Deleted") {
132
+ return ` - ${n.title} [DELETED]`;
133
+ }
134
+ else if (n.folder) {
135
+ return ` - ${n.title} (${n.folder})`;
136
+ }
137
+ return ` - ${n.title}`;
138
+ })
139
+ .join("\n");
140
+ return successResponse(`Found ${notes.length} notes (searched ${searchType}):\n${noteList}`);
141
+ }, "Error searching notes"));
142
+ // --- get-note-content ---
143
+ server.tool("get-note-content", noteTitleSchema, withErrorHandling(({ title, account }) => {
144
+ const content = notesManager.getNoteContent(title, account);
145
+ if (!content) {
146
+ return errorResponse(`Note "${title}" not found`);
147
+ }
148
+ return successResponse(content);
149
+ }, "Error retrieving note content"));
150
+ // --- get-note-by-id ---
151
+ server.tool("get-note-by-id", {
152
+ id: z.string().min(1, "Note ID is required"),
153
+ }, withErrorHandling(({ id }) => {
154
+ const note = notesManager.getNoteById(id);
155
+ if (!note) {
156
+ return errorResponse(`Note with ID "${id}" not found`);
157
+ }
158
+ // Return structured metadata as JSON
159
+ const metadata = {
160
+ id: note.id,
161
+ title: note.title,
162
+ created: note.created.toISOString(),
163
+ modified: note.modified.toISOString(),
164
+ shared: note.shared,
165
+ passwordProtected: note.passwordProtected,
166
+ };
167
+ return successResponse(JSON.stringify(metadata, null, 2));
168
+ }, "Error retrieving note"));
169
+ // --- get-note-details ---
170
+ server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, account }) => {
171
+ const note = notesManager.getNoteDetails(title, account);
172
+ if (!note) {
173
+ return errorResponse(`Note "${title}" not found`);
174
+ }
175
+ // Return structured metadata as JSON
176
+ const metadata = {
177
+ id: note.id,
178
+ title: note.title,
179
+ created: note.created.toISOString(),
180
+ modified: note.modified.toISOString(),
181
+ shared: note.shared,
182
+ passwordProtected: note.passwordProtected,
183
+ account: note.account,
184
+ };
185
+ return successResponse(JSON.stringify(metadata, null, 2));
186
+ }, "Error retrieving note details"));
187
+ // --- update-note ---
188
+ server.tool("update-note", {
189
+ title: z.string().min(1, "Current note title is required"),
190
+ newTitle: z.string().optional().describe("New title for the note"),
191
+ newContent: z.string().min(1, "New content is required"),
192
+ account: z.string().optional().describe("Account containing the note"),
193
+ }, withErrorHandling(({ title, newTitle, newContent, account }) => {
194
+ const success = notesManager.updateNote(title, newTitle, newContent, account);
195
+ if (!success) {
196
+ return errorResponse(`Failed to update note "${title}". Note may not exist.`);
197
+ }
198
+ const finalTitle = newTitle || title;
199
+ return successResponse(`Note updated: "${finalTitle}"`);
200
+ }, "Error updating note"));
201
+ // --- delete-note ---
202
+ server.tool("delete-note", noteTitleSchema, withErrorHandling(({ title, account }) => {
203
+ const success = notesManager.deleteNote(title, account);
204
+ if (!success) {
205
+ return errorResponse(`Failed to delete note "${title}". Note may not exist.`);
206
+ }
207
+ return successResponse(`Note deleted: "${title}"`);
208
+ }, "Error deleting note"));
209
+ // --- move-note ---
210
+ server.tool("move-note", {
211
+ title: z.string().min(1, "Note title is required"),
212
+ folder: z.string().min(1, "Destination folder is required"),
213
+ account: z.string().optional().describe("Account containing the note"),
214
+ }, withErrorHandling(({ title, folder, account }) => {
215
+ const success = notesManager.moveNote(title, folder, account);
216
+ if (!success) {
217
+ return errorResponse(`Failed to move note "${title}" to folder "${folder}". Note or folder may not exist.`);
218
+ }
219
+ return successResponse(`Note moved: "${title}" -> "${folder}"`);
220
+ }, "Error moving note"));
221
+ // --- list-notes ---
222
+ server.tool("list-notes", {
223
+ account: z.string().optional().describe("Account to list notes from"),
224
+ folder: z.string().optional().describe("Filter to specific folder"),
225
+ }, withErrorHandling(({ account, folder }) => {
226
+ const notes = notesManager.listNotes(account, folder);
227
+ // Build context string for the response
228
+ const location = folder ? ` in folder "${folder}"` : "";
229
+ const acct = account ? ` (${account})` : "";
230
+ if (notes.length === 0) {
231
+ return successResponse(`No notes found${location}${acct}`);
232
+ }
233
+ const noteList = notes.map((t) => ` - ${t}`).join("\n");
234
+ return successResponse(`Found ${notes.length} notes${location}${acct}:\n${noteList}`);
235
+ }, "Error listing notes"));
236
+ // =============================================================================
237
+ // Folder Tools
238
+ // =============================================================================
239
+ // --- list-folders ---
240
+ server.tool("list-folders", {
241
+ account: z.string().optional().describe("Account to list folders from"),
242
+ }, withErrorHandling(({ account }) => {
243
+ const folders = notesManager.listFolders(account);
244
+ const acct = account ? ` (${account})` : "";
245
+ if (folders.length === 0) {
246
+ return successResponse(`No folders found${acct}`);
247
+ }
248
+ const folderList = folders.map((f) => ` - ${f.name}`).join("\n");
249
+ return successResponse(`Found ${folders.length} folders${acct}:\n${folderList}`);
250
+ }, "Error listing folders"));
251
+ // --- create-folder ---
252
+ server.tool("create-folder", folderNameSchema, withErrorHandling(({ name, account }) => {
253
+ const folder = notesManager.createFolder(name, account);
254
+ if (!folder) {
255
+ return errorResponse(`Failed to create folder "${name}". It may already exist.`);
256
+ }
257
+ return successResponse(`Folder created: "${folder.name}"`);
258
+ }, "Error creating folder"));
259
+ // --- delete-folder ---
260
+ server.tool("delete-folder", folderNameSchema, withErrorHandling(({ name, account }) => {
261
+ const success = notesManager.deleteFolder(name, account);
262
+ if (!success) {
263
+ return errorResponse(`Failed to delete folder "${name}". Folder may not exist or may contain notes.`);
264
+ }
265
+ return successResponse(`Folder deleted: "${name}"`);
266
+ }, "Error deleting folder"));
267
+ // =============================================================================
268
+ // Account Tools
269
+ // =============================================================================
270
+ // --- list-accounts ---
271
+ server.tool("list-accounts", {}, withErrorHandling(() => {
272
+ const accounts = notesManager.listAccounts();
273
+ if (accounts.length === 0) {
274
+ return successResponse("No Notes accounts found");
275
+ }
276
+ const accountList = accounts.map((a) => ` - ${a.name}`).join("\n");
277
+ return successResponse(`Found ${accounts.length} accounts:\n${accountList}`);
278
+ }, "Error listing accounts"));
279
+ // =============================================================================
280
+ // Server Startup
281
+ // =============================================================================
282
+ /**
283
+ * Initialize and start the MCP server.
284
+ *
285
+ * The server uses stdio transport for communication with MCP clients.
286
+ * This is the standard transport for CLI-based MCP servers.
287
+ */
288
+ const transport = new StdioServerTransport();
289
+ await server.connect(transport);