@bli-cockpit/mcp 0.1.109 → 0.1.111

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 CHANGED
@@ -109,7 +109,7 @@ about what an agent can reach.
109
109
 
110
110
  <!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
111
111
 
112
- **138 of 143 Tower verbs have an MCP twin.**
112
+ **147 of 152 Tower verbs have an MCP twin.**
113
113
  Each tool goes through the SAME door its CLI verb calls, with the same
114
114
  collector device token — never a second route and never a service-role
115
115
  reader. `src/verb-census.test.ts` fails when a verb is in none of the
@@ -117,6 +117,7 @@ three tables below.
117
117
 
118
118
  | CLI verb | MCP tool | Door |
119
119
  | --- | --- | --- |
120
+ | `cockpit brain retry` | `brain_retry` | `POST /api/brain/incidents/retry` |
120
121
  | `cockpit brain status` | `brain_status` | `GET /api/brain/incidents` |
121
122
  | `cockpit brief done` | `brief_done` | `POST /api/jarvis/ticks` |
122
123
  | `cockpit brief history` | `brief_history` | `GET /api/jarvis/brief?history=1` |
@@ -125,6 +126,8 @@ three tables below.
125
126
  | `cockpit brief status` | `brief_status` | `GET /api/ops/brief-status` |
126
127
  | `cockpit brief undone` | `brief_undone` | `POST /api/jarvis/ticks` |
127
128
  | `cockpit cal calendars` | `cal_calendars` | `GET /api/cal/calendars` |
129
+ | `cockpit cal cancel` | `cal_delete` | `DELETE /api/cal/events/[eventId]` |
130
+ | `cockpit cal change` | `cal_change` | `PATCH /api/cal/events/[eventId]` |
128
131
  | `cockpit cal create` | `cal_create` | `POST /api/cal/events` |
129
132
  | `cockpit cal delete` | `cal_delete` | `DELETE /api/cal/events/[eventId]` |
130
133
  | `cockpit cal detach` | `cal_detach` | `DELETE /api/cal/calendars` |
@@ -176,10 +179,15 @@ three tables below.
176
179
  | `cockpit logs` | `logs_digest` | `LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs); `project` reads GET /api/logs (the stored digests)` |
177
180
  | `cockpit mail accounts` | `mail_accounts` | `GET /api/mail/accounts` |
178
181
  | `cockpit mail attachment` | `mail_attachment` | `GET /api/mail/attachments/[id]?meta=1` |
182
+ | `cockpit mail default` | `mail_default` | `PATCH /api/mail/accounts` |
179
183
  | `cockpit mail detach` | `mail_detach` | `DELETE /api/mail/accounts` |
184
+ | `cockpit mail disable` | `mail_disable` | `PATCH /api/mail/accounts` |
180
185
  | `cockpit mail draft` | `mail_draft` | `POST /api/mail/drafts` |
186
+ | `cockpit mail enable` | `mail_enable` | `PATCH /api/mail/accounts` |
181
187
  | `cockpit mail inbox` | `mail_inbox` | `GET /api/mail/inbox` |
182
188
  | `cockpit mail read` | `mail_read` | `GET /api/mail/threads/[id]` |
189
+ | `cockpit mail remove` | `mail_remove` | `DELETE /api/mail/accounts` |
190
+ | `cockpit mail rename` | `mail_rename` | `PATCH /api/mail/accounts` |
183
191
  | `cockpit mail search` | `mail_search` | `GET /api/mail/search` |
184
192
  | `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
185
193
  | `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
@@ -207,6 +215,7 @@ three tables below.
207
215
  | `cockpit msg send` | `msg_send` | `POST /api/msg/channels/[id]/messages` |
208
216
  | `cockpit msg thread` | `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` |
209
217
  | `cockpit notes accounts` | `notes_accounts` | `GET /api/notes/accounts` |
218
+ | `cockpit notes bot-upcoming` | `notes_bot_upcoming` | `GET /api/notetaker/upcoming` |
210
219
  | `cockpit notes detach` | `notes_detach` | `DELETE /api/notes/accounts` |
211
220
  | `cockpit notes folders` | `notes_folders` | `GET /api/notes/folders` |
212
221
  | `cockpit notes granola-status` | `notes_granola_status` | `GET /api/notes/granola-status` |
