ocean-brain 0.5.0 → 0.6.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 (77) hide show
  1. package/dist/mcp.js +660 -31
  2. package/package.json +2 -1
  3. package/server/client/dist/assets/{Calendar-BU9XgX2N.js → Calendar-BytW4PeT.js} +1 -1
  4. package/server/client/dist/assets/{Callout-C6tVCxOT.js → Callout-9sJgGNRb.js} +1 -1
  5. package/server/client/dist/assets/Graph-o5u-NzrA.js +1 -0
  6. package/server/client/dist/assets/{Image.es-BjGK7y6Y.js → Image.es-B0LL3pb7.js} +1 -1
  7. package/server/client/dist/assets/Note-CYoYzZNu.js +21 -0
  8. package/server/client/dist/assets/{Plus.es-EcmtS_uw.js → Plus.es-DpxzgRtY.js} +1 -1
  9. package/server/client/dist/assets/Reminders-DAepTZg1.js +1 -0
  10. package/server/client/dist/assets/Search-6KH7An44.js +1 -0
  11. package/server/client/dist/assets/{SurfaceCard-CO0oVxeL.js → SurfaceCard-EMs50xSF.js} +1 -1
  12. package/server/client/dist/assets/Tag-C89j8x56.js +1 -0
  13. package/server/client/dist/assets/TagNotes-BTbu-f6v.js +1 -0
  14. package/server/client/dist/assets/{Trash.es-DmwfSy3l.js → Trash.es-C5x35NKF.js} +1 -1
  15. package/server/client/dist/assets/ViewNotes-DPEuki3y.js +1 -0
  16. package/server/client/dist/assets/Views-BGkuNhVd.js +1 -0
  17. package/server/client/dist/assets/{image.api-DeF9ck7g.js → image.api-DRhRQ0Gf.js} +1 -1
  18. package/server/client/dist/assets/{index-BZUGWdOU.css → index-Cd81UxEh.css} +1 -1
  19. package/server/client/dist/assets/{index-BvKBTjQq.js → index-DYlYUrjH.js} +118 -37
  20. package/server/client/dist/assets/index-DswSFFMb.js +1 -0
  21. package/server/client/dist/assets/manage-image-C6sHhWnI.js +1 -0
  22. package/server/client/dist/assets/manage-image-detail-CJkfybR9.js +1 -0
  23. package/server/client/dist/assets/{mcp-Bh743HBz.js → mcp-piouFDNm.js} +1 -1
  24. package/server/client/dist/assets/{placeholder-XQqo1Fvr.js → placeholder-DGz3CHej.js} +5 -5
  25. package/server/client/dist/assets/properties-CM4khczg.js +1 -0
  26. package/server/client/dist/assets/trash-C2lItz-b.js +1 -0
  27. package/server/client/dist/assets/{useReminderMutate-D21PurIS.js → useReminderMutate-DHjDDHKg.js} +1 -1
  28. package/server/client/dist/assets/view-dashboard-C17WsdL7.js +99 -0
  29. package/server/client/dist/index.html +2 -2
  30. package/server/dist/features/note/graphql/note.field.resolver.js +2 -0
  31. package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
  32. package/server/dist/features/note/graphql/note.mutation.resolver.js +119 -0
  33. package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
  34. package/server/dist/features/note/graphql/note.query.resolver.js +14 -0
  35. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
  36. package/server/dist/features/note/graphql/note.type-defs.js +100 -0
  37. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
  38. package/server/dist/features/note/http/mcp.js +394 -1
  39. package/server/dist/features/note/http/mcp.js.map +1 -1
  40. package/server/dist/features/note/services/markdown-intent-write.js +478 -0
  41. package/server/dist/features/note/services/markdown-intent-write.js.map +1 -0
  42. package/server/dist/features/note/services/markdown-patch.js +601 -0
  43. package/server/dist/features/note/services/markdown-patch.js.map +1 -0
  44. package/server/dist/features/note/services/properties.js +874 -0
  45. package/server/dist/features/note/services/properties.js.map +1 -0
  46. package/server/dist/features/note/services/snapshot.js +94 -12
  47. package/server/dist/features/note/services/snapshot.js.map +1 -1
  48. package/server/dist/features/note/services/trash.js +70 -2
  49. package/server/dist/features/note/services/trash.js.map +1 -1
  50. package/server/dist/features/note/services/write.js +21 -7
  51. package/server/dist/features/note/services/write.js.map +1 -1
  52. package/server/dist/features/view/graphql/view.query.resolver.js +31 -1
  53. package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -1
  54. package/server/dist/features/view/graphql/view.type-defs.js +57 -1
  55. package/server/dist/features/view/graphql/view.type-defs.js.map +1 -1
  56. package/server/dist/features/view/services/workspace.js +367 -43
  57. package/server/dist/features/view/services/workspace.js.map +1 -1
  58. package/server/dist/modules/blocknote.js +47 -2
  59. package/server/dist/modules/blocknote.js.map +1 -1
  60. package/server/dist/routes/mcp.js +21 -1
  61. package/server/dist/routes/mcp.js.map +1 -1
  62. package/server/prisma/migrations/20260527073000_0017_note_properties/migration.sql +105 -0
  63. package/server/prisma/migrations/20260531073000_0018_view_query/migration.sql +3 -0
  64. package/server/prisma/schema.prisma +226 -130
  65. package/server/client/dist/assets/Graph-Dond2hGK.js +0 -1
  66. package/server/client/dist/assets/Note-Cr6GtXUM.js +0 -21
  67. package/server/client/dist/assets/Reminders-BlLLT_x0.js +0 -1
  68. package/server/client/dist/assets/Search-CrZ0ap8x.js +0 -1
  69. package/server/client/dist/assets/Tag-vws5AfNW.js +0 -1
  70. package/server/client/dist/assets/TagNotes-Fmf1Kgim.js +0 -1
  71. package/server/client/dist/assets/ViewNotes-BALN7IfH.js +0 -1
  72. package/server/client/dist/assets/Views-Ccc_Ml5U.js +0 -1
  73. package/server/client/dist/assets/index-BSVE2cGd.js +0 -1
  74. package/server/client/dist/assets/manage-image-IoTXaMBP.js +0 -1
  75. package/server/client/dist/assets/manage-image-detail-BRTXwOsh.js +0 -1
  76. package/server/client/dist/assets/trash-hiJIg6-c.js +0 -1
  77. package/server/client/dist/assets/view-dashboard-BuaZ4MvI.js +0 -90
