affine-mcp-server 1.13.0 → 2.0.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.
@@ -40,6 +40,7 @@ Auth priority within the active configuration:
40
40
 
41
41
  | Variable | Purpose |
42
42
  | --- | --- |
43
+ | `AFFINE_TOOL_PROFILE` | Select a predefined tool surface profile (`full`, `read_only`, `core`, `authoring`) |
43
44
  | `AFFINE_DISABLED_GROUPS` | Disable entire tool groups by comma-separated group name |
44
45
  | `AFFINE_DISABLED_TOOLS` | Disable individual tools by exact canonical name |
45
46
 
@@ -152,6 +153,25 @@ OAuth mode behavior:
152
153
 
153
154
  ## Least-privilege tool exposure
154
155
 
156
+ ### Use a tool profile
157
+
158
+ Profiles are the easiest way to reduce the MCP tool surface without listing every tool by name.
159
+
160
+ Example:
161
+
162
+ ```json
163
+ {
164
+ "AFFINE_TOOL_PROFILE": "core"
165
+ }
166
+ ```
167
+
168
+ Available profiles:
169
+
170
+ - `full`: expose the complete public tool surface; this is the default
171
+ - `read_only`: expose discovery, reading, export, fidelity, and inspection tools, plus `sign_in`
172
+ - `core`: expose the compact everyday surface for workspace/doc discovery, basic document authoring, tags, and database row/schema edits; omits admin tools, cleanup tools, experimental organize tools, and destructive tools
173
+ - `authoring`: expose non-destructive creation and editing tools, including semantic pages, native templates, database composition, and edgeless canvas authoring; omits admin, cleanup, destructive, and experimental organize tools
174
+
155
175
  ### Disable whole groups
156
176
 
157
177
  Example:
@@ -165,14 +185,51 @@ Example:
165
185
  Current group names:
166
186
 
167
187
  - `workspaces`
188
+ - `workspaces.read`
189
+ - `workspaces.write`
168
190
  - `docs`
191
+ - `docs.read`
192
+ - `docs.write`
193
+ - `docs.markdown`
194
+ - `docs.tags`
195
+ - `docs.tree`
196
+ - `docs.export`
197
+ - `docs.semantic`
198
+ - `docs.template`
199
+ - `docs.database`
200
+ - `docs.edgeless`
201
+ - `docs.surface`
202
+ - `docs.intent`
203
+ - `docs.share`
169
204
  - `comments`
205
+ - `comments.read`
206
+ - `comments.write`
170
207
  - `history`
208
+ - `history.read`
171
209
  - `organize`
210
+ - `organize.read`
211
+ - `organize.write`
212
+ - `organize.collections`
213
+ - `organize.folders`
172
214
  - `users`
215
+ - `users.read`
216
+ - `users.write`
217
+ - `users.auth`
173
218
  - `access_tokens`
219
+ - `access_tokens.read`
220
+ - `access_tokens.write`
174
221
  - `blobs`
222
+ - `blobs.write`
175
223
  - `notifications`
224
+ - `notifications.read`
225
+ - `notifications.write`
226
+ - `admin`
227
+ - `auth`
228
+ - `cleanup`
229
+ - `destructive`
230
+ - `experimental`
231
+ - `read`
232
+ - `write`
176
233
 
177
234
  ### Disable specific tools
178
235
 
@@ -184,7 +241,7 @@ Example:
184
241
  }
