mcp-icloud 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Local Notes Client
3
+ * Accesses Notes.app via AppleScript
4
+ */
5
+
6
+ const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt } = require('../utils/applescript');
7
+
8
+ /**
9
+ * List note folders
10
+ * @returns {Promise<Array>} - List of folders
11
+ */
12
+ async function listNoteFolders() {
13
+ const script = `
14
+ const notes = Application('Notes');
15
+ const folders = notes.folders();
16
+ let result = [];
17
+
18
+ for (let folder of folders) {
19
+ try {
20
+ result.push({
21
+ id: folder.id(),
22
+ name: folder.name(),
23
+ noteCount: folder.notes().length
24
+ });
25
+ } catch (e) {}
26
+ }
27
+
28
+ JSON.stringify(result);
29
+ `;
30
+
31
+ const result = await runJXA(script);
32
+ return result ? JSON.parse(result) : [];
33
+ }
34
+
35
+ /**
36
+ * List notes
37
+ * @param {string} folderName - Folder name (optional)
38
+ * @param {number} count - Max notes to return
39
+ * @returns {Promise<Array>} - List of notes
40
+ */
41
+ async function listNotes(folderName = null, count = 25) {
42
+ const script = `
43
+ const notes = Application('Notes');
44
+ let allNotes = [];
45
+
46
+ ${folderName ? `
47
+ const folder = notes.folders.byName("${escapeJXA(folderName)}");
48
+ const notesList = folder.notes();
49
+ for (let note of notesList) {
50
+ allNotes.push({
51
+ id: note.id(),
52
+ name: note.name(),
53
+ creationDate: note.creationDate().toISOString(),
54
+ modificationDate: note.modificationDate().toISOString(),
55
+ folder: "${escapeJXA(folderName)}"
56
+ });
57
+ }
58
+ ` : `
59
+ const folders = notes.folders();
60
+ for (let folder of folders) {
61
+ try {
62
+ const notesList = folder.notes();
63
+ for (let note of notesList) {
64
+ allNotes.push({
65
+ id: note.id(),
66
+ name: note.name(),
67
+ creationDate: note.creationDate().toISOString(),
68
+ modificationDate: note.modificationDate().toISOString(),
69
+ folder: folder.name()
70
+ });
71
+ }
72
+ } catch (e) {}
73
+ }
74
+ `}
75
+
76
+ // Sort by modification date (newest first)
77
+ allNotes.sort((a, b) => new Date(b.modificationDate) - new Date(a.modificationDate));
78
+ JSON.stringify(allNotes.slice(0, ${asInt(count, 25)}));
79
+ `;
80
+
81
+ const result = await runJXA(script);
82
+ return result ? JSON.parse(result) : [];
83
+ }
84
+
85
+ /**
86
+ * Read a note's content
87
+ * @param {string} noteId - Note ID
88
+ * @returns {Promise<Object>} - Note content
89
+ */
90
+ async function readNote(noteId) {
91
+ const script = `
92
+ const notes = Application('Notes');
93
+ const folders = notes.folders();
94
+
95
+ for (let folder of folders) {
96
+ try {
97
+ const notesList = folder.notes();
98
+ for (let note of notesList) {
99
+ if (note.id() === "${escapeJXA(noteId)}") {
100
+ // Get plain text by stripping HTML
101
+ const body = note.body();
102
+ const plaintext = note.plaintext();
103
+
104
+ return JSON.stringify({
105
+ id: note.id(),
106
+ name: note.name(),
107
+ body: body,
108
+ plaintext: plaintext,
109
+ creationDate: note.creationDate().toISOString(),
110
+ modificationDate: note.modificationDate().toISOString(),
111
+ folder: folder.name()
112
+ });
113
+ }
114
+ }
115
+ } catch (e) {}
116
+ }
117
+
118
+ return JSON.stringify(null);
119
+ `;
120
+
121
+ const result = await runJXA(script);
122
+ return result ? JSON.parse(result) : null;
123
+ }
124
+
125
+ /**
126
+ * Create a new note
127
+ * @param {Object} options - Note options
128
+ * @returns {Promise<Object>} - Created note info
129
+ */
130
+ async function createNote({ title, body, folderName = 'Notes' }) {
131
+ // Notes uses an HTML body. Split on real newlines BEFORE escaping: escaping
132
+ // turns a newline into the two characters \ and n, so a replace afterwards
133
+ // would find nothing and the note would render with literal \n in it.
134
+ const htmlLines = String(body || '').split('\n').map(escapeAppleScript).join('<br>');
135
+ const htmlBody = `<h1>${escapeAppleScript(title)}</h1><br>${htmlLines}`;
136
+
137
+ const script = `
138
+ tell application "Notes"
139
+ tell folder "${escapeAppleScript(folderName)}"
140
+ set newNote to make new note with properties {body:"${htmlBody}"}
141
+ return id of newNote
142
+ end tell
143
+ end tell
144
+ `;
145
+
146
+ const id = await runAppleScript(script);
147
+ return { success: true, id, message: 'Note created successfully' };
148
+ }
149
+
150
+ /**
151
+ * Search notes
152
+ * @param {string} query - Search query
153
+ * @param {number} count - Max results
154
+ * @returns {Promise<Array>} - Matching notes
155
+ */
156
+ async function searchNotes(query, count = 25) {
157
+ const searchTerm = escapeJXA(query.toLowerCase());
158
+ const limit = asInt(count, 25);
159
+
160
+ const script = `
161
+ const notes = Application('Notes');
162
+ const folders = notes.folders();
163
+ let results = [];
164
+
165
+ for (let folder of folders) {
166
+ if (results.length >= ${limit}) break;
167
+
168
+ try {
169
+ const notesList = folder.notes();
170
+ for (let note of notesList) {
171
+ if (results.length >= ${limit}) break;
172
+
173
+ const name = (note.name() || '').toLowerCase();
174
+ const plaintext = (note.plaintext() || '').toLowerCase();
175
+
176
+ if (name.includes("${searchTerm}") || plaintext.includes("${searchTerm}")) {
177
+ results.push({
178
+ id: note.id(),
179
+ name: note.name(),
180
+ creationDate: note.creationDate().toISOString(),
181
+ modificationDate: note.modificationDate().toISOString(),
182
+ folder: folder.name(),
183
+ snippet: note.plaintext().substring(0, 200)
184
+ });
185
+ }
186
+ }
187
+ } catch (e) {}
188
+ }
189
+
190
+ JSON.stringify(results);
191
+ `;
192
+
193
+ const result = await runJXA(script);
194
+ return result ? JSON.parse(result) : [];
195
+ }
196
+
197
+ module.exports = {
198
+ listNoteFolders,
199
+ listNotes,
200
+ readNote,
201
+ createNote,
202
+ searchNotes
203
+ };
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "mcp-icloud",
3
+ "mcpName": "io.github.MrGo2/icloud-mcp",
4
+ "version": "2.1.1",
5
+ "description": "MCP server for Apple services - Email, Calendar, Contacts, Reminders, Notes, Messages and Safari - with dual local (AppleScript) and cloud (IMAP/CalDAV/CardDAV) modes",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "icloud-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "mode.js",
13
+ "config.js",
14
+ "auth/",
15
+ "email/",
16
+ "calendar/",
17
+ "contacts/",
18
+ "reminders/",
19
+ "notes/",
20
+ "messages/",
21
+ "safari/",
22
+ "utils/",
23
+ "LICENSE",
24
+ "README.md",
25
+ "DISCLOSURE"
26
+ ],
27
+ "scripts": {
28
+ "start": "node index.js",
29
+ "test": "node test/security.test.js && node test/local-output.test.js",
30
+ "test-mode": "USE_TEST_MODE=true node index.js",
31
+ "inspect": "npx @modelcontextprotocol/inspector node index.js",
32
+ "test:local": "node test/local-output.test.js"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/MrGo2/icloud-mcp.git"
37
+ },
38
+ "homepage": "https://github.com/MrGo2/icloud-mcp#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/MrGo2/icloud-mcp/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "keywords": [
46
+ "mcp",
47
+ "mcp-server",
48
+ "model-context-protocol",
49
+ "icloud",
50
+ "apple",
51
+ "applescript",
52
+ "imap",
53
+ "caldav",
54
+ "carddav",
55
+ "claude",
56
+ "reminders",
57
+ "notes",
58
+ "imessage",
59
+ "safari"
60
+ ],
61
+ "author": "Carlos Lorenzo",
62
+ "license": "MIT",
63
+ "dependencies": {
64
+ "@modelcontextprotocol/server": "2.0.0",
65
+ "dotenv": "^16.4.5",
66
+ "ical.js": "^2.1.0",
67
+ "imap": "^0.8.19",
68
+ "mailparser": "^3.7.1",
69
+ "nodemailer": "^9.0.1",
70
+ "tsdav": "^2.1.1",
71
+ "zod": "^4.4.3"
72
+ },
73
+ "contentPolicy": {
74
+ "class": "dual-use"
75
+ }
76
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Reminders Module
3
+ * MCP tool definitions for Reminders.app
4
+ * NOTE: Only available in LOCAL mode (AppleScript)
5
+ */
6
+
7
+ const { z } = require('zod');
8
+ const localClient = require('./local-client');
9
+ const { formatError } = require('../utils/error-handler');
10
+ const { listOutput, listResult } = require('../utils/schemas');
11
+ const { isLocalMode } = require('../mode');
12
+
13
+ /**
14
+ * Check if we're in local mode, return error if not
15
+ */
16
+ function requireLocalMode(toolName) {
17
+ if (!isLocalMode()) {
18
+ return formatError(new Error(`${toolName} is only available in LOCAL mode. Use set-mode to switch.`));
19
+ }
20
+ return null;
21
+ }
22
+
23
+ const remindersTools = [
24
+ {
25
+ name: 'list-reminder-lists',
26
+ outputSchema: listOutput('list-reminder-lists'),
27
+ title: 'List Reminder Lists',
28
+ description: 'Lists all reminder lists from Reminders.app (LOCAL mode only)',
29
+ inputSchema: {},
30
+ annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
31
+ handler: async () => {
32
+ const modeError = requireLocalMode('list-reminder-lists');
33
+ if (modeError) return modeError;
34
+
35
+ try {
36
+ const lists = await localClient.listReminderLists();
37
+ return {
38
+ content: [{
39
+ type: 'text',
40
+ text: JSON.stringify(lists, null, 2)
41
+ }],
42
+ structuredContent: listResult(lists)
43
+ };
44
+ } catch (error) {
45
+ return formatError(error, 'list-reminder-lists');
46
+ }
47
+ }
48
+ },
49
+ {
50
+ name: 'list-reminders',
51
+ outputSchema: listOutput('list-reminders'),
52
+ title: 'List Reminders',
53
+ description: 'Lists reminders from a specific list or all lists (LOCAL mode only)',
54
+ inputSchema: {
55
+ listName: z.string().optional().describe('Name of the reminder list (optional, lists all if not provided)'),
56
+ includeCompleted: z.boolean().optional().describe('Include completed reminders (default: false)'),
57
+ count: z.number().int().min(1).max(200).optional().describe('Maximum number of reminders to return (default: 50)')
58
+ },
59
+ annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
60
+ handler: async ({ listName, includeCompleted = false, count = 50 }) => {
61
+ const modeError = requireLocalMode('list-reminders');
62
+ if (modeError) return modeError;
63
+
64
+ try {
65
+ const reminders = await localClient.listReminders(listName, includeCompleted, count);
66
+ return {
67
+ content: [{
68
+ type: 'text',
69
+ text: JSON.stringify(reminders, null, 2)
70
+ }],
71
+ structuredContent: listResult(reminders)
72
+ };
73
+ } catch (error) {
74
+ return formatError(error, 'list-reminders');
75
+ }
76
+ }
77
+ },
78
+ {
79
+ name: 'create-reminder',
80
+ title: 'Create Reminder',
81
+ description: 'Creates a new reminder (LOCAL mode only)',
82
+ inputSchema: {
83
+ name: z.string().describe('Reminder title (required)'),
84
+ body: z.string().optional().describe('Reminder notes/description'),
85
+ dueDate: z.string().optional().describe('Due date in ISO format (e.g., 2026-01-15T10:00:00)'),
86
+ listName: z.string().optional().describe('Name of the list to add to (default: Reminders)'),
87
+ priority: z.number().int().min(0).max(9).optional().describe('Priority level (0=none, 1=high, 5=medium, 9=low)')
88
+ },
89
+ annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false},
90
+ handler: async (args) => {
91
+ const modeError = requireLocalMode('create-reminder');
92
+ if (modeError) return modeError;
93
+
94
+ try {
95
+ const result = await localClient.createReminder(args);
96
+ return {
97
+ content: [{
98
+ type: 'text',
99
+ text: JSON.stringify(result, null, 2)
100
+ }]
101
+ };
102
+ } catch (error) {
103
+ return formatError(error, 'create-reminder');
104
+ }
105
+ }
106
+ },
107
+ {
108
+ name: 'update-reminder',
109
+ title: 'Update Reminder',
110
+ description: 'Updates an existing reminder (LOCAL mode only)',
111
+ inputSchema: {
112
+ reminderId: z.string().describe('ID of the reminder to update (required)'),
113
+ name: z.string().optional().describe('New title'),
114
+ body: z.string().optional().describe('New notes/description'),
115
+ dueDate: z.string().optional().describe('New due date in ISO format'),
116
+ priority: z.number().int().min(0).max(9).optional().describe('New priority level')
117
+ },
118
+ annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
119
+ handler: async ({ reminderId, ...updates }) => {
120
+ const modeError = requireLocalMode('update-reminder');
121
+ if (modeError) return modeError;
122
+
123
+ try {
124
+ const result = await localClient.updateReminder(reminderId, updates);
125
+ return {
126
+ content: [{
127
+ type: 'text',
128
+ text: JSON.stringify(result, null, 2)
129
+ }]
130
+ };
131
+ } catch (error) {
132
+ return formatError(error, 'update-reminder');
133
+ }
134
+ }
135
+ },
136
+ {
137
+ name: 'complete-reminder',
138
+ title: 'Complete Reminder',
139
+ description: 'Marks a reminder as complete or incomplete (LOCAL mode only)',
140
+ inputSchema: {
141
+ reminderId: z.string().describe('ID of the reminder (required)'),
142
+ completed: z.boolean().optional().describe('Set to true to complete, false to uncomplete (default: true)')
143
+ },
144
+ annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
145
+ handler: async ({ reminderId, completed = true }) => {
146
+ const modeError = requireLocalMode('complete-reminder');
147
+ if (modeError) return modeError;
148
+
149
+ try {
150
+ const result = await localClient.completeReminder(reminderId, completed);
151
+ return {
152
+ content: [{
153
+ type: 'text',
154
+ text: JSON.stringify(result, null, 2)
155
+ }]
156
+ };
157
+ } catch (error) {
158
+ return formatError(error, 'complete-reminder');
159
+ }
160
+ }
161
+ },
162
+ {
163
+ name: 'delete-reminder',
164
+ title: 'Delete Reminder',
165
+ description: 'Deletes a reminder (LOCAL mode only)',
166
+ inputSchema: {
167
+ reminderId: z.string().describe('ID of the reminder to delete (required)')
168
+ },
169
+ annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},
170
+ handler: async ({ reminderId }) => {
171
+ const modeError = requireLocalMode('delete-reminder');
172
+ if (modeError) return modeError;
173
+
174
+ try {
175
+ const result = await localClient.deleteReminder(reminderId);
176
+ return {
177
+ content: [{
178
+ type: 'text',
179
+ text: JSON.stringify(result, null, 2)
180
+ }]
181
+ };
182
+ } catch (error) {
183
+ return formatError(error, 'delete-reminder');
184
+ }
185
+ }
186
+ },
187
+ {
188
+ name: 'search-reminders',
189
+ title: 'Search Reminders',
190
+ description: 'Search reminders by text in name or body (LOCAL mode only)',
191
+ inputSchema: {
192
+ query: z.string().describe('Search text (required)'),
193
+ count: z.number().int().min(1).max(200).optional().describe('Maximum results to return (default: 25)')
194
+ },
195
+ annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
196
+ handler: async ({ query, count = 25 }) => {
197
+ const modeError = requireLocalMode('search-reminders');
198
+ if (modeError) return modeError;
199
+
200
+ try {
201
+ const results = await localClient.searchReminders(query, count);
202
+ return {
203
+ content: [{
204
+ type: 'text',
205
+ text: JSON.stringify(results, null, 2)
206
+ }]
207
+ };
208
+ } catch (error) {
209
+ return formatError(error, 'search-reminders');
210
+ }
211
+ }
212
+ }
213
+ ];
214
+
215
+ module.exports = { remindersTools };