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,214 @@
1
+ /**
2
+ * AppleScript Executor Utility
3
+ * Executes AppleScript and JXA scripts via osascript
4
+ */
5
+
6
+ const { spawn } = require('child_process');
7
+
8
+ /**
9
+ * Execute an AppleScript string via stdin (handles multi-line scripts)
10
+ * @param {string} script - The AppleScript code to execute
11
+ * @returns {Promise<string>} - The script output
12
+ */
13
+ async function runAppleScript(script) {
14
+ return new Promise((resolve, reject) => {
15
+ const proc = spawn('osascript', ['-']);
16
+
17
+ let stdout = '';
18
+ let stderr = '';
19
+
20
+ proc.stdout.on('data', (data) => {
21
+ stdout += data.toString();
22
+ });
23
+
24
+ proc.stderr.on('data', (data) => {
25
+ stderr += data.toString();
26
+ });
27
+
28
+ proc.on('close', (code) => {
29
+ if (code === 0) {
30
+ resolve(stdout.trim());
31
+ } else {
32
+ reject(new AppleScriptError(stderr || stdout, script));
33
+ }
34
+ });
35
+
36
+ proc.on('error', (err) => {
37
+ reject(new AppleScriptError(err.message, script));
38
+ });
39
+
40
+ // Write script to stdin
41
+ proc.stdin.write(script);
42
+ proc.stdin.end();
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Execute a JXA (JavaScript for Automation) script via stdin
48
+ * @param {string} script - The JXA code to execute
49
+ * @returns {Promise<string>} - The script output
50
+ */
51
+ async function runJXA(script) {
52
+ return new Promise((resolve, reject) => {
53
+ const proc = spawn('osascript', ['-l', 'JavaScript', '-']);
54
+
55
+ let stdout = '';
56
+ let stderr = '';
57
+
58
+ proc.stdout.on('data', (data) => {
59
+ stdout += data.toString();
60
+ });
61
+
62
+ proc.stderr.on('data', (data) => {
63
+ stderr += data.toString();
64
+ });
65
+
66
+ proc.on('close', (code) => {
67
+ if (code === 0) {
68
+ resolve(stdout.trim());
69
+ } else {
70
+ reject(new AppleScriptError(stderr || stdout, script));
71
+ }
72
+ });
73
+
74
+ proc.on('error', (err) => {
75
+ reject(new AppleScriptError(err.message, script));
76
+ });
77
+
78
+ // Write script to stdin
79
+ proc.stdin.write(script);
80
+ proc.stdin.end();
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Execute JXA and parse JSON result
86
+ * @param {string} script - JXA script that returns JSON.stringify() output
87
+ * @returns {Promise<any>} - Parsed JSON result
88
+ */
89
+ async function runJXAWithJSON(script) {
90
+ const result = await runJXA(script);
91
+ if (!result) return null;
92
+ try {
93
+ return JSON.parse(result);
94
+ } catch (e) {
95
+ return result;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Escape string for use in AppleScript
101
+ * @param {string} str - String to escape
102
+ * @returns {string} - Escaped string
103
+ */
104
+ function escapeAppleScript(str) {
105
+ if (!str) return '';
106
+ return str
107
+ .replace(/\\/g, '\\\\')
108
+ .replace(/"/g, '\\"')
109
+ .replace(/\n/g, '\\n')
110
+ .replace(/\r/g, '\\r')
111
+ .replace(/\t/g, '\\t');
112
+ }
113
+
114
+ /**
115
+ * Escape string for use in JXA
116
+ * @param {string} str - String to escape
117
+ * @returns {string} - Escaped string
118
+ */
119
+ function escapeJXA(str) {
120
+ if (!str) return '';
121
+ return str
122
+ .replace(/\\/g, '\\\\')
123
+ .replace(/"/g, '\\"')
124
+ .replace(/'/g, "\\'")
125
+ .replace(/\n/g, '\\n')
126
+ .replace(/\r/g, '\\r');
127
+ }
128
+
129
+ /**
130
+ * Coerce a value that gets interpolated into script source as a bare number.
131
+ * Anything non-numeric would be injected as raw AppleScript/JXA, so reject it.
132
+ * @param {*} value - Value to coerce
133
+ * @param {number} fallback - Used when value is null/undefined
134
+ * @returns {number} - A finite integer
135
+ */
136
+ function asInt(value, fallback = 0) {
137
+ if (value === undefined || value === null || value === '') return Math.trunc(fallback);
138
+ const n = Number(value);
139
+ if (!Number.isFinite(n)) {
140
+ throw new Error(`Expected a number, got: ${JSON.stringify(value)}`);
141
+ }
142
+ return Math.trunc(n);
143
+ }
144
+
145
+ /**
146
+ * Coerce a value that gets interpolated into script source as a bare boolean.
147
+ * @param {*} value - Value to coerce
148
+ * @param {boolean} fallback - Used when value is null/undefined
149
+ * @returns {boolean}
150
+ */
151
+ function asBool(value, fallback = false) {
152
+ if (value === undefined || value === null || value === '') return fallback;
153
+ if (typeof value === 'boolean') return value;
154
+ if (value === 'true') return true;
155
+ if (value === 'false') return false;
156
+ throw new Error(`Expected a boolean, got: ${JSON.stringify(value)}`);
157
+ }
158
+
159
+ /**
160
+ * Format date for AppleScript
161
+ * @param {Date|string} date - Date to format
162
+ * @returns {string} - AppleScript date string
163
+ */
164
+ function formatAppleScriptDate(date) {
165
+ const d = date instanceof Date ? date : new Date(date);
166
+ // Format: "January 15, 2026 at 10:30:00 AM"
167
+ return d.toLocaleString('en-US', {
168
+ year: 'numeric',
169
+ month: 'long',
170
+ day: 'numeric',
171
+ hour: 'numeric',
172
+ minute: '2-digit',
173
+ second: '2-digit',
174
+ hour12: true
175
+ }).replace(',', ' at');
176
+ }
177
+
178
+ /**
179
+ * Custom error class for AppleScript errors
180
+ */
181
+ class AppleScriptError extends Error {
182
+ constructor(message, script) {
183
+ super(message);
184
+ this.name = 'AppleScriptError';
185
+ this.script = script;
186
+
187
+ // Parse common AppleScript errors
188
+ if (message.includes('not allowed')) {
189
+ this.code = 'PERMISSION_DENIED';
190
+ this.userMessage = 'Permission denied. Please grant access in System Settings > Privacy & Security.';
191
+ } else if (message.includes('Application isn\'t running')) {
192
+ this.code = 'APP_NOT_RUNNING';
193
+ this.userMessage = 'The application is not running.';
194
+ } else if (message.includes('Can\'t get')) {
195
+ this.code = 'NOT_FOUND';
196
+ this.userMessage = 'The requested item was not found.';
197
+ } else {
198
+ this.code = 'SCRIPT_ERROR';
199
+ this.userMessage = message;
200
+ }
201
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ runAppleScript,
206
+ runJXA,
207
+ runJXAWithJSON,
208
+ escapeAppleScript,
209
+ escapeJXA,
210
+ asInt,
211
+ asBool,
212
+ formatAppleScriptDate,
213
+ AppleScriptError
214
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Date formatting utilities
3
+ */
4
+
5
+ const config = require('../config');
6
+
7
+ /**
8
+ * Format date for display (Spanish locale by default)
9
+ */
10
+ function formatDate(date, options = {}) {
11
+ const d = date instanceof Date ? date : new Date(date);
12
+
13
+ const defaultOptions = {
14
+ year: 'numeric',
15
+ month: '2-digit',
16
+ day: '2-digit',
17
+ hour: '2-digit',
18
+ minute: '2-digit',
19
+ timeZone: config.DEFAULTS.TIMEZONE
20
+ };
21
+
22
+ return d.toLocaleString(config.DEFAULTS.DATE_FORMAT, { ...defaultOptions, ...options });
23
+ }
24
+
25
+ /**
26
+ * Format date for iCalendar (YYYYMMDDTHHMMSS format)
27
+ */
28
+ function formatICalDate(date) {
29
+ const d = date instanceof Date ? date : new Date(date);
30
+ return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
31
+ }
32
+
33
+ /**
34
+ * Parse ISO date string
35
+ */
36
+ function parseDate(dateStr) {
37
+ return new Date(dateStr);
38
+ }
39
+
40
+ /**
41
+ * Get date range for calendar queries
42
+ */
43
+ function getDateRange(daysAhead = 30) {
44
+ const start = new Date();
45
+ const end = new Date();
46
+ end.setDate(end.getDate() + daysAhead);
47
+
48
+ return { start, end };
49
+ }
50
+
51
+ /**
52
+ * Format relative date (today, yesterday, etc.)
53
+ */
54
+ function formatRelative(date) {
55
+ const d = date instanceof Date ? date : new Date(date);
56
+ const now = new Date();
57
+ const diff = Math.floor((now - d) / (1000 * 60 * 60 * 24));
58
+
59
+ if (diff === 0) return 'Hoy';
60
+ if (diff === 1) return 'Ayer';
61
+ if (diff < 7) return `Hace ${diff} días`;
62
+
63
+ return formatDate(d, { hour: undefined, minute: undefined });
64
+ }
65
+
66
+ module.exports = {
67
+ formatDate,
68
+ formatICalDate,
69
+ parseDate,
70
+ getDateRange,
71
+ formatRelative
72
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Centralized error handling utilities
3
+ */
4
+
5
+ /**
6
+ * Format error for MCP response
7
+ */
8
+ function formatError(error, context = '') {
9
+ const prefix = context ? `[${context}] ` : '';
10
+
11
+ if (error.message === 'UNAUTHORIZED' || error.message?.includes('authentication')) {
12
+ return {
13
+ content: [{
14
+ type: 'text',
15
+ text: `${prefix}Authentication failed. Please verify your iCloud credentials in .env file.\n\nTo set up:\n1. Go to https://appleid.apple.com\n2. Security → App-Specific Passwords → Generate\n3. Copy the password to ICLOUD_APP_PASSWORD in .env`
16
+ }],
17
+ isError: true
18
+ };
19
+ }
20
+
21
+ return {
22
+ content: [{
23
+ type: 'text',
24
+ text: `${prefix}Error: ${error.message || 'Unknown error occurred'}`
25
+ }],
26
+ isError: true
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Create success response
32
+ *
33
+ * @param {string} text - Human-readable result
34
+ * @param {Object} [structuredContent] - Machine-readable result. Only pass this
35
+ * when the tool declares an outputSchema; the SDK validates one against the
36
+ * other and fails the call if they disagree.
37
+ */
38
+ function formatSuccess(text, structuredContent) {
39
+ const response = {
40
+ content: [{
41
+ type: 'text',
42
+ text: text
43
+ }]
44
+ };
45
+
46
+ if (structuredContent !== undefined) {
47
+ response.structuredContent = structuredContent;
48
+ }
49
+
50
+ return response;
51
+ }
52
+
53
+ /**
54
+ * Wrap handler with error catching
55
+ */
56
+ function withErrorHandler(handler, context) {
57
+ return async (args) => {
58
+ try {
59
+ return await handler(args);
60
+ } catch (error) {
61
+ console.error(`[${context}] Error:`, error.message);
62
+ return formatError(error, context);
63
+ }
64
+ };
65
+ }
66
+
67
+ module.exports = {
68
+ formatError,
69
+ formatSuccess,
70
+ withErrorHandler
71
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared output schemas for tools that return a list.
3
+ *
4
+ * Deliberately permissive: the items come from AppleScript, IMAP, CalDAV and
5
+ * CardDAV, whose exact fields vary by macOS version, account type and server.
6
+ * The SDK fails a call whose structuredContent does not match its outputSchema,
7
+ * so pinning field names here would turn a harmless extra property into a
8
+ * broken tool. The contract is "an array of objects, plus a count".
9
+ */
10
+
11
+ const { z } = require('zod');
12
+
13
+ /**
14
+ * @param {string} description - What the items are, e.g. 'Contacts'
15
+ * @returns {Object} - A zod raw shape for use as a tool outputSchema
16
+ */
17
+ function listOutput(description) {
18
+ return {
19
+ items: z.array(z.looseObject({})).describe(description),
20
+ total: z.number().int().describe('Number of items returned')
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Build the structuredContent payload matching listOutput().
26
+ * @param {Array} items
27
+ */
28
+ function listResult(items) {
29
+ const array = Array.isArray(items) ? items : [];
30
+ return { items: array, total: array.length };
31
+ }
32
+
33
+ module.exports = { listOutput, listResult };