ocean-brain 0.8.1 → 0.8.2

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 (78) hide show
  1. package/README.md +113 -0
  2. package/dist/mcp.js +230 -291
  3. package/package.json +5 -5
  4. package/server/client/dist/assets/ArrowRight.es-g2YZTklZ.js +1 -0
  5. package/server/client/dist/assets/Calendar-BP6HTuSn.js +25 -0
  6. package/server/client/dist/assets/Callout-B_IPSF_5.js +1 -0
  7. package/server/client/dist/assets/Copy.es-CNxe4ujr.js +1 -0
  8. package/server/client/dist/assets/Graph-D9GeO71k.js +1 -0
  9. package/server/client/dist/assets/Graph-Xha3fwhp.js +46 -0
  10. package/server/client/dist/assets/Image-BLEjLUqd.js +1 -0
  11. package/server/client/dist/assets/Image.es-DnON8cv2.js +1 -0
  12. package/server/client/dist/assets/Note-DjrCYtat.js +24 -0
  13. package/server/client/dist/assets/NotePropertiesPanel-2DgXEM4B.js +2 -0
  14. package/server/client/dist/assets/Plus.es-CQ8wn2is.js +1 -0
  15. package/server/client/dist/assets/Reminders-B4l3KdlK.js +1 -0
  16. package/server/client/dist/assets/Search-BN5sf7bE.js +1 -0
  17. package/server/client/dist/assets/SurfaceCard-BZm73nyS.js +1 -0
  18. package/server/client/dist/assets/Tag-oSnmViVp.js +1 -0
  19. package/server/client/dist/assets/TagNotes-BDTCcJcL.js +1 -0
  20. package/server/client/dist/assets/Trash.es-0aPpA-3S.js +1 -0
  21. package/server/client/dist/assets/ViewNotes-CiZtGJNE.js +1 -0
  22. package/server/client/dist/assets/{ViewSectionDialog-brTrsF6G.js → ViewSectionDialog-Ctl_694Y.js} +17 -17
  23. package/server/client/dist/assets/Views-D-zlatYD.js +1 -0
  24. package/server/client/dist/assets/app-DmqIOMWF.css +1 -0
  25. package/server/client/dist/assets/app-cTRcKMGc.js +26 -0
  26. package/server/client/dist/assets/floating-ui.react-dom-wWLZ1CdM.js +1 -0
  27. package/server/client/dist/assets/{image.api-CYsjXiMr.js → image.api-CVuuvTBk.js} +1 -1
  28. package/server/client/dist/assets/index-DKJChPr4.js +1 -0
  29. package/server/client/dist/assets/manage-image-BZjaxp-f.js +1 -0
  30. package/server/client/dist/assets/manage-image-detail-DPv0FQ2m.js +1 -0
  31. package/server/client/dist/assets/{mcp-Dd98N9Hq.js → mcp-CT_9IV2c.js} +2 -2
  32. package/server/client/dist/assets/mcp-admin-adapter-xAuyZc_M.js +1 -0
  33. package/server/client/dist/assets/note-core-Buwzyxi7.js +13 -0
  34. package/server/client/dist/assets/note-runtime-CPDVOFEo.js +168 -0
  35. package/server/client/dist/assets/note-ui-CHI-tzj-.js +2 -0
  36. package/server/client/dist/assets/{index-DPI6YoDt.css → note-ui-DL8owV-U.css} +1 -1
  37. package/server/client/dist/assets/note.api-D_00ZzJC.js +356 -0
  38. package/server/client/dist/assets/placeholder-DbdGiYGL.js +34 -0
  39. package/server/client/dist/assets/preload-helper-BXl3LOEh.js +1 -0
  40. package/server/client/dist/assets/properties-D447wsWk.js +1 -0
  41. package/server/client/dist/assets/route-preload.js +2 -0
  42. package/server/client/dist/assets/{tag.api-CwmC8crB.js → tag.api-CX0SE06J.js} +1 -1
  43. package/server/client/dist/assets/time-B3pt8vt4.js +9 -0
  44. package/server/client/dist/assets/trash-8sH7EZgc.js +1 -0
  45. package/server/client/dist/assets/url-BSnWpmJx.js +1 -0
  46. package/server/client/dist/assets/{useReminderMutate-DuVKxpBh.js → useReminderMutate-CLK1tHvT.js} +4 -4
  47. package/server/client/dist/index.html +10 -5
  48. package/server/dist/app.js +1 -1
  49. package/server/dist/modules/blocknote.js +262 -75
  50. package/server/dist/modules/error-handler.js +8 -0
  51. package/server/client/dist/assets/ArrowRight.es-CbA8ey6a.js +0 -1
  52. package/server/client/dist/assets/Calendar-CgWgr1BV.js +0 -25
  53. package/server/client/dist/assets/Callout-DKruIT0f.js +0 -1
  54. package/server/client/dist/assets/Copy.es-DFbgpaNJ.js +0 -1
  55. package/server/client/dist/assets/Graph-kCltGRlY.js +0 -1
  56. package/server/client/dist/assets/Image-7W0DZwMo.js +0 -1
  57. package/server/client/dist/assets/Image.es-5pXLx9TU.js +0 -1
  58. package/server/client/dist/assets/Note-BW6Np_5g.js +0 -20
  59. package/server/client/dist/assets/Plus.es-B0sg64VJ.js +0 -1
  60. package/server/client/dist/assets/Reminders-C10fHQcl.js +0 -1
  61. package/server/client/dist/assets/Search-tf2nV4v0.js +0 -1
  62. package/server/client/dist/assets/SurfaceCard-pydtvHfu.js +0 -1
  63. package/server/client/dist/assets/Tag-uCoYbISy.js +0 -1
  64. package/server/client/dist/assets/TagNotes-CqOcQiRw.js +0 -1
  65. package/server/client/dist/assets/Trash.es-w32Goxfa.js +0 -1
  66. package/server/client/dist/assets/ViewNotes-slPXXoOY.js +0 -1
  67. package/server/client/dist/assets/Views-B_wsb-XJ.js +0 -1
  68. package/server/client/dist/assets/graph-vendor-BAF95C0g.js +0 -46
  69. package/server/client/dist/assets/index-BR05aSXr.js +0 -342
  70. package/server/client/dist/assets/index-D83QbOcw.js +0 -1
  71. package/server/client/dist/assets/manage-image-BYIhWu56.js +0 -1
  72. package/server/client/dist/assets/manage-image-detail-6KgmQvbN.js +0 -1
  73. package/server/client/dist/assets/mcp-admin-adapter-DOgBsYpl.js +0 -1
  74. package/server/client/dist/assets/note-core-CMGUaW1v.js +0 -176
  75. package/server/client/dist/assets/note-vendor-Hoz9Q4-4.js +0 -54
  76. package/server/client/dist/assets/placeholder-siHGg9aK.js +0 -34
  77. package/server/client/dist/assets/properties-CyVRKgcT.js +0 -1
  78. package/server/client/dist/assets/trash-wojQBK9Z.js +0 -1
