affine-mcp-server 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted or cloud). It exposes AFFiNE workspaces and documents to AI assistants over stdio.
4
4
 
5
- [![Version](https://img.shields.io/badge/version-1.4.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
5
+ [![Version](https://img.shields.io/badge/version-1.5.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
6
6
  [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.17.2-green)](https://github.com/modelcontextprotocol/typescript-sdk)
7
7
  [![CI](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml)
8
8
  [![License](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)
@@ -19,7 +19,7 @@ A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted
19
19
  - Tools: 32 focused tools with WebSocket-based document editing
20
20
  - Status: Active
21
21
 
22
- > New in v1.4.0: Added `read_doc` for document content snapshots (blocks + plain text), plus Cursor setup/troubleshooting guidance.
22
+ > New in v1.5.0: `append_block` now supports 30 verified block profiles, including database/edgeless (`frame`, `edgeless_text`, `surface_ref`, `note`) insertion paths. For stability on AFFiNE 0.26.x, `type=\"data_view\"` is currently mapped to a database block.
23
23
 
24
24
  ## Features
25
25
 
@@ -166,7 +166,7 @@ If you prefer `npx`:
166
166
  - `revoke_doc` – revoke public access
167
167
  - `create_doc` – create a new document (WebSocket)
168
168
  - `append_paragraph` – append a paragraph block (WebSocket)
169
- - `append_block` – append slash-command style blocks (`heading/list/todo/code/divider/quote`)
169
+ - `append_block` – append canonical block types (text/list/code/media/embed/database/edgeless) with strict validation and placement control (`data_view` currently falls back to database)
170
170
  - `delete_doc` – delete a document (WebSocket)
171
171
 
172
172
  ### Comments
@@ -243,6 +243,11 @@ Workspace visibility
243
243
 
244
244
  ## Version History
245
245
 
246
+ ### 1.5.0 (2026‑02‑13)
247
+ - Expanded `append_block` from Step1 to Step4 profiles: canonical text/list/code/divider/callout/latex/table/bookmark/media/embed plus `database`, `data_view`, `surface_ref`, `frame`, `edgeless_text`, `note` (`data_view` currently mapped to database for stability)
248
+ - Added strict field validation and canonical parent enforcement for page/note/surface containers
249
+ - Added local integration runner coverage for all 30 append_block cases against a live AFFINE server
250
+
246
251
  ### 1.4.0 (2026‑02‑13)
247
252
  - Added `read_doc` for reading document block snapshot + plain text
248
253
  - Added Cursor setup examples and troubleshooting notes for JSON-RPC method usage
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { loginWithPassword } from "./auth.js";
15
15
  import { registerAuthTools } from "./tools/auth.js";
16
16
  const config = loadConfig();
17
17
  async function buildServer() {
18
- const server = new McpServer({ name: "affine-mcp", version: "1.4.0" });
18
+ const server = new McpServer({ name: "affine-mcp", version: "1.5.0" });
19
19
  // Initialize GraphQL client with authentication
20
20
  const gql = new GraphQLClient({
21
21
  endpoint: `${config.baseUrl}${config.graphqlPath}`,
@@ -4,19 +4,52 @@ import { wsUrlFromGraphQLEndpoint, connectWorkspaceSocket, joinWorkspace, loadDo
4
4
  import * as Y from "yjs";
5
5
  const WorkspaceId = z.string().min(1, "workspaceId required");
6
6
  const DocId = z.string().min(1, "docId required");
7
- const APPEND_BLOCK_TYPE_VALUES = [
7
+ const APPEND_BLOCK_CANONICAL_TYPE_VALUES = [
8
8
  "paragraph",
9
- "heading1",
10
- "heading2",
11
- "heading3",
9
+ "heading",
12
10
  "quote",
13
- "bulleted_list",
14
- "numbered_list",
15
- "todo",
11
+ "list",
16
12
  "code",
17
13
  "divider",
14
+ "callout",
15
+ "latex",
16
+ "table",
17
+ "bookmark",
18
+ "image",
19
+ "attachment",
20
+ "embed_youtube",
21
+ "embed_github",
22
+ "embed_figma",
23
+ "embed_loom",
24
+ "embed_html",
25
+ "embed_linked_doc",
26
+ "embed_synced_doc",
27
+ "embed_iframe",
28
+ "database",
29
+ "data_view",
30
+ "surface_ref",
31
+ "frame",
32
+ "edgeless_text",
33
+ "note",
18
34
  ];
19
- const AppendBlockType = z.enum(APPEND_BLOCK_TYPE_VALUES);
35
+ const APPEND_BLOCK_LEGACY_ALIAS_MAP = {
36
+ heading1: "heading",
37
+ heading2: "heading",
38
+ heading3: "heading",
39
+ bulleted_list: "list",
40
+ numbered_list: "list",
41
+ todo: "list",
42
+ };
43
+ const APPEND_BLOCK_LIST_STYLE_VALUES = ["bulleted", "numbered", "todo"];
44
+ const AppendBlockListStyle = z.enum(APPEND_BLOCK_LIST_STYLE_VALUES);
45
+ const APPEND_BLOCK_BOOKMARK_STYLE_VALUES = [
46
+ "vertical",
47
+ "horizontal",
48
+ "list",
49
+ "cube",
50
+ "citation",
51
+ ];
52
+ const AppendBlockBookmarkStyle = z.enum(APPEND_BLOCK_BOOKMARK_STYLE_VALUES);
20
53
  function blockVersion(flavour) {
21
54
  switch (flavour) {
22
55
  case "affine:page":
@@ -121,71 +154,816 @@ export function registerDocTools(server, gql, defaults) {
121
154
  pageChildren.push([noteId]);
122
155
  return noteId;
123
156
  }
124
- function createBlock(noteId, parsed) {
157
+ function ensureSurfaceBlock(blocks) {
158
+ const existingSurfaceId = findBlockIdByFlavour(blocks, "affine:surface");
159
+ if (existingSurfaceId) {
160
+ return existingSurfaceId;
161
+ }
162
+ const pageId = findBlockIdByFlavour(blocks, "affine:page");
163
+ if (!pageId) {
164
+ throw new Error("Document has no page block; unable to create/find surface.");
165
+ }
166
+ const surfaceId = generateId();
167
+ const surface = new Y.Map();
168
+ setSysFields(surface, surfaceId, "affine:surface");
169
+ surface.set("sys:parent", pageId);
170
+ surface.set("sys:children", new Y.Array());
171
+ const elements = new Y.Map();
172
+ elements.set("type", "$blocksuite:internal:native$");
173
+ elements.set("value", new Y.Map());
174
+ surface.set("prop:elements", elements);
175
+ blocks.set(surfaceId, surface);
176
+ const page = blocks.get(pageId);
177
+ let pageChildren = page.get("sys:children");
178
+ if (!(pageChildren instanceof Y.Array)) {
179
+ pageChildren = new Y.Array();
180
+ page.set("sys:children", pageChildren);
181
+ }
182
+ pageChildren.push([surfaceId]);
183
+ return surfaceId;
184
+ }
185
+ function normalizeBlockTypeInput(typeInput) {
186
+ const key = typeInput.trim().toLowerCase();
187
+ if (APPEND_BLOCK_CANONICAL_TYPE_VALUES.includes(key)) {
188
+ return { type: key };
189
+ }
190
+ if (Object.prototype.hasOwnProperty.call(APPEND_BLOCK_LEGACY_ALIAS_MAP, key)) {
191
+ const legacyType = key;
192
+ const type = APPEND_BLOCK_LEGACY_ALIAS_MAP[legacyType];
193
+ const listStyleFromAlias = legacyType === "bulleted_list"
194
+ ? "bulleted"
195
+ : legacyType === "numbered_list"
196
+ ? "numbered"
197
+ : legacyType === "todo"
198
+ ? "todo"
199
+ : undefined;
200
+ const headingLevelFromAlias = legacyType === "heading1"
201
+ ? 1
202
+ : legacyType === "heading2"
203
+ ? 2
204
+ : legacyType === "heading3"
205
+ ? 3
206
+ : undefined;
207
+ return { type, legacyType, headingLevelFromAlias, listStyleFromAlias };
208
+ }
209
+ const supported = [
210
+ ...APPEND_BLOCK_CANONICAL_TYPE_VALUES,
211
+ ...Object.keys(APPEND_BLOCK_LEGACY_ALIAS_MAP),
212
+ ].join(", ");
213
+ throw new Error(`Unsupported append_block type '${typeInput}'. Supported types: ${supported}`);
214
+ }
215
+ function normalizePlacement(placement) {
216
+ if (!placement)
217
+ return undefined;
218
+ const normalized = {};
219
+ if (placement.parentId?.trim())
220
+ normalized.parentId = placement.parentId.trim();
221
+ if (placement.afterBlockId?.trim())
222
+ normalized.afterBlockId = placement.afterBlockId.trim();
223
+ if (placement.beforeBlockId?.trim())
224
+ normalized.beforeBlockId = placement.beforeBlockId.trim();
225
+ if (placement.index !== undefined)
226
+ normalized.index = placement.index;
227
+ const hasAfter = Boolean(normalized.afterBlockId);
228
+ const hasBefore = Boolean(normalized.beforeBlockId);
229
+ if (hasAfter && hasBefore) {
230
+ throw new Error("placement.afterBlockId and placement.beforeBlockId are mutually exclusive.");
231
+ }
232
+ if (normalized.index !== undefined) {
233
+ if (!Number.isInteger(normalized.index) || normalized.index < 0) {
234
+ throw new Error("placement.index must be an integer greater than or equal to 0.");
235
+ }
236
+ if (hasAfter || hasBefore) {
237
+ throw new Error("placement.index cannot be used with placement.afterBlockId/beforeBlockId.");
238
+ }
239
+ }
240
+ if (!normalized.parentId && !normalized.afterBlockId && !normalized.beforeBlockId && normalized.index === undefined) {
241
+ return undefined;
242
+ }
243
+ return normalized;
244
+ }
245
+ function validateNormalizedAppendBlockInput(normalized, raw) {
246
+ if (normalized.type === "heading") {
247
+ if (!Number.isInteger(normalized.headingLevel) || normalized.headingLevel < 1 || normalized.headingLevel > 6) {
248
+ throw new Error("Heading level must be an integer from 1 to 6.");
249
+ }
250
+ }
251
+ else if (raw.level !== undefined && normalized.strict) {
252
+ throw new Error("The 'level' field can only be used with type='heading'.");
253
+ }
254
+ if (normalized.type === "list") {
255
+ if (!APPEND_BLOCK_LIST_STYLE_VALUES.includes(normalized.listStyle)) {
256
+ throw new Error(`Invalid list style '${normalized.listStyle}'.`);
257
+ }
258
+ if (normalized.listStyle !== "todo" && raw.checked !== undefined && normalized.strict) {
259
+ throw new Error("The 'checked' field can only be used when list style is 'todo'.");
260
+ }
261
+ }
262
+ else {
263
+ if (raw.style !== undefined && normalized.strict) {
264
+ throw new Error("The 'style' field can only be used with type='list'.");
265
+ }
266
+ if (raw.checked !== undefined && normalized.strict) {
267
+ throw new Error("The 'checked' field can only be used with type='list' (style='todo').");
268
+ }
269
+ }
270
+ if (normalized.type !== "code") {
271
+ if (raw.language !== undefined && normalized.strict) {
272
+ throw new Error("The 'language' field can only be used with type='code'.");
273
+ }
274
+ const allowsCaption = normalized.type === "bookmark" ||
275
+ normalized.type === "image" ||
276
+ normalized.type === "attachment" ||
277
+ normalized.type === "surface_ref" ||
278
+ normalized.type.startsWith("embed_");
279
+ if (raw.caption !== undefined && !allowsCaption && normalized.strict) {
280
+ throw new Error("The 'caption' field is not valid for this block type.");
281
+ }
282
+ }
283
+ else if (normalized.language.length > 64) {
284
+ throw new Error("Code language is too long (max 64 chars).");
285
+ }
286
+ if (normalized.type === "divider" && raw.text && raw.text.length > 0 && normalized.strict) {
287
+ throw new Error("Divider blocks do not accept text.");
288
+ }
289
+ const requiresUrl = [
290
+ "bookmark",
291
+ "embed_youtube",
292
+ "embed_github",
293
+ "embed_figma",
294
+ "embed_loom",
295
+ "embed_iframe",
296
+ ];
297
+ const urlAllowedTypes = [...requiresUrl];
298
+ if (urlAllowedTypes.includes(normalized.type)) {
299
+ if (!normalized.url) {
300
+ throw new Error(`${normalized.type} blocks require a non-empty url.`);
301
+ }
302
+ try {
303
+ new URL(normalized.url);
304
+ }
305
+ catch {
306
+ throw new Error(`Invalid url for ${normalized.type} block: '${normalized.url}'.`);
307
+ }
308
+ }
309
+ if (normalized.type === "bookmark") {
310
+ if (!APPEND_BLOCK_BOOKMARK_STYLE_VALUES.includes(normalized.bookmarkStyle)) {
311
+ throw new Error(`Invalid bookmark style '${normalized.bookmarkStyle}'.`);
312
+ }
313
+ }
314
+ else {
315
+ if (raw.bookmarkStyle !== undefined && normalized.strict) {
316
+ throw new Error("The 'bookmarkStyle' field can only be used with type='bookmark'.");
317
+ }
318
+ if (raw.url !== undefined && !urlAllowedTypes.includes(normalized.type) && normalized.strict) {
319
+ throw new Error("The 'url' field is not valid for this block type.");
320
+ }
321
+ }
322
+ if (normalized.type === "image" || normalized.type === "attachment") {
323
+ if (!normalized.sourceId) {
324
+ throw new Error(`${normalized.type} blocks require sourceId (use upload_blob first).`);
325
+ }
326
+ if (normalized.type === "attachment" && (!normalized.name || !normalized.mimeType)) {
327
+ throw new Error("attachment blocks require valid name and mimeType.");
328
+ }
329
+ }
330
+ else if (raw.sourceId !== undefined && normalized.strict) {
331
+ throw new Error("The 'sourceId' field can only be used with type='image' or type='attachment'.");
332
+ }
333
+ else if ((raw.name !== undefined || raw.mimeType !== undefined || raw.embed !== undefined || raw.size !== undefined) &&
334
+ normalized.strict) {
335
+ throw new Error("The 'name'/'mimeType'/'embed'/'size' fields are only valid for image/attachment blocks.");
336
+ }
337
+ if (normalized.type === "latex") {
338
+ if (!normalized.latex && normalized.strict) {
339
+ throw new Error("latex blocks require a non-empty 'latex' value in strict mode.");
340
+ }
341
+ }
342
+ else if (raw.latex !== undefined && normalized.strict) {
343
+ throw new Error("The 'latex' field can only be used with type='latex'.");
344
+ }
345
+ if (normalized.type === "embed_linked_doc" || normalized.type === "embed_synced_doc") {
346
+ if (!normalized.pageId) {
347
+ throw new Error(`${normalized.type} blocks require pageId.`);
348
+ }
349
+ }
350
+ else if (raw.pageId !== undefined && normalized.strict) {
351
+ throw new Error("The 'pageId' field can only be used with linked/synced doc embed types.");
352
+ }
353
+ if (normalized.type === "embed_html") {
354
+ if (!normalized.html && !normalized.design && normalized.strict) {
355
+ throw new Error("embed_html blocks require html or design.");
356
+ }
357
+ }
358
+ else if ((raw.html !== undefined || raw.design !== undefined) && normalized.strict) {
359
+ throw new Error("The 'html'/'design' fields can only be used with type='embed_html'.");
360
+ }
361
+ if (normalized.type === "embed_iframe") {
362
+ if (raw.iframeUrl !== undefined && !normalized.iframeUrl && normalized.strict) {
363
+ throw new Error("embed_iframe iframeUrl cannot be empty when provided.");
364
+ }
365
+ }
366
+ else if (raw.iframeUrl !== undefined && normalized.strict) {
367
+ throw new Error("The 'iframeUrl' field can only be used with type='embed_iframe'.");
368
+ }
369
+ if (normalized.type === "surface_ref") {
370
+ if (!normalized.reference) {
371
+ throw new Error("surface_ref blocks require 'reference' (target element/block id).");
372
+ }
373
+ if (!normalized.refFlavour) {
374
+ throw new Error("surface_ref blocks require 'refFlavour' (for example affine:frame).");
375
+ }
376
+ }
377
+ else if ((raw.reference !== undefined || raw.refFlavour !== undefined) && normalized.strict) {
378
+ throw new Error("The 'reference'/'refFlavour' fields can only be used with type='surface_ref'.");
379
+ }
380
+ if (normalized.type === "frame" || normalized.type === "edgeless_text" || normalized.type === "note") {
381
+ if (!Number.isInteger(normalized.width) || normalized.width < 1 || normalized.width > 10000) {
382
+ throw new Error(`${normalized.type} width must be an integer between 1 and 10000.`);
383
+ }
384
+ if (!Number.isInteger(normalized.height) || normalized.height < 1 || normalized.height > 10000) {
385
+ throw new Error(`${normalized.type} height must be an integer between 1 and 10000.`);
386
+ }
387
+ }
388
+ else if ((raw.width !== undefined || raw.height !== undefined) && normalized.strict) {
389
+ throw new Error("The 'width'/'height' fields are only valid for frame/edgeless_text/note.");
390
+ }
391
+ if (normalized.type !== "frame" && normalized.type !== "note" && raw.background !== undefined && normalized.strict) {
392
+ throw new Error("The 'background' field is only valid for frame/note.");
393
+ }
394
+ if (normalized.type === "table") {
395
+ if (!Number.isInteger(normalized.rows) || normalized.rows < 1 || normalized.rows > 20) {
396
+ throw new Error("table rows must be an integer between 1 and 20.");
397
+ }
398
+ if (!Number.isInteger(normalized.columns) || normalized.columns < 1 || normalized.columns > 20) {
399
+ throw new Error("table columns must be an integer between 1 and 20.");
400
+ }
401
+ }
402
+ else if ((raw.rows !== undefined || raw.columns !== undefined) && normalized.strict) {
403
+ throw new Error("The 'rows'/'columns' fields can only be used with type='table'.");
404
+ }
405
+ }
406
+ function normalizeAppendBlockInput(parsed) {
407
+ const strict = parsed.strict !== false;
408
+ const typeInfo = normalizeBlockTypeInput(parsed.type);
409
+ const headingLevelCandidate = parsed.level ?? typeInfo.headingLevelFromAlias ?? 1;
410
+ const headingLevelNumber = Number(headingLevelCandidate);
411
+ const headingLevel = Math.max(1, Math.min(6, headingLevelNumber));
412
+ const listStyle = typeInfo.listStyleFromAlias ?? parsed.style ?? "bulleted";
413
+ const bookmarkStyle = parsed.bookmarkStyle ?? "horizontal";
414
+ const language = (parsed.language ?? "txt").trim().toLowerCase() || "txt";
415
+ const placement = normalizePlacement(parsed.placement);
416
+ const url = (parsed.url ?? "").trim();
417
+ const pageId = (parsed.pageId ?? "").trim();
418
+ const iframeUrl = (parsed.iframeUrl ?? "").trim();
419
+ const html = parsed.html ?? "";
420
+ const design = parsed.design ?? "";
421
+ const reference = (parsed.reference ?? "").trim();
422
+ const refFlavour = (parsed.refFlavour ?? "").trim();
423
+ const width = Number.isFinite(parsed.width) ? Math.max(1, Math.floor(parsed.width)) : 100;
424
+ const height = Number.isFinite(parsed.height) ? Math.max(1, Math.floor(parsed.height)) : 100;
425
+ const background = (parsed.background ?? "transparent").trim() || "transparent";
426
+ const sourceId = (parsed.sourceId ?? "").trim();
427
+ const name = (parsed.name ?? "attachment").trim() || "attachment";
428
+ const mimeType = (parsed.mimeType ?? "application/octet-stream").trim() || "application/octet-stream";
429
+ const size = Number.isFinite(parsed.size) ? Math.max(0, Math.floor(parsed.size)) : 0;
430
+ const rows = Number.isInteger(parsed.rows) ? parsed.rows : 3;
431
+ const columns = Number.isInteger(parsed.columns) ? parsed.columns : 3;
432
+ const latex = (parsed.latex ?? "").trim();
433
+ const normalized = {
434
+ workspaceId: parsed.workspaceId,
435
+ docId: parsed.docId,
436
+ type: typeInfo.type,
437
+ strict,
438
+ placement,
439
+ text: parsed.text ?? "",
440
+ url,
441
+ pageId,
442
+ iframeUrl,
443
+ html,
444
+ design,
445
+ reference,
446
+ refFlavour,
447
+ width,
448
+ height,
449
+ background,
450
+ sourceId,
451
+ name,
452
+ mimeType,
453
+ size,
454
+ embed: Boolean(parsed.embed),
455
+ rows,
456
+ columns,
457
+ latex,
458
+ headingLevel,
459
+ listStyle,
460
+ bookmarkStyle,
461
+ checked: Boolean(parsed.checked),
462
+ language,
463
+ caption: parsed.caption,
464
+ legacyType: typeInfo.legacyType,
465
+ };
466
+ validateNormalizedAppendBlockInput(normalized, parsed);
467
+ return normalized;
468
+ }
469
+ function findBlockById(blocks, blockId) {
470
+ const value = blocks.get(blockId);
471
+ if (value instanceof Y.Map)
472
+ return value;
473
+ return null;
474
+ }
475
+ function ensureChildrenArray(block) {
476
+ const current = block.get("sys:children");
477
+ if (current instanceof Y.Array)
478
+ return current;
479
+ const created = new Y.Array();
480
+ block.set("sys:children", created);
481
+ return created;
482
+ }
483
+ function indexOfChild(children, blockId) {
484
+ let index = -1;
485
+ children.forEach((entry, i) => {
486
+ if (index >= 0)
487
+ return;
488
+ if (typeof entry === "string") {
489
+ if (entry === blockId)
490
+ index = i;
491
+ return;
492
+ }
493
+ if (Array.isArray(entry)) {
494
+ for (const child of entry) {
495
+ if (child === blockId) {
496
+ index = i;
497
+ return;
498
+ }
499
+ }
500
+ }
501
+ });
502
+ return index;
503
+ }
504
+ function resolveInsertContext(blocks, normalized) {
505
+ const placement = normalized.placement;
506
+ let parentId;
507
+ let referenceBlockId;
508
+ let mode = "append";
509
+ if (placement?.afterBlockId) {
510
+ mode = "after";
511
+ referenceBlockId = placement.afterBlockId;
512
+ const referenceBlock = findBlockById(blocks, referenceBlockId);
513
+ if (!referenceBlock)
514
+ throw new Error(`placement.afterBlockId '${referenceBlockId}' was not found.`);
515
+ const refParentId = referenceBlock.get("sys:parent");
516
+ if (typeof refParentId !== "string" || !refParentId) {
517
+ throw new Error(`Block '${referenceBlockId}' has no parent.`);
518
+ }
519
+ parentId = refParentId;
520
+ }
521
+ else if (placement?.beforeBlockId) {
522
+ mode = "before";
523
+ referenceBlockId = placement.beforeBlockId;
524
+ const referenceBlock = findBlockById(blocks, referenceBlockId);
525
+ if (!referenceBlock)
526
+ throw new Error(`placement.beforeBlockId '${referenceBlockId}' was not found.`);
527
+ const refParentId = referenceBlock.get("sys:parent");
528
+ if (typeof refParentId !== "string" || !refParentId) {
529
+ throw new Error(`Block '${referenceBlockId}' has no parent.`);
530
+ }
531
+ parentId = refParentId;
532
+ }
533
+ else if (placement?.parentId) {
534
+ mode = placement.index !== undefined ? "index" : "append";
535
+ parentId = placement.parentId;
536
+ }
537
+ if (!parentId) {
538
+ if (normalized.type === "frame" || normalized.type === "edgeless_text") {
539
+ parentId = ensureSurfaceBlock(blocks);
540
+ }
541
+ else if (normalized.type === "note") {
542
+ parentId = findBlockIdByFlavour(blocks, "affine:page") || undefined;
543
+ if (!parentId) {
544
+ throw new Error("Document has no page block; unable to insert note.");
545
+ }
546
+ }
547
+ else {
548
+ parentId = ensureNoteBlock(blocks);
549
+ }
550
+ }
551
+ const parentBlock = findBlockById(blocks, parentId);
552
+ if (!parentBlock) {
553
+ throw new Error(`Target parent block '${parentId}' was not found.`);
554
+ }
555
+ const parentFlavour = parentBlock.get("sys:flavour");
556
+ if (normalized.strict) {
557
+ if (parentFlavour === "affine:page" && normalized.type !== "note") {
558
+ throw new Error(`Cannot append '${normalized.type}' directly under 'affine:page'.`);
559
+ }
560
+ if (parentFlavour === "affine:surface" &&
561
+ normalized.type !== "frame" &&
562
+ normalized.type !== "edgeless_text") {
563
+ throw new Error(`Cannot append '${normalized.type}' directly under 'affine:surface'.`);
564
+ }
565
+ if (normalized.type === "note" && parentFlavour !== "affine:page") {
566
+ throw new Error("note blocks must be appended under affine:page.");
567
+ }
568
+ if ((normalized.type === "frame" || normalized.type === "edgeless_text") &&
569
+ parentFlavour !== "affine:surface") {
570
+ throw new Error(`${normalized.type} blocks must be appended under affine:surface.`);
571
+ }
572
+ }
573
+ const children = ensureChildrenArray(parentBlock);
574
+ let insertIndex = children.length;
575
+ if (mode === "after" || mode === "before") {
576
+ const idx = indexOfChild(children, referenceBlockId);
577
+ if (idx < 0) {
578
+ throw new Error(`Reference block '${referenceBlockId}' is not a child of parent '${parentId}'.`);
579
+ }
580
+ insertIndex = mode === "after" ? idx + 1 : idx;
581
+ }
582
+ else if (mode === "index") {
583
+ const requestedIndex = placement?.index ?? children.length;
584
+ if (requestedIndex > children.length && normalized.strict) {
585
+ throw new Error(`placement.index ${requestedIndex} is out of range (max ${children.length}).`);
586
+ }
587
+ insertIndex = Math.min(requestedIndex, children.length);
588
+ }
589
+ return { parentId, parentBlock, children, insertIndex };
590
+ }
591
+ function createBlock(parentId, normalized) {
125
592
  const blockId = generateId();
126
593
  const block = new Y.Map();
127
- const content = parsed.text ?? "";
128
- switch (parsed.type) {
594
+ const content = normalized.text;
595
+ switch (normalized.type) {
129
596
  case "paragraph":
130
- case "heading1":
131
- case "heading2":
132
- case "heading3":
597
+ case "heading":
133
598
  case "quote": {
134
599
  setSysFields(block, blockId, "affine:paragraph");
135
- block.set("sys:parent", noteId);
600
+ block.set("sys:parent", parentId);
136
601
  block.set("sys:children", new Y.Array());
137
- const blockType = parsed.type === "heading1"
138
- ? "h1"
139
- : parsed.type === "heading2"
140
- ? "h2"
141
- : parsed.type === "heading3"
142
- ? "h3"
143
- : parsed.type === "quote"
144
- ? "quote"
145
- : "text";
602
+ const blockType = normalized.type === "heading"
603
+ ? `h${normalized.headingLevel}`
604
+ : normalized.type === "quote"
605
+ ? "quote"
606
+ : "text";
146
607
  block.set("prop:type", blockType);
147
608
  block.set("prop:text", makeText(content));
148
609
  return { blockId, block, flavour: "affine:paragraph", blockType };
149
610
  }
150
- case "bulleted_list":
151
- case "numbered_list":
152
- case "todo": {
611
+ case "list": {
153
612
  setSysFields(block, blockId, "affine:list");
154
- block.set("sys:parent", noteId);
613
+ block.set("sys:parent", parentId);
155
614
  block.set("sys:children", new Y.Array());
156
- const blockType = parsed.type === "bulleted_list"
157
- ? "bulleted"
158
- : parsed.type === "numbered_list"
159
- ? "numbered"
160
- : "todo";
161
- block.set("prop:type", blockType);
162
- if (blockType === "todo") {
163
- block.set("prop:checked", Boolean(parsed.checked));
164
- }
615
+ block.set("prop:type", normalized.listStyle);
616
+ block.set("prop:checked", normalized.listStyle === "todo" ? normalized.checked : false);
165
617
  block.set("prop:text", makeText(content));
166
- return { blockId, block, flavour: "affine:list", blockType };
618
+ return { blockId, block, flavour: "affine:list", blockType: normalized.listStyle };
167
619
  }
168
620
  case "code": {
169
621
  setSysFields(block, blockId, "affine:code");
170
- block.set("sys:parent", noteId);
622
+ block.set("sys:parent", parentId);
171
623
  block.set("sys:children", new Y.Array());
172
- block.set("prop:language", (parsed.language || "txt").toLowerCase());
173
- if (parsed.caption) {
174
- block.set("prop:caption", parsed.caption);
624
+ block.set("prop:language", normalized.language);
625
+ if (normalized.caption) {
626
+ block.set("prop:caption", normalized.caption);
175
627
  }
176
628
  block.set("prop:text", makeText(content));
177
629
  return { blockId, block, flavour: "affine:code" };
178
630
  }
179
631
  case "divider": {
180
632
  setSysFields(block, blockId, "affine:divider");
181
- block.set("sys:parent", noteId);
633
+ block.set("sys:parent", parentId);
182
634
  block.set("sys:children", new Y.Array());
183
635
  return { blockId, block, flavour: "affine:divider" };
184
636
  }
637
+ case "callout": {
638
+ setSysFields(block, blockId, "affine:callout");
639
+ block.set("sys:parent", parentId);
640
+ block.set("sys:children", new Y.Array());
641
+ block.set("prop:icon", { type: "emoji", unicode: "💡" });
642
+ block.set("prop:backgroundColorName", "grey");
643
+ block.set("prop:text", makeText(content));
644
+ return { blockId, block, flavour: "affine:callout" };
645
+ }
646
+ case "latex": {
647
+ setSysFields(block, blockId, "affine:latex");
648
+ block.set("sys:parent", parentId);
649
+ block.set("sys:children", new Y.Array());
650
+ block.set("prop:xywh", "[0,0,16,16]");
651
+ block.set("prop:index", "a0");
652
+ block.set("prop:lockedBySelf", false);
653
+ block.set("prop:scale", 1);
654
+ block.set("prop:rotate", 0);
655
+ block.set("prop:latex", normalized.latex);
656
+ return { blockId, block, flavour: "affine:latex" };
657
+ }
658
+ case "table": {
659
+ setSysFields(block, blockId, "affine:table");
660
+ block.set("sys:parent", parentId);
661
+ block.set("sys:children", new Y.Array());
662
+ const rows = {};
663
+ const columns = {};
664
+ const cells = {};
665
+ for (let i = 0; i < normalized.rows; i++) {
666
+ const rowId = generateId();
667
+ rows[rowId] = { rowId, order: `r${String(i).padStart(4, "0")}` };
668
+ }
669
+ for (let i = 0; i < normalized.columns; i++) {
670
+ const columnId = generateId();
671
+ columns[columnId] = { columnId, order: `c${String(i).padStart(4, "0")}` };
672
+ }
673
+ for (const rowId of Object.keys(rows)) {
674
+ for (const columnId of Object.keys(columns)) {
675
+ cells[`${rowId}:${columnId}`] = { text: makeText("") };
676
+ }
677
+ }
678
+ block.set("prop:rows", rows);
679
+ block.set("prop:columns", columns);
680
+ block.set("prop:cells", cells);
681
+ block.set("prop:comments", undefined);
682
+ block.set("prop:textAlign", undefined);
683
+ return { blockId, block, flavour: "affine:table" };
684
+ }
685
+ case "bookmark": {
686
+ setSysFields(block, blockId, "affine:bookmark");
687
+ block.set("sys:parent", parentId);
688
+ block.set("sys:children", new Y.Array());
689
+ block.set("prop:style", normalized.bookmarkStyle);
690
+ block.set("prop:url", normalized.url);
691
+ block.set("prop:caption", normalized.caption ?? null);
692
+ block.set("prop:description", null);
693
+ block.set("prop:icon", null);
694
+ block.set("prop:image", null);
695
+ block.set("prop:title", null);
696
+ block.set("prop:xywh", "[0,0,0,0]");
697
+ block.set("prop:index", "a0");
698
+ block.set("prop:lockedBySelf", false);
699
+ block.set("prop:rotate", 0);
700
+ block.set("prop:footnoteIdentifier", null);
701
+ return { blockId, block, flavour: "affine:bookmark" };
702
+ }
703
+ case "image": {
704
+ setSysFields(block, blockId, "affine:image");
705
+ block.set("sys:parent", parentId);
706
+ block.set("sys:children", new Y.Array());
707
+ block.set("prop:caption", normalized.caption ?? "");
708
+ block.set("prop:sourceId", normalized.sourceId);
709
+ block.set("prop:width", 0);
710
+ block.set("prop:height", 0);
711
+ block.set("prop:size", normalized.size || -1);
712
+ block.set("prop:xywh", "[0,0,0,0]");
713
+ block.set("prop:index", "a0");
714
+ block.set("prop:lockedBySelf", false);
715
+ block.set("prop:rotate", 0);
716
+ return { blockId, block, flavour: "affine:image" };
717
+ }
718
+ case "attachment": {
719
+ setSysFields(block, blockId, "affine:attachment");
720
+ block.set("sys:parent", parentId);
721
+ block.set("sys:children", new Y.Array());
722
+ block.set("prop:name", normalized.name);
723
+ block.set("prop:size", normalized.size);
724
+ block.set("prop:type", normalized.mimeType);
725
+ block.set("prop:sourceId", normalized.sourceId);
726
+ block.set("prop:caption", normalized.caption ?? undefined);
727
+ block.set("prop:embed", normalized.embed);
728
+ block.set("prop:style", "horizontalThin");
729
+ block.set("prop:index", "a0");
730
+ block.set("prop:xywh", "[0,0,0,0]");
731
+ block.set("prop:lockedBySelf", false);
732
+ block.set("prop:rotate", 0);
733
+ block.set("prop:footnoteIdentifier", null);
734
+ return { blockId, block, flavour: "affine:attachment" };
735
+ }
736
+ case "embed_youtube": {
737
+ setSysFields(block, blockId, "affine:embed-youtube");
738
+ block.set("sys:parent", parentId);
739
+ block.set("sys:children", new Y.Array());
740
+ block.set("prop:index", "a0");
741
+ block.set("prop:xywh", "[0,0,0,0]");
742
+ block.set("prop:lockedBySelf", false);
743
+ block.set("prop:rotate", 0);
744
+ block.set("prop:style", "video");
745
+ block.set("prop:url", normalized.url);
746
+ block.set("prop:caption", normalized.caption ?? null);
747
+ block.set("prop:image", null);
748
+ block.set("prop:title", null);
749
+ block.set("prop:description", null);
750
+ block.set("prop:creator", null);
751
+ block.set("prop:creatorUrl", null);
752
+ block.set("prop:creatorImage", null);
753
+ block.set("prop:videoId", null);
754
+ return { blockId, block, flavour: "affine:embed-youtube" };
755
+ }
756
+ case "embed_github": {
757
+ setSysFields(block, blockId, "affine:embed-github");
758
+ block.set("sys:parent", parentId);
759
+ block.set("sys:children", new Y.Array());
760
+ block.set("prop:index", "a0");
761
+ block.set("prop:xywh", "[0,0,0,0]");
762
+ block.set("prop:lockedBySelf", false);
763
+ block.set("prop:rotate", 0);
764
+ block.set("prop:style", "horizontal");
765
+ block.set("prop:owner", "");
766
+ block.set("prop:repo", "");
767
+ block.set("prop:githubType", "issue");
768
+ block.set("prop:githubId", "");
769
+ block.set("prop:url", normalized.url);
770
+ block.set("prop:caption", normalized.caption ?? null);
771
+ block.set("prop:image", null);
772
+ block.set("prop:status", null);
773
+ block.set("prop:statusReason", null);
774
+ block.set("prop:title", null);
775
+ block.set("prop:description", null);
776
+ block.set("prop:createdAt", null);
777
+ block.set("prop:assignees", null);
778
+ return { blockId, block, flavour: "affine:embed-github" };
779
+ }
780
+ case "embed_figma": {
781
+ setSysFields(block, blockId, "affine:embed-figma");
782
+ block.set("sys:parent", parentId);
783
+ block.set("sys:children", new Y.Array());
784
+ block.set("prop:index", "a0");
785
+ block.set("prop:xywh", "[0,0,0,0]");
786
+ block.set("prop:lockedBySelf", false);
787
+ block.set("prop:rotate", 0);
788
+ block.set("prop:style", "figma");
789
+ block.set("prop:url", normalized.url);
790
+ block.set("prop:caption", normalized.caption ?? null);
791
+ block.set("prop:title", null);
792
+ block.set("prop:description", null);
793
+ return { blockId, block, flavour: "affine:embed-figma" };
794
+ }
795
+ case "embed_loom": {
796
+ setSysFields(block, blockId, "affine:embed-loom");
797
+ block.set("sys:parent", parentId);
798
+ block.set("sys:children", new Y.Array());
799
+ block.set("prop:index", "a0");
800
+ block.set("prop:xywh", "[0,0,0,0]");
801
+ block.set("prop:lockedBySelf", false);
802
+ block.set("prop:rotate", 0);
803
+ block.set("prop:style", "video");
804
+ block.set("prop:url", normalized.url);
805
+ block.set("prop:caption", normalized.caption ?? null);
806
+ block.set("prop:image", null);
807
+ block.set("prop:title", null);
808
+ block.set("prop:description", null);
809
+ block.set("prop:videoId", null);
810
+ return { blockId, block, flavour: "affine:embed-loom" };
811
+ }
812
+ case "embed_html": {
813
+ setSysFields(block, blockId, "affine:embed-html");
814
+ block.set("sys:parent", parentId);
815
+ block.set("sys:children", new Y.Array());
816
+ block.set("prop:index", "a0");
817
+ block.set("prop:xywh", "[0,0,0,0]");
818
+ block.set("prop:lockedBySelf", false);
819
+ block.set("prop:rotate", 0);
820
+ block.set("prop:style", "html");
821
+ block.set("prop:caption", normalized.caption ?? null);
822
+ block.set("prop:html", normalized.html || undefined);
823
+ block.set("prop:design", normalized.design || undefined);
824
+ return { blockId, block, flavour: "affine:embed-html" };
825
+ }
826
+ case "embed_linked_doc": {
827
+ setSysFields(block, blockId, "affine:embed-linked-doc");
828
+ block.set("sys:parent", parentId);
829
+ block.set("sys:children", new Y.Array());
830
+ block.set("prop:index", "a0");
831
+ block.set("prop:xywh", "[0,0,0,0]");
832
+ block.set("prop:lockedBySelf", false);
833
+ block.set("prop:rotate", 0);
834
+ block.set("prop:style", "horizontal");
835
+ block.set("prop:caption", normalized.caption ?? null);
836
+ block.set("prop:pageId", normalized.pageId);
837
+ block.set("prop:title", undefined);
838
+ block.set("prop:description", undefined);
839
+ block.set("prop:footnoteIdentifier", null);
840
+ return { blockId, block, flavour: "affine:embed-linked-doc" };
841
+ }
842
+ case "embed_synced_doc": {
843
+ setSysFields(block, blockId, "affine:embed-synced-doc");
844
+ block.set("sys:parent", parentId);
845
+ block.set("sys:children", new Y.Array());
846
+ block.set("prop:index", "a0");
847
+ block.set("prop:xywh", "[0,0,800,100]");
848
+ block.set("prop:lockedBySelf", false);
849
+ block.set("prop:rotate", 0);
850
+ block.set("prop:style", "syncedDoc");
851
+ block.set("prop:caption", normalized.caption ?? undefined);
852
+ block.set("prop:pageId", normalized.pageId);
853
+ block.set("prop:scale", undefined);
854
+ block.set("prop:preFoldHeight", undefined);
855
+ block.set("prop:title", undefined);
856
+ block.set("prop:description", undefined);
857
+ return { blockId, block, flavour: "affine:embed-synced-doc" };
858
+ }
859
+ case "embed_iframe": {
860
+ setSysFields(block, blockId, "affine:embed-iframe");
861
+ block.set("sys:parent", parentId);
862
+ block.set("sys:children", new Y.Array());
863
+ block.set("prop:index", "a0");
864
+ block.set("prop:xywh", "[0,0,0,0]");
865
+ block.set("prop:lockedBySelf", false);
866
+ block.set("prop:scale", 1);
867
+ block.set("prop:url", normalized.url);
868
+ block.set("prop:iframeUrl", normalized.iframeUrl || normalized.url);
869
+ block.set("prop:width", undefined);
870
+ block.set("prop:height", undefined);
871
+ block.set("prop:caption", normalized.caption ?? null);
872
+ block.set("prop:title", null);
873
+ block.set("prop:description", null);
874
+ return { blockId, block, flavour: "affine:embed-iframe" };
875
+ }
876
+ case "database": {
877
+ setSysFields(block, blockId, "affine:database");
878
+ block.set("sys:parent", parentId);
879
+ block.set("sys:children", new Y.Array());
880
+ block.set("prop:views", new Y.Array());
881
+ block.set("prop:title", makeText(content));
882
+ block.set("prop:cells", new Y.Map());
883
+ block.set("prop:columns", new Y.Array());
884
+ block.set("prop:comments", undefined);
885
+ return { blockId, block, flavour: "affine:database" };
886
+ }
887
+ case "data_view": {
888
+ // AFFiNE 0.26.x currently crashes on raw affine:data-view render path.
889
+ // Keep API compatibility for type="data_view" by mapping it to the stable database block.
890
+ setSysFields(block, blockId, "affine:database");
891
+ block.set("sys:parent", parentId);
892
+ block.set("sys:children", new Y.Array());
893
+ block.set("prop:views", new Y.Array());
894
+ block.set("prop:title", makeText(content));
895
+ block.set("prop:cells", new Y.Map());
896
+ block.set("prop:columns", new Y.Array());
897
+ block.set("prop:comments", undefined);
898
+ return { blockId, block, flavour: "affine:database", blockType: "data_view_fallback" };
899
+ }
900
+ case "surface_ref": {
901
+ setSysFields(block, blockId, "affine:surface-ref");
902
+ block.set("sys:parent", parentId);
903
+ block.set("sys:children", new Y.Array());
904
+ block.set("prop:reference", normalized.reference);
905
+ block.set("prop:caption", normalized.caption ?? "");
906
+ block.set("prop:refFlavour", normalized.refFlavour);
907
+ block.set("prop:comments", undefined);
908
+ return { blockId, block, flavour: "affine:surface-ref" };
909
+ }
910
+ case "frame": {
911
+ setSysFields(block, blockId, "affine:frame");
912
+ block.set("sys:parent", parentId);
913
+ block.set("sys:children", new Y.Array());
914
+ block.set("prop:title", makeText(content || "Frame"));
915
+ block.set("prop:background", normalized.background);
916
+ block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
917
+ block.set("prop:index", "a0");
918
+ block.set("prop:childElementIds", new Y.Map());
919
+ block.set("prop:presentationIndex", "a0");
920
+ block.set("prop:lockedBySelf", false);
921
+ return { blockId, block, flavour: "affine:frame" };
922
+ }
923
+ case "edgeless_text": {
924
+ setSysFields(block, blockId, "affine:edgeless-text");
925
+ block.set("sys:parent", parentId);
926
+ block.set("sys:children", new Y.Array());
927
+ block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
928
+ block.set("prop:index", "a0");
929
+ block.set("prop:lockedBySelf", false);
930
+ block.set("prop:scale", 1);
931
+ block.set("prop:rotate", 0);
932
+ block.set("prop:hasMaxWidth", false);
933
+ block.set("prop:comments", undefined);
934
+ block.set("prop:color", "black");
935
+ block.set("prop:fontFamily", "Inter");
936
+ block.set("prop:fontStyle", "normal");
937
+ block.set("prop:fontWeight", "regular");
938
+ block.set("prop:textAlign", "left");
939
+ return { blockId, block, flavour: "affine:edgeless-text" };
940
+ }
941
+ case "note": {
942
+ setSysFields(block, blockId, "affine:note");
943
+ block.set("sys:parent", parentId);
944
+ block.set("sys:children", new Y.Array());
945
+ block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
946
+ block.set("prop:background", normalized.background);
947
+ block.set("prop:index", "a0");
948
+ block.set("prop:lockedBySelf", false);
949
+ block.set("prop:hidden", false);
950
+ block.set("prop:displayMode", "both");
951
+ const edgeless = new Y.Map();
952
+ const style = new Y.Map();
953
+ style.set("borderRadius", 8);
954
+ style.set("borderSize", 1);
955
+ style.set("borderStyle", "solid");
956
+ style.set("shadowType", "none");
957
+ edgeless.set("style", style);
958
+ block.set("prop:edgeless", edgeless);
959
+ block.set("prop:comments", undefined);
960
+ return { blockId, block, flavour: "affine:note" };
961
+ }
185
962
  }
186
963
  }
187
964
  async function appendBlockInternal(parsed) {
188
- const workspaceId = parsed.workspaceId || defaults.workspaceId;
965
+ const normalized = normalizeAppendBlockInput(parsed);
966
+ const workspaceId = normalized.workspaceId || defaults.workspaceId;
189
967
  if (!workspaceId)
190
968
  throw new Error("workspaceId is required");
191
969
  const { endpoint, cookie } = await getCookieAndEndpoint();
@@ -194,25 +972,24 @@ export function registerDocTools(server, gql, defaults) {
194
972
  try {
195
973
  await joinWorkspace(socket, workspaceId);
196
974
  const doc = new Y.Doc();
197
- const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
975
+ const snapshot = await loadDoc(socket, workspaceId, normalized.docId);
198
976
  if (snapshot.missing) {
199
977
  Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
200
978
  }
201
979
  const prevSV = Y.encodeStateVector(doc);
202
980
  const blocks = doc.getMap("blocks");
203
- const noteId = ensureNoteBlock(blocks);
204
- const { blockId, block, flavour, blockType } = createBlock(noteId, parsed);
981
+ const context = resolveInsertContext(blocks, normalized);
982
+ const { blockId, block, flavour, blockType } = createBlock(context.parentId, normalized);
205
983
  blocks.set(blockId, block);
206
- const note = blocks.get(noteId);
207
- let noteChildren = note.get("sys:children");
208
- if (!(noteChildren instanceof Y.Array)) {
209
- noteChildren = new Y.Array();
210
- note.set("sys:children", noteChildren);
984
+ if (context.insertIndex >= context.children.length) {
985
+ context.children.push([blockId]);
986
+ }
987
+ else {
988
+ context.children.insert(context.insertIndex, [blockId]);
211
989
  }
212
- noteChildren.push([blockId]);
213
990
  const delta = Y.encodeStateAsUpdate(doc, prevSV);
214
- await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
215
- return { appended: true, blockId, flavour, blockType };
991
+ await pushDocUpdate(socket, workspaceId, normalized.docId, Buffer.from(delta).toString("base64"));
992
+ return { appended: true, blockId, flavour, blockType, normalizedType: normalized.type, legacyType: normalized.legacyType || null };
216
993
  }
217
994
  finally {
218
995
  socket.disconnect();
@@ -520,19 +1297,52 @@ export function registerDocTools(server, gql, defaults) {
520
1297
  blockId: result.blockId,
521
1298
  flavour: result.flavour,
522
1299
  type: result.blockType || null,
1300
+ normalizedType: result.normalizedType,
1301
+ legacyType: result.legacyType,
523
1302
  });
524
1303
  };
525
1304
  server.registerTool("append_block", {
526
1305
  title: "Append Block",
527
- description: "Append a slash-command style block (heading/list/todo/code/divider/quote) to a document.",
1306
+ description: "Append document blocks with canonical types and legacy aliases (supports placement + strict validation).",
528
1307
  inputSchema: {
529
1308
  workspaceId: WorkspaceId.optional(),
530
1309
  docId: DocId,
531
- type: AppendBlockType.describe("Block type to append"),
1310
+ type: z.string().min(1).describe("Block type. Canonical: paragraph|heading|quote|list|code|divider|callout|latex|table|bookmark|image|attachment|embed_youtube|embed_github|embed_figma|embed_loom|embed_html|embed_linked_doc|embed_synced_doc|embed_iframe|database|data_view|surface_ref|frame|edgeless_text|note. Legacy aliases remain supported."),
532
1311
  text: z.string().optional().describe("Block content text"),
1312
+ url: z.string().optional().describe("URL for bookmark/embeds"),
1313
+ pageId: z.string().optional().describe("Target page/doc id for linked/synced doc embeds"),
1314
+ iframeUrl: z.string().optional().describe("Override iframe src for embed_iframe"),
1315
+ html: z.string().optional().describe("Raw html for embed_html"),
1316
+ design: z.string().optional().describe("Design payload for embed_html"),
1317
+ reference: z.string().optional().describe("Target id for surface_ref"),
1318
+ refFlavour: z.string().optional().describe("Target flavour for surface_ref (e.g. affine:frame)"),
1319
+ width: z.number().int().min(1).max(10000).optional().describe("Width for frame/edgeless_text/note"),
1320
+ height: z.number().int().min(1).max(10000).optional().describe("Height for frame/edgeless_text/note"),
1321
+ background: z.string().optional().describe("Background for frame/note"),
1322
+ sourceId: z.string().optional().describe("Blob source id for image/attachment"),
1323
+ name: z.string().optional().describe("Attachment file name"),
1324
+ mimeType: z.string().optional().describe("Attachment mime type"),
1325
+ size: z.number().optional().describe("Attachment/image file size in bytes"),
1326
+ embed: z.boolean().optional().describe("Attachment embed mode"),
1327
+ rows: z.number().int().min(1).max(20).optional().describe("Table row count"),
1328
+ columns: z.number().int().min(1).max(20).optional().describe("Table column count"),
1329
+ latex: z.string().optional().describe("Latex expression"),
1330
+ level: z.number().int().min(1).max(6).optional().describe("Heading level for type=heading"),
1331
+ style: AppendBlockListStyle.optional().describe("List style for type=list"),
1332
+ bookmarkStyle: AppendBlockBookmarkStyle.optional().describe("Bookmark card style"),
533
1333
  checked: z.boolean().optional().describe("Todo state when type is todo"),
534
1334
  language: z.string().optional().describe("Code language when type is code"),
535
1335
  caption: z.string().optional().describe("Code caption when type is code"),
1336
+ strict: z.boolean().optional().describe("Strict validation mode (default true)"),
1337
+ placement: z
1338
+ .object({
1339
+ parentId: z.string().optional(),
1340
+ afterBlockId: z.string().optional(),
1341
+ beforeBlockId: z.string().optional(),
1342
+ index: z.number().int().min(0).optional(),
1343
+ })
1344
+ .optional()
1345
+ .describe("Optional insertion target/position"),
536
1346
  },
537
1347
  }, appendBlockHandler);
538
1348
  // DELETE DOC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "affine-mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Model Context Protocol server for AFFiNE - enables AI assistants to interact with AFFiNE workspaces, documents, and collaboration features.",