@bli-cockpit/mcp 0.1.70 → 0.1.71

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
@@ -103,7 +103,7 @@ about what an agent can reach.
103
103
 
104
104
  <!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
105
105
 
106
- **111 of 116 Tower verbs have an MCP twin.**
106
+ **115 of 120 Tower verbs have an MCP twin.**
107
107
  Each tool goes through the SAME door its CLI verb calls, with the same
108
108
  collector device token — never a second route and never a service-role
109
109
  reader. `src/verb-census.test.ts` fails when a verb is in none of the
@@ -125,6 +125,7 @@ three tables below.
125
125
  | `cockpit cal next` | `cal_next` | `GET /api/cal/next` |
126
126
  | `cockpit cal share` | `cal_share` | `PATCH /api/cal/calendars` |
127
127
  | `cockpit cal sync` | `cal_sync` | `POST /api/cal/calendars/[id]/sync` |
128
+ | `cockpit cal sync-status` | `cal_sync_status` | `GET /api/cal/sync-status` |
128
129
  | `cockpit cal today` | `cal_today` | `GET /api/cal/today` |
129
130
  | `cockpit cal week` | `cal_week` | `GET /api/cal/week` |
130
131
  | `cockpit careers decide` | `careers_decide` | `POST /api/careers/applications/[id]/decide` |
@@ -164,6 +165,7 @@ three tables below.
164
165
  | `cockpit mail search` | `mail_search` | `GET /api/mail/search` |
165
166
  | `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
166
167
  | `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
168
+ | `cockpit mail sync-status` | `mail_sync_status` | `GET /api/mail/sync-status` |
167
169
  | `cockpit memory consolidation` | `memory_consolidation` | `GET /api/memory/consolidation` |
168
170
  | `cockpit memory forget` | `forget_memory` (`bli-memory`) | `POST /api/memory/forget` |
169
171
  | `cockpit memory log` | `memory_experience` | `POST /api/memory/experience` |
@@ -186,6 +188,7 @@ three tables below.
186
188
  | `cockpit notes accounts` | `notes_accounts` | `GET /api/notes/accounts` |
187
189
  | `cockpit notes detach` | `notes_detach` | `DELETE /api/notes/accounts` |
188
190
  | `cockpit notes folders` | `notes_folders` | `GET /api/notes/folders` |
191
+ | `cockpit notes granola-status` | `notes_granola_status` | `GET /api/notes/granola-status` |
189
192
  | `cockpit notes list` | `notes_list` | `GET /api/notes/library` |
190
193
  | `cockpit notes mkdir` | `notes_mkdir` | `POST /api/notes/folders` |
191
194
  | `cockpit notes move` | `notes_move` | `POST /api/notes/move` |
@@ -198,6 +201,7 @@ three tables below.
198
201
  | `cockpit notes shelves` | `notes_shelves` | `GET /api/notes/library` |
199
202
  | `cockpit notes show` | `notes_show` | `GET /api/notes/library/[id]` |
200
203
  | `cockpit notes sync` | `notes_sync` | `POST /api/notes/accounts/[id]/sync` |
204
+ | `cockpit notes sync-status` | `notes_sync_status` | `GET /api/notes/sync-status` |
201
205
  | `cockpit notes unshare` | `notes_unshare` | `POST /api/notes/share (share=false)` |
202
206
  | `cockpit notes upload` | `notes_upload` | `POST /api/notes/upload` |
203
207
  | `cockpit ops recompile` | `ops_recompile` | `POST /api/ops/recompile` |
@@ -157,4 +157,24 @@ export function registerBriefTools(server, deps) {
157
157
  return textResult(`${status.displayName ?? "This person"} — ${reason}${detail}${slack}${readError}`
158
158
  + `\n${status.owningCadence ?? "?"} owns this morning${status.timeZone ? ` (${status.timeZone})` : ""}${rendered}`, { status, ...(body.render ? { render: body.render } : {}) });
159
159
  }));
