apple-reminders-mcp 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # remindctl-mcp
2
+
3
+ MCP server for Apple Reminders via [`remindctl`](https://github.com/nicklama/remindctl) CLI. Say "remind me to deploy on Friday" in Claude Code and your iPhone buzzes.
4
+
5
+ ## Prerequisites
6
+
7
+ - **macOS 14+**
8
+ - **Node.js 18+**
9
+ - **remindctl** installed and authorized:
10
+
11
+ ```bash
12
+ brew install steipete/tap/remindctl
13
+ remindctl authorize
14
+ ```
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ git clone https://github.com/namearth5005/remindctl-mcp.git
20
+ cd remindctl-mcp
21
+ npm install
22
+ npm run build
23
+ ```
24
+
25
+ ## Claude Code Configuration
26
+
27
+ Add to `~/.claude/settings.local.json`:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "apple-reminders": {
33
+ "command": "node",
34
+ "args": ["/absolute/path/to/remindctl-mcp/dist/index.js"],
35
+ "type": "stdio"
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Then restart Claude Code.
42
+
43
+ ## Tools
44
+
45
+ | Tool | Description |
46
+ |------|-------------|
47
+ | `add_reminder` | Add a reminder with optional due date, list, notes, priority |
48
+ | `show_reminders` | Show reminders (today, tomorrow, week, overdue, upcoming, all) |
49
+ | `list_reminder_lists` | List all reminder lists or show contents of a specific list |
50
+ | `edit_reminder` | Edit title, due date, notes, priority, or move to another list |
51
+ | `complete_reminders` | Mark one or more reminders as complete |
52
+ | `delete_reminders` | Permanently delete reminders |
53
+ | `manage_reminder_list` | Create, rename, or delete reminder lists |
54
+
55
+ ## Usage Examples
56
+
57
+ ```
58
+ "remind me to deploy the API on Friday at 3pm"
59
+ "what's on my list for today?"
60
+ "mark the deploy reminder as done"
61
+ "show me all overdue tasks"
62
+ "create a new list called Sprint 12"
63
+ "delete the test reminder"
64
+ ```
65
+
66
+ ## Environment Variables
67
+
68
+ | Variable | Description |
69
+ |----------|-------------|
70
+ | `REMINDCTL_PATH` | Custom path to remindctl binary (defaults to PATH lookup) |
71
+
72
+ ## Troubleshooting
73
+
74
+ **"remindctl is not authorized"** — Run `remindctl authorize` and grant Reminders access in System Settings.
75
+
76
+ **"Could not find remindctl"** — Install via Homebrew: `brew install steipete/tap/remindctl`, or set `REMINDCTL_PATH`.
77
+
78
+ **Tools not showing in Claude Code** — Restart Claude Code after adding the MCP server config. Check `~/.claude/settings.local.json` for correct absolute path.
79
+
80
+ **Using nvm?** MCP stdio servers don't load nvm. Use the full node path in your config:
81
+
82
+ ```json
83
+ "command": "/opt/homebrew/bin/node"
84
+ ```
85
+
86
+ Find yours with: `readlink -f $(which node)` or `ls /opt/homebrew/bin/node`.
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { execFile } from "node:child_process";
5
+ import { existsSync } from "node:fs";
6
+ import { promisify } from "node:util";
7
+ import { z } from "zod";
8
+ const execFileAsync = promisify(execFile);
9
+ // ---------------------------------------------------------------------------
10
+ // Resolve remindctl binary
11
+ // ---------------------------------------------------------------------------
12
+ function resolveRemindctl() {
13
+ if (process.env.REMINDCTL_PATH)
14
+ return process.env.REMINDCTL_PATH;
15
+ // MCP stdio servers often inherit a minimal PATH that excludes Homebrew.
16
+ // Check common install locations explicitly.
17
+ const candidates = [
18
+ "/opt/homebrew/bin/remindctl", // Apple Silicon
19
+ "/usr/local/bin/remindctl", // Intel Mac
20
+ ];
21
+ for (const path of candidates) {
22
+ if (existsSync(path))
23
+ return path;
24
+ }
25
+ // Fall back to PATH lookup
26
+ return "remindctl";
27
+ }
28
+ const REMINDCTL = resolveRemindctl();
29
+ async function runRemindctl(args) {
30
+ const fullArgs = [...args, "--json", "--no-input"];
31
+ try {
32
+ const { stdout } = await execFileAsync(REMINDCTL, fullArgs, {
33
+ timeout: 10_000,
34
+ env: { ...process.env, NO_COLOR: "1" },
35
+ });
36
+ return stdout;
37
+ }
38
+ catch (err) {
39
+ const e = err;
40
+ const parts = [];
41
+ if (e.killed)
42
+ parts.push("process timed out (10s limit)");
43
+ else if (e.code === "ENOENT")
44
+ parts.push(`binary not found: ${REMINDCTL}`);
45
+ else if (e.code === "EACCES")
46
+ parts.push(`permission denied: ${REMINDCTL}`);
47
+ else if (e.stderr?.trim())
48
+ parts.push(e.stderr.trim());
49
+ else if (e.message)
50
+ parts.push(e.message);
51
+ else
52
+ parts.push("unknown error");
53
+ if (e.signal)
54
+ parts.push(`signal: ${e.signal}`);
55
+ if (e.exitCode !== undefined && e.exitCode !== null)
56
+ parts.push(`exit code: ${e.exitCode}`);
57
+ const error = new Error(`remindctl failed: ${parts.join("; ")}`);
58
+ error.code = e.code;
59
+ throw error;
60
+ }
61
+ }
62
+ function parseJson(raw) {
63
+ try {
64
+ return JSON.parse(raw);
65
+ }
66
+ catch {
67
+ console.error(`[remindctl-mcp] Warning: failed to parse JSON output. ` +
68
+ `Raw (first 200 chars): ${raw.slice(0, 200)}`);
69
+ return raw.trim();
70
+ }
71
+ }
72
+ function errorMessage(e) {
73
+ if (e instanceof Error)
74
+ return e.message;
75
+ if (typeof e === "string")
76
+ return e;
77
+ return String(e);
78
+ }
79
+ function ok(data) {
80
+ const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
81
+ return { content: [{ type: "text", text }] };
82
+ }
83
+ function fail(msg) {
84
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Server setup
88
+ // ---------------------------------------------------------------------------
89
+ const server = new McpServer({
90
+ name: "remindctl-mcp",
91
+ version: "1.0.0",
92
+ });
93
+ // ---------------------------------------------------------------------------
94
+ // Tool 1: add_reminder
95
+ // ---------------------------------------------------------------------------
96
+ server.registerTool("add_reminder", {
97
+ title: "Add Reminder",
98
+ description: `Add a new reminder to Apple Reminders. Use when the user wants to remember something, create a task, set a to-do, schedule a deadline, or needs to be reminded about anything.
99
+
100
+ Args:
101
+ - title (string, required): The reminder text
102
+ - list (string, optional): Target reminder list (e.g. "Work", "Personal"). Uses default list if omitted.
103
+ - due (string, optional): Due date in natural language ("tomorrow", "Friday 3pm", "2026-04-05")
104
+ - notes (string, optional): Additional notes or context
105
+ - priority (string, optional): none, low, medium, or high`,
106
+ inputSchema: {
107
+ title: z.string().min(1).describe("Reminder title/text"),
108
+ list: z.string().optional().describe("Target reminder list name"),
109
+ due: z.string().optional().describe("Due date — natural language or ISO date"),
110
+ notes: z.string().optional().describe("Additional notes"),
111
+ priority: z.enum(["none", "low", "medium", "high"]).optional().describe("Priority level"),
112
+ },
113
+ annotations: {
114
+ readOnlyHint: false,
115
+ destructiveHint: false,
116
+ idempotentHint: false,
117
+ },
118
+ }, async ({ title, list, due, notes, priority }) => {
119
+ try {
120
+ const args = ["add", title];
121
+ if (list !== undefined)
122
+ args.push("--list", list);
123
+ if (due !== undefined)
124
+ args.push("--due", due);
125
+ if (notes !== undefined)
126
+ args.push("--notes", notes);
127
+ if (priority !== undefined)
128
+ args.push("--priority", priority);
129
+ const raw = await runRemindctl(args);
130
+ return ok(parseJson(raw));
131
+ }
132
+ catch (e) {
133
+ return fail(errorMessage(e));
134
+ }
135
+ });
136
+ // ---------------------------------------------------------------------------
137
+ // Tool 2: show_reminders
138
+ // ---------------------------------------------------------------------------
139
+ server.registerTool("show_reminders", {
140
+ title: "Show Reminders",
141
+ description: `Show reminders from Apple Reminders. Use when the user asks what they need to do, what's on their list, what's due, overdue, or wants to see their agenda/tasks.
142
+
143
+ Args:
144
+ - filter (string, optional): today, tomorrow, week, overdue, upcoming, completed, all, or a date string. Defaults to "upcoming".
145
+ - list (string, optional): Limit to a specific reminder list`,
146
+ inputSchema: {
147
+ filter: z
148
+ .enum(["today", "tomorrow", "week", "overdue", "upcoming", "completed", "all"])
149
+ .or(z.string())
150
+ .optional()
151
+ .default("upcoming")
152
+ .describe("Filter: today, tomorrow, week, overdue, upcoming, completed, all, or a date"),
153
+ list: z.string().optional().describe("Limit to a specific reminder list"),
154
+ },
155
+ annotations: {
156
+ readOnlyHint: true,
157
+ destructiveHint: false,
158
+ idempotentHint: true,
159
+ },
160
+ }, async ({ filter, list }) => {
161
+ try {
162
+ const args = ["show"];
163
+ if (filter !== undefined)
164
+ args.push(filter);
165
+ if (list !== undefined)
166
+ args.push("--list", list);
167
+ const raw = await runRemindctl(args);
168
+ return ok(parseJson(raw));
169
+ }
170
+ catch (e) {
171
+ return fail(errorMessage(e));
172
+ }
173
+ });
174
+ // ---------------------------------------------------------------------------
175
+ // Tool 3: list_reminder_lists
176
+ // ---------------------------------------------------------------------------
177
+ server.registerTool("list_reminder_lists", {
178
+ title: "List Reminder Lists",
179
+ description: `List all Apple Reminder lists, or show the contents of a specific list. Use when the user wants to know what lists they have or browse a specific list.
180
+
181
+ Args:
182
+ - name (string, optional): If provided, shows reminders in that list. If omitted, lists all available lists.`,
183
+ inputSchema: {
184
+ name: z.string().optional().describe("List name — omit to list all lists, provide to show contents"),
185
+ },
186
+ annotations: {
187
+ readOnlyHint: true,
188
+ destructiveHint: false,
189
+ idempotentHint: true,
190
+ },
191
+ }, async ({ name }) => {
192
+ try {
193
+ const args = ["list"];
194
+ if (name !== undefined)
195
+ args.push(name);
196
+ const raw = await runRemindctl(args);
197
+ return ok(parseJson(raw));
198
+ }
199
+ catch (e) {
200
+ return fail(errorMessage(e));
201
+ }
202
+ });
203
+ // ---------------------------------------------------------------------------
204
+ // Tool 4: edit_reminder
205
+ // ---------------------------------------------------------------------------
206
+ server.registerTool("edit_reminder", {
207
+ title: "Edit Reminder",
208
+ description: `Edit an existing Apple Reminder. Use when the user wants to change a reminder's title, due date, notes, priority, move it to another list, or mark it complete/incomplete.
209
+
210
+ Args:
211
+ - id (string, required): Index number or ID prefix from show_reminders output
212
+ - title (string, optional): New title
213
+ - list (string, optional): Move to a different list
214
+ - due (string, optional): New due date
215
+ - notes (string, optional): New notes
216
+ - priority (string, optional): none, low, medium, high
217
+ - clear_due (boolean, optional): Clear the due date
218
+ - complete (boolean, optional): Mark as completed
219
+ - incomplete (boolean, optional): Mark as incomplete`,
220
+ inputSchema: {
221
+ id: z.string().min(1).describe("Index or ID prefix from show output"),
222
+ title: z.string().optional().describe("New title"),
223
+ list: z.string().optional().describe("Move to list"),
224
+ due: z.string().optional().describe("New due date"),
225
+ notes: z.string().optional().describe("New notes"),
226
+ priority: z.enum(["none", "low", "medium", "high"]).optional().describe("Priority level"),
227
+ clear_due: z.boolean().optional().describe("Clear the due date"),
228
+ complete: z.boolean().optional().describe("Mark completed"),
229
+ incomplete: z.boolean().optional().describe("Mark incomplete"),
230
+ },
231
+ annotations: {
232
+ readOnlyHint: false,
233
+ destructiveHint: false,
234
+ idempotentHint: true,
235
+ },
236
+ }, async ({ id, title, list, due, notes, priority, clear_due, complete, incomplete }) => {
237
+ try {
238
+ const hasEdits = title !== undefined || list !== undefined || due !== undefined ||
239
+ notes !== undefined || priority !== undefined || clear_due || complete || incomplete;
240
+ if (!hasEdits) {
241
+ return fail("No edit fields provided. Specify at least one of: title, list, due, notes, priority, clear_due, complete, incomplete.");
242
+ }
243
+ const args = ["edit", id];
244
+ if (title !== undefined)
245
+ args.push("--title", title);
246
+ if (list !== undefined)
247
+ args.push("--list", list);
248
+ if (due !== undefined)
249
+ args.push("--due", due);
250
+ if (notes !== undefined)
251
+ args.push("--notes", notes);
252
+ if (priority !== undefined)
253
+ args.push("--priority", priority);
254
+ if (clear_due)
255
+ args.push("--clear-due");
256
+ if (complete)
257
+ args.push("--complete");
258
+ if (incomplete)
259
+ args.push("--incomplete");
260
+ const raw = await runRemindctl(args);
261
+ return ok(parseJson(raw));
262
+ }
263
+ catch (e) {
264
+ return fail(errorMessage(e));
265
+ }
266
+ });
267
+ // ---------------------------------------------------------------------------
268
+ // Tool 5: complete_reminders
269
+ // ---------------------------------------------------------------------------
270
+ server.registerTool("complete_reminders", {
271
+ title: "Complete Reminders",
272
+ description: `Mark one or more Apple Reminders as complete. Use when the user says they finished a task, did something, or wants to check off a reminder.
273
+
274
+ Args:
275
+ - ids (string[], required): Array of index numbers or ID prefixes from show_reminders output`,
276
+ inputSchema: {
277
+ ids: z.array(z.string()).min(1).describe("Indexes or ID prefixes to mark complete"),
278
+ },
279
+ annotations: {
280
+ readOnlyHint: false,
281
+ destructiveHint: false,
282
+ idempotentHint: true,
283
+ },
284
+ }, async ({ ids }) => {
285
+ try {
286
+ const args = ["complete", ...ids];
287
+ const raw = await runRemindctl(args);
288
+ return ok(parseJson(raw));
289
+ }
290
+ catch (e) {
291
+ return fail(errorMessage(e));
292
+ }
293
+ });
294
+ // ---------------------------------------------------------------------------
295
+ // Tool 6: delete_reminders
296
+ // ---------------------------------------------------------------------------
297
+ server.registerTool("delete_reminders", {
298
+ title: "Delete Reminders",
299
+ description: `Delete one or more Apple Reminders permanently. Use when the user wants to remove a reminder entirely (not just complete it).
300
+
301
+ Args:
302
+ - ids (string[], required): Array of index numbers or ID prefixes from show_reminders output`,
303
+ inputSchema: {
304
+ ids: z.array(z.string()).min(1).describe("Indexes or ID prefixes to delete"),
305
+ },
306
+ annotations: {
307
+ readOnlyHint: false,
308
+ destructiveHint: true,
309
+ idempotentHint: false,
310
+ },
311
+ }, async ({ ids }) => {
312
+ try {
313
+ const args = ["delete", ...ids, "--force"];
314
+ const raw = await runRemindctl(args);
315
+ return ok(parseJson(raw));
316
+ }
317
+ catch (e) {
318
+ return fail(errorMessage(e));
319
+ }
320
+ });
321
+ // ---------------------------------------------------------------------------
322
+ // Tool 7: manage_reminder_list
323
+ // ---------------------------------------------------------------------------
324
+ server.registerTool("manage_reminder_list", {
325
+ title: "Manage Reminder List",
326
+ description: `Create, rename, or delete an Apple Reminder list. Use when the user wants to organize their reminders by creating new lists, renaming existing ones, or removing lists.
327
+
328
+ Args:
329
+ - name (string, required): The list name to act on
330
+ - action (string, required): "create", "rename", or "delete"
331
+ - new_name (string, optional): Required when action is "rename" — the new name for the list`,
332
+ inputSchema: {
333
+ name: z.string().min(1).describe("List name"),
334
+ action: z.enum(["create", "rename", "delete"]).describe("Action to perform"),
335
+ new_name: z.string().optional().describe("New name (required for rename)"),
336
+ },
337
+ annotations: {
338
+ readOnlyHint: false,
339
+ destructiveHint: true,
340
+ idempotentHint: false,
341
+ },
342
+ }, async ({ name, action, new_name }) => {
343
+ try {
344
+ const args = ["list", name];
345
+ switch (action) {
346
+ case "create":
347
+ args.push("--create");
348
+ break;
349
+ case "rename":
350
+ if (!new_name)
351
+ return fail("new_name is required when action is 'rename'");
352
+ args.push("--rename", new_name);
353
+ break;
354
+ case "delete":
355
+ args.push("--delete", "--force");
356
+ break;
357
+ }
358
+ const raw = await runRemindctl(args);
359
+ return ok(parseJson(raw));
360
+ }
361
+ catch (e) {
362
+ return fail(errorMessage(e));
363
+ }
364
+ });
365
+ // ---------------------------------------------------------------------------
366
+ // Startup
367
+ // ---------------------------------------------------------------------------
368
+ async function main() {
369
+ // Check remindctl is available and authorized
370
+ try {
371
+ const raw = await runRemindctl(["status"]);
372
+ const status = parseJson(raw);
373
+ if (!status.authorized) {
374
+ console.error("[remindctl-mcp] remindctl is not authorized to access Reminders. Run: remindctl authorize");
375
+ }
376
+ }
377
+ catch (err) {
378
+ const e = err;
379
+ if (e.code === "ENOENT" || e.message?.includes("binary not found")) {
380
+ console.error("[remindctl-mcp] Could not find remindctl binary. Install: brew install steipete/tap/remindctl");
381
+ console.error("[remindctl-mcp] Or set REMINDCTL_PATH env var to the binary location.");
382
+ }
383
+ else {
384
+ console.error(`[remindctl-mcp] Health check failed: ${errorMessage(err)}`);
385
+ }
386
+ console.error("[remindctl-mcp] Server starting in degraded mode. Tool calls may fail.");
387
+ }
388
+ const transport = new StdioServerTransport();
389
+ await server.connect(transport);
390
+ console.error("[remindctl-mcp] Server started (stdio)");
391
+ }
392
+ main().catch((err) => {
393
+ console.error("[remindctl-mcp] Fatal:", err);
394
+ process.exit(1);
395
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "apple-reminders-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Apple Reminders via remindctl CLI",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "apple-reminders-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && chmod 755 dist/index.js",
15
+ "prepublishOnly": "npm run build",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.22.0",
20
+ "zod": "^3.24.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^25.5.0",
24
+ "typescript": "^5.7.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "os": [
30
+ "darwin"
31
+ ],
32
+ "license": "MIT"
33
+ }