@abraca/mcp 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/index.d.ts
CHANGED
|
@@ -78,6 +78,31 @@ declare class AbracadabraMCPServer {
|
|
|
78
78
|
* Clears the child provider cache (children belong to the previous space).
|
|
79
79
|
*/
|
|
80
80
|
switchSpace(docId: string): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Make the space that OWNS `targetId` the active connection before a tree
|
|
83
|
+
* read/write, so the op targets the right space's `doc-tree` map.
|
|
84
|
+
*
|
|
85
|
+
* Without this, every tree tool used `getTreeMap()` (the single active
|
|
86
|
+
* space) and treated `rootId`/`parentId` as a mere filter — so reading a
|
|
87
|
+
* non-active space returned stale/empty trees, and *writing* to one
|
|
88
|
+
* silently orphaned the entry into the active space's map (a real
|
|
89
|
+
* data-loss path: docs built "in Ideas" while Development was active
|
|
90
|
+
* landed in Development under a non-node parent and never reached Ideas).
|
|
91
|
+
*
|
|
92
|
+
* Resolution order:
|
|
93
|
+
* - falsy or already the active root → no-op.
|
|
94
|
+
* - a known Space id → connect/activate it (cheap; `_spaceConnections`
|
|
95
|
+
* caches the provider so re-activation is free).
|
|
96
|
+
* - present in the active space's tree → no-op (a normal child doc).
|
|
97
|
+
* - present in an already-connected *other* space's tree → activate that.
|
|
98
|
+
* - otherwise unresolved → returns `false`; callers decide (reads report
|
|
99
|
+
* not-found/empty, writes refuse rather than orphan).
|
|
100
|
+
*
|
|
101
|
+
* Switching uses {@link _connectToSpace} (not {@link switchSpace}) so the
|
|
102
|
+
* child-content provider cache survives — content docs are keyed by global
|
|
103
|
+
* guid, independent of which space is active.
|
|
104
|
+
*/
|
|
105
|
+
ensureSpaceActive(targetId: string | null | undefined): Promise<boolean>;
|
|
81
106
|
/** Get the root doc-tree Y.Map of the active space. */
|
|
82
107
|
getTreeMap(): Y.Map<any> | null;
|
|
83
108
|
/**
|
|
@@ -198,6 +223,17 @@ declare class AbracadabraMCPServer {
|
|
|
198
223
|
target?: string;
|
|
199
224
|
detail?: unknown;
|
|
200
225
|
} | null): void;
|
|
226
|
+
/**
|
|
227
|
+
* Attach expandable `detail` (arguments + result summary) to the most recent
|
|
228
|
+
* tool-history entry for `toolName`, then re-broadcast `toolHistory`.
|
|
229
|
+
*
|
|
230
|
+
* Called by the central tool wrapper in index.ts *after* a tool resolves, so
|
|
231
|
+
* the dashboard's tool cards can expand to show what the tool was called with
|
|
232
|
+
* and what it returned — the same affordance built-in Claude tools (bash /
|
|
233
|
+
* read / edit) already get via the hook-bridge. Tools that never emitted a
|
|
234
|
+
* pill (no `setActiveToolCall`) have no entry to attach to, so this no-ops.
|
|
235
|
+
*/
|
|
236
|
+
recordToolDetail(toolName: string, detail: unknown): void;
|
|
201
237
|
/**
|
|
202
238
|
* Send a typing indicator to a chat channel. Pass the channel doc id
|
|
203
239
|
* (or for legacy callers, a `group:<docId>` string — we strip the prefix).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abraca/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "MCP server for Abracadabra — AI agent collaboration on CRDT documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"y-protocols": "^1.0.6",
|
|
37
37
|
"yjs": "^13.6.8",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
|
-
"@abraca/convert": "2.
|
|
39
|
+
"@abraca/convert": "2.9.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"test": "node --no-warnings --conditions=source --experimental-transform-types --test 'tests/*.test.ts'"
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { registerTreeResource } from "./resources/tree-resource.ts";
|
|
|
33
33
|
import { loadSchemaBundle, SchemaBundleLoadError } from "./schema/loader.ts";
|
|
34
34
|
import type { SchemaBundleValidator } from "./schema/validator.ts";
|
|
35
35
|
import { AbracadabraMCPServer, type TriggerMode } from "./server.ts";
|
|
36
|
+
import { wrapToolsWithDetail } from "./tool-detail.ts";
|
|
36
37
|
import { registerAwarenessTools } from "./tools/awareness.ts";
|
|
37
38
|
import { registerChannelTools } from "./tools/channel.ts";
|
|
38
39
|
import { registerContentTools } from "./tools/content.ts";
|
|
@@ -149,6 +150,15 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
|
|
|
149
150
|
},
|
|
150
151
|
);
|
|
151
152
|
|
|
153
|
+
// Wrap mcp.tool ONCE so every tool invocation records an expandable detail
|
|
154
|
+
// (the arguments it was called with + the text it returned) onto its
|
|
155
|
+
// tool-history entry. This is what lets the cou-sh chat cards expand to show
|
|
156
|
+
// "which doc / which tree / what it found" — the same way built-in Claude
|
|
157
|
+
// tools (bash / read / edit) already expand via the hook-bridge. The pill
|
|
158
|
+
// itself is still emitted by each tool's own setActiveToolCall; we only
|
|
159
|
+
// enrich it after the handler resolves. Must run before the register* calls.
|
|
160
|
+
wrapToolsWithDetail(mcp, server);
|
|
161
|
+
|
|
152
162
|
// Register tools
|
|
153
163
|
registerTreeTools(mcp, server, schemaValidator);
|
|
154
164
|
registerContentTools(mcp, server);
|
package/src/server.ts
CHANGED
|
@@ -311,6 +311,58 @@ export class AbracadabraMCPServer {
|
|
|
311
311
|
console.error(`[abracadabra-mcp] Switched active space to ${docId}`);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Make the space that OWNS `targetId` the active connection before a tree
|
|
316
|
+
* read/write, so the op targets the right space's `doc-tree` map.
|
|
317
|
+
*
|
|
318
|
+
* Without this, every tree tool used `getTreeMap()` (the single active
|
|
319
|
+
* space) and treated `rootId`/`parentId` as a mere filter — so reading a
|
|
320
|
+
* non-active space returned stale/empty trees, and *writing* to one
|
|
321
|
+
* silently orphaned the entry into the active space's map (a real
|
|
322
|
+
* data-loss path: docs built "in Ideas" while Development was active
|
|
323
|
+
* landed in Development under a non-node parent and never reached Ideas).
|
|
324
|
+
*
|
|
325
|
+
* Resolution order:
|
|
326
|
+
* - falsy or already the active root → no-op.
|
|
327
|
+
* - a known Space id → connect/activate it (cheap; `_spaceConnections`
|
|
328
|
+
* caches the provider so re-activation is free).
|
|
329
|
+
* - present in the active space's tree → no-op (a normal child doc).
|
|
330
|
+
* - present in an already-connected *other* space's tree → activate that.
|
|
331
|
+
* - otherwise unresolved → returns `false`; callers decide (reads report
|
|
332
|
+
* not-found/empty, writes refuse rather than orphan).
|
|
333
|
+
*
|
|
334
|
+
* Switching uses {@link _connectToSpace} (not {@link switchSpace}) so the
|
|
335
|
+
* child-content provider cache survives — content docs are keyed by global
|
|
336
|
+
* guid, independent of which space is active.
|
|
337
|
+
*/
|
|
338
|
+
async ensureSpaceActive(
|
|
339
|
+
targetId: string | null | undefined,
|
|
340
|
+
): Promise<boolean> {
|
|
341
|
+
if (!targetId || targetId === this._rootDocId) return true;
|
|
342
|
+
|
|
343
|
+
// A top-level Space id — activate it directly (handles the common
|
|
344
|
+
// `list_spaces` → operate-on-space-id flow that triggered the bug).
|
|
345
|
+
if (this._spaces.some((s) => s.id === targetId)) {
|
|
346
|
+
await this._connectToSpace(targetId);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// A normal doc already in the active space's tree — nothing to do.
|
|
351
|
+
if (this.getTreeMap()?.has(targetId)) return true;
|
|
352
|
+
|
|
353
|
+
// A doc in another space we've already connected to — activate it.
|
|
354
|
+
for (const [spaceId, conn] of this._spaceConnections) {
|
|
355
|
+
if (spaceId === this._rootDocId) continue;
|
|
356
|
+
if (conn.doc.getMap("doc-tree").has(targetId)) {
|
|
357
|
+
await this._connectToSpace(spaceId);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Unresolved: not a space, not in any connected tree. Caller guards.
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
314
366
|
/** Get the root doc-tree Y.Map of the active space. */
|
|
315
367
|
getTreeMap(): Y.Map<any> | null {
|
|
316
368
|
return this._activeConnection?.doc.getMap("doc-tree") ?? null;
|
|
@@ -1122,6 +1174,33 @@ export class AbracadabraMCPServer {
|
|
|
1122
1174
|
}
|
|
1123
1175
|
}
|
|
1124
1176
|
|
|
1177
|
+
/**
|
|
1178
|
+
* Attach expandable `detail` (arguments + result summary) to the most recent
|
|
1179
|
+
* tool-history entry for `toolName`, then re-broadcast `toolHistory`.
|
|
1180
|
+
*
|
|
1181
|
+
* Called by the central tool wrapper in index.ts *after* a tool resolves, so
|
|
1182
|
+
* the dashboard's tool cards can expand to show what the tool was called with
|
|
1183
|
+
* and what it returned — the same affordance built-in Claude tools (bash /
|
|
1184
|
+
* read / edit) already get via the hook-bridge. Tools that never emitted a
|
|
1185
|
+
* pill (no `setActiveToolCall`) have no entry to attach to, so this no-ops.
|
|
1186
|
+
*/
|
|
1187
|
+
recordToolDetail(toolName: string, detail: unknown): void {
|
|
1188
|
+
const provider = this._activeConnection?.provider;
|
|
1189
|
+
if (!provider) return;
|
|
1190
|
+
// The matching pill was pushed at the start of this tool's handler, so the
|
|
1191
|
+
// last detail-less entry for this name is this invocation's entry.
|
|
1192
|
+
for (let i = this._toolHistory.length - 1; i >= 0; i--) {
|
|
1193
|
+
const h = this._toolHistory[i];
|
|
1194
|
+
if (h.tool === toolName && h.detail == null) {
|
|
1195
|
+
h.detail = detail;
|
|
1196
|
+
provider.awareness.setLocalStateField("toolHistory", [
|
|
1197
|
+
...this._toolHistory,
|
|
1198
|
+
]);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1125
1204
|
/**
|
|
1126
1205
|
* Send a typing indicator to a chat channel. Pass the channel doc id
|
|
1127
1206
|
* (or for legacy callers, a `group:<docId>` string — we strip the prefix).
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-detail — central enrichment of abracadabra MCP tool cards.
|
|
3
|
+
*
|
|
4
|
+
* Built-in Claude tools (Bash/Read/Edit/…) flow through the hook-bridge, which
|
|
5
|
+
* attaches an expandable `detail` (command text, file path, diff) that the
|
|
6
|
+
* cou-sh chat renders as a click-to-expand block. abracadabra's own MCP tools
|
|
7
|
+
* are explicitly skipped by the hook-bridge and only ever emitted a bare
|
|
8
|
+
* name+target pill — so their cards had nothing to expand ("no idea what docs
|
|
9
|
+
* it sees or what the tree was").
|
|
10
|
+
*
|
|
11
|
+
* `wrapToolsWithDetail` patches `McpServer.tool` ONCE, before any tool is
|
|
12
|
+
* registered, so every tool invocation records an expandable detail onto its
|
|
13
|
+
* tool-history entry (via `server.recordToolDetail`). The detail leads with the
|
|
14
|
+
* human-readable title + id of any document the call referenced (resolved from
|
|
15
|
+
* the live tree), then the remaining arguments, then the text the tool returned
|
|
16
|
+
* (e.g. the full document tree for `get_document_tree`). The pill itself is
|
|
17
|
+
* still emitted by each tool's own `setActiveToolCall`; this only fills in the
|
|
18
|
+
* expandable detail afterwards.
|
|
19
|
+
*/
|
|
20
|
+
import { toPlain } from "@abraca/dabra";
|
|
21
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
|
+
import type { AbracadabraMCPServer } from "./server.ts";
|
|
23
|
+
|
|
24
|
+
/** Per-field cap (chars) — keeps the awareness payload bounded. Mirrors the
|
|
25
|
+
* hook-bridge's DETAIL_MAX so built-in and MCP tool cards behave alike. */
|
|
26
|
+
const DETAIL_MAX = 4000;
|
|
27
|
+
|
|
28
|
+
/** Argument keys that hold a document id, in the order we surface them. */
|
|
29
|
+
const DOC_ID_KEYS = [
|
|
30
|
+
"docId",
|
|
31
|
+
"id",
|
|
32
|
+
"rootId",
|
|
33
|
+
"parentId",
|
|
34
|
+
"newParentId",
|
|
35
|
+
"uploadId",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
function cap(s: string): string {
|
|
39
|
+
return s.length > DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n… (truncated)` : s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeJson(value: unknown): string {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
} catch {
|
|
46
|
+
return String(value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Pull the joined text content out of a tool result, if any. */
|
|
51
|
+
function resultText(result: unknown): string {
|
|
52
|
+
const content = (result as { content?: unknown })?.content;
|
|
53
|
+
if (!Array.isArray(content)) return "";
|
|
54
|
+
return content
|
|
55
|
+
.filter((c) => c && (c as { type?: string }).type === "text")
|
|
56
|
+
.map((c) => (c as { text?: string }).text ?? "")
|
|
57
|
+
.join("\n")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the expandable detail shown when a tool card is clicked:
|
|
63
|
+
* 1. one `id — "Title"` line per document the args referenced (title resolved
|
|
64
|
+
* from the live tree when available),
|
|
65
|
+
* 2. the remaining arguments as compact JSON,
|
|
66
|
+
* 3. the text the tool returned (document tree, content, confirmation, …).
|
|
67
|
+
* Rendered by cou-sh's ChatActionCard as a `kind: "text"` block.
|
|
68
|
+
*/
|
|
69
|
+
export function buildToolDetail(
|
|
70
|
+
toolArgs: unknown,
|
|
71
|
+
result: unknown,
|
|
72
|
+
resolveLabel?: (id: string) => string | undefined,
|
|
73
|
+
): { kind: "text"; text: string } {
|
|
74
|
+
const lines: string[] = [];
|
|
75
|
+
const rest: Record<string, unknown> =
|
|
76
|
+
toolArgs && typeof toolArgs === "object"
|
|
77
|
+
? { ...(toolArgs as Record<string, unknown>) }
|
|
78
|
+
: {};
|
|
79
|
+
|
|
80
|
+
// Headline: each referenced doc as "id — title" (title when resolvable).
|
|
81
|
+
for (const key of DOC_ID_KEYS) {
|
|
82
|
+
const v = rest[key];
|
|
83
|
+
if (typeof v === "string" && v.length > 0) {
|
|
84
|
+
const label = resolveLabel?.(v);
|
|
85
|
+
lines.push(label ? `${key}: ${v} — "${label}"` : `${key}: ${v}`);
|
|
86
|
+
delete rest[key];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Remaining arguments (depth, type, label, meta, content, query, …).
|
|
91
|
+
const restStr = Object.keys(rest).length > 0 ? safeJson(rest) : "";
|
|
92
|
+
if (restStr && restStr !== "{}") lines.push(`args: ${restStr}`);
|
|
93
|
+
if (lines.length === 0) lines.push("args: (no arguments)");
|
|
94
|
+
|
|
95
|
+
const text = resultText(result);
|
|
96
|
+
if (text) {
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push(text);
|
|
99
|
+
}
|
|
100
|
+
return { kind: "text", text: cap(lines.join("\n")) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Monkey-patch `mcp.tool` so each registered tool's handler is wrapped to record
|
|
105
|
+
* arguments + result detail after it resolves. Call once at startup, before the
|
|
106
|
+
* register* functions run. Errors in detail-recording are swallowed — they must
|
|
107
|
+
* never break the underlying tool.
|
|
108
|
+
*/
|
|
109
|
+
export function wrapToolsWithDetail(
|
|
110
|
+
mcp: McpServer,
|
|
111
|
+
server: AbracadabraMCPServer,
|
|
112
|
+
): void {
|
|
113
|
+
// Resolve a document id to its current label from the live tree, if loaded.
|
|
114
|
+
const resolveLabel = (id: string): string | undefined => {
|
|
115
|
+
try {
|
|
116
|
+
const raw = server.getTreeMap()?.get(id);
|
|
117
|
+
if (!raw) return undefined;
|
|
118
|
+
const label = (toPlain(raw) as { label?: unknown })?.label;
|
|
119
|
+
return typeof label === "string" && label.length > 0 ? label : undefined;
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const original = mcp.tool.bind(mcp) as (...args: unknown[]) => unknown;
|
|
126
|
+
(mcp as { tool: unknown }).tool = (...args: unknown[]) => {
|
|
127
|
+
const name = args[0];
|
|
128
|
+
const cbIndex = args.length - 1;
|
|
129
|
+
const cb = args[cbIndex];
|
|
130
|
+
if (typeof name === "string" && typeof cb === "function") {
|
|
131
|
+
const handler = cb as (...a: unknown[]) => unknown;
|
|
132
|
+
args[cbIndex] = async (...handlerArgs: unknown[]) => {
|
|
133
|
+
const result = await handler(...handlerArgs);
|
|
134
|
+
try {
|
|
135
|
+
server.recordToolDetail(
|
|
136
|
+
name,
|
|
137
|
+
buildToolDetail(handlerArgs[0], result, resolveLabel),
|
|
138
|
+
);
|
|
139
|
+
} catch {
|
|
140
|
+
/* detail recording must never break a tool */
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return original(...args);
|
|
146
|
+
};
|
|
147
|
+
}
|
package/src/tools/content.ts
CHANGED
|
@@ -21,6 +21,7 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
|
|
|
21
21
|
server.setAutoStatus('reading', docId)
|
|
22
22
|
server.setActiveToolCall({ name: 'read_document', target: docId })
|
|
23
23
|
|
|
24
|
+
await server.ensureSpaceActive(docId)
|
|
24
25
|
const provider = await server.getChildProvider(docId)
|
|
25
26
|
const fragment = provider.document.getXmlFragment('default')
|
|
26
27
|
|
|
@@ -90,6 +91,7 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
|
|
|
90
91
|
server.setAutoStatus('writing', docId)
|
|
91
92
|
server.setActiveToolCall({ name: 'write_document', target: docId })
|
|
92
93
|
|
|
94
|
+
await server.ensureSpaceActive(docId)
|
|
93
95
|
const writeMode = mode ?? 'replace'
|
|
94
96
|
const provider = await server.getChildProvider(docId)
|
|
95
97
|
const doc = provider.document
|
|
@@ -127,9 +129,21 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
|
|
|
127
129
|
})
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
// Write new content
|
|
132
|
+
// Write new content.
|
|
133
|
+
//
|
|
134
|
+
// ROOT-CAUSE FIX for the "card title silently becomes Untitled" bug:
|
|
135
|
+
// when the markdown carries neither a frontmatter `title:` nor a
|
|
136
|
+
// leading `# H1`, populateYDocFromMarkdown seeds the body
|
|
137
|
+
// `documentHeader` with `fallbackTitle`. Passing the literal
|
|
138
|
+
// 'Untitled' baked the placeholder string into the header — which a
|
|
139
|
+
// client's title-sync (cou-sh) then read back and propagated into
|
|
140
|
+
// the tree `label`, destroying the real title. Seed from the doc's
|
|
141
|
+
// ACTUAL label instead so the header agrees with the tree from the
|
|
142
|
+
// start (header === label → title-sync is a no-op). Only fall back
|
|
143
|
+
// to 'Untitled' when the doc genuinely has no label yet.
|
|
131
144
|
const contentToWrite = body || markdown
|
|
132
|
-
const
|
|
145
|
+
const existingLabel = server.getDocSummary(docId)?.label
|
|
146
|
+
const fallbackTitle = title || existingLabel || 'Untitled'
|
|
133
147
|
populateYDocFromMarkdown(fragment, contentToWrite, fallbackTitle)
|
|
134
148
|
|
|
135
149
|
// Auto-update presence: mark this doc as focused and place cursor at end
|
package/src/tools/meta.ts
CHANGED
|
@@ -21,6 +21,7 @@ export function registerMetaTools(
|
|
|
21
21
|
async ({ docId }) => {
|
|
22
22
|
server.setAutoStatus('reading', docId)
|
|
23
23
|
server.setActiveToolCall({ name: 'get_metadata', target: docId })
|
|
24
|
+
await server.ensureSpaceActive(docId)
|
|
24
25
|
const treeMap = server.getTreeMap()
|
|
25
26
|
if (!treeMap) {
|
|
26
27
|
return { content: [{ type: 'text', text: 'Not connected' }] }
|
|
@@ -55,6 +56,7 @@ export function registerMetaTools(
|
|
|
55
56
|
async ({ docId, meta }) => {
|
|
56
57
|
server.setAutoStatus('writing', docId)
|
|
57
58
|
server.setActiveToolCall({ name: 'update_metadata', target: docId })
|
|
59
|
+
await server.ensureSpaceActive(docId)
|
|
58
60
|
const treeMap = server.getTreeMap()
|
|
59
61
|
if (!treeMap) {
|
|
60
62
|
return { content: [{ type: 'text', text: 'Not connected' }] }
|