affine-mcp-server 1.7.1 → 1.7.2

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.7.1-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
5
+ [![Version](https://img.shields.io/badge/version-1.7.2-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)
@@ -19,7 +19,7 @@ A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted
19
19
  - Tools: 43 focused tools with WebSocket-based document editing
20
20
  - Status: Active
21
21
 
22
- > New in v1.7.1: Fixed MCP-created document structure parity with AFFiNE UI (`sys:parent` handling) and callout text rendering, plus regression coverage for UI visibility paths.
22
+ > New in v1.7.2: Fixed tag visibility parity in AFFiNE Web/App for MCP-created tags and hardened Docker E2E startup reliability with retry/diagnostics.
23
23
 
24
24
  ## Features
25
25
 
@@ -409,6 +409,18 @@ Workspace visibility
409
409
 
410
410
  ## Version History
411
411
 
412
+ ### 1.7.2 (2026‑03‑04)
413
+ - Fixed MCP tag persistence to use AFFiNE canonical tag option IDs so tags are visible in Web/App UI
414
+ - Added backward-compatible tag normalization for legacy string tag entries
415
+ - Added tag visibility regression coverage (`tests/test-tag-visibility.mjs`, `tests/playwright/verify-tag-visibility.pw.ts`)
416
+ - Hardened E2E credential bootstrap with configurable health retries, retry attempts, and Docker diagnostics on failure
417
+ - Verified CI gates (`validate`, `e2e`) for PR #46 and local `npm run ci`
418
+
419
+ ### 1.7.1 (2026‑03‑03)
420
+ - Fixed MCP-created document structure parity with AFFiNE UI (`sys:parent` handling)
421
+ - Fixed callout text rendering parity in AFFiNE UI for MCP-created blocks
422
+ - Added regression assertions for visibility-sensitive document creation paths
423
+
412
424
  ### 1.7.0 (2026‑02‑27)
413
425
  - Added Streamable HTTP MCP support on `/mcp` for remote hosting while keeping legacy SSE compatibility paths (`/sse`, `/messages`)
414
426
  - Added HTTP deployment controls: `AFFINE_MCP_HTTP_HOST`, `AFFINE_MCP_HTTP_TOKEN`, `AFFINE_MCP_HTTP_ALLOWED_ORIGINS`, `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS`
@@ -118,6 +118,11 @@ export function registerDocTools(server, gql, defaults) {
118
118
  }
119
119
  return normalized;
120
120
  }
121
+ const TAG_OPTION_COLORS = [
122
+ "var(--affine-tag-blue)", "var(--affine-tag-green)", "var(--affine-tag-red)",
123
+ "var(--affine-tag-orange)", "var(--affine-tag-purple)", "var(--affine-tag-yellow)",
124
+ "var(--affine-tag-teal)", "var(--affine-tag-pink)", "var(--affine-tag-gray)",
125
+ ];
121
126
  function getStringArray(value) {
122
127
  if (!(value instanceof Y.Array)) {
123
128
  return [];
@@ -146,23 +151,235 @@ export function registerDocTools(server, gql, defaults) {
146
151
  target.set(key, next);
147
152
  return next;
148
153
  }
149
- function hasTag(tagValues, tag, ignoreCase) {
150
- const normalizedTag = ignoreCase ? tag.toLocaleLowerCase() : tag;
151
- return tagValues.some((entry) => (ignoreCase ? entry.toLocaleLowerCase() : entry) === normalizedTag);
154
+ function getYMap(target, key) {
155
+ const value = target.get(key);
156
+ if (!(value instanceof Y.Map)) {
157
+ return null;
158
+ }
159
+ return value;
152
160
  }
153
- function findTagIndex(tags, tag, ignoreCase) {
154
- const normalizedTag = ignoreCase ? tag.toLocaleLowerCase() : tag;
155
- let index = -1;
156
- tags.forEach((entry, i) => {
157
- if (index >= 0 || typeof entry !== "string") {
161
+ function ensureYMap(target, key) {
162
+ const current = getYMap(target, key);
163
+ if (current) {
164
+ return current;
165
+ }
166
+ const next = new Y.Map();
167
+ target.set(key, next);
168
+ return next;
169
+ }
170
+ function getWorkspaceTagOptionsArray(meta) {
171
+ const properties = getYMap(meta, "properties");
172
+ if (!properties) {
173
+ return null;
174
+ }
175
+ const tags = getYMap(properties, "tags");
176
+ if (!tags) {
177
+ return null;
178
+ }
179
+ const options = tags.get("options");
180
+ if (!(options instanceof Y.Array)) {
181
+ return null;
182
+ }
183
+ return options;
184
+ }
185
+ function ensureWorkspaceTagOptionsArray(meta) {
186
+ const properties = ensureYMap(meta, "properties");
187
+ const tags = ensureYMap(properties, "tags");
188
+ const existing = tags.get("options");
189
+ if (existing instanceof Y.Array) {
190
+ return existing;
191
+ }
192
+ const next = new Y.Array();
193
+ tags.set("options", next);
194
+ return next;
195
+ }
196
+ function asNumberOrNull(value) {
197
+ return typeof value === "number" ? value : null;
198
+ }
199
+ function parseWorkspaceTagOption(raw) {
200
+ let id;
201
+ let value;
202
+ let color;
203
+ let createDate;
204
+ let updateDate;
205
+ if (raw instanceof Y.Map) {
206
+ id = raw.get("id");
207
+ value = raw.get("value");
208
+ color = raw.get("color");
209
+ createDate = raw.get("createDate");
210
+ updateDate = raw.get("updateDate");
211
+ }
212
+ else if (raw && typeof raw === "object") {
213
+ id = raw.id;
214
+ value = raw.value;
215
+ color = raw.color;
216
+ createDate = raw.createDate;
217
+ updateDate = raw.updateDate;
218
+ }
219
+ else {
220
+ return null;
221
+ }
222
+ if (typeof id !== "string" || id.trim().length === 0) {
223
+ return null;
224
+ }
225
+ if (typeof value !== "string" || value.trim().length === 0) {
226
+ return null;
227
+ }
228
+ return {
229
+ id,
230
+ value,
231
+ color: typeof color === "string" && color.trim().length > 0 ? color : TAG_OPTION_COLORS[0],
232
+ createDate: asNumberOrNull(createDate),
233
+ updateDate: asNumberOrNull(updateDate),
234
+ };
235
+ }
236
+ function getWorkspaceTagOptions(meta) {
237
+ const options = getWorkspaceTagOptionsArray(meta);
238
+ if (!options) {
239
+ return [];
240
+ }
241
+ const parsed = [];
242
+ options.forEach((raw) => {
243
+ const option = parseWorkspaceTagOption(raw);
244
+ if (option) {
245
+ parsed.push(option);
246
+ }
247
+ });
248
+ return parsed;
249
+ }
250
+ function getWorkspaceTagOptionMaps(meta) {
251
+ const options = getWorkspaceTagOptions(meta);
252
+ const byId = new Map();
253
+ const byValueLower = new Map();
254
+ for (const option of options) {
255
+ if (!byId.has(option.id)) {
256
+ byId.set(option.id, option);
257
+ }
258
+ const key = option.value.toLocaleLowerCase();
259
+ if (!byValueLower.has(key)) {
260
+ byValueLower.set(key, option);
261
+ }
262
+ }
263
+ return { options, byId, byValueLower };
264
+ }
265
+ function resolveTagLabels(tagEntries, byId) {
266
+ const deduped = new Set();
267
+ const resolved = [];
268
+ for (const entry of tagEntries) {
269
+ const raw = entry.trim();
270
+ if (!raw) {
271
+ continue;
272
+ }
273
+ const option = byId.get(raw);
274
+ const label = (option ? option.value : raw).trim();
275
+ if (!label) {
276
+ continue;
277
+ }
278
+ const dedupeKey = label.toLocaleLowerCase();
279
+ if (deduped.has(dedupeKey)) {
280
+ continue;
281
+ }
282
+ deduped.add(dedupeKey);
283
+ resolved.push(label);
284
+ }
285
+ return resolved;
286
+ }
287
+ function ensureWorkspaceTagOption(meta, tag) {
288
+ const normalizedTag = normalizeTag(tag);
289
+ const maps = getWorkspaceTagOptionMaps(meta);
290
+ const existing = maps.byValueLower.get(normalizedTag.toLocaleLowerCase());
291
+ if (existing) {
292
+ return { option: existing, created: false };
293
+ }
294
+ const optionsArray = ensureWorkspaceTagOptionsArray(meta);
295
+ const color = TAG_OPTION_COLORS[maps.options.length % TAG_OPTION_COLORS.length];
296
+ const now = Date.now();
297
+ const option = {
298
+ id: generateId(),
299
+ value: normalizedTag,
300
+ color,
301
+ createDate: now,
302
+ updateDate: now,
303
+ };
304
+ const optionMap = new Y.Map();
305
+ optionMap.set("id", option.id);
306
+ optionMap.set("value", option.value);
307
+ optionMap.set("color", option.color);
308
+ optionMap.set("createDate", now);
309
+ optionMap.set("updateDate", now);
310
+ optionsArray.push([optionMap]);
311
+ return { option, created: true };
312
+ }
313
+ function collectMatchingTagIndexes(tags, requestedTag, option, ignoreCase) {
314
+ const normalizedRequested = ignoreCase ? requestedTag.toLocaleLowerCase() : requestedTag;
315
+ const normalizedOptionId = option
316
+ ? (ignoreCase ? option.id.toLocaleLowerCase() : option.id)
317
+ : null;
318
+ const normalizedOptionValue = option
319
+ ? (ignoreCase ? option.value.toLocaleLowerCase() : option.value)
320
+ : null;
321
+ const indexes = [];
322
+ tags.forEach((entry, index) => {
323
+ if (typeof entry !== "string") {
158
324
  return;
159
325
  }
160
326
  const current = ignoreCase ? entry.toLocaleLowerCase() : entry;
161
- if (current === normalizedTag) {
162
- index = i;
327
+ if (current === normalizedRequested ||
328
+ (normalizedOptionId && current === normalizedOptionId) ||
329
+ (normalizedOptionValue && current === normalizedOptionValue)) {
330
+ indexes.push(index);
163
331
  }
164
332
  });
165
- return index;
333
+ return indexes;
334
+ }
335
+ function deleteArrayIndexes(arr, indexes) {
336
+ if (indexes.length === 0) {
337
+ return false;
338
+ }
339
+ const sorted = [...indexes].sort((a, b) => b - a);
340
+ for (const index of sorted) {
341
+ arr.delete(index, 1);
342
+ }
343
+ return true;
344
+ }
345
+ function syncTagArrayToOption(tags, requestedTag, option) {
346
+ const optionId = option.id.toLocaleLowerCase();
347
+ const optionValue = option.value.toLocaleLowerCase();
348
+ const requested = requestedTag.toLocaleLowerCase();
349
+ let existed = false;
350
+ let hasCanonicalId = false;
351
+ const removeIndexes = [];
352
+ tags.forEach((entry, index) => {
353
+ if (typeof entry !== "string") {
354
+ return;
355
+ }
356
+ const current = entry.toLocaleLowerCase();
357
+ const matched = current === optionId || current === optionValue || current === requested;
358
+ if (!matched) {
359
+ return;
360
+ }
361
+ existed = true;
362
+ if (current === optionId) {
363
+ if (hasCanonicalId) {
364
+ removeIndexes.push(index);
365
+ }
366
+ else {
367
+ hasCanonicalId = true;
368
+ }
369
+ return;
370
+ }
371
+ removeIndexes.push(index);
372
+ });
373
+ let changed = deleteArrayIndexes(tags, removeIndexes);
374
+ if (!hasCanonicalId) {
375
+ tags.push([option.id]);
376
+ changed = true;
377
+ }
378
+ return { existed, changed };
379
+ }
380
+ function hasTag(tagValues, tag, ignoreCase) {
381
+ const normalizedTag = ignoreCase ? tag.toLocaleLowerCase() : tag;
382
+ return tagValues.some((entry) => (ignoreCase ? entry.toLocaleLowerCase() : entry) === normalizedTag);
166
383
  }
167
384
  function getWorkspacePageEntries(meta) {
168
385
  const pages = meta.get("pages");
@@ -1373,9 +1590,9 @@ export function registerDocTools(server, gql, defaults) {
1373
1590
  }
1374
1591
  return tableData;
1375
1592
  }
1376
- function collectDocForMarkdown(doc) {
1593
+ function collectDocForMarkdown(doc, tagOptionsById = new Map()) {
1377
1594
  const meta = doc.getMap("meta");
1378
- const tags = getStringArray(getTagArray(meta));
1595
+ const tags = resolveTagLabels(getStringArray(getTagArray(meta)), tagOptionsById);
1379
1596
  const blocks = doc.getMap("blocks");
1380
1597
  const pageId = findBlockIdByFlavour(blocks, "affine:page");
1381
1598
  const noteId = findBlockIdByFlavour(blocks, "affine:note");
@@ -1640,8 +1857,10 @@ export function registerDocTools(server, gql, defaults) {
1640
1857
  Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
1641
1858
  const meta = wsDoc.getMap("meta");
1642
1859
  const pages = getWorkspacePageEntries(meta);
1860
+ const { byId } = getWorkspaceTagOptionMaps(meta);
1643
1861
  for (const page of pages) {
1644
- tagsByDocId.set(page.id, getStringArray(page.tagsArray));
1862
+ const tagEntries = getStringArray(page.tagsArray);
1863
+ tagsByDocId.set(page.id, resolveTagLabels(tagEntries, byId));
1645
1864
  }
1646
1865
  }
1647
1866
  }
@@ -1700,9 +1919,10 @@ export function registerDocTools(server, gql, defaults) {
1700
1919
  Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
1701
1920
  const meta = wsDoc.getMap("meta");
1702
1921
  const pages = getWorkspacePageEntries(meta);
1922
+ const { options, byId } = getWorkspaceTagOptionMaps(meta);
1703
1923
  const tagCounts = new Map();
1704
- for (const tag of getStringArray(getTagArray(meta))) {
1705
- const normalized = tag.trim();
1924
+ for (const option of options) {
1925
+ const normalized = option.value.trim();
1706
1926
  if (!normalized || tagCounts.has(normalized)) {
1707
1927
  continue;
1708
1928
  }
@@ -1710,7 +1930,8 @@ export function registerDocTools(server, gql, defaults) {
1710
1930
  }
1711
1931
  for (const page of pages) {
1712
1932
  const uniqueTags = new Set();
1713
- for (const tag of getStringArray(page.tagsArray)) {
1933
+ const resolved = resolveTagLabels(getStringArray(page.tagsArray), byId);
1934
+ for (const tag of resolved) {
1714
1935
  const normalized = tag.trim();
1715
1936
  if (!normalized) {
1716
1937
  continue;
@@ -1761,18 +1982,22 @@ export function registerDocTools(server, gql, defaults) {
1761
1982
  Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
1762
1983
  const meta = wsDoc.getMap("meta");
1763
1984
  const pages = getWorkspacePageEntries(meta);
1985
+ const { byId } = getWorkspaceTagOptionMaps(meta);
1764
1986
  const docs = pages
1765
1987
  .map((page) => {
1766
- const tags = getStringArray(page.tagsArray);
1988
+ const rawTags = getStringArray(page.tagsArray);
1989
+ const tags = resolveTagLabels(rawTags, byId);
1767
1990
  return {
1768
1991
  id: page.id,
1769
1992
  title: page.title,
1770
1993
  createDate: page.createDate,
1771
1994
  updatedDate: page.updatedDate,
1772
1995
  tags,
1996
+ rawTags,
1773
1997
  };
1774
1998
  })
1775
- .filter((page) => hasTag(page.tags, tag, ignoreCase));
1999
+ .filter((page) => hasTag(page.tags, tag, ignoreCase) || hasTag(page.rawTags, tag, ignoreCase))
2000
+ .map(({ rawTags: _rawTags, ...page }) => page);
1776
2001
  return text({
1777
2002
  workspaceId,
1778
2003
  tag,
@@ -1813,11 +2038,10 @@ export function registerDocTools(server, gql, defaults) {
1813
2038
  Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
1814
2039
  const prevSV = Y.encodeStateVector(wsDoc);
1815
2040
  const meta = wsDoc.getMap("meta");
1816
- const registry = ensureTagArray(meta);
1817
- if (findTagIndex(registry, tag, true) >= 0) {
2041
+ const { created } = ensureWorkspaceTagOption(meta, tag);
2042
+ if (!created) {
1818
2043
  return text({ workspaceId, tag, created: false });
1819
2044
  }
1820
- registry.push([tag]);
1821
2045
  const delta = Y.encodeStateAsUpdate(wsDoc, prevSV);
1822
2046
  await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(delta).toString("base64"));
1823
2047
  return text({ workspaceId, tag, created: true });
@@ -1857,17 +2081,11 @@ export function registerDocTools(server, gql, defaults) {
1857
2081
  if (!page) {
1858
2082
  throw new Error(`docId ${parsed.docId} is not present in workspace ${workspaceId}`);
1859
2083
  }
2084
+ const { option, created: optionCreated } = ensureWorkspaceTagOption(wsMeta, tag);
1860
2085
  const pageTags = ensureTagArray(page.entry);
1861
- const existedInDoc = findTagIndex(pageTags, tag, true) >= 0;
1862
- if (!existedInDoc) {
1863
- pageTags.push([tag]);
1864
- }
1865
- const registry = ensureTagArray(wsMeta);
1866
- const existedInRegistry = findTagIndex(registry, tag, true) >= 0;
1867
- if (!existedInRegistry) {
1868
- registry.push([tag]);
1869
- }
1870
- if (!existedInDoc || !existedInRegistry) {
2086
+ const pageSync = syncTagArrayToOption(pageTags, tag, option);
2087
+ const wsChanged = optionCreated || pageSync.changed;
2088
+ if (wsChanged) {
1871
2089
  const wsDelta = Y.encodeStateAsUpdate(wsDoc, wsPrevSV);
1872
2090
  await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(wsDelta).toString("base64"));
1873
2091
  }
@@ -1883,20 +2101,20 @@ export function registerDocTools(server, gql, defaults) {
1883
2101
  const docPrevSV = Y.encodeStateVector(doc);
1884
2102
  const docMeta = doc.getMap("meta");
1885
2103
  const docTags = ensureTagArray(docMeta);
1886
- const existedInDocMeta = findTagIndex(docTags, tag, true) >= 0;
1887
- if (!existedInDocMeta) {
1888
- docTags.push([tag]);
2104
+ const docSync = syncTagArrayToOption(docTags, tag, option);
2105
+ if (docSync.changed) {
1889
2106
  const docDelta = Y.encodeStateAsUpdate(doc, docPrevSV);
1890
2107
  await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(docDelta).toString("base64"));
1891
2108
  }
1892
2109
  docMetaSynced = true;
1893
2110
  }
2111
+ const { byId } = getWorkspaceTagOptionMaps(wsMeta);
1894
2112
  return text({
1895
2113
  workspaceId,
1896
2114
  docId: parsed.docId,
1897
2115
  tag,
1898
- added: !existedInDoc,
1899
- tags: getStringArray(pageTags),
2116
+ added: !pageSync.existed,
2117
+ tags: resolveTagLabels(getStringArray(pageTags), byId),
1900
2118
  docMetaSynced,
1901
2119
  warning,
1902
2120
  });
@@ -1937,10 +2155,11 @@ export function registerDocTools(server, gql, defaults) {
1937
2155
  if (!page) {
1938
2156
  throw new Error(`docId ${parsed.docId} is not present in workspace ${workspaceId}`);
1939
2157
  }
2158
+ const option = getWorkspaceTagOptionMaps(wsMeta).byValueLower.get(tag.toLocaleLowerCase()) || null;
1940
2159
  const pageTags = ensureTagArray(page.entry);
1941
- const pageTagIndex = findTagIndex(pageTags, tag, true);
1942
- if (pageTagIndex >= 0) {
1943
- pageTags.delete(pageTagIndex, 1);
2160
+ const pageTagIndexes = collectMatchingTagIndexes(pageTags, tag, option, true);
2161
+ const pageRemoved = deleteArrayIndexes(pageTags, pageTagIndexes);
2162
+ if (pageRemoved) {
1944
2163
  const wsDelta = Y.encodeStateAsUpdate(wsDoc, wsPrevSV);
1945
2164
  await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(wsDelta).toString("base64"));
1946
2165
  }
@@ -1956,20 +2175,20 @@ export function registerDocTools(server, gql, defaults) {
1956
2175
  const docPrevSV = Y.encodeStateVector(doc);
1957
2176
  const docMeta = doc.getMap("meta");
1958
2177
  const docTags = ensureTagArray(docMeta);
1959
- const docTagIndex = findTagIndex(docTags, tag, true);
1960
- if (docTagIndex >= 0) {
1961
- docTags.delete(docTagIndex, 1);
2178
+ const docTagIndexes = collectMatchingTagIndexes(docTags, tag, option, true);
2179
+ if (deleteArrayIndexes(docTags, docTagIndexes)) {
1962
2180
  const docDelta = Y.encodeStateAsUpdate(doc, docPrevSV);
1963
2181
  await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(docDelta).toString("base64"));
1964
2182
  }
1965
2183
  docMetaSynced = true;
1966
2184
  }
2185
+ const { byId } = getWorkspaceTagOptionMaps(wsMeta);
1967
2186
  return text({
1968
2187
  workspaceId,
1969
2188
  docId: parsed.docId,
1970
2189
  tag,
1971
- removed: pageTagIndex >= 0,
1972
- tags: getStringArray(pageTags),
2190
+ removed: pageRemoved,
2191
+ tags: resolveTagLabels(getStringArray(pageTags), byId),
1973
2192
  docMetaSynced,
1974
2193
  warning,
1975
2194
  });
@@ -2014,6 +2233,13 @@ export function registerDocTools(server, gql, defaults) {
2014
2233
  const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
2015
2234
  try {
2016
2235
  await joinWorkspace(socket, workspaceId);
2236
+ let tagOptionsById = new Map();
2237
+ const workspaceSnapshot = await loadDoc(socket, workspaceId, workspaceId);
2238
+ if (workspaceSnapshot.missing) {
2239
+ const workspaceDoc = new Y.Doc();
2240
+ Y.applyUpdate(workspaceDoc, Buffer.from(workspaceSnapshot.missing, "base64"));
2241
+ tagOptionsById = getWorkspaceTagOptionMaps(workspaceDoc.getMap("meta")).byId;
2242
+ }
2017
2243
  const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
2018
2244
  if (!snapshot.missing) {
2019
2245
  return text({
@@ -2029,7 +2255,7 @@ export function registerDocTools(server, gql, defaults) {
2029
2255
  const doc = new Y.Doc();
2030
2256
  Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
2031
2257
  const meta = doc.getMap("meta");
2032
- const tags = getStringArray(getTagArray(meta));
2258
+ const tags = resolveTagLabels(getStringArray(getTagArray(meta)), tagOptionsById);
2033
2259
  const blocks = doc.getMap("blocks");
2034
2260
  const pageId = findBlockIdByFlavour(blocks, "affine:page");
2035
2261
  const noteId = findBlockIdByFlavour(blocks, "affine:note");
@@ -2238,6 +2464,13 @@ export function registerDocTools(server, gql, defaults) {
2238
2464
  const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
2239
2465
  try {
2240
2466
  await joinWorkspace(socket, workspaceId);
2467
+ let tagOptionsById = new Map();
2468
+ const workspaceSnapshot = await loadDoc(socket, workspaceId, workspaceId);
2469
+ if (workspaceSnapshot.missing) {
2470
+ const wsDoc = new Y.Doc();
2471
+ Y.applyUpdate(wsDoc, Buffer.from(workspaceSnapshot.missing, "base64"));
2472
+ tagOptionsById = getWorkspaceTagOptionMaps(wsDoc.getMap("meta")).byId;
2473
+ }
2241
2474
  const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
2242
2475
  if (!snapshot.missing) {
2243
2476
  return text({
@@ -2256,7 +2489,7 @@ export function registerDocTools(server, gql, defaults) {
2256
2489
  }
2257
2490
  const doc = new Y.Doc();
2258
2491
  Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
2259
- const collected = collectDocForMarkdown(doc);
2492
+ const collected = collectDocForMarkdown(doc, tagOptionsById);
2260
2493
  const rendered = renderBlocksToMarkdown({
2261
2494
  rootBlockIds: collected.rootBlockIds,
2262
2495
  blocksById: collected.blocksById,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "affine-mcp-server",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
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.",
@@ -38,6 +38,7 @@
38
38
  "test:e2e": "bash tests/run-e2e.sh",
39
39
  "test:db-create": "node tests/test-database-creation.mjs",
40
40
  "test:bearer": "node tests/test-bearer-auth.mjs",
41
+ "test:tag-visibility": "node tests/test-tag-visibility.mjs",
41
42
  "test:playwright": "npx playwright test --config tests/playwright/playwright.config.ts",
42
43
  "pack:check": "npm pack --dry-run",
43
44
  "ci": "npm run build && npm run test:tool-manifest && npm run pack:check",