keepsake-mcp 1.8.0 → 1.9.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/README.md +14 -6
- package/build/tools.js +110 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for [Keepsake](https://keepsake.place) — the personal CRM that helps you nurture your relationships.
|
|
4
4
|
|
|
5
|
-
Connect your AI assistant (Claude, Cursor, or any MCP-compatible client) to your Keepsake data: contacts, interactions, tasks, notes, daily
|
|
5
|
+
Connect your AI assistant (Claude, Cursor, or any MCP-compatible client) to your Keepsake data: contacts, interactions, tasks, notes, daily intentions, companies, and tags.
|
|
6
6
|
|
|
7
7
|
## Why
|
|
8
8
|
|
|
@@ -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 (
|
|
117
|
+
## Available tools (71)
|
|
118
118
|
|
|
119
119
|
### Contacts
|
|
120
120
|
| Tool | Description |
|
|
@@ -181,12 +181,20 @@ Material kept *alongside* a note without entering its text — an idea, a refere
|
|
|
181
181
|
| `update_note_comment` | Edit the content of a marginalia |
|
|
182
182
|
| `delete_note_comment` | Permanently delete a marginalia |
|
|
183
183
|
|
|
184
|
-
###
|
|
184
|
+
### Days (intention or question of the day)
|
|
185
185
|
| Tool | Description |
|
|
186
186
|
|------|-------------|
|
|
187
|
-
| `list_days` | List
|
|
188
|
-
| `get_day` | Get a
|
|
189
|
-
| `update_day` |
|
|
187
|
+
| `list_days` | List days with their intention or question of the day, by date range |
|
|
188
|
+
| `get_day` | Get a day and its intention or question (field `note`) |
|
|
189
|
+
| `update_day` | Set a day's intention or question — one short line, not a journal (upsert) |
|
|
190
|
+
|
|
191
|
+
### Day blocks (Day-view timeline)
|
|
192
|
+
| Tool | Description |
|
|
193
|
+
|------|-------------|
|
|
194
|
+
| `list_day_blocks` | List a day's time blocks, in timeline order |
|
|
195
|
+
| `create_day_block` | Create a block, auto-placed first-fit (or pinned via anchor_time) |
|
|
196
|
+
| `update_day_block` | Update a block (title, duration, anchor, note, done) |
|
|
197
|
+
| `delete_day_block` | Delete a block and prune its timeline ref |
|
|
190
198
|
|
|
191
199
|
### Tags
|
|
192
200
|
| Tool | Description |
|
package/build/tools.js
CHANGED
|
@@ -7,8 +7,14 @@ export function toContent(result) {
|
|
|
7
7
|
: result.error.message || JSON.stringify(result.error);
|
|
8
8
|
return { content: [{ type: "text", text: `Error: ${msg}` }] };
|
|
9
9
|
}
|
|
10
|
+
// List endpoints ship pagination alongside the rows (meta.total / limit /
|
|
11
|
+
// offset). Dropping it forced agents to binary-search offsets just to count
|
|
12
|
+
// an inbox — keep data and meta together whenever meta is present.
|
|
13
|
+
const payload = result.meta !== undefined
|
|
14
|
+
? { data: result.data ?? [], meta: result.meta }
|
|
15
|
+
: (result.data ?? result);
|
|
10
16
|
return {
|
|
11
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
17
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
12
18
|
};
|
|
13
19
|
}
|
|
14
20
|
/** Build query string from optional params, skipping undefined values. */
|
|
@@ -358,6 +364,18 @@ export function registerAllTools(server, fetchApi) {
|
|
|
358
364
|
.positive()
|
|
359
365
|
.optional()
|
|
360
366
|
.describe("Recurrence interval (e.g., every N days)"),
|
|
367
|
+
start_time: z
|
|
368
|
+
.string()
|
|
369
|
+
.regex(/^([01]\d|2[0-3]):[0-5]\d$/)
|
|
370
|
+
.optional()
|
|
371
|
+
.describe("Wall-clock start time HH:MM (24h). A task of the day WITH a time appears anchored on the Day-view timeline; without one it stays in the day's task list."),
|
|
372
|
+
duration_minutes: z
|
|
373
|
+
.number()
|
|
374
|
+
.int()
|
|
375
|
+
.min(1)
|
|
376
|
+
.max(1440)
|
|
377
|
+
.optional()
|
|
378
|
+
.describe("Estimated duration in minutes (Day-view timeline shows 15 by default when a start time is set)"),
|
|
361
379
|
contact_ids: z
|
|
362
380
|
.array(z.string().uuid())
|
|
363
381
|
.optional()
|
|
@@ -383,6 +401,20 @@ export function registerAllTools(server, fetchApi) {
|
|
|
383
401
|
.optional()
|
|
384
402
|
.describe("Date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
|
|
385
403
|
priority: z.enum(["low", "medium", "high"]).optional().describe("Priority level"),
|
|
404
|
+
start_time: z
|
|
405
|
+
.string()
|
|
406
|
+
.regex(/^([01]\d|2[0-3]):[0-5]\d$/)
|
|
407
|
+
.nullable()
|
|
408
|
+
.optional()
|
|
409
|
+
.describe("Wall-clock start time HH:MM (24h) — anchors the task on the Day-view timeline. Pass null to clear it (unschedule)."),
|
|
410
|
+
duration_minutes: z
|
|
411
|
+
.number()
|
|
412
|
+
.int()
|
|
413
|
+
.min(1)
|
|
414
|
+
.max(1440)
|
|
415
|
+
.nullable()
|
|
416
|
+
.optional()
|
|
417
|
+
.describe("Estimated duration in minutes. Pass null to clear."),
|
|
386
418
|
contact_ids: z
|
|
387
419
|
.array(z.string().uuid())
|
|
388
420
|
.optional()
|
|
@@ -535,7 +567,7 @@ export function registerAllTools(server, fetchApi) {
|
|
|
535
567
|
// DAYS (Daily Summaries)
|
|
536
568
|
// ===========================================================================
|
|
537
569
|
server.registerTool("list_days", {
|
|
538
|
-
description: "List
|
|
570
|
+
description: "List days with their intention or question of the day (field `note`). Filter by date range.",
|
|
539
571
|
inputSchema: {
|
|
540
572
|
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
541
573
|
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
@@ -547,7 +579,7 @@ export function registerAllTools(server, fetchApi) {
|
|
|
547
579
|
return toContent(await fetchApi(`/days${qs({ from, to, limit, offset })}`));
|
|
548
580
|
});
|
|
549
581
|
server.registerTool("get_day", {
|
|
550
|
-
description: "Get a specific day
|
|
582
|
+
description: "Get a specific day by date, including its intention or question of the day (field `note`).",
|
|
551
583
|
inputSchema: {
|
|
552
584
|
date: z.string().describe("Date (YYYY-MM-DD)"),
|
|
553
585
|
},
|
|
@@ -556,16 +588,89 @@ export function registerAllTools(server, fetchApi) {
|
|
|
556
588
|
return toContent(await fetchApi(`/days/${date}`));
|
|
557
589
|
});
|
|
558
590
|
server.registerTool("update_day", {
|
|
559
|
-
description: "Create or update a
|
|
591
|
+
description: "Create or update a day's intention or question of the day (upsert on the date). The `note` field is the intention or question of the day — one short line at the top of the Today view (a mantra, an intention, a single priority, or a question to keep in mind). Not a journal: never write a summary of the day here.",
|
|
560
592
|
inputSchema: {
|
|
561
593
|
date: z.string().describe("Date (YYYY-MM-DD)"),
|
|
562
|
-
note: z.string().describe("
|
|
594
|
+
note: z.string().describe("The intention or question of the day: one short line (mantra, intention, single priority, or a question to keep in mind). Not a journal summary."),
|
|
563
595
|
},
|
|
564
596
|
annotations: { title: "Update day", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
565
597
|
}, async (params) => {
|
|
566
598
|
return toContent(await fetchApi("/days", "POST", params));
|
|
567
599
|
});
|
|
568
600
|
// ===========================================================================
|
|
601
|
+
// DAY BLOCKS (Day-view timeline)
|
|
602
|
+
// ===========================================================================
|
|
603
|
+
server.registerTool("list_day_blocks", {
|
|
604
|
+
description: "List the time blocks of a day's timeline (Day view), in timeline order. A block is either an activity (type 'block') or a task bucket (type 'tasks' — groups the day's untimed tasks). The meta carries the day's raw timeline_order: \"b:<uuid>\" refs are blocks, bare uuids are tasks inside a bucket's run.",
|
|
605
|
+
inputSchema: {
|
|
606
|
+
date: z.string().describe("Date (YYYY-MM-DD)"),
|
|
607
|
+
},
|
|
608
|
+
annotations: { title: "List day blocks", readOnlyHint: true, openWorldHint: false },
|
|
609
|
+
}, async ({ date }) => {
|
|
610
|
+
return toContent(await fetchApi(`/day-blocks${qs({ date })}`));
|
|
611
|
+
});
|
|
612
|
+
server.registerTool("create_day_block", {
|
|
613
|
+
description: "Create a time block on a day's timeline. The block is auto-placed in the earliest free gap large enough for it (same first-fit engine as the app); pass anchor_time to pin it at a fixed hour instead. Its \"b:<id>\" ref is inserted into the day's timeline_order for you — never write timeline_order by hand.",
|
|
614
|
+
inputSchema: {
|
|
615
|
+
date: z.string().describe("Date (YYYY-MM-DD)"),
|
|
616
|
+
title: z.string().optional().describe("Block label (e.g. \"Deep work\", \"Lunch\")"),
|
|
617
|
+
type: z
|
|
618
|
+
.enum(["block", "tasks"])
|
|
619
|
+
.optional()
|
|
620
|
+
.describe("'block' = activity (default). 'tasks' = task bucket: a window that gathers the day's untimed tasks."),
|
|
621
|
+
duration_minutes: z
|
|
622
|
+
.number()
|
|
623
|
+
.int()
|
|
624
|
+
.min(1)
|
|
625
|
+
.max(1440)
|
|
626
|
+
.optional()
|
|
627
|
+
.describe("Duration in minutes (default 30 for an activity, 60 for a bucket)"),
|
|
628
|
+
anchor_time: z
|
|
629
|
+
.string()
|
|
630
|
+
.regex(/^([01]\d|2[0-3]):[0-5]\d$/)
|
|
631
|
+
.optional()
|
|
632
|
+
.describe("Pin the block at a fixed wall-clock hour HH:MM (it becomes a wall other blocks flow around). Omit for automatic first-fit placement."),
|
|
633
|
+
note: z.string().optional().describe("Free note attached to the block"),
|
|
634
|
+
},
|
|
635
|
+
annotations: { title: "Create day block", destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
636
|
+
}, async (params) => {
|
|
637
|
+
return toContent(await fetchApi("/day-blocks", "POST", params));
|
|
638
|
+
});
|
|
639
|
+
server.registerTool("update_day_block", {
|
|
640
|
+
description: "Update a time block (title, duration, anchor, note, done). Only send fields you want to change. The block keeps its position in the timeline order.",
|
|
641
|
+
inputSchema: {
|
|
642
|
+
id: z.string().uuid().describe("Block UUID"),
|
|
643
|
+
title: z.string().nullable().optional().describe("Block label"),
|
|
644
|
+
duration_minutes: z
|
|
645
|
+
.number()
|
|
646
|
+
.int()
|
|
647
|
+
.min(1)
|
|
648
|
+
.max(1440)
|
|
649
|
+
.optional()
|
|
650
|
+
.describe("Duration in minutes"),
|
|
651
|
+
anchor_time: z
|
|
652
|
+
.string()
|
|
653
|
+
.regex(/^([01]\d|2[0-3]):[0-5]\d$/)
|
|
654
|
+
.nullable()
|
|
655
|
+
.optional()
|
|
656
|
+
.describe("Fixed wall-clock hour HH:MM. Pass null to unpin (the block flows with the rest again)."),
|
|
657
|
+
note: z.string().nullable().optional().describe("Free note. Pass null to clear."),
|
|
658
|
+
done: z.boolean().optional().describe("Mark the block done (true) or not done (false)"),
|
|
659
|
+
},
|
|
660
|
+
annotations: { title: "Update day block", destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
661
|
+
}, async ({ id, ...body }) => {
|
|
662
|
+
return toContent(await fetchApi(`/day-blocks/${id}`, "PATCH", body));
|
|
663
|
+
});
|
|
664
|
+
server.registerTool("delete_day_block", {
|
|
665
|
+
description: "Delete a time block. Its ref is pruned from the day's timeline_order; for a task bucket, the tasks themselves are untouched (they fall back to the day's computed bucket).",
|
|
666
|
+
inputSchema: {
|
|
667
|
+
id: z.string().uuid().describe("Block UUID"),
|
|
668
|
+
},
|
|
669
|
+
annotations: { title: "Delete day block", destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
670
|
+
}, async ({ id }) => {
|
|
671
|
+
return toContent(await fetchApi(`/day-blocks/${id}`, "DELETE"));
|
|
672
|
+
});
|
|
673
|
+
// ===========================================================================
|
|
569
674
|
// TAGS
|
|
570
675
|
// ===========================================================================
|
|
571
676
|
server.registerTool("list_tags", {
|