@abraca/mcp 2.4.0 → 2.5.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 ADDED
@@ -0,0 +1,32 @@
1
+ # @abraca/mcp
2
+
3
+ A Model Context Protocol server that exposes an Abracadabra space to AI tooling — documents, tree, content, metadata, files, chat, and presence over stdio. Bin: `abracadabra-mcp`.
4
+
5
+ ## Documentation
6
+
7
+ Full, code-derived documentation lives in [`docs/`](docs/) — a numbered Nuxt-Content
8
+ site covering startup/transport, every env var, all 27 tools, the 3 resources, schema
9
+ validation, and the channel/dispatch model + gotchas. It is the source of truth.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ ABRA_URL=https://my-server.example.com abracadabra-mcp
15
+ ```
16
+
17
+ Identity is an Ed25519 key file (`ABRA_KEY_FILE`, default `~/.abracadabra/agent.key`); the agent auto-registers on first run (`ABRA_INVITE_CODE` optional). See [`docs/1.getting-started/configuration`](docs/1.getting-started/2.configuration.md) for every variable.
18
+
19
+ ## Highlights
20
+
21
+ - **27 tools** — tree/documents, content/meta, files, channel (`reply`, `send_chat_message`), awareness, SVG, hooks.
22
+ - **3 resources** — `agent-guide` (the authoritative in-band reference), `server-info`, `tree`.
23
+ - **Opt-in schema validation** via `ABRA_MCP_SCHEMA_BUNDLE` (a `@abraca/schema` bundle).
24
+ - **Hook bridge** mirrors Claude Code tool activity into the agent's presence.
25
+
26
+ ::
27
+
28
+ > Plain text is invisible to users — chat replies go via `send_chat_message`, ai:task answers via `reply`. `read_document`'s body is **not** full content (children are content). See [`docs/4.reference/`](docs/4.reference/1.auth-channel-gotchas.md).
29
+
30
+ ## License
31
+
32
+ MIT.
@@ -14092,6 +14092,9 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14092
14092
  static {
14093
14093
  this.TOOL_HISTORY_MAX = 20;
14094
14094
  }
14095
+ static {
14096
+ this.DEDUPE_MAX = 1e3;
14097
+ }
14095
14098
  constructor(config) {
14096
14099
  this._serverInfo = null;
14097
14100
  this._rootDocId = null;
@@ -14109,6 +14112,14 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14109
14112
  this._lastChatChannel = null;
14110
14113
  this._signFn = null;
14111
14114
  this._toolHistory = [];
14115
+ this._inboxStarted = false;
14116
+ this._inboxDocId = null;
14117
+ this._inboxDoc = null;
14118
+ this._inboxProvider = null;
14119
+ this._inboxDisposers = [];
14120
+ this._inboxInitialized = false;
14121
+ this._seenInboxIds = /* @__PURE__ */ new Set();
14122
+ this._dispatchedMessageIds = /* @__PURE__ */ new Set();
14112
14123
  this.config = config;
14113
14124
  this.client = new _abraca_dabra.AbracadabraClient({
14114
14125
  url: config.url,
@@ -14338,6 +14349,182 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14338
14349
  this._handleStatelessChat(payload);
14339
14350
  });
14340
14351
  console.error("[abracadabra-mcp] Stateless chat listener attached");
14352
+ this._startInboxNotifications();
14353
+ }
14354
+ }
14355
+ /**
14356
+ * Bootstrap inbox observation. Sends `messages:inbox_fetch` over the active
14357
+ * provider; the server's `messages:inbox_history` reply carries the
14358
+ * `inbox_doc_id`. We then open a dedicated provider on that doc and observe
14359
+ * its `entries` Y.Array for live DM/mention dispatch. Mirrors the
14360
+ * dashboard's `useNotifications` pattern.
14361
+ */
14362
+ async _startInboxNotifications() {
14363
+ if (this._inboxStarted) return;
14364
+ const provider = this._activeConnection?.provider;
14365
+ if (!provider) return;
14366
+ this._inboxStarted = true;
14367
+ provider.on("stateless", ({ payload }) => {
14368
+ if (!payload.includes("\"messages:inbox_history\"")) return;
14369
+ try {
14370
+ const data = JSON.parse(payload);
14371
+ if (data?.type !== "messages:inbox_history") return;
14372
+ const inboxDocId = typeof data.inbox_doc_id === "string" ? data.inbox_doc_id : null;
14373
+ if (inboxDocId) this._ensureInboxObserver(inboxDocId);
14374
+ } catch {}
14375
+ });
14376
+ provider.sendStateless(JSON.stringify({
14377
+ type: "messages:inbox_fetch",
14378
+ limit: 50,
14379
+ unread_only: false
14380
+ }));
14381
+ console.error("[abracadabra-mcp] Inbox bootstrap sent (messages:inbox_fetch)");
14382
+ }
14383
+ /**
14384
+ * Open a dedicated provider on the agent's inbox doc and observe its
14385
+ * `entries` Y.Array. The server is the only writer; we only read.
14386
+ */
14387
+ async _ensureInboxObserver(inboxDocId) {
14388
+ if (this._inboxDocId) return;
14389
+ this._inboxDocId = inboxDocId;
14390
+ if (!this.client.isTokenValid() && this._signFn && this._userId) await this.client.loginWithKey(this._userId, this._signFn);
14391
+ const doc = new yjs.Doc({ guid: inboxDocId });
14392
+ const provider = new _abraca_dabra.AbracadabraProvider({
14393
+ name: inboxDocId,
14394
+ document: doc,
14395
+ client: this.client,
14396
+ disableOfflineStore: true,
14397
+ subdocLoading: "lazy"
14398
+ });
14399
+ this._inboxDoc = doc;
14400
+ this._inboxProvider = provider;
14401
+ try {
14402
+ await waitForSync(provider);
14403
+ } catch (err) {
14404
+ console.error(`[abracadabra-mcp] Inbox sync failed: ${err?.message ?? err}`);
14405
+ return;
14406
+ }
14407
+ const entriesArr = doc.getArray("entries");
14408
+ const readMap = doc.getMap("read");
14409
+ const onChange = () => {
14410
+ this._pumpInbox(entriesArr, readMap);
14411
+ };
14412
+ entriesArr.observe(onChange);
14413
+ readMap.observe(onChange);
14414
+ this._inboxDisposers.push(() => entriesArr.unobserve(onChange));
14415
+ this._inboxDisposers.push(() => readMap.unobserve(onChange));
14416
+ await this._pumpInbox(entriesArr, readMap);
14417
+ console.error(`[abracadabra-mcp] Inbox observer attached (${inboxDocId})`);
14418
+ }
14419
+ /**
14420
+ * Diff the inbox `entries` array against what we've already seen. On the
14421
+ * first pump we only record a baseline (don't replay history). New entries
14422
+ * are classified by their authoritative `kind` and dispatched.
14423
+ */
14424
+ async _pumpInbox(entriesArr, readMap) {
14425
+ const entries = entriesArr.toArray().map((e) => typeof e?.toJSON === "function" ? e.toJSON() : e);
14426
+ if (!this._inboxInitialized) {
14427
+ for (const e of entries) if (e?.id) this._seenInboxIds.add(e.id);
14428
+ this._inboxInitialized = true;
14429
+ console.error(`[abracadabra-mcp] Inbox baseline: ${this._seenInboxIds.size} existing entries (not replayed)`);
14430
+ return;
14431
+ }
14432
+ for (const e of entries) {
14433
+ const id = e?.id;
14434
+ if (!id || this._seenInboxIds.has(id)) continue;
14435
+ this._seenInboxIds.add(id);
14436
+ if (readMap.get(id)) continue;
14437
+ try {
14438
+ await this._dispatchInboxEntry(e);
14439
+ } catch (err) {
14440
+ console.error(`[abracadabra-mcp] Inbox dispatch failed for ${id}: ${err?.message ?? err}`);
14441
+ }
14442
+ }
14443
+ this._trimSet(this._seenInboxIds);
14444
+ }
14445
+ /** Classify + dispatch one inbox entry as a channel notification. */
14446
+ async _dispatchInboxEntry(entry) {
14447
+ if (!this._serverRef) return;
14448
+ const kind = typeof entry?.kind === "string" ? entry.kind : "";
14449
+ const channelDocId = typeof entry?.channel_doc_id === "string" ? entry.channel_doc_id : "";
14450
+ const messageId = typeof entry?.message_id === "string" ? entry.message_id : null;
14451
+ const senderId = typeof entry?.sender_id === "string" ? entry.sender_id : "";
14452
+ if (!channelDocId) return;
14453
+ if (senderId && senderId === this._userId) return;
14454
+ const mode = this.triggerMode;
14455
+ if (kind === "mention" || kind === "reply") {
14456
+ if (mode === "task") return;
14457
+ } else if (kind !== "dm") return;
14458
+ if (messageId && this._rememberDispatched(messageId)) return;
14459
+ const content = await this._resolveInboxContent(channelDocId, messageId, entry?.preview);
14460
+ this._lastChatChannel = channelDocId;
14461
+ this._beginTurn();
14462
+ this.setAutoStatus("thinking");
14463
+ await this._serverRef.notification({
14464
+ method: "notifications/claude/channel",
14465
+ params: {
14466
+ content,
14467
+ instructions: `You MUST use send_chat_message with channel_doc_id="${channelDocId}" for ALL responses — both progress updates and final answers. The user CANNOT see plain text output; they only see messages sent via send_chat_message. When doing multi-step work, send brief status updates via send_chat_message (e.g. "Looking into that..." or "Found it, writing up results...") so the user knows you're working. Never output plain text as a substitute for send_chat_message.`,
14468
+ meta: {
14469
+ source: "abracadabra",
14470
+ type: kind === "dm" ? "dm_message" : "chat_message",
14471
+ channel_doc_id: channelDocId,
14472
+ sender: entry?.sender_name ?? "Unknown",
14473
+ sender_id: senderId,
14474
+ doc_id: channelDocId
14475
+ }
14476
+ }
14477
+ });
14478
+ console.error(`[abracadabra-mcp] Dispatched ${kind} on ${channelDocId} from ${entry?.sender_name ?? (senderId || "unknown")}`);
14479
+ this._activeConnection?.provider?.sendStateless(JSON.stringify({
14480
+ type: "messages:inbox_mark_read",
14481
+ id: entry.id
14482
+ }));
14483
+ }
14484
+ /**
14485
+ * Resolve full message content. The inbox `preview` is truncated to ≤200
14486
+ * bytes (and null for E2E), so for fidelity we read the actual record from
14487
+ * the channel/DM doc's active period — same shape the dashboard reads.
14488
+ * Falls back to the preview, then a short notice.
14489
+ */
14490
+ async _resolveInboxContent(channelDocId, messageId, preview) {
14491
+ const previewStr = typeof preview === "string" && preview.length > 0 ? preview : null;
14492
+ const root = this._activeConnection?.provider;
14493
+ if (root && messageId) try {
14494
+ const wrapper = await root.loadChild(channelDocId);
14495
+ if (!wrapper.synced) await waitForSync(wrapper);
14496
+ const periods = wrapper.document.getArray("periods");
14497
+ const len = periods.length;
14498
+ for (let i = len - 1; i >= 0 && i >= len - 2; i--) {
14499
+ const p = periods.get(i);
14500
+ const periodId = p?.id ?? p?.get?.("id");
14501
+ if (!periodId) continue;
14502
+ const period = await root.loadChild(periodId);
14503
+ if (!period.synced) await waitForSync(period);
14504
+ const hit = (0, _abraca_dabra.foldRecords)(period.document.getArray("messages").toArray().map((v) => (0, _abraca_dabra.recordFromYAny)(v)).filter((r) => r !== null)).find((f) => f.id === messageId);
14505
+ if (hit) {
14506
+ if ((0, _abraca_dabra.isEncryptedContent)(hit.content)) return previewStr ?? "[encrypted message — this agent is not provisioned to read this channel]";
14507
+ return hit.content;
14508
+ }
14509
+ }
14510
+ } catch (err) {
14511
+ console.error(`[abracadabra-mcp] content read failed for ${channelDocId}/${messageId}: ${err?.message ?? err}`);
14512
+ }
14513
+ return previewStr ?? "[message content unavailable]";
14514
+ }
14515
+ _rememberDispatched(messageId) {
14516
+ if (this._dispatchedMessageIds.has(messageId)) return true;
14517
+ this._dispatchedMessageIds.add(messageId);
14518
+ this._trimSet(this._dispatchedMessageIds);
14519
+ return false;
14520
+ }
14521
+ _trimSet(s) {
14522
+ if (s.size <= AbracadabraMCPServer.DEDUPE_MAX) return;
14523
+ const excess = s.size - AbracadabraMCPServer.DEDUPE_MAX;
14524
+ let i = 0;
14525
+ for (const v of s) {
14526
+ if (i++ >= excess) break;
14527
+ s.delete(v);
14341
14528
  }
14342
14529
  }
