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.
- package/DISCLOSURE +35 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/auth/index.js +175 -0
- package/calendar/caldav-client.js +320 -0
- package/calendar/index.js +252 -0
- package/calendar/local-client.js +223 -0
- package/config.js +69 -0
- package/contacts/carddav-client.js +331 -0
- package/contacts/index.js +350 -0
- package/contacts/local-client.js +372 -0
- package/email/imap-client.js +351 -0
- package/email/index.js +278 -0
- package/email/local-client.js +257 -0
- package/email/smtp-client.js +79 -0
- package/index.js +123 -0
- package/messages/index.js +157 -0
- package/messages/local-client.js +74 -0
- package/mode.js +82 -0
- package/notes/index.js +162 -0
- package/notes/local-client.js +203 -0
- package/package.json +76 -0
- package/reminders/index.js +215 -0
- package/reminders/local-client.js +269 -0
- package/safari/index.js +122 -0
- package/safari/local-client.js +156 -0
- package/utils/applescript.js +214 -0
- package/utils/date-utils.js +72 -0
- package/utils/error-handler.js +71 -0
- package/utils/schemas.js +33 -0
|
@@ -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
|
+
};
|
package/utils/schemas.js
ADDED
|
@@ -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 };
|