package/dist/mcp.js CHANGED
@@ -6,7 +6,7 @@ import path2 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 z3 } from "zod";
10
10
 
11
11
  // src/mcp-write-safety.ts
12
12
  import crypto from "crypto";
@@ -15,11 +15,16 @@ import os from "os";
15
15
  import path from "path";
16
16
  import { z } from "zod";
17
17
  var destructiveMcpWriteFields = {
18
- dryRun: z.boolean().optional().default(true).describe("Preview the destructive change first. Set dryRun to false only after you receive an operationId and confirmToken."),
19
- operationId: z.string().optional().describe("Operation id returned by the dry-run response. Required when dryRun is false."),
20
- confirmToken: z.string().optional().describe("Confirmation token returned by the dry-run response. Required when dryRun is false."),
18
+ dryRun: z.boolean().optional().default(true).describe("Preview the destructive change first. Commit only after dry-run returns operation.operationId and operation.confirmToken."),
19
+ operationId: z.string().optional().describe("Dry-run response operation.operationId. Required when dryRun is false."),
20
+ confirmToken: z.string().optional().describe("Dry-run response operation.confirmToken. Required when dryRun is false."),
21
21
  force: z.boolean().optional().default(false).describe("Explicitly confirm a bulk destructive change when the preview says force is required.")
22
22
  };
