@bli-cockpit/mcp 0.1.44 → 0.1.45

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
@@ -34,7 +34,7 @@ about what an agent can reach.
34
34
 
35
35
  <!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
36
36
 
37
- **91 of 94 Tower verbs have an MCP twin.**
37
+ **94 of 98 Tower verbs have an MCP twin.**
38
38
  Each tool goes through the SAME door its CLI verb calls, with the same
39
39
  collector device token — never a second route and never a service-role
40
40
  reader. `src/verb-census.test.ts` fails when a verb is in none of the
@@ -97,6 +97,8 @@ three tables below.
97
97
  | `cockpit msg read` | `msg_read` | `GET /api/msg/channels/[id]/messages` |
98
98
  | `cockpit msg send` | `msg_send` | `POST /api/msg/channels/[id]/messages` |
99
99
  | `cockpit msg thread` | `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` |
100
+ | `cockpit notes accounts` | `notes_accounts` | `GET /api/notes/accounts` |
101
+ | `cockpit notes detach` | `notes_detach` | `DELETE /api/notes/accounts` |
100
102
  | `cockpit notes folders` | `notes_folders` | `GET /api/notes/folders` |
101
103
  | `cockpit notes list` | `notes_list` | `GET /api/notes/library` |
102
104
  | `cockpit notes mkdir` | `notes_mkdir` | `POST /api/notes/folders` |
@@ -109,6 +111,7 @@ three tables below.
109
111
  | `cockpit notes shelf` | `notes_shelf` | `GET /api/notes/shelf` |
110
112
  | `cockpit notes shelves` | `notes_shelves` | `GET /api/notes/library` |
111
113
  | `cockpit notes show` | `notes_show` | `GET /api/notes/library/[id]` |
114
+ | `cockpit notes sync` | `notes_sync` | `POST /api/notes/accounts/[id]/sync` |
112
115
  | `cockpit notes unshare` | `notes_unshare` | `POST /api/notes/share (share=false)` |
113
116
  | `cockpit notes upload` | `notes_upload` | `POST /api/notes/upload` |
114
117
  | `cockpit ops recompile` | `ops_recompile` | `POST /api/ops/recompile` |
@@ -149,6 +152,7 @@ A claim about the verb's nature, not a backlog.
149
152
  | `cockpit brief edit` | opens the person's own $EDITOR on this machine and files what they changed; an agent has no editor to open (commands/editor.ts) |
150
153
  | `cockpit cal add-ical` | carries a calendar's SECRET iCal ADDRESS — a permanent, unauthenticated, read-anything-on-that-calendar URL. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a calendar stays a thing a person does at a terminal (BLI-3709) |
151
154
  | `cockpit mail add-imap` | carries a Google app password. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a mailbox stays a thing a person does at a terminal (BLI-3708) |
155
+ | `cockpit notes connect` | carries a notetaker API key (Fathom today, Granola next). The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so connecting a notetaker stays a thing a person does at a terminal or in Tower's Settings (BLI-4383) |
152
156
 
153
157
  <!-- END GENERATED verb census -->
