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.
- package/README.md +99 -23
- package/bin/affine-mcp +0 -0
- package/dist/index.js +1 -3
- package/dist/tools/accessTokens.js +3 -23
- package/dist/tools/auth.js +0 -8
- package/dist/tools/blobStorage.js +59 -35
- package/dist/tools/comments.js +4 -48
- package/dist/tools/docs.js +331 -197
- package/dist/tools/history.js +0 -36
- package/dist/tools/notifications.js +25 -54
- package/dist/tools/user.js +0 -5
- package/dist/tools/userCRUD.js +20 -152
- package/dist/tools/workspaces.js +35 -106
- package/dist/ws.js +25 -3
- package/package.json +15 -5
- package/dist/tools/updates.js +0 -32
|
@@ -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($
|
|
8
|
+
query GetNotifications($pagination: PaginationInput!) {
|
|
9
9
|
currentUser {
|
|
10
|
-
notifications(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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, {
|
|
28
|
-
|
|
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.",
|
package/dist/tools/user.js
CHANGED
|
@@ -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.",
|
package/dist/tools/userCRUD.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
}
|
package/dist/tools/workspaces.js
CHANGED
|
@@ -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
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
const socket =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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": "^
|
|
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": "^
|
|
63
|
+
"@types/node": "^25.2.3",
|
|
54
64
|
"tsx": "^4.16.2",
|
|
55
65
|
"typescript": "^5.5.4"
|
|
56
66
|
}
|