affine-mcp-server 1.2.2 → 1.3.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,156 @@ 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 setSysFields(block, blockId, flavour) {
53
+ block.set("sys:id", blockId);
54
+ block.set("sys:flavour", flavour);
55
+ block.set("sys:version", blockVersion(flavour));
56
+ }
57
+ function findBlockIdByFlavour(blocks, flavour) {
58
+ for (const [, value] of blocks) {
59
+ const block = value;
60
+ if (block?.get && block.get("sys:flavour") === flavour) {
61
+ return String(block.get("sys:id"));
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ function ensureNoteBlock(blocks) {
67
+ const existingNoteId = findBlockIdByFlavour(blocks, "affine:note");
68
+ if (existingNoteId) {
69
+ return existingNoteId;
70
+ }
71
+ const pageId = findBlockIdByFlavour(blocks, "affine:page");
72
+ if (!pageId) {
73
+ throw new Error("Document has no page block; unable to insert content.");
74
+ }
75
+ const noteId = generateId();
76
+ const note = new Y.Map();
77
+ setSysFields(note, noteId, "affine:note");
78
+ note.set("sys:parent", pageId);
79
+ note.set("sys:children", new Y.Array());
80
+ note.set("prop:xywh", "[0,0,800,95]");
81
+ note.set("prop:index", "a0");
82
+ note.set("prop:hidden", false);
83
+ note.set("prop:displayMode", "both");
84
+ const background = new Y.Map();
85
+ background.set("light", "#ffffff");
86
+ background.set("dark", "#252525");
87
+ note.set("prop:background", background);
88
+ blocks.set(noteId, note);
89
+ const page = blocks.get(pageId);
90
+ let pageChildren = page.get("sys:children");
91
+ if (!(pageChildren instanceof Y.Array)) {
92
+ pageChildren = new Y.Array();
93
+ page.set("sys:children", pageChildren);
94
+ }
95
+ pageChildren.push([noteId]);
96
+ return noteId;
97
+ }
98
+ function createBlock(noteId, parsed) {
99
+ const blockId = generateId();
100
+ const block = new Y.Map();
101
+ const content = parsed.text ?? "";
102
+ switch (parsed.type) {
103
+ case "paragraph":
104
+ case "heading1":
105
+ case "heading2":
106
+ case "heading3":
107
+ case "quote": {
108
+ setSysFields(block, blockId, "affine:paragraph");
109
+ block.set("sys:parent", noteId);
110
+ block.set("sys:children", new Y.Array());
111
+ const blockType = parsed.type === "heading1"
112
+ ? "h1"
113
+ : parsed.type === "heading2"
114
+ ? "h2"
115
+ : parsed.type === "heading3"
116
+ ? "h3"
117
+ : parsed.type === "quote"
118
+ ? "quote"
119
+ : "text";
120
+ block.set("prop:type", blockType);
121
+ block.set("prop:text", makeText(content));
122
+ return { blockId, block, flavour: "affine:paragraph", blockType };
123
+ }
124
+ case "bulleted_list":
125
+ case "numbered_list":
126
+ case "todo": {
127
+ setSysFields(block, blockId, "affine:list");
128
+ block.set("sys:parent", noteId);
129
+ block.set("sys:children", new Y.Array());
130
+ const blockType = parsed.type === "bulleted_list"
131
+ ? "bulleted"
132
+ : parsed.type === "numbered_list"
133
+ ? "numbered"
134
+ : "todo";
135
+ block.set("prop:type", blockType);
136
+ if (blockType === "todo") {
137
+ block.set("prop:checked", Boolean(parsed.checked));
138
+ }
139
+ block.set("prop:text", makeText(content));
140
+ return { blockId, block, flavour: "affine:list", blockType };
141
+ }
142
+ case "code": {
143
+ setSysFields(block, blockId, "affine:code");
144
+ block.set("sys:parent", noteId);
145
+ block.set("sys:children", new Y.Array());
146
+ block.set("prop:language", (parsed.language || "txt").toLowerCase());
147
+ if (parsed.caption) {
148
+ block.set("prop:caption", parsed.caption);
149
+ }
150
+ block.set("prop:text", makeText(content));
151
+ return { blockId, block, flavour: "affine:code" };
152
+ }
153
+ case "divider": {
154
+ setSysFields(block, blockId, "affine:divider");
155
+ block.set("sys:parent", noteId);
156
+ block.set("sys:children", new Y.Array());
157
+ return { blockId, block, flavour: "affine:divider" };
158
+ }
159
+ }
160
+ }
161
+ async function appendBlockInternal(parsed) {
162
+ const workspaceId = parsed.workspaceId || defaults.workspaceId;
163
+ if (!workspaceId)
164
+ throw new Error("workspaceId is required");
165
+ const { endpoint, cookie } = await getCookieAndEndpoint();
166
+ const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
167
+ const socket = await connectWorkspaceSocket(wsUrl, cookie);
168
+ try {
169
+ await joinWorkspace(socket, workspaceId);
170
+ const doc = new Y.Doc();
171
+ const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
172
+ if (snapshot.missing) {
173
+ Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
174
+ }
175
+ const prevSV = Y.encodeStateVector(doc);
176
+ const blocks = doc.getMap("blocks");
177
+ const noteId = ensureNoteBlock(blocks);
178
+ const { blockId, block, flavour, blockType } = createBlock(noteId, parsed);
179
+ blocks.set(blockId, block);
180
+ const note = blocks.get(noteId);
181
+ let noteChildren = note.get("sys:children");
182
+ if (!(noteChildren instanceof Y.Array)) {
183
+ noteChildren = new Y.Array();
184
+ note.set("sys:children", noteChildren);
185
+ }
186
+ noteChildren.push([blockId]);
187
+ const delta = Y.encodeStateAsUpdate(doc, prevSV);
188
+ await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
189
+ return { appended: true, blockId, flavour, blockType };
190
+ }
191
+ finally {
192
+ socket.disconnect();
193
+ }
194
+ }
22
195
  const listDocsHandler = async (parsed) => {
23
196
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
24
197
  if (!workspaceId) {
@@ -38,16 +211,6 @@ export function registerDocTools(server, gql, defaults) {
38
211
  after: z.string().optional()
39
212
  }
40
213
  }, 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
214
  const getDocHandler = async (parsed) => {
52
215
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
53
216
  if (!workspaceId) {
@@ -65,78 +228,6 @@ export function registerDocTools(server, gql, defaults) {
65
228
  docId: DocId
66
229
  }
67
230
  }, 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
74
- }
75
- }, getDocHandler);
76
- const searchDocsHandler = async (parsed) => {
77
- 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.");
81
- }
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()
108
- }
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.");
114
- }
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
- };
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.",
133
- inputSchema: {
134
- workspaceId: z.string().optional(),
135
- first: z.number().optional(),
136
- offset: z.number().optional(),
137
- after: z.string().optional()
138
- }
139
- }, recentDocsHandler);
140
231
  const publishDocHandler = async (parsed) => {
141
232
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
142
233
  if (!workspaceId) {
@@ -155,15 +246,6 @@ export function registerDocTools(server, gql, defaults) {
155
246
  mode: z.enum(["Page", "Edgeless"]).optional()
156
247
  }
157
248
  }, 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
249
  const revokeDocHandler = async (parsed) => {
168
250
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
169
251
  if (!workspaceId) {
@@ -181,14 +263,6 @@ export function registerDocTools(server, gql, defaults) {
181
263
  docId: z.string()
182
264
  }
183
265
  }, 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
266
  // CREATE DOC (high-level)
193
267
  const createDocHandler = async (parsed) => {
194
268
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
@@ -205,8 +279,7 @@ export function registerDocTools(server, gql, defaults) {
205
279
  const blocks = ydoc.getMap('blocks');
206
280
  const pageId = generateId();
207
281
  const page = new Y.Map();
208
- page.set('sys:id', pageId);
209
- page.set('sys:flavour', 'affine:page');
282
+ setSysFields(page, pageId, "affine:page");
210
283
  const titleText = new Y.Text();
211
284
  titleText.insert(0, parsed.title || 'Untitled');
212
285
  page.set('prop:title', titleText);
@@ -215,21 +288,27 @@ export function registerDocTools(server, gql, defaults) {
215
288
  blocks.set(pageId, page);
216
289
  const surfaceId = generateId();
217
290
  const surface = new Y.Map();
218
- surface.set('sys:id', surfaceId);
219
- surface.set('sys:flavour', 'affine:surface');
291
+ setSysFields(surface, surfaceId, "affine:surface");
220
292
  surface.set('sys:parent', pageId);
221
293
  surface.set('sys:children', new Y.Array());
294
+ const elements = new Y.Map();
295
+ elements.set("type", "$blocksuite:internal:native$");
296
+ elements.set("value", new Y.Map());
297
+ surface.set("prop:elements", elements);
222
298
  blocks.set(surfaceId, surface);
223
299
  children.push([surfaceId]);
224
300
  const noteId = generateId();
225
301
  const note = new Y.Map();
226
- note.set('sys:id', noteId);
227
- note.set('sys:flavour', 'affine:note');
302
+ setSysFields(note, noteId, "affine:note");
228
303
  note.set('sys:parent', pageId);
229
- note.set('prop:displayMode', 'DocAndEdgeless');
230
- note.set('prop:xywh', '[0,0,800,600]');
304
+ note.set('prop:displayMode', 'both');
305
+ note.set('prop:xywh', '[0,0,800,95]');
231
306
  note.set('prop:index', 'a0');
232
- note.set('prop:lockedBySelf', false);
307
+ note.set('prop:hidden', false);
308
+ const background = new Y.Map();
309
+ background.set("light", "#ffffff");
310
+ background.set("dark", "#252525");
311
+ note.set("prop:background", background);
233
312
  const noteChildren = new Y.Array();
234
313
  note.set('sys:children', noteChildren);
235
314
  blocks.set(noteId, note);
@@ -237,8 +316,7 @@ export function registerDocTools(server, gql, defaults) {
237
316
  if (parsed.content) {
238
317
  const paraId = generateId();
239
318
  const para = new Y.Map();
240
- para.set('sys:id', paraId);
241
- para.set('sys:flavour', 'affine:paragraph');
319
+ setSysFields(para, paraId, "affine:paragraph");
242
320
  para.set('sys:parent', noteId);
243
321
  para.set('sys:children', new Y.Array());
244
322
  para.set('prop:type', 'text');
@@ -293,90 +371,15 @@ export function registerDocTools(server, gql, defaults) {
293
371
  content: z.string().optional(),
294
372
  },
295
373
  }, 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
374
  // APPEND PARAGRAPH
306
375
  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
- }
376
+ const result = await appendBlockInternal({
377
+ workspaceId: parsed.workspaceId,
378
+ docId: parsed.docId,
379
+ type: "paragraph",
380
+ text: parsed.text,
381
+ });
382
+ return text({ appended: result.appended, paragraphId: result.blockId });
380
383
  };
381
384
  server.registerTool('append_paragraph', {
382
385
  title: 'Append Paragraph',
@@ -387,15 +390,28 @@ export function registerDocTools(server, gql, defaults) {
387
390
  text: z.string(),
388
391
  },
389
392
  }, appendParagraphHandler);
390
- server.registerTool('affine_append_paragraph', {
391
- title: 'Append Paragraph',
392
- description: 'Append a text paragraph block to a document',
393
+ const appendBlockHandler = async (parsed) => {
394
+ const result = await appendBlockInternal(parsed);
395
+ return text({
396
+ appended: result.appended,
397
+ blockId: result.blockId,
398
+ flavour: result.flavour,
399
+ type: result.blockType || null,
400
+ });
401
+ };
402
+ server.registerTool("append_block", {
403
+ title: "Append Block",
404
+ description: "Append a slash-command style block (heading/list/todo/code/divider/quote) to a document.",
393
405
  inputSchema: {
394
- workspaceId: z.string().optional(),
395
- docId: z.string(),
396
- text: z.string(),
406
+ workspaceId: WorkspaceId.optional(),
407
+ docId: DocId,
408
+ type: AppendBlockType.describe("Block type to append"),
409
+ text: z.string().optional().describe("Block content text"),
410
+ checked: z.boolean().optional().describe("Todo state when type is todo"),
411
+ language: z.string().optional().describe("Code language when type is code"),
412
+ caption: z.string().optional().describe("Code caption when type is code"),
397
413
  },
398
- }, appendParagraphHandler);
414
+ }, appendBlockHandler);
399
415
  // DELETE DOC
400
416
  const deleteDocHandler = async (parsed) => {
401
417
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
@@ -441,9 +457,4 @@ export function registerDocTools(server, gql, defaults) {
441
457
  description: 'Delete a document and remove from workspace list',
442
458
  inputSchema: { workspaceId: z.string().optional(), docId: z.string() },
443
459
  }, 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
460
  }
@@ -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
  }
@@ -2,30 +2,41 @@ import { z } from "zod";
2
2
  import { text } from "../util/mcp.js";
3
3
  export function registerNotificationTools(server, gql) {
4
4
  // LIST NOTIFICATIONS
5
- const listNotificationsHandler = async ({ first = 20, unreadOnly = false }) => {
5
+ const listNotificationsHandler = async ({ first = 20, offset, after, unreadOnly = false }) => {
6
6
  try {
7
7
  const query = `
8
- query GetNotifications($first: Int!) {
8
+ query GetNotifications($pagination: PaginationInput!) {
9
9
  currentUser {
10
- notifications(first: $first) {
11
- nodes {
12
- id
13
- type
14
- title
15
- body
16
- read
17
- createdAt
10
+ notifications(pagination: $pagination) {
11
+ edges {
12
+ cursor
13
+ node {
14
+ id
15
+ type
16
+ body
17
+ read
18
+ level
19
+ createdAt
20
+ updatedAt
21
+ }
18
22
  }
19
23
  totalCount
20
24
  pageInfo {
21
25
  hasNextPage
26
+ endCursor
22
27
  }
23
28
  }
24
29
  }
25
30
  }
26
31
  `;
27
- const data = await gql.request(query, { first });
28
- let notifications = data.currentUser?.notifications?.nodes || [];
32
+ const data = await gql.request(query, {
33
+ pagination: {
34
+ first,
35
+ offset,
36
+ after
37
+ }
38
+ });
39
+ let notifications = (data.currentUser?.notifications?.edges || []).map((edge) => edge.node);
29
40
  if (unreadOnly) {
30
41
  notifications = notifications.filter((n) => !n.read);
31
42
  }
@@ -35,51 +46,16 @@ export function registerNotificationTools(server, gql) {
35
46
  return text({ error: error.message });
36
47
  }
37
48
  };
38
- server.registerTool("affine_list_notifications", {
39
- title: "List Notifications",
40
- description: "Get user notifications.",
41
- inputSchema: {
42
- first: z.number().optional().describe("Number of notifications to fetch"),
43
- unreadOnly: z.boolean().optional().describe("Show only unread notifications")
44
- }
45
- }, listNotificationsHandler);
46
49
  server.registerTool("list_notifications", {
47
50
  title: "List Notifications",
48
51
  description: "Get user notifications.",
49
52
  inputSchema: {
50
53
  first: z.number().optional().describe("Number of notifications to fetch"),
54
+ offset: z.number().optional().describe("Offset for pagination"),
55
+ after: z.string().optional().describe("Cursor for pagination"),
51
56
  unreadOnly: z.boolean().optional().describe("Show only unread notifications")
52
57
  }
53
58
  }, listNotificationsHandler);
54
- // MARK NOTIFICATION AS READ
55
- const readNotificationHandler = async ({ id }) => {
56
- try {
57
- const mutation = `
58
- mutation ReadNotification($id: String!) {
59
- readNotification(id: $id)
60
- }
61
- `;
62
- const data = await gql.request(mutation, { id });
63
- return text({ success: data.readNotification, notificationId: id });
64
- }
65
- catch (error) {
66
- return text({ error: error.message });
67
- }
68
- };
69
- server.registerTool("affine_read_notification", {
70
- title: "Mark Notification Read",
71
- description: "Mark a notification as read.",
72
- inputSchema: {
73
- id: z.string().describe("Notification ID")
74
- }
75
- }, readNotificationHandler);
76
- server.registerTool("read_notification", {
77
- title: "Mark Notification Read",
78
- description: "Mark a notification as read.",
79
- inputSchema: {
80
- id: z.string().describe("Notification ID")
81
- }
82
- }, readNotificationHandler);
83
59
  // MARK ALL NOTIFICATIONS READ
84
60
  const readAllNotificationsHandler = async () => {
85
61
  try {
@@ -95,11 +71,6 @@ export function registerNotificationTools(server, gql) {
95
71
  return text({ error: error.message });
96
72
  }
97
73
  };
98
- server.registerTool("affine_read_all_notifications", {
99
- title: "Mark All Notifications Read",
100
- description: "Mark all notifications as read.",
101
- inputSchema: {}
102
- }, readAllNotificationsHandler);
103
74
  server.registerTool("read_all_notifications", {
104
75
  title: "Mark All Notifications Read",
105
76
  description: "Mark all notifications as read.",