@@ -0,0 +1,2 @@
1
+ import { type RegisterTool, type ToolDeps } from "./tool-result.js";
2
+ export declare function registerAccountTools(register: RegisterTool, deps: ToolDeps): void;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { callAgentDoor } from "./agent-door.js";
3
+ import { withSession, textResult, errorResult, doorFailureText } from "./tool-result.js";
4
+ export function registerAccountTools(register, deps) {
5
+ for (const action of ["rename", "enable", "disable", "default", "remove"]) {
6
+ register(`mail_${action}`, { title: `${action} a connected account`, description: "Manage the caller's shared mail/calendar account. Exact case-folded label, address or id. Removing disconnects the account and deletes Tower's stored copies; provider data is untouched.",
7
+ inputSchema: {
8
+ account: z.string().min(1).max(320).describe("The connected account: its exact label, address or id (mail_accounts lists them)."),
9
+ ...(action === "rename" ? { name: z.string().trim().min(1).max(100).describe("The new label, e.g. personal 1.") } : {}),
10
+ ...(["enable", "disable"].includes(action) ? { source: z.enum(["mail", "calendar"]).describe("Which source to switch: mail or calendar.") } : {}),
11
+ },
12
+ }, async (args) => withSession(deps, async (session) => {
13
+ const response = await callAgentDoor(session, deps.fetchImpl, action === "remove" ? "DELETE" : "PATCH", action === "remove" ? `/api/mail/accounts?account=${encodeURIComponent(String(args.account))}` : "/api/mail/accounts", action === "remove" ? undefined : { account: args.account, ...(action === "rename" ? { account_label: args.name } : action === "default" ? { is_default: true } : { [`${args.source}_enabled`]: action === "enable" }) });
14
+ if (!response.ok)
15
+ return errorResult(doorFailureText(`mail_${action}`, response));
16
+ return textResult(`Account ${args.account}: ${action} completed.`, response.body);
17
+ }));
18
+ }
19
+ }
@@ -36,4 +36,21 @@ export function registerBrainTools(server, deps) {
36
36
  const text = typeof response.body.text === "string" ? response.body.text : "";
37
37
  return dataResult(text, response.body);
38
38
  }));
39
+ registrarFor(server)("brain_retry", {
40
+ title: "Send incidents back to Tower's brain",
41
+ description: "Retry needs_human or not_actionable incidents through the same door as cockpit brain retry. Only members and super admins. Resets attempts and claims, records who/why/when, and comments once on each incident issue. Other states are skipped.",
42
+ inputSchema: {
43
+ ids: z.array(z.string()).max(100).optional().describe("Incident UUIDs or BLI issue identifiers."),
44
+ all_needs_human: z.boolean().optional().describe("Retry all needs_human incidents, optionally in one project."),
45
+ project: z.string().optional().describe("Narrow every selection to this registry project id."),
46
+ reason: z.string().trim().min(1).max(4000).describe("Why these incidents should be tried again."),
47
+ },
48
+ }, async (args) => withSession(deps, async (session) => {
49
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/brain/incidents/retry", args);
50
+ if (!response.ok)
51
+ return errorResult(doorFailureText("brain_retry", response));
52
+ if (response.body.ok === false)
53
+ return { ...dataResult(String(response.body.text ?? "Retry partially failed."), response.body), isError: true };
54
+ return dataResult(String(response.body.text ?? ""), response.body);
55
+ }));
39
56
  }
@@ -9,6 +9,22 @@ import { z } from "zod";
9
9
  import { callAgentDoor } from "./agent-door.js";
10
10
  import { CONFIRM_INPUT, doorFailureText, errorResult, textResult, unconfirmed, withSession, } from "./tool-result.js";
