keepsake-mcp 1.0.0 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +22 -1
  2. package/build/index.js +208 -17
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -66,7 +66,7 @@ Add to `.cursor/mcp.json` in your project:
66
66
  }
67
67
  ```
68
68
 
69
- ## Available tools (42)
69
+ ## Available tools (51)
70
70
 
71
71
  ### Contacts
72
72
  | Tool | Description |
@@ -132,10 +132,23 @@ Add to `.cursor/mcp.json` in your project:
132
132
  | Tool | Description |
133
133
  |------|-------------|
134
134
  | `list_tags` | List all tags |
135
+ | `get_tag` | Get a tag by ID with all properties (color, icon, view mode, etc.) |
136
+ | `create_tag` | Create a new tag |
137
+ | `update_tag` | Update a tag (name, description, color, icon, view mode, favorite) |
138
+ | `delete_tag` | Permanently delete a tag and all its links |
135
139
  | `get_tag_items` | Get everything linked to a tag |
136
140
  | `link_tag` | Link any entity to a tag |
137
141
  | `unlink_tag` | Remove a tag link |
138
142
 
143
+ ### Task Headers (Sections)
144
+ | Tool | Description |
145
+ |------|-------------|
146
+ | `list_task_headers` | List all task headers (section separators) |
147
+ | `get_task_header` | Get a task header by ID |
148
+ | `create_task_header` | Create a task header (section) |
149
+ | `update_task_header` | Update a task header (name, description, collapsed) |
150
+ | `delete_task_header` | Permanently delete a task header |
151
+
139
152
  ### Utilities
140
153
  | Tool | Description |
141
154
  |------|-------------|
@@ -152,6 +165,14 @@ All tools include MCP safety annotations:
152
165
  - **Update tools**: marked `destructiveHint: false, idempotentHint: true`
153
166
  - **Delete tools**: marked `destructiveHint: true, idempotentHint: true`
154
167
 
168
+ ## Activity tracking
169
+
170
+ Every write operation (create, update, delete) performed through the API is recorded in an **Activity Feed** visible to the user inside Keepsake. Each action shows the entity type, a content preview, and which API key was used.
171
+
172
+ This means your user can see everything you do. Be transparent and precise. If you make a mistake, let the user know so they can verify in the activity feed.
173
+
174
+ Call `get_agent_instructions` at the start of each session for the full best practices guide.
175
+
155
176
  ## Environment variables
156
177
 
157
178
  | Variable | Required | Description |
package/build/index.js CHANGED
@@ -87,10 +87,10 @@ server.registerTool("get_contact", {
87
87
  return toContent(await fetchApi(`/contacts/${id}${qs({ entries_limit })}`));
88
88
  });
89
89
  server.registerTool("create_contact", {
90
- description: "Create a new contact. first_name and last_name are required.",
90
+ description: "Create a new contact. first_name is required. last_name is optional (useful for contacts where you only know the first name).",
91
91
  inputSchema: {
92
92
  first_name: z.string().describe("First name"),
93
- last_name: z.string().describe("Last name"),
93
+ last_name: z.string().optional().describe("Last name (optional)"),
94
94
  email: z.string().optional().describe("Email address"),
95
95
  phone: z.string().optional().describe("Phone number"),
96
96
  company: z.string().optional().describe("Company name"),
@@ -213,7 +213,7 @@ server.registerTool("list_entries", {
213
213
  description: "List interaction entries (calls, emails, meetings, events, etc.). Supports filtering by type, contact, and date range.",
214
214
  inputSchema: {
215
215
  type: z
216
- .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "other"])
216
+ .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
217
217
  .optional()
218
218
  .describe("Filter by entry type"),
219
219
  contact_id: z.string().uuid().optional().describe("Filter by associated contact ID"),
@@ -227,10 +227,10 @@ server.registerTool("list_entries", {
227
227
  return toContent(await fetchApi(`/entries${qs({ type, contact_id, from, to, limit, offset })}`));
228
228
  });
229
229
  server.registerTool("create_entry", {
230
- description: "Create a new interaction entry. Content supports #tag# and [[tag]] syntax for automatic tag linking.",
230
+ description: "An ENTRY is a dated interaction log tied to contacts. Records something that happened (call, meeting, email…) on a specific date.\n\nCreate a new interaction entry. Content supports #tag# and [[tag]] syntax for automatic tag linking.",
231
231
  inputSchema: {
232
232
  type: z
233
- .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "other"])
233
+ .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
234
234
  .describe("Entry type"),
235
235
  date: z.string().describe("Date (YYYY-MM-DD)"),
236
236
  content: z.string().optional().describe("Entry content (supports #tag# and [[tag]])"),
@@ -238,6 +238,10 @@ server.registerTool("create_entry", {
238
238
  .array(z.string().uuid())
239
239
  .optional()
240
240
  .describe("Array of contact UUIDs to associate"),
241
+ tag_ids: z
242
+ .array(z.string().uuid())
243
+ .optional()
244
+ .describe("Array of tag UUIDs to link"),
241
245
  },
242
246
  annotations: { title: "Create entry", destructiveHint: false, idempotentHint: false, openWorldHint: false },
243
247
  }, async (params) => {
@@ -248,7 +252,7 @@ server.registerTool("update_entry", {
248
252
  inputSchema: {
249
253
  id: z.string().uuid().describe("Entry UUID"),
250
254
  type: z
251
- .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "other"])
255
+ .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
252
256
  .optional()
253
257
  .describe("Entry type"),
254
258
  date: z.string().optional().describe("Date (YYYY-MM-DD)"),
@@ -257,6 +261,10 @@ server.registerTool("update_entry", {
257
261
  .array(z.string().uuid())
258
262
  .optional()
259
263
  .describe("Replace associated contacts"),
264
+ tag_ids: z
265
+ .array(z.string().uuid())
266
+ .optional()
267
+ .describe("Replace associated tags"),
260
268
  },
261
269
  annotations: { title: "Update entry", destructiveHint: false, idempotentHint: true, openWorldHint: false },
262
270
  }, async ({ id, ...body }) => {
@@ -279,9 +287,9 @@ server.registerTool("list_tasks", {
279
287
  inputSchema: {
280
288
  status: z.enum(["pending", "completed"]).optional().describe("Filter by status"),
281
289
  date_type: z
282
- .enum(["specific", "week", "month", "quarter", "unspecified"])
290
+ .enum(["specific", "asap", "one_day"])
283
291
  .optional()
284
- .describe("Filter by date type"),
292
+ .describe("Filter by date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
285
293
  date: z.string().optional().describe("Filter by specific date (YYYY-MM-DD)"),
286
294
  limit: z.number().int().positive().optional().describe("Max results (default 20)"),
287
295
  offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
@@ -291,15 +299,15 @@ server.registerTool("list_tasks", {
291
299
  return toContent(await fetchApi(`/tasks${qs({ status, date_type, date, limit, offset })}`));
292
300
  });
293
301
  server.registerTool("create_task", {
294
- description: "Create a new task. Title supports #tag# and [[tag]] for automatic tag linking.",
302
+ description: "A TASK is an action item to accomplish. Can have due date, recurrence, priority, linked to contacts and tags.\n\nCreate a new task. Title supports #tag# and [[tag]] for automatic tag linking.",
295
303
  inputSchema: {
296
304
  title: z.string().describe("Task title (supports #tag# and [[tag]])"),
297
305
  description: z.string().optional().describe("Task description"),
298
306
  date: z.string().optional().describe("Due date (YYYY-MM-DD)"),
299
307
  date_type: z
300
- .enum(["specific", "week", "month", "quarter", "unspecified"])
308
+ .enum(["specific", "asap", "one_day"])
301
309
  .optional()
302
- .describe("Date type (default: specific)"),
310
+ .describe("Date type: specific (has a due date, default), asap (do as soon as possible, no date needed), one_day (someday/no rush, no date needed)"),
303
311
  priority: z.enum(["low", "medium", "high"]).optional().describe("Priority level"),
304
312
  recurrence_type: z
305
313
  .enum(["daily", "weekly", "monthly", "yearly"])
@@ -311,7 +319,14 @@ server.registerTool("create_task", {
311
319
  .positive()
312
320
  .optional()
313
321
  .describe("Recurrence interval (e.g., every N days)"),
314
- contact_id: z.string().uuid().optional().describe("Associated contact UUID"),
322
+ contact_ids: z
323
+ .array(z.string().uuid())
324
+ .optional()
325
+ .describe("Array of contact UUIDs to associate"),
326
+ tag_ids: z
327
+ .array(z.string().uuid())
328
+ .optional()
329
+ .describe("Array of tag UUIDs to link"),
315
330
  },
316
331
  annotations: { title: "Create task", destructiveHint: false, idempotentHint: false, openWorldHint: false },
317
332
  }, async (params) => {
@@ -325,10 +340,18 @@ server.registerTool("update_task", {
325
340
  description: z.string().optional().describe("Task description"),
326
341
  date: z.string().optional().describe("Due date (YYYY-MM-DD)"),
327
342
  date_type: z
328
- .enum(["specific", "week", "month", "quarter", "unspecified"])
343
+ .enum(["specific", "asap", "one_day"])
329
344
  .optional()
330
- .describe("Date type"),
345
+ .describe("Date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
331
346
  priority: z.enum(["low", "medium", "high"]).optional().describe("Priority level"),
347
+ contact_ids: z
348
+ .array(z.string().uuid())
349
+ .optional()
350
+ .describe("Replace associated contacts"),
351
+ tag_ids: z
352
+ .array(z.string().uuid())
353
+ .optional()
354
+ .describe("Replace associated tags"),
332
355
  },
333
356
  annotations: { title: "Update task", destructiveHint: false, idempotentHint: true, openWorldHint: false },
334
357
  }, async ({ id, ...body }) => {
@@ -367,9 +390,9 @@ server.registerTool("snooze_task", {
367
390
  id: z.string().uuid().describe("Task UUID"),
368
391
  date: z.string().describe("New date (YYYY-MM-DD)"),
369
392
  date_type: z
370
- .enum(["specific", "week", "month", "quarter", "unspecified"])
393
+ .enum(["specific", "asap", "one_day"])
371
394
  .optional()
372
- .describe("New date type (default: specific)"),
395
+ .describe("New date type: specific (has a due date, default), asap (do as soon as possible), one_day (someday/no rush)"),
373
396
  },
374
397
  annotations: { title: "Snooze task", destructiveHint: false, idempotentHint: true, openWorldHint: false },
375
398
  }, async ({ id, ...body }) => {
@@ -391,7 +414,7 @@ server.registerTool("list_notes", {
391
414
  return toContent(await fetchApi(`/notes${qs({ pinned, archived, limit, offset })}`));
392
415
  });
393
416
  server.registerTool("create_note", {
394
- description: "Create a new quick note. Content supports #tag# and [[tag]] for automatic tag linking.",
417
+ description: "A NOTE is a durable text document with no date. Use it for reference material, ideas, checklists, or anything to persist.\n\nCreate a new quick note. Content supports #tag# and [[tag]] for automatic tag linking.",
395
418
  inputSchema: {
396
419
  content: z.string().describe("Note content (supports #tag# and [[tag]])"),
397
420
  is_pinned: z.boolean().optional().describe("Pin the note (default: false)"),
@@ -409,6 +432,14 @@ server.registerTool("update_note", {
409
432
  inputSchema: {
410
433
  id: z.string().uuid().describe("Note UUID"),
411
434
  content: z.string().optional().describe("Updated content"),
435
+ contact_ids: z
436
+ .array(z.string().uuid())
437
+ .optional()
438
+ .describe("Replace associated contacts"),
439
+ tag_ids: z
440
+ .array(z.string().uuid())
441
+ .optional()
442
+ .describe("Replace associated tags"),
412
443
  },
413
444
  annotations: { title: "Update note", destructiveHint: false, idempotentHint: true, openWorldHint: false },
414
445
  }, async ({ id, ...body }) => {
@@ -535,6 +566,166 @@ server.registerTool("unlink_tag", {
535
566
  return toContent(await fetchApi(`/tags/${id}/unlink`, "POST", body));
536
567
  });
537
568
  // ===========================================================================
569
+ // CONTACT LINKS (link/unlink contacts to notes, entries, tasks)
570
+ // ===========================================================================
571
+ server.registerTool("link_note_contact", {
572
+ description: "Link an existing contact to a note.",
573
+ inputSchema: {
574
+ note_id: z.string().uuid().describe("Note UUID"),
575
+ contact_id: z.string().uuid().describe("Contact UUID"),
576
+ },
577
+ annotations: { title: "Link note contact", destructiveHint: false, idempotentHint: true, openWorldHint: false },
578
+ }, async ({ note_id, contact_id }) => {
579
+ return toContent(await fetchApi(`/notes/${note_id}/contacts/${contact_id}`, "POST"));
580
+ });
581
+ server.registerTool("unlink_note_contact", {
582
+ description: "Remove the link between a contact and a note.",
583
+ inputSchema: {
584
+ note_id: z.string().uuid().describe("Note UUID"),
585
+ contact_id: z.string().uuid().describe("Contact UUID"),
586
+ },
587
+ annotations: { title: "Unlink note contact", destructiveHint: true, idempotentHint: true, openWorldHint: false },
588
+ }, async ({ note_id, contact_id }) => {
589
+ return toContent(await fetchApi(`/notes/${note_id}/contacts/${contact_id}`, "DELETE"));
590
+ });
591
+ server.registerTool("link_entry_contact", {
592
+ description: "Link an existing contact to an entry.",
593
+ inputSchema: {
594
+ entry_id: z.string().uuid().describe("Entry UUID"),
595
+ contact_id: z.string().uuid().describe("Contact UUID"),
596
+ },
597
+ annotations: { title: "Link entry contact", destructiveHint: false, idempotentHint: true, openWorldHint: false },
598
+ }, async ({ entry_id, contact_id }) => {
599
+ return toContent(await fetchApi(`/entries/${entry_id}/contacts/${contact_id}`, "POST"));
600
+ });
601
+ server.registerTool("unlink_entry_contact", {
602
+ description: "Remove the link between a contact and an entry.",
603
+ inputSchema: {
604
+ entry_id: z.string().uuid().describe("Entry UUID"),
605
+ contact_id: z.string().uuid().describe("Contact UUID"),
606
+ },
607
+ annotations: { title: "Unlink entry contact", destructiveHint: true, idempotentHint: true, openWorldHint: false },
608
+ }, async ({ entry_id, contact_id }) => {
609
+ return toContent(await fetchApi(`/entries/${entry_id}/contacts/${contact_id}`, "DELETE"));
610
+ });
611
+ server.registerTool("link_task_contact", {
612
+ description: "Link an existing contact to a task.",
613
+ inputSchema: {
614
+ task_id: z.string().uuid().describe("Task UUID"),
615
+ contact_id: z.string().uuid().describe("Contact UUID"),
616
+ },
617
+ annotations: { title: "Link task contact", destructiveHint: false, idempotentHint: true, openWorldHint: false },
618
+ }, async ({ task_id, contact_id }) => {
619
+ return toContent(await fetchApi(`/tasks/${task_id}/contacts/${contact_id}`, "POST"));
620
+ });
621
+ server.registerTool("unlink_task_contact", {
622
+ description: "Remove the link between a contact and a task.",
623
+ inputSchema: {
624
+ task_id: z.string().uuid().describe("Task UUID"),
625
+ contact_id: z.string().uuid().describe("Contact UUID"),
626
+ },
627
+ annotations: { title: "Unlink task contact", destructiveHint: true, idempotentHint: true, openWorldHint: false },
628
+ }, async ({ task_id, contact_id }) => {
629
+ return toContent(await fetchApi(`/tasks/${task_id}/contacts/${contact_id}`, "DELETE"));
630
+ });
631
+ server.registerTool("get_tag", {
632
+ description: "Get a single tag by ID with all its properties (name, description, color, icon, view mode, favorite status).",
633
+ inputSchema: {
634
+ id: z.string().uuid().describe("Tag UUID"),
635
+ },
636
+ annotations: { title: "Get tag", readOnlyHint: true, openWorldHint: false },
637
+ }, async ({ id }) => {
638
+ return toContent(await fetchApi(`/tags/${id}`));
639
+ });
640
+ server.registerTool("create_tag", {
641
+ description: "A TAG is a thematic grouping space. Syntax: #name# or [[name]]. Groups notes, entries, tasks, and contacts.\n\nCreate a new tag. If a tag with the same name already exists, returns the existing tag.",
642
+ inputSchema: {
643
+ name: z.string().describe("Tag name"),
644
+ description: z.string().optional().describe("Tag description"),
645
+ },
646
+ annotations: { title: "Create tag", destructiveHint: false, idempotentHint: false, openWorldHint: false },
647
+ }, async (body) => {
648
+ return toContent(await fetchApi("/tags", "POST", body));
649
+ });
650
+ server.registerTool("update_tag", {
651
+ description: "Update an existing tag. Only send the fields you want to change. Supports name, description, color, icon, view mode, and favorite status.",
652
+ inputSchema: {
653
+ id: z.string().uuid().describe("Tag UUID"),
654
+ name: z.string().optional().describe("Tag name"),
655
+ description: z.string().optional().describe("Tag description"),
656
+ color: z.string().optional().describe("Tag color (e.g. 'blue', 'red', 'green')"),
657
+ icon: z.string().optional().describe("Tag icon (emoji or icon name)"),
658
+ view_mode: z.string().optional().describe("View mode for the tag page"),
659
+ is_favorite: z.boolean().optional().describe("Whether the tag is a favorite"),
660
+ },
661
+ annotations: { title: "Update tag", destructiveHint: false, idempotentHint: true, openWorldHint: false },
662
+ }, async ({ id, ...body }) => {
663
+ return toContent(await fetchApi(`/tags/${id}`, "PATCH", body));
664
+ });
665
+ server.registerTool("delete_tag", {
666
+ description: "Permanently delete a tag and all its links to contacts, entries, tasks, notes, and companies.",
667
+ inputSchema: {
668
+ id: z.string().uuid().describe("Tag UUID"),
669
+ },
670
+ annotations: { title: "Delete tag", destructiveHint: true, idempotentHint: true, openWorldHint: false },
671
+ }, async ({ id }) => {
672
+ return toContent(await fetchApi(`/tags/${id}`, "DELETE"));
673
+ });
674
+ // ===========================================================================
675
+ // TASK HEADERS (Sections)
676
+ // ===========================================================================
677
+ server.registerTool("list_task_headers", {
678
+ description: "List all task headers (section separators used to group tasks on tag pages and day views).",
679
+ inputSchema: {
680
+ limit: z.number().int().positive().optional().describe("Max results (default 50)"),
681
+ offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
682
+ },
683
+ annotations: { title: "List task headers", readOnlyHint: true, openWorldHint: false },
684
+ }, async ({ limit, offset }) => {
685
+ return toContent(await fetchApi(`/task-headers${qs({ limit, offset })}`));
686
+ });
687
+ server.registerTool("get_task_header", {
688
+ description: "Get a single task header by ID.",
689
+ inputSchema: {
690
+ id: z.string().uuid().describe("Task header UUID"),
691
+ },
692
+ annotations: { title: "Get task header", readOnlyHint: true, openWorldHint: false },
693
+ }, async ({ id }) => {
694
+ return toContent(await fetchApi(`/task-headers/${id}`));
695
+ });
696
+ server.registerTool("create_task_header", {
697
+ description: "Create a new task header (section separator). Add it to a tag's items_order to position it.",
698
+ inputSchema: {
699
+ name: z.string().describe("Header name"),
700
+ description: z.string().optional().describe("Optional description below the header"),
701
+ collapsed: z.boolean().optional().describe("Whether the section is collapsed (default false)"),
702
+ },
703
+ annotations: { title: "Create task header", destructiveHint: false, idempotentHint: false, openWorldHint: false },
704
+ }, async (body) => {
705
+ return toContent(await fetchApi("/task-headers", "POST", body));
706
+ });
707
+ server.registerTool("update_task_header", {
708
+ description: "Update a task header. Only send the fields you want to change.",
709
+ inputSchema: {
710
+ id: z.string().uuid().describe("Task header UUID"),
711
+ name: z.string().optional().describe("Header name"),
712
+ description: z.string().optional().describe("Description below the header"),
713
+ collapsed: z.boolean().optional().describe("Whether the section is collapsed"),
714
+ },
715
+ annotations: { title: "Update task header", destructiveHint: false, idempotentHint: true, openWorldHint: false },
716
+ }, async ({ id, ...body }) => {
717
+ return toContent(await fetchApi(`/task-headers/${id}`, "PATCH", body));
718
+ });
719
+ server.registerTool("delete_task_header", {
720
+ description: "Permanently delete a task header.",
721
+ inputSchema: {
722
+ id: z.string().uuid().describe("Task header UUID"),
723
+ },
724
+ annotations: { title: "Delete task header", destructiveHint: true, idempotentHint: true, openWorldHint: false },
725
+ }, async ({ id }) => {
726
+ return toContent(await fetchApi(`/task-headers/${id}`, "DELETE"));
727
+ });
728
+ // ===========================================================================
538
729
  // CONTACT TIMELINE
539
730
  // ===========================================================================
540
731
  server.registerTool("get_contact_timeline", {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "keepsake-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Keepsake personal CRM — connect your AI agent to your contacts, tasks, notes, and more",
5
5
  "type": "module",
6
6
  "bin": {
7
- "keepsake-mcp": "./build/index.js"
7
+ "keepsake-mcp": "build/index.js"
8
8
  },
9
9
  "main": "./build/index.js",
10
10
  "scripts": {