keepsake-mcp 1.11.0 → 1.13.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 +12 -8
  2. package/build/tools.js +115 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -114,7 +114,7 @@ rather than in the chat, which disappears.
114
114
  |--------|-----------|-------------|
115
115
  | `review_note` | `note_id` | Act as the editor of a note: read it, judge form and substance, leave anchored remarks in the margin, never rewrite the text |
116
116
 
117
- ## Available tools (76)
117
+ ## Available tools (83)
118
118
 
119
119
  ### Contacts
120
120
  | Tool | Description |
@@ -143,7 +143,7 @@ rather than in the chat, which disappears.
143
143
  ### Entries (Interactions)
144
144
  | Tool | Description |
145
145
  |------|-------------|
146
- | `list_entries` | List interactions (calls, emails, meetings, etc.) |
146
+ | `list_entries` | List interactions (calls, emails, meetings, etc.) — filter by type, contact, company, page (`tag_id`), dates |
147
147
  | `create_entry` | Log a new interaction — supports `#tag#` and `[[tag]]` syntax |
148
148
  | `update_entry` | Update an interaction |
149
149
  | `delete_entry` | Delete an interaction |
@@ -151,7 +151,8 @@ rather than in the chat, which disappears.
151
151
  ### Tasks
152
152
  | Tool | Description |
153
153
  |------|-------------|
154
- | `list_tasks` | List tasks with status/date filters |
154
+ | `list_tasks` | List tasks — filter by status, date, company, page (`tag_id`) |
155
+ | `get_task` | Get a task with its tags, contacts, companies and linked notes |
155
156
  | `create_task` | Create a task — supports `#tag#` and `[[tag]]` syntax |
156
157
  | `update_task` | Update task fields |
157
158
  | `delete_task` | Delete a task |
@@ -164,12 +165,12 @@ rather than in the chat, which disappears.
164
165
  ### QuickNotes
165
166
  | Tool | Description |
166
167
  |------|-------------|
167
- | `list_notes` | List notes (filter by pinned/archived) |
168
+ | `list_notes` | List notes — filter by pinned/archived, day, company, page (`tag_id`) |
168
169
  | `get_note` | Get one note by ID with its tags, contacts, tasks and linked notes |
169
170
  | `create_note` | Create a note — supports `#tag#` and `[[tag]]` syntax |
170
171
  | `update_note` | Update note content |
171
172
  | `delete_note` | Soft-delete (or permanent) |
172
- | `pin_note` | Pin to top |
173
+ | `pin_note` | Pin as a post-it (short reference always at hand) |
173
174
  | `archive_note` | Archive a note |
174
175
  | `restore_note` | Restore a deleted/archived note |
175
176
 
@@ -205,7 +206,7 @@ Material kept *alongside* a note without entering its text — an idea, a refere
205
206
  | `list_tags` | List tags (lightweight — ordering arrays omitted), with optional name search (`q`) |
206
207
  | `get_tag` | Get a tag by ID with all properties, including `tasks_order` (section markers `h:<header_id>`) |
207
208
  | `create_tag` | Create a new tag |
208
- | `update_tag` | Update a tag (name, description, color, icon, view mode, favorite) |
209
+ | `update_tag` | Update a tag (name, description, color, icon, favorite, order of tasks and sections via `tasks_order`) |
209
210
  | `delete_tag` | Permanently delete a tag and all its links |
210
211
  | `get_tag_items` | Get items linked to a tag — filter by `types`/`status`, `summary` mode, task `sections` included |
211
212
  | `link_tag` | Link any entity to a tag |
@@ -216,17 +217,20 @@ Material kept *alongside* a note without entering its text — an idea, a refere
216
217
  |------|-------------|
217
218
  | `list_task_headers` | List all task headers (section separators) |
218
219
  | `get_task_header` | Get a task header by ID |
219
- | `create_task_header` | Create a task header (section) |
220
+ | `create_task_header` | Create a task header (section) — insert `h:<id>` in the tag's `tasks_order` to place it |
220
221
  | `update_task_header` | Update a task header (name, description, collapsed) |
221
222
  | `delete_task_header` | Permanently delete a task header |
222
223
 
223
- ### Contact Links
224
+ ### Contact & company links
224
225
  | Tool | Description |
225
226
  |------|-------------|
227
+ | `link_note_company` / `unlink_note_company` | Link / unlink a company to a note |
226
228
  | `link_note_contact` | Link a contact to a note |
227
229
  | `unlink_note_contact` | Remove a contact link from a note |