160
+ register("brief_missed", {
161
+ title: "Who is missing their Tower daily brief right now",
162
+ description: "Every roster person owed a brief who has no delivered receipt for their own most recently passed window, "
163
+ + "and why — `compile_failed`, `page_refused`, `dm_failed`, or `no_page_shape` for a person with no page "
164
+ + "shape at all (named, never dropped). Same reader `cockpit ops status`'s `jarvis-ic-dm` row folds in. "
165
+ + "Reads across the team, same gate as `brief_status`'s `subject`.",
166
+ inputSchema: {},
167
+ }, async () => withSession(deps, async (session) => {
168
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/ops/brief-status?missed=1");
169
+ if (!response.ok)
170
+ return errorResult(doorFailureText("brief_missed", response));
171
+ const body = response.body;
172
+ const coverage = body.coverage ?? {};
173
+ const missed = coverage.missed ?? [];
174
+ const lines = missed
175
+ .map((person) => `${person.displayName ?? "?"} ${person.missReason ?? "?"}${person.detail ? ` — ${person.detail}` : ""}`)
176
+ .join("\n");
177
+ return textResult(`${coverage.verdict ?? "unknown"}: ${missed.length} missing of those owed a brief${coverage.detail ? `\n${coverage.detail}` : ""}`
178
+ + `${lines ? `\n${lines}` : ""}`, { coverage });
179
+ }));
160
180
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `cal_sync_status` (ticket 10730) — split out of `cal-tools.ts` when adding
3
+ * it there would have pushed that file over the 500-line readability band.
4
+ * Same door as `cockpit cal sync-status`: every calendar's sync health, or
5
+ * one's, read from `cal_sync_runs` on the caller's own RLS client. Built
6
+ * because the integrations audit had no Tower door for this and went to the
7
+ * ledger table directly; `mail_sync_status` and `notes_sync_status` are its
8
+ * siblings, same shape, same reason.
9
+ */
10
+ import { type RegisterTool, type ToolDeps } from "./tool-result.js";
11
+ export declare function registerCalSyncStatusTool(register: RegisterTool, deps: ToolDeps): void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `cal_sync_status` (ticket 10730) — split out of `cal-tools.ts` when adding
3
+ * it there would have pushed that file over the 500-line readability band.
4
+ * Same door as `cockpit cal sync-status`: every calendar's sync health, or
5
+ * one's, read from `cal_sync_runs` on the caller's own RLS client. Built
6
+ * because the integrations audit had no Tower door for this and went to the
7
+ * ledger table directly; `mail_sync_status` and `notes_sync_status` are its
8
+ * siblings, same shape, same reason.
9
+ */
10
+ import { z } from "zod";
11
+ import { callAgentDoor } from "./agent-door.js";
12
+ import { doorFailureText, errorResult, textResult, withSession } from "./tool-result.js";
13
+ export function registerCalSyncStatusTool(register, deps) {
14
+ register("cal_sync_status", {
15
+ title: "Read calendar sync health",
16
+ description: "Every calendar's sync health, or one's: the last successful pass, the last pass's own result and reason, "
17
+ + "how many events it carried, failures in the last 24 hours, and the LAG between an event's own provider "
18
+ + "clock and when Tower actually stored it. Metadata only — the same read `cockpit cal sync-status` makes "
19
+ + "(ticket 10730, built because the integrations audit had no Tower door for this and went to the table directly).",
20
+ inputSchema: {
21
+ calendar_id: z.string().uuid().optional().describe("One calendar id (cal_calendars). Omit for every calendar you may read."),
22
+ },
23
+ }, async (args) => withSession(deps, async (session) => {
24
+ const path = args.calendar_id
25
+ ? `/api/cal/sync-status?calendar_id=${encodeURIComponent(String(args.calendar_id))}`
26
+ : "/api/cal/sync-status";
27
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", path);
28
+ if (!response.ok)
29
+ return errorResult(doorFailureText("cal_sync_status", response));
30
+ const calendars = (Array.isArray(response.body.calendars) ? response.body.calendars : []);
31
+ const lines = calendars
32
+ .map((calendar) => `${calendar.name} last_success=${calendar.last_success_at ?? "never"} last_run=${calendar.last_run ? `${calendar.last_run.reason}@${calendar.last_run.started_at}` : "never"} failures_24h=${calendar.failures_24h}${calendar.data_incomplete ? " (partial read)" : ""}`)
33
+ .join("\n");
34
+ return textResult(calendars.length === 0 ? "No calendars are attached." : `${calendars.length} calendar(s).\n${lines}`, {
35
+ calendars,
36
+ });
37
+ }));
38
+ }
package/dist/cal-tools.js CHANGED
@@ -33,6 +33,7 @@
33
33
  */
34
34
  import { z } from "zod";
35
35
  import { callAgentDoor } from "./agent-door.js";
