affine-mcp-server 1.10.0 → 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 +84 -11
- 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
|
|
|
@@ -314,9 +321,9 @@ export PORT=3000
|
|
|
314
321
|
npm run start:http
|
|
315
322
|
```
|
|
316
323
|
|
|
317
|
-
Notes for
|
|
324
|
+
Notes for OAuth mode:
|
|
318
325
|
- use HTTPS for non-local deployments
|
|
319
|
-
- `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS=true` is rejected in
|
|
326
|
+
- `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS=true` is rejected in OAuth mode
|
|
320
327
|
- tokens are validated against the issuer discovery metadata and JWKS
|
|
321
328
|
- the protected resource metadata is also served at `/.well-known/oauth-protected-resource/mcp` for path-specific discovery
|
|
322
329
|
- `GET /healthz` and `GET /readyz` are available for deployment diagnostics
|
|
@@ -343,8 +350,8 @@ Render / Railway / VPS (public endpoint):
|
|
|
343
350
|
- `AFFINE_MCP_AUTH_MODE=bearer` or `oauth`
|
|
344
351
|
- `AFFINE_MCP_HTTP_HOST=0.0.0.0`
|
|
345
352
|
- `AFFINE_MCP_HTTP_TOKEN=<strong-token>` (bearer mode)
|
|
346
|
-
- `AFFINE_MCP_PUBLIC_BASE_URL=<public base URL>` (
|
|
347
|
-
- `AFFINE_OAUTH_ISSUER_URL=<issuer URL>` (
|
|
353
|
+
- `AFFINE_MCP_PUBLIC_BASE_URL=<public base URL>` (OAuth mode)
|
|
354
|
+
- `AFFINE_OAUTH_ISSUER_URL=<issuer URL>` (OAuth mode)
|
|
348
355
|
- `AFFINE_MCP_HTTP_ALLOWED_ORIGINS=<your client origin(s)>`
|
|
349
356
|
|
|
350
357
|
Endpoints currently available:
|
|
@@ -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
|
|
@@ -362,32 +370,63 @@ Endpoints currently available:
|
|
|
362
370
|
- `create_workspace` – create workspace with initial document
|
|
363
371
|
- `update_workspace` – update workspace settings
|
|
364
372
|
- `delete_workspace` – delete workspace permanently
|
|
373
|
+
- `list_workspace_tree` – return the workspace document hierarchy as a tree
|
|
374
|
+
- `get_orphan_docs` – find documents that are not linked from any parent doc in the sidebar tree
|
|
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
|
+
|
|
365
392
|
|
|
366
393
|
### Documents
|
|
367
394
|
- `list_docs` – list documents with pagination (includes `node.tags`)
|
|
368
395
|
- `list_tags` – list all tags in a workspace
|
|
369
396
|
- `search_docs` – fast title search with substring/prefix/exact matching, optional tag filtering, and updatedAt sorting
|
|
370
|
-
- `list_docs_by_tag` – list documents
|
|
397
|
+
- `list_docs_by_tag` – list documents that contain the requested tag
|
|
398
|
+
- `get_docs_by_tag` – discover documents by case-insensitive tag substring and return `availableTags` when nothing matches
|
|
371
399
|
- `get_doc` – get document metadata
|
|
400
|
+
- `get_doc_by_title` – find a document by title and return its Markdown content
|
|
372
401
|
- `read_doc` – read document block content and plain text snapshot (WebSocket)
|
|
373
402
|
- `export_doc_markdown` – export document content as markdown
|
|
374
403
|
- `publish_doc` – make document public
|
|
375
404
|
- `revoke_doc` – revoke public access
|
|
376
405
|
- `create_doc` – create a new document (WebSocket)
|
|
377
406
|
- `create_doc_from_markdown` – create a document from markdown content
|
|
407
|
+
- `create_doc_from_template` – clone a template doc, substitute `{{variables}}`, and optionally link it under a parent doc
|
|
408
|
+
- `duplicate_doc` – clone a document into a new doc, optionally under a parent doc
|
|
378
409
|
- `create_tag` – create a reusable workspace-level tag
|
|
379
410
|
- `add_tag_to_doc` – attach a tag to a document
|
|
380
411
|
- `remove_tag_from_doc` – detach a tag from a document
|
|
412
|
+
- `update_doc_title` – rename a document in both workspace metadata and the internal page block
|
|
381
413
|
- `append_paragraph` – append a paragraph block (WebSocket)
|
|
382
414
|
- `append_block` – append canonical block types (text/list/code/media/embed/database/edgeless) with strict validation and placement control (`viewMode=kanban` enables preset-backed data views; `data_view` defaults to kanban)
|
|
415
|
+
- `move_doc` – move a document in the sidebar by relinking it under a different parent
|
|
416
|
+
- `batch_create_docs` – create up to 20 documents in a single call
|
|
383
417
|
- `add_database_column` – add a column to a database block (`rich-text`, `select`, `multi-select`, `number`, `checkbox`, `link`, `date`)
|
|
384
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
|
|
385
420
|
- `read_database_columns` – read database schema metadata including column IDs/types, select options, and table view column mappings
|
|
386
421
|
- `read_database_cells` – read row titles plus decoded database cell values with optional row / column filters
|
|
387
422
|
- `update_database_cell` – update a single database cell or the built-in row title (`createOption` defaults to `true` for select fields)
|
|
388
423
|
- `update_database_row` – batch update multiple cells on a database row (`createOption` defaults to `true` for select fields)
|
|
389
424
|
- `append_markdown` – append markdown content to an existing document
|
|
390
425
|
- `replace_doc_with_markdown` – replace the main note content with markdown content
|
|
426
|
+
- `list_children` – list the direct child docs linked from a document
|
|
427
|
+
- `list_backlinks` – list the parent/reference docs that link to a document
|
|
428
|
+
- `cleanup_orphan_embeds` – remove linked-doc embeds that point to missing docs
|
|
429
|
+
- `find_and_replace` – preview or apply text replacement across a document
|
|
391
430
|
- `delete_doc` – delete a document (WebSocket)
|
|
392
431
|
|
|
393
432
|
### Comments
|
|
@@ -406,6 +445,40 @@ Endpoints currently available:
|
|
|
406
445
|
### Blob Storage
|
|
407
446
|
- `upload_blob`, `delete_blob`, `cleanup_blobs`
|
|
408
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
|
+
|
|
409
482
|
## Use Locally (clone)
|
|
410
483
|
|
|
411
484
|
```bash
|
|
@@ -434,7 +507,7 @@ npm run pack:check
|
|
|
434
507
|
- For full tool-surface verification, run `npm run test:comprehensive` (self-bootstraps a local Docker AFFiNE stack).
|
|
435
508
|
- For pre-provisioned environments, use `npm run test:comprehensive:raw`.
|
|
436
509
|
- For full environment verification, run `npm run test:e2e` (Docker + MCP + Playwright).
|
|
437
|
-
- 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`.
|
|
438
511
|
|
|
439
512
|
## Troubleshooting
|
|
440
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() {
|