@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.
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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);
|