@abraca/mcp 2.8.0 → 2.10.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.
@@ -12806,10 +12806,10 @@ var StdioServerTransport = class {
12806
12806
  * fields (status, activeToolCall) so the cou-sh dashboard shows real-time activity.
12807
12807
  */
12808
12808
  /** Per-field cap for detail strings (chars). Keeps awareness lean. */
12809
- const DETAIL_MAX = 4e3;
12810
- function cap(s) {
12809
+ const DETAIL_MAX$1 = 4e3;
12810
+ function cap$1(s) {
12811
12811
  if (typeof s !== "string") return void 0;
12812
- return s.length > DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n… (truncated)` : s;
12812
+ return s.length > DETAIL_MAX$1 ? `${s.slice(0, DETAIL_MAX$1)}\n… (truncated)` : s;
12813
12813
  }
12814
12814
  /** Map Claude Code tool names to awareness-friendly names + target + detail. */
12815
12815
  function mapToolCall(toolName, toolInput) {
@@ -12819,7 +12819,7 @@ function mapToolCall(toolName, toolInput) {
12819
12819
  target: truncate(toolInput.command ?? toolInput.description, 60),
12820
12820
  detail: {
12821
12821
  kind: "command",
12822
- text: cap(toolInput.command)
12822
+ text: cap$1(toolInput.command)
12823
12823
  }
12824
12824
  };
12825
12825
  case "Read": return {
@@ -12843,8 +12843,8 @@ function mapToolCall(toolName, toolInput) {
12843
12843
  detail: {
12844
12844
  kind: "diff",
12845
12845
  file: toolInput.file_path,
12846
- old: cap(oldText),
12847
- new: cap(newText)
12846
+ old: cap$1(oldText),
12847
+ new: cap$1(newText)
12848
12848
  }
12849
12849
  };
12850
12850
  }
@@ -12854,7 +12854,7 @@ function mapToolCall(toolName, toolInput) {
12854
12854
  detail: {
12855
12855
  kind: "code",
12856
12856
  file: toolInput.file_path,
12857
- text: cap(toolInput.content)
12857
+ text: cap$1(toolInput.content)
12858
12858
  }
12859
12859
  };
12860
12860
  case "Grep": return {
@@ -12878,7 +12878,7 @@ function mapToolCall(toolName, toolInput) {
12878
12878
  target: toolInput.description || toolInput.subagent_type || "agent",
12879
12879
  detail: {
12880
12880
  kind: "text",
12881
- text: cap(toolInput.prompt)
12881
+ text: cap$1(toolInput.prompt)
12882
12882
  }
12883
12883
  };
12884
12884
  case "WebFetch": return {
@@ -21425,8 +21425,16 @@ function stripMention(text, aliases) {
21425
21425
  //#region packages/mcp/src/utils.ts
21426
21426
  /**
21427
21427
  * Wait for a provider's `synced` event with a timeout.
21428
+ *
21429
+ * Mirrors `@abraca/dabra`'s `DocUtils.waitForSync`: the `isSynced` short-circuit
21430
+ * is load-bearing. `synced` is emitted once per sync transition (and deferred a
21431
+ * microtask), so a provider that has ALREADY synced — e.g. the cached child
21432
+ * provider that `loadChild` returns on a repeat read/write of the same doc —
21433
+ * will never re-emit. Without this guard those callers block for the full
21434
+ * `timeoutMs` ("works once, then every later op on that doc times out at 15s").
21428
21435
  */
21429
21436
  function waitForSync(provider, timeoutMs = 15e3) {
21437
+ if (provider.isSynced) return Promise.resolve();
21430
21438
  return new Promise((resolve, reject) => {
21431
21439
  const timer = setTimeout(() => {
21432
21440
  provider.off("synced", handler);
@@ -21434,6 +21442,7 @@ function waitForSync(provider, timeoutMs = 15e3) {
21434
21442
  }, timeoutMs);
21435
21443
  function handler() {
21436
21444
  clearTimeout(timer);
21445
+ provider.off("synced", handler);
21437
21446
  resolve();
21438
21447
  }
21439
21448
  provider.on("synced", handler);
@@ -21614,6 +21623,46 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
21614
21623
  await this._connectToSpace(docId);
21615
21624
  console.error(`[abracadabra-mcp] Switched active space to ${docId}`);
21616
21625
  }
21626
+ /**
21627
+ * Make the space that OWNS `targetId` the active connection before a tree
21628
+ * read/write, so the op targets the right space's `doc-tree` map.
21629
+ *
21630
+ * Without this, every tree tool used `getTreeMap()` (the single active
21631
+ * space) and treated `rootId`/`parentId` as a mere filter — so reading a
21632
+ * non-active space returned stale/empty trees, and *writing* to one
21633
+ * silently orphaned the entry into the active space's map (a real
21634
+ * data-loss path: docs built "in Ideas" while Development was active
21635
+ * landed in Development under a non-node parent and never reached Ideas).
21636
+ *
21637
+ * Resolution order:
21638
+ * - falsy or already the active root → no-op.
21639
+ * - a known Space id → connect/activate it (cheap; `_spaceConnections`
21640
+ * caches the provider so re-activation is free).
21641
+ * - present in the active space's tree → no-op (a normal child doc).
21642
+ * - present in an already-connected *other* space's tree → activate that.
21643
+ * - otherwise unresolved → returns `false`; callers decide (reads report
21644
+ * not-found/empty, writes refuse rather than orphan).
21645
+ *
21646
+ * Switching uses {@link _connectToSpace} (not {@link switchSpace}) so the
21647
+ * child-content provider cache survives — content docs are keyed by global
21648
+ * guid, independent of which space is active.
21649
+ */
21650
+ async ensureSpaceActive(targetId) {
21651
+ if (!targetId || targetId === this._rootDocId) return true;
21652
+ if (this._spaces.some((s) => s.id === targetId)) {
21653
+ await this._connectToSpace(targetId);
21654
+ return true;
21655
+ }
21656
+ if (this.getTreeMap()?.has(targetId)) return true;
21657
+ for (const [spaceId, conn] of this._spaceConnections) {
21658
+ if (spaceId === this._rootDocId) continue;
21659
+ if (conn.doc.getMap("doc-tree").has(targetId)) {
21660
+ await this._connectToSpace(spaceId);
21661
+ return true;
21662
+ }
21663
+ }
21664
+ return false;
21665
+ }
21617
21666
  /** Get the root doc-tree Y.Map of the active space. */
21618
21667
  getTreeMap() {
21619
21668
  return this._activeConnection?.doc.getMap("doc-tree") ?? null;
@@ -22151,6 +22200,28 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
22151
22200
  }
22152
22201
  }
22153
22202
  /**
22203
+ * Attach expandable `detail` (arguments + result summary) to the most recent
22204
+ * tool-history entry for `toolName`, then re-broadcast `toolHistory`.
22205
+ *
22206
+ * Called by the central tool wrapper in index.ts *after* a tool resolves, so
22207
+ * the dashboard's tool cards can expand to show what the tool was called with
22208
+ * and what it returned — the same affordance built-in Claude tools (bash /
22209
+ * read / edit) already get via the hook-bridge. Tools that never emitted a
22210
+ * pill (no `setActiveToolCall`) have no entry to attach to, so this no-ops.
22211
+ */
22212
+ recordToolDetail(toolName, detail) {
22213
+ const provider = this._activeConnection?.provider;
22214
+ if (!provider) return;
22215
+ for (let i = this._toolHistory.length - 1; i >= 0; i--) {
22216
+ const h = this._toolHistory[i];
22217
+ if (h.tool === toolName && h.detail == null) {
22218
+ h.detail = detail;
22219
+ provider.awareness.setLocalStateField("toolHistory", [...this._toolHistory]);
22220
+ return;
22221
+ }
22222
+ }
22223
+ }
22224
+ /**
22154
22225
  * Send a typing indicator to a chat channel. Pass the channel doc id
22155
22226
  * (or for legacy callers, a `group:<docId>` string — we strip the prefix).
22156
22227
  */
@@ -22198,6 +22269,123 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
22198
22269
  }
22199
22270
  };
22200
22271
 
22272
+ //#endregion
22273
+ //#region packages/mcp/src/tool-detail.ts
22274
+ /**
22275
+ * tool-detail — central enrichment of abracadabra MCP tool cards.
22276
+ *
22277
+ * Built-in Claude tools (Bash/Read/Edit/…) flow through the hook-bridge, which
22278
+ * attaches an expandable `detail` (command text, file path, diff) that the
22279
+ * cou-sh chat renders as a click-to-expand block. abracadabra's own MCP tools
22280
+ * are explicitly skipped by the hook-bridge and only ever emitted a bare
22281
+ * name+target pill — so their cards had nothing to expand ("no idea what docs
22282
+ * it sees or what the tree was").
22283
+ *
22284
+ * `wrapToolsWithDetail` patches `McpServer.tool` ONCE, before any tool is
22285
+ * registered, so every tool invocation records an expandable detail onto its
22286
+ * tool-history entry (via `server.recordToolDetail`). The detail leads with the
22287
+ * human-readable title + id of any document the call referenced (resolved from
22288
+ * the live tree), then the remaining arguments, then the text the tool returned
22289
+ * (e.g. the full document tree for `get_document_tree`). The pill itself is
22290
+ * still emitted by each tool's own `setActiveToolCall`; this only fills in the
22291
+ * expandable detail afterwards.
22292
+ */
22293
+ /** Per-field cap (chars) — keeps the awareness payload bounded. Mirrors the
22294
+ * hook-bridge's DETAIL_MAX so built-in and MCP tool cards behave alike. */
22295
+ const DETAIL_MAX = 4e3;
22296
+ /** Argument keys that hold a document id, in the order we surface them. */
22297
+ const DOC_ID_KEYS = [
22298
+ "docId",
22299
+ "id",
22300
+ "rootId",
22301
+ "parentId",
22302
+ "newParentId",
22303
+ "uploadId"
22304
+ ];
22305
+ function cap(s) {
22306
+ return s.length > DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n… (truncated)` : s;
22307
+ }
22308
+ function safeJson(value) {
22309
+ try {
22310
+ return JSON.stringify(value);
22311
+ } catch {
22312
+ return String(value);
22313
+ }
22314
+ }
22315
+ /** Pull the joined text content out of a tool result, if any. */
22316
+ function resultText(result) {
22317
+ const content = result?.content;
22318
+ if (!Array.isArray(content)) return "";
22319
+ return content.filter((c) => c && c.type === "text").map((c) => c.text ?? "").join("\n").trim();
22320
+ }
22321
+ /**
22322
+ * Build the expandable detail shown when a tool card is clicked:
22323
+ * 1. one `id — "Title"` line per document the args referenced (title resolved
22324
+ * from the live tree when available),
22325
+ * 2. the remaining arguments as compact JSON,
22326
+ * 3. the text the tool returned (document tree, content, confirmation, …).
22327
+ * Rendered by cou-sh's ChatActionCard as a `kind: "text"` block.
22328
+ */
22329
+ function buildToolDetail(toolArgs, result, resolveLabel) {
22330
+ const lines = [];
22331
+ const rest = toolArgs && typeof toolArgs === "object" ? { ...toolArgs } : {};
22332
+ for (const key of DOC_ID_KEYS) {
22333
+ const v = rest[key];
22334
+ if (typeof v === "string" && v.length > 0) {
22335
+ const label = resolveLabel?.(v);
22336
+ lines.push(label ? `${key}: ${v} — "${label}"` : `${key}: ${v}`);
22337
+ delete rest[key];
22338
+ }
22339
+ }
22340
+ const restStr = Object.keys(rest).length > 0 ? safeJson(rest) : "";
22341
+ if (restStr && restStr !== "{}") lines.push(`args: ${restStr}`);
22342
+ if (lines.length === 0) lines.push("args: (no arguments)");
22343
+ const text = resultText(result);
22344
+ if (text) {
22345
+ lines.push("");
22346
+ lines.push(text);
22347
+ }
22348
+ return {
22349
+ kind: "text",
22350
+ text: cap(lines.join("\n"))
22351
+ };
22352
+ }
22353
+ /**
22354
+ * Monkey-patch `mcp.tool` so each registered tool's handler is wrapped to record
22355
+ * arguments + result detail after it resolves. Call once at startup, before the
22356
+ * register* functions run. Errors in detail-recording are swallowed — they must
22357
+ * never break the underlying tool.
22358
+ */
22359
+ function wrapToolsWithDetail(mcp, server) {
22360
+ const resolveLabel = (id) => {
22361
+ try {
22362
+ const raw = server.getTreeMap()?.get(id);
22363
+ if (!raw) return void 0;
22364
+ const label = (0, _abraca_dabra.toPlain)(raw)?.label;
22365
+ return typeof label === "string" && label.length > 0 ? label : void 0;
22366
+ } catch {
22367
+ return;
22368
+ }
22369
+ };
22370
+ const original = mcp.tool.bind(mcp);
22371
+ mcp.tool = (...args) => {
22372
+ const name = args[0];
22373
+ const cbIndex = args.length - 1;
22374
+ const cb = args[cbIndex];
22375
+ if (typeof name === "string" && typeof cb === "function") {
22376
+ const handler = cb;
22377
+ args[cbIndex] = async (...handlerArgs) => {
22378
+ const result = await handler(...handlerArgs);
22379
+ try {
22380
+ server.recordToolDetail(name, buildToolDetail(handlerArgs[0], result, resolveLabel));
22381
+ } catch {}
22382
+ return result;
22383
+ };
22384
+ }
22385
+ return original(...args);
22386
+ };
22387
+ }
22388
+
22201
22389
  //#endregion
22202
22390
  //#region packages/mcp/src/tools/awareness.ts
22203
22391
  function registerAwarenessTools(mcp, server) {
@@ -22433,6 +22621,7 @@ function registerContentTools(mcp, server) {
22433
22621
  name: "read_document",
22434
22622
  target: docId
22435
22623
  });
22624
+ await server.ensureSpaceActive(docId);
22436
22625
  const fragment = (await server.getChildProvider(docId)).document.getXmlFragment("default");
22437
22626
  server.setFocusedDoc(docId);
22438
22627
  server.setDocCursor(docId, 0);
@@ -22496,6 +22685,7 @@ function registerContentTools(mcp, server) {
22496
22685
  name: "write_document",
22497
22686
  target: docId
22498
22687
  });
22688
+ await server.ensureSpaceActive(docId);
22499
22689
  const writeMode = mode ?? "replace";
22500
22690
  const doc = (await server.getChildProvider(docId)).document;
22501
22691
  const fragment = doc.getXmlFragment("default");
@@ -22522,7 +22712,9 @@ function registerContentTools(mcp, server) {
22522
22712
  if (writeMode === "replace") doc.transact(() => {
22523
22713
  while (fragment.length > 0) fragment.delete(0);
22524
22714
  });
22525
- (0, _abraca_convert.populateYDocFromMarkdown)(fragment, body || markdown, title || "Untitled");
22715
+ const contentToWrite = body || markdown;
22716
+ const existingLabel = server.getDocSummary(docId)?.label;
22717
+ (0, _abraca_convert.populateYDocFromMarkdown)(fragment, contentToWrite, title || existingLabel || "Untitled");
22526
22718
  server.setFocusedDoc(docId);
22527
22719
  server.setDocCursor(docId, fragment.length);
22528
22720
  return { content: [{
@@ -22703,6 +22895,7 @@ function registerMetaTools(mcp, server, validator = null) {
22703
22895
  name: "get_metadata",
22704
22896
  target: docId
22705
22897
  });
22898
+ await server.ensureSpaceActive(docId);
22706
22899
  const treeMap = server.getTreeMap();
22707
22900
  if (!treeMap) return { content: [{
22708
22901
  type: "text",
@@ -22732,6 +22925,7 @@ function registerMetaTools(mcp, server, validator = null) {
22732
22925
  name: "update_metadata",
22733
22926
  target: docId
22734
22927
  });
22928
+ await server.ensureSpaceActive(docId);
22735
22929
  const treeMap = server.getTreeMap();
22736
22930
  if (!treeMap) return { content: [{
22737
22931
  type: "text",
@@ -23688,9 +23882,22 @@ function normalizeRootId(id, server) {
23688
23882
  function toPlain(val) {
23689
23883
  return val instanceof yjs.Map ? val.toJSON() : val;
23690
23884
  }
23691
- function readEntries(treeMap) {
23885
+ /**
23886
+ * Read every doc-tree entry, optionally skipping the space's self-descriptor.
23887
+ *
23888
+ * A space's own `doc-tree` Y.Map holds an entry keyed by the space's OWN id
23889
+ * (the connected root doc), with `parentId: null` and `order: -1`. It stores
23890
+ * the space root's own icon/color/type/label — it is NOT a child document.
23891
+ * Because its `parentId` is null it otherwise shows up alongside real
23892
+ * top-level docs, making the space render as a phantom first child of itself
23893
+ * ("Health space's first document is 'Development'"). Pass `selfId =
23894
+ * server.rootDocId` to drop it. cou-sh's DocumentTree applies the same guard
23895
+ * (`if (id === spaceDocId) continue`).
23896
+ */
23897
+ function readEntries(treeMap, selfId) {
23692
23898
  const entries = [];
23693
23899
  treeMap.forEach((raw, id) => {
23900
+ if (selfId && id === selfId) return;
23694
23901
  const value = toPlain(raw);
23695
23902
  if (typeof value !== "object" || value === null) return;
23696
23903
  entries.push({
@@ -23742,13 +23949,14 @@ function registerTreeTools(mcp, server, validator = null) {
23742
23949
  mcp.tool("list_documents", "List direct children of a document (defaults to root). Returns id, label, type, meta, order. NOTE: Only returns ONE level. Use find_document to search by name across the full tree, or get_document_tree to see the complete hierarchy.", { parentId: zod.z.string().optional().describe("Parent document ID. Omit for root-level documents.") }, async ({ parentId }) => {
23743
23950
  server.setAutoStatus("reading");
23744
23951
  server.setActiveToolCall({ name: "list_documents" });
23952
+ await server.ensureSpaceActive(parentId);
23745
23953
  const treeMap = server.getTreeMap();
23746
23954
  if (!treeMap) return { content: [{
23747
23955
  type: "text",
23748
23956
  text: "Not connected"
23749
23957
  }] };
23750
23958
  const targetId = normalizeRootId(parentId, server);
23751
- const children = childrenOf(readEntries(treeMap), targetId);
23959
+ const children = childrenOf(readEntries(treeMap, server.rootDocId), targetId);
23752
23960
  return { content: [{
23753
23961
  type: "text",
23754
23962
  text: JSON.stringify(children, null, 2)
@@ -23760,6 +23968,7 @@ function registerTreeTools(mcp, server, validator = null) {
23760
23968
  }, async ({ rootId, depth }) => {
23761
23969
  server.setAutoStatus("reading");
23762
23970
  server.setActiveToolCall({ name: "get_document_tree" });
23971
+ await server.ensureSpaceActive(rootId);
23763
23972
  const treeMap = server.getTreeMap();
23764
23973
  if (!treeMap) return { content: [{
23765
23974
  type: "text",
@@ -23767,7 +23976,7 @@ function registerTreeTools(mcp, server, validator = null) {
23767
23976
  }] };
23768
23977
  const targetId = normalizeRootId(rootId, server);
23769
23978
  const maxDepth = depth ?? 3;
23770
- const tree = buildTree(readEntries(treeMap), targetId, maxDepth);
23979
+ const tree = buildTree(readEntries(treeMap, server.rootDocId), targetId, maxDepth);
23771
23980
  return { content: [{
23772
23981
  type: "text",
23773
23982
  text: JSON.stringify(tree, null, 2)
@@ -23782,12 +23991,13 @@ function registerTreeTools(mcp, server, validator = null) {
23782
23991
  name: "find_document",
23783
23992
  target: query
23784
23993
  });
23994
+ await server.ensureSpaceActive(rootId);
23785
23995
  const treeMap = server.getTreeMap();
23786
23996
  if (!treeMap) return { content: [{
23787
23997
  type: "text",
23788
23998
  text: "Not connected"
23789
23999
  }] };
23790
- const entries = readEntries(treeMap);
24000
+ const entries = readEntries(treeMap, server.rootDocId);
23791
24001
  const lowerQuery = query.toLowerCase();
23792
24002
  const normalizedRoot = normalizeRootId(rootId, server);
23793
24003
  const matches = (normalizedRoot ? descendantsOf(entries, normalizedRoot) : entries).filter((e) => e.label.toLowerCase().includes(lowerQuery));
@@ -23868,6 +24078,7 @@ function registerTreeTools(mcp, server, validator = null) {
23868
24078
  name: "create_document",
23869
24079
  target: label
23870
24080
  });
24081
+ await server.ensureSpaceActive(parentId);
23871
24082
  const treeMap = server.getTreeMap();
23872
24083
  const rootDoc = server.rootDocument;
23873
24084
  if (!treeMap || !rootDoc) return { content: [{
@@ -23887,6 +24098,30 @@ function registerTreeTools(mcp, server, validator = null) {
23887
24098
  const id = crypto.randomUUID();
23888
24099
  const normalizedParent = normalizeRootId(parentId, server);
23889
24100
  const now = Date.now();
24101
+ if (normalizedParent && !treeMap.has(normalizedParent)) return {
24102
+ content: [{
24103
+ type: "text",
24104
+ text: `Parent "${parentId}" was not found in any space the MCP can currently reach (active space: ${server.rootDocId}). Refusing to create — writing here would orphan the document. If the parent lives in another space, call switch_space to that space first, then retry.`
24105
+ }],
24106
+ isError: true
24107
+ };
24108
+ const restParent = normalizedParent ?? server.rootDocId;
24109
+ if (restParent) try {
24110
+ await server.client.createChild(restParent, {
24111
+ child_id: id,
24112
+ label,
24113
+ doc_type: type,
24114
+ kind: "page"
24115
+ });
24116
+ } catch (e) {
24117
+ return {
24118
+ content: [{
24119
+ type: "text",
24120
+ text: `Failed to register document ${id} under ${restParent}: ${e instanceof Error ? e.message : String(e)}`
24121
+ }],
24122
+ isError: true
24123
+ };
24124
+ }
23890
24125
  rootDoc.transact(() => {
23891
24126
  treeMap.set(id, (0, _abraca_dabra.makeEntryMap)({
23892
24127
  label,
@@ -23918,6 +24153,7 @@ function registerTreeTools(mcp, server, validator = null) {
23918
24153
  name: "rename_document",
23919
24154
  target: id
23920
24155
  });
24156
+ await server.ensureSpaceActive(id);
23921
24157
  const treeMap = server.getTreeMap();
23922
24158
  if (!treeMap) return { content: [{
23923
24159
  type: "text",
@@ -23931,9 +24167,15 @@ function registerTreeTools(mcp, server, validator = null) {
23931
24167
  label,
23932
24168
  updatedAt: Date.now()
23933
24169
  });
24170
+ let restNote = "";
24171
+ try {
24172
+ await server.client.updateDocumentMeta(id, { label });
24173
+ } catch (e) {
24174
+ restNote = ` (registry update failed: ${e instanceof Error ? e.message : String(e)})`;
24175
+ }
23934
24176
  return { content: [{
23935
24177
  type: "text",
23936
- text: `Renamed to "${label}"`
24178
+ text: `Renamed to "${label}"${restNote}`
23937
24179
  }] };
23938
24180
  });
23939
24181
  mcp.tool("move_document", "Move a document to a new parent and/or reorder it.", {
@@ -23946,6 +24188,7 @@ function registerTreeTools(mcp, server, validator = null) {
23946
24188
  name: "move_document",
23947
24189
  target: id
23948
24190
  });
24191
+ await server.ensureSpaceActive(id);
23949
24192
  const treeMap = server.getTreeMap();
23950
24193
  if (!treeMap) return { content: [{
23951
24194
  type: "text",
@@ -23955,14 +24198,29 @@ function registerTreeTools(mcp, server, validator = null) {
23955
24198
  type: "text",
23956
24199
  text: `Document ${id} not found`
23957
24200
  }] };
24201
+ const normalizedParent = normalizeRootId(newParentId, server);
24202
+ if (normalizedParent && !treeMap.has(normalizedParent)) return {
24203
+ content: [{
24204
+ type: "text",
24205
+ text: `New parent "${newParentId}" is not in the same space as "${id}" (active space: ${server.rootDocId}). Cross-space moves aren't supported — refusing to avoid orphaning the document.`
24206
+ }],
24207
+ isError: true
24208
+ };
23958
24209
  (0, _abraca_dabra.patchEntry)(treeMap, id, {
23959
- parentId: normalizeRootId(newParentId, server),
24210
+ parentId: normalizedParent,
23960
24211
  order: order ?? Date.now(),
23961
24212
  updatedAt: Date.now()
23962
24213
  });
24214
+ const restParent = normalizedParent ?? server.rootDocId;
24215
+ let restNote = "";
24216
+ if (restParent) try {
24217
+ await server.client.updateDocumentMeta(id, { parent_id: restParent });
24218
+ } catch (e) {
24219
+ restNote = ` (registry reparent failed: ${e instanceof Error ? e.message : String(e)})`;
24220
+ }
23963
24221
  return { content: [{
23964
24222
  type: "text",
23965
- text: `Moved ${id} to parent ${newParentId}`
24223
+ text: `Moved ${id} to parent ${newParentId}${restNote}`
23966
24224
  }] };
23967
24225
  });
23968
24226
  mcp.tool("delete_document", "Soft-delete a document and its descendants (moves to trash).", { id: zod.z.string().describe("Document ID to delete.") }, async ({ id }) => {
@@ -23971,6 +24229,7 @@ function registerTreeTools(mcp, server, validator = null) {
23971
24229
  name: "delete_document",
23972
24230
  target: id
23973
24231
  });
24232
+ await server.ensureSpaceActive(id);
23974
24233
  const treeMap = server.getTreeMap();
23975
24234
  const trashMap = server.getTrashMap();
23976
24235
  const rootDoc = server.rootDocument;
@@ -23978,7 +24237,7 @@ function registerTreeTools(mcp, server, validator = null) {
23978
24237
  type: "text",
23979
24238
  text: "Not connected"
23980
24239
  }] };
23981
- const toDelete = [id, ...descendantsOf(readEntries(treeMap), id).map((e) => e.id)];
24240
+ const toDelete = [id, ...descendantsOf(readEntries(treeMap, server.rootDocId), id).map((e) => e.id)];
23982
24241
  const now = Date.now();
23983
24242
  rootDoc.transact(() => {
23984
24243
  for (const nid of toDelete) {
@@ -23996,9 +24255,15 @@ function registerTreeTools(mcp, server, validator = null) {
23996
24255
  treeMap.delete(nid);
23997
24256
  }
23998
24257
  });
24258
+ let restNote = "";
24259
+ try {
24260
+ await server.client.deleteDoc(id);
24261
+ } catch (e) {
24262
+ restNote = ` (registry delete failed: ${e instanceof Error ? e.message : String(e)})`;
24263
+ }
23999
24264
  return { content: [{
24000
24265
  type: "text",
24001
- text: `Deleted ${toDelete.length} document(s)`
24266
+ text: `Deleted ${toDelete.length} document(s)${restNote}`
24002
24267
  }] };
24003
24268
  });
24004
24269
  mcp.tool("change_document_type", "Change the page type view of a document (data is preserved).", {
@@ -24010,6 +24275,7 @@ function registerTreeTools(mcp, server, validator = null) {
24010
24275
  name: "change_document_type",
24011
24276
  target: id
24012
24277
  });
24278
+ await server.ensureSpaceActive(id);
24013
24279
  const treeMap = server.getTreeMap();
24014
24280
  if (!treeMap) return { content: [{
24015
24281
  type: "text",
@@ -24029,6 +24295,7 @@ function registerTreeTools(mcp, server, validator = null) {
24029
24295
  }] };
24030
24296
  });
24031
24297
  mcp.tool("list_spaces", "List all Spaces available on the server. Spaces are top-level documents (direct children of the server root) tagged with kind=\"space\". Returns id, label, public_access, and active flag. Use switch_space to navigate.", {}, async () => {
24298
+ server.setActiveToolCall({ name: "list_spaces" });
24032
24299
  const spaces = server.spaces;
24033
24300
  if (!spaces.length) return { content: [{
24034
24301
  type: "text",
@@ -24045,6 +24312,10 @@ function registerTreeTools(mcp, server, validator = null) {
24045
24312
  }] };
24046
24313
  });
24047
24314
  mcp.tool("switch_space", "Switch the active Space. All subsequent tree operations will target the new Space. Use list_spaces to discover available ids.", { docId: zod.z.string().describe("The id of the Space to switch to (from list_spaces).") }, async ({ docId }) => {
24315
+ server.setActiveToolCall({
24316
+ name: "switch_space",
24317
+ target: docId
24318
+ });
24048
24319
  await server.switchSpace(docId);
24049
24320
  return { content: [{
24050
24321
  type: "text",
@@ -24052,6 +24323,11 @@ function registerTreeTools(mcp, server, validator = null) {
24052
24323
  }] };
24053
24324
  });
24054
24325
  mcp.tool("duplicate_document", "Shallow-clone a document with \" (copy)\" suffix. Returns new document ID.", { id: zod.z.string().describe("Document ID to duplicate.") }, async ({ id }) => {
24326
+ server.setActiveToolCall({
24327
+ name: "duplicate_document",
24328
+ target: id
24329
+ });
24330
+ await server.ensureSpaceActive(id);
24055
24331
  const treeMap = server.getTreeMap();
24056
24332
  if (!treeMap) return { content: [{
24057
24333
  type: "text",
@@ -24079,6 +24355,10 @@ function registerTreeTools(mcp, server, validator = null) {
24079
24355
  }] };
24080
24356
  });
24081
24357
  mcp.tool("list_page_types", "Enumerate all known Abracadabra page types with their metadata schemas. Returns an array of { key, label, icon, description, core, plugin, supportsChildren, childLabel, grandchildLabel, defaultDepth, metaSchema, defaultMetaFields }. `metaSchema` describes fields that apply to DESCENDANTS (children, grandchildren, ...) of a page of this type. `defaultMetaFields` are view-config fields on the page doc itself. Plugin types (core: false) require the named plugin to be enabled on the server. Use this to discover what meta keys a given renderer supports instead of guessing.", { key: zod.z.string().optional().describe("Filter to a single type by key (e.g. \"kanban\", \"calendar\"). Aliases are resolved (e.g. \"desktop\" → \"dashboard\"). Omit to list all types.") }, async ({ key }) => {
24358
+ server.setActiveToolCall({
24359
+ name: "list_page_types",
24360
+ target: key
24361
+ });
24082
24362
  if (key) {
24083
24363
  const resolved = resolvePageType(key);
24084
24364
  if (!resolved) return { content: [{
@@ -24206,6 +24486,7 @@ The user CANNOT see your plain text output — they only see messages sent via M
24206
24486
  ## Full Reference
24207
24487
  Read the resource at abracadabra://agent-guide for the complete guide covering page type schemas, metadata reference, awareness/presence, content structure, and detailed examples.`
24208
24488
  });
24489
+ wrapToolsWithDetail(mcp, server);
24209
24490
  registerTreeTools(mcp, server, schemaValidator);
24210
24491
  registerContentTools(mcp, server);
24211
24492
  registerMetaTools(mcp, server, schemaValidator);