@mseep/affine-mcp-server 2.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.
Files changed (43) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +270 -0
  3. package/bin/affine-mcp +5 -0
  4. package/dist/auth.js +61 -0
  5. package/dist/cli.js +726 -0
  6. package/dist/config.js +178 -0
  7. package/dist/edgeless/layout.js +222 -0
  8. package/dist/graphqlClient.js +116 -0
  9. package/dist/httpAuth.js +147 -0
  10. package/dist/httpDiagnostics.js +38 -0
  11. package/dist/index.js +209 -0
  12. package/dist/markdown/parse.js +559 -0
  13. package/dist/markdown/render.js +227 -0
  14. package/dist/markdown/types.js +1 -0
  15. package/dist/oauth.js +154 -0
  16. package/dist/sse.js +261 -0
  17. package/dist/toolSurface.js +349 -0
  18. package/dist/tools/accessTokens.js +45 -0
  19. package/dist/tools/auth.js +18 -0
  20. package/dist/tools/blobStorage.js +136 -0
  21. package/dist/tools/comments.js +104 -0
  22. package/dist/tools/docs.js +7478 -0
  23. package/dist/tools/history.js +22 -0
  24. package/dist/tools/icons.js +125 -0
  25. package/dist/tools/notifications.js +79 -0
  26. package/dist/tools/organize.js +1145 -0
  27. package/dist/tools/properties.js +426 -0
  28. package/dist/tools/user.js +13 -0
  29. package/dist/tools/userCRUD.js +77 -0
  30. package/dist/tools/workspaces.js +322 -0
  31. package/dist/util/explorerIcon.js +95 -0
  32. package/dist/util/mcp.js +28 -0
  33. package/dist/ws.js +113 -0
  34. package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
  35. package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
  36. package/docs/client-setup.md +174 -0
  37. package/docs/configuration-and-deployment.md +265 -0
  38. package/docs/edgeless-canvas-cookbook.md +226 -0
  39. package/docs/getting-started.md +229 -0
  40. package/docs/tool-reference.md +200 -0
  41. package/docs/workflow-recipes.md +147 -0
  42. package/package.json +118 -0
  43. package/tool-manifest.json +99 -0
