affine-mcp-server 1.10.1 → 1.11.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 +66 -6
- package/dist/cli.js +145 -43
- package/dist/index.js +70 -13
- package/dist/markdown/parse.js +59 -18
- package/dist/tools/docs.js +74 -4
- package/dist/tools/organize.js +759 -0
- package/package.json +7 -2
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 (default) or HTTP (`/mcp`).
|
|
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,16 +16,17 @@ 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 (default) and optional HTTP (`/mcp`) for remote MCP deployments
|
|
18
18
|
- Auth: Token, Cookie, or Email/Password (priority order)
|
|
19
|
-
- Tools:
|
|
19
|
+
- Tools: 76 focused tools with WebSocket-based document editing
|
|
20
20
|
- Status: Active
|
|
21
21
|
|
|
22
|
-
> New in v1.
|
|
22
|
+
> New in v1.11.0: Added sidebar organize tools, configurable tool filtering, `delete_database_row`, and richer markdown import formatting for lists and table cells.
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
26
|
- Workspace: create (with initial doc), read, update, delete
|
|
27
27
|
- Documents: list/get/read/publish/revoke + create/append/replace/delete + markdown import/export + tags (WebSocket‑based)
|
|
28
|
-
-
|
|
28
|
+
- Sidebar data: collections, folders, and organize links for AFFiNE workspace trees
|
|
29
|
+
- Database workflows: create database blocks, inspect schema, add/update/delete rows, and read or update cell values via MCP tools
|
|
29
30
|
- Comments: full CRUD and resolve
|
|
30
31
|
- Version History: list
|
|
31
32
|
- Users & Tokens: current user, sign in, profile/settings, and personal access tokens
|
|
@@ -95,13 +96,17 @@ The MCP server will use these credentials automatically.
|
|
|
95
96
|
Other CLI commands:
|
|
96
97
|
- `affine-mcp --help` / `-h` / `help` — show command help
|
|
97
98
|
- `affine-mcp status` — show current config and test connection
|
|
99
|
+
- `affine-mcp status --json` — machine-readable status output
|
|
98
100
|
- `affine-mcp doctor` — run config and connectivity diagnostics
|
|
99
101
|
- `affine-mcp show-config` — print the effective config with secrets redacted
|
|
100
102
|
- `affine-mcp config-path` — print the config file path
|
|
101
|
-
- `affine-mcp snippet <claude|cursor|codex> [--env]` — print ready-to-paste client configuration snippets
|
|
103
|
+
- `affine-mcp snippet <claude|cursor|codex|all> [--env]` — print ready-to-paste client configuration snippets
|
|
102
104
|
- `affine-mcp logout` — remove stored credentials
|
|
103
105
|
- `affine-mcp --version` / `-v` / `version` — print the installed CLI version and exit
|
|
104
106
|
|
|
107
|
+
Non-interactive login helpers:
|
|
108
|
+
- `affine-mcp login --url <url> --token <token> --workspace-id <id> --force`
|
|
109
|
+
|
|
105
110
|
### Environment variables
|
|
106
111
|
|
|
107
112
|
You can also configure via environment variables (they override the config file):
|
|
@@ -109,6 +114,7 @@ You can also configure via environment variables (they override the config file)
|
|
|
109
114
|
- Required: `AFFINE_BASE_URL`
|
|
110
115
|
- Auth (choose one): `AFFINE_API_TOKEN` | `AFFINE_COOKIE` | `AFFINE_EMAIL` + `AFFINE_PASSWORD`
|
|
111
116
|
- Optional: `AFFINE_GRAPHQL_PATH` (default `/graphql`), `AFFINE_WORKSPACE_ID`, `AFFINE_LOGIN_AT_START` (set `sync` only when you must block startup)
|
|
117
|
+
- Tool filtering: `AFFINE_DISABLED_GROUPS`, `AFFINE_DISABLED_TOOLS` (see [Filtering Exposed Tools](#filtering-exposed-tools))
|
|
112
118
|
|
|
113
119
|
Authentication priority:
|
|
114
120
|
1) `AFFINE_API_TOKEN` → 2) `AFFINE_COOKIE` → 3) `AFFINE_EMAIL` + `AFFINE_PASSWORD`
|
|
@@ -193,6 +199,7 @@ Tips
|
|
|
193
199
|
- If your password contains `!` (zsh history expansion), wrap it in single quotes in shells or use the JSON config above.
|
|
194
200
|
- `affine-mcp doctor` is the fastest way to confirm that your saved config still works.
|
|
195
201
|
- `affine-mcp snippet claude --env` and `affine-mcp snippet codex --env` can generate ready-to-paste client setup from your current config.
|
|
202
|
+
- `affine-mcp snippet all --env` prints Claude, Cursor, and Codex setup in one shot.
|
|
196
203
|
|
|
197
204
|
### Codex CLI
|
|
198
205
|
|
|
@@ -354,6 +361,7 @@ Endpoints currently available:
|
|
|
354
361
|
- `/healthz` - HTTP liveness probe
|
|
355
362
|
- `/readyz` - HTTP readiness probe
|
|
356
363
|
|
|
364
|
+
|
|
357
365
|
## Available Tools
|
|
358
366
|
|
|
359
367
|
### Workspace
|
|
@@ -365,6 +373,23 @@ Endpoints currently available:
|
|
|
365
373
|
- `list_workspace_tree` – return the workspace document hierarchy as a tree
|
|
366
374
|
- `get_orphan_docs` – find documents that are not linked from any parent doc in the sidebar tree
|
|
367
375
|
|
|
376
|
+
### Organization
|
|
377
|
+
- `list_collections` – list workspace collections
|
|
378
|
+
- `get_collection` – get a collection by id
|
|
379
|
+
- `create_collection` – create a collection
|
|
380
|
+
- `update_collection` – rename a collection
|
|
381
|
+
- `delete_collection` – delete a collection
|
|
382
|
+
- `add_doc_to_collection` – add a document to a collection allow-list
|
|
383
|
+
- `remove_doc_from_collection` – remove a document from a collection allow-list
|
|
384
|
+
- `list_organize_nodes` – experimental organize/folder tree dump
|
|
385
|
+
- `create_folder` – experimental root or nested folder creation
|
|
386
|
+
- `rename_folder` – experimental folder rename
|
|
387
|
+
- `delete_folder` – experimental recursive folder delete
|
|
388
|
+
- `move_organize_node` – experimental folder/link move
|
|
389
|
+
- `add_organize_link` – experimental doc/tag/collection link under a folder
|
|
390
|
+
- `delete_organize_link` – experimental doc/tag/collection link delete
|
|
391
|
+
|
|
392
|
+
|
|
368
393
|
### Documents
|
|
369
394
|
- `list_docs` – list documents with pagination (includes `node.tags`)
|
|
370
395
|
- `list_tags` – list all tags in a workspace
|
|
@@ -391,6 +416,7 @@ Endpoints currently available:
|
|
|
391
416
|
- `batch_create_docs` – create up to 20 documents in a single call
|
|
392
417
|
- `add_database_column` – add a column to a database block (`rich-text`, `select`, `multi-select`, `number`, `checkbox`, `link`, `date`)
|
|
393
418
|
- `add_database_row` – add a row to a database block with values mapped by column name/ID (`title` / `Title` updates the built-in row title)
|
|
419
|
+
- `delete_database_row` – delete a row from a database block by row block id
|
|
394
420
|
- `read_database_columns` – read database schema metadata including column IDs/types, select options, and table view column mappings
|
|
395
421
|
- `read_database_cells` – read row titles plus decoded database cell values with optional row / column filters
|
|
396
422
|
- `update_database_cell` – update a single database cell or the built-in row title (`createOption` defaults to `true` for select fields)
|
|
@@ -419,6 +445,40 @@ Endpoints currently available:
|
|
|
419
445
|
### Blob Storage
|
|
420
446
|
- `upload_blob`, `delete_blob`, `cleanup_blobs`
|
|
421
447
|
|
|
448
|
+
## Filtering Exposed Tools
|
|
449
|
+
|
|
450
|
+
Optional environment variables to narrow the exposed surface.
|
|
451
|
+
|
|
452
|
+
### Group-level — `AFFINE_DISABLED_GROUPS`
|
|
453
|
+
|
|
454
|
+
| Group name | Tools included |
|
|
455
|
+
|---|---|
|
|
456
|
+
| `workspaces` | `list_workspaces`, `get_workspace`, `create_workspace`, `update_workspace`, `delete_workspace` |
|
|
457
|
+
| `docs` | `list_docs`, `read_doc`, `search_docs`, `create_doc`, `create_doc_from_markdown`, `create_doc_from_template`, `duplicate_doc`, `append_paragraph`, `append_block`, `append_markdown`, `replace_doc_with_markdown`, `delete_doc`, `publish_doc`, `revoke_doc`, `list_tags`, `list_docs_by_tag`, `create_tag`, `add_tag_to_doc`, `remove_tag_from_doc`, `list_workspace_tree`, `get_orphan_docs`, `list_children`, `update_doc_title`, `get_doc_by_title`, `get_docs_by_tag`, `list_backlinks`, `move_doc`, `batch_create_docs`, `cleanup_orphan_embeds`, `find_and_replace`, `add_database_column`, `add_database_row`, `delete_database_row`, `read_database_columns`, `read_database_cells`, `update_database_cell`, `update_database_row` |
|
|
458
|
+
| `comments` | `list_comments`, `create_comment`, `update_comment`, `delete_comment`, `resolve_comment` |
|
|
459
|
+
| `history` | `list_histories` |
|
|
460
|
+
| `organize` | `list_collections`, `get_collection`, `create_collection`, `update_collection`, `delete_collection`, `add_doc_to_collection`, `remove_doc_from_collection`, `list_organize_nodes`, `create_folder`, `rename_folder`, `delete_folder`, `move_organize_node`, `add_organize_link`, `delete_organize_link` |
|
|
461
|
+
| `users` | `current_user`, `sign_in`, `update_profile`, `update_settings` |
|
|
462
|
+
| `access_tokens` | `list_access_tokens`, `generate_access_token`, `revoke_access_token` |
|
|
463
|
+
| `blobs` | `upload_blob`, `delete_blob`, `cleanup_blobs` |
|
|
464
|
+
| `notifications` | `list_notifications`, `read_all_notifications` |
|
|
465
|
+
|
|
466
|
+
```json
|
|
467
|
+
"env": {
|
|
468
|
+
"AFFINE_DISABLED_GROUPS": "comments,history,blobs,users"
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Tool-level — `AFFINE_DISABLED_TOOLS`
|
|
473
|
+
|
|
474
|
+
Disables individual tools by exact name (comma-separated).
|
|
475
|
+
|
|
476
|
+
```json
|
|
477
|
+
"env": {
|
|
478
|
+
"AFFINE_DISABLED_TOOLS": "delete_workspace,delete_doc"
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
422
482
|
## Use Locally (clone)
|
|
423
483
|
|
|
424
484
|
```bash
|
|
@@ -447,7 +507,7 @@ npm run pack:check
|
|
|
447
507
|
- For full tool-surface verification, run `npm run test:comprehensive` (self-bootstraps a local Docker AFFiNE stack).
|
|
448
508
|
- For pre-provisioned environments, use `npm run test:comprehensive:raw`.
|
|
449
509
|
- For full environment verification, run `npm run test:e2e` (Docker + MCP + Playwright).
|
|
450
|
-
- Additional focused runners: `npm run test:db-create`, `npm run test:db-cells`, `npm run test:db-schema`, `npm run test:supporting-tools`, `npm run test:bearer`, `npm run test:http-email-password`, `npm run test:http-bearer`, `npm run test:oauth-http`, `npm run test:doc-discovery`, `npm run test:cli-version`, `npm run test:cli-commands`, `npm run test:playwright`.
|
|
510
|
+
- Additional focused runners: `npm run test:db-create`, `npm run test:db-cells`, `npm run test:db-schema`, `npm run test:supporting-tools`, `npm run test:organize`, `npm run test:bearer`, `npm run test:http-email-password`, `npm run test:http-bearer`, `npm run test:oauth-http`, `npm run test:doc-discovery`, `npm run test:cli-version`, `npm run test:cli-commands`, `npm run test:cli-live`, `npm run test:tool-filtering`, `npm run test:markdown-rich-text-import`, `npm run test:playwright`.
|
|
451
511
|
|
|
452
512
|
## Troubleshooting
|
|
453
513
|
|
package/dist/cli.js
CHANGED
|
@@ -105,6 +105,29 @@ async function gql(baseUrl, auth, query, variables) {
|
|
|
105
105
|
function parseFlag(args, ...flags) {
|
|
106
106
|
return args.some((arg) => flags.includes(arg));
|
|
107
107
|
}
|
|
108
|
+
function consumeOption(args, flag) {
|
|
109
|
+
const index = args.indexOf(flag);
|
|
110
|
+
if (index === -1)
|
|
111
|
+
return undefined;
|
|
112
|
+
const value = args[index + 1];
|
|
113
|
+
if (!value || value.startsWith("--")) {
|
|
114
|
+
throw new CliError(`Missing value for '${flag}'.`);
|
|
115
|
+
}
|
|
116
|
+
args.splice(index, 2);
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
function consumeFlags(args, ...flags) {
|
|
120
|
+
let found = false;
|
|
121
|
+
for (const flag of flags) {
|
|
122
|
+
let index = args.indexOf(flag);
|
|
123
|
+
while (index !== -1) {
|
|
124
|
+
args.splice(index, 1);
|
|
125
|
+
found = true;
|
|
126
|
+
index = args.indexOf(flag);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
108
131
|
function ensureNoUnexpectedArgs(args, command) {
|
|
109
132
|
if (args.length > 0) {
|
|
110
133
|
throw new CliError(`Unexpected arguments for '${command}': ${args.join(" ")}`);
|
|
@@ -178,6 +201,14 @@ async function resolveCliAuth(baseUrl) {
|
|
|
178
201
|
}
|
|
179
202
|
throw new CliError("No authentication configured. Run 'affine-mcp login' or set AFFINE_API_TOKEN.");
|
|
180
203
|
}
|
|
204
|
+
async function inspectConnection(baseUrl, auth) {
|
|
205
|
+
const data = await gql(baseUrl, auth, "query { currentUser { name email } workspaces { id } }");
|
|
206
|
+
return {
|
|
207
|
+
userName: data.currentUser.name,
|
|
208
|
+
userEmail: data.currentUser.email,
|
|
209
|
+
workspaceCount: data.workspaces.length,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
181
212
|
function printHelp(command) {
|
|
182
213
|
if (command) {
|
|
183
214
|
const definition = COMMANDS[command];
|
|
@@ -208,7 +239,11 @@ function printHelp(command) {
|
|
|
208
239
|
console.log(" affine-mcp --version");
|
|
209
240
|
console.log(" affine-mcp --help");
|
|
210
241
|
}
|
|
211
|
-
async function detectWorkspace(baseUrl, auth) {
|
|
242
|
+
async function detectWorkspace(baseUrl, auth, preferredWorkspaceId) {
|
|
243
|
+
if (preferredWorkspaceId) {
|
|
244
|
+
console.error(`Using workspace override: ${preferredWorkspaceId}`);
|
|
245
|
+
return preferredWorkspaceId;
|
|
246
|
+
}
|
|
212
247
|
console.error("Detecting workspaces...");
|
|
213
248
|
try {
|
|
214
249
|
const data = await gql(baseUrl, auth, `query {
|
|
@@ -306,7 +341,13 @@ async function loginWithToken(baseUrl) {
|
|
|
306
341
|
const workspaceId = await detectWorkspace(baseUrl, { token });
|
|
307
342
|
return { token, workspaceId };
|
|
308
343
|
}
|
|
309
|
-
async function login(
|
|
344
|
+
async function login(args) {
|
|
345
|
+
const parsedArgs = [...args];
|
|
346
|
+
const providedUrl = consumeOption(parsedArgs, "--url");
|
|
347
|
+
const providedToken = consumeOption(parsedArgs, "--token");
|
|
348
|
+
const providedWorkspaceId = consumeOption(parsedArgs, "--workspace-id");
|
|
349
|
+
const force = consumeFlags(parsedArgs, "--force", "-f");
|
|
350
|
+
ensureNoUnexpectedArgs(parsedArgs, "login");
|
|
310
351
|
console.error("Affine MCP Server — Login\n");
|
|
311
352
|
const existing = loadConfigFile();
|
|
312
353
|
if (existing.AFFINE_API_TOKEN) {
|
|
@@ -314,24 +355,53 @@ async function login(_args) {
|
|
|
314
355
|
console.error(` URL: ${existing.AFFINE_BASE_URL || "(default)"}`);
|
|
315
356
|
console.error(" Token: (set)");
|
|
316
357
|
console.error(` Workspace: ${existing.AFFINE_WORKSPACE_ID || "(none)"}\n`);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
358
|
+
if (!force) {
|
|
359
|
+
const overwrite = await ask("Overwrite? [y/N] ");
|
|
360
|
+
if (!/^[yY]$/.test(overwrite)) {
|
|
361
|
+
console.error("Keeping existing config.");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
console.error("");
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.error("Overwriting existing config (--force).\n");
|
|
321
368
|
}
|
|
322
|
-
console.error("");
|
|
323
369
|
}
|
|
324
370
|
const defaultUrl = "https://app.affine.pro";
|
|
325
|
-
const rawUrl = (await ask(`Affine URL [${defaultUrl}]: `)) || defaultUrl;
|
|
371
|
+
const rawUrl = providedUrl ?? ((await ask(`Affine URL [${defaultUrl}]: `)) || defaultUrl);
|
|
326
372
|
const baseUrl = validateBaseUrl(rawUrl);
|
|
327
|
-
const isSelfHosted = !baseUrl.includes("affine.pro");
|
|
328
373
|
let result;
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
374
|
+
if (providedToken) {
|
|
375
|
+
console.error("Testing provided token...");
|
|
376
|
+
try {
|
|
377
|
+
const info = await inspectConnection(baseUrl, { token: providedToken });
|
|
378
|
+
console.error(`✓ Authenticated as: ${info.userName} <${info.userEmail}>\n`);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
throw new CliError(`Authentication failed: ${err.message}`);
|
|
382
|
+
}
|
|
383
|
+
result = {
|
|
384
|
+
token: providedToken,
|
|
385
|
+
workspaceId: await detectWorkspace(baseUrl, { token: providedToken }, providedWorkspaceId),
|
|
386
|
+
};
|
|
332
387
|
}
|
|
333
388
|
else {
|
|
334
|
-
|
|
389
|
+
const isSelfHosted = !baseUrl.includes("affine.pro");
|
|
390
|
+
if (isSelfHosted) {
|
|
391
|
+
const method = await ask("\nAuth method — [1] Email/password (recommended) [2] Paste API token: ");
|
|
392
|
+
const loginResult = method === "2" ? await loginWithToken(baseUrl) : await loginWithEmail(baseUrl);
|
|
393
|
+
result = {
|
|
394
|
+
...loginResult,
|
|
395
|
+
workspaceId: providedWorkspaceId || loginResult.workspaceId,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const loginResult = await loginWithToken(baseUrl);
|
|
400
|
+
result = {
|
|
401
|
+
...loginResult,
|
|
402
|
+
workspaceId: providedWorkspaceId || loginResult.workspaceId,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
335
405
|
}
|
|
336
406
|
writeConfigFile({
|
|
337
407
|
AFFINE_BASE_URL: baseUrl,
|
|
@@ -342,19 +412,32 @@ async function login(_args) {
|
|
|
342
412
|
console.error("The MCP server will use these credentials automatically.");
|
|
343
413
|
}
|
|
344
414
|
async function status(args) {
|
|
345
|
-
|
|
415
|
+
const parsedArgs = [...args];
|
|
416
|
+
const asJson = consumeFlags(parsedArgs, "--json");
|
|
417
|
+
ensureNoUnexpectedArgs(parsedArgs, "status");
|
|
346
418
|
const config = loadConfigFile();
|
|
347
419
|
if (!config.AFFINE_API_TOKEN) {
|
|
348
420
|
throw new CliError("Not logged in. Run: affine-mcp login");
|
|
349
421
|
}
|
|
350
|
-
console.error(`Config: ${CONFIG_FILE}`);
|
|
351
|
-
console.error(`URL: ${config.AFFINE_BASE_URL || "(default)"}`);
|
|
352
|
-
console.error("Token: (set)");
|
|
353
|
-
console.error(`Workspace: ${config.AFFINE_WORKSPACE_ID || "(none)"}\n`);
|
|
354
422
|
try {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
423
|
+
const inspection = await inspectConnection(config.AFFINE_BASE_URL || "https://app.affine.pro", { token: config.AFFINE_API_TOKEN });
|
|
424
|
+
if (asJson) {
|
|
425
|
+
console.log(JSON.stringify({
|
|
426
|
+
configFile: CONFIG_FILE,
|
|
427
|
+
baseUrl: config.AFFINE_BASE_URL || "https://app.affine.pro",
|
|
428
|
+
workspaceId: config.AFFINE_WORKSPACE_ID || null,
|
|
429
|
+
userName: inspection.userName,
|
|
430
|
+
userEmail: inspection.userEmail,
|
|
431
|
+
workspaceCount: inspection.workspaceCount,
|
|
432
|
+
}, null, 2));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
console.error(`Config: ${CONFIG_FILE}`);
|
|
436
|
+
console.error(`URL: ${config.AFFINE_BASE_URL || "(default)"}`);
|
|
437
|
+
console.error("Token: (set)");
|
|
438
|
+
console.error(`Workspace: ${config.AFFINE_WORKSPACE_ID || "(none)"}\n`);
|
|
439
|
+
console.error(`User: ${inspection.userName} <${inspection.userEmail}>`);
|
|
440
|
+
console.error(`Workspaces: ${inspection.workspaceCount}`);
|
|
358
441
|
}
|
|
359
442
|
catch (err) {
|
|
360
443
|
throw new CliError(`Connection failed: ${err.message}`);
|
|
@@ -375,11 +458,9 @@ function configPath(args) {
|
|
|
375
458
|
console.log(CONFIG_FILE);
|
|
376
459
|
}
|
|
377
460
|
function showConfig(args) {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
throw new CliError(`Unexpected arguments for 'show-config': ${unexpectedArgs.join(" ")}`);
|
|
382
|
-
}
|
|
461
|
+
const parsedArgs = [...args];
|
|
462
|
+
const asJson = consumeFlags(parsedArgs, "--json");
|
|
463
|
+
ensureNoUnexpectedArgs(parsedArgs, "show-config");
|
|
383
464
|
const summary = buildEffectiveConfigSummary();
|
|
384
465
|
if (asJson) {
|
|
385
466
|
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -405,11 +486,9 @@ function showConfig(args) {
|
|
|
405
486
|
console.log(`OAuth scopes: ${summary.oauthScopes.join(", ")} (${summary.sources.oauthScopes})`);
|
|
406
487
|
}
|
|
407
488
|
async function doctor(args) {
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
throw new CliError(`Unexpected arguments for 'doctor': ${unexpectedArgs.join(" ")}`);
|
|
412
|
-
}
|
|
489
|
+
const parsedArgs = [...args];
|
|
490
|
+
const asJson = consumeFlags(parsedArgs, "--json");
|
|
491
|
+
ensureNoUnexpectedArgs(parsedArgs, "doctor");
|
|
413
492
|
const summary = buildEffectiveConfigSummary();
|
|
414
493
|
const checks = [];
|
|
415
494
|
checks.push({
|
|
@@ -447,11 +526,11 @@ async function doctor(args) {
|
|
|
447
526
|
clearTimeout(healthTimer);
|
|
448
527
|
}
|
|
449
528
|
try {
|
|
450
|
-
const data = await
|
|
529
|
+
const data = await inspectConnection(summary.baseUrl, auth);
|
|
451
530
|
checks.push({
|
|
452
531
|
name: "graphql-auth",
|
|
453
532
|
ok: true,
|
|
454
|
-
detail: `${data.
|
|
533
|
+
detail: `${data.userEmail} (${data.workspaceCount} workspace(s))`,
|
|
455
534
|
});
|
|
456
535
|
}
|
|
457
536
|
catch (err) {
|
|
@@ -522,16 +601,39 @@ function getSnippetEnv() {
|
|
|
522
601
|
return env;
|
|
523
602
|
}
|
|
524
603
|
function snippet(args) {
|
|
525
|
-
const
|
|
604
|
+
const parsedArgs = [...args];
|
|
605
|
+
const includeEnv = consumeFlags(parsedArgs, "--env");
|
|
606
|
+
const target = parsedArgs[0];
|
|
526
607
|
if (!target) {
|
|
527
608
|
throw new CliError("Usage: affine-mcp snippet <claude|cursor|codex> [--env]");
|
|
528
609
|
}
|
|
529
|
-
|
|
530
|
-
const unexpectedArgs = args.slice(1).filter((arg) => arg !== "--env");
|
|
531
|
-
if (unexpectedArgs.length > 0) {
|
|
532
|
-
throw new CliError(`Unexpected arguments for 'snippet': ${unexpectedArgs.join(" ")}`);
|
|
533
|
-
}
|
|
610
|
+
ensureNoUnexpectedArgs(parsedArgs.slice(1), "snippet");
|
|
534
611
|
const env = includeEnv ? getSnippetEnv() : undefined;
|
|
612
|
+
if (target === "all") {
|
|
613
|
+
const payload = {
|
|
614
|
+
claude: {
|
|
615
|
+
mcpServers: {
|
|
616
|
+
affine: {
|
|
617
|
+
command: "affine-mcp",
|
|
618
|
+
...(env && Object.keys(env).length > 0 ? { env } : {}),
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
cursor: {
|
|
623
|
+
mcpServers: {
|
|
624
|
+
affine: {
|
|
625
|
+
command: "affine-mcp",
|
|
626
|
+
...(env && Object.keys(env).length > 0 ? { env } : {}),
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
codex: env && Object.keys(env).length > 0
|
|
631
|
+
? `codex mcp add affine ${Object.entries(env).map(([key, value]) => `--env ${key}=${JSON.stringify(value)}`).join(" ")} -- affine-mcp`
|
|
632
|
+
: "codex mcp add affine -- affine-mcp",
|
|
633
|
+
};
|
|
634
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
535
637
|
if (target === "claude" || target === "cursor") {
|
|
536
638
|
const payload = {
|
|
537
639
|
mcpServers: {
|
|
@@ -555,7 +657,7 @@ function snippet(args) {
|
|
|
555
657
|
console.log(`codex mcp add affine ${envArgs} -- affine-mcp`);
|
|
556
658
|
return;
|
|
557
659
|
}
|
|
558
|
-
throw new CliError(`Unknown snippet target '${target}'. Expected claude, cursor, or
|
|
660
|
+
throw new CliError(`Unknown snippet target '${target}'. Expected claude, cursor, codex, or all.`);
|
|
559
661
|
}
|
|
560
662
|
function help(args) {
|
|
561
663
|
if (args.length > 1) {
|
|
@@ -571,12 +673,12 @@ const COMMANDS = {
|
|
|
571
673
|
},
|
|
572
674
|
login: {
|
|
573
675
|
summary: "Interactive login and config bootstrap",
|
|
574
|
-
usage: "affine-mcp login",
|
|
676
|
+
usage: "affine-mcp login [--url <url>] [--token <token>] [--workspace-id <id>] [--force]",
|
|
575
677
|
handler: login,
|
|
576
678
|
},
|
|
577
679
|
status: {
|
|
578
680
|
summary: "Test the saved config and print current user info",
|
|
579
|
-
usage: "affine-mcp status",
|
|
681
|
+
usage: "affine-mcp status [--json]",
|
|
580
682
|
handler: status,
|
|
581
683
|
},
|
|
582
684
|
logout: {
|
|
@@ -601,7 +703,7 @@ const COMMANDS = {
|
|
|
601
703
|
},
|
|
602
704
|
snippet: {
|
|
603
705
|
summary: "Print ready-to-paste Claude/Cursor/Codex snippets",
|
|
604
|
-
usage: "affine-mcp snippet <claude|cursor|codex> [--env]",
|
|
706
|
+
usage: "affine-mcp snippet <claude|cursor|codex|all> [--env]",
|
|
605
707
|
handler: snippet,
|
|
606
708
|
},
|
|
607
709
|
};
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,11 @@ import { registerBlobTools } from "./tools/blobStorage.js";
|
|
|
13
13
|
import { registerNotificationTools } from "./tools/notifications.js";
|
|
14
14
|
import { loginWithPassword } from "./auth.js";
|
|
15
15
|
import { registerAuthTools } from "./tools/auth.js";
|
|
16
|
+
import { registerOrganizeTools } from "./tools/organize.js";
|
|
16
17
|
import { runCli } from "./cli.js";
|
|
17
18
|
import { startHttpMcpServer } from "./sse.js";
|
|
19
|
+
import { existsSync } from "fs";
|
|
20
|
+
import { CONFIG_FILE } from "./config.js";
|
|
18
21
|
// CLI commands: affine-mcp login|status|logout|version
|
|
19
22
|
const rawArgs = process.argv.slice(2);
|
|
20
23
|
const cliArgs = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
|
|
@@ -40,9 +43,22 @@ if (subcommand) {
|
|
|
40
43
|
const config = loadConfig();
|
|
41
44
|
const transportMode = (process.env.MCP_TRANSPORT || "stdio").toLowerCase();
|
|
42
45
|
const useHttpTransport = transportMode === "sse" || transportMode === "http" || transportMode === "streamable";
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Tool filtering — parsed once at module load (not per-session in HTTP mode)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
const KNOWN_GROUPS = new Set([
|
|
50
|
+
"workspaces", "docs", "comments", "history", "organize",
|
|
51
|
+
"users", "access_tokens", "blobs", "notifications",
|
|
52
|
+
]);
|
|
53
|
+
const DISABLED_GROUPS = new Set((process.env.AFFINE_DISABLED_GROUPS || "")
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((s) => s.trim().toLowerCase())
|
|
56
|
+
.filter(Boolean));
|
|
57
|
+
const DISABLED_TOOLS = new Set((process.env.AFFINE_DISABLED_TOOLS || "")
|
|
58
|
+
.split(",")
|
|
59
|
+
.map((s) => s.trim())
|
|
60
|
+
.filter(Boolean));
|
|
43
61
|
// Startup diagnostics (visible in Claude Code MCP server logs via stderr)
|
|
44
|
-
import { existsSync } from "fs";
|
|
45
|
-
import { CONFIG_FILE } from "./config.js";
|
|
46
62
|
console.error(`[affine-mcp] Config: ${CONFIG_FILE} (${existsSync(CONFIG_FILE) ? 'found' : 'missing'})`);
|
|
47
63
|
console.error(`[affine-mcp] Endpoint: ${config.baseUrl}${config.graphqlPath}`);
|
|
48
64
|
const hasAuth = !!(config.apiToken || config.cookie || (config.email && config.password));
|
|
@@ -54,6 +70,13 @@ if (hasAuth && config.baseUrl.startsWith("http://")
|
|
|
54
70
|
console.error("WARNING: Credentials configured over plain HTTP. Use HTTPS for remote servers.");
|
|
55
71
|
}
|
|
56
72
|
console.error(`[affine-mcp] Workspace: ${config.defaultWorkspaceId ? 'set' : '(none)'}`);
|
|
73
|
+
// Warn about unknown group names (likely typos) before they silently do nothing
|
|
74
|
+
for (const g of DISABLED_GROUPS) {
|
|
75
|
+
if (!KNOWN_GROUPS.has(g)) {
|
|
76
|
+
console.error(`[affine-mcp] WARNING: Unknown group "${g}" in AFFINE_DISABLED_GROUPS — ` +
|
|
77
|
+
`valid groups: ${[...KNOWN_GROUPS].join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
57
80
|
if (config.authMode === "oauth" && !useHttpTransport) {
|
|
58
81
|
throw new Error("AFFINE_MCP_AUTH_MODE=oauth requires MCP_TRANSPORT=http (or streamable/sse).");
|
|
59
82
|
}
|
|
@@ -132,18 +155,52 @@ async function buildServer() {
|
|
|
132
155
|
console.error("WARNING: No authentication configured. Some operations may fail.");
|
|
133
156
|
console.error("Set AFFINE_API_TOKEN or run: affine-mcp login");
|
|
134
157
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Per-tool blacklist: patch registerTool on this server instance so individual
|
|
160
|
+
// tools in AFFINE_DISABLED_TOOLS are silently skipped during registration.
|
|
161
|
+
// All tool files use server.registerTool exclusively — no need to patch server.tool.
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
if (DISABLED_TOOLS.size > 0) {
|
|
164
|
+
const originalRegisterTool = server.registerTool?.bind(server);
|
|
165
|
+
if (typeof originalRegisterTool !== "function") {
|
|
166
|
+
console.error("[affine-mcp] WARNING: server.registerTool not found — " +
|
|
167
|
+
"AFFINE_DISABLED_TOOLS will have no effect. " +
|
|
168
|
+
"The MCP SDK API may have changed.");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
server.registerTool = (name, options, handler) => {
|
|
172
|
+
if (DISABLED_TOOLS.has(name))
|
|
173
|
+
return;
|
|
174
|
+
return originalRegisterTool(name, options, handler);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Log filters directly from environment variables
|
|
179
|
+
console.error(`[affine-mcp] Disabled groups: ${process.env.AFFINE_DISABLED_GROUPS || "(none)"}`);
|
|
180
|
+
console.error(`[affine-mcp] Disabled tools: ${process.env.AFFINE_DISABLED_TOOLS || "(none)"}`);
|
|
181
|
+
if (!DISABLED_GROUPS.has("workspaces"))
|
|
182
|
+
registerWorkspaceTools(server, gql);
|
|
183
|
+
if (!DISABLED_GROUPS.has("docs"))
|
|
184
|
+
registerDocTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
185
|
+
if (!DISABLED_GROUPS.has("comments"))
|
|
186
|
+
registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
187
|
+
if (!DISABLED_GROUPS.has("history"))
|
|
188
|
+
registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
189
|
+
if (!DISABLED_GROUPS.has("organize"))
|
|
190
|
+
registerOrganizeTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
191
|
+
if (!DISABLED_GROUPS.has("users")) {
|
|
192
|
+
registerUserTools(server, gql);
|
|
193
|
+
registerUserCRUDTools(server, gql);
|
|
194
|
+
if (config.authMode !== "oauth") {
|
|
195
|
+
registerAuthTools(server, gql, config.baseUrl);
|
|
196
|
+
}
|
|
146
197
|
}
|
|
198
|
+
if (!DISABLED_GROUPS.has("access_tokens"))
|
|
199
|
+
registerAccessTokenTools(server, gql);
|
|
200
|
+
if (!DISABLED_GROUPS.has("blobs"))
|
|
201
|
+
registerBlobTools(server, gql);
|
|
202
|
+
if (!DISABLED_GROUPS.has("notifications"))
|
|
203
|
+
registerNotificationTools(server, gql);
|
|
147
204
|
return server;
|
|
148
205
|
}
|
|
149
206
|
async function start() {
|