ocean-brain 0.7.4 → 0.8.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/index.js +3 -3
- package/dist/mcp.js +180 -736
- package/package.json +4 -1
- package/server/client/dist/assets/ArrowRight.es-C4zlc54Z.js +1 -0
- package/server/client/dist/assets/{Calendar-Bz7jheg6.js → Calendar-COrN6Hjv.js} +2 -2
- package/server/client/dist/assets/Callout-BK0K9E74.js +1 -0
- package/server/client/dist/assets/Copy.es-zm8f_XXX.js +1 -0
- package/server/client/dist/assets/Graph-BH1fzhue.js +1 -0
- package/server/client/dist/assets/{Image-DxWhlIDG.js → Image-KXuHOG4b.js} +1 -1
- package/server/client/dist/assets/Image.es-BKahRbIm.js +1 -0
- package/server/client/dist/assets/Note-C143cKKw.js +12 -0
- package/server/client/dist/assets/Plus.es-BqeC8T8h.js +1 -0
- package/server/client/dist/assets/Reminders-BYG5rz1M.js +1 -0
- package/server/client/dist/assets/Search-DBWsIMDP.js +1 -0
- package/server/client/dist/assets/{SurfaceCard-CfL1jD-O.js → SurfaceCard-CNSQfYfM.js} +1 -1
- package/server/client/dist/assets/Tag-5aCfSO8Y.js +1 -0
- package/server/client/dist/assets/TagNotes-BPZjN-6l.js +1 -0
- package/server/client/dist/assets/Trash.es-CRQkTnXZ.js +1 -0
- package/server/client/dist/assets/ViewNotes-DGLNF96x.js +1 -0
- package/server/client/dist/assets/ViewSectionDialog-CzskgmMF.js +102 -0
- package/server/client/dist/assets/Views-DR5jT23G.js +1 -0
- package/server/client/dist/assets/{graph-vendor-CUxe67Lr.js → graph-vendor-UTvF2NS_.js} +2 -2
- package/server/client/dist/assets/image.api-8dlMwzMd.js +17 -0
- package/server/client/dist/assets/{index-C5BGzDgt.css → index-BEUqDuAZ.css} +1 -1
- package/server/client/dist/assets/{index-DiMt3Dz4.js → index-DGIAh3ZY.js} +1 -1
- package/server/client/dist/assets/index-VHp17GQX.js +342 -0
- package/server/client/dist/assets/manage-image-RqtWKas2.js +1 -0
- package/server/client/dist/assets/{manage-image-detail-Bqw8NURO.js → manage-image-detail-BTSIbLlQ.js} +1 -1
- package/server/client/dist/assets/mcp-EDurbggR.js +4 -0
- package/server/client/dist/assets/mcp-admin-adapter-DmGUo5G8.js +1 -0
- package/server/client/dist/assets/{note-core-Z6kqulGB.js → note-core-BklwyZHk.js} +1 -1
- package/server/client/dist/assets/{note-vendor-i1M2FSI2.js → note-vendor-DkYKUmLo.js} +1 -1
- package/server/client/dist/assets/{placeholder-Ro00byT7.js → placeholder-DeKgSBnC.js} +4 -4
- package/server/client/dist/assets/{properties-Tid6-ghT.js → properties-BjrZvOYn.js} +1 -1
- package/server/client/dist/assets/tag.api-Dl8DQd0m.js +21 -0
- package/server/client/dist/assets/trash-C5ntddLu.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-BWymIujb.js → useReminderMutate-CoXXqJY2.js} +1 -1
- package/server/client/dist/index.html +5 -5
- package/server/dist/app.js +2 -2
- package/server/dist/app.js.map +1 -1
- package/server/dist/features/auth/http/api.js +5 -1
- package/server/dist/features/auth/http/api.js.map +1 -1
- package/server/dist/features/auth/http/login-page.js +1 -1
- package/server/dist/features/auth/http/pages.js +8 -1
- package/server/dist/features/auth/http/pages.js.map +1 -1
- package/server/dist/features/auth/service.js +16 -7
- package/server/dist/features/auth/service.js.map +1 -1
- package/server/dist/features/image/http/upload.js +34 -23
- package/server/dist/features/image/http/upload.js.map +1 -1
- package/server/dist/features/image/services/validation.js +111 -0
- package/server/dist/features/image/services/validation.js.map +1 -0
- package/server/dist/features/mcp-admin/http/handlers.js +11 -3
- package/server/dist/features/mcp-admin/http/handlers.js.map +1 -1
- package/server/dist/features/note/http/mcp.js +8 -43
- package/server/dist/features/note/http/mcp.js.map +1 -1
- package/server/dist/features/note/services/cleanup.js +2 -2
- package/server/dist/features/note/services/cleanup.js.map +1 -1
- package/server/dist/features/note/services/markdown-intent-write.js +11 -43
- package/server/dist/features/note/services/markdown-intent-write.js.map +1 -1
- package/server/dist/features/note/services/markdown-patch.js +4 -123
- package/server/dist/features/note/services/markdown-patch.js.map +1 -1
- package/server/dist/features/note/services/write.js +13 -13
- package/server/dist/features/note/services/write.js.map +1 -1
- package/server/dist/features/tag/graphql/tag.query.resolver.js +16 -1
- package/server/dist/features/tag/graphql/tag.query.resolver.js.map +1 -1
- package/server/dist/features/tag/graphql/tag.type-defs.js +2 -0
- package/server/dist/features/tag/graphql/tag.type-defs.js.map +1 -1
- package/server/dist/modules/app-version.js +105 -0
- package/server/dist/modules/app-version.js.map +1 -0
- package/server/dist/modules/auth-guard.js +21 -2
- package/server/dist/modules/auth-guard.js.map +1 -1
- package/server/dist/modules/auth-redirect.js +10 -0
- package/server/dist/modules/auth-redirect.js.map +1 -0
- package/server/dist/modules/mcp-auth.js +62 -1
- package/server/dist/modules/mcp-auth.js.map +1 -1
- package/server/dist/modules/rate-limit.js +15 -1
- package/server/dist/modules/rate-limit.js.map +1 -1
- package/server/dist/modules/server-events-handler.js +17 -0
- package/server/dist/modules/server-events-handler.js.map +1 -1
- package/server/dist/modules/session-store.js +149 -0
- package/server/dist/modules/session-store.js.map +1 -0
- package/server/dist/paths.js +1 -0
- package/server/dist/paths.js.map +1 -1
- package/server/dist/routes/api.js +39 -20
- package/server/dist/routes/api.js.map +1 -1
- package/server/dist/routes/auth-pages.js +13 -2
- package/server/dist/routes/auth-pages.js.map +1 -1
- package/server/dist/routes/client.js +63 -3
- package/server/dist/routes/client.js.map +1 -1
- package/server/dist/routes/graphql.js +23 -19
- package/server/dist/routes/graphql.js.map +1 -1
- package/server/client/dist/assets/Callout-DlFLkTCG.js +0 -1
- package/server/client/dist/assets/Graph-BdxtJvog.js +0 -1
- package/server/client/dist/assets/Image.es-BFodqKS5.js +0 -1
- package/server/client/dist/assets/Note-BgMxsWZT.js +0 -22
- package/server/client/dist/assets/Plus.es-Yq7J1Ur4.js +0 -1
- package/server/client/dist/assets/Reminders-CUsR56ff.js +0 -1
- package/server/client/dist/assets/Search-DncrGgOZ.js +0 -1
- package/server/client/dist/assets/Tag-CKDLs2Y6.js +0 -1
- package/server/client/dist/assets/TagNotes-ByX_Qias.js +0 -1
- package/server/client/dist/assets/Trash.es-BPrF6TPV.js +0 -1
- package/server/client/dist/assets/ViewNotes-r1kxKZ8O.js +0 -1
- package/server/client/dist/assets/ViewSectionTableRenderer-DQ4UeCnm.js +0 -102
- package/server/client/dist/assets/Views-FzFBtNT2.js +0 -1
- package/server/client/dist/assets/image.api-Bcg3TFCG.js +0 -17
- package/server/client/dist/assets/index-BFfQKAUY.js +0 -361
- package/server/client/dist/assets/manage-image-DzDlncCM.js +0 -1
- package/server/client/dist/assets/mcp-BqasqTWC.js +0 -1
- package/server/client/dist/assets/trash-bHK02-Up.js +0 -1
- package/server/dist/features/image/services/remote-fetch.js +0 -176
- package/server/dist/features/image/services/remote-fetch.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -1,226 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/mcp.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
-
import { z as
|
|
10
|
-
|
|
11
|
-
// src/mcp-write-safety.ts
|
|
12
|
-
import crypto from "crypto";
|
|
13
|
-
import fs from "fs";
|
|
14
|
-
import os from "os";
|
|
15
|
-
import path from "path";
|
|
16
|
-
import { z } from "zod";
|
|
17
|
-
var destructiveMcpWriteFields = {
|
|
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
|
-
force: z.boolean().optional().default(false).describe("Explicitly confirm a bulk destructive change when the preview says force is required.")
|
|
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
|
-
};
|
|
28
|
-
var McpCliWriteSafetyError = class extends Error {
|
|
29
|
-
code;
|
|
30
|
-
constructor(code, message) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.name = "McpCliWriteSafetyError";
|
|
33
|
-
this.code = code;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
var DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
37
|
-
var DEFAULT_ROOT_DIR = path.resolve(os.homedir(), ".ocean-brain", "mcp-write-safety");
|
|
38
|
-
var createError = (code, message) => new McpCliWriteSafetyError(code, message);
|
|
39
|
-
var ensureDirExists = (dirPath) => {
|
|
40
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
41
|
-
};
|
|
42
|
-
var hashToken = (token) => crypto.createHash("sha256").update(token).digest("hex");
|
|
43
|
-
var toPublicPendingOperation = (operation) => ({
|
|
44
|
-
actor: operation.actor,
|
|
45
|
-
affectedIds: operation.affectedIds,
|
|
46
|
-
createdAt: operation.createdAt,
|
|
47
|
-
estimatedChangeCount: operation.estimatedChangeCount,
|
|
48
|
-
expiresAt: operation.expiresAt,
|
|
49
|
-
force: operation.force,
|
|
50
|
-
...operation.operationFingerprint ? { operationFingerprint: operation.operationFingerprint } : {},
|
|
51
|
-
operationId: operation.operationId,
|
|
52
|
-
risk: operation.risk,
|
|
53
|
-
summary: operation.summary,
|
|
54
|
-
toolName: operation.toolName
|
|
55
|
-
});
|
|
56
|
-
var parsePendingOperation = (raw) => JSON.parse(raw);
|
|
57
|
-
var defaultMcpWriteSafetyDir = () => {
|
|
58
|
-
return path.resolve(process.env.OCEAN_BRAIN_MCP_WRITE_SAFETY_DIR || DEFAULT_ROOT_DIR);
|
|
59
|
-
};
|
|
60
|
-
var formatMcpGraphqlError = (error) => {
|
|
61
|
-
const suffix = [];
|
|
62
|
-
if (error.extensions?.code) {
|
|
63
|
-
suffix.push(`code=${error.extensions.code}`);
|
|
64
|
-
}
|
|
65
|
-
if (error.extensions?.operationId) {
|
|
66
|
-
suffix.push(`operationId=${error.extensions.operationId}`);
|
|
67
|
-
}
|
|
68
|
-
if (suffix.length === 0) {
|
|
69
|
-
return `GraphQL error: ${error.message}`;
|
|
70
|
-
}
|
|
71
|
-
return `GraphQL error: ${error.message} (${suffix.join(", ")})`;
|
|
72
|
-
};
|
|
73
|
-
var createMcpWriteSafetyCoordinator = (options = {}) => {
|
|
74
|
-
const rootDir = path.resolve(options.rootDir ?? defaultMcpWriteSafetyDir());
|
|
75
|
-
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
76
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
77
|
-
const randomBytes = options.randomBytes ?? ((size) => crypto.randomBytes(size));
|
|
78
|
-
const pendingDir = path.resolve(rootDir, "pending");
|
|
79
|
-
const logPath = path.resolve(rootDir, "operations.jsonl");
|
|
80
|
-
const appendLog = (entry) => {
|
|
81
|
-
ensureDirExists(rootDir);
|
|
82
|
-
fs.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
83
|
-
`, "utf-8");
|
|
84
|
-
};
|
|
85
|
-
const buildLogEntry = (operation, phase, detail) => ({
|
|
86
|
-
actor: operation.actor,
|
|
87
|
-
affectedIds: operation.affectedIds,
|
|
88
|
-
estimatedChangeCount: operation.estimatedChangeCount,
|
|
89
|
-
force: operation.force,
|
|
90
|
-
operationId: operation.operationId,
|
|
91
|
-
phase,
|
|
92
|
-
risk: operation.risk,
|
|
93
|
-
summary: operation.summary,
|
|
94
|
-
timestamp: now().toISOString(),
|
|
95
|
-
toolName: operation.toolName,
|
|
96
|
-
...detail ? { detail } : {}
|
|
97
|
-
});
|
|
98
|
-
const getPendingPath = (operationId) => path.resolve(pendingDir, `${operationId}.json`);
|
|
99
|
-
const cleanupExpiredOperations = () => {
|
|
100
|
-
if (!fs.existsSync(pendingDir)) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
for (const fileName of fs.readdirSync(pendingDir)) {
|
|
104
|
-
const filePath = path.resolve(pendingDir, fileName);
|
|
105
|
-
const pendingOperation = parsePendingOperation(fs.readFileSync(filePath, "utf-8"));
|
|
106
|
-
if (new Date(pendingOperation.expiresAt).getTime() > now().getTime()) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
fs.rmSync(filePath, { force: true });
|
|
110
|
-
appendLog(buildLogEntry(toPublicPendingOperation(pendingOperation), "expired"));
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
const listPendingOperations = () => {
|
|
114
|
-
cleanupExpiredOperations();
|
|
115
|
-
if (!fs.existsSync(pendingDir)) {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
return fs.readdirSync(pendingDir).map((fileName) => path.resolve(pendingDir, fileName)).map((filePath) => parsePendingOperation(fs.readFileSync(filePath, "utf-8"))).map(toPublicPendingOperation).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
119
|
-
};
|
|
120
|
-
const prepareOperation = (input) => {
|
|
121
|
-
ensureDirExists(pendingDir);
|
|
122
|
-
const operationId = `mcpw_${randomBytes(8).toString("hex")}`;
|
|
123
|
-
const confirmToken = `confirm_${randomBytes(12).toString("hex")}`;
|
|
124
|
-
const currentTime = now();
|
|
125
|
-
const createdAt = currentTime.toISOString();
|
|
126
|
-
const expiresAt = new Date(currentTime.getTime() + ttlMs).toISOString();
|
|
127
|
-
const pendingOperation = {
|
|
128
|
-
actor: input.actor,
|
|
129
|
-
affectedIds: input.affectedIds,
|
|
130
|
-
confirmTokenHash: hashToken(confirmToken),
|
|
131
|
-
createdAt,
|
|
132
|
-
estimatedChangeCount: input.estimatedChangeCount,
|
|
133
|
-
expiresAt,
|
|
134
|
-
force: Boolean(input.force),
|
|
135
|
-
...input.operationFingerprint ? { operationFingerprint: input.operationFingerprint } : {},
|
|
136
|
-
operationId,
|
|
137
|
-
risk: input.risk,
|
|
138
|
-
summary: input.summary,
|
|
139
|
-
toolName: input.toolName
|
|
140
|
-
};
|
|
141
|
-
fs.writeFileSync(getPendingPath(operationId), JSON.stringify(pendingOperation, null, 2), "utf-8");
|
|
142
|
-
appendLog(buildLogEntry(toPublicPendingOperation(pendingOperation), "prepared"));
|
|
143
|
-
return {
|
|
144
|
-
...toPublicPendingOperation(pendingOperation),
|
|
145
|
-
confirmToken
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
const requireConfirmedOperation = (request, expected) => {
|
|
149
|
-
if (!request.operationId || !request.confirmToken) {
|
|
150
|
-
throw createError(
|
|
151
|
-
"CONFIRMATION_REQUIRED",
|
|
152
|
-
"A destructive MCP write requires operationId and confirmToken after a dry run."
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
const pendingPath = getPendingPath(request.operationId);
|
|
156
|
-
if (!fs.existsSync(pendingPath)) {
|
|
157
|
-
throw createError("INVALID_CONFIRMATION", "The write confirmation was not found or has already been consumed.");
|
|
158
|
-
}
|
|
159
|
-
const pendingOperation = parsePendingOperation(fs.readFileSync(pendingPath, "utf-8"));
|
|
160
|
-
const publicPendingOperation = toPublicPendingOperation(pendingOperation);
|
|
161
|
-
if (new Date(pendingOperation.expiresAt).getTime() <= now().getTime()) {
|
|
162
|
-
fs.rmSync(pendingPath, { force: true });
|
|
163
|
-
appendLog(buildLogEntry(publicPendingOperation, "expired"));
|
|
164
|
-
throw createError("CONFIRMATION_EXPIRED", "The write confirmation expired. Run dryRun again.");
|
|
165
|
-
}
|
|
166
|
-
if (pendingOperation.toolName !== expected.toolName) {
|
|
167
|
-
appendLog(buildLogEntry(publicPendingOperation, "rejected", "tool_mismatch"));
|
|
168
|
-
throw createError("INVALID_CONFIRMATION", "The write confirmation does not match this MCP tool.");
|
|
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
|
-
}
|
|
174
|
-
if (pendingOperation.confirmTokenHash !== hashToken(request.confirmToken)) {
|
|
175
|
-
appendLog(buildLogEntry(publicPendingOperation, "rejected", "token_mismatch"));
|
|
176
|
-
throw createError("INVALID_CONFIRMATION", "The write confirmation token is invalid.");
|
|
177
|
-
}
|
|
178
|
-
fs.rmSync(pendingPath, { force: true });
|
|
179
|
-
appendLog(buildLogEntry(publicPendingOperation, "confirmed"));
|
|
180
|
-
return publicPendingOperation;
|
|
181
|
-
};
|
|
182
|
-
const ensureDestructiveWriteRequest = (request, input) => {
|
|
183
|
-
if (request.dryRun ?? true) {
|
|
184
|
-
return {
|
|
185
|
-
kind: "dry-run",
|
|
186
|
-
operation: prepareOperation({
|
|
187
|
-
...input,
|
|
188
|
-
force: request.force ?? input.force
|
|
189
|
-
})
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
kind: "confirmed",
|
|
194
|
-
operation: requireConfirmedOperation(request, {
|
|
195
|
-
toolName: input.toolName,
|
|
196
|
-
operationFingerprint: input.operationFingerprint
|
|
197
|
-
})
|
|
198
|
-
};
|
|
199
|
-
};
|
|
200
|
-
const recordWriteResult = (operation, success, detail) => {
|
|
201
|
-
appendLog(buildLogEntry(operation, success ? "executed" : "failed", detail));
|
|
202
|
-
};
|
|
203
|
-
const getStatus = () => {
|
|
204
|
-
const pendingOperations = listPendingOperations();
|
|
205
|
-
return {
|
|
206
|
-
confirmationTtlMs: ttlMs,
|
|
207
|
-
logPath,
|
|
208
|
-
pendingDir,
|
|
209
|
-
pendingOperationCount: pendingOperations.length,
|
|
210
|
-
pendingOperations,
|
|
211
|
-
rootDir,
|
|
212
|
-
writeToolsEnabled: false
|
|
213
|
-
};
|
|
214
|
-
};
|
|
215
|
-
return {
|
|
216
|
-
ensureDestructiveWriteRequest,
|
|
217
|
-
getStatus,
|
|
218
|
-
listPendingOperations,
|
|
219
|
-
prepareOperation,
|
|
220
|
-
recordWriteResult,
|
|
221
|
-
requireConfirmedOperation
|
|
222
|
-
};
|
|
223
|
-
};
|
|
9
|
+
import { z as z2 } from "zod";
|
|
224
10
|
|
|
225
11
|
// src/mcp-note-output.ts
|
|
226
12
|
var formatBackReferenceLink = (backReference) => {
|
|
@@ -272,69 +58,65 @@ var formatMcpReadNoteOutput = ({
|
|
|
272
58
|
};
|
|
273
59
|
|
|
274
60
|
// src/mcp-intent-write-tools.ts
|
|
275
|
-
import
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
after: z2.string().optional()
|
|
61
|
+
import { z } from "zod";
|
|
62
|
+
var markdownPatchSelectorSchema = z.discriminatedUnion("type", [
|
|
63
|
+
z.object({
|
|
64
|
+
type: z.literal("exact_text"),
|
|
65
|
+
text: z.string(),
|
|
66
|
+
before: z.string().optional(),
|
|
67
|
+
after: z.string().optional()
|
|
283
68
|
}),
|
|
284
|
-
|
|
285
|
-
type:
|
|
286
|
-
text:
|
|
287
|
-
matchIndex:
|
|
288
|
-
lineStart:
|
|
289
|
-
matchSha256:
|
|
290
|
-
surroundingHash:
|
|
291
|
-
positionHint:
|
|
69
|
+
z.object({
|
|
70
|
+
type: z.literal("match_candidate"),
|
|
71
|
+
text: z.string(),
|
|
72
|
+
matchIndex: z.number().int().nonnegative(),
|
|
73
|
+
lineStart: z.number().int().positive(),
|
|
74
|
+
matchSha256: z.string(),
|
|
75
|
+
surroundingHash: z.string(),
|
|
76
|
+
positionHint: z.enum(["first", "last"]).optional()
|
|
292
77
|
})
|
|
293
78
|
]);
|
|
294
|
-
var markdownPatchOperationSchema =
|
|
295
|
-
|
|
296
|
-
type:
|
|
297
|
-
replacement:
|
|
79
|
+
var markdownPatchOperationSchema = z.discriminatedUnion("type", [
|
|
80
|
+
z.object({
|
|
81
|
+
type: z.literal("replace"),
|
|
82
|
+
replacement: z.string()
|
|
298
83
|
}),
|
|
299
|
-
|
|
300
|
-
type:
|
|
301
|
-
insertion:
|
|
84
|
+
z.object({
|
|
85
|
+
type: z.literal("insert_before"),
|
|
86
|
+
insertion: z.string()
|
|
302
87
|
}),
|
|
303
|
-
|
|
304
|
-
type:
|
|
305
|
-
insertion:
|
|
88
|
+
z.object({
|
|
89
|
+
type: z.literal("insert_after"),
|
|
90
|
+
insertion: z.string()
|
|
306
91
|
})
|
|
307
92
|
]);
|
|
308
|
-
var markdownAppendPlacementSchema =
|
|
309
|
-
|
|
310
|
-
type:
|
|
93
|
+
var markdownAppendPlacementSchema = z.discriminatedUnion("type", [
|
|
94
|
+
z.object({
|
|
95
|
+
type: z.literal("end")
|
|
311
96
|
}),
|
|
312
|
-
|
|
313
|
-
type:
|
|
314
|
-
heading:
|
|
315
|
-
level:
|
|
97
|
+
z.object({
|
|
98
|
+
type: z.literal("after_heading"),
|
|
99
|
+
heading: z.string(),
|
|
100
|
+
level: z.number().int().min(1).max(6).optional()
|
|
316
101
|
})
|
|
317
102
|
]);
|
|
318
|
-
var markdownWritePolicySchema =
|
|
319
|
-
allowNoop:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
diffPreviewMaxChars: z2.number().int().nonnegative().optional(),
|
|
323
|
-
preserveTags: z2.union([z2.boolean(), z2.literal("warn")]).optional(),
|
|
324
|
-
preserveReferences: z2.union([z2.boolean(), z2.literal("warn")]).optional()
|
|
103
|
+
var markdownWritePolicySchema = z.object({
|
|
104
|
+
allowNoop: z.boolean().optional(),
|
|
105
|
+
preserveTags: z.union([z.boolean(), z.literal("warn")]).optional(),
|
|
106
|
+
preserveReferences: z.union([z.boolean(), z.literal("warn")]).optional()
|
|
325
107
|
}).optional();
|
|
326
108
|
var MCP_METADATA_PROPERTY_PATCH_LIMIT = 50;
|
|
327
109
|
var normalizeMetadataPropertyKey = (key) => key.trim().toLowerCase().replace(/\s+/g, "-");
|
|
328
|
-
var metadataPropertyPatchSchema =
|
|
329
|
-
set:
|
|
330
|
-
key:
|
|
331
|
-
value:
|
|
110
|
+
var metadataPropertyPatchSchema = z.object({
|
|
111
|
+
set: z.array(z.object({
|
|
112
|
+
key: z.string().min(1).describe("Property key from ocean_brain_list_properties."),
|
|
113
|
+
value: z.string().describe("String value: select option.value, date YYYY-MM-DD, boolean true/false, number finite string, url http(s).")
|
|
332
114
|
})).max(MCP_METADATA_PROPERTY_PATCH_LIMIT).optional().describe("Set up to 50 property values. Keys must be unique."),
|
|
333
|
-
deleteKeys:
|
|
115
|
+
deleteKeys: z.array(z.string().min(1)).max(MCP_METADATA_PROPERTY_PATCH_LIMIT).optional().describe("Remove up to 50 property values from this note. Keys must be unique and not also in set.")
|
|
334
116
|
}).superRefine((patch, context) => {
|
|
335
117
|
if ((patch.set?.length ?? 0) === 0 && (patch.deleteKeys?.length ?? 0) === 0) {
|
|
336
118
|
context.addIssue({
|
|
337
|
-
code:
|
|
119
|
+
code: z.ZodIssueCode.custom,
|
|
338
120
|
message: "properties must include set or deleteKeys."
|
|
339
121
|
});
|
|
340
122
|
return;
|
|
@@ -344,7 +126,7 @@ var metadataPropertyPatchSchema = z2.object({
|
|
|
344
126
|
const key = normalizeMetadataPropertyKey(item.key);
|
|
345
127
|
if (setKeys.has(key)) {
|
|
346
128
|
context.addIssue({
|
|
347
|
-
code:
|
|
129
|
+
code: z.ZodIssueCode.custom,
|
|
348
130
|
message: `properties.set contains duplicate key ${key}.`
|
|
349
131
|
});
|
|
350
132
|
}
|
|
@@ -355,154 +137,41 @@ var metadataPropertyPatchSchema = z2.object({
|
|
|
355
137
|
const key = normalizeMetadataPropertyKey(rawKey);
|
|
356
138
|
if (deleteKeys.has(key)) {
|
|
357
139
|
context.addIssue({
|
|
358
|
-
code:
|
|
140
|
+
code: z.ZodIssueCode.custom,
|
|
359
141
|
message: `properties.deleteKeys contains duplicate key ${key}.`
|
|
360
142
|
});
|
|
361
143
|
}
|
|
362
144
|
if (setKeys.has(key)) {
|
|
363
145
|
context.addIssue({
|
|
364
|
-
code:
|
|
146
|
+
code: z.ZodIssueCode.custom,
|
|
365
147
|
message: `properties cannot set and delete ${key} in one request.`
|
|
366
148
|
});
|
|
367
149
|
}
|
|
368
150
|
deleteKeys.add(key);
|
|
369
151
|
}
|
|
370
152
|
}).optional();
|
|
371
|
-
var sha256 = (value) => crypto2.createHash("sha256").update(value).digest("hex");
|
|
372
|
-
var isRecord = (value) => {
|
|
373
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
374
|
-
};
|
|
375
|
-
var stableJsonStringify = (value) => {
|
|
376
|
-
if (Array.isArray(value)) {
|
|
377
|
-
return `[${value.map(stableJsonStringify).join(",")}]`;
|
|
378
|
-
}
|
|
379
|
-
if (isRecord(value)) {
|
|
380
|
-
const properties = Object.keys(value).filter((key) => value[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableJsonStringify(value[key])}`);
|
|
381
|
-
return `{${properties.join(",")}}`;
|
|
382
|
-
}
|
|
383
|
-
return JSON.stringify(value) ?? "null";
|
|
384
|
-
};
|
|
385
|
-
var normalizeServerUrl = (serverUrl) => {
|
|
386
|
-
try {
|
|
387
|
-
const url = new URL(serverUrl);
|
|
388
|
-
url.hash = "";
|
|
389
|
-
return url.toString().replace(/\/$/, "");
|
|
390
|
-
} catch {
|
|
391
|
-
return serverUrl.replace(/\/$/, "");
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
var normalizeIntentWritePayloadForFingerprint = (toolName, payload) => {
|
|
395
|
-
const normalizedPayload = { ...payload };
|
|
396
|
-
if (toolName === "ocean_brain_append_note_markdown") {
|
|
397
|
-
normalizedPayload.placement ??= { type: "end" };
|
|
398
|
-
normalizedPayload.separator ??= "\n\n";
|
|
399
|
-
}
|
|
400
|
-
return normalizedPayload;
|
|
401
|
-
};
|
|
402
|
-
var createIntentWriteOperationFingerprint = (serverUrl, token, toolName, payload) => {
|
|
403
|
-
return sha256(stableJsonStringify({
|
|
404
|
-
payload: normalizeIntentWritePayloadForFingerprint(toolName, payload),
|
|
405
|
-
serverUrl: normalizeServerUrl(serverUrl),
|
|
406
|
-
tokenDigest: token ? sha256(token) : null,
|
|
407
|
-
toolName
|
|
408
|
-
}));
|
|
409
|
-
};
|
|
410
|
-
var getErrorDetail = (error) => {
|
|
411
|
-
return error instanceof Error ? error.message : "Unknown MCP write error";
|
|
412
|
-
};
|
|
413
|
-
var requireCommitConfirmationFields = (request) => {
|
|
414
|
-
if (request.dryRun !== false) {
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (!request.operationId || !request.confirmToken) {
|
|
418
|
-
throw new Error("Commit mode requires both operationId and confirmToken from the dry-run response.");
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
var resolvePayloadWithBaseline = async (fetchNoteWriteBaseline, writeToken, id, payload) => {
|
|
422
|
-
if (typeof payload.expectedUpdatedAt === "string" || typeof payload.baseMarkdownSha256 === "string") {
|
|
423
|
-
return payload;
|
|
424
|
-
}
|
|
425
|
-
const baseline = await fetchNoteWriteBaseline(id, writeToken);
|
|
426
|
-
return {
|
|
427
|
-
...payload,
|
|
428
|
-
expectedUpdatedAt: baseline.updatedAt
|
|
429
|
-
};
|
|
430
|
-
};
|
|
431
|
-
var prepareConfirmedWriteOperation = (writeSafety, result, input) => {
|
|
432
|
-
if (result.status !== "dry_run") {
|
|
433
|
-
return result;
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
...result,
|
|
437
|
-
operation: writeSafety.prepareOperation({
|
|
438
|
-
actor: "mcp-bearer",
|
|
439
|
-
affectedIds: input.affectedIds,
|
|
440
|
-
estimatedChangeCount: result.proposed?.changedLineCount ?? 1,
|
|
441
|
-
operationFingerprint: input.operationFingerprint,
|
|
442
|
-
risk: "high-impact",
|
|
443
|
-
summary: input.summary,
|
|
444
|
-
toolName: input.toolName
|
|
445
|
-
})
|
|
446
|
-
};
|
|
447
|
-
};
|
|
448
|
-
var requireConfirmedWriteOperation = (writeSafety, request, input) => {
|
|
449
|
-
const intent = writeSafety.ensureDestructiveWriteRequest(
|
|
450
|
-
request,
|
|
451
|
-
{
|
|
452
|
-
actor: "mcp-bearer",
|
|
453
|
-
affectedIds: input.affectedIds,
|
|
454
|
-
estimatedChangeCount: 1,
|
|
455
|
-
operationFingerprint: input.operationFingerprint,
|
|
456
|
-
risk: "high-impact",
|
|
457
|
-
summary: input.summary,
|
|
458
|
-
toolName: input.toolName
|
|
459
|
-
}
|
|
460
|
-
);
|
|
461
|
-
if (intent.kind !== "confirmed") {
|
|
462
|
-
throw new Error("Expected a confirmed MCP write operation.");
|
|
463
|
-
}
|
|
464
|
-
return intent.operation;
|
|
465
|
-
};
|
|
466
|
-
var executeConfirmedWrite = async (writeSafety, operation, request) => {
|
|
467
|
-
try {
|
|
468
|
-
const result = await request();
|
|
469
|
-
writeSafety.recordWriteResult(
|
|
470
|
-
operation,
|
|
471
|
-
result.status === "applied",
|
|
472
|
-
result.status === "applied" ? void 0 : result.reason
|
|
473
|
-
);
|
|
474
|
-
return result;
|
|
475
|
-
} catch (error) {
|
|
476
|
-
writeSafety.recordWriteResult(operation, false, getErrorDetail(error));
|
|
477
|
-
throw error;
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
153
|
var registerIntentWriteTools = (server, {
|
|
481
|
-
fetchNoteWriteBaseline,
|
|
482
154
|
jsonRequest: jsonRequest2,
|
|
483
155
|
requireWriteToken: requireWriteToken2,
|
|
484
156
|
serverUrl,
|
|
485
157
|
token,
|
|
486
|
-
tools
|
|
487
|
-
writeSafety
|
|
158
|
+
tools
|
|
488
159
|
}) => {
|
|
489
160
|
server.tool(
|
|
490
161
|
tools.patchNoteMarkdown,
|
|
491
|
-
"Patch a
|
|
162
|
+
"Patch a specific part of a note, like finding text with `rg` and editing only that match. Use for localized edits: fix one sentence, replace one paragraph, insert before/after exact text, or edit one section. Do not use for whole-note rewrites; use ocean_brain_replace_note_markdown instead.",
|
|
492
163
|
{
|
|
493
|
-
id:
|
|
494
|
-
expectedUpdatedAt:
|
|
495
|
-
baseMarkdownSha256:
|
|
496
|
-
intent:
|
|
164
|
+
id: z.string().describe("Note ID to patch"),
|
|
165
|
+
expectedUpdatedAt: z.string().optional().describe("Expected note updatedAt from the read you based this edit on. Required unless baseMarkdownSha256 is provided."),
|
|
166
|
+
baseMarkdownSha256: z.string().optional().describe("SHA-256 of the current markdown. Optional alternative to expectedUpdatedAt."),
|
|
167
|
+
intent: z.string().describe("Human-readable reason for the patch."),
|
|
497
168
|
selector: markdownPatchSelectorSchema.describe("Exact text or previously returned match candidate."),
|
|
498
169
|
operation: markdownPatchOperationSchema.describe("replace, insert_before, or insert_after."),
|
|
499
|
-
policy: markdownWritePolicySchema
|
|
500
|
-
...confirmedMcpWriteFields
|
|
170
|
+
policy: markdownWritePolicySchema
|
|
501
171
|
},
|
|
502
|
-
async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, selector, operation, policy
|
|
172
|
+
async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, selector, operation, policy }) => {
|
|
503
173
|
const writeToken = requireWriteToken2(token, tools.patchNoteMarkdown);
|
|
504
|
-
|
|
505
|
-
const payload = await resolvePayloadWithBaseline(fetchNoteWriteBaseline, writeToken, id, {
|
|
174
|
+
const payload = {
|
|
506
175
|
id,
|
|
507
176
|
...expectedUpdatedAt ? { expectedUpdatedAt } : {},
|
|
508
177
|
...baseMarkdownSha256 ? { baseMarkdownSha256 } : {},
|
|
@@ -510,43 +179,8 @@ var registerIntentWriteTools = (server, {
|
|
|
510
179
|
selector,
|
|
511
180
|
operation,
|
|
512
181
|
...policy ? { policy } : {}
|
|
513
|
-
}
|
|
514
|
-
const
|
|
515
|
-
if (dryRun ?? true) {
|
|
516
|
-
const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/patch-markdown", {
|
|
517
|
-
...payload,
|
|
518
|
-
dryRun: true
|
|
519
|
-
});
|
|
520
|
-
return {
|
|
521
|
-
content: [{
|
|
522
|
-
type: "text",
|
|
523
|
-
text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
|
|
524
|
-
affectedIds: [id],
|
|
525
|
-
operationFingerprint,
|
|
526
|
-
summary: intent,
|
|
527
|
-
toolName: tools.patchNoteMarkdown
|
|
528
|
-
}), null, 2)
|
|
529
|
-
}]
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
const confirmedOperation = requireConfirmedWriteOperation(
|
|
533
|
-
writeSafety,
|
|
534
|
-
{ dryRun, operationId, confirmToken },
|
|
535
|
-
{
|
|
536
|
-
affectedIds: [id],
|
|
537
|
-
operationFingerprint,
|
|
538
|
-
summary: intent,
|
|
539
|
-
toolName: tools.patchNoteMarkdown
|
|
540
|
-
}
|
|
541
|
-
);
|
|
542
|
-
const result = await executeConfirmedWrite(
|
|
543
|
-
writeSafety,
|
|
544
|
-
confirmedOperation,
|
|
545
|
-
() => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/patch-markdown", {
|
|
546
|
-
...payload,
|
|
547
|
-
dryRun: false
|
|
548
|
-
})
|
|
549
|
-
);
|
|
182
|
+
};
|
|
183
|
+
const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/patch-markdown", payload);
|
|
550
184
|
return {
|
|
551
185
|
content: [{
|
|
552
186
|
type: "text",
|
|
@@ -557,22 +191,20 @@ var registerIntentWriteTools = (server, {
|
|
|
557
191
|
);
|
|
558
192
|
server.tool(
|
|
559
193
|
tools.appendNoteMarkdown,
|
|
560
|
-
"Append markdown
|
|
194
|
+
"Append markdown without changing existing content, like `cat addition.md >> note.md`. Use for adding logs, status updates, meeting notes, or new sections at the end or after a heading. Do not use to modify or replace existing body content.",
|
|
561
195
|
{
|
|
562
|
-
id:
|
|
563
|
-
expectedUpdatedAt:
|
|
564
|
-
baseMarkdownSha256:
|
|
565
|
-
intent:
|
|
566
|
-
insertion:
|
|
196
|
+
id: z.string().describe("Note ID to append to"),
|
|
197
|
+
expectedUpdatedAt: z.string().optional().describe("Expected note updatedAt from the read you based this edit on. Required unless baseMarkdownSha256 is provided."),
|
|
198
|
+
baseMarkdownSha256: z.string().optional().describe("SHA-256 of the current markdown. Optional alternative to expectedUpdatedAt."),
|
|
199
|
+
intent: z.string().describe("Human-readable reason for the append."),
|
|
200
|
+
insertion: z.string().describe("Markdown to append. Tags are body tokens such as [@tag] or [#tag]."),
|
|
567
201
|
placement: markdownAppendPlacementSchema.optional().describe("Default is end. after_heading requires one unique matching heading."),
|
|
568
|
-
separator:
|
|
569
|
-
policy: markdownWritePolicySchema
|
|
570
|
-
...confirmedMcpWriteFields
|
|
202
|
+
separator: z.enum(["\n\n", "\n"]).optional().describe("Separator inserted around appended markdown. Defaults to a blank line."),
|
|
203
|
+
policy: markdownWritePolicySchema
|
|
571
204
|
},
|
|
572
|
-
async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, insertion, placement, separator, policy
|
|
205
|
+
async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, insertion, placement, separator, policy }) => {
|
|
573
206
|
const writeToken = requireWriteToken2(token, tools.appendNoteMarkdown);
|
|
574
|
-
|
|
575
|
-
const payload = await resolvePayloadWithBaseline(fetchNoteWriteBaseline, writeToken, id, {
|
|
207
|
+
const payload = {
|
|
576
208
|
id,
|
|
577
209
|
...expectedUpdatedAt ? { expectedUpdatedAt } : {},
|
|
578
210
|
...baseMarkdownSha256 ? { baseMarkdownSha256 } : {},
|
|
@@ -581,43 +213,8 @@ var registerIntentWriteTools = (server, {
|
|
|
581
213
|
...placement ? { placement } : {},
|
|
582
214
|
...separator ? { separator } : {},
|
|
583
215
|
...policy ? { policy } : {}
|
|
584
|
-
}
|
|
585
|
-
const
|
|
586
|
-
if (dryRun ?? true) {
|
|
587
|
-
const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/append-markdown", {
|
|
588
|
-
...payload,
|
|
589
|
-
dryRun: true
|
|
590
|
-
});
|
|
591
|
-
return {
|
|
592
|
-
content: [{
|
|
593
|
-
type: "text",
|
|
594
|
-
text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
|
|
595
|
-
affectedIds: [id],
|
|
596
|
-
operationFingerprint,
|
|
597
|
-
summary: intent,
|
|
598
|
-
toolName: tools.appendNoteMarkdown
|
|
599
|
-
}), null, 2)
|
|
600
|
-
}]
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
const confirmedOperation = requireConfirmedWriteOperation(
|
|
604
|
-
writeSafety,
|
|
605
|
-
{ dryRun, operationId, confirmToken },
|
|
606
|
-
{
|
|
607
|
-
affectedIds: [id],
|
|
608
|
-
operationFingerprint,
|
|
609
|
-
summary: intent,
|
|
610
|
-
toolName: tools.appendNoteMarkdown
|
|
611
|
-
}
|
|
612
|
-
);
|
|
613
|
-
const result = await executeConfirmedWrite(
|
|
614
|
-
writeSafety,
|
|
615
|
-
confirmedOperation,
|
|
616
|
-
() => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/append-markdown", {
|
|
617
|
-
...payload,
|
|
618
|
-
dryRun: false
|
|
619
|
-
})
|
|
620
|
-
);
|
|
216
|
+
};
|
|
217
|
+
const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/append-markdown", payload);
|
|
621
218
|
return {
|
|
622
219
|
content: [{
|
|
623
220
|
type: "text",
|
|
@@ -628,62 +225,24 @@ var registerIntentWriteTools = (server, {
|
|
|
628
225
|
);
|
|
629
226
|
server.tool(
|
|
630
227
|
tools.updateNoteMetadata,
|
|
631
|
-
"Update note title
|
|
228
|
+
"Update note metadata only: title, layout, or properties. This does not edit the note body. For properties, call ocean_brain_list_properties first. Use existing property key and string value only; do not send valueType. select=option.value, date=YYYY-MM-DD, boolean=true/false, number=finite string, url=http(s). Use deleteKeys to remove values. Definitions are not created.",
|
|
632
229
|
{
|
|
633
|
-
id:
|
|
634
|
-
expectedUpdatedAt:
|
|
635
|
-
title:
|
|
636
|
-
layout:
|
|
637
|
-
properties: metadataPropertyPatchSchema.describe("Patch existing shared property values. Include set and/or deleteKeys; empty patches are rejected.")
|
|
638
|
-
...confirmedMcpWriteFields
|
|
230
|
+
id: z.string().describe("Note ID to update"),
|
|
231
|
+
expectedUpdatedAt: z.string().describe("Expected note updatedAt from the read you based this metadata edit on. Required to prevent stale writes."),
|
|
232
|
+
title: z.string().optional().describe("New note title"),
|
|
233
|
+
layout: z.enum(["narrow", "wide", "full"]).optional().describe("New note layout"),
|
|
234
|
+
properties: metadataPropertyPatchSchema.describe("Patch existing shared property values. Include set and/or deleteKeys; empty patches are rejected.")
|
|
639
235
|
},
|
|
640
|
-
async ({ id, expectedUpdatedAt, title, layout, properties
|
|
236
|
+
async ({ id, expectedUpdatedAt, title, layout, properties }) => {
|
|
641
237
|
const writeToken = requireWriteToken2(token, tools.updateNoteMetadata);
|
|
642
|
-
|
|
643
|
-
const payload = await resolvePayloadWithBaseline(fetchNoteWriteBaseline, writeToken, id, {
|
|
238
|
+
const payload = {
|
|
644
239
|
id,
|
|
645
240
|
...expectedUpdatedAt ? { expectedUpdatedAt } : {},
|
|
646
241
|
...title !== void 0 ? { title } : {},
|
|
647
242
|
...layout ? { layout } : {},
|
|
648
243
|
...properties ? { properties } : {}
|
|
649
|
-
}
|
|
650
|
-
const
|
|
651
|
-
const operationFingerprint = createIntentWriteOperationFingerprint(serverUrl, writeToken, tools.updateNoteMetadata, payload);
|
|
652
|
-
if (dryRun ?? true) {
|
|
653
|
-
const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/metadata", {
|
|
654
|
-
...payload,
|
|
655
|
-
dryRun: true
|
|
656
|
-
});
|
|
657
|
-
return {
|
|
658
|
-
content: [{
|
|
659
|
-
type: "text",
|
|
660
|
-
text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
|
|
661
|
-
affectedIds: [id],
|
|
662
|
-
operationFingerprint,
|
|
663
|
-
summary,
|
|
664
|
-
toolName: tools.updateNoteMetadata
|
|
665
|
-
}), null, 2)
|
|
666
|
-
}]
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
const confirmedOperation = requireConfirmedWriteOperation(
|
|
670
|
-
writeSafety,
|
|
671
|
-
{ dryRun, operationId, confirmToken },
|
|
672
|
-
{
|
|
673
|
-
affectedIds: [id],
|
|
674
|
-
operationFingerprint,
|
|
675
|
-
summary,
|
|
676
|
-
toolName: tools.updateNoteMetadata
|
|
677
|
-
}
|
|
678
|
-
);
|
|
679
|
-
const result = await executeConfirmedWrite(
|
|
680
|
-
writeSafety,
|
|
681
|
-
confirmedOperation,
|
|
682
|
-
() => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/metadata", {
|
|
683
|
-
...payload,
|
|
684
|
-
dryRun: false
|
|
685
|
-
})
|
|
686
|
-
);
|
|
244
|
+
};
|
|
245
|
+
const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/metadata", payload);
|
|
687
246
|
return {
|
|
688
247
|
content: [{
|
|
689
248
|
type: "text",
|
|
@@ -694,63 +253,26 @@ var registerIntentWriteTools = (server, {
|
|
|
694
253
|
);
|
|
695
254
|
server.tool(
|
|
696
255
|
tools.replaceNoteMarkdown,
|
|
697
|
-
"Replace
|
|
256
|
+
"Replace the entire note body, like overwriting a markdown file with `cat new.md > note.md`. Use when the user explicitly asks to rewrite, replace, regenerate, restructure, or overwrite the whole note. Do not use for localized edits such as fixing one sentence, changing one paragraph, or inserting under a heading; use ocean_brain_patch_note_markdown instead.",
|
|
698
257
|
{
|
|
699
|
-
id:
|
|
700
|
-
expectedUpdatedAt:
|
|
701
|
-
baseMarkdownSha256:
|
|
702
|
-
intent:
|
|
703
|
-
replacement:
|
|
704
|
-
policy: markdownWritePolicySchema
|
|
705
|
-
...confirmedMcpWriteFields
|
|
258
|
+
id: z.string().describe("Note ID to replace"),
|
|
259
|
+
expectedUpdatedAt: z.string().optional().describe("Expected note updatedAt from the read you based this edit on. Required unless baseMarkdownSha256 is provided."),
|
|
260
|
+
baseMarkdownSha256: z.string().optional().describe("SHA-256 of the current markdown. Optional alternative to expectedUpdatedAt."),
|
|
261
|
+
intent: z.string().describe("Human-readable reason for the full replace."),
|
|
262
|
+
replacement: z.string().describe("Complete replacement markdown body. Tags are body tokens such as [@tag] or [#tag]."),
|
|
263
|
+
policy: markdownWritePolicySchema
|
|
706
264
|
},
|
|
707
|
-
async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, replacement, policy
|
|
265
|
+
async ({ id, expectedUpdatedAt, baseMarkdownSha256, intent, replacement, policy }) => {
|
|
708
266
|
const writeToken = requireWriteToken2(token, tools.replaceNoteMarkdown);
|
|
709
|
-
|
|
710
|
-
const payload = await resolvePayloadWithBaseline(fetchNoteWriteBaseline, writeToken, id, {
|
|
267
|
+
const payload = {
|
|
711
268
|
id,
|
|
712
269
|
...expectedUpdatedAt ? { expectedUpdatedAt } : {},
|
|
713
270
|
...baseMarkdownSha256 ? { baseMarkdownSha256 } : {},
|
|
714
271
|
intent,
|
|
715
272
|
replacement,
|
|
716
273
|
...policy ? { policy } : {}
|
|
717
|
-
}
|
|
718
|
-
const
|
|
719
|
-
if (dryRun ?? true) {
|
|
720
|
-
const result2 = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/replace-markdown", {
|
|
721
|
-
...payload,
|
|
722
|
-
dryRun: true
|
|
723
|
-
});
|
|
724
|
-
return {
|
|
725
|
-
content: [{
|
|
726
|
-
type: "text",
|
|
727
|
-
text: JSON.stringify(prepareConfirmedWriteOperation(writeSafety, result2, {
|
|
728
|
-
affectedIds: [id],
|
|
729
|
-
operationFingerprint,
|
|
730
|
-
summary: intent,
|
|
731
|
-
toolName: tools.replaceNoteMarkdown
|
|
732
|
-
}), null, 2)
|
|
733
|
-
}]
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
const confirmedOperation = requireConfirmedWriteOperation(
|
|
737
|
-
writeSafety,
|
|
738
|
-
{ dryRun, operationId, confirmToken },
|
|
739
|
-
{
|
|
740
|
-
affectedIds: [id],
|
|
741
|
-
operationFingerprint,
|
|
742
|
-
summary: intent,
|
|
743
|
-
toolName: tools.replaceNoteMarkdown
|
|
744
|
-
}
|
|
745
|
-
);
|
|
746
|
-
const result = await executeConfirmedWrite(
|
|
747
|
-
writeSafety,
|
|
748
|
-
confirmedOperation,
|
|
749
|
-
() => jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/replace-markdown", {
|
|
750
|
-
...payload,
|
|
751
|
-
dryRun: false
|
|
752
|
-
})
|
|
753
|
-
);
|
|
274
|
+
};
|
|
275
|
+
const result = await jsonRequest2(serverUrl, writeToken, "/api/mcp/notes/replace-markdown", payload);
|
|
754
276
|
return {
|
|
755
277
|
content: [{
|
|
756
278
|
type: "text",
|
|
@@ -787,10 +309,24 @@ var formatPropertyQueryResponse = ({
|
|
|
787
309
|
});
|
|
788
310
|
|
|
789
311
|
// src/mcp.ts
|
|
790
|
-
var __dirname =
|
|
312
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
791
313
|
var pkg = JSON.parse(
|
|
792
|
-
|
|
314
|
+
fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8")
|
|
793
315
|
);
|
|
316
|
+
if (!pkg.oceanBrain?.mcpCompatibilityVersion) {
|
|
317
|
+
throw new Error("Ocean Brain MCP compatibility version is required in package metadata.");
|
|
318
|
+
}
|
|
319
|
+
var OCEAN_BRAIN_MCP_VERSION_HEADER = "X-Ocean-Brain-MCP-Version";
|
|
320
|
+
var OCEAN_BRAIN_MCP_COMPATIBILITY_VERSION_HEADER = "X-Ocean-Brain-MCP-Compatibility-Version";
|
|
321
|
+
var OCEAN_BRAIN_MCP_CLIENT_VERSION_HEADER = "X-Ocean-Brain-MCP-Client-Version";
|
|
322
|
+
var OCEAN_BRAIN_MCP_COMPATIBILITY_VERSION = pkg.oceanBrain.mcpCompatibilityVersion;
|
|
323
|
+
var createMcpRequestHeaders = (token) => ({
|
|
324
|
+
"Content-Type": "application/json",
|
|
325
|
+
[OCEAN_BRAIN_MCP_VERSION_HEADER]: OCEAN_BRAIN_MCP_COMPATIBILITY_VERSION,
|
|
326
|
+
[OCEAN_BRAIN_MCP_COMPATIBILITY_VERSION_HEADER]: OCEAN_BRAIN_MCP_COMPATIBILITY_VERSION,
|
|
327
|
+
[OCEAN_BRAIN_MCP_CLIENT_VERSION_HEADER]: pkg.version,
|
|
328
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
329
|
+
});
|
|
794
330
|
var OCEAN_BRAIN_MCP_TOOLS = {
|
|
795
331
|
searchNotes: "ocean_brain_search_notes",
|
|
796
332
|
readNote: "ocean_brain_read_note",
|
|
@@ -806,22 +342,34 @@ var OCEAN_BRAIN_MCP_TOOLS = {
|
|
|
806
342
|
listNotesByTag: "ocean_brain_list_notes_by_tag",
|
|
807
343
|
listNotesByTags: "ocean_brain_list_notes_by_tags",
|
|
808
344
|
listRecentNotes: "ocean_brain_list_recent_notes",
|
|
809
|
-
writeSafetyStatus: "ocean_brain_write_safety_status",
|
|
810
345
|
findNoteCleanupCandidates: "ocean_brain_find_note_cleanup_candidates",
|
|
811
346
|
createTag: "ocean_brain_create_tag",
|
|
812
347
|
deleteNote: "ocean_brain_delete_note"
|
|
813
348
|
};
|
|
814
|
-
var noteLayoutSchema =
|
|
815
|
-
var tagMatchModeSchema =
|
|
816
|
-
var propertyValueTypeSchema =
|
|
817
|
-
var propertyFilterOperatorSchema =
|
|
818
|
-
var viewSortBySchema =
|
|
819
|
-
var viewSortOrderSchema =
|
|
820
|
-
var
|
|
821
|
-
|
|
349
|
+
var noteLayoutSchema = z2.enum(["narrow", "wide", "full"]);
|
|
350
|
+
var tagMatchModeSchema = z2.enum(["and", "or"]);
|
|
351
|
+
var propertyValueTypeSchema = z2.enum(["text", "url", "number", "date", "boolean", "select"]);
|
|
352
|
+
var propertyFilterOperatorSchema = z2.enum(["equals", "before", "after", "exists", "notExists"]);
|
|
353
|
+
var viewSortBySchema = z2.enum(["updatedAt", "createdAt", "title"]);
|
|
354
|
+
var viewSortOrderSchema = z2.enum(["asc", "desc"]);
|
|
355
|
+
var formatMcpGraphqlError = (error) => {
|
|
356
|
+
const suffix = [];
|
|
357
|
+
if (error.extensions?.code) {
|
|
358
|
+
suffix.push(`code=${error.extensions.code}`);
|
|
359
|
+
}
|
|
360
|
+
if (error.extensions?.operationId) {
|
|
361
|
+
suffix.push(`operationId=${error.extensions.operationId}`);
|
|
362
|
+
}
|
|
363
|
+
if (suffix.length === 0) {
|
|
364
|
+
return `GraphQL error: ${error.message}`;
|
|
365
|
+
}
|
|
366
|
+
return `GraphQL error: ${error.message} (${suffix.join(", ")})`;
|
|
367
|
+
};
|
|
368
|
+
var propertyFilterSchema = z2.object({
|
|
369
|
+
key: z2.string().describe("Property key from ocean_brain_list_properties, e.g. state"),
|
|
822
370
|
valueType: propertyValueTypeSchema.describe("Property value type from the property definition"),
|
|
823
371
|
operator: propertyFilterOperatorSchema.describe("Filter operator. before/after are only valid for date or number properties."),
|
|
824
|
-
value:
|
|
372
|
+
value: z2.string().nullable().optional().describe("Filter value. Required unless operator is exists or notExists. select=option.value, date=YYYY-MM-DD, boolean=true/false, number=finite, url=http(s).")
|
|
825
373
|
});
|
|
826
374
|
var normalizeOceanBrainTagName = (name) => {
|
|
827
375
|
const trimmedName = name.trim();
|
|
@@ -860,14 +408,14 @@ var fetchOceanBrainTags = async (serverUrl, token, query) => {
|
|
|
860
408
|
async function graphql(serverUrl, token, query, variables) {
|
|
861
409
|
const response = await fetch(`${serverUrl}/graphql/mcp`, {
|
|
862
410
|
method: "POST",
|
|
863
|
-
headers:
|
|
864
|
-
"Content-Type": "application/json",
|
|
865
|
-
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
866
|
-
},
|
|
411
|
+
headers: createMcpRequestHeaders(token),
|
|
867
412
|
body: JSON.stringify({ query, variables })
|
|
868
413
|
});
|
|
869
414
|
if (!response.ok) {
|
|
870
|
-
|
|
415
|
+
const errorBody = await response.json().catch(() => void 0);
|
|
416
|
+
throw new Error(
|
|
417
|
+
errorBody?.message ? `GraphQL request failed: ${errorBody.code || response.status} ${errorBody.message}` : `GraphQL request failed: ${response.status} ${response.statusText}`
|
|
418
|
+
);
|
|
871
419
|
}
|
|
872
420
|
const result = await response.json();
|
|
873
421
|
if (result.errors?.length) {
|
|
@@ -878,10 +426,7 @@ async function graphql(serverUrl, token, query, variables) {
|
|
|
878
426
|
async function jsonRequest(serverUrl, token, pathName, body) {
|
|
879
427
|
const response = await fetch(`${serverUrl}${pathName}`, {
|
|
880
428
|
method: "POST",
|
|
881
|
-
headers:
|
|
882
|
-
"Content-Type": "application/json",
|
|
883
|
-
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
884
|
-
},
|
|
429
|
+
headers: createMcpRequestHeaders(token),
|
|
885
430
|
body: JSON.stringify(body)
|
|
886
431
|
});
|
|
887
432
|
const result = await response.json();
|
|
@@ -890,30 +435,23 @@ async function jsonRequest(serverUrl, token, pathName, body) {
|
|
|
890
435
|
}
|
|
891
436
|
return result;
|
|
892
437
|
}
|
|
893
|
-
var fetchMcpNoteWriteBaseline = async (serverUrl, token, id, request = jsonRequest) => {
|
|
894
|
-
const result = await request(serverUrl, token, "/api/mcp/notes/baseline", { id });
|
|
895
|
-
return result.note;
|
|
896
|
-
};
|
|
897
438
|
var requireWriteToken = (token, toolName) => {
|
|
898
439
|
if (token) {
|
|
899
440
|
return token;
|
|
900
441
|
}
|
|
901
442
|
throw new Error(`${toolName} requires an MCP bearer token. Set --token or --token-file.`);
|
|
902
443
|
};
|
|
903
|
-
async function startMcpServer(serverUrl, token
|
|
444
|
+
async function startMcpServer(serverUrl, token) {
|
|
904
445
|
const server = new McpServer({
|
|
905
446
|
name: "ocean-brain",
|
|
906
447
|
version: pkg.version
|
|
907
448
|
});
|
|
908
|
-
const writeSafety = createMcpWriteSafetyCoordinator({
|
|
909
|
-
rootDir: options.writeSafetyDir ?? defaultMcpWriteSafetyDir()
|
|
910
|
-
});
|
|
911
449
|
server.tool(
|
|
912
450
|
OCEAN_BRAIN_MCP_TOOLS.searchNotes,
|
|
913
451
|
"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.",
|
|
914
452
|
{
|
|
915
|
-
query:
|
|
916
|
-
limit:
|
|
453
|
+
query: z2.string().describe("Search keyword"),
|
|
454
|
+
limit: z2.number().optional().default(10).describe("Max results (default: 10)")
|
|
917
455
|
},
|
|
918
456
|
async ({ query, limit }) => {
|
|
919
457
|
const data = await graphql(serverUrl, token, `
|
|
@@ -953,8 +491,8 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
953
491
|
OCEAN_BRAIN_MCP_TOOLS.readNote,
|
|
954
492
|
"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.",
|
|
955
493
|
{
|
|
956
|
-
id:
|
|
957
|
-
maxLength:
|
|
494
|
+
id: z2.string().describe("Note ID"),
|
|
495
|
+
maxLength: z2.number().optional().default(1e3).describe("Max content length in characters. 0 for full content. (default: 1000)")
|
|
958
496
|
},
|
|
959
497
|
async ({ id, maxLength }) => {
|
|
960
498
|
const data = await graphql(serverUrl, token, `
|
|
@@ -993,8 +531,8 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
993
531
|
OCEAN_BRAIN_MCP_TOOLS.createNote,
|
|
994
532
|
"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.",
|
|
995
533
|
{
|
|
996
|
-
title:
|
|
997
|
-
markdown:
|
|
534
|
+
title: z2.string().describe("Note title"),
|
|
535
|
+
markdown: z2.string().optional().default("").describe("Markdown body for the new note. In MCP markdown, tags must use [@tag] or [#tag]. Defaults to an empty note body."),
|
|
998
536
|
layout: noteLayoutSchema.optional().describe("Optional note layout: narrow, wide, or full. Prefer wide for most notes unless the user explicitly wants narrow or full.")
|
|
999
537
|
},
|
|
1000
538
|
async ({ title, markdown, layout }) => {
|
|
@@ -1014,11 +552,11 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1014
552
|
);
|
|
1015
553
|
server.tool(
|
|
1016
554
|
OCEAN_BRAIN_MCP_TOOLS.updateNote,
|
|
1017
|
-
"Legacy full-field note update
|
|
555
|
+
"Legacy full-field note update kept for backward compatibility. Prefer ocean_brain_patch_note_markdown for localized body edits, ocean_brain_append_note_markdown for additions, ocean_brain_replace_note_markdown for whole-body rewrites, and ocean_brain_update_note_metadata for title/layout/properties. Use this only when a legacy client needs combined field updates.",
|
|
1018
556
|
{
|
|
1019
|
-
id:
|
|
1020
|
-
title:
|
|
1021
|
-
markdown:
|
|
557
|
+
id: z2.string().describe("Note ID to update"),
|
|
558
|
+
title: z2.string().optional().describe("New note title"),
|
|
559
|
+
markdown: z2.string().optional().describe("Replacement markdown body. In MCP markdown, tags must use [@tag] or [#tag]."),
|
|
1022
560
|
layout: noteLayoutSchema.optional().describe("Optional note layout: narrow, wide, or full. Prefer wide for most notes unless the user explicitly wants narrow or full.")
|
|
1023
561
|
},
|
|
1024
562
|
async ({ id, title, markdown, layout }) => {
|
|
@@ -1038,13 +576,11 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1038
576
|
}
|
|
1039
577
|
);
|
|
1040
578
|
registerIntentWriteTools(server, {
|
|
1041
|
-
fetchNoteWriteBaseline: (id, writeToken) => fetchMcpNoteWriteBaseline(serverUrl, writeToken, id),
|
|
1042
579
|
jsonRequest,
|
|
1043
580
|
requireWriteToken,
|
|
1044
581
|
serverUrl,
|
|
1045
582
|
token,
|
|
1046
|
-
tools: OCEAN_BRAIN_MCP_TOOLS
|
|
1047
|
-
writeSafety
|
|
583
|
+
tools: OCEAN_BRAIN_MCP_TOOLS
|
|
1048
584
|
});
|
|
1049
585
|
server.tool(
|
|
1050
586
|
OCEAN_BRAIN_MCP_TOOLS.listTags,
|
|
@@ -1079,9 +615,9 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1079
615
|
OCEAN_BRAIN_MCP_TOOLS.listProperties,
|
|
1080
616
|
"List shared Ocean Brain property definitions. Use this before property queries so keys, value types, and select option values are valid.",
|
|
1081
617
|
{
|
|
1082
|
-
query:
|
|
1083
|
-
limit:
|
|
1084
|
-
offset:
|
|
618
|
+
query: z2.string().optional().default("").describe("Optional property key/name search query"),
|
|
619
|
+
limit: z2.number().optional().default(50).describe("Max results (default: 50, server max: 100)"),
|
|
620
|
+
offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
|
|
1085
621
|
},
|
|
1086
622
|
async ({ query, limit, offset }) => {
|
|
1087
623
|
const data = await graphql(serverUrl, token, `
|
|
@@ -1118,9 +654,9 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1118
654
|
OCEAN_BRAIN_MCP_TOOLS.listNotesByTag,
|
|
1119
655
|
"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.",
|
|
1120
656
|
{
|
|
1121
|
-
tag:
|
|
1122
|
-
limit:
|
|
1123
|
-
offset:
|
|
657
|
+
tag: z2.string().describe("Tag name to inspect. You can pass @project or project."),
|
|
658
|
+
limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
|
|
659
|
+
offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
|
|
1124
660
|
},
|
|
1125
661
|
async ({ tag, limit, offset }) => {
|
|
1126
662
|
const normalizedTag = normalizeOceanBrainTagName(tag);
|
|
@@ -1183,10 +719,10 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1183
719
|
OCEAN_BRAIN_MCP_TOOLS.listNotesByTags,
|
|
1184
720
|
"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.",
|
|
1185
721
|
{
|
|
1186
|
-
tags:
|
|
722
|
+
tags: z2.array(z2.string()).min(1).describe("Tag names to inspect. You can pass @project or project values."),
|
|
1187
723
|
mode: tagMatchModeSchema.optional().default("and").describe("Tag match mode. Use and for intersection, or for union. Defaults to and."),
|
|
1188
|
-
limit:
|
|
1189
|
-
offset:
|
|
724
|
+
limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
|
|
725
|
+
offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
|
|
1190
726
|
},
|
|
1191
727
|
async ({ tags, mode, limit, offset }) => {
|
|
1192
728
|
const normalizedTags = normalizeOceanBrainTagNames(tags);
|
|
@@ -1260,15 +796,15 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1260
796
|
OCEAN_BRAIN_MCP_TOOLS.queryNotesByProperties,
|
|
1261
797
|
"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.",
|
|
1262
798
|
{
|
|
1263
|
-
propertyFilters:
|
|
1264
|
-
tagNames:
|
|
799
|
+
propertyFilters: z2.array(propertyFilterSchema).min(1).max(10).describe("Required property filters. Multiple property filters are combined with AND."),
|
|
800
|
+
tagNames: z2.array(z2.string()).optional().default([]).describe("Optional tag filters. You can pass @project, #project, or project."),
|
|
1265
801
|
mode: tagMatchModeSchema.optional().default("and").describe("Tag match mode for tagNames only. Property filters are always combined with AND."),
|
|
1266
802
|
sortBy: viewSortBySchema.optional().default("updatedAt").describe("Sort field (default: updatedAt)"),
|
|
1267
803
|
sortOrder: viewSortOrderSchema.optional().default("desc").describe("Sort order (default: desc)"),
|
|
1268
|
-
includeProperties:
|
|
1269
|
-
propertyKeys:
|
|
1270
|
-
limit:
|
|
1271
|
-
offset:
|
|
804
|
+
includeProperties: z2.boolean().optional().default(false).describe("Include returned note properties. Defaults to false to reduce tokens."),
|
|
805
|
+
propertyKeys: z2.array(z2.string()).optional().default([]).describe("Property keys to include in output; automatically includes properties."),
|
|
806
|
+
limit: z2.number().optional().default(20).describe("Max results (default: 20, server max: 50)"),
|
|
807
|
+
offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
|
|
1272
808
|
},
|
|
1273
809
|
async ({ propertyFilters, tagNames, mode, sortBy, sortOrder, includeProperties, propertyKeys, limit, offset }) => {
|
|
1274
810
|
const shouldIncludeProperties = includeProperties || propertyKeys.length > 0;
|
|
@@ -1327,7 +863,7 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1327
863
|
OCEAN_BRAIN_MCP_TOOLS.listRecentNotes,
|
|
1328
864
|
"List recently updated Ocean Brain notes. Returns titles and tags only. Use ocean_brain_read_note to get full content for a specific note.",
|
|
1329
865
|
{
|
|
1330
|
-
limit:
|
|
866
|
+
limit: z2.number().optional().default(10).describe("Max results (default: 10)")
|
|
1331
867
|
},
|
|
1332
868
|
async ({ limit }) => {
|
|
1333
869
|
const data = await graphql(serverUrl, token, `
|
|
@@ -1361,40 +897,13 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1361
897
|
};
|
|
1362
898
|
}
|
|
1363
899
|
);
|
|
1364
|
-
server.tool(
|
|
1365
|
-
OCEAN_BRAIN_MCP_TOOLS.writeSafetyStatus,
|
|
1366
|
-
"Inspect pending destructive write confirmations and local operation log state for enabled Ocean Brain MCP write tools.",
|
|
1367
|
-
{},
|
|
1368
|
-
async () => {
|
|
1369
|
-
return {
|
|
1370
|
-
content: [{
|
|
1371
|
-
type: "text",
|
|
1372
|
-
text: JSON.stringify({
|
|
1373
|
-
...writeSafety.getStatus(),
|
|
1374
|
-
destructiveWriteTools: [OCEAN_BRAIN_MCP_TOOLS.deleteNote],
|
|
1375
|
-
writeTools: [
|
|
1376
|
-
OCEAN_BRAIN_MCP_TOOLS.createNote,
|
|
1377
|
-
OCEAN_BRAIN_MCP_TOOLS.updateNote,
|
|
1378
|
-
OCEAN_BRAIN_MCP_TOOLS.patchNoteMarkdown,
|
|
1379
|
-
OCEAN_BRAIN_MCP_TOOLS.appendNoteMarkdown,
|
|
1380
|
-
OCEAN_BRAIN_MCP_TOOLS.updateNoteMetadata,
|
|
1381
|
-
OCEAN_BRAIN_MCP_TOOLS.replaceNoteMarkdown,
|
|
1382
|
-
OCEAN_BRAIN_MCP_TOOLS.createTag,
|
|
1383
|
-
OCEAN_BRAIN_MCP_TOOLS.deleteNote
|
|
1384
|
-
],
|
|
1385
|
-
writeToolsEnabled: true
|
|
1386
|
-
}, null, 2)
|
|
1387
|
-
}]
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
|
-
);
|
|
1391
900
|
server.tool(
|
|
1392
901
|
OCEAN_BRAIN_MCP_TOOLS.findNoteCleanupCandidates,
|
|
1393
|
-
"Find Ocean Brain note cleanup candidates for temporary or draft notes before deletion.
|
|
902
|
+
"Find Ocean Brain note cleanup candidates for temporary or draft notes before deletion. This is optional discovery; ocean_brain_delete_note moves a single note to trash directly.",
|
|
1394
903
|
{
|
|
1395
|
-
keywords:
|
|
1396
|
-
limit:
|
|
1397
|
-
offset:
|
|
904
|
+
keywords: z2.string().optional().default("temp tmp draft test wip").describe("Keywords that mark a note as a cleanup candidate. Comma or space separated."),
|
|
905
|
+
limit: z2.number().optional().default(20).describe("Max results (default: 20)"),
|
|
906
|
+
offset: z2.number().optional().default(0).describe("Pagination offset (default: 0)")
|
|
1398
907
|
},
|
|
1399
908
|
async ({ keywords, limit, offset }) => {
|
|
1400
909
|
const normalizedKeywords = keywords.split(/[,\s]+/).map((keyword) => keyword.trim()).filter(Boolean);
|
|
@@ -1437,7 +946,7 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1437
946
|
OCEAN_BRAIN_MCP_TOOLS.createTag,
|
|
1438
947
|
"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.",
|
|
1439
948
|
{
|
|
1440
|
-
name:
|
|
949
|
+
name: z2.string().describe("Tag name to create. You can pass @project or project.")
|
|
1441
950
|
},
|
|
1442
951
|
async ({ name }) => {
|
|
1443
952
|
const writeToken = requireWriteToken(token, OCEAN_BRAIN_MCP_TOOLS.createTag);
|
|
@@ -1454,92 +963,23 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1454
963
|
);
|
|
1455
964
|
server.tool(
|
|
1456
965
|
OCEAN_BRAIN_MCP_TOOLS.deleteNote,
|
|
1457
|
-
"
|
|
966
|
+
"Move an Ocean Brain note to trash, like `mv note.md trash/`. This is a recoverable trash move, not permanent deletion. Deleted-note tag names are kept as restore metadata; orphan tags are not a blocking condition.",
|
|
1458
967
|
{
|
|
1459
|
-
id:
|
|
1460
|
-
...destructiveMcpWriteFields
|
|
968
|
+
id: z2.string().describe("Note ID to move to trash")
|
|
1461
969
|
},
|
|
1462
|
-
async ({ id
|
|
970
|
+
async ({ id }) => {
|
|
1463
971
|
const writeToken = requireWriteToken(token, OCEAN_BRAIN_MCP_TOOLS.deleteNote);
|
|
1464
|
-
const data = await graphql(serverUrl, writeToken, `
|
|
1465
|
-
query ($id: ID!) {
|
|
1466
|
-
noteCleanupPreview(id: $id) {
|
|
1467
|
-
id
|
|
1468
|
-
title
|
|
1469
|
-
updatedAt
|
|
1470
|
-
pinned
|
|
1471
|
-
tagNames
|
|
1472
|
-
reminderCount
|
|
1473
|
-
backReferences {
|
|
1474
|
-
id
|
|
1475
|
-
title
|
|
1476
|
-
}
|
|
1477
|
-
orphanedTagNames
|
|
1478
|
-
requiresForce
|
|
1479
|
-
forceReasons
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
`, { id });
|
|
1483
|
-
const preview = data?.noteCleanupPreview;
|
|
1484
|
-
if (!preview) {
|
|
1485
|
-
return {
|
|
1486
|
-
content: [{
|
|
1487
|
-
type: "text",
|
|
1488
|
-
text: JSON.stringify({
|
|
1489
|
-
deleted: false,
|
|
1490
|
-
reason: "NOTE_NOT_FOUND",
|
|
1491
|
-
id
|
|
1492
|
-
}, null, 2)
|
|
1493
|
-
}]
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
const intent = writeSafety.ensureDestructiveWriteRequest(
|
|
1497
|
-
{ dryRun, operationId, confirmToken, force },
|
|
1498
|
-
{
|
|
1499
|
-
actor: "mcp-bearer",
|
|
1500
|
-
affectedIds: [preview.id],
|
|
1501
|
-
estimatedChangeCount: 1,
|
|
1502
|
-
force: preview.requiresForce,
|
|
1503
|
-
risk: "destructive",
|
|
1504
|
-
summary: `Delete note ${preview.id} (${preview.title})`,
|
|
1505
|
-
toolName: OCEAN_BRAIN_MCP_TOOLS.deleteNote
|
|
1506
|
-
}
|
|
1507
|
-
);
|
|
1508
|
-
if (intent.kind === "dry-run") {
|
|
1509
|
-
return {
|
|
1510
|
-
content: [{
|
|
1511
|
-
type: "text",
|
|
1512
|
-
text: JSON.stringify({
|
|
1513
|
-
mode: "dry-run",
|
|
1514
|
-
preview,
|
|
1515
|
-
operation: intent.operation
|
|
1516
|
-
}, null, 2)
|
|
1517
|
-
}]
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
if (intent.operation.force && !force) {
|
|
1521
|
-
throw new Error(`${OCEAN_BRAIN_MCP_TOOLS.deleteNote} requires force=true because ${preview.forceReasons.join(", ")}.`);
|
|
1522
|
-
}
|
|
1523
972
|
try {
|
|
1524
|
-
const result = await jsonRequest(serverUrl, writeToken, "/api/mcp/notes/delete", {
|
|
1525
|
-
id: preview.id
|
|
1526
|
-
});
|
|
1527
|
-
writeSafety.recordWriteResult(intent.operation, true);
|
|
973
|
+
const result = await jsonRequest(serverUrl, writeToken, "/api/mcp/notes/delete", { id });
|
|
1528
974
|
return {
|
|
1529
975
|
content: [{
|
|
1530
976
|
type: "text",
|
|
1531
|
-
text: JSON.stringify(
|
|
1532
|
-
mode: "commit",
|
|
1533
|
-
preview,
|
|
1534
|
-
operation: intent.operation,
|
|
1535
|
-
result
|
|
1536
|
-
}, null, 2)
|
|
977
|
+
text: JSON.stringify(result, null, 2)
|
|
1537
978
|
}]
|
|
1538
979
|
};
|
|
1539
980
|
} catch (error) {
|
|
1540
981
|
const message = error instanceof Error ? error.message : "Unknown MCP note delete error";
|
|
1541
|
-
|
|
1542
|
-
throw error;
|
|
982
|
+
throw new Error(message);
|
|
1543
983
|
}
|
|
1544
984
|
}
|
|
1545
985
|
);
|
|
@@ -1547,7 +987,11 @@ async function startMcpServer(serverUrl, token, options = {}) {
|
|
|
1547
987
|
await server.connect(transport);
|
|
1548
988
|
}
|
|
1549
989
|
export {
|
|
990
|
+
OCEAN_BRAIN_MCP_CLIENT_VERSION_HEADER,
|
|
991
|
+
OCEAN_BRAIN_MCP_COMPATIBILITY_VERSION,
|
|
992
|
+
OCEAN_BRAIN_MCP_COMPATIBILITY_VERSION_HEADER,
|
|
1550
993
|
OCEAN_BRAIN_MCP_TOOLS,
|
|
1551
|
-
|
|
994
|
+
OCEAN_BRAIN_MCP_VERSION_HEADER,
|
|
995
|
+
createMcpRequestHeaders,
|
|
1552
996
|
startMcpServer
|
|
1553
997
|
};
|