affine-mcp-server 1.2.2 → 1.4.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.
@@ -4,6 +4,29 @@ 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 = [
8
+ "paragraph",
9
+ "heading1",
10
+ "heading2",
11
+ "heading3",
12
+ "quote",
13
+ "bulleted_list",
14
+ "numbered_list",
15
+ "todo",
16
+ "code",
17
+ "divider",
18
+ ];
19
+ const AppendBlockType = z.enum(APPEND_BLOCK_TYPE_VALUES);
20
+ function blockVersion(flavour) {
21
+ switch (flavour) {
22
+ case "affine:page":
23
+ return 2;
24
+ case "affine:surface":
25
+ return 5;
26
+ default:
27
+ return 1;
28
+ }
29
+ }
7
30
  export function registerDocTools(server, gql, defaults) {
8
31
  // helpers
9
32
  function generateId() {
@@ -19,6 +42,182 @@ export function registerDocTools(server, gql, defaults) {
19
42
  const cookie = gql.cookie || headers.Cookie || '';
20
43
  return { endpoint, cookie };
21
44
  }
45
+ function makeText(content) {
46
+ const yText = new Y.Text();
47
+ if (content.length > 0) {
48
+ yText.insert(0, content);
49
+ }
50
+ return yText;
51
+ }
52
+ function asText(value) {
53
+ if (value instanceof Y.Text)
54
+ return value.toString();
55
+ if (typeof value === "string")
56
+ return value;
57
+ return "";
58
+ }
59
+ function childIdsFrom(value) {
60
+ if (!(value instanceof Y.Array))
61
+ return [];
62
+ const childIds = [];
63
+ value.forEach((entry) => {
64
+ if (typeof entry === "string") {
65
+ childIds.push(entry);
66
+ return;
67
+ }
68
+ if (Array.isArray(entry)) {
69
+ for (const child of entry) {
70
+ if (typeof child === "string") {
71
+ childIds.push(child);
72
+ }
73
+ }
74
+ }
75
+ });
76
+ return childIds;
77
+ }
78
+ function setSysFields(block, blockId, flavour) {
79
+ block.set("sys:id", blockId);
80
+ block.set("sys:flavour", flavour);
81
+ block.set("sys:version", blockVersion(flavour));
82
+ }
83
+ function findBlockIdByFlavour(blocks, flavour) {
84
+ for (const [, value] of blocks) {
85
+ const block = value;
86
+ if (block?.get && block.get("sys:flavour") === flavour) {
87
+ return String(block.get("sys:id"));
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ function ensureNoteBlock(blocks) {
93
+ const existingNoteId = findBlockIdByFlavour(blocks, "affine:note");
94
+ if (existingNoteId) {
95
+ return existingNoteId;
96
+ }
97
+ const pageId = findBlockIdByFlavour(blocks, "affine:page");
98
+ if (!pageId) {
99
+ throw new Error("Document has no page block; unable to insert content.");
100
+ }
101
+ const noteId = generateId();
102
+ const note = new Y.Map();
103
+ setSysFields(note, noteId, "affine:note");
104
+ note.set("sys:parent", pageId);
105
+ note.set("sys:children", new Y.Array());
106
+ note.set("prop:xywh", "[0,0,800,95]");
107
+ note.set("prop:index", "a0");
108
+ note.set("prop:hidden", false);
109
+ note.set("prop:displayMode", "both");
110
+ const background = new Y.Map();
111
+ background.set("light", "#ffffff");
112
+ background.set("dark", "#252525");
113
+ note.set("prop:background", background);
114
+ blocks.set(noteId, note);
115
+ const page = blocks.get(pageId);
116
+ let pageChildren = page.get("sys:children");
117
+ if (!(pageChildren instanceof Y.Array)) {
118
+ pageChildren = new Y.Array();
119
+ page.set("sys:children", pageChildren);
120
+ }
121
+ pageChildren.push([noteId]);
122
+ return noteId;
123
+ }
124
+ function createBlock(noteId, parsed) {
125
+ const blockId = generateId();
126
+ const block = new Y.Map();
127
+ const content = parsed.text ?? "";
128
+ switch (parsed.type) {
129
+ case "paragraph":
130
+ case "heading1":
131
+ case "heading2":
132
+ case "heading3":
133
+ case "quote": {
134
+ setSysFields(block, blockId, "affine:paragraph");
135
+ block.set("sys:parent", noteId);
136
+ 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";
146
+ block.set("prop:type", blockType);
147
+ block.set("prop:text", makeText(content));
148
+ return { blockId, block, flavour: "affine:paragraph", blockType };
149
+ }
150
+ case "bulleted_list":
151
+ case "numbered_list":
152
+ case "todo": {
153
+ setSysFields(block, blockId, "affine:list");
154
+ block.set("sys:parent", noteId);
155
+ 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
+ }
165
+ block.set("prop:text", makeText(content));
166
+ return { blockId, block, flavour: "affine:list", blockType };
167
+ }
168
+ case "code": {
169
+ setSysFields(block, blockId, "affine:code");
170
+ block.set("sys:parent", noteId);
171
+ 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);
175
+ }
176
+ block.set("prop:text", makeText(content));
177
+ return { blockId, block, flavour: "affine:code" };
178
+ }
179
+ case "divider": {
180
+ setSysFields(block, blockId, "affine:divider");
181
+ block.set("sys:parent", noteId);
182
+ block.set("sys:children", new Y.Array());
183
+ return { blockId, block, flavour: "affine:divider" };
184
+ }
185
+ }
186
+ }
187
+ async function appendBlockInternal(parsed) {
188
+ const workspaceId = parsed.workspaceId || defaults.workspaceId;
189
+ if (!workspaceId)
190
+ throw new Error("workspaceId is required");
191
+ const { endpoint, cookie } = await getCookieAndEndpoint();
192
+ const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
193
+ const socket = await connectWorkspaceSocket(wsUrl, cookie);
194
+ try {
195
+ await joinWorkspace(socket, workspaceId);
196
+ const doc = new Y.Doc();
197
+ const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
198
+ if (snapshot.missing) {
199
+ Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
200
+ }
201
+ const prevSV = Y.encodeStateVector(doc);
202
+ const blocks = doc.getMap("blocks");
203
+ const noteId = ensureNoteBlock(blocks);
204
+ const { blockId, block, flavour, blockType } = createBlock(noteId, parsed);
205
+ 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);
211
+ }
212
+ noteChildren.push([blockId]);
213
+ 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 };
216
+ }
217
+ finally {
218
+ socket.disconnect();
219
+ }
220
+ }
22
221
  const listDocsHandler = async (parsed) => {
23
222
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
24
223
  if (!workspaceId) {
@@ -38,16 +237,6 @@ export function registerDocTools(server, gql, defaults) {
38
237
  after: z.string().optional()
39
238
  }
40
239
  }, listDocsHandler);
