affine-mcp-server 1.3.0 → 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 +56 -4
- package/dist/index.js +1 -1
- package/dist/tools/docs.js +123 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted or cloud). It exposes AFFiNE workspaces and documents to AI assistants over stdio.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/dawncr0w/affine-mcp-server/releases)
|
|
6
6
|
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
7
7
|
[](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
@@ -16,15 +16,15 @@ A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted
|
|
|
16
16
|
- Purpose: Manage AFFiNE workspaces and documents through MCP
|
|
17
17
|
- Transport: stdio only (Claude Desktop / Codex compatible)
|
|
18
18
|
- Auth: Token, Cookie, or Email/Password (priority order)
|
|
19
|
-
- Tools:
|
|
19
|
+
- Tools: 32 focused tools with WebSocket-based document editing
|
|
20
20
|
- Status: Active
|
|
21
21
|
|
|
22
|
-
> New in v1.
|
|
22
|
+
> New in v1.4.0: Added `read_doc` for document content snapshots (blocks + plain text), plus Cursor setup/troubleshooting guidance.
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
26
|
- Workspace: create (with initial doc), read, update, delete
|
|
27
|
-
- Documents: list/get/publish/revoke + create/append paragraph/delete (WebSocket‑based)
|
|
27
|
+
- Documents: list/get/read/publish/revoke + create/append paragraph/delete (WebSocket‑based)
|
|
28
28
|
- Comments: full CRUD and resolve
|
|
29
29
|
- Version History: list
|
|
30
30
|
- Users & Tokens: current user, sign in, profile/settings, and personal access tokens
|
|
@@ -112,6 +112,43 @@ Notes
|
|
|
112
112
|
- Command: `affine-mcp`
|
|
113
113
|
- Environment: `AFFINE_BASE_URL` + one auth method (`AFFINE_API_TOKEN` | `AFFINE_COOKIE` | `AFFINE_EMAIL`/`AFFINE_PASSWORD`)
|
|
114
114
|
|
|
115
|
+
### Cursor
|
|
116
|
+
|
|
117
|
+
Cursor also supports MCP over stdio with `mcp.json`.
|
|
118
|
+
|
|
119
|
+
Project-local (`.cursor/mcp.json`) example:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"affine": {
|
|
125
|
+
"command": "affine-mcp",
|
|
126
|
+
"env": {
|
|
127
|
+
"AFFINE_BASE_URL": "https://your-affine-instance.com",
|
|
128
|
+
"AFFINE_API_TOKEN": "apt_xxx"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If you prefer `npx`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"affine": {
|
|
141
|
+
"command": "npx",
|
|
142
|
+
"args": ["-y", "-p", "affine-mcp-server", "affine-mcp"],
|
|
143
|
+
"env": {
|
|
144
|
+
"AFFINE_BASE_URL": "https://your-affine-instance.com",
|
|
145
|
+
"AFFINE_API_TOKEN": "apt_xxx"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
115
152
|
## Available Tools
|
|
116
153
|
|
|
117
154
|
### Workspace
|
|
@@ -124,6 +161,7 @@ Notes
|
|
|
124
161
|
### Documents
|
|
125
162
|
- `list_docs` – list documents with pagination
|
|
126
163
|
- `get_doc` – get document metadata
|
|
164
|
+
- `read_doc` – read document block content and plain text snapshot (WebSocket)
|
|
127
165
|
- `publish_doc` – make document public
|
|
128
166
|
- `revoke_doc` – revoke public access
|
|
129
167
|
- `create_doc` – create a new document (WebSocket)
|
|
@@ -186,6 +224,15 @@ Connection
|
|
|
186
224
|
- GraphQL endpoint default is `/graphql`
|
|
187
225
|
- Check firewall/proxy rules; verify CORS if self‑hosted
|
|
188
226
|
|
|
227
|
+
Method not found
|
|
228
|
+
- MCP tool names (for example `list_workspaces`) are not JSON-RPC top-level method names.
|
|
229
|
+
- Use an MCP client (`tools/list`, `tools/call`) instead of sending direct JSON-RPC calls like `{\"method\":\"list_workspaces\"}`.
|
|
230
|
+
- From v1.3.0, only canonical tool names are exposed (legacy `affine_*` aliases were removed).
|
|
231
|
+
|
|
232
|
+
Workspace visibility
|
|
233
|
+
- This MCP server can access server-backed workspaces only (AFFiNE cloud/self-hosted).
|
|
234
|
+
- Browser local-storage workspaces are client-side data, so they are not visible via server GraphQL/WebSocket APIs.
|
|
235
|
+
|
|
189
236
|
## Security Considerations
|
|
190
237
|
|
|
191
238
|
- Never commit `.env` with secrets
|
|
@@ -196,6 +243,11 @@ Connection
|
|
|
196
243
|
|
|
197
244
|
## Version History
|
|
198
245
|
|
|
246
|
+
### 1.4.0 (2026‑02‑13)
|
|
247
|
+
- Added `read_doc` for reading document block snapshot + plain text
|
|
248
|
+
- Added Cursor setup examples and troubleshooting notes for JSON-RPC method usage
|
|
249
|
+
- Added explicit local-storage workspace limitation notes
|
|
250
|
+
|
|
199
251
|
### 1.3.0 (2026‑02‑13)
|
|
200
252
|
- Added `append_block` for slash-command style editing (`heading/list/todo/code/divider/quote`)
|
|
201
253
|
- Tool surface simplified to 31 canonical tools (duplicate aliases removed)
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { loginWithPassword } from "./auth.js";
|
|
|
15
15
|
import { registerAuthTools } from "./tools/auth.js";
|
|
16
16
|
const config = loadConfig();
|
|
17
17
|
async function buildServer() {
|
|
18
|
-
const server = new McpServer({ name: "affine-mcp", version: "1.
|
|
18
|
+
const server = new McpServer({ name: "affine-mcp", version: "1.4.0" });
|
|
19
19
|
// Initialize GraphQL client with authentication
|
|
20
20
|
const gql = new GraphQLClient({
|
|
21
21
|
endpoint: `${config.baseUrl}${config.graphqlPath}`,
|
package/dist/tools/docs.js
CHANGED
|
@@ -49,6 +49,32 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
49
49
|
}
|
|
50
50
|
return yText;
|
|
51
51
|
}
|
|
52
|
+
function asText(value) {
|
|
53
|
+
if (value instanceof Y.Text)
|
|
54
|
+
return value.toString();
|
|
55
|
+
if (typeof value === "string")
|
|
56
|
+
return value;
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
function childIdsFrom(value) {
|
|
60
|
+
if (!(value instanceof Y.Array))
|
|
61
|
+
return [];
|
|
62
|
+
const childIds = [];
|
|
63
|
+
value.forEach((entry) => {
|
|
64
|
+
if (typeof entry === "string") {
|
|
65
|
+
childIds.push(entry);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(entry)) {
|
|
69
|
+
for (const child of entry) {
|
|
70
|
+
if (typeof child === "string") {
|
|
71
|
+
childIds.push(child);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return childIds;
|
|
77
|
+
}
|
|
52
78
|
function setSysFields(block, blockId, flavour) {
|
|
53
79
|
block.set("sys:id", blockId);
|
|
54
80
|
block.set("sys:flavour", flavour);
|
|
@@ -228,6 +254,103 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
228
254
|
docId: DocId
|
|
229
255
|
}
|
|
230
256
|
}, getDocHandler);
|
|
257
|
+
const readDocHandler = async (parsed) => {
|
|
258
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
259
|
+
if (!workspaceId) {
|
|
260
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
261
|
+
}
|
|
262
|
+
const { endpoint, cookie } = await getCookieAndEndpoint();
|
|
263
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
264
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie);
|
|
265
|
+
try {
|
|
266
|
+
await joinWorkspace(socket, workspaceId);
|
|
267
|
+
const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
|
|
268
|
+
if (!snapshot.missing) {
|
|
269
|
+
return text({
|
|
270
|
+
docId: parsed.docId,
|
|
271
|
+
title: null,
|
|
272
|
+
exists: false,
|
|
273
|
+
blockCount: 0,
|
|
274
|
+
blocks: [],
|
|
275
|
+
plainText: "",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
const doc = new Y.Doc();
|
|
279
|
+
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
280
|
+
const blocks = doc.getMap("blocks");
|
|
281
|
+
const pageId = findBlockIdByFlavour(blocks, "affine:page");
|
|
282
|
+
const noteId = findBlockIdByFlavour(blocks, "affine:note");
|
|
283
|
+
const visited = new Set();
|
|
284
|
+
const blockRows = [];
|
|
285
|
+
const plainTextLines = [];
|
|
286
|
+
let title = "";
|
|
287
|
+
const visit = (blockId) => {
|
|
288
|
+
if (visited.has(blockId))
|
|
289
|
+
return;
|
|
290
|
+
visited.add(blockId);
|
|
291
|
+
const raw = blocks.get(blockId);
|
|
292
|
+
if (!(raw instanceof Y.Map))
|
|
293
|
+
return;
|
|
294
|
+
const flavour = raw.get("sys:flavour");
|
|
295
|
+
const parentId = raw.get("sys:parent");
|
|
296
|
+
const type = raw.get("prop:type");
|
|
297
|
+
const textValue = asText(raw.get("prop:text"));
|
|
298
|
+
const language = raw.get("prop:language");
|
|
299
|
+
const checked = raw.get("prop:checked");
|
|
300
|
+
const childIds = childIdsFrom(raw.get("sys:children"));
|
|
301
|
+
if (flavour === "affine:page") {
|
|
302
|
+
title = asText(raw.get("prop:title")) || title;
|
|
303
|
+
}
|
|
304
|
+
if (textValue.length > 0) {
|
|
305
|
+
plainTextLines.push(textValue);
|
|
306
|
+
}
|
|
307
|
+
blockRows.push({
|
|
308
|
+
id: blockId,
|
|
309
|
+
parentId: typeof parentId === "string" ? parentId : null,
|
|
310
|
+
flavour: typeof flavour === "string" ? flavour : null,
|
|
311
|
+
type: typeof type === "string" ? type : null,
|
|
312
|
+
text: textValue.length > 0 ? textValue : null,
|
|
313
|
+
checked: typeof checked === "boolean" ? checked : null,
|
|
314
|
+
language: typeof language === "string" ? language : null,
|
|
315
|
+
childIds,
|
|
316
|
+
});
|
|
317
|
+
for (const childId of childIds) {
|
|
318
|
+
visit(childId);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
if (pageId) {
|
|
322
|
+
visit(pageId);
|
|
323
|
+
}
|
|
324
|
+
else if (noteId) {
|
|
325
|
+
visit(noteId);
|
|
326
|
+
}
|
|
327
|
+
for (const [id] of blocks) {
|
|
328
|
+
const blockId = String(id);
|
|
329
|
+
if (!visited.has(blockId)) {
|
|
330
|
+
visit(blockId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return text({
|
|
334
|
+
docId: parsed.docId,
|
|
335
|
+
title: title || null,
|
|
336
|
+
exists: true,
|
|
337
|
+
blockCount: blockRows.length,
|
|
338
|
+
blocks: blockRows,
|
|
339
|
+
plainText: plainTextLines.join("\n"),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
socket.disconnect();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
server.registerTool("read_doc", {
|
|
347
|
+
title: "Read Document Content",
|
|
348
|
+
description: "Read document block content via WebSocket snapshot (blocks + plain text).",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
workspaceId: WorkspaceId.optional(),
|
|
351
|
+
docId: DocId,
|
|
352
|
+
},
|
|
353
|
+
}, readDocHandler);
|
|
231
354
|
const publishDocHandler = async (parsed) => {
|
|
232
355
|
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
233
356
|
if (!workspaceId) {
|
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.",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"form-data": "^4.0.4",
|
|
56
56
|
"node-fetch": "^3.3.2",
|
|
57
57
|
"socket.io-client": "^4.8.1",
|
|
58
|
-
"undici": "^
|
|
58
|
+
"undici": "^7.21.0",
|
|
59
59
|
"yjs": "^13.6.27",
|
|
60
60
|
"zod": "^3.23.8"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@types/node": "^
|
|
63
|
+
"@types/node": "^25.2.3",
|
|
64
64
|
"tsx": "^4.16.2",
|
|
65
65
|
"typescript": "^5.5.4"
|
|
66
66
|
}
|