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