185
242
  ```
186
243
 
187
- Use tool-level filtering when you want a mostly complete tool surface but need to remove destructive operations.
244
+ Use tool-level filtering when you want a mostly complete tool surface but need to remove specific operations such as destructive actions or administrative access-token tools.
188
245
 
189
246
  ## Deployment checklist
190
247
 
@@ -0,0 +1,226 @@
1
+ # Edgeless Canvas Cookbook
2
+
3
+ A worked, live-authored walkthrough of the edgeless canvas tools. Every call in this doc was executed against a running AFFiNE instance while authoring it; the IDs, coordinates, and responses below are real output from that session, not illustrative fiction.
4
+
5
+ ## What you'll build
6
+
7
+ An auth-flow diagram: four rectangles (User, Auth Service, Database, Cache) stitched with labeled connectors, wrapped in a **Frame that owns the diagram** — drag the frame in the editor and everything inside moves with it. Followed by an epilogue note that lands in the right place by itself, no coordinate math.
8
+
9
+ ```
10
+ ┌─ Frame "Auth Flow" ────────────────────────────────────────┐
11
+ │ │
12
+ │ [ User ] ──authenticate─→ [ Auth Service ] │
13
+ │ │ │
14
+ │ ──verify──→ │
15
+ │ │ │
16
+ │ [ Database ] │
17
+ │ │
18
+ │ [ Cache ] ←─session lookup─ │
19
+ └────────────────────────────────────────────────────────────┘
20
+
21
+ [ Epilogue note — auto-placed below the frame with padding gap ]
22
+ ```
23
+
24
+ ## The full call sequence
25
+
26
+ Every step below is copy-pasteable. Replace `W` with your workspace id.
27
+
28
+ ### 1. Fresh doc
29
+
30
+ ```js
31
+ const { docId: D } = await call("create_doc", {
32
+ workspaceId: W,
33
+ title: "Edgeless Canvas Cookbook — Live Demo",
34
+ content: "This doc was seeded live by the edgeless-canvas cookbook.",
35
+ });
36
+ ```
37
+
38
+ AFFiNE seeds a default note at `[0,0,800,~268]` — we'll leave it; step 6 demonstrates how the auto-placement default dodges it.
39
+
40
+ ### 2. Three surface shapes
41
+
42
+ ```js
43
+ const user = await call("add_surface_element", {
44
+ workspaceId: W, docId: D, type: "shape", shapeType: "rect", radius: 0.2,
45
+ x: 200, y: 400, width: 160, height: 80, text: "User", fontSize: 18,
46
+ fillColor: "--affine-palette-shape-blue",
47
+ });
48
+ const auth = await call("add_surface_element", {
49
+ workspaceId: W, docId: D, type: "shape", shapeType: "rect", radius: 0.2,
50
+ x: 500, y: 400, width: 160, height: 80, text: "Auth Service", fontSize: 18,
51
+ fillColor: "--affine-palette-shape-green",
52
+ });
53
+ const db = await call("add_surface_element", {
54
+ workspaceId: W, docId: D, type: "shape", shapeType: "rect", radius: 0.2,
55
+ x: 800, y: 400, width: 160, height: 80, text: "Database", fontSize: 18,
56
+ fillColor: "--affine-palette-shape-purple",
57
+ });
58
+ // → returns { added: true, elementId: "cczYKQ593K", type: "shape", surfaceBlockId: "wpv4iPX3Qj" }, ...
59
+ ```
60
+
61
+ ### 3. Labeled connectors between them
62
+
63
+ ```js
64
+ const c1 = await call("add_surface_element", {
65
+ workspaceId: W, docId: D, type: "connector",
66
+ sourceId: user.elementId, targetId: auth.elementId, label: "authenticate",
67
+ });
68
+ const c2 = await call("add_surface_element", {
69
+ workspaceId: W, docId: D, type: "connector",
70
+ sourceId: auth.elementId, targetId: db.elementId, label: "verify",
71
+ });
72
+ ```
73
+
74
+ With both endpoints bound by id and no explicit `sourcePosition` / `targetPosition`, BlockSuite's side-midpoint auto-snap kicks in — each endpoint lands on one of `[0.5,0]`, `[0.5,1]`, `[0,0.5]`, `[1,0.5]`. `labelXYWH` is seeded at the source→target midpoint so the label renders on first open.
75
+
76
+ ### 4. Wrap the diagram in a frame that **owns** it
77
+
78
+ ```js
79
+ const frame = await call("append_block", {
80
+ workspaceId: W, docId: D, type: "frame",
81
+ text: "Auth Flow",
82
+ childElementIds: [user.elementId, auth.elementId, db.elementId, c1.elementId, c2.elementId],
83
+ padding: 50,
84
+ });
85
+ // → {
86
+ // appended: true, blockId: "wx0OB2I2cp", flavour: "affine:frame",
87
+ // ownedIds: ["cczYKQ593K","dSfmVkc3Io","goh9bQO5sg","jDvyiSy5Su","O5Gtcr17O2"],
88
+ // missing: []
89
+ // }
90
+ ```
91
+
92
+ With `width`/`height` omitted, the frame auto-sizes to the union of its children's bounds plus `padding` on each side and a 30px title band at the top. Every resolved id lands in `ownedIds` — dragging the frame in the editor now drags the whole diagram. BlockSuite's `prop:childElementIds` accepts both surface elements (shapes/connectors/groups) and edgeless blocks (notes/frames/edgeless-text), so you can wrap either without triage.
93
+
94
+ ### 5. Add a new member and let the frame regrow
95
+
96
+ ```js
97
+ const cache = await call("add_surface_element", {
98
+ workspaceId: W, docId: D, type: "shape", shapeType: "rect", radius: 0.2,
99
+ x: 500, y: 600, width: 160, height: 80, text: "Cache", fontSize: 18,
100
+ fillColor: "--affine-palette-shape-orange",
101
+ });
102
+ const c3 = await call("add_surface_element", {
103
+ workspaceId: W, docId: D, type: "connector", mode: 1,
104
+ sourceId: auth.elementId, targetId: cache.elementId, label: "session lookup",
105
+ });
106
+
107
+ await call("update_frame_children", {
108
+ workspaceId: W, docId: D, blockId: frame.blockId,
109
+ childElementIds: [user.elementId, auth.elementId, db.elementId, c1.elementId, c2.elementId, cache.elementId, c3.elementId],
110
+ padding: 50,
111
+ });
112
+ // → {
113
+ // updated: true, blockId: "wx0OB2I2cp", flavour: "affine:frame",
114
+ // ownedIds: [..., "9aYW_HNajo", "wzoKIrLkO-"],
115
+ // missing: [],
116
+ // resized: true, xywh: { x: 150, y: 290, width: 860, height: 440 }
117
+ // }
118
+ ```
119
+
120
+ `update_frame_children` replaces ownership **wholesale** (same semantics as `update_surface_element` for a group's `children`) and by default recomputes `xywh` so the box fits its new contents. Pass `resizeToFit: false` to keep the box untouched:
121
+
122
+ ```js
123
+ await call("update_frame_children", {
124
+ workspaceId: W, docId: D, blockId: frame.blockId,
125
+ childElementIds: [user.elementId, auth.elementId, db.elementId, c1.elementId, c2.elementId],
126
+ resizeToFit: false,
127
+ });
128
+ // → { updated: true, ownedIds: [...], resized: false }
129
+ ```
130
+
131
+ Use the opt-out when you want to shrink ownership without the frame jumping around the canvas.
132
+
133
+ ### 6. Append a note with no coordinates — it lands in the right place
134
+
135
+ ```js
136
+ await call("append_block", {
137
+ workspaceId: W, docId: D, type: "note",
138
+ width: 800, height: 120,
139
+ markdown: [
140
+ "## How this canvas was built",
141
+ "",
142
+ "Every block, shape, and frame above was authored with a single MCP tool call.",
143
+ "The frame owns its shapes via `prop:childElementIds` — drag it and the diagram moves with it.",
144
+ ].join("\n"),
145
+ });
146
+ // → note xywh ends up at [150, 770, 800, 166.5]
147
+ ```
148
+
149
+ No `x`/`y`, no `stackAfter` — yet the note lands at `y=770`, which is the frame's bottom edge (`290 + 440 = 730`) plus the default `padding` gap of 40. When you append a `frame`/`note`/`edgeless_text` to a doc and don't provide an explicit position or `stackAfter`, the server auto-stacks it below whichever edgeless block sits lowest. The common "new note overlaps AFFiNE's seeded default note at `[0,0,…]`" papercut is gone.
150
+
151
+ Pass `x: 0, y: 0` explicitly if you *want* the old behavior back.
152
+
153
+ ## The id triage: owned vs missing
154
+
155
+ `childElementIds` (on both `append_block` and `update_frame_children`) accepts any mix of surface-element and block ids. Everything that resolves gets written to the frame's `prop:childElementIds` Y.Map — the same shape BlockSuite's editor writes when you drag members into a frame, so dragging the frame drags every owned member regardless of flavour.
156
+
157
+ | Lands in | When |
158
+ | --- | --- |
159
+ | `ownedIds` | id resolves to an existing surface element OR edgeless block. Written to `prop:childElementIds`. Frame drags them along. |
160
+ | `missing` | id doesn't resolve to either. Skipped; returned so callers can tell stale ids from intentional ones. |
161
+
162
+ If **every** id is missing on `append_block`, the call throws (`None of the ids in childElementIds were found: [...]`) — that's almost always a caller bug. `update_frame_children` tolerates all-missing and treats it as "clear ownership" (paired with a skipped resize).
163
+
164
+ ## Read the whole canvas back
165
+
166
+ ```js
167
+ const canvas = await call("get_edgeless_canvas", { workspaceId: W, docId: D });
168
+ // canvas.edgelessBlocks: [
169
+ // { flavour: "affine:note", xywh: "[0,0,800,268]", bounds: {...}, children: [...] },
170
+ // { flavour: "affine:frame", xywh: "[150,290,860,440]", title: "Auth Flow",
171
+ // childElementIds: ["cczYKQ593K","dSfmVkc3Io","goh9bQO5sg","jDvyiSy5Su","O5Gtcr17O2","9aYW_HNajo","wzoKIrLkO-"] },
172
+ // { flavour: "affine:note", xywh: "[150,770,800,166.5]", children: [
173
+ // { flavour: "affine:paragraph", text: "How this canvas was built", type: "h2" },
174
+ // { flavour: "affine:paragraph", text: "Every block, shape, and frame above...", type: "text" },
175
+ // ] },
176
+ // ],
177
+ // canvas.surfaceElements: [shape(User), shape(Auth), shape(Database),
178
+ // connector(authenticate), connector(verify),
179
+ // shape(Cache), connector(session lookup)],
180
+ // canvas.bounds: { minX: 0, minY: 0, maxX: 1010, maxY: 936.5, width: 1010, height: 936.5 }
181
+ ```
182
+
183
+ Frame entries now carry `childElementIds: string[]` so agents can see ownership without crawling the surface layer. Note entries emit a structured `children: [{ flavour, type, text, language?, checked? }]` array — markdown round-trips with heading/list/code semantics intact, no re-parsing needed.
184
+
185
+ ## Running it
186
+
187
+ From the repo root with Docker available:
188
+
189
+ ```bash
190
+ . tests/generate-test-env.sh
191
+ docker compose -f docker/docker-compose.yml up -d
192
+ node tests/acquire-credentials.mjs
193
+ npm run build
194
+ ```
195
+
196
+ Then drop the calls above into a Node script that opens a `StdioClientTransport` against `dist/index.js` — `tests/test-canvas-tool-map-demo.mjs` is a complete example of the client wiring, minus the auth-flow content. The script prints the seeded doc URL; open it in a browser, switch to edgeless mode (icon next to the doc title), and the frame + its five owned elements select and drag as one.
197
+
198
+ ## Advanced: the tool-map showcase
199
+
200
+ `tests/test-canvas-tool-map-demo.mjs` seeds a much larger canvas — three color-coded columns mapping the full tool catalog, with each column's notes owned by a frame via `childElementIds` so dragging the frame moves the entire column together. It doubles as a layout-helper regression test wired into `tests/run-e2e.sh`. It's the right place to look for end-to-end coverage of `stackAfter`, `childElementIds` ownership across flavours, connector side-midpoint auto-snap, and `labelXYWH` seeding all in one run.
201
+
202
+ <picture>
203
+ <source media="(prefers-color-scheme: dark)" srcset="./assets/edgeless-canvas-demo-advanced-dark.png">
204
+ <img alt="AFFiNE MCP Tool Map — three color-coded columns wrapped in frames, connectors fanning out from a top banner into column chains and fanning in to a bottom agent-view banner" src="./assets/edgeless-canvas-demo-advanced-light.png">
205
+ </picture>
206
+
207
+ ## Tool surface at a glance
208
+
209
+ | Tool | Purpose |
210
+ | --- | --- |
211
+ | `add_surface_element` | Shapes / connectors / canvas text / groups on `affine:surface`. Connectors auto-snap endpoints to side-midpoints when both are bound by id. |
212
+ | `append_block(type="frame", childElementIds)` | Create a frame that owns surface elements and auto-sizes to contain them. |
213
+ | `update_frame_children` | Replace a frame's contents wholesale. Default resizes to fit; `resizeToFit: false` preserves the current box. |
214
+ | `append_block(type="note" / "frame" / "edgeless_text")` | Edgeless blocks. Bare calls auto-stack below existing blocks; pass `x`/`y` or `stackAfter` to override. |
215
+ | `get_edgeless_canvas` | Read the full canvas: edgeless blocks + surface elements with parsed bounds, aggregate bounding box, and per-type counts. Frame entries now include `childElementIds`. |
216
+
217
+ ## BlockSuite alignment notes
218
+
219
+ Everything above writes to the native BlockSuite schema — no custom overlay:
220
+
221
+ - Surface elements land in `affine:surface` → `prop:elements.value` as `Y.Map` entries with fractional-index strings for stable z-order.
222
+ - Frame ownership uses `prop:childElementIds` as a `Y.Map<boolean>` keyed by element id — identical shape to a group's `children` map.
223
+ - Connectors with both endpoints bound by id and no explicit position auto-snap to the four tangent-carrying side-midpoints (`[0.5,0]`, `[0.5,1]`, `[0,0.5]`, `[1,0.5]`).
224
+ - `labelXYWH` is seeded at the source→target midpoint so BlockSuite's label renderer doesn't short-circuit on first render.
225
+ - `append_block(type="edgeless_text", text=…)` auto-creates a child `affine:paragraph` — the edgeless-text view walks `sys:children` for glyphs, so without it the block renders as an invisible sliver.
226
+ - `src/edgeless/layout.ts` is a dependency-free module citing the upstream BlockSuite files each helper mirrors (`connector.ts`, `connector-manager.ts`, `edgeless-note-mask.ts`), so future parity audits stay cheap.
@@ -9,7 +9,7 @@ Use this document as a grouped catalog. For exact schemas, your MCP client shoul
9
9
  - Canonical names only: legacy alias names are not part of the public tool surface
10
10
  - Document editing relies on AFFiNE WebSocket-backed operations where noted
11
11
  - Experimental organize tools are marked explicitly
12
- - Use tool filtering in production if you want a reduced or read-only surface
12
+ - Use `AFFINE_TOOL_PROFILE=read_only`, `core`, or `authoring` in production if you want a reduced surface
13
13
 
14
14
  ## Workspace
15
15
 
@@ -54,14 +54,11 @@ Use this document as a grouped catalog. For exact schemas, your MCP client shoul
54
54
  | `list_tags` | List all tags in a workspace | |
55
55
  | `search_docs` | Search titles with substring, prefix, or exact matching | Supports tag filter and updatedAt sorting |
56
56
  | `list_docs_by_tag` | List documents with a specific tag | |
57
- | `get_docs_by_tag` | Search documents by case-insensitive tag substring | Returns `availableTags` when nothing matches |
58
57
  | `get_doc` | Read document metadata | |
59
- | `get_doc_by_title` | Find a document by title and return Markdown content | Useful for title-based lookup |
60
58
  | `read_doc` | Read block content and plain text snapshot | WebSocket-backed |
61
59
  | `get_capabilities` | Inspect the server's high-level authoring and fidelity capabilities | Useful for adaptive clients |
62
60
  | `analyze_doc_fidelity` | Analyze how a document maps to Markdown and which native AFFiNE structures are lossy | Good before export or migration |
63
61
  | `list_children` | List direct child docs linked from a document | |
64
- | `list_backlinks` | List parent or reference docs that link to a document | |
65
62
 
66
63
  ### Publish and visibility
67
64
 
@@ -76,12 +73,9 @@ Use this document as a grouped catalog. For exact schemas, your MCP client shoul
76
73
  | --- | --- | --- |
77
74
  | `create_doc` | Create a new document | WebSocket-backed |
78
75
  | `create_doc_from_markdown` | Create a document from Markdown content | |
79
- | `create_doc_from_template` | Clone a template doc and substitute `{{variables}}` | Can optionally link the new doc under a parent |
80
76
  | `inspect_template_structure` | Inspect a template's native AFFiNE structure and native-clone support | Helps choose a clone strategy |
81
77
  | `instantiate_template_native` | Instantiate a template via native AFFiNE block cloning, with optional Markdown fallback | Higher-fidelity than Markdown-only cloning |
82
- | `duplicate_doc` | Clone a document into a new doc | Can optionally place the copy under a parent |
83
78
  | `move_doc` | Move a document in the sidebar by relinking it under another parent | |
84
- | `batch_create_docs` | Create up to 20 documents in one call | |
85
79
  | `delete_doc` | Delete a document | WebSocket-backed and destructive |
86
80
 
87
81
  ### Content editing
@@ -89,14 +83,11 @@ Use this document as a grouped catalog. For exact schemas, your MCP client shoul
89
83
  | Tool | Purpose | Notes |
90
84
  | --- | --- | --- |
91
85
  | `update_doc_title` | Rename a document in workspace metadata and in the page block | |
92
- | `append_paragraph` | Append a paragraph block | WebSocket-backed |
93
- | `append_block` | Append canonical block types with validation and placement control | Supports text, media, embeds, database, and edgeless blocks |
86
+ | `append_block` | Append canonical block types with validation and placement control | Supports text, media, embeds, database, and edgeless blocks. `frame`/`edgeless_text`/`note` accept `x`/`y`/`width`/`height`. `note` with `text` auto-creates a child paragraph so it renders on the edgeless canvas. |
94
87
  | `create_semantic_page` | Create an AFFiNE-native page with an intentional section skeleton and native block composition | High-level authoring helper |
95
88
  | `append_semantic_section` | Append a semantic section to an existing page by heading title | High-level authoring helper |
96
89
  | `append_markdown` | Append Markdown content to an existing document | |
97
90
  | `replace_doc_with_markdown` | Replace the main note content with Markdown | Overwrites main note content |
98
- | `find_and_replace` | Preview or apply text replacement across a document | |
99
- | `cleanup_orphan_embeds` | Remove linked-doc embeds that point to missing docs | Cleanup-oriented |
100
91
 
101
92
  ### Tags
102
93
 
@@ -123,15 +114,39 @@ Use this document as a grouped catalog. For exact schemas, your MCP client shoul
123
114
  | `delete_database_row` | Delete a row by row block id | Destructive |
124
115
  | `read_database_columns` | Read schema metadata, types, options, and view mappings | Useful before edits |
125
116
  | `read_database_cells` | Read row titles and decoded cell values | Supports row and column filters |
126
- | `update_database_cell` | Update a single cell or built-in title | `createOption` defaults to `true` for select-like fields |
127
117
  | `update_database_row` | Update multiple cells on a row at once | `createOption` defaults to `true` |
128
118
 
119
+ ## Edgeless canvas and surface elements
120
+
121
+ AFFiNE's edgeless doc has two layers: top-level edgeless blocks (`note`, `frame`, `edgeless-text`) with `prop:xywh`, and the surface layer (`affine:surface`) which stores free-floating shapes, connectors, canvas text, and groups in `prop:elements.value` — the native BlockSuite representation.
122
+
123
+ | Tool | Purpose | Notes |
124
+ | --- | --- | --- |
125
+ | `get_edgeless_canvas` | Read the full canvas: edgeless blocks + surface elements with parsed `{x,y,width,height}`, aggregate `bounds`, per-type `elementCounts` | Deterministic z-order (fractional-index sorted). Note entries carry a structured `children` array of their block descendants (`flavour`, `type`, `text`, `language`, `checked`) so markdown-seeded content round-trips faithfully. |
126
+ | `add_surface_element` | Add a `shape`, `connector`, `text`, or `group` to the surface | Shapes: rect/ellipse/diamond/triangle with fill, stroke, and text. Connectors accept `sourceId`/`targetId` and optional `sourcePosition`/`targetPosition` relative `[x,y]` in `[0,1]`. When both endpoints are bound by id and neither position is supplied, they auto-snap to BlockSuite's four tangent-carrying side-midpoints based on relative bounds. Creates the surface block if the doc doesn't have one. |
127
+ | `list_surface_elements` | List all surface elements (optionally filter by `type` or `elementId`) | Returns raw `xywh` plus parsed `bounds` sorted by fractional `index` ascending; serializes `Y.Text` fields to plain strings. |
128
+ | `update_surface_element` | Partially update an element by id | `x`/`y`/`width`/`height` merge with current `xywh` (move without resizing, or vice versa). `text`/`label`/`title` replace their `Y.Text` wholesale. Fields not applicable to the element's type come back in the response `ignored` list. |
129
+ | `delete_surface_element` | Delete an element by id | `pruneConnectors: true` additionally removes any connectors referencing the deleted element. |
130
+ | `update_frame_children` | Replace a frame block's contents wholesale | Every resolved id (surface element or edgeless block) goes into `prop:childElementIds` and comes back in `ownedIds`; unknown ids in `missing`. Default `resizeToFit: true` recomputes xywh to match new contents + `padding` + title band; pass `resizeToFit: false` to preserve the current box. Pass `[]` to clear ownership (resize skipped). |
131
+ | `update_edgeless_block` | Partially update a note/frame/edgeless-text block | `x`/`y`/`width`/`height` merge with current `prop:xywh`; `background` replaces `prop:background`. Fields not applicable to the flavour come back under `ignored`. Use for repositioning / resizing / recoloring without re-creating the block. |
132
+ | `delete_block` | Delete a block by id | Removes descendants and unlinks from the parent's `sys:children` by default. `deleteChildren: false` keeps descendants orphaned; `pruneConnectors: true` also drops surface connectors referencing any deleted id. Refuses `affine:page`. |
133
+
134
+ ### Layout helpers on `append_block`
135
+
136
+ When the new block is a frame/note/edgeless_text on the canvas, `append_block` accepts three optional fields that compute coordinates from the current doc state instead of the caller doing arithmetic:
137
+
138
+ | Field | Applies to | Purpose |
139
+ | --- | --- | --- |
140
+ | `markdown` | `type="note"` | Parse markdown into heading/paragraph/list/code child blocks inside the note. Height auto-estimated from the content when `height` is omitted. |
141
+ | `childElementIds: [id, ...]` | `type="frame"` | The frame's contents. Accepts ids of surface elements (shapes/connectors/groups) AND edgeless blocks (notes/frames/edgeless-text) — every resolved id goes into `prop:childElementIds`, matching what BlockSuite's editor writes when you drag members into a frame. Dragging the frame drags every owned member. Unresolved ids come back under `missing`. If `width`/`height` are omitted, the frame is sized to the union of resolvable bounds + `padding` + a 30px title band. |
142
+ | `stackAfter: { blockId, direction?, gap? }` | any canvas block | Position relative to one or more existing siblings. `blockId` may be an array — picks whichever ref is furthest in the stack direction (useful when stacking below a row of columns) and centers the new block on the union bounds' orthogonal axis (when widths match, same as inheriting the anchor's x). Caller-provided `x` / `y` on the orthogonal axis still wins. Default `gap` is direction-aware: **80px horizontal** (left/right), **40px vertical** (up/down) — mirrors native-flowchart spacing where the flow axis gets more breathing room. |
143
+ | `padding` | used by `childElementIds` auto-sizing and as fallback `gap` for `stackAfter` | Default 40. Explicit `padding` on the block overrides the direction-aware default; explicit `stackAfter.gap` wins over both. |
144
+
129
145
  ## Comments
130
146
 
131
147
  | Tool | Purpose | Notes |
132
148
  | --- | --- | --- |
133
149
  | `list_comments` | List comments on a document | |
134
- | `list_unresolved_threads` | List unresolved comment threads on a document | Useful for review and triage flows |
135
150
  | `create_comment` | Create a comment on a document | |
136
151
  | `update_comment` | Update comment content | |
137
152
  | `delete_comment` | Delete a comment | Destructive |
@@ -30,7 +30,7 @@ Use when:
30
30
 
31
31
  Typical tool sequence:
32
32
 
33
- 1. `search_docs` or `get_doc_by_title` to find the parent
33
+ 1. `search_docs` to find the parent
34
34
  2. `create_doc` or `create_doc_from_markdown`
35
35
  3. `move_doc` if you created the doc before deciding its final parent
36
36
  4. `list_children` to verify placement
@@ -49,7 +49,7 @@ Use when:
49
49
  Typical tool sequence:
50
50
 
51
51
  1. `list_tags`
52
- 2. `get_docs_by_tag` or `list_docs_by_tag`
52
+ 2. `list_docs_by_tag` or `search_docs` with `tag`
53
53
  3. `update_doc_title`
54
54
  4. `add_tag_to_doc` or `remove_tag_from_doc`
55
55
 
@@ -67,7 +67,7 @@ Use when:
67
67
  Typical tool sequence:
68
68
 
69
69
  1. `read_doc`
70
- 2. `append_paragraph`, `append_block`, or `append_markdown`
70
+ 2. `append_block` or `append_markdown`
71
71
  3. `replace_doc_with_markdown` only when you intend to overwrite the main note
72
72
 
73
73
  Prompt example:
@@ -87,7 +87,7 @@ Typical tool sequence:
87
87
  2. `read_database_columns` to inspect schema
88
88
  3. `add_database_column` if needed
89
89
  4. `add_database_row`
90
- 5. `update_database_cell` or `update_database_row`
90
+ 5. `update_database_row`
91
91
  6. `read_database_cells` to verify
92
92
 
93
93
  Prompt example:
@@ -133,15 +133,15 @@ Prompt example:
133
133
  Use when:
134
134
 
135
135
  - you need a markdown backup
136
- - you want to clone a page
137
- - you need to remove stale linked-doc embeds
136
+ - you want to recreate a page from Markdown
137
+ - you need to inspect linked child pages
138
138
 
139
139
  Typical tool sequence:
140
140
 
141
- 1. `export_doc_markdown` or `duplicate_doc`
142
- 2. `list_backlinks` or `list_children` if you need structural context
143
- 3. `cleanup_orphan_embeds` if linked-doc embeds reference deleted pages
141
+ 1. `export_doc_markdown`
142
+ 2. `create_doc_from_markdown` if you need a Markdown-based copy
143
+ 3. `list_children` if you need structural context
144
144
 
145
145
  Prompt example:
146
146
 
147
- > Duplicate the template page under the current parent, export the original as Markdown, and clean up any orphaned linked-doc embeds on the copy.
147
+ > Export the template page as Markdown, create a copy under the current parent, and verify the copied page's child links.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "affine-mcp-server",
3
- "version": "1.13.0",
3
+ "version": "2.0.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.",
@@ -48,6 +48,10 @@
48
48
  "test:data-view": "node tests/test-data-view.mjs",
49
49
  "test:doc-discovery": "node tests/test-doc-discovery.mjs",
50
50
  "test:create-placement": "node tests/test-create-placement.mjs",
51
+ "test:surface-elements": "node tests/test-surface-elements.mjs",
52
+ "test:surface-element-gating": "node scripts/verify-surface-element-gating.mjs",
53
+ "test:edgeless-seed": "node tests/test-edgeless-canvas-setup.mjs",
54
+ "test:edgeless-ui": "npx playwright test tests/playwright/verify-edgeless-canvas.pw.ts --config tests/playwright/playwright.config.ts",
51
55
  "test:capabilities-fidelity": "node tests/test-capabilities-fidelity.mjs",
52
56
  "test:native-template": "node tests/test-native-template-instantiation.mjs",
53
57
  "test:data-view-ui": "npx playwright test tests/playwright/verify-data-view.pw.ts --config tests/playwright/playwright.config.ts",
@@ -1,25 +1,22 @@
1
1
  {
2
- "version": "1.13.0",
2
+ "version": "2.0.0",
3
3
  "tools": [
4
4
  "add_database_column",
5
5
  "add_database_row",
6
6
  "add_doc_to_collection",
7
7
  "add_organize_link",
8
+ "add_surface_element",
8
9
  "add_tag_to_doc",
9
10
  "analyze_doc_fidelity",
10
11
  "append_block",
11
12
  "append_markdown",
12
- "append_paragraph",
13
13
  "append_semantic_section",
14
- "batch_create_docs",
15
14
  "cleanup_blobs",
16
- "cleanup_orphan_embeds",
17
15
  "compose_database_from_intent",
18
16
  "create_collection",
19
17
  "create_comment",
20
18
  "create_doc",
21
19
  "create_doc_from_markdown",
22
- "create_doc_from_template",
23
20
  "create_folder",
24
21
  "create_semantic_page",
25
22
  "create_tag",
@@ -27,29 +24,27 @@
27
24
  "create_workspace_blueprint",
28
25
  "current_user",
29
26
  "delete_blob",
27
+ "delete_block",
30
28
  "delete_collection",
31
29
  "delete_comment",
32
30
  "delete_database_row",
33
31
  "delete_doc",
34
32
  "delete_folder",
35
33
  "delete_organize_link",
34
+ "delete_surface_element",
36
35
  "delete_workspace",
37
- "duplicate_doc",
38
36
  "export_doc_markdown",
39
37
  "export_with_fidelity_report",
40
- "find_and_replace",
41
38
  "generate_access_token",
42
39
  "get_capabilities",
43
40
  "get_collection",
44
41
  "get_doc",
45
- "get_doc_by_title",
46
- "get_docs_by_tag",
42
+ "get_edgeless_canvas",
47
43
  "get_orphan_docs",
48
44
  "get_workspace",
49
45
  "inspect_template_structure",
50
46
  "instantiate_template_native",
51
47
  "list_access_tokens",
52
- "list_backlinks",
53
48
  "list_children",
54
49
  "list_collections",
55
50
  "list_comments",
@@ -58,8 +53,8 @@
58
53
  "list_histories",
59
54
  "list_notifications",
60
55
  "list_organize_nodes",
56
+ "list_surface_elements",
61
57
  "list_tags",
62
- "list_unresolved_threads",
63
58
  "list_workspace_tree",
64
59
  "list_workspaces",
65
60
  "move_doc",
@@ -81,11 +76,13 @@
81
76
  "update_collection",
82
77
  "update_collection_rules",
83
78
  "update_comment",
84
- "update_database_cell",
85
79
  "update_database_row",
86
80
  "update_doc_title",
81
+ "update_edgeless_block",
82
+ "update_frame_children",
87
83
  "update_profile",
88
84
  "update_settings",
85
+ "update_surface_element",
89
86
  "update_workspace",
90
87
  "upload_blob"
91
88
  ]