11
11
  export function registerCalDeleteTool(register, deps) {
12
+ register("cal_change", { title: "Change an event", description: "Change a caller-owned event, checking the named account before writing to Google.", inputSchema: {
13
+ event_id: z.string().uuid().describe("Tower's event id (cal_today/cal_week/cal_next/cal_find return it as event_id)."),
14
+ account: z.string().min(1).max(100).optional().describe("Exact connected account label, address or id (mail_accounts lists them). Omit to use the account the event is on."),
15
+ title: z.string().min(1).max(500).optional().describe("The new title."),
16
+ starts_at: z.string().optional().describe("The new start, ISO 8601 with offset. Send with ends_at."),
17
+ ends_at: z.string().optional().describe("The new end, ISO 8601 with offset. Send with starts_at."),
18
+ location: z.string().max(1000).optional().describe("The new location."),
19
+ confirm: CONFIRM_INPUT,
20
+ } }, async (args) => withSession(deps, async (session) => {
21
+ const refusal = unconfirmed(args, "This changes the event at Google and may notify guests.");
22
+ if (refusal)
23
+ return refusal;
24
+ const { event_id, confirm: _confirm, ...changes } = args;
25
+ const response = await callAgentDoor(session, deps.fetchImpl, "PATCH", `/api/cal/events/${encodeURIComponent(String(event_id))}`, changes);
26
+ return response.ok ? textResult(`Event changed. Account: ${response.body.event?.account_name ?? "the event’s account"}.`, response.body) : errorResult(doorFailureText("cal_change", response));
27
+ }));
12
28
  register("cal_delete", {
13
29
  title: "Delete an event",
14
30
  description: "Removes ONE event, AT GOOGLE first and then here — the same order every calendar write in this server "
@@ -16,6 +32,7 @@ export function registerCalDeleteTool(register, deps) {
16
32
  + "Attendees are notified (sendUpdates=all) only when the event has any. Google already having it gone "
17
33
  + "(404/410) is answered as success, not a refusal. Requires confirm: true.",
18
34
  inputSchema: {
35
+ account: z.string().min(1).max(100).optional().describe("Exact connected account label, address or id (mail_accounts lists them). Omit to use the account the event is on."),
19
36
  event_id: z.string().uuid().describe("Tower's event id (cal_today/cal_week/cal_next/cal_find return it as event_id)."),
20
37
  confirm: CONFIRM_INPUT,
21
38
  },
@@ -23,18 +40,19 @@ export function registerCalDeleteTool(register, deps) {
23
40
  const refusal = unconfirmed(args, "This would remove that event at Google (emailing any attendees) and then here.");
24
41
  if (refusal)
25
42
  return refusal;
26
- const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/cal/events/${encodeURIComponent(String(args.event_id))}`);
43
+ const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/cal/events/${encodeURIComponent(String(args.event_id))}${args.account ? `?account=${encodeURIComponent(String(args.account))}` : ""}`);
27
44
  if (!response.ok)
28
45
  return errorResult(doorFailureText("cal_delete", response));
29
46
  const deleted = response.body.deleted;
30
47
  if (!deleted)
31
48
  return textResult("Tower answered without a result. Nothing to report.", response.body);
32
49
  const alreadyGone = deleted.google === "already_gone_at_google";
50
+ const accountReceipt = deleted.account_name ? ` Account: ${deleted.account_name}.` : "";
33
51
  const stored = deleted.local_store === "removed"
34
52
  ? "Tower's copy is removed."
35
53
  : "Tower's copy is still there for now — the next sync will pick up that it is gone.";
36
54
  return textResult(alreadyGone
37
- ? `Google already had that event gone. ${stored}`
38
- : `Deleted at Google${deleted.notified === "all" ? " — attendees were notified." : "."} ${stored}`, { deleted });
55
+ ? `Google already had that event gone. ${stored}${accountReceipt}`
56
+ : `Deleted at Google${deleted.notified === "all" ? " — attendees were notified." : "."} ${stored}${accountReceipt}`, { deleted });
39
57
  }));
40
58
  }
package/dist/cal-tools.js CHANGED
@@ -169,7 +169,7 @@ export function registerCalTools(server, deps) {
169
169
  ? "No calendars are attached. A person attaches one with `cockpit cal add-ical` (personal) or by connecting "
170
170
  + "Google in the browser (work)."
171
171
  : `${calendars.length} calendar(s).\n${calendars
172
- .map((calendar) => `${calendar.name} ${calendar.provider} ${calendar.org_visible ? "shared" : "private"} ${calendar.status}${calendar.status_reason ? ` (${calendar.status_reason})` : ""} ${calendar.id}`)
172
+ .map((calendar) => `${calendar.account_label ?? calendar.provider} ${calendar.name} ${calendar.provider} ${calendar.org_visible ? "shared" : "private"} ${calendar.status}${calendar.status_reason ? ` (${calendar.status_reason})` : ""} ${calendar.id}`)
173
173
  .join("\n")}`, { calendars });
174
174
  }));