36
+ import { registerCalSyncStatusTool } from "./cal-sync-status-tool.js";
36
37
  import { registerCalDeleteTool } from "./cal-tools-delete.js";
37
38
  import { CONFIRM_INPUT, EMAIL_INPUT, doorFailureText, errorResult, queryString, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
38
39
  /** This machine's zone: what a person means by "today" unless they say otherwise. */
@@ -278,6 +279,7 @@ export function registerCalTools(server, deps) {
278
279
  return textResult(`Detached ${String(args.calendar_id)}. Its stored events went with it; the calendar itself is untouched at Google.`, response.body);
279
280
  }));
280
281
  registerCalDeleteTool(register, deps);
282
+ registerCalSyncStatusTool(register, deps);
281
283
  }
282
284
  /** BLI-10560: the new event's Tower id, or why there is none yet. */
283
285
  function towerIdLine(event) {
@@ -81,9 +81,20 @@ export function registerDocsNotionTools(server, deps) {
81
81
  const summary = Object.entries(counts?.by_state ?? {})
82
82
  .map(([state, count]) => `${count} ${state}`)
83
83
  .join(", ");
84
- return textResult(`${counts?.linked ?? 0} linked: ${summary}.`, {
84
+ const lastRun = response.body.last_run;
85
+ const runLine = ["push", "pull"]
86
+ .map((direction) => `${direction} ${runSentence(lastRun?.[direction] ?? null)}`)
87
+ .join(" · ");
88
+ return textResult(`${counts?.linked ?? 0} linked: ${summary}. Last run — ${runLine}.`, {
85
89
  counts,
86
90
  documents: response.body.documents ?? [],
91
+ last_run: lastRun ?? { push: null, pull: null },
87
92
  });
88
93
  }));
89
94
  }
95
+ function runSentence(run) {
96
+ if (!run)
97
+ return "never run";
98
+ const moved = run.pushed || run.pulled;
99
+ return `${run.reason} (${moved} moved, ${run.failed} failed) at ${run.started_at}`;
100
+ }
@@ -51,6 +51,30 @@ export function registerMailTools(server, deps) {
51
51
  ? "No mailboxes are attached. A person attaches one with `cockpit mail add-imap` (personal) or the browser (work)."
52
52
  : `${accounts.length} mailbox(es).\n${lines}`, { accounts });
53
53
  }));
54
+ register("mail_sync_status", {
55
+ title: "Read mail sync health",
56
+ description: "Every mailbox's sync health, or one's: the last successful pass, the last pass's own result and reason, "
57
+ + "how many messages it carried, failures in the last 24 hours, and the LAG between a message's own clock "
58
+ + "and when Tower actually stored it. Metadata only — the same read `cockpit mail sync-status` makes "
59
+ + "(ticket 10730, built because the integrations audit had no Tower door for this and went to the table directly).",
60
+ inputSchema: {
61
+ account_id: z.string().uuid().optional().describe("One mailbox id (mail_accounts). Omit for every mailbox you may read."),
62
+ },
63
+ }, async (args) => withSession(deps, async (session) => {
64
+ const path = args.account_id
65
+ ? `/api/mail/sync-status?account_id=${encodeURIComponent(String(args.account_id))}`
66
+ : "/api/mail/sync-status";
67
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", path);
68
+ if (!response.ok)
69
+ return errorResult(doorFailureText("mail_sync_status", response));
70
+ const accounts = (Array.isArray(response.body.accounts) ? response.body.accounts : []);
71
+ const lines = accounts
72
+ .map((account) => `${account.address} last_success=${account.last_success_at ?? "never"} last_run=${account.last_run ? `${account.last_run.reason}@${account.last_run.started_at}` : "never"} failures_24h=${account.failures_24h}${account.data_incomplete ? " (partial read)" : ""}`)
73
+ .join("\n");
74
+ return textResult(accounts.length === 0 ? "No mailboxes are attached." : `${accounts.length} mailbox(es).\n${lines}`, {
75
+ accounts,
76
+ });
77
+ }));
54
78
  register("mail_inbox", {
55
79
  title: "Read the unified inbox",
56
80
  description: "Messages across every mailbox you may read, newest first. Never a body — call mail_read on a thread for that. "
@@ -45,6 +45,30 @@ export function registerNotesAccountTools(server, deps) {
45
45
  ? "No notetaker is connected. A person connects one with `cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin`, or in Tower's Settings."
46
46
  : `${accounts.length} notetaker(s).\n${accounts.map(accountLine).join("\n")}`, { accounts });
47
47
  }));