@@ -0,0 +1,22 @@
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("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
+ }
@@ -0,0 +1,125 @@
1
+ import { z } from "zod";
2
+ import { receipt } from "../util/mcp.js";
3
+ import { connectWorkspaceSocket, joinWorkspace, wsUrlFromGraphQLEndpoint, } from "../ws.js";
4
+ import { docIconKey, folderIconKey, getExplorerIcon, normalizeIconInput, setExplorerIcon, } from "../util/explorerIcon.js";
5
+ /**
6
+ * Zod shape for the `icon` parameter shared by both setters. Accepts an emoji
7
+ * shorthand string, a full `{type:"emoji",unicode}` / `{type:"icon",name}`
8
+ * object, or `null` to clear the icon.
9
+ */
10
+ const iconSchema = z
11
+ .union([
12
+ z.string(),
13
+ z.object({ type: z.literal("emoji"), unicode: z.string() }),
14
+ z.object({ type: z.literal("icon"), name: z.string() }),
15
+ z.null(),
16
+ ])
17
+ .describe('Emoji shorthand ("๐Ÿงช"), a full object ({type:"emoji",unicode:"๐Ÿงช"} or ' +
18
+ '{type:"icon",name:"check"}), or null to remove the icon.');
19
+ /**
20
+ * Registers the explorer-icon tools: set/clear and read the Notion-style
21
+ * sidebar icon on a document or an organize folder. All four share the
22
+ * `explorerIcon` sub-doc helper so the storage model lives in one place.
23
+ */
24
+ export function registerIconTools(server, gql, defaults) {
25
+ function resolveWorkspaceId(workspaceId) {
26
+ const resolved = workspaceId || defaults.workspaceId;
27
+ if (!resolved) {
28
+ throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID.");
29
+ }
30
+ return resolved;
31
+ }
32
+ async function withSocket(workspaceId, fn) {
33
+ const wsUrl = wsUrlFromGraphQLEndpoint(gql.endpoint);
34
+ const socket = await connectWorkspaceSocket(wsUrl, gql.cookie, gql.bearer);
35
+ try {
36
+ await joinWorkspace(socket, workspaceId);
37
+ return await fn(socket);
38
+ }
39
+ finally {
40
+ socket.disconnect();
41
+ }
42
+ }
43
+ // โ”€โ”€โ”€ update_doc_icon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
44
+ const updateDocIconHandler = async (parsed) => {
45
+ const workspaceId = resolveWorkspaceId(parsed.workspaceId);
46
+ const icon = normalizeIconInput(parsed.icon);
47
+ const result = await withSocket(workspaceId, (socket) => setExplorerIcon(socket, workspaceId, docIconKey(parsed.docId), icon));
48
+ return receipt("doc.update_icon", {
49
+ workspaceId,
50
+ docId: parsed.docId,
51
+ icon: result.icon,
52
+ cleared: result.icon === null,
53
+ });
54
+ };
55
+ server.registerTool("update_doc_icon", {
56
+ title: "Update Document Icon",
57
+ description: "Set or clear the sidebar icon (the Notion-style emoji slot) on a document. " +
58
+ "Pass an emoji string, a full icon object, or null to remove it.",
59
+ inputSchema: {
60
+ workspaceId: z.string().optional(),
61
+ docId: z.string().describe("The document whose icon to update."),
62
+ icon: iconSchema,
63
+ },
64
+ }, updateDocIconHandler);
65
+ // โ”€โ”€โ”€ update_folder_icon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
66
+ const updateFolderIconHandler = async (parsed) => {
67
+ const workspaceId = resolveWorkspaceId(parsed.workspaceId);
68
+ const icon = normalizeIconInput(parsed.icon);
69
+ const result = await withSocket(workspaceId, (socket) => setExplorerIcon(socket, workspaceId, folderIconKey(parsed.folderId), icon));
70
+ return receipt("folder.update_icon", {
71
+ workspaceId,
72
+ folderId: parsed.folderId,
73
+ icon: result.icon,
74
+ cleared: result.icon === null,
75
+ });
76
+ };
77
+ server.registerTool("update_folder_icon", {
78
+ title: "Update Folder Icon",
79
+ description: "Set or clear the sidebar icon on an organize folder. " +
80
+ "Pass an emoji string, a full icon object, or null to remove it. Experimental.",
81
+ inputSchema: {
82
+ workspaceId: z.string().optional(),
83
+ folderId: z.string().describe("The organize folder whose icon to update."),
84
+ icon: iconSchema,
85
+ },
86
+ }, updateFolderIconHandler);
87
+ // โ”€โ”€โ”€ get_doc_icon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
88
+ const getDocIconHandler = async (parsed) => {
89
+ const workspaceId = resolveWorkspaceId(parsed.workspaceId);
90
+ const result = await withSocket(workspaceId, (socket) => getExplorerIcon(socket, workspaceId, docIconKey(parsed.docId)));
91
+ return receipt("doc.get_icon", {
92
+ workspaceId,
93
+ docId: parsed.docId,
94
+ icon: result.icon,
95
+ hasIcon: result.icon !== null,
96
+ });
97
+ };
98
+ server.registerTool("get_doc_icon", {
99
+ title: "Get Document Icon",
100
+ description: "Read the current sidebar icon of a document. Returns null when none is set.",
101
+ inputSchema: {
102
+ workspaceId: z.string().optional(),
103
+ docId: z.string().describe("The document whose icon to read."),
104
+ },
105
+ }, getDocIconHandler);
106
+ // โ”€โ”€โ”€ get_folder_icon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
+ const getFolderIconHandler = async (parsed) => {
108
+ const workspaceId = resolveWorkspaceId(parsed.workspaceId);
109
+ const result = await withSocket(workspaceId, (socket) => getExplorerIcon(socket, workspaceId, folderIconKey(parsed.folderId)));
110
+ return receipt("folder.get_icon", {
111
+ workspaceId,
112
+ folderId: parsed.folderId,
113
+ icon: result.icon,
114
+ hasIcon: result.icon !== null,
115
+ });
116
+ };
117
+ server.registerTool("get_folder_icon", {
118
+ title: "Get Folder Icon",
119
+ description: "Read the current sidebar icon of an organize folder. Returns null when none is set. Experimental.",
120
+ inputSchema: {
121
+ workspaceId: z.string().optional(),
122
+ folderId: z.string().describe("The organize folder whose icon to read."),
123
+ },
124
+ }, getFolderIconHandler);
125
+ }
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { text } from "../util/mcp.js";
3
+ export function registerNotificationTools(server, gql) {
4
+ // LIST NOTIFICATIONS
5
+ const listNotificationsHandler = async ({ first = 20, offset, after, unreadOnly = false }) => {
6
+ try {
7
+ const query = `
8
+ query GetNotifications($pagination: PaginationInput!) {
9
+ currentUser {
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
+ }
22
+ }
23
+ totalCount
24
+ pageInfo {
25
+ hasNextPage
26
+ endCursor
27
+ }
28
+ }
29
+ }
30
+ }
31
+ `;
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);
40
+ if (unreadOnly) {
41
+ notifications = notifications.filter((n) => !n.read);
42
+ }
43
+ return text(notifications);
44
+ }
45
+ catch (error) {
46
+ return text({ error: error.message });
47
+ }
48
+ };
49
+ server.registerTool("list_notifications", {
50
+ title: "List Notifications",
51
+ description: "Get user notifications.",
52
+ inputSchema: {
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"),
56
+ unreadOnly: z.boolean().optional().describe("Show only unread notifications")
57
+ }
58
+ }, listNotificationsHandler);
59
+ // MARK ALL NOTIFICATIONS READ
60
+ const readAllNotificationsHandler = async () => {
61
+ try {
62
+ const mutation = `
63
+ mutation ReadAllNotifications {
64
+ readAllNotifications
65
+ }
66
+ `;
67
+ const data = await gql.request(mutation);
68
+ return text({ success: data.readAllNotifications, message: "All notifications marked as read" });
69
+ }
70
+ catch (error) {
71
+ return text({ error: error.message });
72
+ }
73
+ };
74
+ server.registerTool("read_all_notifications", {
75
+ title: "Mark All Notifications Read",
76
+ description: "Mark all notifications as read.",
77
+ inputSchema: {}
78
+ }, readAllNotificationsHandler);
79
+ }