package/dist/mcp.js CHANGED
@@ -6,7 +6,7 @@ import path from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
- import { z as z2 } from "zod";
9
+ import { z as z4 } from "zod";
10
10
 
11
11
  // src/mcp-note-output.ts
12
12
  var formatBackReferenceLink = (backReference) => {
@@ -58,65 +58,86 @@ var formatMcpReadNoteOutput = ({
58
58
  };
59
59
 
60
60
  // src/mcp-intent-write-tools.ts
61
+ import { z as z2 } from "zod";
62
+
63
+ // src/mcp-tool-support.ts
61
64
  import { z } from "zod";
62
- var markdownPatchSelectorSchema = z.discriminatedUnion("type", [
63
- z.object({
64
- type: z.literal("exact_text"),
65
- text: z.string(),
66
- before: z.string().optional(),
67
- after: z.string().optional()
65
+ var noteLayoutSchema = z.enum(["narrow", "wide", "full"]);
66
+ var createMcpTextToolResult = (text) => ({
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text
71
+ }
72
+ ]
73
+ });
74
+ var createMcpJsonToolResult = (value) => {
75
+ return createMcpTextToolResult(JSON.stringify(value, null, 2));
76
+ };
77
+
78
+ // src/mcp-intent-write-tools.ts
79
+ var markdownPatchSelectorSchema = z2.discriminatedUnion("type", [
80
+ z2.object({
81
+ type: z2.literal("exact_text"),
82
+ text: z2.string(),
83
+ before: z2.string().optional(),
84
+ after: z2.string().optional()
68
85
  }),
69
- z.object({
70
- type: z.literal("match_candidate"),
71
- text: z.string(),
72
- matchIndex: z.number().int().nonnegative(),
73
- lineStart: z.number().int().positive(),
74
- matchSha256: z.string(),
75
- surroundingHash: z.string(),
76
- positionHint: z.enum(["first", "last"]).optional()
86
+ z2.object({
87
+ type: z2.literal("match_candidate"),
88
+ text: z2.string(),
89
+ matchIndex: z2.number().int().nonnegative(),
90
+ lineStart: z2.number().int().positive(),
91
+ matchSha256: z2.string(),
92
+ surroundingHash: z2.string(),
93
+ positionHint: z2.enum(["first", "last"]).optional()
77
94
  })
78
95
  ]);
79
- var markdownPatchOperationSchema = z.discriminatedUnion("type", [
80
- z.object({
81
- type: z.literal("replace"),
82
- replacement: z.string()
96
+ var markdownPatchOperationSchema = z2.discriminatedUnion("type", [
97
+ z2.object({
98
+ type: z2.literal("replace"),
99
+ replacement: z2.string()
83
100
  }),
84
- z.object({
85
- type: z.literal("insert_before"),
86
- insertion: z.string()
101
+ z2.object({
102
+ type: z2.literal("insert_before"),
103
+ insertion: z2.string()
87
104
  }),
88
- z.object({
89
- type: z.literal("insert_after"),
90
- insertion: z.string()
105
+ z2.object({
106
+ type: z2.literal("insert_after"),
107
+ insertion: z2.string()
91
108
  })
92
109
  ]);
93
- var markdownAppendPlacementSchema = z.discriminatedUnion("type", [
94
- z.object({
95
- type: z.literal("end")
110
+ var markdownAppendPlacementSchema = z2.discriminatedUnion("type", [
111
+ z2.object({
112
+ type: z2.literal("end")
96
113
  }),
97
- z.object({
98
- type: z.literal("after_heading"),
99
- heading: z.string(),
100
- level: z.number().int().min(1).max(6).optional()
114
+ z2.object({
115
+ type: z2.literal("after_heading"),
116
+ heading: z2.string(),
117
+ level: z2.number().int().min(1).max(6).optional()
101
118
  })
102
119
  ]);
103
- var markdownWritePolicySchema = z.object({
104
- allowNoop: z.boolean().optional(),
105
- preserveTags: z.union([z.boolean(), z.literal("warn")]).optional(),
106
- preserveReferences: z.union([z.boolean(), z.literal("warn")]).optional()
120
+ var markdownWritePolicySchema = z2.object({
121
+ allowNoop: z2.boolean().optional(),
122
+ preserveTags: z2.union([z2.boolean(), z2.literal("warn")]).optional(),
123
+ preserveReferences: z2.union([z2.boolean(), z2.literal("warn")]).optional()
107
124
  }).optional();
125
+ var markdownWriteBaselineFields = {
126
+ expectedUpdatedAt: z2.string().optional().describe("Expected note updatedAt from the read you based this edit on. Required unless baseMarkdownSha256 is provided."),
127
+ baseMarkdownSha256: z2.string().optional().describe("SHA-256 of the current markdown. Optional alternative to expectedUpdatedAt.")
128
+ };
108
129
  var MCP_METADATA_PROPERTY_PATCH_LIMIT = 50;
109
130
  var normalizeMetadataPropertyKey = (key) => key.trim().toLowerCase().replace(/\s+/g, "-");
110
- var metadataPropertyPatchSchema = z.object({
111
- set: z.array(z.object({
112
- key: z.string().min(1).describe("Property key from ocean_brain_list_properties."),
113
- value: z.string().describe("String value: select option.value, date YYYY-MM-DD, boolean true/false, number finite string, url http(s).")
131
+ var metadataPropertyPatchSchema = z2.object({
132
+ set: z2.array(z2.object({
133
+ key: z2.string().min(1).describe("Property key from ocean_brain_list_properties."),
134
+ value: z2.string().describe("String value: select option.value, date YYYY-MM-DD, boolean true/false, number finite string, url http(s).")
114
135
  })).max(MCP_METADATA_PROPERTY_PATCH_LIMIT).optional().describe("Set up to 50 property values. Keys must be unique."),
115
- deleteKeys: z.array(z.string().min(1)).max(MCP_METADATA_PROPERTY_PATCH_LIMIT).optional().describe("Remove up to 50 property values from this note. Keys must be unique and not also in set.")
136
+ deleteKeys: z2.array(z2.string().min(1)).max(MCP_METADATA_PROPERTY_PATCH_LIMIT).optional().describe("Remove up to 50 property values from this note. Keys must be unique and not also in set.")
116
137
  }).superRefine((patch, context) => {
117
138
  if ((patch.set?.length ?? 0) === 0 && (patch.deleteKeys?.length ?? 0) === 0) {
118
139
  context.addIssue({
119
- code: z.ZodIssueCode.custom,
140
+ code: z2.ZodIssueCode.custom,
120
141
  message: "properties must include set or deleteKeys."
121
142
  });
122
143
  return;
@@ -126,7 +147,7 @@ var metadataPropertyPatchSchema = z.object({
126
147
  const key = normalizeMetadataPropertyKey(item.key);
127
148
  if (setKeys.has(key)) {
128
149
  context.addIssue({
129
- code: z.ZodIssueCode.custom,
150
+ code: z2.ZodIssueCode.custom,
130
151
  message: `properties.set contains duplicate key ${key}.`
131
152
  });
132
153
  }
@@ -137,13 +158,13 @@ var metadataPropertyPatchSchema = z.object({
137
158
  const key = normalizeMetadataPropertyKey(rawKey);
138
159
  if (deleteKeys.has(key)) {
139
160
  context.addIssue({
140
- code: z.ZodIssueCode.custom,
161
+ code: z2.ZodIssueCode.custom,
141
162
  message: `properties.deleteKeys contains duplicate key ${key}.`
142
163
  });
143
164
  }
144
165
  if (setKeys.has(key)) {
145
166
  context.addIssue({
146
- code: z.ZodIssueCode.custom,
167
+ code: z2.ZodIssueCode.custom,
147
168
  message: `properties cannot set and delete ${key} in one request.`
148
169
  });
149
170
  }
@@ -161,10 +182,9 @@ var registerIntentWriteTools = (server, {
161
182
  tools.patchNoteMarkdown,
162
183
  "Patch a specific part of a note, like finding text with `rg` and editing only that match. Use for localized edits: fix one sentence, replace one paragraph, insert before/after exact text, or edit one section. Do not use for whole-note rewrites; use ocean_brain_replace_note_markdown instead.",
163
184
  {
164
- id: z.string().describe("Note ID to patch"),
165
- expectedUpdatedAt: z.string().optional().describe("Expected note updatedAt from the read you based this edit on. Required unless baseMarkdownSha256 is provided."),
166
- baseMarkdownSha256: z.string().optional().describe("SHA-256 of the current markdown. Optional alternative to expectedUpdatedAt."),
167
- intent: z.string().describe("Human-readable reason for the patch."),
185
+ id: z2.string().describe("Note ID to patch"),
186
+ ...markdownWriteBaselineFields,
187
+ intent: z2.string().describe("Human-readable reason for the patch."),
168
188
  selector: markdownPatchSelectorSchema.describe("Exact text or previously returned match candidate."),
169
189
  operation: markdownPatchOperationSchema.describe("replace, insert_before, or insert_after."),
170
190
  policy: markdownWritePolicySchema
@@ -181,25 +201,19 @@ var registerIntentWriteTools = (server, {
181
201
  ...policy ? { policy } : {}
182
202
  };
183
203
  const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/patch-markdown", payload);
184
- return {
185
- content: [{
186
- type: "text",
187
- text: JSON.stringify(result, null, 2)
188
- }]
189
- };
204
+ return createMcpJsonToolResult(result);
190
205
  }
191
206
  );
192
207
  server.tool(
193
208
  tools.appendNoteMarkdown,
194
209
  "Append markdown without changing existing content, like `cat addition.md >> note.md`. Use for adding logs, status updates, meeting notes, or new sections at the end or after a heading. Do not use to modify or replace existing body content.",
195
210
  {
196
- id: z.string().describe("Note ID to append to"),
197
- expectedUpdatedAt: z.string().optional().describe("Expected note updatedAt from the read you based this edit on. Required unless baseMarkdownSha256 is provided."),
198
- baseMarkdownSha256: z.string().optional().describe("SHA-256 of the current markdown. Optional alternative to expectedUpdatedAt."),
199
- intent: z.string().describe("Human-readable reason for the append."),
200
- insertion: z.string().describe("Markdown to append. Tags are body tokens such as [@tag] or [#tag]."),
211
+ id: z2.string().describe("Note ID to append to"),
212
+ ...markdownWriteBaselineFields,
213
+ intent: z2.string().describe("Human-readable reason for the append."),
214
+ insertion: z2.string().describe("Markdown to append. Tags are body tokens such as [@tag] or [#tag]."),
201
215
  placement: markdownAppendPlacementSchema.optional().describe("Default is end. after_heading requires one unique matching heading."),
202
- separator: z.enum(["\n\n", "\n"]).optional().describe("Separator inserted around appended markdown. Defaults to a blank line."),
216
+ separator: z2.enum(["\n\n", "\n"]).optional().describe("Separator inserted around appended markdown. Defaults to a blank line."),
203
217
  policy: markdownWritePolicySchema
204
218
  },
205
219
  async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, insertion, placement, separator, policy }) => {
@@ -215,22 +229,17 @@ var registerIntentWriteTools = (server, {
215
229
  ...policy ? { policy } : {}
216
230
  };
217
231
  const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/append-markdown", payload);
218
- return {
219
- content: [{
220
- type: "text",
221
- text: JSON.stringify(result, null, 2)
222
- }]
223
- };
232
+ return createMcpJsonToolResult(result);
224
233
  }
225
234
  );
226
235
  server.tool(
227
236
  tools.updateNoteMetadata,
228
237
  "Update note metadata only: title, layout, or properties. This does not edit the note body. For properties, call ocean_brain_list_properties first. Use existing property key and string value only; do not send valueType. select=option.value, date=YYYY-MM-DD, boolean=true/false, number=finite string, url=http(s). Use deleteKeys to remove values. Definitions are not created.",
229
238
  {
230
- id: z.string().describe("Note ID to update"),
231
- expectedUpdatedAt: z.string().describe("Expected note updatedAt from the read you based this metadata edit on. Required to prevent stale writes."),
232
- title: z.string().optional().describe("New note title"),
233
- layout: z.enum(["narrow", "wide", "full"]).optional().describe("New note layout"),
239
+ id: z2.string().describe("Note ID to update"),
240
+ expectedUpdatedAt: z2.string().describe("Expected note updatedAt from the read you based this metadata edit on. Required to prevent stale writes."),
241
+ title: z2.string().optional().describe("New note title"),
242
+ layout: noteLayoutSchema.optional().describe("New note layout"),
234
243
  properties: metadataPropertyPatchSchema.describe("Patch existing shared property values. Include set and/or deleteKeys; empty patches are rejected.")
235
244
  },
236
245
  async ({ id, expectedUpdatedAt, title, layout, properties }) => {
@@ -243,23 +252,17 @@ var registerIntentWriteTools = (server, {
243
252
  ...properties ? { properties } : {}
244
253
  };
245
254
  const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/metadata", payload);
246
- return {
247
- content: [{
248
- type: "text",
249
- text: JSON.stringify(result, null, 2)
250
- }]
251
- };
255
+ return createMcpJsonToolResult(result);
252
256
  }
253
257
  );
254
258
  server.tool(
255
259
  tools.replaceNoteMarkdown,
256
260
  "Replace the entire note body, like overwriting a markdown file with `cat new.md > note.md`. Use when the user explicitly asks to rewrite, replace, regenerate, restructure, or overwrite the whole note. Do not use for localized edits such as fixing one sentence, changing one paragraph, or inserting under a heading; use ocean_brain_patch_note_markdown instead.",
257
261
  {
258
- id: z.string().describe("Note ID to replace"),
259
- expectedUpdatedAt: z.string().optional().describe("Expected note updatedAt from the read you based this edit on. Required unless baseMarkdownSha256 is provided."),
260
- baseMarkdownSha256: z.string().optional().describe("SHA-256 of the current markdown. Optional alternative to expectedUpdatedAt."),
261
- intent: z.string().describe("Human-readable reason for the full replace."),
262
- replacement: z.string().describe("Complete replacement markdown body. Tags are body tokens such as [@tag] or [#tag]."),
262
+ id: z2.string().describe("Note ID to replace"),
263
+ ...markdownWriteBaselineFields,
264
+ intent: z2.string().describe("Human-readable reason for the full replace."),
265
+ replacement: z2.string().describe("Complete replacement markdown body. Tags are body tokens such as [@tag] or [#tag]."),
263
266
  policy: markdownWritePolicySchema
264
267
  },
265
268
  async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, replacement, policy }) => {
@@ -273,12 +276,33 @@ var registerIntentWriteTools = (server, {
273
276
  ...policy ? { policy } : {}
274
277
  };
275
278
  const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/replace-markdown", payload);
276
- return {
277
- content: [{
278
- type: "text",
279
- text: JSON.stringify(result, null, 2)
280
- }]
281
- };
279
+ return createMcpJsonToolResult(result);
280
+ }
281
+ );
282
+ };
283
+
284
+ // src/mcp-legacy-write-tools.ts
285
+ import { z as z3 } from "zod";
286
+ var LEGACY_UPDATE_NOTE_DESCRIPTION = "Legacy full-field note update kept for backward compatibility. Prefer ocean_brain_patch_note_markdown for localized body edits, ocean_brain_append_note_markdown for additions, ocean_brain_replace_note_markdown for whole-body rewrites, and ocean_brain_update_note_metadata for title/layout/properties. Use this only when a legacy client needs combined field updates.";
287
+ var registerLegacyWriteTools = (server, { jsonRequest: jsonRequest2, requireWriteToken: requireWriteToken2, serverUrl, token, tools }) => {
288
+ server.tool(
289
+ tools.updateNote,
290
+ LEGACY_UPDATE_NOTE_DESCRIPTION,
291
+ {
292
+ id: z3.string().describe("Note ID to update"),
293
+ title: z3.string().optional().describe("New note title"),
294
+ markdown: z3.string().optional().describe("Replacement markdown body. In MCP markdown, tags must use [@tag] or [#tag]."),
295
+ layout: noteLayoutSchema.optional().describe("Optional note layout: narrow, wide, or full. Prefer wide for most notes unless the user explicitly wants narrow or full.")
296
+ },
297
+ async ({ id, title, markdown, layout }) => {
298
+ const writeToken = requireWriteToken2(token, tools.updateNote);
299
+ const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/update", {
300
+ id,
301
+ ...title !== void 0 ? { title } : {},
302
+ ...markdown !== void 0 ? { markdown } : {},
303
+ ...layout ? { layout } : {}
304
+ });
305
+ return createMcpJsonToolResult(result);
282
306
  }
283
307
  );
284
308
  };
@@ -346,12 +370,11 @@ var OCEAN_BRAIN_MCP_TOOLS = {
346
370
  createTag: "ocean_brain_create_tag",
347
371
  deleteNote: "ocean_brain_delete_note"
348
372
  };
349
- var noteLayoutSchema = z2.enum(["narrow", "wide", "full"]);
350
- var tagMatchModeSchema = z2.enum(["and", "or"]);
351
- var propertyValueTypeSchema = z2.enum(["text", "url", "number", "date", "boolean", "select"]);
352
- var propertyFilterOperatorSchema = z2.enum(["equals", "before", "after", "exists", "notExists"]);
353
- var viewSortBySchema = z2.enum(["updatedAt", "createdAt", "title"]);
354
- var viewSortOrderSchema = z2.enum(["asc", "desc"]);
373
+ var tagMatchModeSchema = z4.enum(["and", "or"]);
374
+ var propertyValueTypeSchema = z4.enum(["text", "url", "number", "date", "boolean", "select"]);
375
+ var propertyFilterOperatorSchema = z4.enum(["equals", "before", "after", "exists", "notExists"]);
376
+ var viewSortBySchema = z4.enum(["updatedAt", "createdAt", "title"]);
377
+ var viewSortOrderSchema = z4.enum(["asc", "desc"]);
355
378
  var formatMcpGraphqlError = (error) => {
356
379
  const suffix = [];
357
380
  if (error.extensions?.code) {
@@ -365,11 +388,11 @@ var formatMcpGraphqlError = (error) => {
365
388
  }
366
389
  return `GraphQL error: ${error.message} (${suffix.join(", ")})`;
367
390
  };
368
- var propertyFilterSchema = z2.object({
369
- key: z2.string().describe("Property key from ocean_brain_list_properties, e.g. state"),
391
+ var propertyFilterSchema = z4.object({
392
+ key: z4.string().describe("Property key from ocean_brain_list_properties, e.g. state"),
370
393
  valueType: propertyValueTypeSchema.describe("Property value type from the property definition"),
371
394
  operator: propertyFilterOperatorSchema.describe("Filter operator. before/after are only valid for date or number properties."),
372
- value: z2.string().nullable().optional().describe("Filter value. Required unless operator is exists or notExists. select=option.value, date=YYYY-MM-DD, boolean=true/false, number=finite, url=http(s).")
395
+ value: z4.string().nullable().optional().describe("Filter value. Required unless operator is exists or notExists. select=option.value, date=YYYY-MM-DD, boolean=true/false, number=finite, url=http(s).")
373
396
  });
374
397
  var normalizeOceanBrainTagName = (name) => {
375
398
  const trimmedName = name.trim();
@@ -441,17 +464,13 @@ var requireWriteToken = (token, toolName) => {
441
464
  }
442
465
  throw new Error(`${toolName} requires an MCP bearer token. Set --token or --token-file.`);
443
466
  };
444
- async function startMcpServer(serverUrl, token) {
445
- const server = new McpServer({
446
- name: "ocean-brain",
447
- version: pkg.version
448
- });
467
+ var registerMcpTools = (server, serverUrl, token) => {
449
468
  server.tool(
450
469
  OCEAN_BRAIN_MCP_TOOLS.searchNotes,
451
470
  "Search Ocean Brain notes by keyword. Returns matching note titles and tags. Use ocean_brain_read_note to get full content for a specific note.",
452
471
  {
453
- query: z2.string().describe("Search keyword"),
454
- limit: z2.number().optional().default(10).describe("Max results (default: 10)")
472
+ query: z4.string().describe("Search keyword"),
473
+ limit: z4.number().optional().default(10).describe("Max results (default: 10)")
455
474
  },
456
475
  async ({ query, limit }) => {
457
476
  const data = await graphql(serverUrl, token, `
@@ -479,20 +498,15 @@ async function startMcpServer(serverUrl, token) {
479
498
  tags: note.tags.map((t) => t.name),
480
499
  preview: note.contentAsMarkdown.slice(0, 100)
481
500
  }));
482
- return {
483
- content: [{
484
- type: "text",
485
- text: JSON.stringify({ totalCount: result.totalCount, notes }, null, 2)
486
- }]
487
- };
501
+ return createMcpJsonToolResult({ totalCount: result.totalCount, notes });
488
502
  }
489
503
  );
490
504
  server.tool(
491
505
  OCEAN_BRAIN_MCP_TOOLS.readNote,
492
506
  "Read an Ocean Brain note by ID. Returns properties, tags, truncated content by default (1000 chars), and a back-reference summary. Set maxLength to 0 only when full content is necessary.",
493
507
  {
494
- id: z2.string().describe("Note ID"),
495
- maxLength: z2.number().optional().default(1e3).describe("Max content length in characters. 0 for full content. (default: 1000)")
508
+ id: z4.string().describe("Note ID"),
509
+ maxLength: z4.number().optional().default(1e3).describe("Max content length in characters. 0 for full content. (default: 1000)")
496
510
  },
497
511
  async ({ id, maxLength }) => {
498
512
  const data = await graphql(serverUrl, token, `
@@ -519,20 +533,15 @@ async function startMcpServer(serverUrl, token) {
519
533
  backReferences,
520
534
  maxLength
521
535
  });
522
- return {
523
- content: [{
524
- type: "text",
525
- text: output
526
- }]
527
- };
536
+ return createMcpTextToolResult(output);
528
537
  }
529
538
  );
530
539
  server.tool(
531
540
  OCEAN_BRAIN_MCP_TOOLS.createNote,
532
541
  "Create an Ocean Brain note from markdown. Search/read first to avoid duplicates; prefer patching an existing note when the intent is a local change.",
533
542
  {
534
- title: z2.string().describe("Note title"),
535
- markdown: z2.string().optional().default("").describe("Markdown body for the new note. In MCP markdown, tags must use [@tag] or [#tag]. Defaults to an empty note body."),
543
+ title: z4.string().describe("Note title"),
544
+ markdown: z4.string().optional().default("").describe("Markdown body for the new note. In MCP markdown, tags must use [@tag] or [#tag]. Defaults to an empty note body."),
536
545
  layout: noteLayoutSchema.optional().describe("Optional note layout: narrow, wide, or full. Prefer wide for most notes unless the user explicitly wants narrow or full.")
537
546
  },
538
547
  async ({ title, markdown, layout }) => {
@@ -542,39 +551,16 @@ async function startMcpServer(serverUrl, token) {
542
551
  markdown,
543
552
  ...layout ? { layout } : {}
544
553
  });
545
- return {
546
- content: [{
547
- type: "text",
548
- text: JSON.stringify(result, null, 2)
549
- }]
550
- };
551
- }
552
- );
553
- server.tool(
554
- OCEAN_BRAIN_MCP_TOOLS.updateNote,
555
- "Legacy full-field note update kept for backward compatibility. Prefer ocean_brain_patch_note_markdown for localized body edits, ocean_brain_append_note_markdown for additions, ocean_brain_replace_note_markdown for whole-body rewrites, and ocean_brain_update_note_metadata for title/layout/properties. Use this only when a legacy client needs combined field updates.",
556
- {
557
- id: z2.string().describe("Note ID to update"),
558
- title: z2.string().optional().describe("New note title"),
559
- markdown: z2.string().optional().describe("Replacement markdown body. In MCP markdown, tags must use [@tag] or [#tag]."),
560
- layout: noteLayoutSchema.optional().describe("Optional note layout: narrow, wide, or full. Prefer wide for most notes unless the user explicitly wants narrow or full.")
561
- },
562
- async ({ id, title, markdown, layout }) => {
563
- const writeToken = requireWriteToken(token, OCEAN_BRAIN_MCP_TOOLS.updateNote);
564
- const result = await jsonRequest(serverUrl, writeToken, "/api/mcp/notes/update", {
565
- id,
566
- ...title !== void 0 ? { title } : {},
567
- ...markdown !== void 0 ? { markdown } : {},
568
- ...layout ? { layout } : {}
569
- });
570
- return {
571
- content: [{
572
- type: "text",
573
- text: JSON.stringify(result, null, 2)
574
- }]
575
- };
554
+ return createMcpJsonToolResult(result);
576
555
  }
577
556
  );
557
+ registerLegacyWriteTools(server, {
558
+ jsonRequest,
559
+ requireWriteToken,
560
+ serverUrl,
561
+ token,
562
+ tools: OCEAN_BRAIN_MCP_TOOLS
563
+ });
578
564
  registerIntentWriteTools(server, {
579
565
  jsonRequest,
580
566
  requireWriteToken,
@@ -603,21 +589,16 @@ async function startMcpServer(serverUrl, token) {
603
589
  pagination: { limit: 100, offset: 0 }
604
590
  });
605
591
  const result = data?.allTags;
606
- return {
607
- content: [{
608
- type: "text",
609
- text: JSON.stringify(result, null, 2)
610
- }]
611
- };
592
+ return createMcpJsonToolResult(result);
612
593
  }
613
594
  );
614
595
  server.tool(
615
596
  OCEAN_BRAIN_MCP_TOOLS.listProperties,
616
597
  "List shared Ocean Brain property definitions. Use this before property queries so keys, value types, and select option values are valid.",
617
598
  {
618
- query: z2.string().optional().default("").describe("Optional property key/name search query"),
619
- limit: z2.number().optional().default(50).describe("Max results (default: 50, server max: 100)"),
620
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
599
+ query: z4.string().optional().default("").describe("Optional property key/name search query"),
600
+ limit: z4.number().optional().default(50).describe("Max results (default: 50, server max: 100)"),
601
+ offset: z4.number().optional().default(0).describe("Pagination offset (default: 0)")
621
602
  },
622
603
  async ({ query, limit, offset }) => {
623
604
  const data = await graphql(serverUrl, token, `
@@ -639,42 +620,32 @@ async function startMcpServer(serverUrl, token) {
639
620
  pagination: { limit, offset }
640
621
  });
641
622
  const result = data?.notePropertyKeys;
642
- return {
643
- content: [{
644
- type: "text",
645
- text: JSON.stringify({
646
- totalCount: result.totalCount,
647
- propertyKeys: result.keys
648
- }, null, 2)
649
- }]
650
- };
623
+ return createMcpJsonToolResult({
624
+ totalCount: result.totalCount,
625
+ propertyKeys: result.keys
626
+ });
651
627
  }
652
628
  );
653
629
  server.tool(
654
630
  OCEAN_BRAIN_MCP_TOOLS.listNotesByTag,
655
631
  "List Ocean Brain notes for a specific tag name. The tool accepts either @tag or tag and resolves it to the canonical Ocean Brain tag name first.",
656
632
  {
657
- tag: z2.string().describe("Tag name to inspect. You can pass @project or project."),
658
- limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
659
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
633
+ tag: z4.string().describe("Tag name to inspect. You can pass @project or project."),
634
+ limit: z4.number().optional().default(20).describe("Max results (default: 20)"),
635
+ offset: z4.number().optional().default(0).describe("Pagination offset (default: 0)")
660
636
  },
661
637
  async ({ tag, limit, offset }) => {
662
638
  const normalizedTag = normalizeOceanBrainTagName(tag);
663
639
  const tagResult = await fetchOceanBrainTags(serverUrl, token, normalizedTag);
664
640
  const exactMatches = tagResult.tags.filter((item) => item.name === normalizedTag);
665
641
  if (exactMatches.length === 0) {
666
- return {
667
- content: [{
668
- type: "text",
669
- text: JSON.stringify({
670
- requestedTag: tag,
671
- normalizedTag,
672
- tagFound: false,
673
- noteCount: 0,
674
- notes: []
675
- }, null, 2)
676
- }]
677
- };
642
+ return createMcpJsonToolResult({
643
+ requestedTag: tag,
644
+ normalizedTag,
645
+ tagFound: false,
646
+ noteCount: 0,
647
+ notes: []
648
+ });
678
649
  }
679
650
  const selectedTag = exactMatches[0];
680
651
  const notesData = await graphql(serverUrl, token, `
@@ -694,35 +665,30 @@ async function startMcpServer(serverUrl, token) {
694
665
  pagination: { limit, offset }
695
666
  });
696
667
  const noteResult = notesData?.tagNotes;
697
- return {
698
- content: [{
699
- type: "text",
700
- text: JSON.stringify({
701
- requestedTag: tag,
702
- normalizedTag,
703
- tagFound: true,
704
- duplicateExactMatchCount: exactMatches.length,
705
- tag: selectedTag,
706
- totalCount: noteResult.totalCount,
707
- notes: noteResult.notes.map((note) => ({
708
- id: note.id,
709
- title: note.title,
710
- updatedAt: note.updatedAt,
711
- tags: note.tags.map((item) => item.name)
712
- }))
713
- }, null, 2)
714
- }]
715
- };
668
+ return createMcpJsonToolResult({
669
+ requestedTag: tag,
670
+ normalizedTag,
671
+ tagFound: true,
672
+ duplicateExactMatchCount: exactMatches.length,
673
+ tag: selectedTag,
674
+ totalCount: noteResult.totalCount,
675
+ notes: noteResult.notes.map((note) => ({
676
+ id: note.id,
677
+ title: note.title,
678
+ updatedAt: note.updatedAt,
679
+ tags: note.tags.map((item) => item.name)
680
+ }))
681
+ });
716
682
  }
717
683
  );
718
684
  server.tool(
719
685
  OCEAN_BRAIN_MCP_TOOLS.listNotesByTags,
720
686
  "List Ocean Brain notes for multiple tag names. Supports AND/OR matching and resolves each input tag to the canonical Ocean Brain tag name first.",
721
687
  {
722
- tags: z2.array(z2.string()).min(1).describe("Tag names to inspect. You can pass @project or project values."),
688
+ tags: z4.array(z4.string()).min(1).describe("Tag names to inspect. You can pass @project or project values."),
723
689
  mode: tagMatchModeSchema.optional().default("and").describe("Tag match mode. Use and for intersection, or for union. Defaults to and."),
724
- limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
725
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
690
+ limit: z4.number().optional().default(20).describe("Max results (default: 20)"),
691
+ offset: z4.number().optional().default(0).describe("Pagination offset (default: 0)")
726
692
  },
727
693
  async ({ tags, mode, limit, offset }) => {
728
694
  const normalizedTags = normalizeOceanBrainTagNames(tags);
@@ -770,41 +736,36 @@ async function startMcpServer(serverUrl, token) {
770
736
  });
771
737
  noteResult = notesData?.notesByTagNames;
772
738
  }
773
- return {
774
- content: [{
775
- type: "text",
776
- text: JSON.stringify({
777
- requestedTags: tags,
778
- normalizedTags,
779
- mode,
780
- allTagsFound: missingTags.length === 0,
781
- missingTags,
782
- tags: matchedTags,
783
- totalCount: noteResult.totalCount,
784
- notes: noteResult.notes.map((note) => ({
785
- id: note.id,
786
- title: note.title,
787
- updatedAt: note.updatedAt,
788
- tags: note.tags.map((item) => item.name)
789
- }))
790
- }, null, 2)
791
- }]
792
- };
739
+ return createMcpJsonToolResult({
740
+ requestedTags: tags,
741
+ normalizedTags,
742
+ mode,
743
+ allTagsFound: missingTags.length === 0,
744
+ missingTags,
745
+ tags: matchedTags,
746
+ totalCount: noteResult.totalCount,
747
+ notes: noteResult.notes.map((note) => ({
748
+ id: note.id,
749
+ title: note.title,
750
+ updatedAt: note.updatedAt,
751
+ tags: note.tags.map((item) => item.name)
752
+ }))
753
+ });
793
754
  }
794
755
  );
795
756
  server.tool(
796
757
  OCEAN_BRAIN_MCP_TOOLS.queryNotesByProperties,
797
758
  "Call ocean_brain_list_properties first. Requires >=1 propertyFilter; use search/recent for broad lists. Use key/valueType from the property definition. Values are strings: select=option.value, date=YYYY-MM-DD, boolean=true/false, number=finite, url=http(s). exists/notExists need no value. Property filters use AND; tagNames use mode. Summaries are returned by default; use propertyKeys for needed property details.",
798
759
  {
799
- propertyFilters: z2.array(propertyFilterSchema).min(1).max(10).describe("Required property filters. Multiple property filters are combined with AND."),
800
- tagNames: z2.array(z2.string()).optional().default([]).describe("Optional tag filters. You can pass @project, #project, or project."),
760
+ propertyFilters: z4.array(propertyFilterSchema).min(1).max(10).describe("Required property filters. Multiple property filters are combined with AND."),
761
+ tagNames: z4.array(z4.string()).optional().default([]).describe("Optional tag filters. You can pass @project, #project, or project."),
801
762
  mode: tagMatchModeSchema.optional().default("and").describe("Tag match mode for tagNames only. Property filters are always combined with AND."),
802
763
  sortBy: viewSortBySchema.optional().default("updatedAt").describe("Sort field (default: updatedAt)"),
803
764
  sortOrder: viewSortOrderSchema.optional().default("desc").describe("Sort order (default: desc)"),
804
- includeProperties: z2.boolean().optional().default(false).describe("Include returned note properties. Defaults to false to reduce tokens."),
805
- propertyKeys: z2.array(z2.string()).optional().default([]).describe("Property keys to include in output; automatically includes properties."),
806
- limit: z2.number().optional().default(20).describe("Max results (default: 20, server max: 50)"),
807
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
765
+ includeProperties: z4.boolean().optional().default(false).describe("Include returned note properties. Defaults to false to reduce tokens."),
766
+ propertyKeys: z4.array(z4.string()).optional().default([]).describe("Property keys to include in output; automatically includes properties."),
767
+ limit: z4.number().optional().default(20).describe("Max results (default: 20, server max: 50)"),
768
+ offset: z4.number().optional().default(0).describe("Pagination offset (default: 0)")
808
769
  },
809
770
  async ({ propertyFilters, tagNames, mode, sortBy, sortOrder, includeProperties, propertyKeys, limit, offset }) => {
810
771
  const shouldIncludeProperties = includeProperties || propertyKeys.length > 0;
@@ -834,36 +795,27 @@ async function startMcpServer(serverUrl, token) {
834
795
  includeProperties: shouldIncludeProperties
835
796
  });
836
797
  const result = data?.notesByProperties;
837
- return {
838
- content: [{
839
- type: "text",
840
- text: JSON.stringify(
841
- formatPropertyQueryResponse({
842
- result,
843
- query: {
844
- propertyFilters,
845
- tagNames,
846
- mode,
847
- sortBy,
848
- sortOrder,
849
- limit,
850
- offset
851
- },
852
- includeProperties: shouldIncludeProperties,
853
- propertyKeys
854
- }),
855
- null,
856
- 2
857
- )
858
- }]
859
- };
798
+ return createMcpJsonToolResult(formatPropertyQueryResponse({
799
+ result,
800
+ query: {
801
+ propertyFilters,
802
+ tagNames,
803
+ mode,
804
+ sortBy,
805
+ sortOrder,
806
+ limit,
807
+ offset
808
+ },
809
+ includeProperties: shouldIncludeProperties,
810
+ propertyKeys
811
+ }));
860
812
  }
861
813
  );
862
814
  server.tool(
863
815
  OCEAN_BRAIN_MCP_TOOLS.listRecentNotes,
864
816
  "List recently updated Ocean Brain notes. Returns titles and tags only. Use ocean_brain_read_note to get full content for a specific note.",
865
817
  {
866
- limit: z2.number().optional().default(10).describe("Max results (default: 10)")
818
+ limit: z4.number().optional().default(10).describe("Max results (default: 10)")
867
819
  },
868
820
  async ({ limit }) => {
869
821
  const data = await graphql(serverUrl, token, `
@@ -889,21 +841,16 @@ async function startMcpServer(serverUrl, token) {
889
841
  updatedAt: note.updatedAt,
890
842
  tags: note.tags.map((t) => t.name)
891
843
  }));
892
- return {
893
- content: [{
894
- type: "text",
895
- text: JSON.stringify({ totalCount: result.totalCount, notes }, null, 2)
896
- }]
897
- };
844
+ return createMcpJsonToolResult({ totalCount: result.totalCount, notes });
898
845
  }
899
846
  );
900
847
  server.tool(
901
848
  OCEAN_BRAIN_MCP_TOOLS.findNoteCleanupCandidates,
902
849
  "Find Ocean Brain note cleanup candidates for temporary or draft notes before deletion. This is optional discovery; ocean_brain_delete_note moves a single note to trash directly.",
903
850
  {
904
- keywords: z2.string().optional().default("temp tmp draft test wip").describe("Keywords that mark a note as a cleanup candidate. Comma or space separated."),
905
- limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
906
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
851
+ keywords: z4.string().optional().default("temp tmp draft test wip").describe("Keywords that mark a note as a cleanup candidate. Comma or space separated."),
852
+ limit: z4.number().optional().default(20).describe("Max results (default: 20)"),
853
+ offset: z4.number().optional().default(0).describe("Pagination offset (default: 0)")
907
854
  },
908
855
  async ({ keywords, limit, offset }) => {
909
856
  const normalizedKeywords = keywords.split(/[,\s]+/).map((keyword) => keyword.trim()).filter(Boolean);
@@ -928,61 +875,53 @@ async function startMcpServer(serverUrl, token) {
928
875
  pagination: { limit, offset }
929
876
  });
930
877
  const result = data?.noteCleanupCandidates;
931
- return {
932
- content: [{
933
- type: "text",
934
- text: JSON.stringify({
935
- keywords: normalizedKeywords,
936
- limit,
937
- offset,
938
- candidateCount: result.length,
939
- notes: result
940
- }, null, 2)
941
- }]
942
- };
878
+ return createMcpJsonToolResult({
879
+ keywords: normalizedKeywords,
880
+ limit,
881
+ offset,
882
+ candidateCount: result.length,
883
+ notes: result
884
+ });
943
885
  }
944
886
  );
945
887
  server.tool(
946
888
  OCEAN_BRAIN_MCP_TOOLS.createTag,
947
889
  "Create an Ocean Brain tag through the MCP write path. The tool accepts either @tag or tag and creates the canonical Ocean Brain tag if it does not already exist.",
948
890
  {
949
- name: z2.string().describe("Tag name to create. You can pass @project or project.")
891
+ name: z4.string().describe("Tag name to create. You can pass @project or project.")
950
892
  },
951
893
  async ({ name }) => {
952
894
  const writeToken = requireWriteToken(token, OCEAN_BRAIN_MCP_TOOLS.createTag);
953
895
  const result = await jsonRequest(serverUrl, writeToken, "/api/mcp/tags/create", {
954
896
  name
955
897
  });
956
- return {
957
- content: [{
958
- type: "text",
959
- text: JSON.stringify(result, null, 2)
960
- }]
961
- };
898
+ return createMcpJsonToolResult(result);
962
899
  }
963
900
  );
964
901
  server.tool(
965
902
  OCEAN_BRAIN_MCP_TOOLS.deleteNote,
966
903
  "Move an Ocean Brain note to trash, like `mv note.md trash/`. This is a recoverable trash move, not permanent deletion. Deleted-note tag names are kept as restore metadata; orphan tags are not a blocking condition.",
967
904
  {
968
- id: z2.string().describe("Note ID to move to trash")
905
+ id: z4.string().describe("Note ID to move to trash")
969
906
  },
970
907
  async ({ id }) => {
971
908
  const writeToken = requireWriteToken(token, OCEAN_BRAIN_MCP_TOOLS.deleteNote);
972
909
  try {
973
910
  const result = await jsonRequest(serverUrl, writeToken, "/api/mcp/notes/delete", { id });
974
- return {
975
- content: [{
976
- type: "text",
977
- text: JSON.stringify(result, null, 2)
978
- }]
979
- };
911
+ return createMcpJsonToolResult(result);
980
912
  } catch (error) {
981
913
  const message = error instanceof Error ? error.message : "Unknown MCP note delete error";
982
914
  throw new Error(message);
983
915
  }
984
916
  }
985
917
  );
918
+ };
919
+ async function startMcpServer(serverUrl, token) {
920
+ const server = new McpServer({
921
+ name: "ocean-brain",
922
+ version: pkg.version
923
+ });
924
+ registerMcpTools(server, serverUrl, token);
986
925
  const transport = new StdioServerTransport();
987
926
  await server.connect(transport);
988
927
  }