affine-mcp-server 1.7.0 → 1.7.1

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 (default) or HTTP (`/mcp`).
4
4
 
5
- [![Version](https://img.shields.io/badge/version-1.7.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
5
+ [![Version](https://img.shields.io/badge/version-1.7.1-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: 43 focused tools with WebSocket-based document editing
20
20
  - Status: Active
21
21
 
22
- > New in v1.7.0: Added remote HTTP MCP support (`/mcp`) with token/CORS controls, while retaining legacy SSE compatibility (`/sse`, `/messages`) for older clients.
22
+ > New in v1.7.1: Fixed MCP-created document structure parity with AFFiNE UI (`sys:parent` handling) and callout text rendering, plus regression coverage for UI visibility paths.
23
23
 
24
24
  ## Features
25
25
 
@@ -219,7 +219,7 @@ export function registerDocTools(server, gql, defaults) {
219
219
  const noteId = generateId();
220
220
  const note = new Y.Map();
221
221
  setSysFields(note, noteId, "affine:note");
222
- note.set("sys:parent", pageId);
222
+ note.set("sys:parent", null);
223
223
  note.set("sys:children", new Y.Array());
224
224
  note.set("prop:xywh", "[0,0,800,95]");
225
225
  note.set("prop:index", "a0");
@@ -251,7 +251,7 @@ export function registerDocTools(server, gql, defaults) {
251
251
  const surfaceId = generateId();
252
252
  const surface = new Y.Map();
253
253
  setSysFields(surface, surfaceId, "affine:surface");
254
- surface.set("sys:parent", pageId);
254
+ surface.set("sys:parent", null);
255
255
  surface.set("sys:children", new Y.Array());
256
256
  const elements = new Y.Map();
257
257
  elements.set("type", "$blocksuite:internal:native$");
@@ -601,6 +601,30 @@ export function registerDocTools(server, gql, defaults) {
601
601
  });
602
602
  return index;
603
603
  }
604
+ function findParentIdByChild(blocks, childId) {
605
+ for (const [id, value] of blocks) {
606
+ if (!(value instanceof Y.Map)) {
607
+ continue;
608
+ }
609
+ const childIds = childIdsFrom(value.get("sys:children"));
610
+ if (childIds.includes(childId)) {
611
+ return String(id);
612
+ }
613
+ }
614
+ return null;
615
+ }
616
+ function resolveBlockParentId(blocks, blockId) {
617
+ const block = findBlockById(blocks, blockId);
618
+ if (!block) {
619
+ return null;
620
+ }
621
+ const rawParentId = block.get("sys:parent");
622
+ if (typeof rawParentId === "string" && rawParentId.trim().length > 0) {
623
+ return rawParentId;
624
+ }
625
+ // AFFiNE UI commonly stores sys:parent as null and derives hierarchy from sys:children.
626
+ return findParentIdByChild(blocks, blockId);
627
+ }
604
628
  function resolveInsertContext(blocks, normalized) {
605
629
  const placement = normalized.placement;
606
630
  let parentId;
@@ -612,8 +636,8 @@ export function registerDocTools(server, gql, defaults) {
612
636
  const referenceBlock = findBlockById(blocks, referenceBlockId);
613
637
  if (!referenceBlock)
614
638
  throw new Error(`placement.afterBlockId '${referenceBlockId}' was not found.`);
615
- const refParentId = referenceBlock.get("sys:parent");
616
- if (typeof refParentId !== "string" || !refParentId) {
639
+ const refParentId = resolveBlockParentId(blocks, referenceBlockId);
640
+ if (!refParentId) {
617
641
  throw new Error(`Block '${referenceBlockId}' has no parent.`);
618
642
  }
619
643
  parentId = refParentId;
@@ -624,8 +648,8 @@ export function registerDocTools(server, gql, defaults) {
624
648
  const referenceBlock = findBlockById(blocks, referenceBlockId);
625
649
  if (!referenceBlock)
626
650
  throw new Error(`placement.beforeBlockId '${referenceBlockId}' was not found.`);
627
- const refParentId = referenceBlock.get("sys:parent");
628
- if (typeof refParentId !== "string" || !refParentId) {
651
+ const refParentId = resolveBlockParentId(blocks, referenceBlockId);
652
+ if (!refParentId) {
629
653
  throw new Error(`Block '${referenceBlockId}' has no parent.`);
630
654
  }
631
655
  parentId = refParentId;
@@ -688,16 +712,17 @@ export function registerDocTools(server, gql, defaults) {
688
712
  }
689
713
  return { parentId, parentBlock, children, insertIndex };
690
714
  }
691
- function createBlock(parentId, normalized) {
715
+ function createBlock(normalized) {
692
716
  const blockId = generateId();
693
717
  const block = new Y.Map();
694
718
  const content = normalized.text;
719
+ // Keep parity with AFFiNE UI-created docs: sys:parent stays null and hierarchy is represented by sys:children.
695
720
  switch (normalized.type) {
696
721
  case "paragraph":
697
722
  case "heading":
698
723
  case "quote": {
699
724
  setSysFields(block, blockId, "affine:paragraph");
700
- block.set("sys:parent", parentId);
725
+ block.set("sys:parent", null);
701
726
  block.set("sys:children", new Y.Array());
702
727
  const blockType = normalized.type === "heading"
703
728
  ? `h${normalized.headingLevel}`
@@ -710,7 +735,7 @@ export function registerDocTools(server, gql, defaults) {
710
735
  }
711
736
  case "list": {
712
737
  setSysFields(block, blockId, "affine:list");
713
- block.set("sys:parent", parentId);
738
+ block.set("sys:parent", null);
714
739
  block.set("sys:children", new Y.Array());
715
740
  block.set("prop:type", normalized.listStyle);
716
741
  block.set("prop:checked", normalized.listStyle === "todo" ? normalized.checked : false);
@@ -719,7 +744,7 @@ export function registerDocTools(server, gql, defaults) {
719
744
  }
720
745
  case "code": {
721
746
  setSysFields(block, blockId, "affine:code");
722
- block.set("sys:parent", parentId);
747
+ block.set("sys:parent", null);
723
748
  block.set("sys:children", new Y.Array());
724
749
  block.set("prop:language", normalized.language);
725
750
  if (normalized.caption) {
@@ -730,22 +755,35 @@ export function registerDocTools(server, gql, defaults) {
730
755
  }
731
756
  case "divider": {
732
757
  setSysFields(block, blockId, "affine:divider");
733
- block.set("sys:parent", parentId);
758
+ block.set("sys:parent", null);
734
759
  block.set("sys:children", new Y.Array());
735
760
  return { blockId, block, flavour: "affine:divider" };
736
761
  }
737
762
  case "callout": {
738
763
  setSysFields(block, blockId, "affine:callout");
739
- block.set("sys:parent", parentId);
740
- block.set("sys:children", new Y.Array());
764
+ block.set("sys:parent", null);
765
+ const calloutChildren = new Y.Array();
766
+ const textBlockId = generateId();
767
+ const textBlock = new Y.Map();
768
+ setSysFields(textBlock, textBlockId, "affine:paragraph");
769
+ textBlock.set("sys:parent", null);
770
+ textBlock.set("sys:children", new Y.Array());
771
+ textBlock.set("prop:type", "text");
772
+ textBlock.set("prop:text", makeText(content));
773
+ calloutChildren.push([textBlockId]);
774
+ block.set("sys:children", calloutChildren);
741
775
  block.set("prop:icon", { type: "emoji", unicode: "💡" });
742
776
  block.set("prop:backgroundColorName", "grey");
743
- block.set("prop:text", makeText(content));
744
- return { blockId, block, flavour: "affine:callout" };
777
+ return {
778
+ blockId,
779
+ block,
780
+ flavour: "affine:callout",
781
+ extraBlocks: [{ blockId: textBlockId, block: textBlock }],
782
+ };
745
783
  }
746
784
  case "latex": {
747
785
  setSysFields(block, blockId, "affine:latex");
748
- block.set("sys:parent", parentId);
786
+ block.set("sys:parent", null);
749
787
  block.set("sys:children", new Y.Array());
750
788
  block.set("prop:xywh", "[0,0,16,16]");
751
789
  block.set("prop:index", "a0");
@@ -757,7 +795,7 @@ export function registerDocTools(server, gql, defaults) {
757
795
  }
758
796
  case "table": {
759
797
  setSysFields(block, blockId, "affine:table");
760
- block.set("sys:parent", parentId);
798
+ block.set("sys:parent", null);
761
799
  block.set("sys:children", new Y.Array());
762
800
  const rows = {};
763
801
  const columns = {};
@@ -792,7 +830,7 @@ export function registerDocTools(server, gql, defaults) {
792
830
  }
793
831
  case "bookmark": {
794
832
  setSysFields(block, blockId, "affine:bookmark");
795
- block.set("sys:parent", parentId);
833
+ block.set("sys:parent", null);
796
834
  block.set("sys:children", new Y.Array());
797
835
  block.set("prop:style", normalized.bookmarkStyle);
798
836
  block.set("prop:url", normalized.url);
@@ -810,7 +848,7 @@ export function registerDocTools(server, gql, defaults) {
810
848
  }
811
849
  case "image": {
812
850
  setSysFields(block, blockId, "affine:image");
813
- block.set("sys:parent", parentId);
851
+ block.set("sys:parent", null);
814
852
  block.set("sys:children", new Y.Array());
815
853
  block.set("prop:caption", normalized.caption ?? "");
816
854
  block.set("prop:sourceId", normalized.sourceId);
@@ -825,7 +863,7 @@ export function registerDocTools(server, gql, defaults) {
825
863
  }
826
864
  case "attachment": {
827
865
  setSysFields(block, blockId, "affine:attachment");
828
- block.set("sys:parent", parentId);
866
+ block.set("sys:parent", null);
829
867
  block.set("sys:children", new Y.Array());
830
868
  block.set("prop:name", normalized.name);
831
869
  block.set("prop:size", normalized.size);
@@ -843,7 +881,7 @@ export function registerDocTools(server, gql, defaults) {
843
881
  }
844
882
  case "embed_youtube": {
845
883
  setSysFields(block, blockId, "affine:embed-youtube");
846
- block.set("sys:parent", parentId);
884
+ block.set("sys:parent", null);
847
885
  block.set("sys:children", new Y.Array());
848
886
  block.set("prop:index", "a0");
849
887
  block.set("prop:xywh", "[0,0,0,0]");
@@ -863,7 +901,7 @@ export function registerDocTools(server, gql, defaults) {
863
901
  }
864
902
  case "embed_github": {
865
903
  setSysFields(block, blockId, "affine:embed-github");
866
- block.set("sys:parent", parentId);
904
+ block.set("sys:parent", null);
867
905
  block.set("sys:children", new Y.Array());
868
906
  block.set("prop:index", "a0");
869
907
  block.set("prop:xywh", "[0,0,0,0]");
@@ -887,7 +925,7 @@ export function registerDocTools(server, gql, defaults) {
887
925
  }
888
926
  case "embed_figma": {
889
927
  setSysFields(block, blockId, "affine:embed-figma");
890
- block.set("sys:parent", parentId);
928
+ block.set("sys:parent", null);
891
929
  block.set("sys:children", new Y.Array());
892
930
  block.set("prop:index", "a0");
893
931
  block.set("prop:xywh", "[0,0,0,0]");
@@ -902,7 +940,7 @@ export function registerDocTools(server, gql, defaults) {
902
940
  }
903
941
  case "embed_loom": {
904
942
  setSysFields(block, blockId, "affine:embed-loom");
905
- block.set("sys:parent", parentId);
943
+ block.set("sys:parent", null);
906
944
  block.set("sys:children", new Y.Array());
907
945
  block.set("prop:index", "a0");
908
946
  block.set("prop:xywh", "[0,0,0,0]");
@@ -919,7 +957,7 @@ export function registerDocTools(server, gql, defaults) {
919
957
  }
920
958
  case "embed_html": {
921
959
  setSysFields(block, blockId, "affine:embed-html");
922
- block.set("sys:parent", parentId);
960
+ block.set("sys:parent", null);
923
961
  block.set("sys:children", new Y.Array());
924
962
  block.set("prop:index", "a0");
925
963
  block.set("prop:xywh", "[0,0,0,0]");
@@ -933,7 +971,7 @@ export function registerDocTools(server, gql, defaults) {
933
971
  }
934
972
  case "embed_linked_doc": {
935
973
  setSysFields(block, blockId, "affine:embed-linked-doc");
936
- block.set("sys:parent", parentId);
974
+ block.set("sys:parent", null);
937
975
  block.set("sys:children", new Y.Array());
938
976
  block.set("prop:index", "a0");
939
977
  block.set("prop:xywh", "[0,0,0,0]");
@@ -949,7 +987,7 @@ export function registerDocTools(server, gql, defaults) {
949
987
  }
950
988
  case "embed_synced_doc": {
951
989
  setSysFields(block, blockId, "affine:embed-synced-doc");
952
- block.set("sys:parent", parentId);
990
+ block.set("sys:parent", null);
953
991
  block.set("sys:children", new Y.Array());
954
992
  block.set("prop:index", "a0");
955
993
  block.set("prop:xywh", "[0,0,800,100]");
@@ -966,7 +1004,7 @@ export function registerDocTools(server, gql, defaults) {
966
1004
  }
967
1005
  case "embed_iframe": {
968
1006
  setSysFields(block, blockId, "affine:embed-iframe");
969
- block.set("sys:parent", parentId);
1007
+ block.set("sys:parent", null);
970
1008
  block.set("sys:children", new Y.Array());
971
1009
  block.set("prop:index", "a0");
972
1010
  block.set("prop:xywh", "[0,0,0,0]");
@@ -983,7 +1021,7 @@ export function registerDocTools(server, gql, defaults) {
983
1021
  }
984
1022
  case "database": {
985
1023
  setSysFields(block, blockId, "affine:database");
986
- block.set("sys:parent", parentId);
1024
+ block.set("sys:parent", null);
987
1025
  block.set("sys:children", new Y.Array());
988
1026
  // Create a default table view so AFFiNE UI renders the database
989
1027
  const defaultView = new Y.Map();
@@ -1008,7 +1046,7 @@ export function registerDocTools(server, gql, defaults) {
1008
1046
  // AFFiNE 0.26.x currently crashes on raw affine:data-view render path.
1009
1047
  // Keep API compatibility for type="data_view" by mapping it to the stable database block.
1010
1048
  setSysFields(block, blockId, "affine:database");
1011
- block.set("sys:parent", parentId);
1049
+ block.set("sys:parent", null);
1012
1050
  block.set("sys:children", new Y.Array());
1013
1051
  const dvDefaultView = new Y.Map();
1014
1052
  dvDefaultView.set("id", generateId());
@@ -1030,7 +1068,7 @@ export function registerDocTools(server, gql, defaults) {
1030
1068
  }
1031
1069
  case "surface_ref": {
1032
1070
  setSysFields(block, blockId, "affine:surface-ref");
1033
- block.set("sys:parent", parentId);
1071
+ block.set("sys:parent", null);
1034
1072
  block.set("sys:children", new Y.Array());
1035
1073
  block.set("prop:reference", normalized.reference);
1036
1074
  block.set("prop:caption", normalized.caption ?? "");
@@ -1040,7 +1078,7 @@ export function registerDocTools(server, gql, defaults) {
1040
1078
  }
1041
1079
  case "frame": {
1042
1080
  setSysFields(block, blockId, "affine:frame");
1043
- block.set("sys:parent", parentId);
1081
+ block.set("sys:parent", null);
1044
1082
  block.set("sys:children", new Y.Array());
1045
1083
  block.set("prop:title", makeText(content || "Frame"));
1046
1084
  block.set("prop:background", normalized.background);
@@ -1053,7 +1091,7 @@ export function registerDocTools(server, gql, defaults) {
1053
1091
  }
1054
1092
  case "edgeless_text": {
1055
1093
  setSysFields(block, blockId, "affine:edgeless-text");
1056
- block.set("sys:parent", parentId);
1094
+ block.set("sys:parent", null);
1057
1095
  block.set("sys:children", new Y.Array());
1058
1096
  block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
1059
1097
  block.set("prop:index", "a0");
@@ -1071,7 +1109,7 @@ export function registerDocTools(server, gql, defaults) {
1071
1109
  }
1072
1110
  case "note": {
1073
1111
  setSysFields(block, blockId, "affine:note");
1074
- block.set("sys:parent", parentId);
1112
+ block.set("sys:parent", null);
1075
1113
  block.set("sys:children", new Y.Array());
1076
1114
  block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
1077
1115
  block.set("prop:background", normalized.background);
@@ -1110,8 +1148,13 @@ export function registerDocTools(server, gql, defaults) {
1110
1148
  const prevSV = Y.encodeStateVector(doc);
1111
1149
  const blocks = doc.getMap("blocks");
1112
1150
  const context = resolveInsertContext(blocks, normalized);
1113
- const { blockId, block, flavour, blockType } = createBlock(context.parentId, normalized);
1151
+ const { blockId, block, flavour, blockType, extraBlocks } = createBlock(normalized);
1114
1152
  blocks.set(blockId, block);
1153
+ if (Array.isArray(extraBlocks)) {
1154
+ for (const extra of extraBlocks) {
1155
+ blocks.set(extra.blockId, extra.block);
1156
+ }
1157
+ }
1115
1158
  if (context.insertIndex >= context.children.length) {
1116
1159
  context.children.push([blockId]);
1117
1160
  }
@@ -1444,8 +1487,13 @@ export function registerDocTools(server, gql, defaults) {
1444
1487
  try {
1445
1488
  const normalized = normalizeAppendBlockInput(appendInput);
1446
1489
  const context = resolveInsertContext(blocks, normalized);
1447
- const { blockId, block } = createBlock(context.parentId, normalized);
1490
+ const { blockId, block, extraBlocks } = createBlock(normalized);
1448
1491
  blocks.set(blockId, block);
1492
+ if (Array.isArray(extraBlocks)) {
1493
+ for (const extra of extraBlocks) {
1494
+ blocks.set(extra.blockId, extra.block);
1495
+ }
1496
+ }
1449
1497
  if (context.insertIndex >= context.children.length) {
1450
1498
  context.children.push([blockId]);
1451
1499
  }
@@ -1499,7 +1547,7 @@ export function registerDocTools(server, gql, defaults) {
1499
1547
  const surfaceId = generateId();
1500
1548
  const surface = new Y.Map();
1501
1549
  setSysFields(surface, surfaceId, "affine:surface");
1502
- surface.set("sys:parent", pageId);
1550
+ surface.set("sys:parent", null);
1503
1551
  surface.set("sys:children", new Y.Array());
1504
1552
  const elements = new Y.Map();
1505
1553
  elements.set("type", "$blocksuite:internal:native$");
@@ -1510,7 +1558,7 @@ export function registerDocTools(server, gql, defaults) {
1510
1558
  const noteId = generateId();
1511
1559
  const note = new Y.Map();
1512
1560
  setSysFields(note, noteId, "affine:note");
1513
- note.set("sys:parent", pageId);
1561
+ note.set("sys:parent", null);
1514
1562
  note.set("prop:displayMode", "both");
1515
1563
  note.set("prop:xywh", "[0,0,800,95]");
1516
1564
  note.set("prop:index", "a0");
@@ -1527,7 +1575,7 @@ export function registerDocTools(server, gql, defaults) {
1527
1575
  const paraId = generateId();
1528
1576
  const para = new Y.Map();
1529
1577
  setSysFields(para, paraId, "affine:paragraph");
1530
- para.set("sys:parent", noteId);
1578
+ para.set("sys:parent", null);
1531
1579
  para.set("sys:children", new Y.Array());
1532
1580
  para.set("prop:type", "text");
1533
1581
  const paragraphText = new Y.Text();
@@ -58,7 +58,7 @@ function createInitialWorkspaceData(workspaceName = 'New Workspace', avatar = ''
58
58
  const surfaceBlock = new Y.Map();
59
59
  surfaceBlock.set('sys:id', surfaceId);
60
60
  surfaceBlock.set('sys:flavour', 'affine:surface');
61
- surfaceBlock.set('sys:parent', pageId);
61
+ surfaceBlock.set('sys:parent', null);
62
62
  surfaceBlock.set('sys:children', new Y.Array());
63
63
  blocks.set(surfaceId, surfaceBlock);
64
64
  pageChildren.push([surfaceId]);
@@ -67,7 +67,7 @@ function createInitialWorkspaceData(workspaceName = 'New Workspace', avatar = ''
67
67
  const noteBlock = new Y.Map();
68
68
  noteBlock.set('sys:id', noteId);
69
69
  noteBlock.set('sys:flavour', 'affine:note');
70
- noteBlock.set('sys:parent', pageId);
70
+ noteBlock.set('sys:parent', null);
71
71
  noteBlock.set('prop:displayMode', 'DocAndEdgeless');
72
72
  noteBlock.set('prop:xywh', '[0,0,800,600]');
73
73
  noteBlock.set('prop:index', 'a0');
@@ -81,7 +81,7 @@ function createInitialWorkspaceData(workspaceName = 'New Workspace', avatar = ''
81
81
  const paragraphBlock = new Y.Map();
82
82
  paragraphBlock.set('sys:id', paragraphId);
83
83
  paragraphBlock.set('sys:flavour', 'affine:paragraph');
84
- paragraphBlock.set('sys:parent', noteId);
84
+ paragraphBlock.set('sys:parent', null);
85
85
  paragraphBlock.set('sys:children', new Y.Array());
86
86
  paragraphBlock.set('prop:type', 'text');
87
87
  const paragraphText = new Y.Text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "affine-mcp-server",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
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.",