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 +21 -0
- package/README.md +90 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +395 -0
- package/package.json +33 -0
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|