230
+ | `link_entry_company` / `unlink_entry_company` | Add / remove a company as participant of an entry |
228
231
  | `link_entry_contact` | Link a contact to an entry |
229
232
  | `unlink_entry_contact` | Remove a contact link from an entry |
233
+ | `link_task_company` / `unlink_task_company` | Link / unlink a company to a task |
230
234
  | `link_task_contact` | Link a contact to a task |
231
235
  | `unlink_task_contact` | Remove a contact link from a task |
232
236
  | `link_task_note` | Link a note to a task (non-destructive, the note survives) |
package/build/tools.js CHANGED
@@ -295,21 +295,23 @@ export function registerAllTools(server, fetchApi) {
295
295
  // ENTRIES (Interactions)
296
296
  // ===========================================================================
297
297
  server.registerTool("list_entries", {
298
- description: "List interaction entries (calls, emails, meetings, events, etc.). Supports filtering by type, contact, and date range.",
298
+ description: "List interaction entries (calls, emails, meetings, events, etc.). Supports filtering by type, contact, company, tag (page) and date range.",
299
299
  inputSchema: {
300
300
  type: z
301
301
  .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
302
302
  .optional()
303
303
  .describe("Filter by entry type"),
304
304
  contact_id: z.string().uuid().optional().describe("Filter by associated contact ID"),
305
+ company_id: z.string().uuid().optional().describe("Only entries where this company is a participant"),
306
+ tag_id: z.string().uuid().optional().describe("Only entries linked to this tag (page)"),
305
307
  from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
306
308
  to: z.string().optional().describe("End date (YYYY-MM-DD)"),
307
309
  limit: z.number().int().positive().optional().describe("Max results (default 20)"),
308
310
  offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
309
311
  },
310
312
  annotations: { title: "List entries", readOnlyHint: true, openWorldHint: false },
311
- }, async ({ type, contact_id, from, to, limit, offset }) => {
312
- return toContent(await fetchApi(`/entries${qs({ type, contact_id, from, to, limit, offset })}`));
313
+ }, async ({ type, contact_id, company_id, tag_id, from, to, limit, offset }) => {
314
+ return toContent(await fetchApi(`/entries${qs({ type, contact_id, company_id, tag_id, from, to, limit, offset })}`));
313
315
  });
314
316
  server.registerTool("create_entry", {
315
317
  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.",
@@ -323,6 +325,10 @@ export function registerAllTools(server, fetchApi) {
323
325
  .array(z.string().uuid())
324
326
  .optional()
325
327
  .describe("Array of contact UUIDs to associate"),
328
+ company_ids: z
329
+ .array(z.string().uuid())
330
+ .optional()
331
+ .describe("Array of company UUIDs to associate (companies are separate from contacts)"),
326
332
  tag_ids: z
327
333
  .array(z.string().uuid())
328
334
  .optional()
@@ -346,6 +352,10 @@ export function registerAllTools(server, fetchApi) {
346
352
  .array(z.string().uuid())
347
353
  .optional()
348
354
  .describe("Replace associated contacts"),
355
+ company_ids: z
356
+ .array(z.string().uuid())
357
+ .optional()
358
+ .describe("Replace associated companies (send [] to unlink all). Contacts are unaffected."),
349
359
  tag_ids: z
350
360
  .array(z.string().uuid())
351
361
  .optional()
@@ -368,7 +378,7 @@ export function registerAllTools(server, fetchApi) {
368
378
  // TASKS
369
379
  // ===========================================================================
370
380
  server.registerTool("list_tasks", {
371
- description: "List tasks. Filter by status (pending/completed), date_type, or specific date.",
381
+ description: "List tasks. Filter by status (pending/completed), date_type, specific date, company, or tag (page) — e.g. tag_id + status=pending lists a project's open tasks.",
372
382
  inputSchema: {
373
383
  status: z.enum(["pending", "completed"]).optional().describe("Filter by status"),
374
384
  date_type: z
@@ -376,12 +386,23 @@ export function registerAllTools(server, fetchApi) {
376
386
  .optional()
377
387
  .describe("Filter by date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
378
388
  date: z.string().optional().describe("Filter by specific date (YYYY-MM-DD)"),
389
+ company_id: z.string().uuid().optional().describe("Only tasks linked directly to this company"),
390
+ tag_id: z.string().uuid().optional().describe("Only tasks linked to this tag (page)"),
379
391
  limit: z.number().int().positive().optional().describe("Max results (default 20)"),
380
392
  offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
381
393
  },
382
394
  annotations: { title: "List tasks", readOnlyHint: true, openWorldHint: false },
383
- }, async ({ status, date_type, date, limit, offset }) => {
384
- return toContent(await fetchApi(`/tasks${qs({ status, date_type, date, limit, offset })}`));
395
+ }, async ({ status, date_type, date, company_id, tag_id, limit, offset }) => {
396
+ return toContent(await fetchApi(`/tasks${qs({ status, date_type, date, company_id, tag_id, limit, offset })}`));
397
+ });
398
+ server.registerTool("get_task", {
399
+ description: "Get a single task by ID, with its tags, linked contacts (contact_ids), linked companies (company_ids) and linked notes (note_ids). Use it when you already know which task you need instead of listing everything.",
400
+ inputSchema: {
401
+ id: z.string().uuid().describe("Task UUID"),
402
+ },
403
+ annotations: { title: "Get task", readOnlyHint: true, openWorldHint: false },
404
+ }, async ({ id }) => {
405
+ return toContent(await fetchApi(`/tasks/${id}`));
385
406
  });
386
407
  server.registerTool("create_task", {
387
408
  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.",
@@ -420,6 +441,10 @@ export function registerAllTools(server, fetchApi) {
420
441
  .array(z.string().uuid())
421
442
  .optional()
422
443
  .describe("Array of contact UUIDs to associate"),
444
+ company_ids: z
445
+ .array(z.string().uuid())
446
+ .optional()
447
+ .describe("Array of company UUIDs to associate (companies are separate from contacts)"),
423
448
  tag_ids: z
424
449
  .array(z.string().uuid())
425
450
  .optional()
@@ -459,6 +484,10 @@ export function registerAllTools(server, fetchApi) {
459
484
  .array(z.string().uuid())
460
485
  .optional()
461
486
  .describe("Replace associated contacts"),
487
+ company_ids: z
488
+ .array(z.string().uuid())
489
+ .optional()
490
+ .describe("Replace associated companies (send [] to unlink all). Contacts are unaffected."),
462
491
  tag_ids: z
463
492
  .array(z.string().uuid())
464
493
  .optional()
@@ -513,19 +542,21 @@ export function registerAllTools(server, fetchApi) {
513
542
  // QUICK NOTES
514
543
  // ===========================================================================
515
544
  server.registerTool("list_notes", {
516
- description: "List notes. QuickNotes (inbox, not yet archived) and Notes (archived, permanent). Filter by pinned or archived status, or by the day(s) a note is linked to (`date`, or `date_from`/`date_to`) — e.g. \"what did I note for tomorrow?\". Each note carries `dates`, the days it is linked to.",
545
+ description: "List notes. QuickNotes (inbox, not yet archived) and Notes (archived, permanent). Filter by pinned or archived status, by tag (page) with `tag_id`, or by the day(s) a note is linked to (`date`, or `date_from`/`date_to`) — e.g. \"what did I note for tomorrow?\". Each note carries `dates`, the days it is linked to.",
517
546
  inputSchema: {
518
547
  pinned: z.boolean().optional().describe("Filter pinned notes only"),
519
548
  archived: z.boolean().optional().describe("Filter by status: true = Notes (archived/permanent), false = QuickNotes (inbox)"),
520
549
  date: z.string().optional().describe("Only notes linked to this day (YYYY-MM-DD)"),
521
550
  date_from: z.string().optional().describe("Only notes linked to a day on or after this date (YYYY-MM-DD)"),
522
551
  date_to: z.string().optional().describe("Only notes linked to a day on or before this date (YYYY-MM-DD)"),
552
+ company_id: z.string().uuid().optional().describe("Only notes linked directly to this company"),
553
+ tag_id: z.string().uuid().optional().describe("Only notes linked to this tag (page)"),
523
554
  limit: z.number().int().positive().optional().describe("Max results (default 20)"),
524
555
  offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
525
556
  },
526
557
  annotations: { title: "List notes", readOnlyHint: true, openWorldHint: false },
527
- }, async ({ pinned, archived, date, date_from, date_to, limit, offset }) => {
528
- return toContent(await fetchApi(`/notes${qs({ pinned, archived, date, date_from, date_to, limit, offset })}`));
558
+ }, async ({ pinned, archived, date, date_from, date_to, company_id, tag_id, limit, offset }) => {
559
+ return toContent(await fetchApi(`/notes${qs({ pinned, archived, date, date_from, date_to, company_id, tag_id, limit, offset })}`));
529
560
  });
530
561
  server.registerTool("get_note", {
531
562
  description: "Get a single note by ID, including its tags, linked contacts, linked tasks, linked notes and the days it is linked to (`dates`). Use this when the user points you at one specific note (e.g. gives you its URL — the UUID is the last path segment) instead of listing everything.",
@@ -545,6 +576,10 @@ export function registerAllTools(server, fetchApi) {
545
576
  .array(z.string().uuid())
546
577
  .optional()
547
578
  .describe("Array of contact UUIDs to associate"),
579
+ company_ids: z
580
+ .array(z.string().uuid())
581
+ .optional()
582
+ .describe("Array of company UUIDs to associate (companies are separate from contacts)"),
548
583
  dates: z
549
584
  .array(z.string())
550
585
  .optional()
@@ -563,6 +598,10 @@ export function registerAllTools(server, fetchApi) {
563
598
  .array(z.string().uuid())
564
599
  .optional()
565
600
  .describe("Replace associated contacts"),
601
+ company_ids: z
602
+ .array(z.string().uuid())
603
+ .optional()
604
+ .describe("Replace associated companies (send [] to unlink all). Contacts are unaffected."),
566
605
  tag_ids: z
567
606
  .array(z.string().uuid())
568
607
  .optional()
@@ -588,7 +627,7 @@ export function registerAllTools(server, fetchApi) {
588
627
  return toContent(await fetchApi(`/notes/${id}${query}`, "DELETE"));
589
628
  });
590
629
  server.registerTool("pin_note", {
591
- description: "Pin a QuickNote or Note so it appears at the top of the Inbox.",
630
+ description: "Pin a QuickNote or Note so it stays in view. A pin is a POST-IT: a short reference the user wants always at hand (gate code, hotel room number, Wi-Fi password). Do not pin a note just because it is important — meeting prep is linked to its day, project material to a tag, actions become tasks. Pin only when the user asks or the note is clearly such a post-it.",
592
631
  inputSchema: {
593
632
  id: z.string().uuid().describe("Note UUID"),
594
633
  },
@@ -773,14 +812,17 @@ export function registerAllTools(server, fetchApi) {
773
812
  return toContent(await fetchApi("/tags", "POST", body));
774
813
  });
775
814
  server.registerTool("update_tag", {
776
- description: "Update an existing tag. Only send the fields you want to change. Supports name, description, color, icon, view mode, and favorite status.",
815
+ description: "Update an existing tag. Only send the fields you want to change. Supports name, description, color, icon, favorite status, and the order of the page's tasks and sections (tasks_order).",
777
816
  inputSchema: {
778
817
  id: z.string().uuid().describe("Tag UUID"),
779
818
  name: z.string().optional().describe("Tag name"),
780
819
  description: z.string().optional().describe("Tag description"),
781
820
  color: z.string().optional().describe("Tag color (e.g. 'blue', 'red', 'green')"),
782
821
  icon: z.string().optional().describe("Tag icon (emoji or icon name)"),
783
- view_mode: z.string().optional().describe("View mode for the tag page"),
822
+ tasks_order: z
823
+ .array(z.string())
824
+ .optional()
825
+ .describe("Full display order of the page's tasks: task ids and \"h:<header_id>\" entries for sections, top to bottom. Read the current one with get_tag, insert or move entries, send the whole array back. Tasks listed after a section belong to it."),
784
826
  is_favorite: z.boolean().optional().describe("Whether the tag is a favorite"),
785
827
  },
786
828
  annotations: { title: "Update tag", destructiveHint: false, idempotentHint: true, openWorldHint: false },
@@ -845,7 +887,7 @@ export function registerAllTools(server, fetchApi) {
845
887
  return toContent(await fetchApi(`/task-headers/${id}`));
846
888
  });
847
889
  server.registerTool("create_task_header", {
848
- description: "Create a new task header (section separator). Add it to a tag's items_order to position it.",
890
+ description: "Create a new task header (section separator). To show it on a tag page, insert \"h:<header_id>\" into that tag's tasks_order (get_tag, then update_tag) where the section should start: the tasks listed after it belong to that section.",
849
891
  inputSchema: {
850
892
  name: z.string().describe("Header name"),
851
893
  description: z.string().optional().describe("Optional description below the header"),
@@ -939,6 +981,66 @@ export function registerAllTools(server, fetchApi) {
939
981
  }, async ({ task_id, contact_id }) => {
940
982
  return toContent(await fetchApi(`/tasks/${task_id}/contacts/${contact_id}`, "DELETE"));
941
983
  });
984
+ server.registerTool("link_entry_company", {
985
+ description: "Link a company to an entry (the company becomes a participant of that interaction). Idempotent. Companies are separate records from contacts: use this (or company_ids) for an organization, link_entry_contact for a person.",
986
+ inputSchema: {
987
+ entry_id: z.string().uuid().describe("Entry UUID"),
988
+ company_id: z.string().uuid().describe("Company UUID"),
989
+ },
990
+ annotations: { title: "Link entry company", destructiveHint: false, idempotentHint: true, openWorldHint: false },
991
+ }, async ({ entry_id, company_id }) => {
992
+ return toContent(await fetchApi(`/entries/${entry_id}/companies/${company_id}`, "POST"));
993
+ });
994
+ server.registerTool("unlink_entry_company", {
995
+ description: "Remove the link between a company and an entry.",
996
+ inputSchema: {
997
+ entry_id: z.string().uuid().describe("Entry UUID"),
998
+ company_id: z.string().uuid().describe("Company UUID"),
999
+ },
1000
+ annotations: { title: "Unlink entry company", destructiveHint: true, idempotentHint: true, openWorldHint: false },
1001
+ }, async ({ entry_id, company_id }) => {
1002
+ return toContent(await fetchApi(`/entries/${entry_id}/companies/${company_id}`, "DELETE"));
1003
+ });
1004
+ server.registerTool("link_task_company", {
1005
+ description: "Link a company to a task. Idempotent. Companies are separate records from contacts: use this (or company_ids) for an organization, link_task_contact for a person.",
1006
+ inputSchema: {
1007
+ task_id: z.string().uuid().describe("Task UUID"),
1008
+ company_id: z.string().uuid().describe("Company UUID"),
1009
+ },
1010
+ annotations: { title: "Link task company", destructiveHint: false, idempotentHint: true, openWorldHint: false },
1011
+ }, async ({ task_id, company_id }) => {
1012
+ return toContent(await fetchApi(`/tasks/${task_id}/companies/${company_id}`, "POST"));
1013
+ });
1014
+ server.registerTool("unlink_task_company", {
1015
+ description: "Remove the link between a company and a task.",
1016
+ inputSchema: {
1017
+ task_id: z.string().uuid().describe("Task UUID"),
1018
+ company_id: z.string().uuid().describe("Company UUID"),
1019
+ },
1020
+ annotations: { title: "Unlink task company", destructiveHint: true, idempotentHint: true, openWorldHint: false },
1021
+ }, async ({ task_id, company_id }) => {
1022
+ return toContent(await fetchApi(`/tasks/${task_id}/companies/${company_id}`, "DELETE"));
1023
+ });
1024
+ server.registerTool("link_note_company", {
1025
+ description: "Link a company to a note. Idempotent. Companies are separate records from contacts: use this (or company_ids) for an organization, link_note_contact for a person.",
1026
+ inputSchema: {
1027
+ note_id: z.string().uuid().describe("Note UUID"),
1028
+ company_id: z.string().uuid().describe("Company UUID"),
1029
+ },
1030
+ annotations: { title: "Link note company", destructiveHint: false, idempotentHint: true, openWorldHint: false },
1031
+ }, async ({ note_id, company_id }) => {
1032
+ return toContent(await fetchApi(`/notes/${note_id}/companies/${company_id}`, "POST"));
1033
+ });
1034
+ server.registerTool("unlink_note_company", {
1035
+ description: "Remove the link between a company and a note.",
1036
+ inputSchema: {
1037
+ note_id: z.string().uuid().describe("Note UUID"),
1038
+ company_id: z.string().uuid().describe("Company UUID"),
1039
+ },
1040
+ annotations: { title: "Unlink note company", destructiveHint: true, idempotentHint: true, openWorldHint: false },
1041
+ }, async ({ note_id, company_id }) => {
1042
+ return toContent(await fetchApi(`/notes/${note_id}/companies/${company_id}`, "DELETE"));
1043
+ });
942
1044
  server.registerTool("link_task_note", {
943
1045
  description: "Link an existing note to a task (non-destructive, N-N). The note stays in the inbox/notes list and the task keeps a live link back to it — use this instead of deleting a note after creating a task from it. The note shows its linked tasks; the task shows the source note.",
944
1046
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keepsake-mcp",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "MCP server for Keepsake personal CRM \u2014 connect your AI agent to your contacts, tasks, notes, and more",
5
5
  "type": "module",
6
6
  "bin": {