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,269 @@
1
+ /**
2
+ * Local Reminders Client
3
+ * Accesses Reminders.app via AppleScript
4
+ */
5
+
6
+ const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt, asBool, formatAppleScriptDate } = require('../utils/applescript');
7
+
8
+ /**
9
+ * List all reminder lists
10
+ * @returns {Promise<Array>} - List of reminder lists
11
+ */
12
+ async function listReminderLists() {
13
+ // Names and ids only, fetched as two batch calls. Counting reminders per
14
+ // list forces one Apple Event round-trip per reminder over the full
15
+ // history (completed included) and hangs for minutes on a real database.
16
+ const script = `
17
+ const reminders = Application('Reminders');
18
+ const lists = reminders.lists;
19
+ const names = lists.name();
20
+ const ids = lists.id();
21
+ let result = [];
22
+
23
+ for (let i = 0; i < names.length; i++) {
24
+ result.push({ id: ids[i], name: names[i] });
25
+ }
26
+
27
+ JSON.stringify(result);
28
+ `;
29
+
30
+ const result = await runJXA(script);
31
+ return result ? JSON.parse(result) : [];
32
+ }
33
+
34
+ /**
35
+ * List reminders from a list
36
+ * @param {string} listName - List name (optional, defaults to all)
37
+ * @param {boolean} includeCompleted - Include completed reminders
38
+ * @param {number} count - Max reminders to return
39
+ * @returns {Promise<Array>} - List of reminders
40
+ */
41
+ async function listReminders(listName = null, includeCompleted = false, count = 50) {
42
+ const limit = asInt(count, 50);
43
+
44
+ const script = `
45
+ const reminders = Application('Reminders');
46
+ let allReminders = [];
47
+ const includeCompleted = ${asBool(includeCompleted)};
48
+ const targetListName = ${listName ? `"${escapeJXA(listName)}"` : 'null'};
49
+
50
+ const lists = reminders.lists();
51
+ for (let i = 0; i < lists.length; i++) {
52
+ const list = lists[i];
53
+ if (targetListName && list.name() !== targetListName) continue;
54
+
55
+ try {
56
+ const rems = list.reminders();
57
+ for (let j = 0; j < rems.length; j++) {
58
+ const r = rems[j];
59
+ const isCompleted = r.completed();
60
+ if (!includeCompleted && isCompleted) continue;
61
+
62
+ let dueDate = null;
63
+ try { dueDate = r.dueDate() ? r.dueDate().toISOString() : null; } catch(e) {}
64
+
65
+ allReminders.push({
66
+ id: r.id(),
67
+ name: r.name(),
68
+ body: r.body() || '',
69
+ completed: isCompleted,
70
+ dueDate: dueDate,
71
+ priority: r.priority(),
72
+ list: list.name()
73
+ });
74
+ }
75
+ } catch (e) {}
76
+ }
77
+
78
+ // Sort by due date (nulls at end)
79
+ allReminders.sort(function(a, b) {
80
+ if (!a.dueDate && !b.dueDate) return 0;
81
+ if (!a.dueDate) return 1;
82
+ if (!b.dueDate) return -1;
83
+ return new Date(a.dueDate) - new Date(b.dueDate);
84
+ });
85
+
86
+ JSON.stringify(allReminders.slice(0, ${limit}));
87
+ `;
88
+
89
+ const result = await runJXA(script);
90
+ return result ? JSON.parse(result) : [];
91
+ }
92
+
93
+ /**
94
+ * Create a new reminder
95
+ * @param {Object} options - Reminder options
96
+ * @returns {Promise<Object>} - Created reminder info
97
+ */
98
+ async function createReminder({ name, body, dueDate, listName = 'Reminders', priority = 0 }) {
99
+ let properties = [`name:"${escapeAppleScript(name)}"`];
100
+
101
+ if (body) properties.push(`body:"${escapeAppleScript(body)}"`);
102
+ if (priority) properties.push(`priority:${asInt(priority, 0)}`);
103
+
104
+ let script = `
105
+ tell application "Reminders"
106
+ tell list "${escapeAppleScript(listName)}"
107
+ set newReminder to make new reminder with properties {${properties.join(', ')}}
108
+ `;
109
+
110
+ if (dueDate) {
111
+ script += `
112
+ set due date of newReminder to date "${formatAppleScriptDate(new Date(dueDate))}"
113
+ `;
114
+ }
115
+
116
+ script += `
117
+ return id of newReminder
118
+ end tell
119
+ end tell
120
+ `;
121
+
122
+ const id = await runAppleScript(script);
123
+ return { success: true, id, message: 'Reminder created successfully' };
124
+ }
125
+
126
+ /**
127
+ * Update a reminder
128
+ * @param {string} reminderId - Reminder ID
129
+ * @param {Object} updates - Fields to update
130
+ * @returns {Promise<Object>} - Result
131
+ */
132
+ async function updateReminder(reminderId, { name, body, dueDate, priority }) {
133
+ let updateCommands = [];
134
+
135
+ if (name) updateCommands.push(`set name of theReminder to "${escapeAppleScript(name)}"`);
136
+ if (body !== undefined) updateCommands.push(`set body of theReminder to "${escapeAppleScript(body || '')}"`);
137
+ if (priority !== undefined) updateCommands.push(`set priority of theReminder to ${asInt(priority, 0)}`);
138
+ if (dueDate) updateCommands.push(`set due date of theReminder to date "${formatAppleScriptDate(new Date(dueDate))}"`);
139
+ if (dueDate === null) updateCommands.push(`set due date of theReminder to missing value`);
140
+
141
+ const script = `
142
+ tell application "Reminders"
143
+ set allLists to lists
144
+ repeat with theList in allLists
145
+ try
146
+ set theReminder to reminder id "${escapeAppleScript(reminderId)}" of theList
147
+ ${updateCommands.join('\n ')}
148
+ return "updated"
149
+ end try
150
+ end repeat
151
+ return "not found"
152
+ end tell
153
+ `;
154
+
155
+ const result = await runAppleScript(script);
156
+ if (result === 'not found') {
157
+ return { success: false, message: 'Reminder not found' };
158
+ }
159
+ return { success: true, message: 'Reminder updated successfully' };
160
+ }
161
+
162
+ /**
163
+ * Mark a reminder as complete/incomplete
164
+ * @param {string} reminderId - Reminder ID
165
+ * @param {boolean} completed - Completion status
166
+ * @returns {Promise<Object>} - Result
167
+ */
168
+ async function completeReminder(reminderId, completed = true) {
169
+ const script = `
170
+ tell application "Reminders"
171
+ set allLists to lists
172
+ repeat with theList in allLists
173
+ try
174
+ set theReminder to reminder id "${escapeAppleScript(reminderId)}" of theList
175
+ set completed of theReminder to ${asBool(completed, true)}
176
+ return "done"
177
+ end try
178
+ end repeat
179
+ return "not found"
180
+ end tell
181
+ `;
182
+
183
+ const result = await runAppleScript(script);
184
+ if (result === 'not found') {
185
+ return { success: false, message: 'Reminder not found' };
186
+ }
187
+ return { success: true, message: `Reminder marked as ${completed ? 'complete' : 'incomplete'}` };
188
+ }
189
+
190
+ /**
191
+ * Delete a reminder
192
+ * @param {string} reminderId - Reminder ID
193
+ * @returns {Promise<Object>} - Result
194
+ */
195
+ async function deleteReminder(reminderId) {
196
+ const script = `
197
+ tell application "Reminders"
198
+ set allLists to lists
199
+ repeat with theList in allLists
200
+ try
201
+ set theReminder to reminder id "${escapeAppleScript(reminderId)}" of theList
202
+ delete theReminder
203
+ return "deleted"
204
+ end try
205
+ end repeat
206
+ return "not found"
207
+ end tell
208
+ `;
209
+
210
+ const result = await runAppleScript(script);
211
+ if (result === 'not found') {
212
+ return { success: false, message: 'Reminder not found' };
213
+ }
214
+ return { success: true, message: 'Reminder deleted successfully' };
215
+ }
216
+
217
+ /**
218
+ * Search reminders
219
+ * @param {string} query - Search query
220
+ * @param {number} count - Max results
221
+ * @returns {Promise<Array>} - Matching reminders
222
+ */
223
+ async function searchReminders(query, count = 25) {
224
+ const searchTerm = escapeJXA(query.toLowerCase());
225
+ const limit = asInt(count, 25);
226
+
227
+ const script = `
228
+ const reminders = Application('Reminders');
229
+ const lists = reminders.lists();
230
+ let results = [];
231
+
232
+ for (let list of lists) {
233
+ const rems = list.reminders();
234
+ for (let r of rems) {
235
+ if (results.length >= ${limit}) break;
236
+
237
+ const name = (r.name() || '').toLowerCase();
238
+ const body = (r.body() || '').toLowerCase();
239
+
240
+ if (name.includes("${searchTerm}") || body.includes("${searchTerm}")) {
241
+ results.push({
242
+ id: r.id(),
243
+ name: r.name(),
244
+ body: r.body() || '',
245
+ completed: r.completed(),
246
+ dueDate: r.dueDate() ? r.dueDate().toISOString() : null,
247
+ priority: r.priority(),
248
+ list: list.name()
249
+ });
250
+ }
251
+ }
252
+ }
253
+
254
+ JSON.stringify(results);
255
+ `;
256
+
257
+ const result = await runJXA(script);
258
+ return result ? JSON.parse(result) : [];
259
+ }
260
+
261
+ module.exports = {
262
+ listReminderLists,
263
+ listReminders,
264
+ createReminder,
265
+ updateReminder,
266
+ completeReminder,
267
+ deleteReminder,
268
+ searchReminders
269
+ };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Safari Module
3
+ * MCP tool definitions for Safari.app
4
+ */
5
+
6
+ const { z } = require('zod');
7
+ const localClient = require('./local-client');
8
+ const { formatError } = require('../utils/error-handler');
9
+ const { listOutput, listResult } = require('../utils/schemas');
10
+ const { isLocalMode } = require('../mode');
11
+
12
+ function requireLocalMode(toolName) {
13
+ if (!isLocalMode()) {
14
+ return formatError(new Error(`${toolName} is only available in LOCAL mode. Use set-mode to switch.`));
15
+ }
16
+ return null;
17
+ }
18
+
19
+ const safariTools = [
20
+ {
21
+ name: 'list-safari-tabs',
22
+ outputSchema: listOutput('list-safari-tabs'),
23
+ title: 'List Safari Tabs',
24
+ description: 'Lists all open tabs in Safari across all windows',
25
+ inputSchema: {},
26
+ annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
27
+ handler: async () => {
28
+ const modeError = requireLocalMode('list-safari-tabs');
29
+ if (modeError) return modeError;
30
+
31
+ try {
32
+ const tabs = await localClient.listTabs();
33
+ return {
34
+ content: [{
35
+ type: 'text',
36
+ text: JSON.stringify(tabs, null, 2)
37
+ }],
38
+ structuredContent: listResult(tabs)
39
+ };
40
+ } catch (error) {
41
+ return formatError(error, 'list-safari-tabs');
42
+ }
43
+ }
44
+ },
45
+ {
46
+ name: 'get-current-safari-url',
47
+ title: 'Get Current Safari URL',
48
+ description: 'Gets the URL and title of the current/active Safari tab',
49
+ inputSchema: {},
50
+ annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
51
+ handler: async () => {
52
+ const modeError = requireLocalMode('get-current-safari-url');
53
+ if (modeError) return modeError;
54
+
55
+ try {
56
+ const result = await localClient.getCurrentUrl();
57
+ return {
58
+ content: [{
59
+ type: 'text',
60
+ text: JSON.stringify(result, null, 2)
61
+ }]
62
+ };
63
+ } catch (error) {
64
+ return formatError(error, 'get-current-safari-url');
65
+ }
66
+ }
67
+ },
68
+ {
69
+ name: 'open-safari-url',
70
+ title: 'Open URL In Safari',
71
+ description: 'Opens a URL in Safari (new tab or new window)',
72
+ inputSchema: {
73
+ url: z.string().describe('URL to open (required)'),
74
+ inNewWindow: z.boolean().optional().describe('Open in new window instead of new tab (default: false)')
75
+ },
76
+ annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true},
77
+ handler: async ({ url, inNewWindow = false }) => {
78
+ const modeError = requireLocalMode('open-safari-url');
79
+ if (modeError) return modeError;
80
+
81
+ try {
82
+ const result = await localClient.openUrl(url, inNewWindow);
83
+ return {
84
+ content: [{
85
+ type: 'text',
86
+ text: JSON.stringify(result, null, 2)
87
+ }]
88
+ };
89
+ } catch (error) {
90
+ return formatError(error, 'open-safari-url');
91
+ }
92
+ }
93
+ },
94
+ {
95
+ name: 'close-safari-tab',
96
+ title: 'Close Safari Tab',
97
+ description: 'Closes a Safari tab',
98
+ inputSchema: {
99
+ windowIndex: z.number().int().min(0).optional().describe('Window index (0-based, default: 0 for front window)'),
100
+ tabIndex: z.number().int().min(0).optional().describe('Tab index to close (0-based). If omitted, closes the current tab.')
101
+ },
102
+ annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},
103
+ handler: async ({ windowIndex = 0, tabIndex }) => {
104
+ const modeError = requireLocalMode('close-safari-tab');
105
+ if (modeError) return modeError;
106
+
107
+ try {
108
+ const result = await localClient.closeTab(windowIndex, tabIndex);
109
+ return {
110
+ content: [{
111
+ type: 'text',
112
+ text: JSON.stringify(result, null, 2)
113
+ }]
114
+ };
115
+ } catch (error) {
116
+ return formatError(error, 'close-safari-tab');
117
+ }
118
+ }
119
+ }
120
+ ];
121
+
122
+ module.exports = { safariTools };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Local Safari Client
3
+ * Accesses Safari.app via AppleScript
4
+ */
5
+
6
+ const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt } = require('../utils/applescript');
7
+
8
+ /**
9
+ * List all open tabs across all windows
10
+ * @returns {Promise<Array>} - List of tabs
11
+ */
12
+ async function listTabs() {
13
+ const script = `
14
+ const safari = Application('Safari');
15
+ const windows = safari.windows();
16
+ let tabs = [];
17
+
18
+ for (let i = 0; i < windows.length; i++) {
19
+ const win = windows[i];
20
+ try {
21
+ const winTabs = win.tabs();
22
+ for (let j = 0; j < winTabs.length; j++) {
23
+ const tab = winTabs[j];
24
+ tabs.push({
25
+ windowIndex: i,
26
+ tabIndex: j,
27
+ name: tab.name(),
28
+ url: tab.url(),
29
+ isCurrent: win.currentTab().index() === tab.index()
30
+ });
31
+ }
32
+ } catch (e) {}
33
+ }
34
+
35
+ JSON.stringify(tabs);
36
+ `;
37
+
38
+ const result = await runJXA(script);
39
+ return result ? JSON.parse(result) : [];
40
+ }
41
+
42
+ /**
43
+ * Get the current/active tab URL
44
+ * @returns {Promise<Object>} - Current tab info
45
+ */
46
+ async function getCurrentUrl() {
47
+ const script = `
48
+ const safari = Application('Safari');
49
+ const frontWindow = safari.windows[0];
50
+ const currentTab = frontWindow.currentTab();
51
+
52
+ JSON.stringify({
53
+ name: currentTab.name(),
54
+ url: currentTab.url(),
55
+ source: currentTab.source ? currentTab.source().substring(0, 1000) : null
56
+ });
57
+ `;
58
+
59
+ const result = await runJXA(script);
60
+ return result ? JSON.parse(result) : null;
61
+ }
62
+
63
+ /**
64
+ * Open a URL in a new tab
65
+ * @param {string} url - URL to open
66
+ * @param {boolean} inNewWindow - Open in new window instead of new tab
67
+ * @returns {Promise<Object>} - Result
68
+ */
69
+ async function openUrl(url, inNewWindow = false) {
70
+ // Ensure URL has protocol
71
+ let fullUrl = url;
72
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
73
+ fullUrl = 'https://' + url;
74
+ }
75
+
76
+ if (inNewWindow) {
77
+ const script = `
78
+ tell application "Safari"
79
+ make new document with properties {URL:"${escapeAppleScript(fullUrl)}"}
80
+ activate
81
+ end tell
82
+ return "opened"
83
+ `;
84
+ await runAppleScript(script);
85
+ } else {
86
+ const script = `
87
+ tell application "Safari"
88
+ tell front window
89
+ set newTab to make new tab with properties {URL:"${escapeAppleScript(fullUrl)}"}
90
+ set current tab to newTab
91
+ end tell
92
+ activate
93
+ end tell
94
+ return "opened"
95
+ `;
96
+ await runAppleScript(script);
97
+ }
98
+
99
+ return { success: true, message: `Opened ${fullUrl}` };
100
+ }
101
+
102
+ /**
103
+ * Close a tab
104
+ * @param {number} windowIndex - Window index
105
+ * @param {number} tabIndex - Tab index
106
+ * @returns {Promise<Object>} - Result
107
+ */
108
+ async function closeTab(windowIndex = 0, tabIndex = null) {
109
+ if (tabIndex !== null && tabIndex !== undefined) {
110
+ const script = `
111
+ tell application "Safari"
112
+ tell window ${asInt(windowIndex, 0) + 1}
113
+ close tab ${asInt(tabIndex, 0) + 1}
114
+ end tell
115
+ end tell
116
+ return "closed"
117
+ `;
118
+ await runAppleScript(script);
119
+ } else {
120
+ // Close current tab
121
+ const script = `
122
+ tell application "Safari"
123
+ tell front window
124
+ close current tab
125
+ end tell
126
+ end tell
127
+ return "closed"
128
+ `;
129
+ await runAppleScript(script);
130
+ }
131
+
132
+ return { success: true, message: 'Tab closed' };
133
+ }
134
+
135
+ /**
136
+ * Get the reading list items
137
+ * Note: This requires parsing Safari's bookmarks plist
138
+ * @returns {Promise<Array>} - Reading list items
139
+ */
140
+ async function getReadingList() {
141
+ // Reading list is stored in ~/Library/Safari/Bookmarks.plist
142
+ // This requires parsing a binary plist which is complex
143
+ // For now, return a note about this limitation
144
+ return {
145
+ message: 'Reading list access requires parsing Safari bookmarks plist. Use Safari directly for reading list management.',
146
+ items: []
147
+ };
148
+ }
149
+
150
+ module.exports = {
151
+ listTabs,
152
+ getCurrentUrl,
153
+ openUrl,
154
+ closeTab,
155
+ getReadingList
156
+ };