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.
@@ -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.",
@@ -5,11 +5,6 @@ export function registerUserTools(server, gql) {
5
5
  const data = await gql.request(query);
6
6
  return text(data.currentUser);
7
7
  };
8
- server.registerTool("affine_current_user", {
9
- title: "Current User",
10
- description: "Get current signed-in user.",
11
- inputSchema: {}
12
- }, currentUserHandler);
13
8
  server.registerTool("current_user", {
14
9
  title: "Current User",
15
10
  description: "Get current signed-in user.",
@@ -26,14 +26,6 @@ export function registerUserCRUDTools(server, gql) {
26
26
  return text({ error: error.message });
27
27
  }
28
28
  };
29
- server.registerTool("affine_update_profile", {
30
- title: "Update Profile",
31
- description: "Update current user's profile information.",
32
- inputSchema: {
33
- name: z.string().optional().describe("Display name"),
34
- avatarUrl: z.string().optional().describe("Avatar URL")
35
- }
36
- }, updateProfileHandler);
37
29
  server.registerTool("update_profile", {
38
30
  title: "Update Profile",
39
31
  description: "Update current user's profile information.",
@@ -47,163 +39,39 @@ export function registerUserCRUDTools(server, gql) {
47
39
  try {
48
40
  const mutation = `
49
41
  mutation UpdateSettings($input: UpdateUserSettingsInput!) {
50
- updateSettings(input: $input) {
51
- success
52
- }
42
+ updateSettings(input: $input)
53
43
  }
54
44
  `;
45
+ const input = {};
46
+ if (typeof settings.receiveCommentEmail === 'boolean')
47
+ input.receiveCommentEmail = settings.receiveCommentEmail;
48
+ if (typeof settings.receiveInvitationEmail === 'boolean')
49
+ input.receiveInvitationEmail = settings.receiveInvitationEmail;
50
+ if (typeof settings.receiveMentionEmail === 'boolean')
51
+ input.receiveMentionEmail = settings.receiveMentionEmail;
52
+ if (Object.keys(input).length === 0) {
53
+ return text({
54
+ error: "settings must include at least one of: receiveCommentEmail, receiveInvitationEmail, receiveMentionEmail",
55
+ });
56
+ }
55
57
  const data = await gql.request(mutation, {
56
- input: settings
58
+ input
57
59
  });
58
- return text(data.updateSettings);
60
+ return text({ success: data.updateSettings });
59
61
  }
60
62
  catch (error) {
61
63
  return text({ error: error.message });
62
64
  }
63
65
  };
64
- server.registerTool("affine_update_settings", {
65
- title: "Update Settings",
66
- description: "Update user settings and preferences.",
67
- inputSchema: {
68
- settings: z.record(z.any()).describe("Settings object with key-value pairs")
69
- }
70
- }, updateSettingsHandler);
71
66
  server.registerTool("update_settings", {
72
67
  title: "Update Settings",
73
68
  description: "Update user settings and preferences.",
74
69
  inputSchema: {
75
- settings: z.record(z.any()).describe("Settings object with key-value pairs")
70
+ settings: z.object({
71
+ receiveCommentEmail: z.boolean().optional(),
72
+ receiveInvitationEmail: z.boolean().optional(),
73
+ receiveMentionEmail: z.boolean().optional(),
74
+ }).describe("User notification settings")
76
75
  }
77
76
  }, updateSettingsHandler);
78
- // SEND VERIFICATION EMAIL
79
- const sendVerifyEmailHandler = async ({ callbackUrl }) => {
80
- try {
81
- const mutation = `
82
- mutation SendVerifyEmail($callbackUrl: String!) {
83
- sendVerifyEmail(callbackUrl: $callbackUrl)
84
- }
85
- `;
86
- const data = await gql.request(mutation, {
87
- callbackUrl: callbackUrl || `${process.env.AFFINE_BASE_URL}/verify`
88
- });
89
- return text({ success: data.sendVerifyEmail, message: "Verification email sent" });
90
- }
91
- catch (error) {
92
- return text({ error: error.message });
93
- }
94
- };
95
- server.registerTool("affine_send_verify_email", {
96
- title: "Send Verification Email",
97
- description: "Send email verification link.",
98
- inputSchema: {
99
- callbackUrl: z.string().optional().describe("Callback URL after verification")
100
- }
101
- }, sendVerifyEmailHandler);
102
- server.registerTool("send_verify_email", {
103
- title: "Send Verification Email",
104
- description: "Send email verification link.",
105
- inputSchema: {
106
- callbackUrl: z.string().optional().describe("Callback URL after verification")
107
- }
108
- }, sendVerifyEmailHandler);
109
- // CHANGE PASSWORD
110
- const changePasswordHandler = async ({ token, newPassword, userId }) => {
111
- try {
112
- const mutation = `
113
- mutation ChangePassword($token: String!, $newPassword: String!, $userId: String) {
114
- changePassword(token: $token, newPassword: $newPassword, userId: $userId)
115
- }
116
- `;
117
- const data = await gql.request(mutation, {
118
- token,
119
- newPassword,
120
- userId
121
- });
122
- return text({ success: data.changePassword, message: "Password changed successfully" });
123
- }
124
- catch (error) {
125
- return text({ error: error.message });
126
- }
127
- };
128
- server.registerTool("affine_change_password", {
129
- title: "Change Password",
130
- description: "Change user password (requires token from email).",
131
- inputSchema: {
132
- token: z.string().describe("Password reset token from email"),
133
- newPassword: z.string().describe("New password"),
134
- userId: z.string().optional().describe("User ID")
135
- }
136
- }, changePasswordHandler);
137
- server.registerTool("change_password", {
138
- title: "Change Password",
139
- description: "Change user password (requires token from email).",
140
- inputSchema: {
141
- token: z.string().describe("Password reset token from email"),
142
- newPassword: z.string().describe("New password"),
143
- userId: z.string().optional().describe("User ID")
144
- }
145
- }, changePasswordHandler);
146
- // SEND PASSWORD RESET EMAIL
147
- const sendPasswordResetHandler = async ({ callbackUrl }) => {
148
- try {
149
- const mutation = `
150
- mutation SendChangePasswordEmail($callbackUrl: String!) {
151
- sendChangePasswordEmail(callbackUrl: $callbackUrl)
152
- }
153
- `;
154
- const data = await gql.request(mutation, {
155
- callbackUrl: callbackUrl || `${process.env.AFFINE_BASE_URL}/reset-password`
156
- });
157
- return text({ success: data.sendChangePasswordEmail, message: "Password reset email sent" });
158
- }
159
- catch (error) {
160
- return text({ error: error.message });
161
- }
162
- };
163
- server.registerTool("affine_send_password_reset", {
164
- title: "Send Password Reset",
165
- description: "Send password reset email.",
166
- inputSchema: {
167
- callbackUrl: z.string().optional().describe("Callback URL for password reset")
168
- }
169
- }, sendPasswordResetHandler);
170
- server.registerTool("send_password_reset", {
171
- title: "Send Password Reset",
172
- description: "Send password reset email.",
173
- inputSchema: {
174
- callbackUrl: z.string().optional().describe("Callback URL for password reset")
175
- }
176
- }, sendPasswordResetHandler);
177
- // DELETE ACCOUNT
178
- const deleteAccountHandler = async ({ confirm }) => {
179
- if (!confirm) {
180
- return text({ error: "Confirmation required. Set confirm: true to delete account." });
181
- }
182
- try {
183
- const mutation = `
184
- mutation DeleteAccount {
185
- deleteAccount
186
- }
187
- `;
188
- const data = await gql.request(mutation);
189
- return text({ success: data.deleteAccount, message: "Account deleted successfully" });
190
- }
191
- catch (error) {
192
- return text({ error: error.message });
193
- }
194
- };
195
- server.registerTool("affine_delete_account", {
196
- title: "Delete Account",
197
- description: "Permanently delete user account. WARNING: This cannot be undone!",
198
- inputSchema: {
199
- confirm: z.literal(true).describe("Must be true to confirm account deletion")
200
- }
201
- }, deleteAccountHandler);
202
- server.registerTool("delete_account", {
203
- title: "Delete Account",
204
- description: "Permanently delete user account. WARNING: This cannot be undone!",
205
- inputSchema: {
206
- confirm: z.literal(true).describe("Must be true to confirm account deletion")
207
- }
208
- }, deleteAccountHandler);
209
77
  }
@@ -2,8 +2,8 @@ import { z } from "zod";
2
2
  import * as Y from "yjs";
3
3
  import FormData from "form-data";
4
4
  import fetch from "node-fetch";
5
- import { io } from "socket.io-client";
6
5
  import { text } from "../util/mcp.js";
6
+ import { connectWorkspaceSocket, joinWorkspace, pushDocUpdate, wsUrlFromGraphQLEndpoint } from "../ws.js";
7
7
  // Generate AFFiNE-style document ID
8
8
  function generateDocId() {
9
9
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
@@ -120,10 +120,6 @@ export function registerWorkspaceTools(server, gql) {
120
120
  title: "List Workspaces",
121
121
  description: "List all available AFFiNE workspaces"
122
122
  }, listWorkspacesHandler);
123
- server.registerTool("affine_list_workspaces", {
124
- title: "List Workspaces",
125
- description: "List all available AFFiNE workspaces"
126
- }, listWorkspacesHandler);
127
123
  // GET WORKSPACE
128
124
  const getWorkspaceHandler = async ({ id }) => {
129
125
  try {
@@ -153,13 +149,6 @@ export function registerWorkspaceTools(server, gql) {
153
149
  id: z.string().describe("Workspace ID")
154
150
  }
155
151
  }, getWorkspaceHandler);
156
- server.registerTool("affine_get_workspace", {
157
- title: "Get Workspace",
158
- description: "Get details of a specific workspace",
159
- inputSchema: {
160
- id: z.string().describe("Workspace ID")
161
- }
162
- }, getWorkspaceHandler);
163
152
  // CREATE WORKSPACE
164
153
  const createWorkspaceHandler = async ({ name, avatar }) => {
165
154
  try {
@@ -208,70 +197,39 @@ export function registerWorkspaceTools(server, gql) {
208
197
  throw new Error(result.errors[0].message);
209
198
  }
210
199
  const workspace = result.data.createWorkspace;
211
- // Now create the actual document via WebSocket
212
- const wsUrl = endpoint.replace('https://', 'wss://').replace('http://', 'ws://').replace('/graphql', '');
213
- return new Promise((resolve) => {
214
- const socket = io(wsUrl, {
215
- transports: ['websocket'],
216
- path: '/socket.io/',
217
- extraHeaders: cookie ? { Cookie: cookie } : undefined
218
- });
219
- socket.on('connect', () => {
220
- // Join the workspace
221
- socket.emit('space:join', {
222
- spaceType: 'workspace',
223
- spaceId: workspace.id
224
- });
225
- // Send the document update
226
- setTimeout(() => {
227
- const docUpdateBase64 = Buffer.from(docUpdate).toString('base64');
228
- socket.emit('space:push-doc-update', {
229
- spaceType: 'workspace',
230
- spaceId: workspace.id,
231
- docId: firstDocId,
232
- update: docUpdateBase64
233
- });
234
- // Wait longer for sync and disconnect
235
- setTimeout(() => {
236
- socket.disconnect();
237
- resolve(text({
238
- ...workspace,
239
- name: name,
240
- avatar: avatar,
241
- firstDocId: firstDocId,
242
- status: "success",
243
- message: "Workspace created successfully",
244
- url: `${process.env.AFFINE_BASE_URL}/workspace/${workspace.id}`
245
- }));
246
- }, 3000);
247
- }, 1000);
248
- });
249
- socket.on('error', () => {
200
+ const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
201
+ const baseUrl = process.env.AFFINE_BASE_URL || endpoint.replace(/\/graphql\/?$/, '');
202
+ try {
203
+ const socket = await connectWorkspaceSocket(wsUrl, cookie);
204
+ try {
205
+ await joinWorkspace(socket, workspace.id);
206
+ const docUpdateBase64 = Buffer.from(docUpdate).toString('base64');
207
+ await pushDocUpdate(socket, workspace.id, firstDocId, docUpdateBase64);
208
+ }
209
+ finally {
250
210
  socket.disconnect();
251
- // Even if WebSocket fails, workspace was created
252
- resolve(text({
253
- ...workspace,
254
- name: name,
255
- avatar: avatar,
256
- firstDocId: firstDocId,
257
- status: "partial",
258
- message: "Workspace created (document sync may be pending)",
259
- url: `${process.env.AFFINE_BASE_URL}/workspace/${workspace.id}`
260
- }));
211
+ }
212
+ }
213
+ catch (_wsError) {
214
+ // Keep workspace creation successful even if initial websocket sync fails.
215
+ return text({
216
+ ...workspace,
217
+ name,
218
+ avatar,
219
+ firstDocId,
220
+ status: "partial",
221
+ message: "Workspace created (document sync may be pending)",
222
+ url: `${baseUrl}/workspace/${workspace.id}`
261
223
  });
262
- // Timeout
263
- setTimeout(() => {
264
- socket.disconnect();
265
- resolve(text({
266
- ...workspace,
267
- name: name,
268
- avatar: avatar,
269
- firstDocId: firstDocId,
270
- status: "success",
271
- message: "Workspace created",
272
- url: `${process.env.AFFINE_BASE_URL}/workspace/${workspace.id}`
273
- }));
274
- }, 10000);
224
+ }
225
+ return text({
226
+ ...workspace,
227
+ name,
228
+ avatar,
229
+ firstDocId,
230
+ status: "success",
231
+ message: "Workspace created successfully",
232
+ url: `${baseUrl}/workspace/${workspace.id}`
275
233
  });
276
234
  }
277
235
  catch (error) {
@@ -286,22 +244,6 @@ export function registerWorkspaceTools(server, gql) {
286
244
  avatar: z.string().optional().describe("Avatar emoji or URL")
287
245
  }
288
246
  }, createWorkspaceHandler);
289
- server.registerTool("affine_create_workspace", {
290
- title: "Create Workspace",
291
- description: "Create a new workspace with initial document (accessible in UI)",
292
- inputSchema: {
293
- name: z.string().describe("Workspace name"),
294
- avatar: z.string().optional().describe("Avatar emoji or URL")
295
- }
296
- }, createWorkspaceHandler);
297
- server.registerTool("affine_create_workspace_fixed", {
298
- title: "Create Workspace (Fixed)",
299
- description: "Create a new workspace with initial document (backward compatible alias)",
300
- inputSchema: {
301
- name: z.string().describe("Workspace name"),
302
- avatar: z.string().optional().describe("Avatar emoji or URL")
303
- }
304
- }, createWorkspaceHandler);
305
247
  // UPDATE WORKSPACE
306
248
  const updateWorkspaceHandler = async ({ id, public: isPublic, enableAi }) => {
307
249
  try {
@@ -310,12 +252,15 @@ export function registerWorkspaceTools(server, gql) {
310
252
  updateWorkspace(input: $input) {
311
253
  id
312
254
  public
255
+ enableAi
313
256
  }
314
257
  }
315
258
  `;
316
259
  const input = { id };
317
260
  if (isPublic !== undefined)
318
261
  input.public = isPublic;
262
+ if (enableAi !== undefined)
263
+ input.enableAi = enableAi;
319
264
  const data = await gql.request(mutation, { input });
320
265
  return text(data.updateWorkspace);
321
266
  }
@@ -332,15 +277,6 @@ export function registerWorkspaceTools(server, gql) {
332
277
  enableAi: z.boolean().optional().describe("Enable AI features")
333
278
  }
334
279
  }, updateWorkspaceHandler);
335
- server.registerTool("affine_update_workspace", {
336
- title: "Update Workspace",
337
- description: "Update workspace settings",
338
- inputSchema: {
339
- id: z.string().describe("Workspace ID"),
340
- public: z.boolean().optional().describe("Make workspace public"),
341
- enableAi: z.boolean().optional().describe("Enable AI features")
342
- }
343
- }, updateWorkspaceHandler);
344
280
  // DELETE WORKSPACE
345
281
  const deleteWorkspaceHandler = async ({ id }) => {
346
282
  try {
@@ -363,11 +299,4 @@ export function registerWorkspaceTools(server, gql) {
363
299
  id: z.string().describe("Workspace ID")
364
300
  }
365
301
  }, deleteWorkspaceHandler);
366
- server.registerTool("affine_delete_workspace", {
367
- title: "Delete Workspace",
368
- description: "Delete a workspace permanently",
369
- inputSchema: {
370
- id: z.string().describe("Workspace ID")
371
- }
372
- }, deleteWorkspaceHandler);
373
302
  }
package/dist/ws.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { io } from "socket.io-client";
2
+ const DEFAULT_WS_CLIENT_VERSION = process.env.AFFINE_WS_CLIENT_VERSION || '0.26.0';
3
+ const WS_CONNECT_TIMEOUT_MS = Number(process.env.AFFINE_WS_CONNECT_TIMEOUT_MS || 10000);
4
+ const WS_ACK_TIMEOUT_MS = Number(process.env.AFFINE_WS_ACK_TIMEOUT_MS || 10000);
2
5
  export function wsUrlFromGraphQLEndpoint(endpoint) {
3
6
  return endpoint
4
7
  .replace('https://', 'wss://')
@@ -13,15 +16,22 @@ export async function connectWorkspaceSocket(wsUrl, cookie) {
13
16
  extraHeaders: cookie ? { Cookie: cookie } : undefined,
14
17
  autoConnect: true
15
18
  });
19
+ const timeout = setTimeout(() => {
20
+ cleanup();
21
+ socket.disconnect();
22
+ reject(new Error(`socket connect timeout after ${WS_CONNECT_TIMEOUT_MS}ms`));
23
+ }, WS_CONNECT_TIMEOUT_MS);
16
24
  const onError = (err) => {
17
25
  cleanup();
26
+ socket.disconnect();
18
27
  reject(err);
19
28
  };
20
29
  const onConnect = () => {
21
- socket.off('connect_error', onError);
30
+ cleanup();
22
31
  resolve(socket);
23
32
  };
24
33
  const cleanup = () => {
34
+ clearTimeout(timeout);
25
35
  socket.off('connect', onConnect);
26
36
  socket.off('connect_error', onError);
27
37
  };
@@ -29,9 +39,13 @@ export async function connectWorkspaceSocket(wsUrl, cookie) {
29
39
  socket.on('connect_error', onError);
30
40
  });
31
41
  }
32
- export async function joinWorkspace(socket, workspaceId) {
42
+ export async function joinWorkspace(socket, workspaceId, clientVersion = DEFAULT_WS_CLIENT_VERSION) {
33
43
  return new Promise((resolve, reject) => {
34
- socket.emit('space:join', { spaceType: 'workspace', spaceId: workspaceId, clientVersion: 'mcp' }, (ack) => {
44
+ const timeout = setTimeout(() => {
45
+ reject(new Error(`space:join timeout after ${WS_ACK_TIMEOUT_MS}ms`));
46
+ }, WS_ACK_TIMEOUT_MS);
47
+ socket.emit('space:join', { spaceType: 'workspace', spaceId: workspaceId, clientVersion }, (ack) => {
48
+ clearTimeout(timeout);
35
49
  if (ack?.error)
36
50
  return reject(new Error(ack.error.message || 'join failed'));
37
51
  resolve();
@@ -40,7 +54,11 @@ export async function joinWorkspace(socket, workspaceId) {
40
54
  }
41
55
  export async function loadDoc(socket, workspaceId, docId) {
42
56
  return new Promise((resolve, reject) => {
57
+ const timeout = setTimeout(() => {
58
+ reject(new Error(`space:load-doc timeout after ${WS_ACK_TIMEOUT_MS}ms`));
59
+ }, WS_ACK_TIMEOUT_MS);
43
60
  socket.emit('space:load-doc', { spaceType: 'workspace', spaceId: workspaceId, docId }, (ack) => {
61
+ clearTimeout(timeout);
44
62
  if (ack?.error) {
45
63
  if (ack.error.name === 'DOC_NOT_FOUND')
46
64
  return resolve({});
@@ -52,7 +70,11 @@ export async function loadDoc(socket, workspaceId, docId) {
52
70
  }
53
71
  export async function pushDocUpdate(socket, workspaceId, docId, updateBase64) {
54
72
  return new Promise((resolve, reject) => {
73
+ const timeout = setTimeout(() => {
74
+ reject(new Error(`space:push-doc-update timeout after ${WS_ACK_TIMEOUT_MS}ms`));
75
+ }, WS_ACK_TIMEOUT_MS);
55
76
  socket.emit('space:push-doc-update', { spaceType: 'workspace', spaceId: workspaceId, docId, update: updateBase64 }, (ack) => {
77
+ clearTimeout(timeout);
56
78
  if (ack?.error)
57
79
  return reject(new Error(ack.error.message || 'push-doc-update failed'));
58
80
  resolve(ack?.data?.timestamp || Date.now());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "affine-mcp-server",
3
- "version": "1.2.2",
3
+ "version": "1.4.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.",
@@ -10,6 +10,10 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/dawncr0w/affine-mcp-server.git"
12
12
  },
13
+ "bugs": {
14
+ "url": "https://github.com/dawncr0w/affine-mcp-server/issues"
15
+ },
16
+ "homepage": "https://github.com/dawncr0w/affine-mcp-server#readme",
13
17
  "keywords": [
14
18
  "mcp",
15
19
  "affine",
@@ -23,10 +27,16 @@
23
27
  "affine-mcp": "bin/affine-mcp"
24
28
  },
25
29
  "scripts": {
26
- "build": "tsc -p tsconfig.json",
30
+ "clean": "node -e \"require('fs').rmSync('dist',{ recursive: true, force: true })\"",
31
+ "build": "npm run clean && tsc -p tsconfig.json",
27
32
  "dev": "tsx watch src/index.ts",
28
33
  "start": "node dist/index.js",
29
- "prepublishOnly": "npm run build"
34
+ "test": "npm run test:tool-manifest",
35
+ "test:tool-manifest": "node scripts/verify-tool-manifest.mjs",
36
+ "test:comprehensive": "node test-comprehensive.mjs",
37
+ "pack:check": "npm pack --dry-run",
38
+ "ci": "npm run build && npm run test:tool-manifest && npm run pack:check",
39
+ "prepublishOnly": "npm run ci"
30
40
  },
31
41
  "files": [
32
42
  "bin",
@@ -45,12 +55,12 @@
45
55
  "form-data": "^4.0.4",
46
56
  "node-fetch": "^3.3.2",
47
57
  "socket.io-client": "^4.8.1",
48
- "undici": "^6.19.8",
58
+ "undici": "^7.21.0",
49
59
  "yjs": "^13.6.27",
50
60
  "zod": "^3.23.8"
51
61
  },
52
62
  "devDependencies": {
53
- "@types/node": "^20.14.11",
63
+ "@types/node": "^25.2.3",
54
64
  "tsx": "^4.16.2",
55
65
  "typescript": "^5.5.4"
56
66
  }