@abraca/mcp 2.8.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.
- package/dist/abracadabra-mcp.cjs +299 -18
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +299 -18
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/dist/index.d.ts +36 -0
- package/package.json +2 -2
- package/src/index.ts +10 -0
- package/src/server.ts +79 -0
- package/src/tool-detail.ts +147 -0
- package/src/tools/content.ts +16 -2
- package/src/tools/meta.ts +2 -0
- package/src/tools/tree.ts +135 -10
- package/src/utils.ts +16 -1
package/dist/abracadabra-mcp.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|