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.
- package/README.md +9 -8
- package/dist/edgeless/layout.js +222 -0
- package/dist/index.js +34 -62
- package/dist/toolSurface.js +322 -0
- package/dist/tools/comments.js +0 -57
- package/dist/tools/docs.js +1675 -606
- package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
- package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
- package/docs/configuration-and-deployment.md +58 -1
- package/docs/edgeless-canvas-cookbook.md +226 -0
- package/docs/tool-reference.md +28 -13
- package/docs/workflow-recipes.md +10 -10
- package/package.json +5 -1
- package/tool-manifest.json +9 -12
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
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.
|
package/docs/tool-reference.md
CHANGED
|
@@ -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
|
|
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
|
-
| `
|
|
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 |
|
package/docs/workflow-recipes.md
CHANGED
|
@@ -30,7 +30,7 @@ Use when:
|
|
|
30
30
|
|
|
31
31
|
Typical tool sequence:
|
|
32
32
|
|
|
33
|
-
1. `search_docs`
|
|
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. `
|
|
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. `
|
|
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. `
|
|
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
|
|
137
|
-
- you need to
|
|
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`
|
|
142
|
-
2. `
|
|
143
|
-
3. `
|
|
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
|
-
>
|
|
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": "
|
|
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",
|
package/tool-manifest.json
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
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
|
-
"
|
|
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
|
]
|