affine-mcp-server 1.8.0 → 1.9.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.8.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
5
+ [![Version](https://img.shields.io/badge/version-1.9.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,16 @@ 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: 46 focused tools with WebSocket-based document editing
19
+ - Tools: 47 focused tools with WebSocket-based document editing
20
20
  - Status: Active
21
21
 
22
- > New in v1.8.0: Added database cell read/write tools, fixed Kanban row title persistence, and added CLI version commands.
22
+ > New in v1.9.0: Added database schema discovery, preset-backed data views, self-bootstrapping comprehensive regression, focused supporting-tools coverage, and markdown callout round-trips.
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, add columns and rows, and read or update cell values via MCP tools
28
+ - Database workflows: create database blocks, inspect schema, add columns and rows, and read or update cell values via MCP tools
29
29
  - Comments: full CRUD and resolve
30
30
  - Version History: list
31
31
  - Users & Tokens: current user, sign in, profile/settings, and personal access tokens
@@ -325,9 +325,10 @@ Endpoints currently available:
325
325
  - `add_tag_to_doc` – attach a tag to a document
326
326
  - `remove_tag_from_doc` – detach a tag from a document
327
327
  - `append_paragraph` – append a paragraph block (WebSocket)
328
- - `append_block` – append canonical block types (text/list/code/media/embed/database/edgeless) with strict validation and placement control (`data_view` currently falls back to database)
328
+ - `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)
329
329
  - `add_database_column` – add a column to a database block (`rich-text`, `select`, `multi-select`, `number`, `checkbox`, `link`, `date`)
330
330
  - `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)
331
+ - `read_database_columns` – read database schema metadata including column IDs/types, select options, and table view column mappings
331
332
  - `read_database_cells` – read row titles plus decoded database cell values with optional row / column filters
332
333
  - `update_database_cell` – update a single database cell or the built-in row title (`createOption` defaults to `true` for select fields)
333
334
  - `update_database_row` – batch update multiple cells on a database row (`createOption` defaults to `true` for select fields)
@@ -376,9 +377,10 @@ npm run pack:check
376
377
 
377
378
  - `tool-manifest.json` is the source of truth for publicly exposed tool names.
378
379
  - CI validates that `registerTool(...)` declarations match the manifest exactly.
379
- - For full tool-surface verification, run `npm run test:comprehensive`.
380
+ - For full tool-surface verification, run `npm run test:comprehensive` (self-bootstraps a local Docker AFFiNE stack).
381
+ - For pre-provisioned environments, use `npm run test:comprehensive:raw`.
380
382
  - For full environment verification, run `npm run test:e2e` (Docker + MCP + Playwright).
381
- - Additional focused runners: `npm run test:db-create`, `npm run test:db-cells`, `npm run test:bearer`, `npm run test:cli-version`, `npm run test:playwright`.
383
+ - 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:cli-version`, `npm run test:playwright`.
382
384
 
383
385
  ## Troubleshooting
384
386
 
@@ -411,83 +413,11 @@ Workspace visibility
411
413
  - Use HTTPS
412
414
  - Store credentials in a secrets manager
413
415
 
414
- ## Version History
415
-
416
- ### 1.8.0 (2026‑03‑09)
417
- - Added `read_database_cells`, `update_database_cell`, and `update_database_row` for database cell-level workflows
418
- - Fixed `add_database_row` so `title` / `Title` persists to the Kanban card header text
419
- - Added CLI version commands: `affine-mcp --version`, `affine-mcp -v`, and `affine-mcp version`
420
- - Added focused regression runners for database cells and CLI version support
421
- - Verified release gates with `npm run ci`, `npm run test:cli-version`, and live `npm run test:db-cells`
422
-
423
- ### 1.7.2 (2026‑03‑04)
424
- - Fixed MCP tag persistence to use AFFiNE canonical tag option IDs so tags are visible in Web/App UI
425
- - Added backward-compatible tag normalization for legacy string tag entries
426
- - Added tag visibility regression coverage (`tests/test-tag-visibility.mjs`, `tests/playwright/verify-tag-visibility.pw.ts`)
427
- - Hardened E2E credential bootstrap with configurable health retries, retry attempts, and Docker diagnostics on failure
428
- - Verified CI gates (`validate`, `e2e`) for PR #46 and local `npm run ci`
429
-
430
- ### 1.7.1 (2026‑03‑03)
431
- - Fixed MCP-created document structure parity with AFFiNE UI (`sys:parent` handling)
432
- - Fixed callout text rendering parity in AFFiNE UI for MCP-created blocks
433
- - Added regression assertions for visibility-sensitive document creation paths
434
-
435
- ### 1.7.0 (2026‑02‑27)
436
- - Added Streamable HTTP MCP support on `/mcp` for remote hosting while keeping legacy SSE compatibility paths (`/sse`, `/messages`)
437
- - Added HTTP deployment controls: `AFFINE_MCP_HTTP_HOST`, `AFFINE_MCP_HTTP_TOKEN`, `AFFINE_MCP_HTTP_ALLOWED_ORIGINS`, `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS`
438
- - Added `npm run start:http` for one-command HTTP mode startup
439
- - Hardened HTTP request handling with explicit 50MB parser application and case-insensitive Bearer auth parsing
440
- - Expanded docs with remote deployment/security presets (Docker, Render, Railway, VPS)
441
- - Verified full release checks with `npm run ci`, `npm run test:e2e`, and `npm run test:comprehensive`
442
-
443
- ### 1.6.0 (2026‑02‑24)
444
- - Added 11 document workflow tools: tags (`list_tags`, `list_docs_by_tag`, `create_tag`, `add_tag_to_doc`, `remove_tag_from_doc`), markdown roundtrip (`export_doc_markdown`, `create_doc_from_markdown`, `append_markdown`, `replace_doc_with_markdown`), and database operations (`add_database_column`, `add_database_row`)
445
- - Added interactive CLI commands: `affine-mcp login`, `affine-mcp status`, `affine-mcp logout`
446
- - Added Docker + Playwright E2E pipeline and CI workflow for auth/database regression checks
447
- - Tool surface increased from 32 to 43 canonical tools
448
- - Added release test commands (`test:e2e`, `test:db-create`, `test:bearer`, `test:playwright`) and package dependencies for markdown conversion + Playwright
449
-
450
- ### 1.5.0 (2026‑02‑13)
451
- - Expanded `append_block` from Step1 to Step4 profiles: canonical text/list/code/divider/callout/latex/table/bookmark/media/embed plus `database`, `data_view`, `surface_ref`, `frame`, `edgeless_text`, `note` (`data_view` currently mapped to database for stability)
452
- - Added strict field validation and canonical parent enforcement for page/note/surface containers
453
- - Added local integration runner coverage for all 30 append_block cases against a live AFFINE server
454
-
455
- ### 1.4.0 (2026‑02‑13)
456
- - Added `read_doc` for reading document block snapshot + plain text
457
- - Added Cursor setup examples and troubleshooting notes for JSON-RPC method usage
458
- - Added explicit local-storage workspace limitation notes
459
-
460
- ### 1.3.0 (2026‑02‑13)
461
- - Added `append_block` for slash-command style editing (`heading/list/todo/code/divider/quote`)
462
- - Tool surface simplified to 31 canonical tools (duplicate aliases removed)
463
- - Added CI + manifest parity verification (`npm run test:tool-manifest`, `npm run ci`)
464
- - Added open-source community health docs and issue/PR templates
465
-
466
- ### 1.2.2 (2025‑09‑18)
467
- - CLI wrapper added to ensure Node runs ESM entry (`bin/affine-mcp`), preventing shell mis-execution
468
- - Docs cleaned: use env vars via shell/app config; `.env` file no longer recommended
469
- - MCP startup behavior unchanged from 1.2.1 (async login by default)
470
-
471
- ### 1.2.1 (2025‑09‑17)
472
- - Default to asynchronous email/password login after MCP stdio handshake
473
- - `AFFINE_LOGIN_AT_START` supports `sync` when you need blocking startup (default is non-blocking)
474
- - Expanded docs for Codex/Claude using npm, npx, and local clone
475
-
476
- ### 1.2.0 (2025‑09‑16)
477
- - WebSocket-based document tools: `create_doc`, `append_paragraph`, `delete_doc` (create/edit/delete now supported)
478
- - Tool aliases introduced at the time (`affine_*` + non-prefixed names). They were removed later to reduce duplication.
479
- - ESM resolution: NodeNext; improved build stability
480
- - CLI binary: `affine-mcp` for easy `npm i -g` usage
481
-
482
- ### 1.1.0 (2025‑08‑12)
483
- - Fixed workspace creation with initial documents (UI accessible)
484
- - 30+ tools, simplified tool names
485
- - Improved error handling and authentication
486
-
487
- ### 1.0.0 (2025‑08‑12)
488
- - Initial stable release
489
- - Basic workspace and document operations
490
- - Full authentication support
416
+ ## Release Notes
417
+
418
+ - Changelog: [CHANGELOG.md](CHANGELOG.md)
419
+ - Release notes: [RELEASE_NOTES.md](RELEASE_NOTES.md)
420
+ - GitHub Releases: [Releases](https://github.com/dawncr0w/affine-mcp-server/releases)
491
421
 
492
422
  ## Contributing
493
423
 
@@ -231,6 +231,14 @@ function collectQuoteText(tokens, start, end) {
231
231
  }
232
232
  return lines.join("\n");
233
233
  }
234
+ function parseCalloutAdmonition(text) {
235
+ const lines = text.split("\n");
236
+ const marker = lines[0]?.trim() ?? "";
237
+ if (!/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/i.test(marker)) {
238
+ return null;
239
+ }
240
+ return lines.slice(1).join("\n").trim();
241
+ }
234
242
  function parseList(tokens, start, end, defaultStyle, state, depth) {
235
243
  const operations = [];
236
244
  let i = start;
@@ -382,7 +390,11 @@ function parseTokens(tokens, start, end, state) {
382
390
  break;
383
391
  }
384
392
  const quoteText = collectQuoteText(tokens, i + 1, close).trim();
385
- if (quoteText.length > 0) {
393
+ const calloutText = parseCalloutAdmonition(quoteText);
394
+ if (calloutText !== null) {
395
+ state.operations.push({ type: "callout", text: calloutText });
396
+ }
397
+ else if (quoteText.length > 0) {
386
398
  state.operations.push({ type: "quote", text: quoteText });
387
399
  }
388
400
  i = close + 1;
@@ -8,6 +8,12 @@ function formatQuote(text) {
8
8
  const lines = text.split("\n");
9
9
  return lines.map(line => `> ${line}`);
10
10
  }
11
+ function formatCallout(lines) {
12
+ return [
13
+ "> [!NOTE]",
14
+ ...lines.map(line => line.length > 0 ? `> ${line}` : ">"),
15
+ ];
16
+ }
11
17
  function escapePipe(value) {
12
18
  return value.replace(/\|/g, "\\|");
13
19
  }
@@ -138,6 +144,25 @@ function renderBlock(blockId, listDepth, state) {
138
144
  isList: false,
139
145
  };
140
146
  }
147
+ case "affine:callout": {
148
+ const contentLines = [];
149
+ for (const childId of children) {
150
+ const child = renderBlock(childId, listDepth, state);
151
+ if (child.lines.length > 0) {
152
+ if (contentLines.length > 0 && !child.isList) {
153
+ contentLines.push("");
154
+ }
155
+ contentLines.push(...child.lines);
156
+ }
157
+ }
158
+ if (contentLines.length === 0 && text.length > 0) {
159
+ contentLines.push(text);
160
+ }
161
+ return {
162
+ lines: formatCallout(contentLines),
163
+ isList: false,
164
+ };
165
+ }
141
166
  case "affine:note":
142
167
  case "affine:page":
143
168
  case "affine:surface": {
@@ -53,6 +53,8 @@ const APPEND_BLOCK_BOOKMARK_STYLE_VALUES = [
53
53
  "citation",
54
54
  ];
55
55
  const AppendBlockBookmarkStyle = z.enum(APPEND_BLOCK_BOOKMARK_STYLE_VALUES);
56
+ const APPEND_BLOCK_DATA_VIEW_MODE_VALUES = ["table", "kanban"];
57
+ const AppendBlockDataViewMode = z.enum(APPEND_BLOCK_DATA_VIEW_MODE_VALUES);
56
58
  function blockVersion(flavour) {
57
59
  switch (flavour) {
58
60
  case "affine:page":
@@ -78,6 +80,11 @@ export function registerDocTools(server, gql, defaults) {
78
80
  const bearer = gql.bearer;
79
81
  return { endpoint, cookie, bearer };
80
82
  }
83
+ const SELECT_COLORS = [
84
+ "var(--affine-tag-blue)", "var(--affine-tag-green)", "var(--affine-tag-red)",
85
+ "var(--affine-tag-orange)", "var(--affine-tag-purple)", "var(--affine-tag-yellow)",
86
+ "var(--affine-tag-teal)", "var(--affine-tag-pink)", "var(--affine-tag-gray)",
87
+ ];
81
88
  function makeText(content) {
82
89
  const yText = new Y.Text();
83
90
  if (content.length > 0) {
@@ -717,6 +724,9 @@ export function registerDocTools(server, gql, defaults) {
717
724
  else if (raw.tableData !== undefined && normalized.strict) {
718
725
  throw new Error("The 'tableData' field can only be used with type='table'.");
719
726
  }
727
+ if (normalized.type !== "database" && normalized.type !== "data_view" && raw.viewMode !== undefined && normalized.strict) {
728
+ throw new Error("The 'viewMode' field can only be used with type='database' or type='data_view'.");
729
+ }
720
730
  }
721
731
  function normalizeAppendBlockInput(parsed) {
722
732
  const strict = parsed.strict !== false;
@@ -726,6 +736,7 @@ export function registerDocTools(server, gql, defaults) {
726
736
  const headingLevel = Math.max(1, Math.min(6, headingLevelNumber));
727
737
  const listStyle = typeInfo.listStyleFromAlias ?? parsed.style ?? "bulleted";
728
738
  const bookmarkStyle = parsed.bookmarkStyle ?? "horizontal";
739
+ const dataViewMode = parsed.viewMode ?? (typeInfo.type === "data_view" ? "kanban" : "table");
729
740
  const language = (parsed.language ?? "txt").trim().toLowerCase() || "txt";
730
741
  const placement = normalizePlacement(parsed.placement);
731
742
  const url = (parsed.url ?? "").trim();
@@ -774,6 +785,7 @@ export function registerDocTools(server, gql, defaults) {
774
785
  headingLevel,
775
786
  listStyle,
776
787
  bookmarkStyle,
788
+ dataViewMode,
777
789
  checked: Boolean(parsed.checked),
778
790
  language,
779
791
  caption: parsed.caption,
@@ -929,6 +941,102 @@ export function registerDocTools(server, gql, defaults) {
929
941
  }
930
942
  return { parentId, parentBlock, children, insertIndex };
931
943
  }
944
+ function createDatabaseViewColumn(columnId, width = 200, hide = false) {
945
+ const column = new Y.Map();
946
+ column.set("id", columnId);
947
+ column.set("width", width);
948
+ column.set("hide", hide);
949
+ return column;
950
+ }
951
+ function createDatabaseColumnDefinition(input) {
952
+ const column = new Y.Map();
953
+ column.set("id", input.id);
954
+ column.set("name", input.name);
955
+ column.set("type", input.type);
956
+ column.set("width", input.width ?? 200);
957
+ if ((input.type === "select" || input.type === "multi-select") && input.options?.length) {
958
+ const data = new Y.Map();
959
+ const options = new Y.Array();
960
+ input.options.forEach((value, index) => {
961
+ const option = new Y.Map();
962
+ option.set("id", generateId());
963
+ option.set("value", value);
964
+ option.set("color", SELECT_COLORS[index % SELECT_COLORS.length]);
965
+ options.push([option]);
966
+ });
967
+ data.set("options", options);
968
+ column.set("data", data);
969
+ }
970
+ return column;
971
+ }
972
+ function createPresetBackedDataViewBlock(blockId, titleText, viewMode, blockType) {
973
+ const block = new Y.Map();
974
+ setSysFields(block, blockId, "affine:database");
975
+ block.set("sys:parent", null);
976
+ block.set("sys:children", new Y.Array());
977
+ block.set("prop:title", makeText(titleText));
978
+ block.set("prop:cells", new Y.Map());
979
+ block.set("prop:comments", undefined);
980
+ const titleColumnId = generateId();
981
+ const columns = new Y.Array();
982
+ columns.push([createDatabaseColumnDefinition({
983
+ id: titleColumnId,
984
+ name: "Title",
985
+ type: "title",
986
+ width: 320,
987
+ })]);
988
+ const viewColumns = new Y.Array();
989
+ viewColumns.push([createDatabaseViewColumn(titleColumnId, 320, false)]);
990
+ const header = {
991
+ titleColumn: titleColumnId,
992
+ iconColumn: "type",
993
+ };
994
+ let groupBy = null;
995
+ let groupProperties = null;
996
+ if (viewMode === "kanban") {
997
+ const statusColumnId = generateId();
998
+ columns.push([createDatabaseColumnDefinition({
999
+ id: statusColumnId,
1000
+ name: "Status",
1001
+ type: "select",
1002
+ options: ["Todo", "In Progress", "Done"],
1003
+ })]);
1004
+ viewColumns.push([createDatabaseViewColumn(statusColumnId, 200, false)]);
1005
+ groupBy = {
1006
+ columnId: statusColumnId,
1007
+ name: "select",
1008
+ type: "groupBy",
1009
+ };
1010
+ groupProperties = [];
1011
+ }
1012
+ const view = new Y.Map();
1013
+ view.set("id", generateId());
1014
+ view.set("name", viewMode === "kanban" ? "Kanban View" : "Table View");
1015
+ view.set("mode", viewMode);
1016
+ view.set("columns", viewColumns);
1017
+ view.set("filter", { type: "group", op: "and", conditions: [] });
1018
+ if (groupBy) {
1019
+ view.set("groupBy", groupBy);
1020
+ }
1021
+ else {
1022
+ view.set("groupBy", null);
1023
+ }
1024
+ if (groupProperties) {
1025
+ view.set("groupProperties", groupProperties);
1026
+ }
1027
+ view.set("sort", null);
1028
+ view.set("header", header);
1029
+ const views = new Y.Array();
1030
+ views.push([view]);
1031
+ block.set("prop:columns", columns);
1032
+ block.set("prop:views", views);
1033
+ return {
1034
+ blockId,
1035
+ block,
1036
+ flavour: "affine:database",
1037
+ blockType,
1038
+ };
1039
+ }
932
1040
  function createBlock(normalized) {
933
1041
  const blockId = generateId();
934
1042
  const block = new Y.Map();
@@ -1237,6 +1345,9 @@ export function registerDocTools(server, gql, defaults) {
1237
1345
  return { blockId, block, flavour: "affine:embed-iframe" };
1238
1346
  }
1239
1347
  case "database": {
1348
+ if (normalized.dataViewMode === "kanban") {
1349
+ return createPresetBackedDataViewBlock(blockId, normalized.text, "kanban", "database_kanban");
1350
+ }
1240
1351
  setSysFields(block, blockId, "affine:database");
1241
1352
  block.set("sys:parent", null);
1242
1353
  block.set("sys:children", new Y.Array());
@@ -1260,28 +1371,7 @@ export function registerDocTools(server, gql, defaults) {
1260
1371
  return { blockId, block, flavour: "affine:database" };
1261
1372
  }
1262
1373
  case "data_view": {
1263
- // AFFiNE 0.26.x currently crashes on raw affine:data-view render path.
1264
- // Keep API compatibility for type="data_view" by mapping it to the stable database block.
1265
- setSysFields(block, blockId, "affine:database");
1266
- block.set("sys:parent", null);
1267
- block.set("sys:children", new Y.Array());
1268
- const dvDefaultView = new Y.Map();
1269
- dvDefaultView.set("id", generateId());
1270
- dvDefaultView.set("name", "Table View");
1271
- dvDefaultView.set("mode", "table");
1272
- dvDefaultView.set("columns", new Y.Array());
1273
- dvDefaultView.set("filter", { type: "group", op: "and", conditions: [] });
1274
- dvDefaultView.set("groupBy", null);
1275
- dvDefaultView.set("sort", null);
1276
- dvDefaultView.set("header", { titleColumn: null, iconColumn: null });
1277
- const dvViews = new Y.Array();
1278
- dvViews.push([dvDefaultView]);
1279
- block.set("prop:views", dvViews);
1280
- block.set("prop:title", makeText(content));
1281
- block.set("prop:cells", new Y.Map());
1282
- block.set("prop:columns", new Y.Array());
1283
- block.set("prop:comments", undefined);
1284
- return { blockId, block, flavour: "affine:database", blockType: "data_view_fallback" };
1374
+ return createPresetBackedDataViewBlock(blockId, normalized.text, normalized.dataViewMode, `data_view_${normalized.dataViewMode}`);
1285
1375
  }
1286
1376
  case "surface_ref": {
1287
1377
  setSysFields(block, blockId, "affine:surface-ref");
@@ -1425,6 +1515,15 @@ export function registerDocTools(server, gql, defaults) {
1425
1515
  strict,
1426
1516
  placement,
1427
1517
  };
1518
+ case "callout":
1519
+ return {
1520
+ workspaceId,
1521
+ docId,
1522
+ type: "callout",
1523
+ text: operation.text,
1524
+ strict,
1525
+ placement,
1526
+ };
1428
1527
  case "list":
1429
1528
  return {
1430
1529
  workspaceId,
@@ -2439,6 +2538,7 @@ export function registerDocTools(server, gql, defaults) {
2439
2538
  level: z.number().int().min(1).max(6).optional().describe("Heading level for type=heading"),
2440
2539
  style: AppendBlockListStyle.optional().describe("List style for type=list"),
2441
2540
  bookmarkStyle: AppendBlockBookmarkStyle.optional().describe("Bookmark card style"),
2541
+ viewMode: AppendBlockDataViewMode.optional().describe("Initial data view preset for type=database or type=data_view. Defaults: database=table, data_view=kanban"),
2442
2542
  checked: z.boolean().optional().describe("Todo state when type is todo"),
2443
2543
  language: z.string().optional().describe("Code language when type is code"),
2444
2544
  caption: z.string().optional().describe("Code caption when type is code"),
@@ -2760,6 +2860,58 @@ export function registerDocTools(server, gql, defaults) {
2760
2860
  });
2761
2861
  return defs;
2762
2862
  }
2863
+ function readDatabaseViewDefs(dbBlock, lookup) {
2864
+ const viewsRaw = dbBlock.get("prop:views");
2865
+ const views = [];
2866
+ if (!(viewsRaw instanceof Y.Array)) {
2867
+ return views;
2868
+ }
2869
+ viewsRaw.forEach((view) => {
2870
+ const id = view instanceof Y.Map ? view.get("id") : view?.id;
2871
+ if (!id) {
2872
+ return;
2873
+ }
2874
+ const columnsRaw = view instanceof Y.Map ? view.get("columns") : view?.columns;
2875
+ const headerRaw = view instanceof Y.Map ? view.get("header") : view?.header;
2876
+ const groupByRaw = view instanceof Y.Map ? view.get("groupBy") : view?.groupBy;
2877
+ const columns = databaseArrayValues(columnsRaw)
2878
+ .map((entry) => {
2879
+ const columnId = entry instanceof Y.Map ? entry.get("id") : entry?.id;
2880
+ if (!columnId || typeof columnId !== "string") {
2881
+ return null;
2882
+ }
2883
+ const columnDef = lookup.colById.get(columnId) || null;
2884
+ const hidden = entry instanceof Y.Map ? entry.get("hide") : entry?.hide;
2885
+ const width = entry instanceof Y.Map ? entry.get("width") : entry?.width;
2886
+ return {
2887
+ id: columnId,
2888
+ name: columnDef?.name || null,
2889
+ hidden: hidden === true,
2890
+ width: typeof width === "number" ? width : null,
2891
+ };
2892
+ })
2893
+ .filter((entry) => entry !== null);
2894
+ views.push({
2895
+ id: String(id),
2896
+ name: String((view instanceof Y.Map ? view.get("name") : view?.name) || ""),
2897
+ mode: String((view instanceof Y.Map ? view.get("mode") : view?.mode) || ""),
2898
+ columns,
2899
+ columnIds: columns.map(column => column.id),
2900
+ groupBy: groupByRaw
2901
+ ? {
2902
+ columnId: typeof groupByRaw?.columnId === "string" ? groupByRaw.columnId : null,
2903
+ name: typeof groupByRaw?.name === "string" ? groupByRaw.name : null,
2904
+ type: typeof groupByRaw?.type === "string" ? groupByRaw.type : null,
2905
+ }
2906
+ : null,
2907
+ header: {
2908
+ titleColumn: typeof headerRaw?.titleColumn === "string" ? headerRaw.titleColumn : null,
2909
+ iconColumn: typeof headerRaw?.iconColumn === "string" ? headerRaw.iconColumn : null,
2910
+ },
2911
+ });
2912
+ });
2913
+ return views;
2914
+ }
2763
2915
  function isTitleAliasKey(value) {
2764
2916
  return value.trim().toLowerCase() === "title";
2765
2917
  }
@@ -2851,11 +3003,6 @@ export function registerDocTools(server, gql, defaults) {
2851
3003
  }
2852
3004
  return [];
2853
3005
  }
2854
- const SELECT_COLORS = [
2855
- "var(--affine-tag-blue)", "var(--affine-tag-green)", "var(--affine-tag-red)",
2856
- "var(--affine-tag-orange)", "var(--affine-tag-purple)", "var(--affine-tag-yellow)",
2857
- "var(--affine-tag-teal)", "var(--affine-tag-pink)", "var(--affine-tag-gray)",
2858
- ];
2859
3006
  /** Find or create a select option for a column, mutating the column's data in place */
2860
3007
  function resolveSelectOptionId(col, valueText, createOption = true) {
2861
3008
  // Try exact match first
@@ -3167,6 +3314,41 @@ export function registerDocTools(server, gql, defaults) {
3167
3314
  columns: z.array(z.string().min(1)).optional().describe("Optional column name or ID filter."),
3168
3315
  },
3169
3316
  }, readDatabaseCellsHandler);
3317
+ const readDatabaseColumnsHandler = async (parsed) => {
3318
+ const workspaceId = parsed.workspaceId || defaults.workspaceId;
3319
+ if (!workspaceId)
3320
+ throw new Error("workspaceId is required");
3321
+ const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
3322
+ try {
3323
+ const columns = ctx.columnDefs.map(col => ({
3324
+ id: col.id,
3325
+ name: col.name || null,
3326
+ type: col.type,
3327
+ options: col.options,
3328
+ }));
3329
+ return text({
3330
+ databaseBlockId: parsed.databaseBlockId,
3331
+ title: richTextValueToString(ctx.dbBlock.get("prop:title")) || null,
3332
+ rowCount: getDatabaseRowIds(ctx.dbBlock).length,
3333
+ columnCount: columns.length,
3334
+ titleColumnId: ctx.titleCol?.id || null,
3335
+ columns,
3336
+ views: readDatabaseViewDefs(ctx.dbBlock, ctx),
3337
+ });
3338
+ }
3339
+ finally {
3340
+ ctx.socket.disconnect();
3341
+ }
3342
+ };
3343
+ server.registerTool("read_database_columns", {
3344
+ title: "Read Database Columns",
3345
+ description: "Read schema metadata for an AFFiNE database block, including columns, select options, and view column mappings. Useful for empty databases before any rows exist.",
3346
+ inputSchema: {
3347
+ workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
3348
+ docId: DocId.describe("Document ID containing the database"),
3349
+ databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
3350
+ },
3351
+ }, readDatabaseColumnsHandler);
3170
3352
  const updateDatabaseCellHandler = async (parsed) => {
3171
3353
  const workspaceId = parsed.workspaceId || defaults.workspaceId;
3172
3354
  if (!workspaceId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "affine-mcp-server",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Model Context Protocol server for AFFiNE - enables AI assistants to interact with AFFiNE workspaces, documents, and collaboration features.",
@@ -35,12 +35,18 @@
35
35
  "test": "npm run test:tool-manifest",
36
36
  "test:cli-version": "node tests/test-cli-version.mjs",
37
37
  "test:tool-manifest": "node scripts/verify-tool-manifest.mjs",
38
- "test:comprehensive": "node test-comprehensive.mjs",
38
+ "test:comprehensive": "bash tests/run-comprehensive.sh",
39
+ "test:comprehensive:raw": "node test-comprehensive.mjs",
39
40
  "test:e2e": "bash tests/run-e2e.sh",
40
41
  "test:db-create": "node tests/test-database-creation.mjs",
41
42
  "test:db-cells": "node tests/test-database-cells.mjs",
43
+ "test:db-schema": "node tests/test-database-schema.mjs",
44
+ "test:data-view": "node tests/test-data-view.mjs",
45
+ "test:data-view-ui": "npx playwright test tests/playwright/verify-data-view.pw.ts --config tests/playwright/playwright.config.ts",
42
46
  "test:bearer": "node tests/test-bearer-auth.mjs",
47
+ "test:supporting-tools": "node tests/test-supporting-tools.mjs",
43
48
  "test:tag-visibility": "node tests/test-tag-visibility.mjs",
49
+ "test:markdown-roundtrip": "node tests/test-markdown-roundtrip.mjs",
44
50
  "test:playwright": "npx playwright test --config tests/playwright/playwright.config.ts",
45
51
  "pack:check": "npm pack --dry-run",
46
52
  "ci": "npm run build && npm run test:tool-manifest && npm run pack:check",