175
175
  register("cal_create", {
@@ -179,7 +179,8 @@ export function registerCalTools(server, deps) {
179
179
  + "says so by name. Times are ISO instants, or plain dates with all_day. Requires confirm: true, because "
180
180
  + "attendees are emailed by Google the moment this succeeds.",
181
181
  inputSchema: {
182
- calendar_id: z.string().uuid().describe("A google_oauth calendar id you own (cal_calendars)."),
182
+ calendar_id: z.string().uuid().optional().describe("A google_oauth calendar id you own (cal_calendars)."),
183
+ account: z.string().min(1).max(100).optional().describe("Exact connected account label; omitted uses default."),
183
184
  title: z.string().min(1).max(500).describe("The event title."),
184
185
  starts_at: z.string().min(4).max(40).describe("An ISO instant, or a YYYY-MM-DD date with all_day."),
185
186
  ends_at: z.string().min(4).max(40).describe("An ISO instant, or a YYYY-MM-DD date with all_day."),
@@ -196,6 +197,7 @@ export function registerCalTools(server, deps) {
196
197
  return refusal;
197
198
  const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/cal/events", {
198
199
  calendar_id: args.calendar_id,
200
+ ...(args.account ? { account: args.account } : {}),
199
201
  title: args.title,
200
202
  starts_at: args.starts_at,
201
203
  ends_at: args.ends_at,
@@ -209,6 +211,7 @@ export function registerCalTools(server, deps) {
209
211
  return errorResult(doorFailureText("cal_create", response));
210
212
  const event = response.body.event;
211
213
  return textResult(`Created "${event?.title ?? String(args.title)}" at ${event?.starts_at ?? String(args.starts_at)}.`
214
+ + (event?.account_name ? `\nAccount: ${event.account_name}.` : "")
212
215
  + towerIdLine(event)
213
216
  + (event?.html_link ? `\n${event.html_link}` : "")
214
217
  + (args.meet === true
@@ -39,7 +39,8 @@ export function registerDocsMsgTools(server, deps) {
39
39
  const more = args.limit === undefined && all.length > DEFAULT_LIST_LIMIT;
40
40
  const rows = more ? all.slice(0, DEFAULT_LIST_LIMIT) : all;
41
41
  const lines = rows.map(docListLine).join("\n");
42
- return textResult(`${rows.length} document(s).${moreNote(more, DOCS_LIST_MAX)}${lines ? `\n${lines}` : ""}`, { documents: rows.map(docListRow), more });
42
+ const counts = response.body.counts;
43
+ return textResult(`${rows.length} document(s).${counts ? ` ${counts.total} match in all. ${counts.coverage}` : ""}${moreNote(more, DOCS_LIST_MAX)}${lines ? `\n${lines}` : ""}`, { documents: rows.map(docListRow), more, counts });
43
44
  }));
44
45
  register("docs_tree", {
45
46
  title: "Read the Tower document tree",
@@ -85,8 +85,10 @@ export function registerDocsNotionTools(server, deps) {
85
85
  const runLine = ["push", "pull"]
86
86
  .map((direction) => `${direction} ${runSentence(lastRun?.[direction] ?? null)}`)
87
87
  .join(" · ");
88
- return textResult(`${counts?.linked ?? 0} linked: ${summary}. Last run — ${runLine}.`, {
88
+ const coverage = response.body.coverage ? `${response.body.coverage} ` : "";
89
+ return textResult(`${counts?.linked ?? 0} linked: ${summary}. ${coverage}Last run — ${runLine}.`, {
89
90
  counts,
91
+ ...response.body,
90
92
  documents: response.body.documents ?? [],
91
93
  last_run: lastRun ?? { push: null, pull: null },
92
94
  });
@@ -28,7 +28,8 @@ export const ATTACHMENTS_INPUT = z
28
28
  /** The JSON both tools post. `attachments` is present only when there are files. */
29
29
  export function composedBody(args, attachments = []) {
30
30
  return {
31
- account_id: args.account_id,
31
+ ...(args.account_id ? { account_id: args.account_id } : {}),
32
+ ...(args.account ? { account: args.account } : {}),
32
33
  to: args.to.map((address) => ({ address })),
33
34
  ...(Array.isArray(args.cc) && args.cc.length > 0
34
35
  ? { cc: args.cc.map((address) => ({ address })) }
@@ -34,6 +34,7 @@
34
34
  * call, so a machine with no `cockpit login` pairing still serves this
35
35
  * server's other tools and only a `mail_` call fails, by name.
36
36
  */
37
+ import { registerAccountTools } from "./account-tools.js";
37
38
  import { z } from "zod";
38
39
  import { callAgentDoor } from "./agent-door.js";
39
40
  import { ATTACHMENTS_INPUT, attachmentResult, prepareMessage, unconfirmedAttachments } from "./mail-compose.js";
@@ -45,10 +46,11 @@ function messageLine(message) {
45
46
  const ADDRESS = EMAIL_INPUT.describe("An email address.");
46
47
  export function registerMailTools(server, deps) {
47
48
  const register = registrarFor(server);
49
+ registerAccountTools(register, deps);
48
50
  register("mail_accounts", {
49
51
  title: "List attached mailboxes",
50
52
  description: "Every mailbox attached to Tower that you may see — address, provider (gmail_oauth or imap), health and last sync. "
51
- + "One person may have several; there is no default mailbox anywhere in this surface, so start here when you need an account id.",
53
+ + "One person may have several, each with a label (e.g. personal 1) and per-source switches; one may be marked default. Start here when you need an account name or id.",
52
54
  inputSchema: {},
53
55
  }, async () => withSession(deps, async (session) => {
54
56
  const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/mail/accounts");
@@ -56,7 +58,7 @@ export function registerMailTools(server, deps) {
56
58
  return errorResult(doorFailureText("mail_accounts", response));
57
59
  const accounts = (Array.isArray(response.body.accounts) ? response.body.accounts : []);
58
60
  const lines = accounts
59
- .map((account) => `${account.address} ${account.provider} ${account.status}${account.status_reason ? ` (${account.status_reason})` : ""} ${account.id}`)
61
+ .map((account) => `${account.account_label ?? account.address} ${account.is_default ? "default" : ""} mail=${account.mail_enabled !== false} calendar=${account.calendar_enabled !== false} ${account.address} ${account.provider} ${account.status}${account.status_reason ? ` (${account.status_reason})` : ""} ${account.id}`)
60
62
  .join("\n");
61
63
  return textResult(accounts.length === 0
62
64
  ? "No mailboxes are attached. A person attaches one with `cockpit mail add-imap` (personal) or the browser (work)."
@@ -187,7 +189,8 @@ export function registerMailTools(server, deps) {
187
189
  + "see that argument for what it refuses. "
188
190
  + "This actually sends mail to real people: confirm must be true.",
189
191
  inputSchema: {
190
- account_id: z.string().uuid().describe("The mailbox it goes out from (mail_accounts). Never guessed."),
192
+ account_id: z.string().uuid().optional().describe("The mailbox id, or use account name/default."),
193
+ account: z.string().min(1).max(100).optional().describe("Exact connected account label."),
191
194
  to: z.array(ADDRESS).min(1).max(50).describe("Recipient email addresses."),
192
195
  cc: z.array(ADDRESS).max(50).optional().describe("Copied email addresses."),
193
196
  subject: z.string().max(998).describe("The subject line."),
@@ -213,14 +216,14 @@ export function registerMailTools(server, deps) {
213
216
  if (notConfirmed)
214
217
  return errorResult(notConfirmed);
215
218
  const files = attachmentResult(message.summary);
216
- return textResult(`Sent from ${sent?.address ?? "that mailbox"} to ${args.to.join(", ")}${sent?.threaded ? " (as a reply)" : ""}.${files.sentence}`, { sent, ...files.structured });
219
+ return textResult(`Sent from ${sent?.account_name ?? sent?.address ?? "that mailbox"} to ${args.to.join(", ")}${sent?.threaded ? " (as a reply)" : ""}.${files.sentence}`, { sent, ...files.structured });
217
220
  }));
218
221
  register("mail_draft", {
219
222
  title: "Save a draft in one mailbox",
220
223
  description: "Saves a message as a DRAFT in one of YOUR mailboxes, for a person to read, edit and send themselves. Use it "
221
224
  + "when you are preparing mail on somebody's behalf and they should be the one to press send. NOTHING IS SENT and "
222
- + "nobody is notified, so unlike mail_send there is no confirm flag. `account_id` is REQUIRED and is the mailbox the "
223
- + "draft is saved in (mail_accounts) — with several attached, that is never something to guess. "
225
+ + "nobody is notified, so unlike mail_send there is no confirm flag. `account` is an exact label; `account_id` selects a mailbox id. Omit both for your default. The "
226
+ + "draft is saved in that mailbox (mail_accounts). With several accounts and no default, Tower asks which one. "
224
227
  + "`in_reply_to_message_id` (a message id from mail_read) threads the draft into that conversation the way mail_send "
225
228
  + "threads a reply; Gmail files it there only when the subject matches the original's, and the answer says whether it "
226
229
  + "did (`threaded`). `attachments` ([{ path }]) puts files from this machine in the draft, up to 10 and 3,000,000 bytes "
@@ -228,7 +231,8 @@ export function registerMailTools(server, deps) {
228
231
  + "mailboxes can take one: an IMAP mailbox is refused as imap_drafts_unsupported. Answers the provider draft id, "
229
232
  + "the thread id and the account id.",
230
233
  inputSchema: {
231
- account_id: z.string().uuid().describe("The mailbox the draft is saved in (mail_accounts). Never guessed."),
234
+ account_id: z.string().uuid().optional().describe("The mailbox id, or use account name/default."),
235
+ account: z.string().min(1).max(100).optional().describe("Exact account label, e.g. work 1."),
232
236
  to: z.array(ADDRESS).min(1).max(50).describe("Recipient email addresses, so the draft is ready to send as it is."),
233
237
  cc: z.array(ADDRESS).max(50).optional().describe("Copied email addresses."),
234
238
  subject: z.string().max(998).describe("The subject line. For a reply, keep the original's subject or Gmail will not thread it."),
@@ -252,7 +256,7 @@ export function registerMailTools(server, deps) {
252
256
  const files = attachmentResult(message.summary);
253
257
  // Both halves carry the ids (BLI-10432): a client that hands the model only
254
258
  // `content`, or only `structuredContent`, must still learn where the draft is.
255
- return textResult(`Saved a draft in ${draft?.address ?? "that mailbox"} for ${args.to.join(", ")}`
259
+ return textResult(`Saved a draft in ${draft?.account_name ?? draft?.address ?? "that mailbox"} for ${args.to.join(", ")}`
256
260
  + `${draft?.threaded ? " (in the reply's thread)" : args.in_reply_to_message_id ? " (Gmail did NOT file it in that thread: its subject has to match the original's)" : ""}. `
257
261
  + "Nothing was sent: it waits in Drafts for a person to read, edit and send."
258
262
  + `${files.sentence} `
@@ -0,0 +1,4 @@
1
+ import { type ToolDeps } from "./tool-result.js";
2
+ export declare function registerNotesBotUpcomingTool(server: {
3
+ registerTool: (...args: never[]) => unknown;
4
+ }, deps: ToolDeps): void;
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { callAgentDoor } from "./agent-door.js";
3
+ import { registrarFor, withSession, textResult, errorResult, doorFailureText } from "./tool-result.js";
4
+ export function registerNotesBotUpcomingTool(server, deps) {
5
+ registrarFor(server)("notes_bot_upcoming", {
6
+ title: "Bot meetings today", description: "Your own Google Meet meetings today the Tower bot will join, with claim state. Decline the event or mark it private to opt out.",
7
+ inputSchema: { tz: z.string().optional().describe("Your IANA time zone; defaults to UTC.") },
8
+ }, async (args) => withSession(deps, async (session) => {
9
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/notetaker/upcoming?tz=${encodeURIComponent(String(args.tz ?? "UTC"))}`);
10
+ if (!response.ok)
11
+ return errorResult(doorFailureText("notes_bot_upcoming", response));
12
+ return textResult(JSON.stringify(response.body), response.body);
13
+ }));
14
+ }
package/dist/server.js CHANGED
@@ -17,6 +17,7 @@ import { registerCalTools } from "./cal-tools.js";
17
17
  import { registerMailTools } from "./mail-tools.js";
18
18
  import { registerNotesAccountTools } from "./notes-account-tools.js";
19
19
  import { registerNotesTools } from "./notes-tools.js";
20
+ import { registerNotesBotUpcomingTool } from "./notes-bot-upcoming-tool.js";
20
21
  import { registerNotesGranolaStatusTool } from "./notes-granola-status-tool.js";
21
22
  import { registerNotesWriteTools } from "./notes-write-tools.js";
22
23
  import { registerMemoryConsolidationTools } from "./memory-consolidation-tools.js";
@@ -476,6 +477,7 @@ export function createServer(deps, options = {}) {
476
477
  registerNotesTools(server, { fetchImpl: deps.fetchImpl });
477
478
  // Ticket 10718: the Granola cron's own health, read-only, over the shared
478
479
  // server-side key rather than a `notes_accounts` row.
480
+ registerNotesBotUpcomingTool(server, { fetchImpl: deps.fetchImpl });
479
481
  registerNotesGranolaStatusTool(server, { fetchImpl: deps.fetchImpl });
480
482
  registerOpsTools(server, { fetchImpl: deps.fetchImpl });
481
483
  registerMemoryExperienceTools(server, { fetchImpl: deps.fetchImpl });
@@ -15,6 +15,6 @@ export function registerSetupTools(server, deps) {
15
15
  if (args.compare)
16
16
  query.set("compare", String(args.compare));
17
17
  const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/setup?${query}`);
18
- return response.ok ? dataResult("Agent setups and outcomes over the requested window.", response.body) : errorResult(doorFailureText("setup_read", response));
18
+ return response.ok ? dataResult(`Agent setups and outcomes over the requested window. ${response.body.read_coverage?.join(" ") ?? ""}`, response.body) : errorResult(doorFailureText("setup_read", response));
19
19
  }));
20
20
  }
@@ -10,7 +10,7 @@
10
10
  * from `src/` under Node's type stripping, so it may import nothing.
11
11
  */
12
12
  /** Tools that change nothing. The smoke calls every one. */
13
- export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "setup_read", "speed_read", "speed_seen", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_rules", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
13
+ export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "setup_read", "speed_read", "speed_seen", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "notes_bot_upcoming", "ops_status", "slack_coverage", "slack_read", "memory_rules", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
14
14
  /**
15
15
  * Tools that write, but answer a dry run that writes nothing. The smoke calls
16
16
  * each one ONLY with its dry-run flag set (`scripts/smoke.ts` says which).
@@ -21,7 +21,7 @@ export declare const DRY_RUN_TOOLS: readonly ["ops_recompile", "memory_requeue"]
21
21
  * candidates, share calendars. Never called by the smoke; covered by the unit
22
22
  * suites (`write-tools.test.ts` and each family's own).
23
23
  */
24
- export declare const WRITE_TOOLS: readonly ["emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "github_merge", "github_retitle", "github_rerun", "mail_send", "mail_draft", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "glossary_add", "glossary_remove", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_device_revoke", "team_device_doctor"];
24
+ export declare const WRITE_TOOLS: readonly ["brain_retry", "emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "github_merge", "github_retitle", "github_rerun", "mail_send", "mail_draft", "mail_rename", "mail_enable", "mail_disable", "mail_default", "mail_remove", "cal_change", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "glossary_add", "glossary_remove", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_device_revoke", "team_device_doctor"];
25
25
  /** Reads that spend a model turn. The smoke's `--no-model` leaves these out. */
26
26
  export declare const MODEL_SPENDING_TOOLS: readonly ["jarvis_ask", "jarvis_check"];
27
27
  /**
@@ -71,6 +71,7 @@ export const READ_TOOLS = [
71
71
  "notes_recording",
72
72
  "notes_sync_status",
73
73
  "notes_granola_status",
74
+ "notes_bot_upcoming",
74
75
  "ops_status",
75
76
  "slack_coverage",
76
77
  "slack_read",
@@ -102,6 +103,7 @@ export const DRY_RUN_TOOLS = ["ops_recompile", "memory_requeue"];
102
103
  * suites (`write-tools.test.ts` and each family's own).
103
104
  */
104
105
  export const WRITE_TOOLS = [
106
+ "brain_retry",
105
107
  "emit_event",
106
108
  "docs_create",
107
109
  "docs_update",
@@ -130,6 +132,7 @@ export const WRITE_TOOLS = [
130
132
  "mail_send",
131
133
  // BLI-12530: writes into a mailbox (a draft), reaches nobody. Never smoked.
132
134
  "mail_draft",
135
+ "mail_rename", "mail_enable", "mail_disable", "mail_default", "mail_remove", "cal_change",
133
136
  "mail_sync",
134
137
  "mail_detach",
135
138
  "cal_create",
@@ -13,6 +13,7 @@ interface GithubCounts {
13
13
  }
14
14
  interface GithubBlock {
15
15
  status: "ok" | "unavailable";
16
+ read_coverage?: string[];
16
17
  reason?: string;
17
18
  detail?: string;
18
19
  counts_label?: string;
@@ -21,6 +21,7 @@ export function githubWords(github, people) {
21
21
  `GitHub: ${noun(sum("pull_requests_opened"), "pull request")} opened, ${sum("pull_requests_merged")} merged, ${noun(sum("commits"), "commit")}, ` +
22
22
  `from ${noun((github.repositories ?? []).length, "mirrored repository", "mirrored repositories")}${range}.`,
23
23
  ];
24
+ lines.push(...(github.read_coverage ?? []));
24
25
  const unmatched = github.unattributed;
25
26
  if (unmatched && unmatched.pull_requests_opened + unmatched.pull_requests_merged + unmatched.commits > 0) {
26
27
  lines.push(`Not matched to a person: ${noun(unmatched.pull_requests_opened, "pull request")} opened, ${unmatched.pull_requests_merged} merged, ${noun(unmatched.commits, "commit")}.`);
@@ -80,7 +81,7 @@ export function registerUsageTools(server, deps) {
80
81
  // agent reading only the text half cannot quote a pull-request count without
81
82
  // what it covers and who it left out.
82
83
  const github = githubSentence(response.body);
83
- return dataResult(`${coverage?.sessions_extracted ?? 0} of ${coverage?.sessions_observed ?? 0} sessions extracted.${why} ${coverage?.sessions_counted_at_upload ?? 0} of ${coverage?.sessions_observed ?? 0} sessions counted at upload, ${coverage?.sessions_counted_by_server ?? 0} by server backfill, ${coverage?.sessions_counted_by_ladder ?? 0} by the ladder.${args.groupBy === "task_type" ? ` ${coverage?.sessions_labelled ?? 0} of ${coverage?.sessions_observed ?? 0} sessions labelled by topic.` : ""}${priceLabel}${github}`, response.body);
84
+ return dataResult(`${response.body.read_coverage?.join(" ") ?? ""} ${coverage?.sessions_extracted ?? 0} of ${coverage?.sessions_observed ?? 0} sessions extracted.${why} ${coverage?.sessions_counted_at_upload ?? 0} of ${coverage?.sessions_observed ?? 0} sessions counted at upload, ${coverage?.sessions_counted_by_server ?? 0} by server backfill, ${coverage?.sessions_counted_by_ladder ?? 0} by the ladder.${args.groupBy === "task_type" ? ` ${coverage?.sessions_labelled ?? 0} of ${coverage?.sessions_observed ?? 0} sessions labelled by topic.` : ""}${priceLabel}${github}`, response.body);
84
85
  }));
85
86
  registrarFor(server)("usage_sessions", {
86
87
  title: "Read task or subject sessions",
@@ -117,6 +118,6 @@ export function registerUsageTools(server, deps) {
117
118
  return dataResult(`${windows} digest window(s) of session ${String(args.session)}.`, response.body);
118
119
  }
119
120
  const sessions = Array.isArray(response.body.sessions) ? response.body.sessions.length : 0;
120
- return dataResult(`${sessions} session(s) for ${args.task ? `task ${String(args.task)}` : `topic "${String(args.subject)}"`}.`, response.body);
121
+ return dataResult(`${response.body.read_coverage?.join(" ") ?? ""} ${sessions} session(s) for ${args.task ? `task ${String(args.task)}` : `topic "${String(args.subject)}"`}.`, response.body);
121
122
  }));
122
123
  }
@@ -239,6 +239,7 @@ export const MCP_TWINS = {
239
239
  // this machine's collector logs and the `vercel` / `railway` CLIs.
240
240
  // BLI-12855: a project name (or --stored, or a local CLI that is missing) reads the digests Tower's cron stored, through GET /api/logs.
241
241
  "logs": { tool: "logs_digest", door: "LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs); `project` reads GET /api/logs (the stored digests)" },
242
+ "brain retry": { tool: "brain_retry", door: "POST /api/brain/incidents/retry" },
242
243
  "brain status": { tool: "brain_status", door: "GET /api/brain/incidents" },
243
244
  "usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
244
245
  "setup": { tool: "setup_read", door: "GET /api/setup" },
@@ -272,6 +273,11 @@ export const MCP_TWINS = {
272
273
  "issue comment": { tool: "work_comment_issue", door: "POST /api/work/issues/[id]/comments" },
273
274
  "issue history": { tool: "work_issue_history", door: "GET /api/work/issues/[id]/history" },
274
275
  "project list": { tool: "work_list_projects", door: "GET /api/work/projects" },
276
+ "mail rename": { tool: "mail_rename", door: "PATCH /api/mail/accounts" },
277
+ "mail enable": { tool: "mail_enable", door: "PATCH /api/mail/accounts" },
278
+ "mail disable": { tool: "mail_disable", door: "PATCH /api/mail/accounts" },
279
+ "mail default": { tool: "mail_default", door: "PATCH /api/mail/accounts" },
280
+ "mail remove": { tool: "mail_remove", door: "DELETE /api/mail/accounts" },
275
281
  "mail accounts": { tool: "mail_accounts", door: "GET /api/mail/accounts" },
276
282
  "mail inbox": { tool: "mail_inbox", door: "GET /api/mail/inbox" },
277
283
  "mail read": { tool: "mail_read", door: "GET /api/mail/threads/[id]" },
@@ -288,6 +294,8 @@ export const MCP_TWINS = {
288
294
  "cal next": { tool: "cal_next", door: "GET /api/cal/next" },
289
295
  "cal find": { tool: "cal_find", door: "GET /api/cal/find" },
290
296
  "cal calendars": { tool: "cal_calendars", door: "GET /api/cal/calendars" },
297
+ "cal change": { tool: "cal_change", door: "PATCH /api/cal/events/[eventId]" },
298
+ "cal cancel": { tool: "cal_delete", door: "DELETE /api/cal/events/[eventId]" },
291
299
  "cal create": { tool: "cal_create", door: "POST /api/cal/events" },
292
300
  "cal share": { tool: "cal_share", door: "PATCH /api/cal/calendars" },
293
301
  "cal sync": { tool: "cal_sync", door: "POST /api/cal/calendars/[id]/sync" },
@@ -325,6 +333,7 @@ export const MCP_TWINS = {
325
333
  "notes sync": { tool: "notes_sync", door: "POST /api/notes/accounts/[id]/sync" },
326
334
  "notes sync-status": { tool: "notes_sync_status", door: "GET /api/notes/sync-status" },
327
335
  // Ticket 10718: Granola's own cron health, over its shared server-side key.
336
+ "notes bot-upcoming": { tool: "notes_bot_upcoming", door: "GET /api/notetaker/upcoming" },
328
337
  "notes granola-status": { tool: "notes_granola_status", door: "GET /api/notes/granola-status" },
329
338
  "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
330
339
  // BLI-4047, BLI-12927: the memory STORE at a terminal. Its twins live on the
@@ -135,10 +135,11 @@ export function registerWorkTools(server, deps) {
135
135
  + ` [p${issue.priority ?? 0}${size ? `, size ${size}` : ""}, assignee ${assigneeLabel(issue.assignee_id, directory)}, project ${issue.project_id ?? "none"}]`;
136
136
  })
137
137
  .join("\n");
138
- const totalNote = completeness?.total != null && completeness.total > issues.length ? ` ${completeness.total} match in all.` : "";
138
+ const counts = response.body.counts;
139
+ const totalNote = counts ? ` ${counts.total} match in all. ${counts.coverage}` : completeness?.total != null && completeness.total > issues.length ? ` ${completeness.total} match in all.` : "";
139
140
  return textResult(`${issues.length} issue(s).${totalNote}${moreNote(more, ISSUE_LIST_MAX)}${outsideNote(outside)}${lines ? `\n${lines}` : ""}`, {
140
141
  issues: issues.map((issue) => withSizeLabel(withAssigneeIdentity(issueListRow(issue), directory))),
141
- more,
142
+ more, counts,
142
143
  ...(completeness ? { total: completeness.total } : {}),
143
144
  ...(outside ? { outside_project: outside } : {}),
144
145
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
4
4
  "private": false,
5
5
  "description": "bli-tower: an MCP server over BLI Cockpit's agent doors: JARVIS (jarvis_*), documents (docs_*), channels (msg_*), issues (work_*), the daily page (brief_*), meeting notes (notes_*), the ops board (ops_status/slack_*), settings/team/model, Scout and the workbook, plus the legacy event-stream tools (emit_event, get_ticket_timeline, get_active_tickets).",
6
6
  "type": "module",