affine-mcp-server 1.2.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/LICENSE +22 -0
- package/README.md +228 -0
- package/dist/auth.js +37 -0
- package/dist/config.js +47 -0
- package/dist/graphqlClient.js +45 -0
- package/dist/index.js +68 -0
- package/dist/tools/accessTokens.js +65 -0
- package/dist/tools/auth.js +26 -0
- package/dist/tools/blobStorage.js +112 -0
- package/dist/tools/comments.js +128 -0
- package/dist/tools/docs.js +449 -0
- package/dist/tools/history.js +58 -0
- package/dist/tools/notifications.js +108 -0
- package/dist/tools/updates.js +32 -0
- package/dist/tools/user.js +18 -0
- package/dist/tools/userCRUD.js +209 -0
- package/dist/tools/workspaces.js +373 -0
- package/dist/types.js +1 -0
- package/dist/util/mcp.js +4 -0
- package/dist/ws.js +64 -0
- package/package.json +57 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { text } from "../util/mcp.js";
|
|
3
|
+
export function registerCommentTools(server, gql, defaults) {
|
|
4
|
+
const listCommentsHandler = async (parsed) => {
|
|
5
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId || parsed.workspaceId;
|
|
6
|
+
if (!workspaceId)
|
|
7
|
+
throw new Error("workspaceId required (or set AFFINE_WORKSPACE_ID)");
|
|
8
|
+
const query = `query ListComments($workspaceId:String!,$docId:String!,$first:Int,$offset:Int,$after:String){ workspace(id:$workspaceId){ comments(docId:$docId, pagination:{first:$first, offset:$offset, after:$after}){ totalCount pageInfo{ hasNextPage endCursor } edges{ cursor node{ id content createdAt updatedAt resolved user{ id name avatarUrl } replies{ id content createdAt updatedAt user{ id name avatarUrl } } } } } } }`;
|
|
9
|
+
const data = await gql.request(query, { workspaceId, docId: parsed.docId, first: parsed.first, offset: parsed.offset, after: parsed.after });
|
|
10
|
+
return text(data.workspace.comments);
|
|
11
|
+
};
|
|
12
|
+
server.registerTool("affine_list_comments", {
|
|
13
|
+
title: "List Comments",
|
|
14
|
+
description: "List comments of a doc (with replies).",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
workspaceId: z.string().optional(),
|
|
17
|
+
docId: z.string(),
|
|
18
|
+
first: z.number().optional(),
|
|
19
|
+
offset: z.number().optional(),
|
|
20
|
+
after: z.string().optional()
|
|
21
|
+
}
|
|
22
|
+
}, listCommentsHandler);
|
|
23
|
+
server.registerTool("list_comments", {
|
|
24
|
+
title: "List Comments",
|
|
25
|
+
description: "List comments of a doc (with replies).",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
workspaceId: z.string().optional(),
|
|
28
|
+
docId: z.string(),
|
|
29
|
+
first: z.number().optional(),
|
|
30
|
+
offset: z.number().optional(),
|
|
31
|
+
after: z.string().optional()
|
|
32
|
+
}
|
|
33
|
+
}, listCommentsHandler);
|
|
34
|
+
const createCommentHandler = async (parsed) => {
|
|
35
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId || parsed.workspaceId;
|
|
36
|
+
if (!workspaceId)
|
|
37
|
+
throw new Error("workspaceId required (or set AFFINE_WORKSPACE_ID)");
|
|
38
|
+
const mutation = `mutation CreateComment($input: CommentCreateInput!){ createComment(input:$input){ id content createdAt updatedAt resolved } }`;
|
|
39
|
+
const input = { content: parsed.content, docId: parsed.docId, workspaceId, docTitle: parsed.docTitle || "", docMode: parsed.docMode || "Page", mentions: parsed.mentions };
|
|
40
|
+
const data = await gql.request(mutation, { input });
|
|
41
|
+
return text(data.createComment);
|
|
42
|
+
};
|
|
43
|
+
server.registerTool("affine_create_comment", {
|
|
44
|
+
title: "Create Comment",
|
|
45
|
+
description: "Create a comment on a doc.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
workspaceId: z.string().optional(),
|
|
48
|
+
docId: z.string(),
|
|
49
|
+
docTitle: z.string().optional(),
|
|
50
|
+
docMode: z.enum(["Page", "Edgeless"]).optional(),
|
|
51
|
+
content: z.any(),
|
|
52
|
+
mentions: z.array(z.string()).optional()
|
|
53
|
+
}
|
|
54
|
+
}, createCommentHandler);
|
|
55
|
+
server.registerTool("create_comment", {
|
|
56
|
+
title: "Create Comment",
|
|
57
|
+
description: "Create a comment on a doc.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
workspaceId: z.string().optional(),
|
|
60
|
+
docId: z.string(),
|
|
61
|
+
docTitle: z.string().optional(),
|
|
62
|
+
docMode: z.enum(["Page", "Edgeless"]).optional(),
|
|
63
|
+
content: z.any(),
|
|
64
|
+
mentions: z.array(z.string()).optional()
|
|
65
|
+
}
|
|
66
|
+
}, createCommentHandler);
|
|
67
|
+
const updateCommentHandler = async (parsed) => {
|
|
68
|
+
const mutation = `mutation UpdateComment($input: CommentUpdateInput!){ updateComment(input:$input) }`;
|
|
69
|
+
const data = await gql.request(mutation, { input: { id: parsed.id, content: parsed.content } });
|
|
70
|
+
return text({ success: data.updateComment });
|
|
71
|
+
};
|
|
72
|
+
server.registerTool("affine_update_comment", {
|
|
73
|
+
title: "Update Comment",
|
|
74
|
+
description: "Update a comment content.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
id: z.string(),
|
|
77
|
+
content: z.any()
|
|
78
|
+
}
|
|
79
|
+
}, updateCommentHandler);
|
|
80
|
+
server.registerTool("update_comment", {
|
|
81
|
+
title: "Update Comment",
|
|
82
|
+
description: "Update a comment content.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
id: z.string(),
|
|
85
|
+
content: z.any()
|
|
86
|
+
}
|
|
87
|
+
}, updateCommentHandler);
|
|
88
|
+
const deleteCommentHandler = async (parsed) => {
|
|
89
|
+
const mutation = `mutation DeleteComment($id:String!){ deleteComment(id:$id) }`;
|
|
90
|
+
const data = await gql.request(mutation, { id: parsed.id });
|
|
91
|
+
return text({ success: data.deleteComment });
|
|
92
|
+
};
|
|
93
|
+
server.registerTool("affine_delete_comment", {
|
|
94
|
+
title: "Delete Comment",
|
|
95
|
+
description: "Delete a comment by id.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
id: z.string()
|
|
98
|
+
}
|
|
99
|
+
}, deleteCommentHandler);
|
|
100
|
+
server.registerTool("delete_comment", {
|
|
101
|
+
title: "Delete Comment",
|
|
102
|
+
description: "Delete a comment by id.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
id: z.string()
|
|
105
|
+
}
|
|
106
|
+
}, deleteCommentHandler);
|
|
107
|
+
const resolveCommentHandler = async (parsed) => {
|
|
108
|
+
const mutation = `mutation ResolveComment($input: CommentResolveInput!){ resolveComment(input:$input) }`;
|
|
109
|
+
const data = await gql.request(mutation, { input: parsed });
|
|
110
|
+
return text({ success: data.resolveComment });
|
|
111
|
+
};
|
|
112
|
+
server.registerTool("affine_resolve_comment", {
|
|
113
|
+
title: "Resolve Comment",
|
|
114
|
+
description: "Resolve or unresolve a comment.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
id: z.string(),
|
|
117
|
+
resolved: z.boolean()
|
|
118
|
+
}
|
|
119
|
+
}, resolveCommentHandler);
|
|
120
|
+
server.registerTool("resolve_comment", {
|
|
121
|
+
title: "Resolve Comment",
|
|
122
|
+
description: "Resolve or unresolve a comment.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
id: z.string(),
|
|
125
|
+
resolved: z.boolean()
|
|
126
|
+
}
|
|
127
|
+
}, resolveCommentHandler);
|
|
128
|
+
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { text } from "../util/mcp.js";
|
|
3
|
+
import { wsUrlFromGraphQLEndpoint, connectWorkspaceSocket, joinWorkspace, loadDoc, pushDocUpdate, deleteDoc as wsDeleteDoc } from "../ws.js";
|
|
4
|
+
import * as Y from "yjs";
|
|
5
|
+
const WorkspaceId = z.string().min(1, "workspaceId required");
|
|
6
|
+
const DocId = z.string().min(1, "docId required");
|
|
7
|
+
export function registerDocTools(server, gql, defaults) {
|
|
8
|
+
// helpers
|
|
9
|
+
function generateId() {
|
|
10
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
|
|
11
|
+
let id = '';
|
|
12
|
+
for (let i = 0; i < 10; i++)
|
|
13
|
+
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
async function getCookieAndEndpoint() {
|
|
17
|
+
const endpoint = gql.endpoint || process.env.AFFINE_BASE_URL + '/graphql';
|
|
18
|
+
const headers = gql.headers || {};
|
|
19
|
+
const cookie = gql.cookie || headers.Cookie || '';
|
|
20
|
+
return { endpoint, cookie };
|
|
21
|
+
}
|
|
22
|
+
const listDocsHandler = async (parsed) => {
|
|
23
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
24
|
+
if (!workspaceId) {
|
|
25
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
26
|
+
}
|
|
27
|
+
const query = `query ListDocs($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 } } } } }`;
|
|
28
|
+
const data = await gql.request(query, { workspaceId, first: parsed.first, offset: parsed.offset, after: parsed.after });
|
|
29
|
+
return text(data.workspace.docs);
|
|
30
|
+
};
|
|
31
|
+
server.registerTool("list_docs", {
|
|
32
|
+
title: "List Documents",
|
|
33
|
+
description: "List documents in a workspace (GraphQL).",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
workspaceId: z.string().describe("Workspace ID (optional if default set).").optional(),
|
|
36
|
+
first: z.number().optional(),
|
|
37
|
+
offset: z.number().optional(),
|
|
38
|
+
after: z.string().optional()
|
|
39
|
+
}
|
|
40
|
+
}, 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
|
+
const getDocHandler = async (parsed) => {
|
|
52
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
53
|
+
if (!workspaceId) {
|
|
54
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
55
|
+
}
|
|
56
|
+
const query = `query GetDoc($workspaceId:String!, $docId:String!){ workspace(id:$workspaceId){ doc(docId:$docId){ id workspaceId title summary public defaultRole createdAt updatedAt } } }`;
|
|
57
|
+
const data = await gql.request(query, { workspaceId, docId: parsed.docId });
|
|
58
|
+
return text(data.workspace.doc);
|
|
59
|
+
};
|
|
60
|
+
server.registerTool("get_doc", {
|
|
61
|
+
title: "Get Document",
|
|
62
|
+
description: "Get a document by ID (GraphQL metadata).",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
workspaceId: z.string().optional(),
|
|
65
|
+
docId: DocId
|
|
66
|
+
}
|
|
67
|
+
}, 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
|
+
const publishDocHandler = async (parsed) => {
|
|
141
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
142
|
+
if (!workspaceId) {
|
|
143
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
144
|
+
}
|
|
145
|
+
const mutation = `mutation PublishDoc($workspaceId:String!,$docId:String!,$mode:PublicDocMode){ publishDoc(workspaceId:$workspaceId, docId:$docId, mode:$mode){ id workspaceId public mode } }`;
|
|
146
|
+
const data = await gql.request(mutation, { workspaceId, docId: parsed.docId, mode: parsed.mode });
|
|
147
|
+
return text(data.publishDoc);
|
|
148
|
+
};
|
|
149
|
+
server.registerTool("publish_doc", {
|
|
150
|
+
title: "Publish Document",
|
|
151
|
+
description: "Publish a doc (make public).",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
workspaceId: z.string().optional(),
|
|
154
|
+
docId: z.string(),
|
|
155
|
+
mode: z.enum(["Page", "Edgeless"]).optional()
|
|
156
|
+
}
|
|
157
|
+
}, 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
|
+
const revokeDocHandler = async (parsed) => {
|
|
168
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
169
|
+
if (!workspaceId) {
|
|
170
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
171
|
+
}
|
|
172
|
+
const mutation = `mutation RevokeDoc($workspaceId:String!,$docId:String!){ revokePublicDoc(workspaceId:$workspaceId, docId:$docId){ id workspaceId public } }`;
|
|
173
|
+
const data = await gql.request(mutation, { workspaceId, docId: parsed.docId });
|
|
174
|
+
return text(data.revokePublicDoc);
|
|
175
|
+
};
|
|
176
|
+
server.registerTool("revoke_doc", {
|
|
177
|
+
title: "Revoke Document",
|
|
178
|
+
description: "Revoke a doc's public access.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
workspaceId: z.string().optional(),
|
|
181
|
+
docId: z.string()
|
|
182
|
+
}
|
|
183
|
+
}, 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
|
+
// CREATE DOC (high-level)
|
|
193
|
+
const createDocHandler = async (parsed) => {
|
|
194
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
195
|
+
if (!workspaceId)
|
|
196
|
+
throw new Error("workspaceId is required. Provide it or set AFFINE_WORKSPACE_ID.");
|
|
197
|
+
const { endpoint, cookie } = await getCookieAndEndpoint();
|
|
198
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
199
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie);
|
|
200
|
+
try {
|
|
201
|
+
await joinWorkspace(socket, workspaceId);
|
|
202
|
+
// 1) Create doc content
|
|
203
|
+
const docId = generateId();
|
|
204
|
+
const ydoc = new Y.Doc();
|
|
205
|
+
const blocks = ydoc.getMap('blocks');
|
|
206
|
+
const pageId = generateId();
|
|
207
|
+
const page = new Y.Map();
|
|
208
|
+
page.set('sys:id', pageId);
|
|
209
|
+
page.set('sys:flavour', 'affine:page');
|
|
210
|
+
const titleText = new Y.Text();
|
|
211
|
+
titleText.insert(0, parsed.title || 'Untitled');
|
|
212
|
+
page.set('prop:title', titleText);
|
|
213
|
+
const children = new Y.Array();
|
|
214
|
+
page.set('sys:children', children);
|
|
215
|
+
blocks.set(pageId, page);
|
|
216
|
+
const surfaceId = generateId();
|
|
217
|
+
const surface = new Y.Map();
|
|
218
|
+
surface.set('sys:id', surfaceId);
|
|
219
|
+
surface.set('sys:flavour', 'affine:surface');
|
|
220
|
+
surface.set('sys:parent', pageId);
|
|
221
|
+
surface.set('sys:children', new Y.Array());
|
|
222
|
+
blocks.set(surfaceId, surface);
|
|
223
|
+
children.push([surfaceId]);
|
|
224
|
+
const noteId = generateId();
|
|
225
|
+
const note = new Y.Map();
|
|
226
|
+
note.set('sys:id', noteId);
|
|
227
|
+
note.set('sys:flavour', 'affine:note');
|
|
228
|
+
note.set('sys:parent', pageId);
|
|
229
|
+
note.set('prop:displayMode', 'DocAndEdgeless');
|
|
230
|
+
note.set('prop:xywh', '[0,0,800,600]');
|
|
231
|
+
note.set('prop:index', 'a0');
|
|
232
|
+
note.set('prop:lockedBySelf', false);
|
|
233
|
+
const noteChildren = new Y.Array();
|
|
234
|
+
note.set('sys:children', noteChildren);
|
|
235
|
+
blocks.set(noteId, note);
|
|
236
|
+
children.push([noteId]);
|
|
237
|
+
if (parsed.content) {
|
|
238
|
+
const paraId = generateId();
|
|
239
|
+
const para = new Y.Map();
|
|
240
|
+
para.set('sys:id', paraId);
|
|
241
|
+
para.set('sys:flavour', 'affine:paragraph');
|
|
242
|
+
para.set('sys:parent', noteId);
|
|
243
|
+
para.set('sys:children', new Y.Array());
|
|
244
|
+
para.set('prop:type', 'text');
|
|
245
|
+
const ptext = new Y.Text();
|
|
246
|
+
ptext.insert(0, parsed.content);
|
|
247
|
+
para.set('prop:text', ptext);
|
|
248
|
+
blocks.set(paraId, para);
|
|
249
|
+
noteChildren.push([paraId]);
|
|
250
|
+
}
|
|
251
|
+
const meta = ydoc.getMap('meta');
|
|
252
|
+
meta.set('id', docId);
|
|
253
|
+
meta.set('title', parsed.title || 'Untitled');
|
|
254
|
+
meta.set('createDate', Date.now());
|
|
255
|
+
meta.set('tags', new Y.Array());
|
|
256
|
+
const updateFull = Y.encodeStateAsUpdate(ydoc);
|
|
257
|
+
const updateBase64 = Buffer.from(updateFull).toString('base64');
|
|
258
|
+
await pushDocUpdate(socket, workspaceId, docId, updateBase64);
|
|
259
|
+
// 2) Update workspace root pages list
|
|
260
|
+
const wsDoc = new Y.Doc();
|
|
261
|
+
const snapshot = await loadDoc(socket, workspaceId, workspaceId);
|
|
262
|
+
if (snapshot.missing) {
|
|
263
|
+
Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, 'base64'));
|
|
264
|
+
}
|
|
265
|
+
const prevSV = Y.encodeStateVector(wsDoc);
|
|
266
|
+
const wsMeta = wsDoc.getMap('meta');
|
|
267
|
+
let pages = wsMeta.get('pages');
|
|
268
|
+
if (!pages) {
|
|
269
|
+
pages = new Y.Array();
|
|
270
|
+
wsMeta.set('pages', pages);
|
|
271
|
+
}
|
|
272
|
+
const entry = new Y.Map();
|
|
273
|
+
entry.set('id', docId);
|
|
274
|
+
entry.set('title', parsed.title || 'Untitled');
|
|
275
|
+
entry.set('createDate', Date.now());
|
|
276
|
+
entry.set('tags', new Y.Array());
|
|
277
|
+
pages.push([entry]);
|
|
278
|
+
const wsDelta = Y.encodeStateAsUpdate(wsDoc, prevSV);
|
|
279
|
+
const wsDeltaB64 = Buffer.from(wsDelta).toString('base64');
|
|
280
|
+
await pushDocUpdate(socket, workspaceId, workspaceId, wsDeltaB64);
|
|
281
|
+
return text({ docId, title: parsed.title || 'Untitled' });
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
socket.disconnect();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
server.registerTool('create_doc', {
|
|
288
|
+
title: 'Create Document',
|
|
289
|
+
description: 'Create a new AFFiNE document with optional content',
|
|
290
|
+
inputSchema: {
|
|
291
|
+
workspaceId: z.string().optional(),
|
|
292
|
+
title: z.string().optional(),
|
|
293
|
+
content: z.string().optional(),
|
|
294
|
+
},
|
|
295
|
+
}, 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
|
+
// APPEND PARAGRAPH
|
|
306
|
+
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
|
+
}
|
|
380
|
+
};
|
|
381
|
+
server.registerTool('append_paragraph', {
|
|
382
|
+
title: 'Append Paragraph',
|
|
383
|
+
description: 'Append a text paragraph block to a document',
|
|
384
|
+
inputSchema: {
|
|
385
|
+
workspaceId: z.string().optional(),
|
|
386
|
+
docId: z.string(),
|
|
387
|
+
text: z.string(),
|
|
388
|
+
},
|
|
389
|
+
}, appendParagraphHandler);
|
|
390
|
+
server.registerTool('affine_append_paragraph', {
|
|
391
|
+
title: 'Append Paragraph',
|
|
392
|
+
description: 'Append a text paragraph block to a document',
|
|
393
|
+
inputSchema: {
|
|
394
|
+
workspaceId: z.string().optional(),
|
|
395
|
+
docId: z.string(),
|
|
396
|
+
text: z.string(),
|
|
397
|
+
},
|
|
398
|
+
}, appendParagraphHandler);
|
|
399
|
+
// DELETE DOC
|
|
400
|
+
const deleteDocHandler = async (parsed) => {
|
|
401
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
402
|
+
if (!workspaceId)
|
|
403
|
+
throw new Error('workspaceId is required');
|
|
404
|
+
const { endpoint, cookie } = await getCookieAndEndpoint();
|
|
405
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
406
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie);
|
|
407
|
+
try {
|
|
408
|
+
await joinWorkspace(socket, workspaceId);
|
|
409
|
+
// remove from workspace pages
|
|
410
|
+
const wsDoc = new Y.Doc();
|
|
411
|
+
const snapshot = await loadDoc(socket, workspaceId, workspaceId);
|
|
412
|
+
if (snapshot.missing)
|
|
413
|
+
Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, 'base64'));
|
|
414
|
+
const prevSV = Y.encodeStateVector(wsDoc);
|
|
415
|
+
const wsMeta = wsDoc.getMap('meta');
|
|
416
|
+
const pages = wsMeta.get('pages');
|
|
417
|
+
if (pages) {
|
|
418
|
+
// find by id
|
|
419
|
+
let idx = -1;
|
|
420
|
+
pages.forEach((m, i) => {
|
|
421
|
+
if (idx >= 0)
|
|
422
|
+
return;
|
|
423
|
+
if (m.get && m.get('id') === parsed.docId)
|
|
424
|
+
idx = i;
|
|
425
|
+
});
|
|
426
|
+
if (idx >= 0)
|
|
427
|
+
pages.delete(idx, 1);
|
|
428
|
+
}
|
|
429
|
+
const wsDelta = Y.encodeStateAsUpdate(wsDoc, prevSV);
|
|
430
|
+
await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(wsDelta).toString('base64'));
|
|
431
|
+
// delete doc content
|
|
432
|
+
wsDeleteDoc(socket, workspaceId, parsed.docId);
|
|
433
|
+
return text({ deleted: true });
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
socket.disconnect();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
server.registerTool('delete_doc', {
|
|
440
|
+
title: 'Delete Document',
|
|
441
|
+
description: 'Delete a document and remove from workspace list',
|
|
442
|
+
inputSchema: { workspaceId: z.string().optional(), docId: z.string() },
|
|
443
|
+
}, 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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { text } from "../util/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export function registerHistoryTools(server, gql, defaults) {
|
|
4
|
+
const listHistoriesHandler = async (parsed) => {
|
|
5
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId || parsed.workspaceId;
|
|
6
|
+
if (!workspaceId)
|
|
7
|
+
throw new Error("workspaceId required (or set AFFINE_WORKSPACE_ID)");
|
|
8
|
+
const query = `query Histories($workspaceId:String!,$guid:String!,$take:Int,$before:DateTime){ workspace(id:$workspaceId){ histories(guid:$guid, take:$take, before:$before){ id timestamp workspaceId } } }`;
|
|
9
|
+
const data = await gql.request(query, { workspaceId, guid: parsed.guid, take: parsed.take, before: parsed.before });
|
|
10
|
+
return text(data.workspace.histories);
|
|
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
|
+
server.registerTool("list_histories", {
|
|
23
|
+
title: "List Histories",
|
|
24
|
+
description: "List doc histories (timestamps) for a doc.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
workspaceId: z.string().optional(),
|
|
27
|
+
guid: z.string(),
|
|
28
|
+
take: z.number().optional(),
|
|
29
|
+
before: z.string().optional()
|
|
30
|
+
}
|
|
31
|
+
}, 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
|
+
}
|