154
158
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `notes_accounts` / `notes_detach` / `notes_sync` MCP tools (BLI-4383) — the
3
+ * notetaker a person connected, on the `bli-tower` server, over the same
4
+ * `/api/notes/accounts` doors `cockpit notes` and the Settings section call,
5
+ * authenticated with this machine's collector device token.
6
+ *
7
+ * ONE DELIBERATE ASYMMETRY WITH THE CLI, recorded in `verb-census.ts`:
8
+ * **`cockpit notes connect` has NO MCP twin.** It carries a notetaker API key,
9
+ * and the CLI takes that on stdin precisely so it never lands in an argument
10
+ * list. An MCP tool argument IS an argument list: it travels through a model's
11
+ * context window and whatever transcript store sits behind it. That is the
12
+ * same ruling `mail add-imap` and `cal add-ical` already carry, for the same
13
+ * credential shape, and connecting a notetaker stays a thing a person does at
14
+ * a terminal or in Settings.
15
+ *
16
+ * Same session discipline as `mail-tools.ts`: the session is loaded fresh per
17
+ * call, so a machine with no `cockpit login` pairing still serves this
18
+ * server's other tools and only a `notes_` account call fails, by name.
19
+ */
20
+ import { type ToolDeps } from "./tool-result.js";
21
+ export type NotesAccountDeps = ToolDeps;
22
+ export declare function registerNotesAccountTools(server: {
23
+ registerTool: (...args: never[]) => unknown;
24
+ }, deps: NotesAccountDeps): void;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `notes_accounts` / `notes_detach` / `notes_sync` MCP tools (BLI-4383) — the
3
+ * notetaker a person connected, on the `bli-tower` server, over the same
4
+ * `/api/notes/accounts` doors `cockpit notes` and the Settings section call,
5
+ * authenticated with this machine's collector device token.
6
+ *
7
+ * ONE DELIBERATE ASYMMETRY WITH THE CLI, recorded in `verb-census.ts`:
8
+ * **`cockpit notes connect` has NO MCP twin.** It carries a notetaker API key,
9
+ * and the CLI takes that on stdin precisely so it never lands in an argument
10
+ * list. An MCP tool argument IS an argument list: it travels through a model's
11
+ * context window and whatever transcript store sits behind it. That is the
12
+ * same ruling `mail add-imap` and `cal add-ical` already carry, for the same
13
+ * credential shape, and connecting a notetaker stays a thing a person does at
14
+ * a terminal or in Settings.
15
+ *
16
+ * Same session discipline as `mail-tools.ts`: the session is loaded fresh per
17
+ * call, so a machine with no `cockpit login` pairing still serves this
18
+ * server's other tools and only a `notes_` account call fails, by name.
19
+ */
20
+ import { z } from "zod";
21
+ import { callAgentDoor } from "./agent-door.js";
22
+ import { CONFIRM_INPUT, doorFailureText, errorResult, queryString, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
23
+ function accountLine(account) {
24
+ const health = account.status === "active"
25
+ ? "active"
26
+ : `${account.status}${account.status_reason ? ` (${account.status_reason})` : ""}`;
27
+ return `${account.label ?? account.provider} ${account.provider} ${health} last read ${account.last_synced_at ?? "never"} ${account.id}`;
28
+ }
29
+ export function registerNotesAccountTools(server, deps) {
30
+ const register = registrarFor(server);
31
+ register("notes_accounts", {
32
+ title: "List connected notetakers",
33
+ description: "Every notetaker account (Fathom, and later Granola) connected to Tower that you may see: name, provider, "
34
+ + "health and when it was last read. Start here when you need an account id. Connecting one is NOT available "
35
+ + "here on purpose — it carries an API key, so it is done at a terminal with `cockpit notes connect "
36
+ + "--provider fathom --key-stdin` or in Tower's Settings.",
37
+ inputSchema: {},
38
+ }, async () => withSession(deps, async (session) => {
39
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/accounts");
40
+ if (!response.ok)
41
+ return errorResult(doorFailureText("notes_accounts", response));
42
+ const accounts = (Array.isArray(response.body.accounts) ? response.body.accounts : []);
43
+ return textResult(accounts.length === 0
44
+ ? "No notetaker is connected. A person connects one with `cockpit notes connect --provider fathom --key-stdin`, or in Tower's Settings."
45
+ : `${accounts.length} notetaker(s).\n${accounts.map(accountLine).join("\n")}`, { accounts });
46
+ }));
47
+ register("notes_sync", {
48
+ title: "Read a notetaker now",
49
+ description: "Reads the meetings this notetaker has recorded since the last pass and files them as meeting notes. "
50
+ + "Answers with the run's own counts and reason label, which is exactly what the ledger row says. "
51
+ + "The half-hourly cron does this unasked; this is the button for when you do not want to wait.",
52
+ inputSchema: { account_id: z.string().uuid() },
53
+ }, async (args) => withSession(deps, async (session) => {
54
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", `/api/notes/accounts/${encodeURIComponent(String(args.account_id))}/sync`);
55
+ if (!response.ok)
56
+ return errorResult(doorFailureText("notes_sync", response));
57
+ const run = response.body.run;
58
+ return textResult(run
59
+ ? `${run.reason}: ${run.notesAdded} stored, ${run.notesUpdated} unchanged, ${run.notesSkipped} skipped. ${run.detail}`
60
+ : "Tower answered without a run.", response.body);
61
+ }));
62
+ register("notes_detach", {
63
+ title: "Detach a notetaker",
64
+ description: "Removes a notetaker account and its stored key from Tower. The account itself is untouched at the provider, "
65
+ + "and the meetings it already filed stay where they are. Requires confirm: true.",
66
+ inputSchema: { account_id: z.string().uuid(), confirm: CONFIRM_INPUT },
67
+ }, async (args) => withSession(deps, async (session) => {
68
+ 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).");
69
+ if (refusal)
70
+ return refusal;
71
+ const query = new URLSearchParams({ account_id: String(args.account_id) });
72
+ const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/notes/accounts${queryString(query)}`);
73
+ if (!response.ok)
74
+ return errorResult(doorFailureText("notes_detach", response));
75
+ return textResult(`Detached ${String(args.account_id)}. Its stored key went with it; the meetings it filed stay where they are.`, response.body);
76
+ }));
77
+ }
package/dist/server.js CHANGED
@@ -12,6 +12,7 @@ import { registerDocsMsgTools } from "./docs-msg-tools.js";
12
12
  import { registerJarvisTools } from "./jarvis-tools.js";
13
13
  import { registerCalTools } from "./cal-tools.js";
14
14
  import { registerMailTools } from "./mail-tools.js";
15
+ import { registerNotesAccountTools } from "./notes-account-tools.js";
15
16
  import { registerNotesTools } from "./notes-tools.js";
16
17
  import { registerNotesWriteTools } from "./notes-write-tools.js";
17
18
  import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
@@ -432,6 +433,10 @@ export function createServer(deps) {
432
433
  // `AWAITING_TWIN` is empty as of this batch.
433
434
  registerBriefWriteTools(server, { fetchImpl: deps.fetchImpl });
434
435
  registerNotesWriteTools(server, { fetchImpl: deps.fetchImpl });
436
+ // BLI-4383: the notetaker a person connected. Connecting one is deliberately
437
+ // absent — it carries an API key, and a tool argument travels through a
438
+ // model's context window (`verb-census.ts`'s TERMINAL_ONLY says so in full).
439
+ registerNotesAccountTools(server, { fetchImpl: deps.fetchImpl });
435
440
  registerSettingsWriteTools(server, { fetchImpl: deps.fetchImpl });
436
441
  registerTeamWriteTools(server, { fetchImpl: deps.fetchImpl });
437
442
  // BLI-3728: one search over documents, messages, issues, meeting notes and
@@ -269,6 +269,11 @@ export const MCP_TWINS = {
269
269
  "notes unshare": { tool: "notes_unshare", door: "POST /api/notes/share (share=false)" },
270
270
  "notes move": { tool: "notes_move", door: "POST /api/notes/move" },
271
271
  "notes place": { tool: "notes_place", door: "POST /api/notes/place" },
272
+ // BLI-4383. `notes connect` is in TERMINAL_ONLY below, for the same reason
273
+ // `mail add-imap` and `cal add-ical` are.
274
+ "notes accounts": { tool: "notes_accounts", door: "GET /api/notes/accounts" },
275
+ "notes detach": { tool: "notes_detach", door: "DELETE /api/notes/accounts" },
276
+ "notes sync": { tool: "notes_sync", door: "POST /api/notes/accounts/[id]/sync" },
272
277
  "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
273
278
  // BLI-4047: the memory STORE at a terminal. The twins live on the second
274
279
  // server, `bli-memory`, over the same four doors these CLI verbs call.
@@ -350,6 +355,7 @@ export const TERMINAL_ONLY = {
350
355
  "mail add-imap": "carries a Google app password. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a mailbox stays a thing a person does at a terminal (BLI-3708)",
351
356
  "cal add-ical": "carries a calendar's SECRET iCal ADDRESS — a permanent, unauthenticated, read-anything-on-that-calendar URL. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a calendar stays a thing a person does at a terminal (BLI-3709)",
352
357
  "brief edit": "opens the person's own $EDITOR on this machine and files what they changed; an agent has no editor to open (commands/editor.ts)",
358
+ "notes connect": "carries a notetaker API key (Fathom today, Granola next). The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so connecting a notetaker stays a thing a person does at a terminal or in Tower's Settings (BLI-4383)",
353
359
  };
354
360
  /**
355
361
  * Verbs owed a twin, with who owes it. This list should only ever shrink.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
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",