@abraca/mcp 2.4.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
|
@@ -4,7 +4,7 @@ import addFormats from "ajv-formats";
|
|
|
4
4
|
import { ZodOptional, z } from "zod";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
6
|
import * as Y from "yjs";
|
|
7
|
-
import { AbracadabraClient, AbracadabraProvider, Kind, SERVER_ROOT_ID, awarenessStatesToArray } from "@abraca/dabra";
|
|
7
|
+
import { AbracadabraClient, AbracadabraProvider, Kind, SERVER_ROOT_ID, awarenessStatesToArray, foldRecords, isEncryptedContent, makeEntryMap, patchEntry, recordFromYAny, toPlain } from "@abraca/dabra";
|
|
8
8
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -14086,6 +14086,9 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
|
|
|
14086
14086
|
static {
|
|
14087
14087
|
this.TOOL_HISTORY_MAX = 20;
|
|
14088
14088
|
}
|
|
14089
|
+
static {
|
|
14090
|
+
this.DEDUPE_MAX = 1e3;
|
|
14091
|
+
}
|
|
14089
14092
|
constructor(config) {
|
|
14090
14093
|
this._serverInfo = null;
|
|
14091
14094
|
this._rootDocId = null;
|
|
@@ -14103,6 +14106,14 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
|
|
|
14103
14106
|
this._lastChatChannel = null;
|
|
14104
14107
|
this._signFn = null;
|
|
14105
14108
|
this._toolHistory = [];
|
|
14109
|
+
this._inboxStarted = false;
|
|
14110
|
+
this._inboxDocId = null;
|
|
14111
|
+
this._inboxDoc = null;
|
|
14112
|
+
this._inboxProvider = null;
|
|
14113
|
+
this._inboxDisposers = [];
|
|
14114
|
+
this._inboxInitialized = false;
|
|
14115
|
+
this._seenInboxIds = /* @__PURE__ */ new Set();
|
|
14116
|
+
this._dispatchedMessageIds = /* @__PURE__ */ new Set();
|
|
14106
14117
|
this.config = config;
|
|
14107
14118
|
this.client = new AbracadabraClient({
|
|
14108
14119
|
url: config.url,
|
|
@@ -14332,6 +14343,182 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
|
|
|
14332
14343
|
this._handleStatelessChat(payload);
|
|
14333
14344
|
});
|
|
14334
14345
|
console.error("[abracadabra-mcp] Stateless chat listener attached");
|
|
14346
|
+
this._startInboxNotifications();
|
|
14347
|
+
}
|
|
14348
|
+
}
|
|
14349
|
+
/**
|
|
14350
|
+
* Bootstrap inbox observation. Sends `messages:inbox_fetch` over the active
|
|
14351
|
+
* provider; the server's `messages:inbox_history` reply carries the
|
|
14352
|
+
* `inbox_doc_id`. We then open a dedicated provider on that doc and observe
|
|
14353
|
+
* its `entries` Y.Array for live DM/mention dispatch. Mirrors the
|
|
14354
|
+
* dashboard's `useNotifications` pattern.
|
|
14355
|
+
*/
|
|
14356
|
+
async _startInboxNotifications() {
|
|
14357
|
+
if (this._inboxStarted) return;
|
|
14358
|
+
const provider = this._activeConnection?.provider;
|
|
14359
|
+
if (!provider) return;
|
|
14360
|
+
this._inboxStarted = true;
|
|
14361
|
+
provider.on("stateless", ({ payload }) => {
|
|
14362
|
+
if (!payload.includes("\"messages:inbox_history\"")) return;
|
|
14363
|
+
try {
|
|
14364
|
+
const data = JSON.parse(payload);
|
|
14365
|
+
if (data?.type !== "messages:inbox_history") return;
|
|
14366
|
+
const inboxDocId = typeof data.inbox_doc_id === "string" ? data.inbox_doc_id : null;
|
|
14367
|
+
if (inboxDocId) this._ensureInboxObserver(inboxDocId);
|
|
14368
|
+
} catch {}
|
|
14369
|
+
});
|
|
14370
|
+
provider.sendStateless(JSON.stringify({
|
|
14371
|
+
type: "messages:inbox_fetch",
|
|
14372
|
+
limit: 50,
|
|
14373
|
+
unread_only: false
|
|
14374
|
+
}));
|
|
14375
|
+
console.error("[abracadabra-mcp] Inbox bootstrap sent (messages:inbox_fetch)");
|
|
14376
|
+
}
|
|
14377
|
+
/**
|
|
14378
|
+
* Open a dedicated provider on the agent's inbox doc and observe its
|
|
14379
|
+
* `entries` Y.Array. The server is the only writer; we only read.
|
|
14380
|
+
*/
|
|
14381
|
+
async _ensureInboxObserver(inboxDocId) {
|
|
14382
|
+
if (this._inboxDocId) return;
|
|
14383
|
+
this._inboxDocId = inboxDocId;
|
|
14384
|
+
if (!this.client.isTokenValid() && this._signFn && this._userId) await this.client.loginWithKey(this._userId, this._signFn);
|
|
14385
|
+
const doc = new Y.Doc({ guid: inboxDocId });
|
|
14386
|
+
const provider = new AbracadabraProvider({
|
|
14387
|
+
name: inboxDocId,
|
|
14388
|
+
document: doc,
|
|
14389
|
+
client: this.client,
|
|
14390
|
+
disableOfflineStore: true,
|
|
14391
|
+
subdocLoading: "lazy"
|
|
14392
|
+
});
|
|
14393
|
+
this._inboxDoc = doc;
|
|
14394
|
+
this._inboxProvider = provider;
|
|
14395
|
+
try {
|
|
14396
|
+
await waitForSync(provider);
|
|
14397
|
+
} catch (err) {
|
|
14398
|
+
console.error(`[abracadabra-mcp] Inbox sync failed: ${err?.message ?? err}`);
|
|
14399
|
+
return;
|
|
14400
|
+
}
|
|
14401
|
+
const entriesArr = doc.getArray("entries");
|
|
14402
|
+
const readMap = doc.getMap("read");
|
|
14403
|
+
const onChange = () => {
|
|
14404
|
+
this._pumpInbox(entriesArr, readMap);
|
|
14405
|
+
};
|
|
14406
|
+
entriesArr.observe(onChange);
|
|
14407
|
+
readMap.observe(onChange);
|
|
14408
|
+
this._inboxDisposers.push(() => entriesArr.unobserve(onChange));
|
|
14409
|
+
this._inboxDisposers.push(() => readMap.unobserve(onChange));
|
|
14410
|
+
await this._pumpInbox(entriesArr, readMap);
|
|
14411
|
+
console.error(`[abracadabra-mcp] Inbox observer attached (${inboxDocId})`);
|
|
14412
|
+
}
|
|
14413
|
+
/**
|
|
14414
|
+
* Diff the inbox `entries` array against what we've already seen. On the
|
|
14415
|
+
* first pump we only record a baseline (don't replay history). New entries
|
|
14416
|
+
* are classified by their authoritative `kind` and dispatched.
|
|
14417
|
+
*/
|
|
14418
|
+
async _pumpInbox(entriesArr, readMap) {
|
|
14419
|
+
const entries = entriesArr.toArray().map((e) => typeof e?.toJSON === "function" ? e.toJSON() : e);
|
|
14420
|
+
if (!this._inboxInitialized) {
|
|
14421
|
+
for (const e of entries) if (e?.id) this._seenInboxIds.add(e.id);
|
|
14422
|
+
this._inboxInitialized = true;
|
|
14423
|
+
console.error(`[abracadabra-mcp] Inbox baseline: ${this._seenInboxIds.size} existing entries (not replayed)`);
|
|
14424
|
+
return;
|
|
14425
|
+
}
|
|
14426
|
+
for (const e of entries) {
|
|
14427
|
+
const id = e?.id;
|
|
14428
|
+
if (!id || this._seenInboxIds.has(id)) continue;
|
|
14429
|
+
this._seenInboxIds.add(id);
|
|
14430
|
+
if (readMap.get(id)) continue;
|
|
14431
|
+
try {
|
|
14432
|
+
await this._dispatchInboxEntry(e);
|
|
14433
|
+
} catch (err) {
|
|
14434
|
+
console.error(`[abracadabra-mcp] Inbox dispatch failed for ${id}: ${err?.message ?? err}`);
|
|
14435
|
+
}
|
|
14436
|
+
}
|
|
14437
|
+
this._trimSet(this._seenInboxIds);
|
|
14438
|
+
}
|
|
14439
|
+
/** Classify + dispatch one inbox entry as a channel notification. */
|
|
14440
|
+
async _dispatchInboxEntry(entry) {
|
|
14441
|
+
if (!this._serverRef) return;
|
|
14442
|
+
const kind = typeof entry?.kind === "string" ? entry.kind : "";
|
|
14443
|
+
const channelDocId = typeof entry?.channel_doc_id === "string" ? entry.channel_doc_id : "";
|
|
14444
|
+
const messageId = typeof entry?.message_id === "string" ? entry.message_id : null;
|
|
14445
|
+
const senderId = typeof entry?.sender_id === "string" ? entry.sender_id : "";
|
|
14446
|
+
if (!channelDocId) return;
|
|
14447
|
+
if (senderId && senderId === this._userId) return;
|
|
14448
|
+
const mode = this.triggerMode;
|
|
14449
|
+
if (kind === "mention" || kind === "reply") {
|
|
14450
|
+
if (mode === "task") return;
|
|
14451
|
+
} else if (kind !== "dm") return;
|
|
14452
|
+
if (messageId && this._rememberDispatched(messageId)) return;
|
|
14453
|
+
const content = await this._resolveInboxContent(channelDocId, messageId, entry?.preview);
|
|
14454
|
+
this._lastChatChannel = channelDocId;
|
|
14455
|
+
this._beginTurn();
|
|
14456
|
+
this.setAutoStatus("thinking");
|
|
14457
|
+
await this._serverRef.notification({
|
|
14458
|
+
method: "notifications/claude/channel",
|
|
14459
|
+
params: {
|
|
14460
|
+
content,
|
|
14461
|
+
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.`,
|
|
14462
|
+
meta: {
|
|
14463
|
+
source: "abracadabra",
|
|
14464
|
+
type: kind === "dm" ? "dm_message" : "chat_message",
|
|
14465
|
+
channel_doc_id: channelDocId,
|
|
14466
|
+
sender: entry?.sender_name ?? "Unknown",
|
|
14467
|
+
sender_id: senderId,
|
|
14468
|
+
doc_id: channelDocId
|
|
14469
|
+
}
|
|
14470
|
+
}
|
|
14471
|
+
});
|
|
14472
|
+
console.error(`[abracadabra-mcp] Dispatched ${kind} on ${channelDocId} from ${entry?.sender_name ?? (senderId || "unknown")}`);
|
|
14473
|
+
this._activeConnection?.provider?.sendStateless(JSON.stringify({
|
|
14474
|
+
type: "messages:inbox_mark_read",
|
|
14475
|
+
id: entry.id
|
|
14476
|
+
}));
|
|
14477
|
+
}
|
|
14478
|
+
/**
|
|
14479
|
+
* Resolve full message content. The inbox `preview` is truncated to ≤200
|
|
14480
|
+
* bytes (and null for E2E), so for fidelity we read the actual record from
|
|
14481
|
+
* the channel/DM doc's active period — same shape the dashboard reads.
|
|
14482
|
+
* Falls back to the preview, then a short notice.
|
|
14483
|
+
*/
|
|
14484
|
+
async _resolveInboxContent(channelDocId, messageId, preview) {
|
|
14485
|
+
const previewStr = typeof preview === "string" && preview.length > 0 ? preview : null;
|
|
14486
|
+
const root = this._activeConnection?.provider;
|
|
14487
|
+
if (root && messageId) try {
|
|
14488
|
+
const wrapper = await root.loadChild(channelDocId);
|
|
14489
|
+
if (!wrapper.synced) await waitForSync(wrapper);
|
|
14490
|
+
const periods = wrapper.document.getArray("periods");
|
|
14491
|
+
const len = periods.length;
|
|
14492
|
+
for (let i = len - 1; i >= 0 && i >= len - 2; i--) {
|
|
14493
|
+
const p = periods.get(i);
|
|
14494
|
+
const periodId = p?.id ?? p?.get?.("id");
|
|
14495
|
+
if (!periodId) continue;
|
|
14496
|
+
const period = await root.loadChild(periodId);
|
|
14497
|
+
if (!period.synced) await waitForSync(period);
|
|
14498
|
+
const hit = foldRecords(period.document.getArray("messages").toArray().map((v) => recordFromYAny(v)).filter((r) => r !== null)).find((f) => f.id === messageId);
|
|
14499
|
+
if (hit) {
|
|
14500
|
+
if (isEncryptedContent(hit.content)) return previewStr ?? "[encrypted message — this agent is not provisioned to read this channel]";
|
|
14501
|
+
return hit.content;
|
|
14502
|
+
}
|
|
14503
|
+
}
|
|
14504
|
+
} catch (err) {
|
|
14505
|
+
console.error(`[abracadabra-mcp] content read failed for ${channelDocId}/${messageId}: ${err?.message ?? err}`);
|
|
14506
|
+
}
|
|
14507
|
+
return previewStr ?? "[message content unavailable]";
|
|
14508
|
+
}
|
|
14509
|
+
_rememberDispatched(messageId) {
|
|
14510
|
+
if (this._dispatchedMessageIds.has(messageId)) return true;
|
|
14511
|
+
this._dispatchedMessageIds.add(messageId);
|
|
14512
|
+
this._trimSet(this._dispatchedMessageIds);
|
|
14513
|
+
return false;
|
|
14514
|
+
}
|
|
14515
|
+
_trimSet(s) {
|
|
14516
|
+
if (s.size <= AbracadabraMCPServer.DEDUPE_MAX) return;
|
|
14517
|
+
const excess = s.size - AbracadabraMCPServer.DEDUPE_MAX;
|
|
14518
|
+
let i = 0;
|
|
14519
|
+
for (const v of s) {
|
|
14520
|
+
if (i++ >= excess) break;
|
|
14521
|
+
s.delete(v);
|
|
14335
14522
|
}
|
|
14336
14523
|
}
|
|
14337
14524
|
/** Attach awareness observer to detect `ai:task` fields from human users. */
|
|
@@ -14417,6 +14604,7 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
|
|
|
14417
14604
|
if (data.sender_id && data.sender_id === this._userId) return;
|
|
14418
14605
|
const channelDocId = data.channel_doc_id;
|
|
14419
14606
|
if (!channelDocId) return;
|
|
14607
|
+
if (data.id && this._rememberDispatched(data.id)) return;
|
|
14420
14608
|
const mode = this.triggerMode;
|
|
14421
14609
|
const content = typeof data.content === "string" ? data.content : "";
|
|
14422
14610
|
let dispatchContent = content;
|
|
@@ -14567,6 +14755,13 @@ var AbracadabraMCPServer = class AbracadabraMCPServer {
|
|
|
14567
14755
|
clearInterval(this.evictionTimer);
|
|
14568
14756
|
this.evictionTimer = null;
|
|
14569
14757
|
}
|
|
14758
|
+
for (const dispose of this._inboxDisposers) try {
|
|
14759
|
+
dispose();
|
|
14760
|
+
} catch {}
|
|
14761
|
+
this._inboxDisposers = [];
|
|
14762
|
+
this._inboxProvider?.destroy();
|
|
14763
|
+
this._inboxProvider = null;
|
|
14764
|
+
this._inboxDoc = null;
|
|
14570
14765
|
for (const [, cached] of this.childCache) cached.provider.destroy();
|
|
14571
14766
|
this.childCache.clear();
|
|
14572
14767
|
for (const [, conn] of this._spaceConnections) {
|
|
@@ -15327,13 +15522,13 @@ function normalizeRootId(id, server) {
|
|
|
15327
15522
|
return id === server.rootDocId ? null : id;
|
|
15328
15523
|
}
|
|
15329
15524
|
/** Safely read a tree map value, converting Y.Map to plain object if needed. */
|
|
15330
|
-
function toPlain(val) {
|
|
15525
|
+
function toPlain$1(val) {
|
|
15331
15526
|
return val instanceof Y.Map ? val.toJSON() : val;
|
|
15332
15527
|
}
|
|
15333
15528
|
function readEntries$1(treeMap) {
|
|
15334
15529
|
const entries = [];
|
|
15335
15530
|
treeMap.forEach((raw, id) => {
|
|
15336
|
-
const value = toPlain(raw);
|
|
15531
|
+
const value = toPlain$1(raw);
|
|
15337
15532
|
if (typeof value !== "object" || value === null) return;
|
|
15338
15533
|
entries.push({
|
|
15339
15534
|
id,
|
|
@@ -15530,7 +15725,7 @@ function registerTreeTools(mcp, server, validator = null) {
|
|
|
15530
15725
|
const normalizedParent = normalizeRootId(parentId, server);
|
|
15531
15726
|
const now = Date.now();
|
|
15532
15727
|
rootDoc.transact(() => {
|
|
15533
|
-
treeMap.set(id, {
|
|
15728
|
+
treeMap.set(id, makeEntryMap({
|
|
15534
15729
|
label,
|
|
15535
15730
|
parentId: normalizedParent,
|
|
15536
15731
|
order: now,
|
|
@@ -15538,7 +15733,7 @@ function registerTreeTools(mcp, server, validator = null) {
|
|
|
15538
15733
|
meta,
|
|
15539
15734
|
createdAt: now,
|
|
15540
15735
|
updatedAt: now
|
|
15541
|
-
});
|
|
15736
|
+
}));
|
|
15542
15737
|
});
|
|
15543
15738
|
server.setFocusedDoc(id);
|
|
15544
15739
|
return { content: [{
|
|
@@ -15565,14 +15760,11 @@ function registerTreeTools(mcp, server, validator = null) {
|
|
|
15565
15760
|
type: "text",
|
|
15566
15761
|
text: "Not connected"
|
|
15567
15762
|
}] };
|
|
15568
|
-
|
|
15569
|
-
if (!raw) return { content: [{
|
|
15763
|
+
if (!treeMap.get(id)) return { content: [{
|
|
15570
15764
|
type: "text",
|
|
15571
15765
|
text: `Document ${id} not found`
|
|
15572
15766
|
}] };
|
|
15573
|
-
|
|
15574
|
-
treeMap.set(id, {
|
|
15575
|
-
...entry,
|
|
15767
|
+
patchEntry(treeMap, id, {
|
|
15576
15768
|
label,
|
|
15577
15769
|
updatedAt: Date.now()
|
|
15578
15770
|
});
|
|
@@ -15596,14 +15788,11 @@ function registerTreeTools(mcp, server, validator = null) {
|
|
|
15596
15788
|
type: "text",
|
|
15597
15789
|
text: "Not connected"
|
|
15598
15790
|
}] };
|
|
15599
|
-
|
|
15600
|
-
if (!raw) return { content: [{
|
|
15791
|
+
if (!treeMap.get(id)) return { content: [{
|
|
15601
15792
|
type: "text",
|
|
15602
15793
|
text: `Document ${id} not found`
|
|
15603
15794
|
}] };
|
|
15604
|
-
|
|
15605
|
-
treeMap.set(id, {
|
|
15606
|
-
...entry,
|
|
15795
|
+
patchEntry(treeMap, id, {
|
|
15607
15796
|
parentId: normalizeRootId(newParentId, server),
|
|
15608
15797
|
order: order ?? Date.now(),
|
|
15609
15798
|
updatedAt: Date.now()
|
|
@@ -15632,7 +15821,7 @@ function registerTreeTools(mcp, server, validator = null) {
|
|
|
15632
15821
|
for (const nid of toDelete) {
|
|
15633
15822
|
const raw = treeMap.get(nid);
|
|
15634
15823
|
if (!raw) continue;
|
|
15635
|
-
const entry = toPlain(raw);
|
|
15824
|
+
const entry = toPlain$1(raw);
|
|
15636
15825
|
trashMap.set(nid, {
|
|
15637
15826
|
label: entry.label || "Untitled",
|
|
15638
15827
|
parentId: entry.parentId ?? null,
|
|
@@ -15663,14 +15852,11 @@ function registerTreeTools(mcp, server, validator = null) {
|
|
|
15663
15852
|
type: "text",
|
|
15664
15853
|
text: "Not connected"
|
|
15665
15854
|
}] };
|
|
15666
|
-
|
|
15667
|
-
if (!raw) return { content: [{
|
|
15855
|
+
if (!treeMap.get(id)) return { content: [{
|
|
15668
15856
|
type: "text",
|
|
15669
15857
|
text: `Document ${id} not found`
|
|
15670
15858
|
}] };
|
|
15671
|
-
|
|
15672
|
-
treeMap.set(id, {
|
|
15673
|
-
...entry,
|
|
15859
|
+
patchEntry(treeMap, id, {
|
|
15674
15860
|
type,
|
|
15675
15861
|
updatedAt: Date.now()
|
|
15676
15862
|
});
|
|
@@ -15713,13 +15899,13 @@ function registerTreeTools(mcp, server, validator = null) {
|
|
|
15713
15899
|
type: "text",
|
|
15714
15900
|
text: `Document ${id} not found`
|
|
15715
15901
|
}] };
|
|
15716
|
-
const entry = toPlain(raw);
|
|
15902
|
+
const entry = toPlain$1(raw);
|
|
15717
15903
|
const newId = crypto.randomUUID();
|
|
15718
|
-
treeMap.set(newId, {
|
|
15904
|
+
treeMap.set(newId, makeEntryMap({
|
|
15719
15905
|
...entry,
|
|
15720
15906
|
label: (entry.label || "Untitled") + " (copy)",
|
|
15721
15907
|
order: Date.now()
|
|
15722
|
-
});
|
|
15908
|
+
}));
|
|
15723
15909
|
server.setFocusedDoc(newId);
|
|
15724
15910
|
return { content: [{
|
|
15725
15911
|
type: "text",
|
|
@@ -15762,31 +15948,37 @@ function registerContentTools(mcp, server) {
|
|
|
15762
15948
|
name: "read_document",
|
|
15763
15949
|
target: docId
|
|
15764
15950
|
});
|
|
15765
|
-
const
|
|
15951
|
+
const fragment = (await server.getChildProvider(docId)).document.getXmlFragment("default");
|
|
15766
15952
|
server.setFocusedDoc(docId);
|
|
15767
15953
|
server.setDocCursor(docId, 0);
|
|
15768
15954
|
const treeMap = server.getTreeMap();
|
|
15769
|
-
let label =
|
|
15955
|
+
let label = "Untitled";
|
|
15770
15956
|
let type;
|
|
15771
15957
|
let meta;
|
|
15772
15958
|
let children = [];
|
|
15773
15959
|
if (treeMap) {
|
|
15774
|
-
const entry = treeMap.get(docId);
|
|
15775
|
-
if (entry) {
|
|
15776
|
-
label = entry.label ||
|
|
15960
|
+
const entry = toPlain(treeMap.get(docId));
|
|
15961
|
+
if (entry && typeof entry === "object") {
|
|
15962
|
+
label = entry.label || label;
|
|
15777
15963
|
type = entry.type;
|
|
15778
15964
|
meta = entry.meta;
|
|
15779
15965
|
}
|
|
15780
|
-
|
|
15781
|
-
|
|
15966
|
+
const collected = [];
|
|
15967
|
+
treeMap.forEach((raw, id) => {
|
|
15968
|
+
const value = toPlain(raw);
|
|
15969
|
+
if (typeof value !== "object" || value === null) return;
|
|
15970
|
+
if (value.parentId === docId) collected.push({
|
|
15782
15971
|
id,
|
|
15783
15972
|
label: value.label || "Untitled",
|
|
15784
15973
|
type: value.type,
|
|
15785
|
-
meta: value.meta
|
|
15974
|
+
meta: value.meta,
|
|
15975
|
+
order: value.order ?? 0
|
|
15786
15976
|
});
|
|
15787
15977
|
});
|
|
15788
|
-
|
|
15978
|
+
collected.sort((a, b) => a.order - b.order);
|
|
15979
|
+
children = collected.map(({ order, ...rest }) => rest);
|
|
15789
15980
|
}
|
|
15981
|
+
const markdown = yjsToMarkdown(fragment, label, meta, type);
|
|
15790
15982
|
const result = {
|
|
15791
15983
|
label,
|
|
15792
15984
|
type,
|
|
@@ -15827,19 +16019,19 @@ function registerContentTools(mcp, server) {
|
|
|
15827
16019
|
const treeMap = server.getTreeMap();
|
|
15828
16020
|
const rootDoc = server.rootDocument;
|
|
15829
16021
|
if (treeMap && rootDoc) {
|
|
15830
|
-
const
|
|
15831
|
-
if (
|
|
15832
|
-
const
|
|
15833
|
-
|
|
15834
|
-
updatedAt: Date.now()
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
15838
|
-
|
|
15839
|
-
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
}
|
|
16022
|
+
const rawEntry = treeMap.get(docId);
|
|
16023
|
+
if (rawEntry) {
|
|
16024
|
+
const cur = toPlain(rawEntry);
|
|
16025
|
+
rootDoc.transact(() => {
|
|
16026
|
+
const patch = { updatedAt: Date.now() };
|
|
16027
|
+
if (title) patch.label = title;
|
|
16028
|
+
if (Object.keys(meta).length > 0) patch.meta = {
|
|
16029
|
+
...cur.meta ?? {},
|
|
16030
|
+
...meta
|
|
16031
|
+
};
|
|
16032
|
+
patchEntry(treeMap, docId, patch);
|
|
16033
|
+
});
|
|
16034
|
+
}
|
|
15843
16035
|
}
|
|
15844
16036
|
}
|
|
15845
16037
|
if (writeMode === "replace") doc.transact(() => {
|
|
@@ -15907,11 +16099,12 @@ function registerMetaTools(mcp, server, validator = null) {
|
|
|
15907
16099
|
type: "text",
|
|
15908
16100
|
text: "Not connected"
|
|
15909
16101
|
}] };
|
|
15910
|
-
const
|
|
15911
|
-
if (!
|
|
16102
|
+
const rawEntry = treeMap.get(docId);
|
|
16103
|
+
if (!rawEntry) return { content: [{
|
|
15912
16104
|
type: "text",
|
|
15913
16105
|
text: `Document ${docId} not found`
|
|
15914
16106
|
}] };
|
|
16107
|
+
const entry = toPlain(rawEntry);
|
|
15915
16108
|
const mergedMeta = {
|
|
15916
16109
|
...entry.meta ?? {},
|
|
15917
16110
|
...meta
|
|
@@ -15929,8 +16122,7 @@ function registerMetaTools(mcp, server, validator = null) {
|
|
|
15929
16122
|
};
|
|
15930
16123
|
}
|
|
15931
16124
|
}
|
|
15932
|
-
treeMap
|
|
15933
|
-
...entry,
|
|
16125
|
+
patchEntry(treeMap, docId, {
|
|
15934
16126
|
meta: mergedMeta,
|
|
15935
16127
|
updatedAt: Date.now()
|
|
15936
16128
|
});
|
|
@@ -16120,7 +16312,7 @@ function registerAwarenessTools(mcp, server) {
|
|
|
16120
16312
|
})
|
|
16121
16313
|
}] };
|
|
16122
16314
|
const resolvedInboxId = inboxId;
|
|
16123
|
-
const
|
|
16315
|
+
const markdown = yjsToMarkdown((await server.getChildProvider(resolvedInboxId)).document.getXmlFragment("default"), "AI Inbox");
|
|
16124
16316
|
const pendingTasks = [];
|
|
16125
16317
|
treeMap.forEach((value, id) => {
|
|
16126
16318
|
if (value.parentId === resolvedInboxId) pendingTasks.push({
|
|
@@ -16206,16 +16398,16 @@ function registerChannelTools(mcp, server) {
|
|
|
16206
16398
|
const replyId = crypto.randomUUID();
|
|
16207
16399
|
const now = Date.now();
|
|
16208
16400
|
rootDoc.transact(() => {
|
|
16209
|
-
treeMap.set(replyId, {
|
|
16401
|
+
treeMap.set(replyId, makeEntryMap({
|
|
16210
16402
|
label,
|
|
16211
16403
|
parentId: doc_id,
|
|
16212
16404
|
order: now,
|
|
16213
16405
|
type: "doc",
|
|
16214
16406
|
createdAt: now,
|
|
16215
16407
|
updatedAt: now
|
|
16216
|
-
});
|
|
16408
|
+
}));
|
|
16217
16409
|
});
|
|
16218
|
-
populateYDocFromMarkdown((await server.getChildProvider(replyId)).document, text);
|
|
16410
|
+
populateYDocFromMarkdown((await server.getChildProvider(replyId)).document.getXmlFragment("default"), text);
|
|
16219
16411
|
if (task_id) server.clearAiTask(task_id);
|
|
16220
16412
|
return { content: [{
|
|
16221
16413
|
type: "text",
|
|
@@ -16989,9 +17181,14 @@ function registerAgentGuide(mcp) {
|
|
|
16989
17181
|
|
|
16990
17182
|
//#endregion
|
|
16991
17183
|
//#region packages/mcp/src/resources/tree-resource.ts
|
|
17184
|
+
/**
|
|
17185
|
+
* Dynamic resource: document tree as navigable JSON.
|
|
17186
|
+
*/
|
|
16992
17187
|
function readEntries(treeMap) {
|
|
16993
17188
|
const entries = [];
|
|
16994
|
-
treeMap.forEach((
|
|
17189
|
+
treeMap.forEach((raw, id) => {
|
|
17190
|
+
const value = toPlain(raw);
|
|
17191
|
+
if (typeof value !== "object" || value === null) return;
|
|
16995
17192
|
entries.push({
|
|
16996
17193
|
id,
|
|
16997
17194
|
label: value.label || "Untitled",
|