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 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
- [![Version](https://img.shields.io/badge/version-1.10.1-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
5
+ [![Version](https://img.shields.io/badge/version-1.11.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
6
6
  [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.17.2-green)](https://github.com/modelcontextprotocol/typescript-sdk)
7
7
  [![CI](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml)
8
8
  [![License](https://img.shields.io/badge/license-MIT-yellow)](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: 61 focused tools with WebSocket-based document editing
19
+ - Tools: 76 focused tools with WebSocket-based document editing
20
20
  - Status: Active
21
21
 
22
- > New in v1.10.1: Refreshed packaged docs and release metadata for the v1.10.x toolset, and tightened tag-publish validation with E2E coverage. No runtime or tool-behavior changes.
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
- - Database workflows: create database blocks, inspect schema, add columns and rows, and read or update cell values via MCP tools
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(_args) {
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
- const overwrite = await ask("Overwrite? [y/N] ");
318
- if (!/^[yY]$/.test(overwrite)) {
319
- console.error("Keeping existing config.");
320
- return;
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 (isSelfHosted) {
330
- const method = await ask("\nAuth method — [1] Email/password (recommended) [2] Paste API token: ");
331
- result = method === "2" ? await loginWithToken(baseUrl) : await loginWithEmail(baseUrl);
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
- result = await loginWithToken(baseUrl);
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
- ensureNoUnexpectedArgs(args, "status");
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 data = await gql(config.AFFINE_BASE_URL || "https://app.affine.pro", { token: config.AFFINE_API_TOKEN }, "query { currentUser { name email } workspaces { id } }");
356
- console.error(`User: ${data.currentUser.name} <${data.currentUser.email}>`);
357
- console.error(`Workspaces: ${data.workspaces.length}`);
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 asJson = parseFlag(args, "--json");
379
- const unexpectedArgs = args.filter((arg) => arg !== "--json");
380
- if (unexpectedArgs.length > 0) {
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 asJson = parseFlag(args, "--json");
409
- const unexpectedArgs = args.filter((arg) => arg !== "--json");
410
- if (unexpectedArgs.length > 0) {
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 gql(summary.baseUrl, auth, "query { currentUser { name email } workspaces { id } }");
529
+ const data = await inspectConnection(summary.baseUrl, auth);
451
530
  checks.push({
452
531
  name: "graphql-auth",
453
532
  ok: true,
454
- detail: `${data.currentUser.email} (${data.workspaces.length} workspace(s))`,
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 target = args[0];
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
- const includeEnv = parseFlag(args, "--env");
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 codex.`);
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
- registerWorkspaceTools(server, gql);
136
- registerDocTools(server, gql, { workspaceId: config.defaultWorkspaceId });
137
- registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
138
- registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
139
- registerUserTools(server, gql);
140
- registerUserCRUDTools(server, gql);
141
- registerAccessTokenTools(server, gql);
142
- registerBlobTools(server, gql);
143
- registerNotificationTools(server, gql);
144
- if (config.authMode !== "oauth") {
145
- registerAuthTools(server, gql, config.baseUrl);
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() {