@bli-cockpit/mcp 0.1.58 → 0.1.61
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 +69 -0
- package/dist/agent-door.d.ts +12 -0
- package/dist/agent-door.js +12 -1
- package/dist/cal-tools.js +25 -22
- package/dist/careers-tools.d.ts +6 -0
- package/dist/careers-tools.js +79 -32
- package/dist/docs-msg-tools.js +109 -34
- package/dist/mail-tools.js +47 -26
- package/dist/memory-consolidation-tools.js +20 -2
- package/dist/memory-experience-tools.js +5 -1
- package/dist/models-tools.js +15 -2
- package/dist/note-folder-tools.js +11 -5
- package/dist/notes-account-tools.js +2 -2
- package/dist/notes-tools.js +11 -4
- package/dist/ops-tools.js +40 -6
- package/dist/server.d.ts +8 -2
- package/dist/server.js +27 -4
- package/dist/settings-tools.js +35 -8
- package/dist/tool-catalog.d.ts +32 -0
- package/dist/tool-catalog.js +150 -0
- package/dist/tool-result.d.ts +52 -1
- package/dist/tool-result.js +92 -2
- package/dist/usage-tools.js +47 -8
- package/dist/work-tools.js +48 -19
- package/package.json +1 -1
package/dist/docs-msg-tools.js
CHANGED
|
@@ -11,7 +11,25 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { callAgentDoor } from "./agent-door.js";
|
|
14
|
-
import { doorFailureText, errorResult, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
14
|
+
import { DEFAULT_LIST_LIMIT, doorFailureText, errorResult, moreNote, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
15
|
+
/** A canonical uuid: a ref that already IS an id needs no list read to resolve. */
|
|
16
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
17
|
+
/** The door's own ceiling on `?limit=` (`/api/docs/documents` `DOCUMENT_LIST_MAX_LIMIT`). */
|
|
18
|
+
const DOCS_LIST_MAX = 1000;
|
|
19
|
+
/**
|
|
20
|
+
* The tree as the text half prints it: id, title, slug, children. The door
|
|
21
|
+
* also sends visibility, source and a timestamp per node, which nothing here
|
|
22
|
+
* promised and which made the structured half 48 KB for 226 documents
|
|
23
|
+
* (BLI-10432). `docs_list` carries those fields for a flat read.
|
|
24
|
+
*/
|
|
25
|
+
function slimTree(nodes) {
|
|
26
|
+
return nodes.map((node) => ({
|
|
27
|
+
id: node.id,
|
|
28
|
+
title: node.title,
|
|
29
|
+
slug: node.slug ?? null,
|
|
30
|
+
children: slimTree(Array.isArray(node.children) ? node.children : []),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
15
33
|
/** Indented lines, deepest last — the same shape `cockpit docs tree` prints. */
|
|
16
34
|
function renderDocTree(nodes, depth) {
|
|
17
35
|
return nodes
|
|
@@ -25,6 +43,27 @@ function renderDocTree(nodes, depth) {
|
|
|
25
43
|
function countTree(nodes) {
|
|
26
44
|
return nodes.reduce((count, node) => count + 1 + countTree(node.children ?? []), 0);
|
|
27
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The fields `docs_list` promises, and only those. The door's row also carries
|
|
48
|
+
* `created_by`, `updated_by`, `created_at`, `import_truncated` and
|
|
49
|
+
* `missing_block_count`: 105 KB for 226 documents before this (BLI-10432).
|
|
50
|
+
*/
|
|
51
|
+
function docListRow(row) {
|
|
52
|
+
return {
|
|
53
|
+
id: row.id,
|
|
54
|
+
slug: row.slug ?? null,
|
|
55
|
+
title: row.title,
|
|
56
|
+
visibility: row.visibility,
|
|
57
|
+
source: row.source ?? null,
|
|
58
|
+
parent_id: row.parent_id ?? null,
|
|
59
|
+
updated_at: row.updated_at,
|
|
60
|
+
body_chars: row.body_chars ?? null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function docListLine(row) {
|
|
64
|
+
return `${row.id} ${row.slug ?? "-"} ${row.title} [${row.visibility}${row.source ? `, ${row.source}` : ""}, `
|
|
65
|
+
+ `${row.body_chars ?? "?"} chars, parent ${row.parent_id ?? "root"}, updated ${row.updated_at}]`;
|
|
66
|
+
}
|
|
28
67
|
/**
|
|
29
68
|
* `docs_list`'s narrowing, as the door's own query parameters (BLI-3737).
|
|
30
69
|
* The list itself is metadata only — asking for a whole library used to cost
|
|
@@ -39,10 +78,18 @@ export function docsListPath(args) {
|
|
|
39
78
|
params.set("query", args.query);
|
|
40
79
|
if (typeof args.limit === "number" && Number.isInteger(args.limit))
|
|
41
80
|
params.set("limit", String(args.limit));
|
|
81
|
+
// No limit named: one page plus one row, so "more exist" is read off the
|
|
82
|
+
// door instead of guessed from a full page (BLI-10432).
|
|
83
|
+
else if (args.limit === undefined)
|
|
84
|
+
params.set("limit", String(DEFAULT_LIST_LIMIT + 1));
|
|
42
85
|
const search = params.toString();
|
|
43
86
|
return search.length > 0 ? `/api/docs/documents?${search}` : "/api/docs/documents";
|
|
44
87
|
}
|
|
45
88
|
async function resolveDocumentId(deps, session, ref) {
|
|
89
|
+
// An id needs no lookup: the read itself answers 404 by name when it is
|
|
90
|
+
// wrong. Only a slug costs the whole-library list read (BLI-10432).
|
|
91
|
+
if (UUID_PATTERN.test(ref))
|
|
92
|
+
return { status: "ok", id: ref };
|
|
46
93
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/docs/documents");
|
|
47
94
|
if (!response.ok)
|
|
48
95
|
return { status: "list_failed", text: doorFailureText("docs_list", response) };
|
|
@@ -59,24 +106,33 @@ async function resolveChannelId(deps, session, ref) {
|
|
|
59
106
|
const match = channels.find((channel) => channel.id === ref || channel.name === bare);
|
|
60
107
|
return match ? { status: "ok", id: match.id } : { status: "not_found" };
|
|
61
108
|
}
|
|
109
|
+
/** One message as a line: id (msg_thread's `thread_parent_id`), time, author, text. */
|
|
110
|
+
function messageLine(message) {
|
|
111
|
+
return `${String(message.id ?? "?")} ${String(message.created_at ?? "?")} `
|
|
112
|
+
+ `${String(message.agent_label ?? message.user_id ?? "?")} ${String(message.content ?? "")}`;
|
|
113
|
+
}
|
|
62
114
|
export function registerDocsMsgTools(server, deps) {
|
|
63
115
|
const register = registrarFor(server);
|
|
64
116
|
register("docs_list", {
|
|
65
117
|
title: "List Tower documents",
|
|
66
118
|
description: "Every document you may read: id, slug, title, visibility, source, parent_id and body_chars (how long the body is). "
|
|
67
119
|
+ "Never a document's body — call docs_read for that. Narrow before reading: parent_id (a document id, or \"root\" for the "
|
|
68
|
-
+ "top of the tree), query (a case-insensitive substring matched in the database over title AND body, never fuzzy),
|
|
120
|
+
+ "top of the tree), query (a case-insensitive substring matched in the database over title AND body, never fuzzy), "
|
|
121
|
+
+ `limit (1-1000, default ${DEFAULT_LIST_LIMIT}; the answer says when more exist).`,
|
|
69
122
|
inputSchema: {
|
|
70
123
|
parent_id: z.string().min(1).max(64).optional().describe('A document id to list under, or "root" for the top of the tree.'),
|
|
71
124
|
query: z.string().min(1).max(200).optional().describe("Keep only documents whose title or body contains this text."),
|
|
72
|
-
limit: z.number().int().min(1).max(1000).optional().describe(
|
|
125
|
+
limit: z.number().int().min(1).max(1000).optional().describe(`At most this many documents. Default ${DEFAULT_LIST_LIMIT}.`),
|
|
73
126
|
},
|
|
74
127
|
}, async (args) => withSession(deps, async (session) => {
|
|
75
128
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", docsListPath(args));
|
|
76
129
|
if (!response.ok)
|
|
77
130
|
return errorResult(doorFailureText("docs_list", response));
|
|
78
|
-
const
|
|
79
|
-
|
|
131
|
+
const all = (Array.isArray(response.body.documents) ? response.body.documents : []);
|
|
132
|
+
const more = args.limit === undefined && all.length > DEFAULT_LIST_LIMIT;
|
|
133
|
+
const rows = more ? all.slice(0, DEFAULT_LIST_LIMIT) : all;
|
|
134
|
+
const lines = rows.map(docListLine).join("\n");
|
|
135
|
+
return textResult(`${rows.length} document(s).${moreNote(more, DOCS_LIST_MAX)}${lines ? `\n${lines}` : ""}`, { documents: rows.map(docListRow), more });
|
|
80
136
|
}));
|
|
81
137
|
register("docs_tree", {
|
|
82
138
|
title: "Read the Tower document tree",
|
|
@@ -87,9 +143,9 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
87
143
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/docs/tree");
|
|
88
144
|
if (!response.ok)
|
|
89
145
|
return errorResult(doorFailureText("docs_tree", response));
|
|
90
|
-
const tree = (Array.isArray(response.body.tree) ? response.body.tree : []);
|
|
146
|
+
const tree = slimTree((Array.isArray(response.body.tree) ? response.body.tree : []));
|
|
91
147
|
const rendered = renderDocTree(tree, 0);
|
|
92
|
-
return textResult(`${countTree(tree)} document(s).${rendered ? `\n${rendered}` : ""}`, { tree });
|
|
148
|
+
return textResult(`${countTree(tree)} document(s), each as "title (slug or id)"; docs_read takes either.${rendered ? `\n${rendered}` : ""}`, { tree });
|
|
93
149
|
}));
|
|
94
150
|
register("docs_read", {
|
|
95
151
|
title: "Read a Tower document",
|
|
@@ -112,10 +168,10 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
112
168
|
title: "Create a Tower document",
|
|
113
169
|
description: "Creates a new document. Omitting body creates an empty one, matching the browser's own default.",
|
|
114
170
|
inputSchema: {
|
|
115
|
-
title: z.string().min(1).max(200),
|
|
116
|
-
body_markdown: z.string().max(200_000).optional(),
|
|
117
|
-
parent_id: z.string().uuid().optional(),
|
|
118
|
-
visibility: z.enum(["org", "private"]).optional(),
|
|
171
|
+
title: z.string().min(1).max(200).describe("The document's title."),
|
|
172
|
+
body_markdown: z.string().max(200_000).optional().describe("The body, markdown. Omit for an empty document."),
|
|
173
|
+
parent_id: z.string().uuid().optional().describe("A document id to nest this one under. Omit for the top of the tree."),
|
|
174
|
+
visibility: z.enum(["org", "private"]).optional().describe("org: everyone signed in may read it. private: only you."),
|
|
119
175
|
},
|
|
120
176
|
}, async (args) => withSession(deps, async (session) => {
|
|
121
177
|
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/docs/documents", {
|
|
@@ -135,10 +191,10 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
135
191
|
+ "pass change. Emptying a document that holds text needs allow_empty: true.",
|
|
136
192
|
inputSchema: {
|
|
137
193
|
id: z.string().min(1).max(200).describe("A document id, or its slug."),
|
|
138
|
-
title: z.string().min(1).max(200).optional(),
|
|
139
|
-
body_markdown: z.string().max(200_000).optional(),
|
|
140
|
-
visibility: z.enum(["org", "private"]).optional(),
|
|
141
|
-
parent_id: z.string().uuid().nullable().optional(),
|
|
194
|
+
title: z.string().min(1).max(200).optional().describe("A new title."),
|
|
195
|
+
body_markdown: z.string().max(200_000).optional().describe("The WHOLE new body, markdown. It replaces the old one; it is not appended."),
|
|
196
|
+
visibility: z.enum(["org", "private"]).optional().describe("org: everyone signed in may read it. private: only you."),
|
|
197
|
+
parent_id: z.string().uuid().nullable().optional().describe("Move it under this document id; null moves it to the top of the tree."),
|
|
142
198
|
allow_empty: z
|
|
143
199
|
.boolean()
|
|
144
200
|
.optional()
|
|
@@ -169,21 +225,40 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
169
225
|
}));
|
|
170
226
|
register("msg_channels", {
|
|
171
227
|
title: "List Tower channels",
|
|
172
|
-
description: "Every channel you are a member of
|
|
228
|
+
description: "Every channel you are a member of: id, name (a direct message has none and lists its participants instead), "
|
|
229
|
+
+ "private, and description. msg_read takes the id or the name.",
|
|
173
230
|
inputSchema: {},
|
|
174
231
|
}, async () => withSession(deps, async (session) => {
|
|
175
232
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/msg/channels");
|
|
176
233
|
if (!response.ok)
|
|
177
234
|
return errorResult(doorFailureText("msg_channels", response));
|
|
178
|
-
const channels = response.body.channels
|
|
179
|
-
|
|
235
|
+
const channels = (Array.isArray(response.body.channels) ? response.body.channels : []);
|
|
236
|
+
const rows = channels.map((channel) => ({
|
|
237
|
+
id: channel.id,
|
|
238
|
+
name: channel.name ?? null,
|
|
239
|
+
description: channel.description ?? null,
|
|
240
|
+
is_private: channel.is_private === true,
|
|
241
|
+
is_dm: channel.is_dm === true,
|
|
242
|
+
...(channel.is_dm === true ? { dm_participants: channel.dm_participants ?? [] } : {}),
|
|
243
|
+
archived_at: channel.archived_at ?? null,
|
|
244
|
+
}));
|
|
245
|
+
const lines = rows
|
|
246
|
+
.map((row) => {
|
|
247
|
+
const label = row.is_dm
|
|
248
|
+
? `DM with ${JSON.stringify(row.dm_participants ?? [])}`
|
|
249
|
+
: `#${String(row.name ?? "?")}${row.is_private ? " (private)" : ""}`;
|
|
250
|
+
return `${String(row.id)} ${label}${row.archived_at ? " [archived]" : ""}`
|
|
251
|
+
+ `${row.description ? ` ${String(row.description)}` : ""}`;
|
|
252
|
+
})
|
|
253
|
+
.join("\n");
|
|
254
|
+
return textResult(`${rows.length} channel(s).${lines ? `\n${lines}` : ""}`, { channels: rows });
|
|
180
255
|
}));
|
|
181
256
|
register("msg_read", {
|
|
182
257
|
title: "Read a Tower channel",
|
|
183
258
|
description: "Recent messages of one channel, newest first. `channel` is a channel id, or its name with or without a leading #.",
|
|
184
259
|
inputSchema: {
|
|
185
|
-
channel: z.string().min(1).max(200),
|
|
186
|
-
limit: z.number().int().min(1).max(200).optional(),
|
|
260
|
+
channel: z.string().min(1).max(200).describe("A channel id, or its name with or without a leading #."),
|
|
261
|
+
limit: z.number().int().min(1).max(200).optional().describe("At most this many messages, newest first."),
|
|
187
262
|
},
|
|
188
263
|
}, async (args) => withSession(deps, async (session) => {
|
|
189
264
|
const ref = String(args.channel ?? "");
|
|
@@ -196,15 +271,19 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
196
271
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/msg/channels/${encodeURIComponent(resolved.id)}/messages${query}`);
|
|
197
272
|
if (!response.ok)
|
|
198
273
|
return errorResult(doorFailureText("msg_read", response));
|
|
199
|
-
const messages = response.body.messages
|
|
200
|
-
|
|
274
|
+
const messages = (Array.isArray(response.body.messages) ? response.body.messages : []);
|
|
275
|
+
// The text half carries the messages themselves, each with its id,
|
|
276
|
+
// which is what msg_thread takes. It used to say only "2 message(s)",
|
|
277
|
+
// so a Claude-family client never saw one (BLI-10432).
|
|
278
|
+
const lines = messages.map(messageLine).join("\n");
|
|
279
|
+
return textResult(`${messages.length} message(s) in ${ref}, newest first; each line is id, time, author, text.${lines ? `\n${lines}` : ""}`, { channelId: resolved.id, messages });
|
|
201
280
|
}));
|
|
202
281
|
register("msg_thread", {
|
|
203
282
|
title: "Read a Tower message thread",
|
|
204
283
|
description: "The replies under one message, oldest first. `thread_parent_id` is the id of the message the thread hangs "
|
|
205
284
|
+ "off — msg_read returns it. `channel` is a channel id, or its name with or without a leading #.",
|
|
206
285
|
inputSchema: {
|
|
207
|
-
channel: z.string().min(1).max(200),
|
|
286
|
+
channel: z.string().min(1).max(200).describe("A channel id, or its name with or without a leading #."),
|
|
208
287
|
thread_parent_id: z.string().uuid().describe("The id of the message the replies hang off."),
|
|
209
288
|
},
|
|
210
289
|
}, async (args) => withSession(deps, async (session) => {
|
|
@@ -221,11 +300,7 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
221
300
|
const messages = (Array.isArray(response.body.messages) ? response.body.messages : []);
|
|
222
301
|
// Oldest first: a thread reads as a conversation, unlike the channel
|
|
223
302
|
// list, which the door returns newest-first.
|
|
224
|
-
const lines = [...messages]
|
|
225
|
-
.reverse()
|
|
226
|
-
.map((message) => `${String(message.created_at ?? "?")} ${String(message.agent_label ?? message.user_id ?? "?")} `
|
|
227
|
-
+ `${String(message.content ?? "")}`)
|
|
228
|
-
.join("\n");
|
|
303
|
+
const lines = [...messages].reverse().map(messageLine).join("\n");
|
|
229
304
|
return textResult(`${messages.length} repl${messages.length === 1 ? "y" : "ies"} in ${ref}.${lines ? `\n${lines}` : ""}`, { channelId: resolved.id, threadParentId: parent, messages });
|
|
230
305
|
}));
|
|
231
306
|
register("msg_create_channel", {
|
|
@@ -233,9 +308,9 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
233
308
|
description: "Creates a channel and returns its id. `private` means membership decides who may read it. `member_emails` names who joins at birth BY EMAIL — an address Tower does not carry refuses the whole create, so no half-built channel is left behind.",
|
|
234
309
|
inputSchema: {
|
|
235
310
|
name: z.string().min(1).max(80).describe("The channel name. A leading # is accepted and dropped."),
|
|
236
|
-
description: z.string().max(500).optional(),
|
|
237
|
-
private: z.boolean().optional(),
|
|
238
|
-
member_emails: z.array(z.string().min(3).max(320)).max(200).optional(),
|
|
311
|
+
description: z.string().max(500).optional().describe("One line saying what the channel is for."),
|
|
312
|
+
private: z.boolean().optional().describe("true: only members may read it. Default false."),
|
|
313
|
+
member_emails: z.array(z.string().min(3).max(320)).max(200).optional().describe("Email addresses of the people who join at birth. You join either way."),
|
|
239
314
|
},
|
|
240
315
|
}, async (args) => withSession(deps, async (session) => {
|
|
241
316
|
const raw = String(args.name ?? "");
|
|
@@ -281,9 +356,9 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
281
356
|
title: "Send a Tower message",
|
|
282
357
|
description: "Posts a message to one channel. `channel` is a channel id, or its name with or without a leading #.",
|
|
283
358
|
inputSchema: {
|
|
284
|
-
channel: z.string().min(1).max(200),
|
|
285
|
-
content: z.string().min(1).max(8_000),
|
|
286
|
-
thread_parent_id: z.string().uuid().optional(),
|
|
359
|
+
channel: z.string().min(1).max(200).describe("A channel id, or its name with or without a leading #."),
|
|
360
|
+
content: z.string().min(1).max(8_000).describe("The message text."),
|
|
361
|
+
thread_parent_id: z.string().uuid().optional().describe("Reply in the thread under this message id (msg_read prints ids)."),
|
|
287
362
|
},
|
|
288
363
|
}, async (args) => withSession(deps, async (session) => {
|
|
289
364
|
const ref = String(args.channel ?? "");
|
package/dist/mail-tools.js
CHANGED
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { z } from "zod";
|
|
28
28
|
import { callAgentDoor } from "./agent-door.js";
|
|
29
|
-
import { CONFIRM_INPUT, doorFailureText, errorResult, queryString, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
|
|
29
|
+
import { CONFIRM_INPUT, EMAIL_INPUT, doorFailureText, errorResult, queryString, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
|
|
30
30
|
function messageLine(message) {
|
|
31
31
|
const from = message.from_address?.address ?? "(no sender)";
|
|
32
32
|
return `${message.is_unread ? "*" : " "} ${message.internal_date.slice(0, 16)} ${from} ${message.subject ?? "(no subject)"} [thread ${message.thread_id ?? "?"}]`;
|
|
33
33
|
}
|
|
34
|
-
const ADDRESS =
|
|
34
|
+
const ADDRESS = EMAIL_INPUT.describe("An email address.");
|
|
35
35
|
export function registerMailTools(server, deps) {
|
|
36
36
|
const register = registrarFor(server);
|
|
37
37
|
register("mail_accounts", {
|
|
@@ -56,11 +56,11 @@ export function registerMailTools(server, deps) {
|
|
|
56
56
|
description: "Messages across every mailbox you may read, newest first. Never a body — call mail_read on a thread for that. "
|
|
57
57
|
+ "`account_id` narrows to one mailbox; `unread` and `label` filter; `before` (an ISO instant) pages backwards.",
|
|
58
58
|
inputSchema: {
|
|
59
|
-
account_id: z.string().uuid().optional(),
|
|
60
|
-
unread: z.boolean().optional(),
|
|
61
|
-
label: z.string().min(1).max(100).optional(),
|
|
62
|
-
before: z.string().min(1).max(40).optional(),
|
|
63
|
-
limit: z.number().int().min(1).max(200).optional(),
|
|
59
|
+
account_id: z.string().uuid().optional().describe("One mailbox id (mail_accounts). Omit for every mailbox you may read."),
|
|
60
|
+
unread: z.boolean().optional().describe("true: unread messages only."),
|
|
61
|
+
label: z.string().min(1).max(100).optional().describe("Only messages carrying this provider label, e.g. INBOX or IMPORTANT."),
|
|
62
|
+
before: z.string().min(1).max(40).optional().describe("An ISO instant: messages received before it, for the next page back."),
|
|
63
|
+
limit: z.number().int().min(1).max(200).optional().describe("At most this many messages, newest first."),
|
|
64
64
|
},
|
|
65
65
|
}, async (args) => withSession(deps, async (session) => {
|
|
66
66
|
const query = new URLSearchParams();
|
|
@@ -84,30 +84,51 @@ export function registerMailTools(server, deps) {
|
|
|
84
84
|
}));
|
|
85
85
|
register("mail_read", {
|
|
86
86
|
title: "Read one conversation",
|
|
87
|
-
description: "One thread with every message's text body, oldest first
|
|
88
|
-
+ "
|
|
89
|
-
|
|
87
|
+
description: "One thread with every message's text body, oldest first, and each message's attachments by id (the id "
|
|
88
|
+
+ "mail_attachment takes). Thread ids come from mail_inbox or mail_search. Read the thread before saying what "
|
|
89
|
+
+ "somebody wrote — a snippet is the first 240 characters and nothing more.",
|
|
90
|
+
inputSchema: { thread_id: z.string().uuid().describe("A thread id, as mail_inbox or mail_search print it.") },
|
|
90
91
|
}, async (args) => withSession(deps, async (session) => {
|
|
91
92
|
const threadId = String(args.thread_id ?? "");
|
|
92
|
-
const response = await callAgentDoor(session, deps.fetchImpl, "GET",
|
|
93
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET",
|
|
94
|
+
// `?attachments=1` adds each message's attachment pointers. Without
|
|
95
|
+
// it no tool here ever showed an attachment id, so mail_attachment
|
|
96
|
+
// could not be reached from an agent at all (BLI-10432).
|
|
97
|
+
`/api/mail/threads/${encodeURIComponent(threadId)}?attachments=1`);
|
|
93
98
|
if (!response.ok)
|
|
94
99
|
return errorResult(doorFailureText("mail_read", response));
|
|
95
100
|
const thread = response.body.thread;
|
|
96
101
|
const messages = (Array.isArray(response.body.messages) ? response.body.messages : []);
|
|
102
|
+
const attachments = (response.body.attachments && typeof response.body.attachments === "object"
|
|
103
|
+
? response.body.attachments
|
|
104
|
+
: {});
|
|
97
105
|
const rendered = messages
|
|
98
|
-
.map((message) =>
|
|
99
|
-
|
|
106
|
+
.map((message) => {
|
|
107
|
+
const files = (attachments[message.id] ?? [])
|
|
108
|
+
.map((file) => `\n attachment ${file.id} ${file.filename ?? "(unnamed)"} ${file.mime_type ?? "?"} ${file.size_bytes ?? "?"} bytes${file.is_inline ? " (inline)" : ""}`)
|
|
109
|
+
.join("");
|
|
110
|
+
return `--- ${message.internal_date} ${message.from_address?.address ?? "(no sender)"} [${message.id}]\n`
|
|
111
|
+
+ (message.body_text ?? message.snippet ?? "(no text body — this message is HTML only)").trim()
|
|
112
|
+
+ files;
|
|
113
|
+
})
|
|
100
114
|
.join("\n\n");
|
|
101
|
-
|
|
115
|
+
// The HTML body is the text body again with markup, and nothing on
|
|
116
|
+
// this surface renders it; its length stays so an HTML-only message
|
|
117
|
+
// still says it has one.
|
|
118
|
+
const slim = messages.map(({ body_html: html, ...rest }) => ({
|
|
119
|
+
...rest,
|
|
120
|
+
body_html_chars: typeof html === "string" ? html.length : 0,
|
|
121
|
+
}));
|
|
122
|
+
return textResult(`${thread?.subject ?? "(no subject)"} — ${thread?.account_address ?? "?"}, ${messages.length} message(s)\n\n${rendered}`, { thread, messages: slim, attachments });
|
|
102
123
|
}));
|
|
103
124
|
register("mail_search", {
|
|
104
125
|
title: "Search your mail",
|
|
105
126
|
description: "Full text over the subject and plain-text body of every message you may read. Quoted phrases and OR work the way "
|
|
106
127
|
+ "they do in a search box. Returns headers only; call mail_read on a hit's thread to read it.",
|
|
107
128
|
inputSchema: {
|
|
108
|
-
query: z.string().min(1).max(400),
|
|
109
|
-
account_id: z.string().uuid().optional(),
|
|
110
|
-
limit: z.number().int().min(1).max(200).optional(),
|
|
129
|
+
query: z.string().min(1).max(400).describe("Words to find. Quoted phrases and OR work."),
|
|
130
|
+
account_id: z.string().uuid().optional().describe("One mailbox id (mail_accounts). Omit for every mailbox you may read."),
|
|
131
|
+
limit: z.number().int().min(1).max(200).optional().describe("At most this many matches, newest first."),
|
|
111
132
|
},
|
|
112
133
|
}, async (args) => withSession(deps, async (session) => {
|
|
113
134
|
const query = new URLSearchParams({ q: String(args.query ?? "") });
|
|
@@ -129,12 +150,12 @@ export function registerMailTools(server, deps) {
|
|
|
129
150
|
+ "that is never something to guess. `in_reply_to_message_id` (a message id from mail_read) threads the reply. "
|
|
130
151
|
+ "This actually sends mail to real people: confirm must be true.",
|
|
131
152
|
inputSchema: {
|
|
132
|
-
account_id: z.string().uuid(),
|
|
133
|
-
to: z.array(ADDRESS).min(1).max(50),
|
|
134
|
-
cc: z.array(ADDRESS).max(50).optional(),
|
|
135
|
-
subject: z.string().max(998),
|
|
136
|
-
text: z.string().min(1).max(500_000),
|
|
137
|
-
in_reply_to_message_id: z.string().uuid().optional(),
|
|
153
|
+
account_id: z.string().uuid().describe("The mailbox it goes out from (mail_accounts). Never guessed."),
|
|
154
|
+
to: z.array(ADDRESS).min(1).max(50).describe("Recipient email addresses."),
|
|
155
|
+
cc: z.array(ADDRESS).max(50).optional().describe("Copied email addresses."),
|
|
156
|
+
subject: z.string().max(998).describe("The subject line."),
|
|
157
|
+
text: z.string().min(1).max(500_000).describe("The plain-text body."),
|
|
158
|
+
in_reply_to_message_id: z.string().uuid().optional().describe("A message id from mail_read: send this as a reply in that thread."),
|
|
138
159
|
confirm: CONFIRM_INPUT,
|
|
139
160
|
},
|
|
140
161
|
}, async (args) => withSession(deps, async (session) => {
|
|
@@ -160,7 +181,7 @@ export function registerMailTools(server, deps) {
|
|
|
160
181
|
title: "Describe an attachment",
|
|
161
182
|
description: "The filename, mime type and size of one attachment. Attachments are POINTERS in Tower — nothing is stored — and "
|
|
162
183
|
+ "the bytes are not returned here: `cockpit mail attachment <id> --out <path>` downloads the file at a terminal.",
|
|
163
|
-
inputSchema: { attachment_id: z.string().uuid() },
|
|
184
|
+
inputSchema: { attachment_id: z.string().uuid().describe("An attachment id, as mail_read lists it.") },
|
|
164
185
|
}, async (args) => withSession(deps, async (session) => {
|
|
165
186
|
const id = String(args.attachment_id ?? "");
|
|
166
187
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/mail/attachments/${encodeURIComponent(id)}?meta=1`);
|
|
@@ -174,7 +195,7 @@ export function registerMailTools(server, deps) {
|
|
|
174
195
|
title: "Read a mailbox now",
|
|
175
196
|
description: "Runs one sync pass on one mailbox instead of waiting for the 15-minute cron, and answers with what landed or the "
|
|
176
197
|
+ "reason it did not (token_revoked, history_gap, auth_failed, quota…). Only the mailbox's OWNER may do this.",
|
|
177
|
-
inputSchema: { account_id: z.string().uuid() },
|
|
198
|
+
inputSchema: { account_id: z.string().uuid().describe("A mailbox id you own (mail_accounts).") },
|
|
178
199
|
}, async (args) => withSession(deps, async (session) => {
|
|
179
200
|
const id = String(args.account_id ?? "");
|
|
180
201
|
const response = await callAgentDoor(session, deps.fetchImpl, "POST", `/api/mail/accounts/${encodeURIComponent(id)}/sync`);
|
|
@@ -190,7 +211,7 @@ export function registerMailTools(server, deps) {
|
|
|
190
211
|
title: "Detach a mailbox",
|
|
191
212
|
description: "Removes a mailbox from Tower along with its stored mail and its credential. The mail itself is untouched at the "
|
|
192
213
|
+ "provider — this unhooks Tower, it does not delete anybody's email. Irreversible here: confirm must be true.",
|
|
193
|
-
inputSchema: { account_id: z.string().uuid(), confirm: CONFIRM_INPUT },
|
|
214
|
+
inputSchema: { account_id: z.string().uuid().describe("A mailbox id you own (mail_accounts)."), confirm: CONFIRM_INPUT },
|
|
194
215
|
}, async (args) => withSession(deps, async (session) => {
|
|
195
216
|
const refusal = unconfirmed(args, "Detaching removes this mailbox's stored mail from Tower.");
|
|
196
217
|
if (refusal)
|
|
@@ -16,6 +16,20 @@
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { callAgentDoor } from "./agent-door.js";
|
|
18
18
|
import { doorFailureText, errorResult, queryString, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
19
|
+
/** `{merge: 2, rewrite: 1}` as `merge 2, rewrite 1`; `none` when empty. */
|
|
20
|
+
function counts(value) {
|
|
21
|
+
if (!value || typeof value !== "object")
|
|
22
|
+
return "none";
|
|
23
|
+
const entries = Object.entries(value);
|
|
24
|
+
return entries.length === 0 ? "none" : entries.map(([key, count]) => `${key} ${String(count)}`).join(", ");
|
|
25
|
+
}
|
|
26
|
+
function runLine(run) {
|
|
27
|
+
const cost = typeof run.costUsd === "number" ? `$${run.costUsd.toFixed(4)}` : "$?";
|
|
28
|
+
return `${String(run.startedAt ?? "?")} ${String(run.containerTag ?? "?")} ${String(run.mode ?? "?")} `
|
|
29
|
+
+ `clusters ${String(run.clusters ?? 0)}; proposed ${counts(run.proposed)}; applied ${counts(run.applied)}; `
|
|
30
|
+
+ `refused ${counts(run.refused)}; ${cost}${run.model ? ` ${String(run.model)}` : ""}`
|
|
31
|
+
+ `${run.reason ? ` (${String(run.reason)})` : ""}`;
|
|
32
|
+
}
|
|
19
33
|
export function registerMemoryConsolidationTools(server, deps) {
|
|
20
34
|
registrarFor(server)("memory_consolidation", {
|
|
21
35
|
title: "Read the nightly memory consolidation runs",
|
|
@@ -44,10 +58,14 @@ export function registerMemoryConsolidationTools(server, deps) {
|
|
|
44
58
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/memory/consolidation${queryString(params)}`);
|
|
45
59
|
if (!response.ok)
|
|
46
60
|
return errorResult(doorFailureText("memory_consolidation", response));
|
|
47
|
-
const runs = Array.isArray(response.body.runs) ? response.body.runs : [];
|
|
61
|
+
const runs = (Array.isArray(response.body.runs) ? response.body.runs : []);
|
|
48
62
|
const reason = typeof response.body.reason === "string" ? response.body.reason : "runs_read";
|
|
63
|
+
// One line per run in the text half. It used to carry only the count,
|
|
64
|
+
// so a Claude-family client never saw a run (BLI-10432).
|
|
65
|
+
const lines = runs.map(runLine).join("\n");
|
|
49
66
|
return textResult(runs.length === 0
|
|
50
67
|
? `No consolidation runs are visible (${reason}). The pass is a nightly cron and its receipts are readable by a super_admin; this is the access rule answering, not an outage.`
|
|
51
|
-
: `${runs.length} consolidation run${runs.length === 1 ? "" : "s"}. report = proposals only, nothing on the shelf changed; apply = accepted edits written as supersessions, which never delete anything
|
|
68
|
+
: `${runs.length} consolidation run${runs.length === 1 ? "" : "s"}. report = proposals only, nothing on the shelf changed; apply = accepted edits written as supersessions, which never delete anything.`
|
|
69
|
+
+ `\n${lines}`, response.body);
|
|
52
70
|
}));
|
|
53
71
|
}
|
|
@@ -7,7 +7,11 @@ export function registerMemoryExperienceTools(server, deps) {
|
|
|
7
7
|
registrarFor(server)("memory_experience", {
|
|
8
8
|
title: "Log a memory-store experience",
|
|
9
9
|
description: "Append an experience receipt locally and post to Tower. Offline receipts ship on the next collector sync. Copy the recall marker: [bli] = BLI Memory, ◪ = Supermemory. Opinions only; never prompts, memory bodies or secrets.",
|
|
10
|
-
inputSchema: {
|
|
10
|
+
inputSchema: {
|
|
11
|
+
store: z.enum(["bli", "supermemory", "both"]).describe("Which store the recall came from: [bli] is BLI Memory, ◪ is Supermemory."),
|
|
12
|
+
verdict: z.enum(["win", "loss", "noise"]).describe("win: it shaped the answer. loss: it misled or was missing. noise: it came back and did not matter."),
|
|
13
|
+
reason: z.string().min(1).max(500).describe("Why, in one line. Never a prompt, a memory body or a secret."),
|
|
14
|
+
},
|
|
11
15
|
}, async (args) => {
|
|
12
16
|
const entry = await appendExperience({ store: args.store, verdict: args.verdict, reason: String(args.reason) }, { homeDir: deps.homeDir, agent: "MCP", project: "workspace" });
|
|
13
17
|
const delivery = await shipExperience(entry, async (body) => {
|
package/dist/models-tools.js
CHANGED
|
@@ -28,7 +28,7 @@ export function registerModelsTools(server, deps) {
|
|
|
28
28
|
description: "Every model Tower knows about: the provider's documented context window, price per million, reasoning "
|
|
29
29
|
+ "modes and tool support, beside our measured latency, observed tokens/s and last probe verdict. "
|
|
30
30
|
+ "`STALE:<field>` means a measurement contradicted the documentation, with the command that re-reads the "
|
|
31
|
-
+ "provider. Read-only.",
|
|
31
|
+
+ "provider. One line per model; models_card reads one model in full. Read-only.",
|
|
32
32
|
inputSchema: {},
|
|
33
33
|
}, async () => withSession(deps, async (session) => {
|
|
34
34
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/models/cards");
|
|
@@ -39,10 +39,23 @@ export function registerModelsTools(server, deps) {
|
|
|
39
39
|
const lines = (Array.isArray(body.lines) ? body.lines : []);
|
|
40
40
|
const stale = cards.filter((card) => (card.stale?.length ?? 0) > 0).map((card) => card.id ?? "?");
|
|
41
41
|
const unreadable = (Array.isArray(body.unreadable) ? body.unreadable : []);
|
|
42
|
+
// The structured half is the shelf's INDEX, the same facts the lines
|
|
43
|
+
// print. Whole cards (documented, measured and bench blocks) were 39 KB
|
|
44
|
+
// for 14 models on 2026-09-26, and models_card reads one in full
|
|
45
|
+
// (BLI-10432).
|
|
46
|
+
const index = cards.map((card) => ({
|
|
47
|
+
id: card.id ?? null,
|
|
48
|
+
display_name: card.display_name ?? null,
|
|
49
|
+
provider: card.provider ?? null,
|
|
50
|
+
model: card.model ?? null,
|
|
51
|
+
status: card.status ?? null,
|
|
52
|
+
slots: card.slots ?? null,
|
|
53
|
+
stale: (card.stale ?? []).map((entry) => entry.field ?? "?"),
|
|
54
|
+
}));
|
|
42
55
|
return textResult(`${cards.length} model card(s)`
|
|
43
56
|
+ `${stale.length > 0 ? `, ${stale.length} stale: ${stale.join(", ")}` : ", none stale"}`
|
|
44
57
|
+ `${unreadable.length > 0 ? `, ${unreadable.length} unreadable: ${unreadable.join(", ")}` : ""}`
|
|
45
|
-
+ `${lines.length > 0 ? `\n${lines.join("\n")}` : ""}`, { cards, stale, unreadable });
|
|
58
|
+
+ `${lines.length > 0 ? `\n${lines.join("\n")}` : ""}`, { cards: index, lines, stale, unreadable });
|
|
46
59
|
}));
|
|
47
60
|
register("models_card", {
|
|
48
61
|
title: "Read one model card",
|
|
@@ -4,8 +4,11 @@ import { registrarFor, withSession, textResult, errorResult, doorFailureText } f
|
|
|
4
4
|
export function registerNoteFolderTools(server, deps) {
|
|
5
5
|
const register = registrarFor(server);
|
|
6
6
|
register("notes_folders", {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// `tree` was declared here and never read: the door answers one shape,
|
|
8
|
+
// flat folders with their paths, and a knob that changes nothing is a
|
|
9
|
+
// description that lies (BLI-10432).
|
|
10
|
+
title: "List note folders", description: "Every note folder you may see, as a flat list of paths with the note count in each, plus how many notes sit at the root.",
|
|
11
|
+
inputSchema: {},
|
|
9
12
|
}, async () => withSession(deps, async (session) => {
|
|
10
13
|
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
11
14
|
if (!response.ok)
|
|
@@ -14,7 +17,7 @@ export function registerNoteFolderTools(server, deps) {
|
|
|
14
17
|
}));
|
|
15
18
|
register("notes_mkdir", {
|
|
16
19
|
title: "Create a note folder", description: "Create a path, including missing ancestors. Existing paths are reused.",
|
|
17
|
-
inputSchema: { path: z.string().min(1).max(2000) },
|
|
20
|
+
inputSchema: { path: z.string().min(1).max(2000).describe("A slash-separated folder path, e.g. Clients/Blue Pearl.") },
|
|
18
21
|
}, async (args) => withSession(deps, async (session) => {
|
|
19
22
|
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/folders", { path: args.path });
|
|
20
23
|
if (!response.ok)
|
|
@@ -23,7 +26,7 @@ export function registerNoteFolderTools(server, deps) {
|
|
|
23
26
|
}));
|
|
24
27
|
register("notes_rmdir", {
|
|
25
28
|
title: "Delete an empty note folder", description: "Delete a folder by path. Refuses any folder with notes or children.",
|
|
26
|
-
inputSchema: { path: z.string().min(1).max(2000) },
|
|
29
|
+
inputSchema: { path: z.string().min(1).max(2000).describe("The folder's slash-separated path, as notes_folders lists it.") },
|
|
27
30
|
}, async (args) => withSession(deps, async (session) => {
|
|
28
31
|
const listed = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
29
32
|
if (!listed.ok)
|
|
@@ -40,7 +43,10 @@ export function registerNoteFolderTools(server, deps) {
|
|
|
40
43
|
}));
|
|
41
44
|
register("notes_rename", {
|
|
42
45
|
title: "Rename a note folder", description: "Rename a folder by path, updating every descendant path.",
|
|
43
|
-
inputSchema: {
|
|
46
|
+
inputSchema: {
|
|
47
|
+
path: z.string().min(1).max(2000).describe("The folder's slash-separated path, as notes_folders lists it."),
|
|
48
|
+
name: z.string().min(1).max(120).describe("The folder's new last segment. Its descendants move with it."),
|
|
49
|
+
},
|
|
44
50
|
}, async (args) => withSession(deps, async (session) => {
|
|
45
51
|
const listed = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
46
52
|
if (!listed.ok)
|
|
@@ -50,7 +50,7 @@ export function registerNotesAccountTools(server, deps) {
|
|
|
50
50
|
description: "Reads the meetings this notetaker has recorded since the last pass and files them as meeting notes. "
|
|
51
51
|
+ "Answers with the run's own counts and reason label, which is exactly what the ledger row says. "
|
|
52
52
|
+ "The half-hourly cron does this unasked; this is the button for when you do not want to wait.",
|
|
53
|
-
inputSchema: { account_id: z.string().uuid() },
|
|
53
|
+
inputSchema: { account_id: z.string().uuid().describe("A notetaker account id (notes_accounts).") },
|
|
54
54
|
}, async (args) => withSession(deps, async (session) => {
|
|
55
55
|
const response = await callAgentDoor(session, deps.fetchImpl, "POST", `/api/notes/accounts/${encodeURIComponent(String(args.account_id))}/sync`);
|
|
56
56
|
if (!response.ok)
|
|
@@ -64,7 +64,7 @@ export function registerNotesAccountTools(server, deps) {
|
|
|
64
64
|
title: "Detach a notetaker",
|
|
65
65
|
description: "Removes a notetaker account and its stored key from Tower. The account itself is untouched at the provider, "
|
|
66
66
|
+ "and the meetings it already filed stay where they are. Requires confirm: true.",
|
|
67
|
-
inputSchema: { account_id: z.string().uuid(), confirm: CONFIRM_INPUT },
|
|
67
|
+
inputSchema: { account_id: z.string().uuid().describe("A notetaker account id (notes_accounts)."), confirm: CONFIRM_INPUT },
|
|
68
68
|
}, async (args) => withSession(deps, async (session) => {
|
|
69
69
|
const refusal = unconfirmed(args, "This would detach that notetaker and forget its stored key (the account itself is untouched at the provider, and the meetings it filed stay).");
|
|
70
70
|
if (refusal)
|
package/dist/notes-tools.js
CHANGED
|
@@ -18,7 +18,7 @@ import { registerNoteFolderTools } from "./note-folder-tools.js";
|
|
|
18
18
|
*/
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import { callAgentDoor } from "./agent-door.js";
|
|
21
|
-
import { doorFailureText, errorResult, registrarFor, textResult, queryString, withSession, } from "./tool-result.js";
|
|
21
|
+
import { DEFAULT_LIST_LIMIT, doorFailureText, errorResult, registrarFor, textResult, queryString, withSession, } from "./tool-result.js";
|
|
22
22
|
/**
|
|
23
23
|
* The door's own words for "this answer is narrower than the browser's", or
|
|
24
24
|
* "" when it was not narrowed. Never rephrased here.
|
|
@@ -48,7 +48,7 @@ const LIBRARY_FILTERS = {
|
|
|
48
48
|
kind: z.string().min(1).max(100).optional().describe("Only this meeting kind."),
|
|
49
49
|
since: z.string().min(1).max(20).optional().describe("YYYY-MM-DD — meetings on or after this date."),
|
|
50
50
|
until: z.string().min(1).max(20).optional().describe("YYYY-MM-DD — meetings on or before this date."),
|
|
51
|
-
limit: z.number().int().min(1).max(500).optional(),
|
|
51
|
+
limit: z.number().int().min(1).max(500).optional().describe("At most this many notes."),
|
|
52
52
|
};
|
|
53
53
|
export function registerNotesTools(server, deps) {
|
|
54
54
|
registerNoteFolderTools(server, deps);
|
|
@@ -56,10 +56,17 @@ export function registerNotesTools(server, deps) {
|
|
|
56
56
|
register("notes_list", {
|
|
57
57
|
title: "List Tower meeting notes",
|
|
58
58
|
description: "The meeting-notes library you may read, grouped by shelf: id, date, kind, participants and file name. "
|
|
59
|
+
+ `Newest ${DEFAULT_LIST_LIMIT} unless you pass limit (up to 500); the answer says when more exist. `
|
|
59
60
|
+ "Never a note's text — call notes_show for one. Says when the answer is narrower than a browser's.",
|
|
60
61
|
inputSchema: LIBRARY_FILTERS,
|
|
61
62
|
}, async (args) => withSession(deps, async (session) => {
|
|
62
|
-
|
|
63
|
+
// A page by default: the whole library was 136 notes and 58 KB on
|
|
64
|
+
// 2026-09-26, mostly participant lists (BLI-10432). The door reports
|
|
65
|
+
// `more` itself, so nothing here guesses.
|
|
66
|
+
const params = libraryQuery(args);
|
|
67
|
+
if (args.limit === undefined)
|
|
68
|
+
params.set("limit", String(DEFAULT_LIST_LIMIT));
|
|
69
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/notes/library${queryString(params)}`);
|
|
63
70
|
if (!response.ok)
|
|
64
71
|
return errorResult(doorFailureText("notes_list", response));
|
|
65
72
|
const body = response.body;
|
|
@@ -100,7 +107,7 @@ export function registerNotesTools(server, deps) {
|
|
|
100
107
|
title: "Your own Tower notes shelf",
|
|
101
108
|
description: "The notes THIS machine's owner put in, with the per-note counts (statements, open to the team, kept back) "
|
|
102
109
|
+ "when the server knows them. Says so when it does not, rather than printing zeros.",
|
|
103
|
-
inputSchema: { limit: z.number().int().min(1).max(500).optional() },
|
|
110
|
+
inputSchema: { limit: z.number().int().min(1).max(500).optional().describe("At most this many notes.") },
|
|
104
111
|
}, async (args) => withSession(deps, async (session) => {
|
|
105
112
|
const params = new URLSearchParams();
|
|
106
113
|
if (typeof args.limit === "number")
|