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,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 };
|