23
+ var confirmedMcpWriteFields = {
24
+ dryRun: z.boolean().optional().default(true).describe("Preview the write first. Commit only after dry-run returns operation.operationId and operation.confirmToken."),
25
+ operationId: z.string().optional().describe("Dry-run response operation.operationId. Required when dryRun is false."),
26
+ confirmToken: z.string().optional().describe("Dry-run response operation.confirmToken. Required when dryRun is false.")
27
+ };
23
28
  var McpCliWriteSafetyError = class extends Error {
24
29
  code;
25
30
  constructor(code, message) {
@@ -42,6 +47,7 @@ var toPublicPendingOperation = (operation) => ({
42
47
  estimatedChangeCount: operation.estimatedChangeCount,
43
48
  expiresAt: operation.expiresAt,
44
49
  force: operation.force,
50
+ ...operation.operationFingerprint ? { operationFingerprint: operation.operationFingerprint } : {},
45
51
  operationId: operation.operationId,
46
52
  risk: operation.risk,
47
53
  summary: operation.summary,
@@ -126,6 +132,7 @@ var createMcpWriteSafetyCoordinator = (options = {}) => {
126
132
  estimatedChangeCount: input.estimatedChangeCount,
127
133
  expiresAt,
128
134
  force: Boolean(input.force),
135
+ ...input.operationFingerprint ? { operationFingerprint: input.operationFingerprint } : {},
129
136
  operationId,
130
137
  risk: input.risk,
131
138
  summary: input.summary,
@@ -160,6 +167,10 @@ var createMcpWriteSafetyCoordinator = (options = {}) => {
160
167
  appendLog(buildLogEntry(publicPendingOperation, "rejected", "tool_mismatch"));
161
168
  throw createError("INVALID_CONFIRMATION", "The write confirmation does not match this MCP tool.");
162
169
  }
170
+ if (expected.operationFingerprint && pendingOperation.operationFingerprint !== expected.operationFingerprint) {
171
+ appendLog(buildLogEntry(publicPendingOperation, "rejected", "operation_fingerprint_mismatch"));
172
+ throw createError("INVALID_CONFIRMATION", "The write confirmation does not match this MCP operation.");
173
+ }
163
174
  if (pendingOperation.confirmTokenHash !== hashToken(request.confirmToken)) {
164
175
  appendLog(buildLogEntry(publicPendingOperation, "rejected", "token_mismatch"));
165
176
  throw createError("INVALID_CONFIRMATION", "The write confirmation token is invalid.");
@@ -180,7 +191,10 @@ var createMcpWriteSafetyCoordinator = (options = {}) => {
180
191
  }
181
192
  return {
182
193
  kind: "confirmed",
183
- operation: requireConfirmedOperation(request, { toolName: input.toolName })
194
+ operation: requireConfirmedOperation(request, {
195
+ toolName: input.toolName,
196
+ operationFingerprint: input.operationFingerprint
197
+ })
184
198
  };
185
199
  };
186
200
  const recordWriteResult = (operation, success, detail) => {
@@ -215,6 +229,12 @@ var formatBackReferenceLine = (backReference) => {
215
229
  }
216
230
  return `- ${backReference.id}: ${backReference.title}`;
217
231
  };
232
+ var formatPropertyLine = (property) => {
233
+ const displayValue = (property.option?.label ?? property.value) || "(empty)";
234
+ const rawValue = property.option?.value ?? property.value;
235
+ const rawValueLabel = rawValue ? `, value=${rawValue}` : "";
236
+ return `- ${property.key} (${property.name}): ${displayValue} [${property.valueType}${rawValueLabel}]`;
237
+ };
218
238
  var formatMcpReadNoteOutput = ({
219
239
  note,
220
240
  backReferences,
@@ -227,10 +247,14 @@ var formatMcpReadNoteOutput = ({
227
247
  markdown = markdown.slice(0, maxLength);
228
248
  }
229
249
  const backReferenceLines = backReferences.length > 0 ? backReferences.map(formatBackReferenceLine) : ["- (none)"];
250
+ const propertyLines = note.properties && note.properties.length > 0 ? note.properties.map(formatPropertyLine) : ["- (none)"];
230
251
  return [
231
252
  `# ${note.title}`,
232
253
  "",
233
254
  `Tags: ${note.tags.map((tag) => tag.name).join(", ") || "(none)"}`,
255
+ "",
256
+ "Properties:",
257
+ ...propertyLines,
234
258
  `Created: ${note.createdAt}`,
235
259
  `Updated: ${note.updatedAt}`,
236
260
  ...truncated ? [`Content: ${totalLength} chars (showing first ${maxLength})`] : [],
@@ -243,6 +267,476 @@ var formatMcpReadNoteOutput = ({
243
267
  ].join("\n");
244
268
  };
245
269
 
270
+ // src/mcp-intent-write-tools.ts
271
+ import crypto2 from "crypto";
272
+ import { z as z2 } from "zod";
273
+ var markdownPatchSelectorSchema = z2.discriminatedUnion("type", [
274
+ z2.object({
275
+ type: z2.literal("exact_text"),
276
+ text: z2.string(),
277
+ before: z2.string().optional(),
278
+ after: z2.string().optional()
279
+ }),
280
+ z2.object({
281
+ type: z2.literal("match_candidate"),
282
+ text: z2.string(),
283
+ matchIndex: z2.number().int().nonnegative(),
284
+ lineStart: z2.number().int().positive(),
285
+ matchSha256: z2.string(),
286
+ surroundingHash: z2.string(),
287
+ positionHint: z2.enum(["first", "last"]).optional()
288
+ })
289
+ ]);
290
+ var markdownPatchOperationSchema = z2.discriminatedUnion("type", [
291
+ z2.object({
292
+ type: z2.literal("replace"),
293
+ replacement: z2.string()
294
+ }),
295
+ z2.object({
296
+ type: z2.literal("insert_before"),
297
+ insertion: z2.string()
298
+ }),
299
+ z2.object({
300
+ type: z2.literal("insert_after"),
301
+ insertion: z2.string()
302
+ })
303
+ ]);
304
+ var markdownAppendPlacementSchema = z2.discriminatedUnion("type", [
305
+ z2.object({
306
+ type: z2.literal("end")
307
+ }),
308
+ z2.object({
309
+ type: z2.literal("after_heading"),
310
+ heading: z2.string(),
311
+ level: z2.number().int().min(1).max(6).optional()
312
+ })
313
+ ]);
314
+ var markdownWritePolicySchema = z2.object({
315
+ allowNoop: z2.boolean().optional(),
316
+ maxChangedChars: z2.number().int().nonnegative().optional(),
317
+ maxChangedLines: z2.number().int().nonnegative().optional(),
318
+ preserveTags: z2.union([z2.boolean(), z2.literal("warn")]).optional(),
319
+ preserveReferences: z2.union([z2.boolean(), z2.literal("warn")]).optional()
320
+ }).optional();
321
+ var MCP_METADATA_PROPERTY_PATCH_LIMIT = 50;
322
+ var normalizeMetadataPropertyKey = (key) => key.trim().toLowerCase().replace(/\s+/g, "-");
323
+ var metadataPropertyPatchSchema = z2.object({
324
+ set: z2.array(z2.object({
325
+ key: z2.string().min(1).describe("Property key from ocean_brain_list_properties."),
326
+ value: z2.string().describe("String value: select option.value, date YYYY-MM-DD, boolean true/false, number finite string, url http(s).")
327
+ })).max(MCP_METADATA_PROPERTY_PATCH_LIMIT).optional().describe("Set up to 50 property values. Keys must be unique."),
328
+ 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.")
329
+ }).superRefine((patch, context) => {
330
+ if ((patch.set?.length ?? 0) === 0 && (patch.deleteKeys?.length ?? 0) === 0) {
331
+ context.addIssue({
332
+ code: z2.ZodIssueCode.custom,
333
+ message: "properties must include set or deleteKeys."
334
+ });
335
+ return;
336
+ }
337
+ const setKeys = /* @__PURE__ */ new Set();
338
+ for (const item of patch.set ?? []) {
339
+ const key = normalizeMetadataPropertyKey(item.key);
340
+ if (setKeys.has(key)) {
341
+ context.addIssue({
342
+ code: z2.ZodIssueCode.custom,
343
+ message: `properties.set contains duplicate key ${key}.`
344
+ });
345
+ }
346
+ setKeys.add(key);
347
+ }
348
+ const deleteKeys = /* @__PURE__ */ new Set();
349
+ for (const rawKey of patch.deleteKeys ?? []) {
350
+ const key = normalizeMetadataPropertyKey(rawKey);
351
+ if (deleteKeys.has(key)) {
352
+ context.addIssue({
353
+ code: z2.ZodIssueCode.custom,
354
+ message: `properties.deleteKeys contains duplicate key ${key}.`
355
+ });
356
+ }
357
+ if (setKeys.has(key)) {
358
+ context.addIssue({
359
+ code: z2.ZodIssueCode.custom,
360
+ message: `properties cannot set and delete ${key} in one request.`
361
+ });
362
+ }
363
+ deleteKeys.add(key);
364
+ }
365
+ }).optional();
366
+ var sha256 = (value) => crypto2.createHash("sha256").update(value).digest("hex");
367
+ var normalizeServerUrl = (serverUrl) => {
368
+ try {
369
+ const url = new URL(serverUrl);
370
+ url.hash = "";
371
+ return url.toString().replace(/\/$/, "");
372
+ } catch {
373
+ return serverUrl.replace(/\/$/, "");
374
+ }
375
+ };
376
+ var createIntentWriteOperationFingerprint = (serverUrl, token, toolName, payload) => {
377
+ return sha256(JSON.stringify({
378
+ payload,
379
+ serverUrl: normalizeServerUrl(serverUrl),
380
+ tokenDigest: token ? sha256(token) : null,
381
+ toolName
382
+ }));
383
+ };
384
+ var getErrorDetail = (error) => {
385
+ return error instanceof Error ? error.message : "Unknown MCP write error";
386
+ };
387
+ var prepareConfirmedWriteOperation = (writeSafety, result, input) => {
388
+ if (result.status !== "dry_run") {
389
+ return result;
390
+ }
391
+ return {
392
+ ...result,
393
+ operation: writeSafety.prepareOperation({
394
+ actor: "mcp-bearer",
395
+ affectedIds: input.affectedIds,
396
+ estimatedChangeCount: result.proposed?.changedLineCount ?? 1,
397
+ operationFingerprint: input.operationFingerprint,
398
+ risk: "high-impact",
399
+ summary: input.summary,
400
+ toolName: input.toolName
401
+ })
402
+ };
403
+ };
404
+ var requireConfirmedWriteOperation = (writeSafety, request, input) => {
405
+ const intent = writeSafety.ensureDestructiveWriteRequest(
406
+ request,
407
+ {
408
+ actor: "mcp-bearer",
409
+ affectedIds: input.affectedIds,
410
+ estimatedChangeCount: 1,
411
+ operationFingerprint: input.operationFingerprint,
412
+ risk: "high-impact",
413
+ summary: input.summary,
414
+ toolName: input.toolName
415
+ }
416
+ );
417
+ if (intent.kind !== "confirmed") {
418
+ throw new Error("Expected a confirmed MCP write operation.");
419
+ }
420
+ return intent.operation;
421
+ };
422
+ var executeConfirmedWrite = async (writeSafety, operation, request) => {
423
+ try {
424
+ const result = await request();
425
+ writeSafety.recordWriteResult(
426
+ operation,
427
+ result.status === "applied",
428
+ result.status === "applied" ? void 0 : result.reason
429
+ );
430
+ return result;
431
+ } catch (error) {
432
+ writeSafety.recordWriteResult(operation, false, getErrorDetail(error));
433
+ throw error;
434
+ }
435
+ };
436
+ var registerIntentWriteTools = (server, {
437
+ jsonRequest: jsonRequest2,
438
+ requireWriteToken: requireWriteToken2,
439
+ serverUrl,
440
+ token,
441
+ tools,
442
+ writeSafety
443
+ }) => {
444
+ server.tool(
445
+ tools.patchNoteMarkdown,
446
+ "Patch a small part of an Ocean Brain note. Dry-run is the default; commit requires operationId and confirmToken from the dry-run result.",
447
+ {
448
+ id: z2.string().describe("Note ID to patch"),
449
+ expectedUpdatedAt: z2.string().optional().describe("Expected note updatedAt from a prior read. Required unless baseMarkdownSha256 is provided."),
450
+ baseMarkdownSha256: z2.string().optional().describe("SHA-256 of the current markdown. Required unless expectedUpdatedAt is provided."),
451
+ intent: z2.string().describe("Human-readable reason for the patch."),
452
+ selector: markdownPatchSelectorSchema.describe("Exact text or previously returned match candidate."),
453
+ operation: markdownPatchOperationSchema.describe("replace, insert_before, or insert_after."),
454
+ policy: markdownWritePolicySchema,
455
+ ...confirmedMcpWriteFields
456
+ },
457
+ async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, selector, operation, policy, dryRun, operationId, confirmToken }) => {
458
+ const writeToken = requireWriteToken2(token, tools.patchNoteMarkdown);
459
+ const payload = {
460
+ id,
461
+ ...expectedUpdatedAt ? { expectedUpdatedAt } : {},
462
+ ...baseMarkdownSha256 ? { baseMarkdownSha256 } : {},
463
+ intent,
464
+ selector,
465
+ operation,
466
+ ...policy ? { policy } : {}
467
+ };
468
+ const operationFingerprint = createIntentWriteOperationFingerprint(serverUrl, writeToken, tools.patchNoteMarkdown, payload);
469
+ if (dryRun) {
470
+ const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/patch-markdown", {
471
+ ...payload,
472
+ dryRun: true
473
+ });
474
+ return {
475
+ content: [{
476
+ type: "text",
477
+ text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
478
+ affectedIds: [id],
479
+ operationFingerprint,
480
+ summary: intent,
481
+ toolName: tools.patchNoteMarkdown
482
+ }), null, 2)
483
+ }]
484
+ };
485
+ }
486
+ const confirmedOperation = requireConfirmedWriteOperation(
487
+ writeSafety,
488
+ { dryRun, operationId, confirmToken },
489
+ {
490
+ affectedIds: [id],
491
+ operationFingerprint,
492
+ summary: intent,
493
+ toolName: tools.patchNoteMarkdown
494
+ }
495
+ );
496
+ const result = await executeConfirmedWrite(
497
+ writeSafety,
498
+ confirmedOperation,
499
+ () => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/patch-markdown", {
500
+ ...payload,
501
+ dryRun: false
502
+ })
503
+ );
504
+ return {
505
+ content: [{
506
+ type: "text",
507
+ text: JSON.stringify(result, null, 2)
508
+ }]
509
+ };
510
+ }
511
+ );
512
+ server.tool(
513
+ tools.appendNoteMarkdown,
514
+ "Append markdown to a note without replacing existing body content. Dry-run is the default; commit requires operationId and confirmToken.",
515
+ {
516
+ id: z2.string().describe("Note ID to append to"),
517
+ expectedUpdatedAt: z2.string().optional().describe("Expected note updatedAt from a prior read. Required unless baseMarkdownSha256 is provided."),
518
+ baseMarkdownSha256: z2.string().optional().describe("SHA-256 of the current markdown. Required unless expectedUpdatedAt is provided."),
519
+ intent: z2.string().describe("Human-readable reason for the append."),
520
+ insertion: z2.string().describe("Markdown to append. Tags are body tokens such as [@tag] or [#tag]."),
521
+ placement: markdownAppendPlacementSchema.optional().describe("Default is end. after_heading requires one unique matching heading."),
522
+ separator: z2.enum(["\n\n", "\n"]).optional().describe("Separator inserted around appended markdown. Defaults to a blank line."),
523
+ policy: markdownWritePolicySchema,
524
+ ...confirmedMcpWriteFields
525
+ },
526
+ async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, insertion, placement, separator, policy, dryRun, operationId, confirmToken }) => {
527
+ const writeToken = requireWriteToken2(token, tools.appendNoteMarkdown);
528
+ const payload = {
529
+ id,
530
+ ...expectedUpdatedAt ? { expectedUpdatedAt } : {},
531
+ ...baseMarkdownSha256 ? { baseMarkdownSha256 } : {},
532
+ intent,
533
+ insertion,
534
+ ...placement ? { placement } : {},
535
+ ...separator ? { separator } : {},
536
+ ...policy ? { policy } : {}
537
+ };
538
+ const operationFingerprint = createIntentWriteOperationFingerprint(serverUrl, writeToken, tools.appendNoteMarkdown, payload);
539
+ if (dryRun) {
540
+ const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/append-markdown", {
541
+ ...payload,
542
+ dryRun: true
543
+ });
544
+ return {
545
+ content: [{
546
+ type: "text",
547
+ text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
548
+ affectedIds: [id],
549
+ operationFingerprint,
550
+ summary: intent,
551
+ toolName: tools.appendNoteMarkdown
552
+ }), null, 2)
553
+ }]
554
+ };
555
+ }
556
+ const confirmedOperation = requireConfirmedWriteOperation(
557
+ writeSafety,
558
+ { dryRun, operationId, confirmToken },
559
+ {
560
+ affectedIds: [id],
561
+ operationFingerprint,
562
+ summary: intent,
563
+ toolName: tools.appendNoteMarkdown
564
+ }
565
+ );
566
+ const result = await executeConfirmedWrite(
567
+ writeSafety,
568
+ confirmedOperation,
569
+ () => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/append-markdown", {
570
+ ...payload,
571
+ dryRun: false
572
+ })
573
+ );
574
+ return {
575
+ content: [{
576
+ type: "text",
577
+ text: JSON.stringify(result, null, 2)
578
+ }]
579
+ };
580
+ }
581
+ );
582
+ server.tool(
583
+ tools.updateNoteMetadata,
584
+ "Update note title/layout/properties. 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.",
585
+ {
586
+ id: z2.string().describe("Note ID to update"),
587
+ expectedUpdatedAt: z2.string().describe("Expected note updatedAt from a prior read."),
588
+ title: z2.string().optional().describe("New note title"),
589
+ layout: z2.enum(["narrow", "wide", "full"]).optional().describe("New note layout"),
590
+ properties: metadataPropertyPatchSchema.describe("Patch existing shared property values. Include set and/or deleteKeys; empty patches are rejected."),
591
+ ...confirmedMcpWriteFields
592
+ },
593
+ async ({ id, expectedUpdatedAt, title, layout, properties, dryRun, operationId, confirmToken }) => {
594
+ const writeToken = requireWriteToken2(token, tools.updateNoteMetadata);
595
+ const payload = {
596
+ id,
597
+ expectedUpdatedAt,
598
+ ...title !== void 0 ? { title } : {},
599
+ ...layout ? { layout } : {},
600
+ ...properties ? { properties } : {}
601
+ };
602
+ const summary = properties ? `Update note ${id} metadata and properties` : `Update note ${id} metadata`;
603
+ const operationFingerprint = createIntentWriteOperationFingerprint(serverUrl, writeToken, tools.updateNoteMetadata, payload);
604
+ if (dryRun) {
605
+ const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/metadata", {
606
+ ...payload,
607
+ dryRun: true
608
+ });
609
+ return {
610
+ content: [{
611
+ type: "text",
612
+ text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
613
+ affectedIds: [id],
614
+ operationFingerprint,
615
+ summary,
616
+ toolName: tools.updateNoteMetadata
617
+ }), null, 2)
618
+ }]
619
+ };
620
+ }
621
+ const confirmedOperation = requireConfirmedWriteOperation(
622
+ writeSafety,
623
+ { dryRun, operationId, confirmToken },
624
+ {
625
+ affectedIds: [id],
626
+ operationFingerprint,
627
+ summary,
628
+ toolName: tools.updateNoteMetadata
629
+ }
630
+ );
631
+ const result = await executeConfirmedWrite(
632
+ writeSafety,
633
+ confirmedOperation,
634
+ () => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/metadata", {
635
+ ...payload,
636
+ dryRun: false
637
+ })
638
+ );
639
+ return {
640
+ content: [{
641
+ type: "text",
642
+ text: JSON.stringify(result, null, 2)
643
+ }]
644
+ };
645
+ }
646
+ );
647
+ server.tool(
648
+ tools.replaceNoteMarkdown,
649
+ "Replace a note body as a high-impact full overwrite. Dry-run returns a full diff; commit requires operationId and confirmToken.",
650
+ {
651
+ id: z2.string().describe("Note ID to replace"),
652
+ expectedUpdatedAt: z2.string().optional().describe("Expected note updatedAt from a prior read. Required unless baseMarkdownSha256 is provided."),
653
+ baseMarkdownSha256: z2.string().optional().describe("SHA-256 of the current markdown. Required unless expectedUpdatedAt is provided."),
654
+ intent: z2.string().describe("Human-readable reason for the full replace."),
655
+ replacement: z2.string().describe("Complete replacement markdown body. Tags are body tokens such as [@tag] or [#tag]."),
656
+ policy: markdownWritePolicySchema,
657
+ ...confirmedMcpWriteFields
658
+ },
659
+ async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, replacement, policy, dryRun, operationId, confirmToken }) => {
660
+ const writeToken = requireWriteToken2(token, tools.replaceNoteMarkdown);
661
+ const payload = {
662
+ id,
663
+ ...expectedUpdatedAt ? { expectedUpdatedAt } : {},
664
+ ...baseMarkdownSha256 ? { baseMarkdownSha256 } : {},
665
+ intent,
666
+ replacement,
667
+ ...policy ? { policy } : {}
668
+ };
669
+ const operationFingerprint = createIntentWriteOperationFingerprint(serverUrl, writeToken, tools.replaceNoteMarkdown, payload);
670
+ if (dryRun) {
671
+ const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/replace-markdown", {
672
+ ...payload,
673
+ dryRun: true
674
+ });
675
+ return {
676
+ content: [{
677
+ type: "text",
678
+ text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
679
+ affectedIds: [id],
680
+ operationFingerprint,
681
+ summary: intent,
682
+ toolName: tools.replaceNoteMarkdown
683
+ }), null, 2)
684
+ }]
685
+ };
686
+ }
687
+ const confirmedOperation = requireConfirmedWriteOperation(
688
+ writeSafety,
689
+ { dryRun, operationId, confirmToken },
690
+ {
691
+ affectedIds: [id],
692
+ operationFingerprint,
693
+ summary: intent,
694
+ toolName: tools.replaceNoteMarkdown
695
+ }
696
+ );
697
+ const result = await executeConfirmedWrite(
698
+ writeSafety,
699
+ confirmedOperation,
700
+ () => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/replace-markdown", {
701
+ ...payload,
702
+ dryRun: false
703
+ })
704
+ );
705
+ return {
706
+ content: [{
707
+ type: "text",
708
+ text: JSON.stringify(result, null, 2)
709
+ }]
710
+ };
711
+ }
712
+ );
713
+ };
714
+
715
+ // src/mcp-property-query-output.ts
716
+ var formatPropertyQueryResponse = ({
717
+ result,
718
+ query,
719
+ includeProperties,
720
+ propertyKeys
721
+ }) => ({
722
+ query: {
723
+ ...query,
724
+ includeProperties,
725
+ propertyKeys
726
+ },
727
+ totalCount: result.totalCount,
728
+ notes: result.notes.map((note) => ({
729
+ id: note.id,
730
+ title: note.title,
731
+ createdAt: note.createdAt,
732
+ updatedAt: note.updatedAt,
733
+ tags: note.tags.map((item) => item.name),
734
+ ...includeProperties ? {
735
+ properties: propertyKeys.length > 0 ? (note.properties ?? []).filter((property) => propertyKeys.includes(property.key)) : note.properties ?? []
736
+ } : {}
737
+ }))
738
+ });
739
+
246
740
  // src/mcp.ts
