@abraca/mcp 2.7.0 → 2.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.
@@ -12800,10 +12800,10 @@ var StdioServerTransport = class {
12800
12800
  * fields (status, activeToolCall) so the cou-sh dashboard shows real-time activity.
12801
12801
  */
12802
12802
  /** Per-field cap for detail strings (chars). Keeps awareness lean. */
12803
- const DETAIL_MAX = 4e3;
12804
- function cap(s) {
12803
+ const DETAIL_MAX$1 = 4e3;
12804
+ function cap$1(s) {
12805
12805
  if (typeof s !== "string") return void 0;
12806
- return s.length > DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n… (truncated)` : s;
12806
+ return s.length > DETAIL_MAX$1 ? `${s.slice(0, DETAIL_MAX$1)}\n… (truncated)` : s;
12807
12807
  }
12808
12808
  /** Map Claude Code tool names to awareness-friendly names + target + detail. */
12809
12809
  function mapToolCall(toolName, toolInput) {
@@ -12813,7 +12813,7 @@ function mapToolCall(toolName, toolInput) {
12813
12813
  target: truncate(toolInput.command ?? toolInput.description, 60),
12814
12814
  detail: {
12815
12815
  kind: "command",
12816
- text: cap(toolInput.command)
12816
+ text: cap$1(toolInput.command)
12817
12817
  }
12818
12818
  };
12819
12819
  case "Read": return {
@@ -12837,8 +12837,8 @@ function mapToolCall(toolName, toolInput) {
12837
12837
  detail: {
12838
12838
  kind: "diff",
12839
12839
  file: toolInput.file_path,
12840
- old: cap(oldText),
12841
- new: cap(newText)
12840
+ old: cap$1(oldText),
12841
+ new: cap$1(newText)
12842
12842
  }
12843
12843
  };
12844
12844
  }
@@ -12848,7 +12848,7 @@ function mapToolCall(toolName, toolInput) {
12848
12848
  detail: {
12849
12849
  kind: "code",
12850
12850
  file: toolInput.file_path,
12851
- text: cap(toolInput.content)
12851
+ text: cap$1(toolInput.content)
12852
12852
  }
12853
12853
  };
12854
12854
  case "Grep": return {
@@ -12872,7 +12872,7 @@ function mapToolCall(toolName, toolInput) {
12872
12872
  target: toolInput.description || toolInput.subagent_type || "agent",
12873
12873
  detail: {
12874
12874
  kind: "text",
12875
- text: cap(toolInput.prompt)
12875
+ text: cap$1(toolInput.prompt)
12876
12876
  }
12877
12877
  };
12878
12878
  case "WebFetch": return {
@@ -21419,8 +21419,16 @@ function stripMention(text, aliases) {
21419
21419
  //#region packages/mcp/src/utils.ts
21420
21420
  /**
21421
21421
  * Wait for a provider's `synced` event with a timeout.
21422
+ *
21423
+ * Mirrors `@abraca/dabra`'s `DocUtils.waitForSync`: the `isSynced` short-circuit
21424
+ * is load-bearing. `synced` is emitted once per sync transition (and deferred a
21425
+ * microtask), so a provider that has ALREADY synced — e.g. the cached child
21426
+ * provider that `loadChild` returns on a repeat read/write of the same doc —
21427
+ * will never re-emit. Without this guard those callers block for the full
21428
+ * `timeoutMs` ("works once, then every later op on that doc times out at 15s").
21422
21429
  */
21423
21430
  function waitForSync(provider, timeoutMs = 15e3) {
21431
+ if (provider.isSynced) return Promise.resolve();
21424
21432
  return new Promise((resolve, reject) => {
21425
21433
  const timer = setTimeout(() => {
21426
21434
  provider.off("synced", handler);
@@ -21428,6 +21436,7 @@ function waitForSync(provider, timeoutMs = 15e3) {
21428
21436
  }, timeoutMs);
21429
21437
  function handler() {
21430
21438
  clearTimeout(timer);
21439
+ provider.off("synced", handler);
21431
21440
  resolve();
21432
21441
  }
21433
21442
  provider.on("synced", handler);
@@ -21608,6 +21617,46 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
21608
21617
  await this._connectToSpace(docId);
21609
21618
  console.error(`[abracadabra-mcp] Switched active space to ${docId}`);
21610
21619
  }
21620
+ /**
21621
+ * Make the space that OWNS `targetId` the active connection before a tree
21622
+ * read/write, so the op targets the right space's `doc-tree` map.
21623
+ *
21624
+ * Without this, every tree tool used `getTreeMap()` (the single active
21625
+ * space) and treated `rootId`/`parentId` as a mere filter — so reading a
21626
+ * non-active space returned stale/empty trees, and *writing* to one
21627
+ * silently orphaned the entry into the active space's map (a real
21628
+ * data-loss path: docs built "in Ideas" while Development was active
21629
+ * landed in Development under a non-node parent and never reached Ideas).
21630
+ *
21631
+ * Resolution order:
21632
+ * - falsy or already the active root → no-op.
21633
+ * - a known Space id → connect/activate it (cheap; `_spaceConnections`
21634
+ * caches the provider so re-activation is free).
21635
+ * - present in the active space's tree → no-op (a normal child doc).
21636
+ * - present in an already-connected *other* space's tree → activate that.
21637
+ * - otherwise unresolved → returns `false`; callers decide (reads report
21638
+ * not-found/empty, writes refuse rather than orphan).
21639
+ *
21640
+ * Switching uses {@link _connectToSpace} (not {@link switchSpace}) so the
21641
+ * child-content provider cache survives — content docs are keyed by global
21642
+ * guid, independent of which space is active.
21643
+ */
21644
+ async ensureSpaceActive(targetId) {
21645
+ if (!targetId || targetId === this._rootDocId) return true;
21646
+ if (this._spaces.some((s) => s.id === targetId)) {
21647
+ await this._connectToSpace(targetId);
21648
+ return true;
21649
+ }
21650
+ if (this.getTreeMap()?.has(targetId)) return true;
21651
+ for (const [spaceId, conn] of this._spaceConnections) {
21652
+ if (spaceId === this._rootDocId) continue;
21653
+ if (conn.doc.getMap("doc-tree").has(targetId)) {
21654
+ await this._connectToSpace(spaceId);
21655
+ return true;
21656
+ }
21657
+ }
21658
+ return false;
21659
+ }
21611
21660
  /** Get the root doc-tree Y.Map of the active space. */
21612
21661
  getTreeMap() {
21613
21662
  return this._activeConnection?.doc.getMap("doc-tree") ?? null;
@@ -22145,6 +22194,28 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
22145
22194
  }
22146
22195
  }
22147
22196
  /**
22197
+ * Attach expandable `detail` (arguments + result summary) to the most recent
22198
+ * tool-history entry for `toolName`, then re-broadcast `toolHistory`.
22199
+ *
22200
+ * Called by the central tool wrapper in index.ts *after* a tool resolves, so
22201
+ * the dashboard's tool cards can expand to show what the tool was called with
22202
+ * and what it returned — the same affordance built-in Claude tools (bash /
22203
+ * read / edit) already get via the hook-bridge. Tools that never emitted a
22204
+ * pill (no `setActiveToolCall`) have no entry to attach to, so this no-ops.
22205
+ */
22206
+ recordToolDetail(toolName, detail) {
22207
+ const provider = this._activeConnection?.provider;
22208
+ if (!provider) return;
22209
+ for (let i = this._toolHistory.length - 1; i >= 0; i--) {
22210
+ const h = this._toolHistory[i];
22211
+ if (h.tool === toolName && h.detail == null) {
22212
+ h.detail = detail;
22213
+ provider.awareness.setLocalStateField("toolHistory", [...this._toolHistory]);
22214
+ return;
22215
+ }
22216
+ }
22217
+ }
22218
+ /**
22148
22219
  * Send a typing indicator to a chat channel. Pass the channel doc id
22149
22220
  * (or for legacy callers, a `group:<docId>` string — we strip the prefix).
22150
22221
  */
@@ -22192,6 +22263,123 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
22192
22263
  }
22193
22264
  };
22194
22265
 
22266
+ //#endregion
22267
+ //#region packages/mcp/src/tool-detail.ts
22268
+ /**
22269
+ * tool-detail — central enrichment of abracadabra MCP tool cards.
22270
+ *
22271
+ * Built-in Claude tools (Bash/Read/Edit/…) flow through the hook-bridge, which
22272
+ * attaches an expandable `detail` (command text, file path, diff) that the
22273
+ * cou-sh chat renders as a click-to-expand block. abracadabra's own MCP tools
22274
+ * are explicitly skipped by the hook-bridge and only ever emitted a bare
22275
+ * name+target pill — so their cards had nothing to expand ("no idea what docs
22276
+ * it sees or what the tree was").
22277
+ *
22278
+ * `wrapToolsWithDetail` patches `McpServer.tool` ONCE, before any tool is
22279
+ * registered, so every tool invocation records an expandable detail onto its
22280
+ * tool-history entry (via `server.recordToolDetail`). The detail leads with the
22281
+ * human-readable title + id of any document the call referenced (resolved from
22282
+ * the live tree), then the remaining arguments, then the text the tool returned
22283
+ * (e.g. the full document tree for `get_document_tree`). The pill itself is
22284
+ * still emitted by each tool's own `setActiveToolCall`; this only fills in the
22285
+ * expandable detail afterwards.
22286
+ */
22287
+ /** Per-field cap (chars) — keeps the awareness payload bounded. Mirrors the
22288
+ * hook-bridge's DETAIL_MAX so built-in and MCP tool cards behave alike. */
22289
+ const DETAIL_MAX = 4e3;
22290
+ /** Argument keys that hold a document id, in the order we surface them. */
22291
+ const DOC_ID_KEYS = [
22292
+ "docId",
22293
+ "id",
22294
+ "rootId",
22295
+ "parentId",
22296
+ "newParentId",
22297
+ "uploadId"
22298
+ ];
22299
+ function cap(s) {
22300
+ return s.length > DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n… (truncated)` : s;
22301
+ }
22302
+ function safeJson(value) {
22303
+ try {
22304
+ return JSON.stringify(value);
22305
+ } catch {
22306
+ return String(value);
22307
+ }
22308
+ }
22309
+ /** Pull the joined text content out of a tool result, if any. */
22310
+ function resultText(result) {
22311
+ const content = result?.content;
22312
+ if (!Array.isArray(content)) return "";
22313
+ return content.filter((c) => c && c.type === "text").map((c) => c.text ?? "").join("\n").trim();
22314
+ }
22315
+ /**
22316
+ * Build the expandable detail shown when a tool card is clicked:
22317
+ * 1. one `id — "Title"` line per document the args referenced (title resolved
22318
+ * from the live tree when available),
22319
+ * 2. the remaining arguments as compact JSON,
22320
+ * 3. the text the tool returned (document tree, content, confirmation, …).
22321
+ * Rendered by cou-sh's ChatActionCard as a `kind: "text"` block.
22322
+ */
22323
+ function buildToolDetail(toolArgs, result, resolveLabel) {
22324
+ const lines = [];
22325
+ const rest = toolArgs && typeof toolArgs === "object" ? { ...toolArgs } : {};
22326
+ for (const key of DOC_ID_KEYS) {
22327
+ const v = rest[key];
22328
+ if (typeof v === "string" && v.length > 0) {
22329
+ const label = resolveLabel?.(v);
22330
+ lines.push(label ? `${key}: ${v} — "${label}"` : `${key}: ${v}`);
22331
+ delete rest[key];
22332
+ }
22333
+ }
22334
+ const restStr = Object.keys(rest).length > 0 ? safeJson(rest) : "";
22335
+ if (restStr && restStr !== "{}") lines.push(`args: ${restStr}`);
22336
+ if (lines.length === 0) lines.push("args: (no arguments)");
22337
+ const text = resultText(result);
22338
+ if (text) {
22339
+ lines.push("");
22340
+ lines.push(text);
22341
+ }
22342
+ return {
22343
+ kind: "text",
22344
+ text: cap(lines.join("\n"))
22345
+ };
22346
+ }
22347
+ /**
22348
+ * Monkey-patch `mcp.tool` so each registered tool's handler is wrapped to record
22349
+ * arguments + result detail after it resolves. Call once at startup, before the
22350
+ * register* functions run. Errors in detail-recording are swallowed — they must
22351
+ * never break the underlying tool.
22352
+ */
22353
+ function wrapToolsWithDetail(mcp, server) {
22354
+ const resolveLabel = (id) => {
22355
+ try {
22356
+ const raw = server.getTreeMap()?.get(id);
22357
+ if (!raw) return void 0;
22358
+ const label = toPlain(raw)?.label;
22359
+ return typeof label === "string" && label.length > 0 ? label : void 0;
22360
+ } catch {
22361
+ return;
22362
+ }
22363
+ };
22364
+ const original = mcp.tool.bind(mcp);
22365
+ mcp.tool = (...args) => {
22366
+ const name = args[0];
22367
+ const cbIndex = args.length - 1;
22368
+ const cb = args[cbIndex];
22369
+ if (typeof name === "string" && typeof cb === "function") {
22370
+ const handler = cb;
22371
+ args[cbIndex] = async (...handlerArgs) => {
22372
+ const result = await handler(...handlerArgs);
22373
+ try {
22374
+ server.recordToolDetail(name, buildToolDetail(handlerArgs[0], result, resolveLabel));
22375
+ } catch {}
22376
+ return result;
22377
+ };
22378
+ }
22379
+ return original(...args);
22380
+ };
22381
+ }
22382
+
22195
22383
  //#endregion
22196
22384
  //#region packages/mcp/src/tools/awareness.ts
22197
22385
  function registerAwarenessTools(mcp, server) {
@@ -22424,6 +22612,7 @@ function registerContentTools(mcp, server) {
22424
22612
  name: "read_document",
22425
22613
  target: docId
22426
22614
  });
22615
+ await server.ensureSpaceActive(docId);
22427
22616
  const fragment = (await server.getChildProvider(docId)).document.getXmlFragment("default");
22428
22617
  server.setFocusedDoc(docId);
22429
22618
  server.setDocCursor(docId, 0);
@@ -22487,6 +22676,7 @@ function registerContentTools(mcp, server) {
22487
22676
  name: "write_document",
22488
22677
  target: docId
22489
22678
  });
22679
+ await server.ensureSpaceActive(docId);
22490
22680
  const writeMode = mode ?? "replace";
22491
22681
  const doc = (await server.getChildProvider(docId)).document;
22492
22682
  const fragment = doc.getXmlFragment("default");
@@ -22513,7 +22703,9 @@ function registerContentTools(mcp, server) {
22513
22703
  if (writeMode === "replace") doc.transact(() => {
22514
22704
  while (fragment.length > 0) fragment.delete(0);
22515
22705
  });
22516
- populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
22706
+ const contentToWrite = body || markdown;
22707
+ const existingLabel = server.getDocSummary(docId)?.label;
22708
+ populateYDocFromMarkdown(fragment, contentToWrite, title || existingLabel || "Untitled");
22517
22709
  server.setFocusedDoc(docId);
22518
22710
  server.setDocCursor(docId, fragment.length);
22519
22711
  return { content: [{
@@ -22694,6 +22886,7 @@ function registerMetaTools(mcp, server, validator = null) {
22694
22886
  name: "get_metadata",
22695
22887
  target: docId
22696
22888
  });
22889
+ await server.ensureSpaceActive(docId);
22697
22890
  const treeMap = server.getTreeMap();
22698
22891
  if (!treeMap) return { content: [{
22699
22892
  type: "text",
@@ -22723,6 +22916,7 @@ function registerMetaTools(mcp, server, validator = null) {
22723
22916
  name: "update_metadata",
22724
22917
  target: docId
22725
22918
  });
22919
+ await server.ensureSpaceActive(docId);
22726
22920
  const treeMap = server.getTreeMap();
22727
22921
  if (!treeMap) return { content: [{
22728
22922
  type: "text",
@@ -23679,9 +23873,22 @@ function normalizeRootId(id, server) {
23679
23873
  function toPlain$1(val) {
23680
23874
  return val instanceof Y.Map ? val.toJSON() : val;
23681
23875
  }
23682
- function readEntries(treeMap) {
23876
+ /**
23877
+ * Read every doc-tree entry, optionally skipping the space's self-descriptor.
23878
+ *
23879
+ * A space's own `doc-tree` Y.Map holds an entry keyed by the space's OWN id
23880
+ * (the connected root doc), with `parentId: null` and `order: -1`. It stores
23881
+ * the space root's own icon/color/type/label — it is NOT a child document.
23882
+ * Because its `parentId` is null it otherwise shows up alongside real
23883
+ * top-level docs, making the space render as a phantom first child of itself
23884
+ * ("Health space's first document is 'Development'"). Pass `selfId =
23885
+ * server.rootDocId` to drop it. cou-sh's DocumentTree applies the same guard
23886
+ * (`if (id === spaceDocId) continue`).
23887
+ */
23888
+ function readEntries(treeMap, selfId) {
23683
23889
  const entries = [];
23684
23890
  treeMap.forEach((raw, id) => {
23891
+ if (selfId && id === selfId) return;
23685
23892
  const value = toPlain$1(raw);
23686
23893
  if (typeof value !== "object" || value === null) return;
23687
23894
  entries.push({
@@ -23733,13 +23940,14 @@ function registerTreeTools(mcp, server, validator = null) {
23733
23940
  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: z.string().optional().describe("Parent document ID. Omit for root-level documents.") }, async ({ parentId }) => {
23734
23941
  server.setAutoStatus("reading");
23735
23942
  server.setActiveToolCall({ name: "list_documents" });
23943
+ await server.ensureSpaceActive(parentId);
23736
23944
  const treeMap = server.getTreeMap();
23737
23945
  if (!treeMap) return { content: [{
23738
23946
  type: "text",
23739
23947
  text: "Not connected"
23740
23948
  }] };
23741
23949
  const targetId = normalizeRootId(parentId, server);
23742
- const children = childrenOf(readEntries(treeMap), targetId);
23950
+ const children = childrenOf(readEntries(treeMap, server.rootDocId), targetId);
23743
23951
  return { content: [{
23744
23952
  type: "text",
23745
23953
  text: JSON.stringify(children, null, 2)
@@ -23751,6 +23959,7 @@ function registerTreeTools(mcp, server, validator = null) {
23751
23959
  }, async ({ rootId, depth }) => {
23752
23960
  server.setAutoStatus("reading");
23753
23961
  server.setActiveToolCall({ name: "get_document_tree" });
23962
+ await server.ensureSpaceActive(rootId);
23754
23963
  const treeMap = server.getTreeMap();
23755
23964
  if (!treeMap) return { content: [{
23756
23965
  type: "text",
@@ -23758,7 +23967,7 @@ function registerTreeTools(mcp, server, validator = null) {
23758
23967
  }] };
23759
23968
  const targetId = normalizeRootId(rootId, server);
23760
23969
  const maxDepth = depth ?? 3;
23761
- const tree = buildTree(readEntries(treeMap), targetId, maxDepth);
23970
+ const tree = buildTree(readEntries(treeMap, server.rootDocId), targetId, maxDepth);
23762
23971
  return { content: [{
23763
23972
  type: "text",
23764
23973
  text: JSON.stringify(tree, null, 2)
@@ -23773,12 +23982,13 @@ function registerTreeTools(mcp, server, validator = null) {
23773
23982
  name: "find_document",
23774
23983
  target: query
23775
23984
  });
23985
+ await server.ensureSpaceActive(rootId);
23776
23986
  const treeMap = server.getTreeMap();
23777
23987
  if (!treeMap) return { content: [{
23778
23988
  type: "text",
23779
23989
  text: "Not connected"
23780
23990
  }] };
23781
- const entries = readEntries(treeMap);
23991
+ const entries = readEntries(treeMap, server.rootDocId);
23782
23992
  const lowerQuery = query.toLowerCase();
23783
23993
  const normalizedRoot = normalizeRootId(rootId, server);
23784
23994
  const matches = (normalizedRoot ? descendantsOf(entries, normalizedRoot) : entries).filter((e) => e.label.toLowerCase().includes(lowerQuery));
@@ -23859,6 +24069,7 @@ function registerTreeTools(mcp, server, validator = null) {
23859
24069
  name: "create_document",
23860
24070
  target: label
23861
24071
  });
24072
+ await server.ensureSpaceActive(parentId);
23862
24073
  const treeMap = server.getTreeMap();
23863
24074
  const rootDoc = server.rootDocument;
23864
24075
  if (!treeMap || !rootDoc) return { content: [{
@@ -23878,6 +24089,30 @@ function registerTreeTools(mcp, server, validator = null) {
23878
24089
  const id = crypto.randomUUID();
23879
24090
  const normalizedParent = normalizeRootId(parentId, server);
23880
24091
  const now = Date.now();
24092
+ if (normalizedParent && !treeMap.has(normalizedParent)) return {
24093
+ content: [{
24094
+ type: "text",
24095
+ 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.`
24096
+ }],
24097
+ isError: true
24098
+ };
24099
+ const restParent = normalizedParent ?? server.rootDocId;
24100
+ if (restParent) try {
24101
+ await server.client.createChild(restParent, {
24102
+ child_id: id,
24103
+ label,
24104
+ doc_type: type,
24105
+ kind: "page"
24106
+ });
24107
+ } catch (e) {
24108
+ return {
24109
+ content: [{
24110
+ type: "text",
24111
+ text: `Failed to register document ${id} under ${restParent}: ${e instanceof Error ? e.message : String(e)}`
24112
+ }],
24113
+ isError: true
24114
+ };
24115
+ }
23881
24116
  rootDoc.transact(() => {
23882
24117
  treeMap.set(id, makeEntryMap({
23883
24118
  label,
@@ -23909,6 +24144,7 @@ function registerTreeTools(mcp, server, validator = null) {
23909
24144
  name: "rename_document",
23910
24145
  target: id
23911
24146
  });
24147
+ await server.ensureSpaceActive(id);
23912
24148
  const treeMap = server.getTreeMap();
23913
24149
  if (!treeMap) return { content: [{
23914
24150
  type: "text",
@@ -23922,9 +24158,15 @@ function registerTreeTools(mcp, server, validator = null) {
23922
24158
  label,
23923
24159
  updatedAt: Date.now()
23924
24160
  });
24161
+ let restNote = "";
24162
+ try {
24163
+ await server.client.updateDocumentMeta(id, { label });
24164
+ } catch (e) {
24165
+ restNote = ` (registry update failed: ${e instanceof Error ? e.message : String(e)})`;
24166
+ }
23925
24167
  return { content: [{
23926
24168
  type: "text",
23927
- text: `Renamed to "${label}"`
24169
+ text: `Renamed to "${label}"${restNote}`
23928
24170
  }] };
23929
24171
  });
23930
24172
  mcp.tool("move_document", "Move a document to a new parent and/or reorder it.", {
@@ -23937,6 +24179,7 @@ function registerTreeTools(mcp, server, validator = null) {
23937
24179
  name: "move_document",
23938
24180
  target: id
23939
24181
  });
24182
+ await server.ensureSpaceActive(id);
23940
24183
  const treeMap = server.getTreeMap();
23941
24184
  if (!treeMap) return { content: [{
23942
24185
  type: "text",
@@ -23946,14 +24189,29 @@ function registerTreeTools(mcp, server, validator = null) {
23946
24189
  type: "text",
23947
24190
  text: `Document ${id} not found`
23948
24191
  }] };
24192
+ const normalizedParent = normalizeRootId(newParentId, server);
24193
+ if (normalizedParent && !treeMap.has(normalizedParent)) return {
24194
+ content: [{
24195
+ type: "text",
24196
+ 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.`
24197
+ }],
24198
+ isError: true
24199
+ };
23949
24200
  patchEntry(treeMap, id, {
23950
- parentId: normalizeRootId(newParentId, server),
24201
+ parentId: normalizedParent,
23951
24202
  order: order ?? Date.now(),
23952
24203
  updatedAt: Date.now()
23953
24204
  });
24205
+ const restParent = normalizedParent ?? server.rootDocId;
24206
+ let restNote = "";
24207
+ if (restParent) try {
24208
+ await server.client.updateDocumentMeta(id, { parent_id: restParent });
24209
+ } catch (e) {
24210
+ restNote = ` (registry reparent failed: ${e instanceof Error ? e.message : String(e)})`;
24211
+ }
23954
24212
  return { content: [{
23955
24213
  type: "text",
23956
- text: `Moved ${id} to parent ${newParentId}`
24214
+ text: `Moved ${id} to parent ${newParentId}${restNote}`
23957
24215
  }] };
23958
24216
  });
23959
24217
  mcp.tool("delete_document", "Soft-delete a document and its descendants (moves to trash).", { id: z.string().describe("Document ID to delete.") }, async ({ id }) => {
@@ -23962,6 +24220,7 @@ function registerTreeTools(mcp, server, validator = null) {
23962
24220
  name: "delete_document",
23963
24221
  target: id
23964
24222
  });
24223
+ await server.ensureSpaceActive(id);
23965
24224
  const treeMap = server.getTreeMap();
23966
24225
  const trashMap = server.getTrashMap();
23967
24226
  const rootDoc = server.rootDocument;
@@ -23969,7 +24228,7 @@ function registerTreeTools(mcp, server, validator = null) {
23969
24228
  type: "text",
23970
24229
  text: "Not connected"
23971
24230
  }] };
23972
- const toDelete = [id, ...descendantsOf(readEntries(treeMap), id).map((e) => e.id)];
24231
+ const toDelete = [id, ...descendantsOf(readEntries(treeMap, server.rootDocId), id).map((e) => e.id)];
23973
24232
  const now = Date.now();
23974
24233
  rootDoc.transact(() => {
23975
24234
  for (const nid of toDelete) {
@@ -23987,9 +24246,15 @@ function registerTreeTools(mcp, server, validator = null) {
23987
24246
  treeMap.delete(nid);
23988
24247
  }
23989
24248
  });
24249
+ let restNote = "";
24250
+ try {
24251
+ await server.client.deleteDoc(id);
24252
+ } catch (e) {
24253
+ restNote = ` (registry delete failed: ${e instanceof Error ? e.message : String(e)})`;
24254
+ }
23990
24255
  return { content: [{
23991
24256
  type: "text",
23992
- text: `Deleted ${toDelete.length} document(s)`
24257
+ text: `Deleted ${toDelete.length} document(s)${restNote}`
23993
24258
  }] };
23994
24259
  });
23995
24260
  mcp.tool("change_document_type", "Change the page type view of a document (data is preserved).", {
@@ -24001,6 +24266,7 @@ function registerTreeTools(mcp, server, validator = null) {
24001
24266
  name: "change_document_type",
24002
24267
  target: id
24003
24268
  });
24269
+ await server.ensureSpaceActive(id);
24004
24270
  const treeMap = server.getTreeMap();
24005
24271
  if (!treeMap) return { content: [{
24006
24272
  type: "text",
@@ -24020,6 +24286,7 @@ function registerTreeTools(mcp, server, validator = null) {
24020
24286
  }] };
24021
24287
  });
24022
24288
  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 () => {
24289
+ server.setActiveToolCall({ name: "list_spaces" });
24023
24290
  const spaces = server.spaces;
24024
24291
  if (!spaces.length) return { content: [{
24025
24292
  type: "text",
@@ -24036,6 +24303,10 @@ function registerTreeTools(mcp, server, validator = null) {
24036
24303
  }] };
24037
24304
  });
24038
24305
  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: z.string().describe("The id of the Space to switch to (from list_spaces).") }, async ({ docId }) => {
24306
+ server.setActiveToolCall({
24307
+ name: "switch_space",
24308
+ target: docId
24309
+ });
24039
24310
  await server.switchSpace(docId);
24040
24311
  return { content: [{
24041
24312
  type: "text",
@@ -24043,6 +24314,11 @@ function registerTreeTools(mcp, server, validator = null) {
24043
24314
  }] };
24044
24315
  });
24045
24316
  mcp.tool("duplicate_document", "Shallow-clone a document with \" (copy)\" suffix. Returns new document ID.", { id: z.string().describe("Document ID to duplicate.") }, async ({ id }) => {
24317
+ server.setActiveToolCall({
24318
+ name: "duplicate_document",
24319
+ target: id
24320
+ });
24321
+ await server.ensureSpaceActive(id);
24046
24322
  const treeMap = server.getTreeMap();
24047
24323
  if (!treeMap) return { content: [{
24048
24324
  type: "text",
@@ -24070,6 +24346,10 @@ function registerTreeTools(mcp, server, validator = null) {
24070
24346
  }] };
24071
24347
  });
24072
24348
  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: 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 }) => {
24349
+ server.setActiveToolCall({
24350
+ name: "list_page_types",
24351
+ target: key
24352
+ });
24073
24353
  if (key) {
24074
24354
  const resolved = resolvePageType(key);
24075
24355
  if (!resolved) return { content: [{
@@ -24197,6 +24477,7 @@ The user CANNOT see your plain text output — they only see messages sent via M
24197
24477
  ## Full Reference
24198
24478
  Read the resource at abracadabra://agent-guide for the complete guide covering page type schemas, metadata reference, awareness/presence, content structure, and detailed examples.`
24199
24479
  });
24480
+ wrapToolsWithDetail(mcp, server);
24200
24481
  registerTreeTools(mcp, server, schemaValidator);
24201
24482
  registerContentTools(mcp, server);
24202
24483
  registerMetaTools(mcp, server, schemaValidator);