41
- server.registerTool("affine_list_docs", {
42
- title: "List Documents",
43
- description: "List documents in a workspace (GraphQL).",
44
- inputSchema: {
45
- workspaceId: z.string().describe("Workspace ID (optional if default set).").optional(),
46
- first: z.number().optional(),
47
- offset: z.number().optional(),
48
- after: z.string().optional()
49
- }
50
- }, listDocsHandler);
51
240
  const getDocHandler = async (parsed) => {
52
241
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
53
242
  if (!workspaceId) {
@@ -65,78 +254,103 @@ export function registerDocTools(server, gql, defaults) {
65
254
  docId: DocId
66
255
  }
67
256
  }, getDocHandler);
68
- server.registerTool("affine_get_doc", {
69
- title: "Get Document",
70
- description: "Get a document by ID (GraphQL metadata).",
71
- inputSchema: {
72
- workspaceId: z.string().optional(),
73
- docId: DocId
257
+ const readDocHandler = async (parsed) => {
258
+ const workspaceId = parsed.workspaceId || defaults.workspaceId;
259
+ if (!workspaceId) {
260
+ throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
74
261
  }
75
- }, getDocHandler);
76
- const searchDocsHandler = async (parsed) => {
262
+ const { endpoint, cookie } = await getCookieAndEndpoint();
263
+ const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
264
+ const socket = await connectWorkspaceSocket(wsUrl, cookie);
77
265
  try {
78
- const workspaceId = parsed.workspaceId || defaults.workspaceId;
79
- if (!workspaceId) {
80
- throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
266
+ await joinWorkspace(socket, workspaceId);
267
+ const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
268
+ if (!snapshot.missing) {
269
+ return text({
270
+ docId: parsed.docId,
271
+ title: null,
272
+ exists: false,
273
+ blockCount: 0,
274
+ blocks: [],
275
+ plainText: "",
276
+ });
81
277
  }
82
- const query = `query SearchDocs($workspaceId:String!, $keyword:String!, $limit:Int){ workspace(id:$workspaceId){ searchDocs(input:{ keyword:$keyword, limit:$limit }){ docId title highlight createdAt updatedAt } } }`;
83
- const data = await gql.request(query, { workspaceId, keyword: parsed.keyword, limit: parsed.limit });
84
- return text(data.workspace?.searchDocs || []);
85
- }
86
- catch (error) {
87
- // Return empty array on error (search might not be available)
88
- console.error("Search docs error:", error.message);
89
- return text([]);
90
- }
91
- };
92
- server.registerTool("search_docs", {
93
- title: "Search Documents",
94
- description: "Search documents in a workspace.",
95
- inputSchema: {
96
- workspaceId: z.string().optional(),
97
- keyword: z.string().min(1),
98
- limit: z.number().optional()
99
- }
100
- }, searchDocsHandler);
101
- server.registerTool("affine_search_docs", {
102
- title: "Search Documents",
103
- description: "Search documents in a workspace.",
104
- inputSchema: {
105
- workspaceId: z.string().optional(),
106
- keyword: z.string().min(1),
107
- limit: z.number().optional()
278
+ const doc = new Y.Doc();
279
+ Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
280
+ const blocks = doc.getMap("blocks");
281
+ const pageId = findBlockIdByFlavour(blocks, "affine:page");
282
+ const noteId = findBlockIdByFlavour(blocks, "affine:note");
283
+ const visited = new Set();
284
+ const blockRows = [];
285
+ const plainTextLines = [];
286
+ let title = "";
287
+ const visit = (blockId) => {
288
+ if (visited.has(blockId))
289
+ return;
290
+ visited.add(blockId);
291
+ const raw = blocks.get(blockId);
292
+ if (!(raw instanceof Y.Map))
293
+ return;
294
+ const flavour = raw.get("sys:flavour");
295
+ const parentId = raw.get("sys:parent");
296
+ const type = raw.get("prop:type");
297
+ const textValue = asText(raw.get("prop:text"));
298
+ const language = raw.get("prop:language");
299
+ const checked = raw.get("prop:checked");
300
+ const childIds = childIdsFrom(raw.get("sys:children"));
301
+ if (flavour === "affine:page") {
302
+ title = asText(raw.get("prop:title")) || title;
303
+ }
304
+ if (textValue.length > 0) {
305
+ plainTextLines.push(textValue);
306
+ }
307
+ blockRows.push({
308
+ id: blockId,
309
+ parentId: typeof parentId === "string" ? parentId : null,
310
+ flavour: typeof flavour === "string" ? flavour : null,
311
+ type: typeof type === "string" ? type : null,
312
+ text: textValue.length > 0 ? textValue : null,
313
+ checked: typeof checked === "boolean" ? checked : null,
314
+ language: typeof language === "string" ? language : null,
315
+ childIds,
316
+ });
317
+ for (const childId of childIds) {
318
+ visit(childId);
319
+ }
320
+ };
321
+ if (pageId) {
322
+ visit(pageId);
323
+ }
324
+ else if (noteId) {
325
+ visit(noteId);
326
+ }
327
+ for (const [id] of blocks) {
328
+ const blockId = String(id);
329
+ if (!visited.has(blockId)) {
330
+ visit(blockId);
331
+ }
332
+ }
333
+ return text({
334
+ docId: parsed.docId,
335
+ title: title || null,
336
+ exists: true,
337
+ blockCount: blockRows.length,
338
+ blocks: blockRows,
339
+ plainText: plainTextLines.join("\n"),
340
+ });
108
341
  }
109
- }, searchDocsHandler);
110
- const recentDocsHandler = async (parsed) => {
111
- const workspaceId = parsed.workspaceId || defaults.workspaceId;
112
- if (!workspaceId) {
113
- throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
342
+ finally {
343
+ socket.disconnect();
114
344
  }
115
- // Note: AFFiNE doesn't have a separate 'recentlyUpdatedDocs' field, just use docs
116
- const query = `query RecentDocs($workspaceId:String!, $first:Int, $offset:Int, $after:String){ workspace(id:$workspaceId){ docs(pagination:{first:$first, offset:$offset, after:$after}){ totalCount pageInfo{ hasNextPage endCursor } edges{ cursor node{ id workspaceId title summary public defaultRole createdAt updatedAt } } } } }`;
117
- const data = await gql.request(query, { workspaceId, first: parsed.first, offset: parsed.offset, after: parsed.after });
118
- return text(data.workspace.docs);
119
345
  };
120
- server.registerTool("recent_docs", {
121
- title: "Recent Documents",
122
- description: "List recently updated docs in a workspace.",
123
- inputSchema: {
124
- workspaceId: z.string().optional(),
125
- first: z.number().optional(),
126
- offset: z.number().optional(),
127
- after: z.string().optional()
128
- }
129
- }, recentDocsHandler);
130
- server.registerTool("affine_recent_docs", {
131
- title: "Recent Documents",
132
- description: "List recently updated docs in a workspace.",
346
+ server.registerTool("read_doc", {
347
+ title: "Read Document Content",
348
+ description: "Read document block content via WebSocket snapshot (blocks + plain text).",
133
349
  inputSchema: {
134
- workspaceId: z.string().optional(),
135
- first: z.number().optional(),
136
- offset: z.number().optional(),
137
- after: z.string().optional()
138
- }
139
- }, recentDocsHandler);
350
+ workspaceId: WorkspaceId.optional(),
351
+ docId: DocId,
352
+ },
353
+ }, readDocHandler);
140
354
  const publishDocHandler = async (parsed) => {
141
355
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
142
356
  if (!workspaceId) {
@@ -155,15 +369,6 @@ export function registerDocTools(server, gql, defaults) {
155
369
  mode: z.enum(["Page", "Edgeless"]).optional()
156
370
  }
157
371
  }, publishDocHandler);
158
- server.registerTool("affine_publish_doc", {
159
- title: "Publish Document",
160
- description: "Publish a doc (make public).",
161
- inputSchema: {
162
- workspaceId: z.string().optional(),
163
- docId: z.string(),
164
- mode: z.enum(["Page", "Edgeless"]).optional()
165
- }
166
- }, publishDocHandler);
167
372
  const revokeDocHandler = async (parsed) => {
168
373
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
169
374
  if (!workspaceId) {
@@ -181,14 +386,6 @@ export function registerDocTools(server, gql, defaults) {
181
386
  docId: z.string()
182
387
  }
183
388
  }, revokeDocHandler);
184
- server.registerTool("affine_revoke_doc", {
185
- title: "Revoke Document",
186
- description: "Revoke a doc's public access.",
187
- inputSchema: {
188
- workspaceId: z.string().optional(),
189
- docId: z.string()
190
- }
191
- }, revokeDocHandler);
192
389
  // CREATE DOC (high-level)
193
390
  const createDocHandler = async (parsed) => {
194
391
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
@@ -205,8 +402,7 @@ export function registerDocTools(server, gql, defaults) {
205
402
  const blocks = ydoc.getMap('blocks');
206
403
  const pageId = generateId();
207
404
  const page = new Y.Map();
208
- page.set('sys:id', pageId);
209
- page.set('sys:flavour', 'affine:page');
405
+ setSysFields(page, pageId, "affine:page");
210
406
  const titleText = new Y.Text();
211
407
  titleText.insert(0, parsed.title || 'Untitled');
212
408
  page.set('prop:title', titleText);
@@ -215,21 +411,27 @@ export function registerDocTools(server, gql, defaults) {
215
411
  blocks.set(pageId, page);
216
412
  const surfaceId = generateId();
217
413
  const surface = new Y.Map();
218
- surface.set('sys:id', surfaceId);
219
- surface.set('sys:flavour', 'affine:surface');
414
+ setSysFields(surface, surfaceId, "affine:surface");
220
415
  surface.set('sys:parent', pageId);
221
416
  surface.set('sys:children', new Y.Array());
417
+ const elements = new Y.Map();
418
+ elements.set("type", "$blocksuite:internal:native$");
419
+ elements.set("value", new Y.Map());
420
+ surface.set("prop:elements", elements);
222
421
  blocks.set(surfaceId, surface);
223
422
  children.push([surfaceId]);
224
423
  const noteId = generateId();
225
424
  const note = new Y.Map();
226
- note.set('sys:id', noteId);
227
- note.set('sys:flavour', 'affine:note');
425
+ setSysFields(note, noteId, "affine:note");
228
426
  note.set('sys:parent', pageId);
229
- note.set('prop:displayMode', 'DocAndEdgeless');
230
- note.set('prop:xywh', '[0,0,800,600]');
427
+ note.set('prop:displayMode', 'both');
428
+ note.set('prop:xywh', '[0,0,800,95]');
231
429
  note.set('prop:index', 'a0');
232
- note.set('prop:lockedBySelf', false);
430
+ note.set('prop:hidden', false);
431
+ const background = new Y.Map();
432
+ background.set("light", "#ffffff");
433
+ background.set("dark", "#252525");
434
+ note.set("prop:background", background);
233
435
  const noteChildren = new Y.Array();
234
436
  note.set('sys:children', noteChildren);
235
437
  blocks.set(noteId, note);
@@ -237,8 +439,7 @@ export function registerDocTools(server, gql, defaults) {
237
439
  if (parsed.content) {
238
440
  const paraId = generateId();
239
441
  const para = new Y.Map();
240
- para.set('sys:id', paraId);
241
- para.set('sys:flavour', 'affine:paragraph');
442
+ setSysFields(para, paraId, "affine:paragraph");
242
443
  para.set('sys:parent', noteId);
243
444
  para.set('sys:children', new Y.Array());
244
445
  para.set('prop:type', 'text');
@@ -293,90 +494,15 @@ export function registerDocTools(server, gql, defaults) {
293
494
  content: z.string().optional(),
294
495
  },
295
496
  }, createDocHandler);
296
- server.registerTool('affine_create_doc', {
297
- title: 'Create Document',
298
- description: 'Create a new AFFiNE document with optional content',
299
- inputSchema: {
300
- workspaceId: z.string().optional(),
301
- title: z.string().optional(),
302
- content: z.string().optional(),
303
- },
304
- }, createDocHandler);
305
497
  // APPEND PARAGRAPH
306
498
  const appendParagraphHandler = async (parsed) => {
307
- const workspaceId = parsed.workspaceId || defaults.workspaceId;
308
- if (!workspaceId)
309
- throw new Error('workspaceId is required');
310
- const { endpoint, cookie } = await getCookieAndEndpoint();
311
- const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
312
- const socket = await connectWorkspaceSocket(wsUrl, cookie);
313
- try {
314
- await joinWorkspace(socket, workspaceId);
315
- const doc = new Y.Doc();
316
- const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
317
- if (snapshot.missing) {
318
- Y.applyUpdate(doc, Buffer.from(snapshot.missing, 'base64'));
319
- }
320
- const prevSV = Y.encodeStateVector(doc);
321
- const blocks = doc.getMap('blocks');
322
- // find a note block
323
- let noteId = null;
324
- for (const [key, val] of blocks) {
325
- const m = val;
326
- if (m?.get && m.get('sys:flavour') === 'affine:note') {
327
- noteId = m.get('sys:id');
328
- break;
329
- }
330
- }
331
- if (!noteId) {
332
- // fallback: create a note under existing page
333
- let pageId = null;
334
- for (const [key, val] of blocks) {
335
- const m = val;
336
- if (m?.get && m.get('sys:flavour') === 'affine:page') {
337
- pageId = m.get('sys:id');
338
- break;
339
- }
340
- }
341
- if (!pageId)
342
- throw new Error('Doc has no page block');
343
- const note = new Y.Map();
344
- noteId = generateId();
345
- note.set('sys:id', noteId);
346
- note.set('sys:flavour', 'affine:note');
347
- note.set('sys:parent', pageId);
348
- note.set('prop:displayMode', 'DocAndEdgeless');
349
- note.set('prop:xywh', '[0,0,800,600]');
350
- note.set('prop:index', 'a0');
351
- note.set('prop:lockedBySelf', false);
352
- note.set('sys:children', new Y.Array());
353
- blocks.set(noteId, note);
354
- const page = blocks.get(pageId);
355
- const children = page.get('sys:children');
356
- children.push([noteId]);
357
- }
358
- const paragraphId = generateId();
359
- const para = new Y.Map();
360
- para.set('sys:id', paragraphId);
361
- para.set('sys:flavour', 'affine:paragraph');
362
- para.set('sys:parent', noteId);
363
- para.set('sys:children', new Y.Array());
364
- para.set('prop:type', 'text');
365
- const ptext = new Y.Text();
366
- ptext.insert(0, parsed.text);
367
- para.set('prop:text', ptext);
368
- blocks.set(paragraphId, para);
369
- const note = blocks.get(noteId);
370
- const noteChildren = note.get('sys:children');
371
- noteChildren.push([paragraphId]);
372
- const delta = Y.encodeStateAsUpdate(doc, prevSV);
373
- const deltaB64 = Buffer.from(delta).toString('base64');
374
- await pushDocUpdate(socket, workspaceId, parsed.docId, deltaB64);
375
- return text({ appended: true, paragraphId });
376
- }
377
- finally {
378
- socket.disconnect();
379
- }
499
+ const result = await appendBlockInternal({
500
+ workspaceId: parsed.workspaceId,
501
+ docId: parsed.docId,
502
+ type: "paragraph",
503
+ text: parsed.text,
504
+ });
505
+ return text({ appended: result.appended, paragraphId: result.blockId });
380
506
  };
381
507
  server.registerTool('append_paragraph', {
382
508
  title: 'Append Paragraph',
@@ -387,15 +513,28 @@ export function registerDocTools(server, gql, defaults) {
387
513
  text: z.string(),
388
514
  },
389
515
  }, appendParagraphHandler);
390
- server.registerTool('affine_append_paragraph', {
391
- title: 'Append Paragraph',
392
- description: 'Append a text paragraph block to a document',
516
+ const appendBlockHandler = async (parsed) => {
517
+ const result = await appendBlockInternal(parsed);
518
+ return text({
519
+ appended: result.appended,
520
+ blockId: result.blockId,
521
+ flavour: result.flavour,
522
+ type: result.blockType || null,
523
+ });
524
+ };
525
+ server.registerTool("append_block", {
526
+ title: "Append Block",
527
+ description: "Append a slash-command style block (heading/list/todo/code/divider/quote) to a document.",
393
528
  inputSchema: {
394
- workspaceId: z.string().optional(),
395
- docId: z.string(),
396
- text: z.string(),
529
+ workspaceId: WorkspaceId.optional(),
530
+ docId: DocId,
531
+ type: AppendBlockType.describe("Block type to append"),
532
+ text: z.string().optional().describe("Block content text"),
533
+ checked: z.boolean().optional().describe("Todo state when type is todo"),
534
+ language: z.string().optional().describe("Code language when type is code"),
535
+ caption: z.string().optional().describe("Code caption when type is code"),
397
536
  },
398
- }, appendParagraphHandler);
537
+ }, appendBlockHandler);
399
538
  // DELETE DOC
400
539
  const deleteDocHandler = async (parsed) => {
401
540
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
@@ -441,9 +580,4 @@ export function registerDocTools(server, gql, defaults) {
441
580
  description: 'Delete a document and remove from workspace list',
442
581
  inputSchema: { workspaceId: z.string().optional(), docId: z.string() },
443
582
  }, deleteDocHandler);
444
- server.registerTool('affine_delete_doc', {
445
- title: 'Delete Document',
446
- description: 'Delete a document and remove from workspace list',
447
- inputSchema: { workspaceId: z.string().optional(), docId: z.string() },
448
- }, deleteDocHandler);
449
583
  }
@@ -9,16 +9,6 @@ export function registerHistoryTools(server, gql, defaults) {
9
9
  const data = await gql.request(query, { workspaceId, guid: parsed.guid, take: parsed.take, before: parsed.before });
10
10
  return text(data.workspace.histories);
11
11
  };
12
- server.registerTool("affine_list_histories", {
13
- title: "List Histories",
14
- description: "List doc histories (timestamps) for a doc.",
15
- inputSchema: {
16
- workspaceId: z.string().optional(),
17
- guid: z.string(),
18
- take: z.number().optional(),
19
- before: z.string().optional()
20
- }
21
- }, listHistoriesHandler);
22
12
  server.registerTool("list_histories", {
23
13
  title: "List Histories",
24
14
  description: "List doc histories (timestamps) for a doc.",
@@ -29,30 +19,4 @@ export function registerHistoryTools(server, gql, defaults) {
29
19
  before: z.string().optional()
30
20
  }
31
21
  }, listHistoriesHandler);
32
- const recoverDocHandler = async (parsed) => {
33
- const workspaceId = parsed.workspaceId || defaults.workspaceId || parsed.workspaceId;
34
- if (!workspaceId)
35
- throw new Error("workspaceId required (or set AFFINE_WORKSPACE_ID)");
36
- const mutation = `mutation Recover($workspaceId:String!,$guid:String!,$timestamp:DateTime!){ recoverDoc(workspaceId:$workspaceId, guid:$guid, timestamp:$timestamp) }`;
37
- const data = await gql.request(mutation, { workspaceId, guid: parsed.guid, timestamp: parsed.timestamp });
38
- return text({ recoveredAt: data.recoverDoc });
39
- };
40
- server.registerTool("affine_recover_doc", {
41
- title: "Recover Document",
42
- description: "Recover a doc to a previous timestamp.",
43
- inputSchema: {
44
- workspaceId: z.string().optional(),
45
- guid: z.string(),
46
- timestamp: z.string()
47
- }
48
- }, recoverDocHandler);
49
- server.registerTool("recover_doc", {
50
- title: "Recover Document",
51
- description: "Recover a doc to a previous timestamp.",
52
- inputSchema: {
53
- workspaceId: z.string().optional(),
54
- guid: z.string(),
55
- timestamp: z.string()
56
- }
57
- }, recoverDocHandler);
58
22
  }