keepsake-mcp 1.5.0 → 1.7.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/README.md CHANGED
@@ -100,7 +100,7 @@ Add to `.cursor/mcp.json` in your project:
100
100
  }
101
101
  ```
102
102
 
103
- ## Available tools (62)
103
+ ## Available tools (67)
104
104
 
105
105
  ### Contacts
106
106
  | Tool | Description |
@@ -148,6 +148,7 @@ Add to `.cursor/mcp.json` in your project:
148
148
  | Tool | Description |
149
149
  |------|-------------|
150
150
  | `list_notes` | List notes (filter by pinned/archived) |
151
+ | `get_note` | Get one note by ID with its tags, contacts, tasks and linked notes |
151
152
  | `create_note` | Create a note — supports `#tag#` and `[[tag]]` syntax |
152
153
  | `update_note` | Update note content |
153
154
  | `delete_note` | Soft-delete (or permanent) |
@@ -155,6 +156,17 @@ Add to `.cursor/mcp.json` in your project:
155
156
  | `archive_note` | Archive a note |
156
157
  | `restore_note` | Restore a deleted/archived note |
157
158
 
159
+ ### Note comments (marginalia)
160
+
161
+ Material kept *alongside* a note without entering its text — an idea, a reference, an excerpt pasted to rewrite a passage later. Anchored to a passage by quoting it, or to the whole note. Never published, and temporary by design: anything worth keeping becomes a note or a linked task.
162
+
163
+ | Tool | Description |
164
+ |------|-------------|
165
+ | `list_note_comments` | List the marginalia attached to a note |
166
+ | `create_note_comment` | Attach a marginalia to a passage (pass `quote`) or to the whole note |
167
+ | `update_note_comment` | Edit the content of a marginalia |
168
+ | `delete_note_comment` | Permanently delete a marginalia |
169
+
158
170
  ### Daily Journal
159
171
  | Tool | Description |
160
172
  |------|-------------|
package/build/tools.js CHANGED
@@ -380,6 +380,15 @@ export function registerAllTools(server, fetchApi) {
380
380
  }, async ({ pinned, archived, limit, offset }) => {
381
381
  return toContent(await fetchApi(`/notes${qs({ pinned, archived, limit, offset })}`));
382
382
  });
383
+ server.registerTool("get_note", {
384
+ description: "Get a single note by ID, including its tags, linked contacts, linked tasks and linked notes. 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.",
385
+ inputSchema: {
386
+ id: z.string().uuid().describe("Note UUID (last segment of the note URL)"),
387
+ },
388
+ annotations: { title: "Get note", readOnlyHint: true, openWorldHint: false },
389
+ }, async ({ id }) => {
390
+ return toContent(await fetchApi(`/notes/${id}`));
391
+ });
383
392
  server.registerTool("create_note", {
384
393
  description: "Create a new QuickNote in the Inbox. QuickNotes are temporary captures — use archive_note to transform one into a permanent Note.\n\nContent supports #tag# and [[tag]] for automatic tag linking.",
385
394
  inputSchema: {
@@ -814,6 +823,68 @@ export function registerAllTools(server, fetchApi) {
814
823
  return toContent(await fetchApi(`/search${qs({ q, type, limit })}`));
815
824
  });
816
825
  // ---------------------------------------------------------------------------
826
+ // Note comments (marginalia)
827
+ // ---------------------------------------------------------------------------
828
+ server.registerTool("list_note_comments", {
829
+ description: "List the marginalia attached to a note. A marginalia is working material the user keeps ALONGSIDE a note without it entering the text: an idea, a reference, a link, an excerpt pasted to rewrite a passage later. They are never published, and they are meant to be TEMPORARY — anything worth keeping becomes a note or a linked task.\n\nEach one is either attached to a specific passage (`quote` is set) or to the whole note (`quote` is null).",
830
+ inputSchema: {
831
+ note_id: z.string().uuid().describe("Note UUID"),
832
+ },
833
+ annotations: { title: "List note comments", readOnlyHint: true, openWorldHint: false },
834
+ }, async ({ note_id }) => {
835
+ return toContent(await fetchApi(`/notes/${note_id}/comments`));
836
+ });
837
+ server.registerTool("create_note_comment", {
838
+ description: "Attach a marginalia to a note — material that must stay OUT of the note text.\n\nTo attach it to a passage, pass `quote` with that passage copied VERBATIM from the note content; the server locates it and stores the surrounding context so the comment survives later edits. Omit `quote` to comment on the note as a whole. If the quote is not found verbatim the call is refused rather than attached to the wrong place.\n\nDo not use this to suggest edits to the text — use update_note for that. And keep the number of comments low: a note peppered with them is a note the user abandons.",
839
+ inputSchema: {
840
+ note_id: z.string().uuid().describe("Note UUID"),
841
+ body: z.string().describe("The material itself (markdown, any length)"),
842
+ quote: z
843
+ .string()
844
+ .optional()
845
+ .describe("Passage to attach to, copied verbatim from the note content. Omit for a note-wide comment."),
846
+ },
847
+ annotations: {
848
+ title: "Create note comment",
849
+ destructiveHint: false,
850
+ idempotentHint: false,
851
+ openWorldHint: false,
852
+ },
853
+ }, async ({ note_id, ...params }) => {
854
+ return toContent(await fetchApi(`/notes/${note_id}/comments`, "POST", params));
855
+ });
856
+ server.registerTool("update_note_comment", {
857
+ description: "Edit the content of a marginalia. There is no intermediate state: a marginalia exists, or it is deleted. A remark that should outlive the note's revision becomes a note or a linked task instead.",
858
+ inputSchema: {
859
+ note_id: z.string().uuid().describe("Note UUID"),
860
+ comment_id: z.string().uuid().describe("Comment UUID"),
861
+ body: z.string().describe("New content (markdown)"),
862
+ },
863
+ annotations: {
864
+ title: "Update note comment",
865
+ destructiveHint: false,
866
+ idempotentHint: true,
867
+ openWorldHint: false,
868
+ },
869
+ }, async ({ note_id, comment_id, ...params }) => {
870
+ return toContent(await fetchApi(`/notes/${note_id}/comments/${comment_id}`, "PATCH", params));
871
+ });
872
+ server.registerTool("delete_note_comment", {
873
+ description: "Delete a marginalia. Marginalia are temporary by design, so this is the normal way to retire one — there is no recoverable middle state.",
874
+ inputSchema: {
875
+ note_id: z.string().uuid().describe("Note UUID"),
876
+ comment_id: z.string().uuid().describe("Comment UUID"),
877
+ },
878
+ annotations: {
879
+ title: "Delete note comment",
880
+ destructiveHint: true,
881
+ idempotentHint: true,
882
+ openWorldHint: false,
883
+ },
884
+ }, async ({ note_id, comment_id }) => {
885
+ return toContent(await fetchApi(`/notes/${note_id}/comments/${comment_id}`, "DELETE"));
886
+ });
887
+ // ---------------------------------------------------------------------------
817
888
  // Agent instructions
818
889
  // ---------------------------------------------------------------------------
819
890
  server.registerTool("get_agent_instructions", {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "keepsake-mcp",
3
- "version": "1.5.0",
4
- "description": "MCP server for Keepsake personal CRM — connect your AI agent to your contacts, tasks, notes, and more",
3
+ "version": "1.7.0",
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": {
7
7
  "keepsake-mcp": "build/index.js"