14343
14530
  /** Attach awareness observer to detect `ai:task` fields from human users. */
@@ -14423,6 +14610,7 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14423
14610
  if (data.sender_id && data.sender_id === this._userId) return;
14424
14611
  const channelDocId = data.channel_doc_id;
14425
14612
  if (!channelDocId) return;
14613
+ if (data.id && this._rememberDispatched(data.id)) return;
14426
14614
  const mode = this.triggerMode;
14427
14615
  const content = typeof data.content === "string" ? data.content : "";
14428
14616
  let dispatchContent = content;
@@ -14573,6 +14761,13 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
14573
14761
  clearInterval(this.evictionTimer);
14574
14762
  this.evictionTimer = null;
14575
14763
  }
14764
+ for (const dispose of this._inboxDisposers) try {
14765
+ dispose();
14766
+ } catch {}
14767
+ this._inboxDisposers = [];
14768
+ this._inboxProvider?.destroy();
14769
+ this._inboxProvider = null;
14770
+ this._inboxDoc = null;
14576
14771
  for (const [, cached] of this.childCache) cached.provider.destroy();
14577
14772
  this.childCache.clear();
14578
14773
  for (const [, conn] of this._spaceConnections) {
@@ -15333,13 +15528,13 @@ function normalizeRootId(id, server) {
15333
15528
  return id === server.rootDocId ? null : id;
15334
15529
  }
15335
15530
  /** Safely read a tree map value, converting Y.Map to plain object if needed. */
15336
- function toPlain(val) {
15531
+ function toPlain$3(val) {
15337
15532
  return val instanceof yjs.Map ? val.toJSON() : val;
15338
15533
  }
15339
15534
  function readEntries$1(treeMap) {
15340
15535
  const entries = [];
15341
15536
  treeMap.forEach((raw, id) => {
15342
- const value = toPlain(raw);
15537
+ const value = toPlain$3(raw);
15343
15538
  if (typeof value !== "object" || value === null) return;
15344
15539
  entries.push({
15345
15540
  id,
@@ -15536,7 +15731,7 @@ function registerTreeTools(mcp, server, validator = null) {
15536
15731
  const normalizedParent = normalizeRootId(parentId, server);
15537
15732
  const now = Date.now();
15538
15733
  rootDoc.transact(() => {
15539
- treeMap.set(id, {
15734
+ treeMap.set(id, (0, _abraca_dabra.makeEntryMap)({
15540
15735
  label,
15541
15736
  parentId: normalizedParent,
15542
15737
  order: now,
@@ -15544,7 +15739,7 @@ function registerTreeTools(mcp, server, validator = null) {
15544
15739
  meta,
15545
15740
  createdAt: now,
15546
15741
  updatedAt: now
15547
- });
15742
+ }));
15548
15743
  });
15549
15744
  server.setFocusedDoc(id);
15550
15745
  return { content: [{
@@ -15571,14 +15766,11 @@ function registerTreeTools(mcp, server, validator = null) {
15571
15766
  type: "text",
15572
15767
  text: "Not connected"
15573
15768
  }] };
15574
- const raw = treeMap.get(id);
15575
- if (!raw) return { content: [{
15769
+ if (!treeMap.get(id)) return { content: [{
15576
15770
  type: "text",
15577
15771
  text: `Document ${id} not found`
15578
15772
  }] };
15579
- const entry = toPlain(raw);
15580
- treeMap.set(id, {
15581
- ...entry,
15773
+ (0, _abraca_dabra.patchEntry)(treeMap, id, {
15582
15774
  label,
15583
15775
  updatedAt: Date.now()
15584
15776
  });
@@ -15602,14 +15794,11 @@ function registerTreeTools(mcp, server, validator = null) {
15602
15794
  type: "text",
15603
15795
  text: "Not connected"
15604
15796
  }] };
15605
- const raw = treeMap.get(id);
15606
- if (!raw) return { content: [{
15797
+ if (!treeMap.get(id)) return { content: [{
15607
15798
  type: "text",
15608
15799
  text: `Document ${id} not found`
15609
15800
  }] };
15610
- const entry = toPlain(raw);
15611
- treeMap.set(id, {
15612
- ...entry,
15801
+ (0, _abraca_dabra.patchEntry)(treeMap, id, {
15613
15802
  parentId: normalizeRootId(newParentId, server),
15614
15803
  order: order ?? Date.now(),
15615
15804
  updatedAt: Date.now()
@@ -15638,7 +15827,7 @@ function registerTreeTools(mcp, server, validator = null) {
15638
15827
  for (const nid of toDelete) {
15639
15828
  const raw = treeMap.get(nid);
15640
15829
  if (!raw) continue;
15641
- const entry = toPlain(raw);
15830
+ const entry = toPlain$3(raw);
15642
15831
  trashMap.set(nid, {
15643
15832
  label: entry.label || "Untitled",
15644
15833
  parentId: entry.parentId ?? null,
@@ -15669,14 +15858,11 @@ function registerTreeTools(mcp, server, validator = null) {
15669
15858
  type: "text",
15670
15859
  text: "Not connected"
15671
15860
  }] };
15672
- const raw = treeMap.get(id);
15673
- if (!raw) return { content: [{
15861
+ if (!treeMap.get(id)) return { content: [{
15674
15862
  type: "text",
15675
15863
  text: `Document ${id} not found`
15676
15864
  }] };
15677
- const entry = toPlain(raw);
15678
- treeMap.set(id, {
15679
- ...entry,
15865
+ (0, _abraca_dabra.patchEntry)(treeMap, id, {
15680
15866
  type,
15681
15867
  updatedAt: Date.now()
15682
15868
  });
@@ -15719,13 +15905,13 @@ function registerTreeTools(mcp, server, validator = null) {
15719
15905
  type: "text",
15720
15906
  text: `Document ${id} not found`
15721
15907
  }] };
15722
- const entry = toPlain(raw);
15908
+ const entry = toPlain$3(raw);
15723
15909
  const newId = crypto.randomUUID();
15724
- treeMap.set(newId, {
15910
+ treeMap.set(newId, (0, _abraca_dabra.makeEntryMap)({
15725
15911
  ...entry,
15726
15912
  label: (entry.label || "Untitled") + " (copy)",
15727
15913
  order: Date.now()
15728
- });
15914
+ }));
15729
15915
  server.setFocusedDoc(newId);
15730
15916
  return { content: [{
15731
15917
  type: "text",
@@ -15771,31 +15957,37 @@ function registerContentTools(mcp, server) {
15771
15957
  name: "read_document",
15772
15958
  target: docId
15773
15959
  });
15774
- const { title, markdown } = (0, _abraca_convert.yjsToMarkdown)((await server.getChildProvider(docId)).document.getXmlFragment("default"));
15960
+ const fragment = (await server.getChildProvider(docId)).document.getXmlFragment("default");
15775
15961
  server.setFocusedDoc(docId);
15776
15962
  server.setDocCursor(docId, 0);
15777
15963
  const treeMap = server.getTreeMap();
15778
- let label = title;
15964
+ let label = "Untitled";
15779
15965
  let type;
15780
15966
  let meta;
15781
15967
  let children = [];
15782
15968
  if (treeMap) {
15783
- const entry = treeMap.get(docId);
15784
- if (entry) {
15785
- label = entry.label || title;
15969
+ const entry = (0, _abraca_dabra.toPlain)(treeMap.get(docId));
15970
+ if (entry && typeof entry === "object") {
15971
+ label = entry.label || label;
15786
15972
  type = entry.type;
15787
15973
  meta = entry.meta;
15788
15974
  }
15789
- treeMap.forEach((value, id) => {
15790
- if (value.parentId === docId) children.push({
15975
+ const collected = [];
15976
+ treeMap.forEach((raw, id) => {
15977
+ const value = (0, _abraca_dabra.toPlain)(raw);
15978
+ if (typeof value !== "object" || value === null) return;
15979
+ if (value.parentId === docId) collected.push({
15791
15980
  id,
15792
15981
  label: value.label || "Untitled",
15793
15982
  type: value.type,
15794
- meta: value.meta
15983
+ meta: value.meta,
15984
+ order: value.order ?? 0
15795
15985
  });
15796
15986
  });
15797
- children.sort((a, b) => (treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0));
15987
+ collected.sort((a, b) => a.order - b.order);
15988
+ children = collected.map(({ order, ...rest }) => rest);
15798
15989
  }
15990
+ const markdown = (0, _abraca_convert.yjsToMarkdown)(fragment, label, meta, type);
15799
15991
  const result = {
15800
15992
  label,
15801
15993
  type,
@@ -15836,19 +16028,19 @@ function registerContentTools(mcp, server) {
15836
16028
  const treeMap = server.getTreeMap();
15837
16029
  const rootDoc = server.rootDocument;
15838
16030
  if (treeMap && rootDoc) {
15839
- const entry = treeMap.get(docId);
15840
- if (entry) rootDoc.transact(() => {
15841
- const updates = {
15842
- ...entry,
15843
- updatedAt: Date.now()
15844
- };
15845
- if (title) updates.label = title;
15846
- if (Object.keys(meta).length > 0) updates.meta = {
15847
- ...entry.meta ?? {},
15848
- ...meta
15849
- };
15850
- treeMap.set(docId, updates);
15851
- });
16031
+ const rawEntry = treeMap.get(docId);
16032
+ if (rawEntry) {
16033
+ const cur = (0, _abraca_dabra.toPlain)(rawEntry);
16034
+ rootDoc.transact(() => {
16035
+ const patch = { updatedAt: Date.now() };
16036
+ if (title) patch.label = title;
16037
+ if (Object.keys(meta).length > 0) patch.meta = {
16038
+ ...cur.meta ?? {},
16039
+ ...meta
16040
+ };
16041
+ (0, _abraca_dabra.patchEntry)(treeMap, docId, patch);
16042
+ });
16043
+ }
15852
16044
  }
15853
16045
  }
15854
16046
  if (writeMode === "replace") doc.transact(() => {
@@ -15916,11 +16108,12 @@ function registerMetaTools(mcp, server, validator = null) {
15916
16108
  type: "text",
15917
16109
  text: "Not connected"
15918
16110
  }] };
15919
- const entry = treeMap.get(docId);
15920
- if (!entry) return { content: [{
16111
+ const rawEntry = treeMap.get(docId);
16112
+ if (!rawEntry) return { content: [{
15921
16113
  type: "text",
15922
16114
  text: `Document ${docId} not found`
15923
16115
  }] };
16116
+ const entry = (0, _abraca_dabra.toPlain)(rawEntry);
15924
16117
  const mergedMeta = {
15925
16118
  ...entry.meta ?? {},
15926
16119
  ...meta
@@ -15938,8 +16131,7 @@ function registerMetaTools(mcp, server, validator = null) {
15938
16131
  };
15939
16132
  }
15940
16133
  }
15941
- treeMap.set(docId, {
15942
- ...entry,
16134
+ (0, _abraca_dabra.patchEntry)(treeMap, docId, {
15943
16135
  meta: mergedMeta,
15944
16136
  updatedAt: Date.now()
15945
16137
  });
@@ -16129,7 +16321,7 @@ function registerAwarenessTools(mcp, server) {
16129
16321
  })
16130
16322
  }] };
16131
16323
  const resolvedInboxId = inboxId;
16132
- const { markdown } = (0, _abraca_convert.yjsToMarkdown)((await server.getChildProvider(resolvedInboxId)).document.getXmlFragment("default"));
16324
+ const markdown = (0, _abraca_convert.yjsToMarkdown)((await server.getChildProvider(resolvedInboxId)).document.getXmlFragment("default"), "AI Inbox");
16133
16325
  const pendingTasks = [];
16134
16326
  treeMap.forEach((value, id) => {
16135
16327
  if (value.parentId === resolvedInboxId) pendingTasks.push({
@@ -16215,16 +16407,16 @@ function registerChannelTools(mcp, server) {
16215
16407
  const replyId = crypto.randomUUID();
16216
16408
  const now = Date.now();
16217
16409
  rootDoc.transact(() => {
16218
- treeMap.set(replyId, {
16410
+ treeMap.set(replyId, (0, _abraca_dabra.makeEntryMap)({
16219
16411
  label,
16220
16412
  parentId: doc_id,
16221
16413
  order: now,
16222
16414
  type: "doc",
16223
16415
  createdAt: now,
16224
16416
  updatedAt: now
16225
- });
16417
+ }));
16226
16418
  });
16227
- (0, _abraca_convert.populateYDocFromMarkdown)((await server.getChildProvider(replyId)).document, text);
16419
+ (0, _abraca_convert.populateYDocFromMarkdown)((await server.getChildProvider(replyId)).document.getXmlFragment("default"), text);
16228
16420
  if (task_id) server.clearAiTask(task_id);
16229
16421
  return { content: [{
16230
16422
  type: "text",
@@ -16998,9 +17190,14 @@ function registerAgentGuide(mcp) {
16998
17190
 
16999
17191
  //#endregion
17000
17192
  //#region packages/mcp/src/resources/tree-resource.ts
17193
+ /**
17194
+ * Dynamic resource: document tree as navigable JSON.
17195
+ */
17001
17196
  function readEntries(treeMap) {
17002
17197
  const entries = [];
17003
- treeMap.forEach((value, id) => {
17198
+ treeMap.forEach((raw, id) => {
17199
+ const value = (0, _abraca_dabra.toPlain)(raw);
17200
+ if (typeof value !== "object" || value === null) return;
17004
17201
  entries.push({
17005
17202
  id,
17006
17203
  label: value.label || "Untitled",