247
741
  var __dirname = path2.dirname(fileURLToPath(import.meta.url));
248
742
  var pkg = JSON.parse(
@@ -253,7 +747,13 @@ var OCEAN_BRAIN_MCP_TOOLS = {
253
747
  readNote: "ocean_brain_read_note",
254
748
  createNote: "ocean_brain_create_note",
255
749
  updateNote: "ocean_brain_update_note",
750
+ patchNoteMarkdown: "ocean_brain_patch_note_markdown",
751
+ appendNoteMarkdown: "ocean_brain_append_note_markdown",
752
+ updateNoteMetadata: "ocean_brain_update_note_metadata",
753
+ replaceNoteMarkdown: "ocean_brain_replace_note_markdown",
256
754
  listTags: "ocean_brain_list_tags",
755
+ listProperties: "ocean_brain_list_properties",
756
+ queryNotesByProperties: "ocean_brain_query_notes_by_properties",
257
757
  listNotesByTag: "ocean_brain_list_notes_by_tag",
258
758
  listNotesByTags: "ocean_brain_list_notes_by_tags",
259
759
  listRecentNotes: "ocean_brain_list_recent_notes",
@@ -262,8 +762,18 @@ var OCEAN_BRAIN_MCP_TOOLS = {
262
762
  createTag: "ocean_brain_create_tag",
263
763
  deleteNote: "ocean_brain_delete_note"
264
764
  };
265
- var noteLayoutSchema = z2.enum(["narrow", "wide", "full"]);
266
- var tagMatchModeSchema = z2.enum(["and", "or"]);
765
+ var noteLayoutSchema = z3.enum(["narrow", "wide", "full"]);
766
+ var tagMatchModeSchema = z3.enum(["and", "or"]);
767
+ var propertyValueTypeSchema = z3.enum(["text", "url", "number", "date", "boolean", "select"]);
768
+ var propertyFilterOperatorSchema = z3.enum(["equals", "before", "after", "exists", "notExists"]);
769
+ var viewSortBySchema = z3.enum(["updatedAt", "createdAt", "title"]);
770
+ var viewSortOrderSchema = z3.enum(["asc", "desc"]);
771
+ var propertyFilterSchema = z3.object({
772
+ key: z3.string().describe("Property key from ocean_brain_list_properties, e.g. state"),
773
+ valueType: propertyValueTypeSchema.describe("Property value type from the property definition"),
774
+ operator: propertyFilterOperatorSchema.describe("Filter operator. before/after are only valid for date or number properties."),
775
+ value: z3.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).")
776
+ });
267
777
  var normalizeOceanBrainTagName = (name) => {
268
778
  const trimmedName = name.trim();
269
779
  if (!trimmedName) {
@@ -349,8 +859,8 @@ async function startMcpServer(serverUrl, token, options = {}) {
349
859
  OCEAN_BRAIN_MCP_TOOLS.searchNotes,
350
860
  "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.",
351
861
  {
352
- query: z2.string().describe("Search keyword"),
353
- limit: z2.number().optional().default(10).describe("Max results (default: 10)")
862
+ query: z3.string().describe("Search keyword"),
863
+ limit: z3.number().optional().default(10).describe("Max results (default: 10)")
354
864
  },
355
865
  async ({ query, limit }) => {
356
866
  const data = await graphql(serverUrl, token, `
@@ -388,10 +898,10 @@ async function startMcpServer(serverUrl, token, options = {}) {
388
898
  );
389
899
  server.tool(
390
900
  OCEAN_BRAIN_MCP_TOOLS.readNote,
391
- "Read an Ocean Brain note by ID. Returns truncated content by default (1000 chars) and includes a back-reference summary. Set maxLength to 0 only when full content is necessary.",
901
+ "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.",
392
902
  {
393
- id: z2.string().describe("Note ID"),
394
- maxLength: z2.number().optional().default(1e3).describe("Max content length in characters. 0 for full content. (default: 1000)")
903
+ id: z3.string().describe("Note ID"),
904
+ maxLength: z3.number().optional().default(1e3).describe("Max content length in characters. 0 for full content. (default: 1000)")
395
905
  },
396
906
  async ({ id, maxLength }) => {
397
907
  const data = await graphql(serverUrl, token, `
@@ -403,6 +913,7 @@ async function startMcpServer(serverUrl, token, options = {}) {
403
913
  createdAt
404
914
  updatedAt
405
915
  tags { id name }
916
+ properties { key name value valueType option { id label value color order } }
406
917
  }
407
918
  backReferences(id: $id) {
408
919
  id
@@ -427,10 +938,10 @@ async function startMcpServer(serverUrl, token, options = {}) {
427
938
  );
428
939
  server.tool(
429
940
  OCEAN_BRAIN_MCP_TOOLS.createNote,
430
- "Create an Ocean Brain note from markdown through the MCP write path. In MCP markdown, use explicit tags like [@project] or [#project], and use [[Note Title]] for note links. When writing linked notes, prefer creating or organizing the reference target note first. A single link is enough for Ocean Brain to surface a back reference on the other note.",
941
+ "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.",
431
942
  {
432
- title: z2.string().describe("Note title"),
433
- 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."),
943
+ title: z3.string().describe("Note title"),
944
+ markdown: z3.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."),
434
945
  layout: noteLayoutSchema.optional().describe("Optional note layout: narrow, wide, or full. Prefer wide for most notes unless the user explicitly wants narrow or full.")
435
946
  },
436
947
  async ({ title, markdown, layout }) => {
@@ -450,11 +961,11 @@ async function startMcpServer(serverUrl, token, options = {}) {
450
961
  );
451
962
  server.tool(
452
963
  OCEAN_BRAIN_MCP_TOOLS.updateNote,
453
- "Update an Ocean Brain note from markdown through the MCP write path. In MCP markdown, use explicit tags like [@project] or [#project], and use [[Note Title]] for note links. When you add linked notes, prefer organizing the reference target note first. A single link is enough for Ocean Brain to surface a back reference on the other note.",
964
+ "Legacy full-field note update through the MCP write path. Prefer patch/append/metadata tools for small changes; use this only when intentionally replacing provided fields.",
454
965
  {
455
- id: z2.string().describe("Note ID to update"),
456
- title: z2.string().optional().describe("New note title"),
457
- markdown: z2.string().optional().describe("Replacement markdown body. In MCP markdown, tags must use [@tag] or [#tag]."),
966
+ id: z3.string().describe("Note ID to update"),
967
+ title: z3.string().optional().describe("New note title"),
968
+ markdown: z3.string().optional().describe("Replacement markdown body. In MCP markdown, tags must use [@tag] or [#tag]."),
458
969
  layout: noteLayoutSchema.optional().describe("Optional note layout: narrow, wide, or full. Prefer wide for most notes unless the user explicitly wants narrow or full.")
459
970
  },
460
971
  async ({ id, title, markdown, layout }) => {
@@ -473,6 +984,14 @@ async function startMcpServer(serverUrl, token, options = {}) {
473
984
  };
474
985
  }
475
986
  );
987
+ registerIntentWriteTools(server, {
988
+ jsonRequest,
989
+ requireWriteToken,
990
+ serverUrl,
991
+ token,
992
+ tools: OCEAN_BRAIN_MCP_TOOLS,
993
+ writeSafety
994
+ });
476
995
  server.tool(
477
996
  OCEAN_BRAIN_MCP_TOOLS.listTags,
478
997
  "List Ocean Brain tags with their note counts.",
@@ -502,13 +1021,52 @@ async function startMcpServer(serverUrl, token, options = {}) {
502
1021
  };
503
1022
  }
504
1023
  );
1024
+ server.tool(
1025
+ OCEAN_BRAIN_MCP_TOOLS.listProperties,
1026
+ "List shared Ocean Brain property definitions. Use this before property queries so keys, value types, and select option values are valid.",
1027
+ {
1028
+ query: z3.string().optional().default("").describe("Optional property key/name search query"),
1029
+ limit: z3.number().optional().default(50).describe("Max results (default: 50, server max: 100)"),
1030
+ offset: z3.number().optional().default(0).describe("Pagination offset (default: 0)")
1031
+ },
1032
+ async ({ query, limit, offset }) => {
1033
+ const data = await graphql(serverUrl, token, `
1034
+ query ($query: String, $pagination: PaginationInput) {
1035
+ notePropertyKeys(query: $query, pagination: $pagination) {
1036
+ totalCount
1037
+ keys {
1038
+ key
1039
+ name
1040
+ valueType
1041
+ noteCount
1042
+ updatedAt
1043
+ options { id label value color order }
1044
+ }
1045
+ }
1046
+ }
1047
+ `, {
1048
+ query,
1049
+ pagination: { limit, offset }
1050
+ });
1051
+ const result = data?.notePropertyKeys;
1052
+ return {
1053
+ content: [{
1054
+ type: "text",
1055
+ text: JSON.stringify({
1056
+ totalCount: result.totalCount,
1057
+ propertyKeys: result.keys
1058
+ }, null, 2)
1059
+ }]
1060
+ };
1061
+ }
1062
+ );
505
1063
  server.tool(
506
1064
  OCEAN_BRAIN_MCP_TOOLS.listNotesByTag,
507
1065
  "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.",
508
1066
  {
509
- tag: z2.string().describe("Tag name to inspect. You can pass @project or project."),
510
- limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
511
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
1067
+ tag: z3.string().describe("Tag name to inspect. You can pass @project or project."),
1068
+ limit: z3.number().optional().default(20).describe("Max results (default: 20)"),
1069
+ offset: z3.number().optional().default(0).describe("Pagination offset (default: 0)")
512
1070
  },
513
1071
  async ({ tag, limit, offset }) => {
514
1072
  const normalizedTag = normalizeOceanBrainTagName(tag);
@@ -571,10 +1129,10 @@ async function startMcpServer(serverUrl, token, options = {}) {
571
1129
  OCEAN_BRAIN_MCP_TOOLS.listNotesByTags,
572
1130
  "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.",
573
1131
  {
574
- tags: z2.array(z2.string()).min(1).describe("Tag names to inspect. You can pass @project or project values."),
1132
+ tags: z3.array(z3.string()).min(1).describe("Tag names to inspect. You can pass @project or project values."),
575
1133
  mode: tagMatchModeSchema.optional().default("and").describe("Tag match mode. Use and for intersection, or for union. Defaults to and."),
576
- limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
577
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
1134
+ limit: z3.number().optional().default(20).describe("Max results (default: 20)"),
1135
+ offset: z3.number().optional().default(0).describe("Pagination offset (default: 0)")
578
1136
  },
579
1137
  async ({ tags, mode, limit, offset }) => {
580
1138
  const normalizedTags = normalizeOceanBrainTagNames(tags);
@@ -644,11 +1202,78 @@ async function startMcpServer(serverUrl, token, options = {}) {
644
1202
  };
645
1203
  }
646
1204
  );
1205
+ server.tool(
1206
+ OCEAN_BRAIN_MCP_TOOLS.queryNotesByProperties,
1207
+ "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.",
1208
+ {
1209
+ propertyFilters: z3.array(propertyFilterSchema).min(1).max(10).describe("Required property filters. Multiple property filters are combined with AND."),
1210
+ tagNames: z3.array(z3.string()).optional().default([]).describe("Optional tag filters. You can pass @project, #project, or project."),
1211
+ mode: tagMatchModeSchema.optional().default("and").describe("Tag match mode for tagNames only. Property filters are always combined with AND."),
1212
+ sortBy: viewSortBySchema.optional().default("updatedAt").describe("Sort field (default: updatedAt)"),
1213
+ sortOrder: viewSortOrderSchema.optional().default("desc").describe("Sort order (default: desc)"),
1214
+ includeProperties: z3.boolean().optional().default(false).describe("Include returned note properties. Defaults to false to reduce tokens."),
1215
+ propertyKeys: z3.array(z3.string()).optional().default([]).describe("Property keys to include in output; automatically includes properties."),
1216
+ limit: z3.number().optional().default(20).describe("Max results (default: 20, server max: 50)"),
1217
+ offset: z3.number().optional().default(0).describe("Pagination offset (default: 0)")
1218
+ },
1219
+ async ({ propertyFilters, tagNames, mode, sortBy, sortOrder, includeProperties, propertyKeys, limit, offset }) => {
1220
+ const shouldIncludeProperties = includeProperties || propertyKeys.length > 0;
1221
+ const data = await graphql(serverUrl, token, `
1222
+ query ($input: NotesByPropertiesInput!, $pagination: PaginationInput, $includeProperties: Boolean!) {
1223
+ notesByProperties(input: $input, pagination: $pagination) {
1224
+ totalCount
1225
+ notes {
1226
+ id
1227
+ title
1228
+ createdAt
1229
+ updatedAt
1230
+ tags { id name }
1231
+ properties @include(if: $includeProperties) { key name value valueType option { id label value color order } }
1232
+ }
1233
+ }
1234
+ }
1235
+ `, {
1236
+ input: {
1237
+ tagNames,
1238
+ mode,
1239
+ propertyFilters,
1240
+ sortBy,
1241
+ sortOrder
1242
+ },
1243
+ pagination: { limit, offset },
1244
+ includeProperties: shouldIncludeProperties
1245
+ });
1246
+ const result = data?.notesByProperties;
1247
+ return {
1248
+ content: [{
1249
+ type: "text",
1250
+ text: JSON.stringify(
1251
+ formatPropertyQueryResponse({
1252
+ result,
1253
+ query: {
1254
+ propertyFilters,
1255
+ tagNames,
1256
+ mode,
1257
+ sortBy,
1258
+ sortOrder,
1259
+ limit,
1260
+ offset
1261
+ },
1262
+ includeProperties: shouldIncludeProperties,
1263
+ propertyKeys
1264
+ }),
1265
+ null,
1266
+ 2
1267
+ )
1268
+ }]
1269
+ };
1270
+ }
1271
+ );
647
1272
  server.tool(
648
1273
  OCEAN_BRAIN_MCP_TOOLS.listRecentNotes,
649
1274
  "List recently updated Ocean Brain notes. Returns titles and tags only. Use ocean_brain_read_note to get full content for a specific note.",
650
1275
  {
651
- limit: z2.number().optional().default(10).describe("Max results (default: 10)")
1276
+ limit: z3.number().optional().default(10).describe("Max results (default: 10)")
652
1277
  },
653
1278
  async ({ limit }) => {
654
1279
  const data = await graphql(serverUrl, token, `
@@ -696,6 +1321,10 @@ async function startMcpServer(serverUrl, token, options = {}) {
696
1321
  writeTools: [
697
1322
  OCEAN_BRAIN_MCP_TOOLS.createNote,
698
1323
  OCEAN_BRAIN_MCP_TOOLS.updateNote,
1324
+ OCEAN_BRAIN_MCP_TOOLS.patchNoteMarkdown,
1325
+ OCEAN_BRAIN_MCP_TOOLS.appendNoteMarkdown,
1326
+ OCEAN_BRAIN_MCP_TOOLS.updateNoteMetadata,
1327
+ OCEAN_BRAIN_MCP_TOOLS.replaceNoteMarkdown,
699
1328
  OCEAN_BRAIN_MCP_TOOLS.createTag,
700
1329
  OCEAN_BRAIN_MCP_TOOLS.deleteNote
701
1330
  ],
@@ -709,9 +1338,9 @@ async function startMcpServer(serverUrl, token, options = {}) {
709
1338
  OCEAN_BRAIN_MCP_TOOLS.findNoteCleanupCandidates,
710
1339
  "Find Ocean Brain note cleanup candidates for temporary or draft notes before deletion. Use this before ocean_brain_delete_note.",
711
1340
  {
712
- keywords: z2.string().optional().default("temp tmp draft test wip").describe("Keywords that mark a note as a cleanup candidate. Comma or space separated."),
713
- limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
714
- offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
1341
+ keywords: z3.string().optional().default("temp tmp draft test wip").describe("Keywords that mark a note as a cleanup candidate. Comma or space separated."),
1342
+ limit: z3.number().optional().default(20).describe("Max results (default: 20)"),
1343
+ offset: z3.number().optional().default(0).describe("Pagination offset (default: 0)")
715
1344
  },
716
1345
  async ({ keywords, limit, offset }) => {
717
1346
  const normalizedKeywords = keywords.split(/[,\s]+/).map((keyword) => keyword.trim()).filter(Boolean);
@@ -754,7 +1383,7 @@ async function startMcpServer(serverUrl, token, options = {}) {
754
1383
  OCEAN_BRAIN_MCP_TOOLS.createTag,
755
1384
  "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.",
756
1385
  {
757
- name: z2.string().describe("Tag name to create. You can pass @project or project.")
1386
+ name: z3.string().describe("Tag name to create. You can pass @project or project.")
758
1387
  },
759
1388
  async ({ name }) => {
760
1389
  const writeToken = requireWriteToken(token, OCEAN_BRAIN_MCP_TOOLS.createTag);
@@ -773,7 +1402,7 @@ async function startMcpServer(serverUrl, token, options = {}) {
773
1402
  OCEAN_BRAIN_MCP_TOOLS.deleteNote,
774
1403
  "Delete an Ocean Brain note safely through a dry-run and confirmation flow. Start with dryRun=true, then call again with dryRun=false, operationId, and confirmToken.",
775
1404
  {
776
- id: z2.string().describe("Note ID to delete"),
1405
+ id: z3.string().describe("Note ID to delete"),
777
1406
  ...destructiveMcpWriteFields
778
1407
  },
779
1408
  async ({ id, dryRun, operationId, confirmToken, force }) => {