48
+ register("notes_sync_status", {
49
+ title: "Read notetaker sync health",
50
+ description: "Every notetaker's sync health, or one's: the last successful pass, the last pass's own result and reason, "
51
+ + "how many meetings it carried, failures in the last 24 hours, and the LAG between the newest meeting it has "
52
+ + "already read and when it last ran. Metadata only — the same read `cockpit notes sync-status` makes "
53
+ + "(ticket 10730, built because the integrations audit had no Tower door for this and went to the table directly).",
54
+ inputSchema: {
55
+ account_id: z.string().uuid().optional().describe("One notetaker account id (notes_accounts). Omit for every account you may read."),
56
+ },
57
+ }, async (args) => withSession(deps, async (session) => {
58
+ const path = args.account_id
59
+ ? `/api/notes/sync-status?account_id=${encodeURIComponent(String(args.account_id))}`
60
+ : "/api/notes/sync-status";
61
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", path);
62
+ if (!response.ok)
63
+ return errorResult(doorFailureText("notes_sync_status", response));
64
+ const accounts = (Array.isArray(response.body.accounts) ? response.body.accounts : []);
65
+ const lines = accounts
66
+ .map((account) => `${account.label ?? account.provider} last_success=${account.last_success_at ?? "never"} last_run=${account.last_run ? `${account.last_run.reason}@${account.last_run.started_at}` : "never"} failures_24h=${account.failures_24h}${account.data_incomplete ? " (partial read)" : ""}`)
67
+ .join("\n");
68
+ return textResult(accounts.length === 0 ? "No notetaker is connected." : `${accounts.length} notetaker(s).\n${lines}`, {
69
+ accounts,
70
+ });
71
+ }));
48
72
  register("notes_sync", {
49
73
  title: "Read a notetaker now",
50
74
  description: "Reads the meetings this notetaker has recorded since the last pass and files them as meeting notes. "
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `notes_granola_status` MCP tool (ticket 10718) — the Granola cron's own
3
+ * last several runs, on the `bli-tower` server, over the same
4
+ * `/api/notes/granola-status` door `cockpit notes granola-status` calls,
5
+ * authenticated with this machine's collector device token.
6
+ *
7
+ * Granola runs on one shared server-side key rather than a `notes_accounts`
8
+ * row a person connects, so this is its own tool beside
9
+ * `notes-account-tools.ts` rather than a filter on `notes_accounts`.
10
+ */
11
+ import { type ToolDeps } from "./tool-result.js";
12
+ export type NotesGranolaStatusDeps = ToolDeps;
13
+ export declare function registerNotesGranolaStatusTool(server: {
14
+ registerTool: (...args: never[]) => unknown;
15
+ }, deps: NotesGranolaStatusDeps): void;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `notes_granola_status` MCP tool (ticket 10718) — the Granola cron's own
3
+ * last several runs, on the `bli-tower` server, over the same
4
+ * `/api/notes/granola-status` door `cockpit notes granola-status` calls,
5
+ * authenticated with this machine's collector device token.
6
+ *
7
+ * Granola runs on one shared server-side key rather than a `notes_accounts`
8
+ * row a person connects, so this is its own tool beside
9
+ * `notes-account-tools.ts` rather than a filter on `notes_accounts`.
10
+ */
11
+ import { z } from "zod";
12
+ import { callAgentDoor } from "./agent-door.js";
13
+ import { doorFailureText, errorResult, queryString, registrarFor, textResult, withSession, } from "./tool-result.js";
14
+ function runLine(run) {
15
+ const spend = run.spend_usd === null ? "n/a" : `$${run.spend_usd.toFixed(4)}`;
16
+ return `${run.started_at} ${run.reason} seen=${run.notes_seen} stored=${run.notes_stored} `
17
+ + `failed=${run.notes_failed} model=${run.model ?? "n/a"} spend=${spend}`;
18
+ }
19
+ export function registerNotesGranolaStatusTool(server, deps) {
20
+ const register = registrarFor(server);
21
+ register("notes_granola_status", {
22
+ title: "The Granola cron's own health",
23
+ description: "The newest granola:pull runs (ticket 10718): each one's reason (ok, partial, or a named setup failure), "
24
+ + "how many notes it saw/stored/failed, the model it used, real spend, and the lookback window it computed. "
25
+ + "Granola runs on one shared server-side key, not a connected account, so this answers for the CRON itself "
26
+ + "rather than `notes_accounts`.",
27
+ inputSchema: { limit: z.number().int().min(1).max(50).optional().describe("Newest N runs. Default 10, ceiling 50.") },
28
+ }, async (args) => withSession(deps, async (session) => {
29
+ const query = new URLSearchParams();
30
+ if (args.limit !== undefined)
31
+ query.set("limit", String(args.limit));
32
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/notes/granola-status${queryString(query)}`);
33
+ if (!response.ok)
34
+ return errorResult(doorFailureText("notes_granola_status", response));
35
+ const runs = (Array.isArray(response.body.runs) ? response.body.runs : []);
36
+ return textResult(runs.length === 0
37
+ ? "No granola:pull run has ever landed a row."
38
+ : `${runs.length} run(s).\n${runs.map(runLine).join("\n")}`, { runs });
39
+ }));
40
+ }
package/dist/ops-tools.js CHANGED
@@ -161,6 +161,12 @@ export function registerOpsTools(server, deps) {
161
161
  ? `TOOL ROUTER ${toolRouterBoard.turnsRouted} routed`
162
162
  : "TOOL ROUTER not read (tool_router_section_absent)"
163
163
  : "";
164
+ // BLI-10728: the daily-brief delivery streak, the server's own
165
+ // sentence relayed verbatim, same rule as `ladderLine` above.
166
+ const briefStreak = asRecord(body.briefStreak);
167
+ const briefStreakLine = typeof briefStreak.streakDays === "number"
168
+ ? `BRIEF STREAK ${briefStreak.streakDays} day(s)${briefStreak.detail ? ` — ${briefStreak.detail}` : ""}`
169
+ : "";
164
170
  // Both branches say something. A board that is entirely green and
165
171
  // reports nothing cannot answer "did anybody look today?".
166
172
  return textResult(`${rows.length} pipeline(s), ${unhealthy.length} unhealthy`
@@ -169,6 +175,7 @@ export function registerOpsTools(server, deps) {
169
175
  + `${fleetSummary ? `\nFLEET ${fleetSummary}` : ""}`
170
176
  + `${coverageSummary ? `\nCOVERAGE ${coverageSummary}` : ""}`
171
177
  + `${ladderLine ? `\n${ladderLine}` : ""}`
178
+ + `${briefStreakLine ? `\n${briefStreakLine}` : ""}`
172
179
  + `${memoryLines ? `\n${memoryLines}` : ""}`
173
180
  + `${experienceLines ? `\n${experienceLines}` : ""}`
174
181
  + `${turnLine ? `\n${turnLine}` : ""}`
@@ -181,6 +188,7 @@ export function registerOpsTools(server, deps) {
181
188
  ...(body.coverage ? { coverage: summaryOnly(body.coverage, args.people === true) } : {}),
182
189
  ...(body.skips ? { skips: body.skips } : {}),
183
190
  ...(body.ladder ? { ladder: body.ladder } : {}),
191
+ ...(body.briefStreak ? { briefStreak: body.briefStreak } : {}),
184
192
  ...(memory?.ok ? { memory: memory.body.memory, hooks: memory.body.hooks, experience: memory.body.experience } : {}),
185
193
  ...(args.turns ? { turns: body.turns ?? null } : {}),
186
194
  ...(toolRouter?.ok ? { toolRouter: toolRouter.body.board ?? null } : {}),
package/dist/server.js CHANGED
@@ -16,6 +16,7 @@ import { registerCalTools } from "./cal-tools.js";
16
16
  import { registerMailTools } from "./mail-tools.js";
17
17
  import { registerNotesAccountTools } from "./notes-account-tools.js";
18
18
  import { registerNotesTools } from "./notes-tools.js";
19
+ import { registerNotesGranolaStatusTool } from "./notes-granola-status-tool.js";
19
20
  import { registerNotesWriteTools } from "./notes-write-tools.js";
20
21
  import { registerMemoryConsolidationTools } from "./memory-consolidation-tools.js";
21
22
  import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
@@ -450,6 +451,9 @@ export function createServer(deps) {
450
451
  // complete.
451
452
  registerBriefTools(server, { fetchImpl: deps.fetchImpl });
452
453
  registerNotesTools(server, { fetchImpl: deps.fetchImpl });
454
+ // Ticket 10718: the Granola cron's own health, read-only, over the shared
455
+ // server-side key rather than a `notes_accounts` row.
456
+ registerNotesGranolaStatusTool(server, { fetchImpl: deps.fetchImpl });
453
457
  registerOpsTools(server, { fetchImpl: deps.fetchImpl });
454
458
  registerMemoryExperienceTools(server, { fetchImpl: deps.fetchImpl });
455
459
  // BLI-4533: the compiled one-page profile, the twin of `cockpit memory profile`.
@@ -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", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "notes_folders", "notes_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search"];
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", "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", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search"];
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).
@@ -36,11 +36,13 @@ export const READ_TOOLS = [
36
36
  "mail_read",
37
37
  "mail_search",
38
38
  "mail_attachment",
39
+ "mail_sync_status",
39
40
  "cal_today",
40
41
  "cal_week",
41
42
  "cal_next",
42
43
  "cal_find",
43
44
  "cal_calendars",
45
+ "cal_sync_status",
44
46
  "jarvis_ask",
45
47
  "jarvis_trace",
46
48
  "jarvis_check",
@@ -50,6 +52,7 @@ export const READ_TOOLS = [
50
52
  "brief_read",
51
53
  "brief_history",
52
54
  "brief_status",
55
+ "brief_missed",
53
56
  "notes_folders",
54
57
  "notes_list",
55
58
  "notes_show",
@@ -57,6 +60,8 @@ export const READ_TOOLS = [
57
60
  "notes_shelves",
58
61
  "notes_place",
59
62
  "notes_accounts",
63
+ "notes_sync_status",
64
+ "notes_granola_status",
60
65
  "ops_status",
61
66
  "slack_coverage",
62
67
  "slack_read",
@@ -258,6 +258,7 @@ export const MCP_TWINS = {
258
258
  "mail send": { tool: "mail_send", door: "POST /api/mail/send" },
259
259
  "mail attachment": { tool: "mail_attachment", door: "GET /api/mail/attachments/[id]?meta=1" },
260
260
  "mail sync": { tool: "mail_sync", door: "POST /api/mail/accounts/[id]/sync" },
261
+ "mail sync-status": { tool: "mail_sync_status", door: "GET /api/mail/sync-status" },
261
262
  "mail detach": { tool: "mail_detach", door: "DELETE /api/mail/accounts" },
262
263
  "cal today": { tool: "cal_today", door: "GET /api/cal/today" },
263
264
  "cal week": { tool: "cal_week", door: "GET /api/cal/week" },
@@ -267,6 +268,7 @@ export const MCP_TWINS = {
267
268
  "cal create": { tool: "cal_create", door: "POST /api/cal/events" },
268
269
  "cal share": { tool: "cal_share", door: "PATCH /api/cal/calendars" },
269
270
  "cal sync": { tool: "cal_sync", door: "POST /api/cal/calendars/[id]/sync" },
271
+ "cal sync-status": { tool: "cal_sync_status", door: "GET /api/cal/sync-status" },
270
272
  "cal detach": { tool: "cal_detach", door: "DELETE /api/cal/calendars" },
271
273
  "cal delete": { tool: "cal_delete", door: "DELETE /api/cal/events/[eventId]" },
272
274
  "brief read": { tool: "brief_read", door: "GET /api/jarvis/brief" },
@@ -297,6 +299,9 @@ export const MCP_TWINS = {
297
299
  "notes accounts": { tool: "notes_accounts", door: "GET /api/notes/accounts" },
298
300
  "notes detach": { tool: "notes_detach", door: "DELETE /api/notes/accounts" },
299
301
  "notes sync": { tool: "notes_sync", door: "POST /api/notes/accounts/[id]/sync" },
302
+ "notes sync-status": { tool: "notes_sync_status", door: "GET /api/notes/sync-status" },
303
+ // Ticket 10718: Granola's own cron health, over its shared server-side key.
304
+ "notes granola-status": { tool: "notes_granola_status", door: "GET /api/notes/granola-status" },
300
305
  "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
301
306
  // BLI-4047: the memory STORE at a terminal. The twins live on the second
302
307
  // server, `bli-memory`, over the same four doors these CLI verbs call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
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",
@@ -30,7 +30,7 @@
30
30
  "readme": "npm run build && node scripts/write-readme-census.mjs"
31
31
  },
32
32
  "dependencies": {
33
- "@bli-cockpit/telemetry-core": "0.1.48",
33
+ "@bli-cockpit/telemetry-core": "0.1.49",
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
35
  "zod": "^4.3.6"
36
36
  },