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.
- package/dist/mcp.js +660 -31
- package/package.json +2 -1
- package/server/client/dist/assets/{Calendar-BU9XgX2N.js → Calendar-BytW4PeT.js} +1 -1
- package/server/client/dist/assets/{Callout-C6tVCxOT.js → Callout-9sJgGNRb.js} +1 -1
- package/server/client/dist/assets/Graph-o5u-NzrA.js +1 -0
- package/server/client/dist/assets/{Image.es-BjGK7y6Y.js → Image.es-B0LL3pb7.js} +1 -1
- package/server/client/dist/assets/Note-CYoYzZNu.js +21 -0
- package/server/client/dist/assets/{Plus.es-EcmtS_uw.js → Plus.es-DpxzgRtY.js} +1 -1
- package/server/client/dist/assets/Reminders-DAepTZg1.js +1 -0
- package/server/client/dist/assets/Search-6KH7An44.js +1 -0
- package/server/client/dist/assets/{SurfaceCard-CO0oVxeL.js → SurfaceCard-EMs50xSF.js} +1 -1
- package/server/client/dist/assets/Tag-C89j8x56.js +1 -0
- package/server/client/dist/assets/TagNotes-BTbu-f6v.js +1 -0
- package/server/client/dist/assets/{Trash.es-DmwfSy3l.js → Trash.es-C5x35NKF.js} +1 -1
- package/server/client/dist/assets/ViewNotes-DPEuki3y.js +1 -0
- package/server/client/dist/assets/Views-BGkuNhVd.js +1 -0
- package/server/client/dist/assets/{image.api-DeF9ck7g.js → image.api-DRhRQ0Gf.js} +1 -1
- package/server/client/dist/assets/{index-BZUGWdOU.css → index-Cd81UxEh.css} +1 -1
- package/server/client/dist/assets/{index-BvKBTjQq.js → index-DYlYUrjH.js} +118 -37
- package/server/client/dist/assets/index-DswSFFMb.js +1 -0
- package/server/client/dist/assets/manage-image-C6sHhWnI.js +1 -0
- package/server/client/dist/assets/manage-image-detail-CJkfybR9.js +1 -0
- package/server/client/dist/assets/{mcp-Bh743HBz.js → mcp-piouFDNm.js} +1 -1
- package/server/client/dist/assets/{placeholder-XQqo1Fvr.js → placeholder-DGz3CHej.js} +5 -5
- package/server/client/dist/assets/properties-CM4khczg.js +1 -0
- package/server/client/dist/assets/trash-C2lItz-b.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-D21PurIS.js → useReminderMutate-DHjDDHKg.js} +1 -1
- package/server/client/dist/assets/view-dashboard-C17WsdL7.js +99 -0
- package/server/client/dist/index.html +2 -2
- package/server/dist/features/note/graphql/note.field.resolver.js +2 -0
- package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.mutation.resolver.js +119 -0
- package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.query.resolver.js +14 -0
- package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.type-defs.js +100 -0
- package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
- package/server/dist/features/note/http/mcp.js +394 -1
- package/server/dist/features/note/http/mcp.js.map +1 -1
- package/server/dist/features/note/services/markdown-intent-write.js +478 -0
- package/server/dist/features/note/services/markdown-intent-write.js.map +1 -0
- package/server/dist/features/note/services/markdown-patch.js +601 -0
- package/server/dist/features/note/services/markdown-patch.js.map +1 -0
- package/server/dist/features/note/services/properties.js +874 -0
- package/server/dist/features/note/services/properties.js.map +1 -0
- package/server/dist/features/note/services/snapshot.js +94 -12
- package/server/dist/features/note/services/snapshot.js.map +1 -1
- package/server/dist/features/note/services/trash.js +70 -2
- package/server/dist/features/note/services/trash.js.map +1 -1
- package/server/dist/features/note/services/write.js +21 -7
- package/server/dist/features/note/services/write.js.map +1 -1
- package/server/dist/features/view/graphql/view.query.resolver.js +31 -1
- package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -1
- package/server/dist/features/view/graphql/view.type-defs.js +57 -1
- package/server/dist/features/view/graphql/view.type-defs.js.map +1 -1
- package/server/dist/features/view/services/workspace.js +367 -43
- package/server/dist/features/view/services/workspace.js.map +1 -1
- package/server/dist/modules/blocknote.js +47 -2
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/routes/mcp.js +21 -1
- package/server/dist/routes/mcp.js.map +1 -1
- package/server/prisma/migrations/20260527073000_0017_note_properties/migration.sql +105 -0
- package/server/prisma/migrations/20260531073000_0018_view_query/migration.sql +3 -0
- package/server/prisma/schema.prisma +226 -130
- package/server/client/dist/assets/Graph-Dond2hGK.js +0 -1
- package/server/client/dist/assets/Note-Cr6GtXUM.js +0 -21
- package/server/client/dist/assets/Reminders-BlLLT_x0.js +0 -1
- package/server/client/dist/assets/Search-CrZ0ap8x.js +0 -1
- package/server/client/dist/assets/Tag-vws5AfNW.js +0 -1
- package/server/client/dist/assets/TagNotes-Fmf1Kgim.js +0 -1
- package/server/client/dist/assets/ViewNotes-BALN7IfH.js +0 -1
- package/server/client/dist/assets/Views-Ccc_Ml5U.js +0 -1
- package/server/client/dist/assets/index-BSVE2cGd.js +0 -1
- package/server/client/dist/assets/manage-image-IoTXaMBP.js +0 -1
- package/server/client/dist/assets/manage-image-detail-BRTXwOsh.js +0 -1
- package/server/client/dist/assets/trash-hiJIg6-c.js +0 -1
- 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
|
|
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.
|
|
19
|
-
operationId: z.string().optional().describe("
|
|
20
|
-
confirmToken: z.string().optional().describe("
|
|
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, {
|
|
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 =
|
|
266
|
-
var tagMatchModeSchema =
|
|
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:
|
|
353
|
-
limit:
|
|
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
|
|
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:
|
|
394
|
-
maxLength:
|
|
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
|
|
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:
|
|
433
|
-
markdown:
|
|
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
|
-
"
|
|
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:
|
|
456
|
-
title:
|
|
457
|
-
markdown:
|
|
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:
|
|
510
|
-
limit:
|
|
511
|
-
offset:
|
|
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:
|
|
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:
|
|
577
|
-
offset:
|
|
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:
|
|
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:
|
|
713
|
-
limit:
|
|
714
|
-
offset:
|
|
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:
|
|
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:
|
|
1405
|
+
id: z3.string().describe("Note ID to delete"),
|
|
777
1406
|
...destructiveMcpWriteFields
|
|
778
1407
|
},
|
|
779
1408
|
async ({ id, dryRun, operationId, confirmToken, force }) => {
|