@bli-cockpit/mcp 0.1.28 → 0.1.30

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
- **77 of 80 Tower verbs have an MCP twin.**
37
+ **83 of 86 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
@@ -78,7 +78,11 @@ three tables below.
78
78
  | `cockpit mail search` | `mail_search` | `GET /api/mail/search` |
79
79
  | `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
80
80
  | `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
81
+ | `cockpit memory forget` | `forget_memory` (`bli-memory`) | `POST /api/memory/forget` |
81
82
  | `cockpit memory log` | `memory_experience` | `POST /api/memory/experience` |
83
+ | `cockpit memory save` | `save_memory` (`bli-memory`) | `POST /api/memory/save` |
84
+ | `cockpit memory search` | `search_memory` (`bli-memory`) | `POST /api/memory/search` |
85
+ | `cockpit memory update` | `update_memory` (`bli-memory`) | `POST /api/memory/update` |
82
86
  | `cockpit model set` | `model_set` | `POST /api/settings/jarvis-model` |
83
87
  | `cockpit model show` | `model_show` | `GET /api/settings/jarvis-model` |
84
88
  | `cockpit models compare` | `models_compare` | `GET /api/models/cards?compare=` |
@@ -93,6 +97,7 @@ three tables below.
93
97
  | `cockpit notes list` | `notes_list` | `GET /api/notes/library` |
94
98
  | `cockpit notes move` | `notes_move` | `POST /api/notes/move` |
95
99
  | `cockpit notes paste` | `notes_paste` | `POST /api/notes/upload (text)` |
100
+ | `cockpit notes place` | `notes_place` | `POST /api/notes/place` |
96
101
  | `cockpit notes share` | `notes_share` | `POST /api/notes/share` |
97
102
  | `cockpit notes shelf` | `notes_shelf` | `GET /api/notes/shelf` |
98
103
  | `cockpit notes shelves` | `notes_shelves` | `GET /api/notes/library` |
@@ -107,17 +112,18 @@ three tables below.
107
112
  | `cockpit scout start` | `scout_start` | `POST /api/cockpit/scout (start)` |
108
113
  | `cockpit scout undo` | `scout_undo` | `POST /api/cockpit/scout (undo_dismiss)` |
109
114
  | `cockpit search` | `tower_search` | `GET /api/search` |
110
- | `cockpit settings delete` | `settings_delete` | `DELETE /api/settings/env-blobs` |
111
- | `cockpit settings list` | `settings_list` | `GET /api/settings/env-blobs` |
112
- | `cockpit settings set` | `settings_set` | `POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor` |
113
- | `cockpit settings show` | `settings_show` | `GET /api/settings/* + /api/team/members` |
115
+ | `cockpit settings env delete` | `settings_delete` | `DELETE /api/settings/env-blobs` |
116
+ | `cockpit settings env list` | `settings_list` | `GET /api/settings/env-blobs` |
117
+ | `cockpit settings switches set` | `settings_set` | `POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor` |
118
+ | `cockpit settings` | `settings_show` | `GET /api/settings/* + /api/team/members` |
114
119
  | `cockpit slack coverage` | `slack_coverage` | `GET /api/ops/slack/coverage` |
115
120
  | `cockpit slack read` | `slack_read` | `POST /api/ops/slack/read` |
116
- | `cockpit team device-list` | `team_device_list` | `GET /api/team/devices` |
117
- | `cockpit team device-revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
121
+ | `cockpit team device list` | `team_device_list` | `GET /api/team/devices` |
122
+ | `cockpit team device revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
118
123
  | `cockpit team invite` | `team_invite` | `POST /api/team/invite` |
119
124
  | `cockpit team members` | `team_members` | `GET /api/team/members` |
120
125
  | `cockpit team role` | `team_role` | `PATCH /api/team/members/[userId]/role` |
126
+ | `cockpit usage people` | `usage_people` | `GET /api/usage/people` |
121
127
  | `cockpit workbook` | `workbook_read` | `GET /api/cockpit/workbook` |
122
128
 
123
129
  ### Owed a twin
@@ -199,6 +199,31 @@ export function registerNotesWriteTools(server, deps) {
199
199
  ? textResult(text, { ok: true, shelf: body.shelf ?? null })
200
200
  : errorResult(text);
201
201
  }));
202
+ register("notes_place", {
203
+ title: "Say which shelf a Tower meeting note belongs on",
204
+ description: "Answers where one note files itself and WHY, from the meeting series it is part of, who was in the room "
205
+ + "and its own name — no model call. Read-only unless `apply: true`, which files it there. It never "
206
+ + "overrules a shelf a person typed: a note that already has one comes back as `already_shelved`.",
207
+ inputSchema: {
208
+ note_id: z.string().min(1).max(200).describe("A note id."),
209
+ apply: z.boolean().optional().describe("Actually put it there. Default: only say where."),
210
+ },
211
+ }, async (args) => withSession(deps, async (session) => {
212
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/place", { note_id: String(args.note_id ?? ""), apply: args.apply === true }, WRITE_DEADLINE_MS);
213
+ if (!response.ok)
214
+ return errorResult(doorFailureText("notes_place", response));
215
+ const body = response.body;
216
+ const text = [body.headline ?? "Tower answered without a sentence.", ...(body.lines ?? [])].join("\n");
217
+ return body.ok === true
218
+ ? textResult(text, {
219
+ ok: true,
220
+ shelf: body.shelf ?? null,
221
+ suggested_shelf: body.suggestedShelf ?? null,
222
+ decided_by: body.decidedBy ?? null,
223
+ applied: body.applied === true,
224
+ })
225
+ : errorResult(text);
226
+ }));
202
227
  }
203
228
  /** Share and unshare are one door and one body; only the boolean differs. */
204
229
  async function shareCall(deps, session, args, share) {
@@ -11,12 +11,17 @@
11
11
  * and returns a string. No file writing here — that is the script's job, so
12
12
  * this can be asserted without touching a disk.
13
13
  */
14
- import { AWAITING_TWIN, MCP_TWINS, TERMINAL_ONLY, towerVerbs } from "./verb-census.js";
14
+ import { AWAITING_TWIN, MCP_TWINS, TERMINAL_ONLY, towerVerbs, typedSpelling } from "./verb-census.js";
15
15
  export const CENSUS_BEGIN = "<!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->";
16
16
  export const CENSUS_END = "<!-- END GENERATED verb census -->";
17
- /** `cockpit docs read` → `` `cockpit docs read` ``; `jarvis --trace` keeps its flag. */
17
+ /**
18
+ * `cockpit docs read` → `` `cockpit docs read` ``; `jarvis --trace` keeps its
19
+ * flag. A census key whose grammar differs from the typed command is rendered
20
+ * as the command (`settings show` → `cockpit settings`), because this column is
21
+ * headed "CLI verb" and a reader takes it literally (BLI-4048).
22
+ */
18
23
  function spell(verb) {
19
- return `\`cockpit ${verb}\``;
24
+ return `\`cockpit ${typedSpelling(verb)}\``;
20
25
  }
21
26
  export function renderCensusMarkdown() {
22
27
  // Every verb the collector's own tables name, plus the one hand-entered mode
@@ -26,7 +31,10 @@ export function renderCensusMarkdown() {
26
31
  .filter((verb) => MCP_TWINS[verb])
27
32
  .map((verb) => {
28
33
  const twin = MCP_TWINS[verb];
29
- return `| ${spell(verb)} | \`${twin.tool}\` | \`${twin.door}\` |`;
34
+ // BLI-4047: two MCP servers ship. A tool cell that did not say which one
35
+ // registers it would send a reader to `bli-tower` for `search_memory`.
36
+ const server = twin.server && twin.server !== "bli-tower" ? ` (\`${twin.server}\`)` : "";
37
+ return `| ${spell(verb)} | \`${twin.tool}\`${server} | \`${twin.door}\` |`;
30
38
  });
31
39
  const owedRows = verbs
32
40
  .filter((verb) => AWAITING_TWIN[verb])
package/dist/server.js CHANGED
@@ -23,6 +23,7 @@ import { registerSettingsTools } from "./settings-tools.js";
23
23
  import { registerSettingsWriteTools } from "./settings-write-tools.js";
24
24
  import { registerTeamWriteTools } from "./team-write-tools.js";
25
25
  import { registerWorkTools } from "./work-tools.js";
26
+ import { registerUsageTools } from "./usage-tools.js";
26
27
  export const PACKAGE_NAME = "@bli-cockpit/mcp";
27
28
  export const PACKAGE_VERSION = "0.1.4";
28
29
  // ---- input schemas (Zod raw shapes) -----------------------------------------
@@ -390,6 +391,7 @@ export function createServer(deps) {
390
391
  // the docs/msg tools above, for the same reason: a coding session should
391
392
  // file and move a Tower issue the way it files and moves a Linear one.
392
393
  registerWorkTools(server, { fetchImpl: deps.fetchImpl });
394
+ registerUsageTools(server, { fetchImpl: deps.fetchImpl });
393
395
  // BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
394
396
  // the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
395
397
  // has no twin (see mail-tools.ts's header — a credential is not a tool
@@ -0,0 +1,4 @@
1
+ import { type ToolDeps } from "./tool-result.js";
2
+ export declare function registerUsageTools(server: {
3
+ registerTool: (...args: never[]) => unknown;
4
+ }, deps: ToolDeps): void;
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import { callAgentDoor } from "./agent-door.js";
3
+ import { doorFailureText, errorResult, registrarFor, textResult, withSession } from "./tool-result.js";
4
+ export function registerUsageTools(server, deps) {
5
+ registrarFor(server)("usage_people", {
6
+ title: "Read usage by person",
7
+ description: "Per-person Claude Code and Codex token usage, list-price equivalent, and extraction coverage.",
8
+ inputSchema: { since: z.string().optional().describe("30d or an ISO timestamp."), until: z.string().optional(), include_automated: z.boolean().optional() },
9
+ }, async (args) => withSession(deps, async (session) => {
10
+ const query = new URLSearchParams({ since: String(args.since ?? "30d") });
11
+ if (args.until)
12
+ query.set("until", String(args.until));
13
+ if (args.include_automated)
14
+ query.set("include_automated", "1");
15
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/usage/people?${query}`);
16
+ if (!response.ok)
17
+ return errorResult(doorFailureText("usage_people", response));
18
+ const coverage = response.body.coverage;
19
+ return textResult(`${coverage?.sessions_extracted ?? 0} of ${coverage?.sessions_observed ?? 0} sessions extracted.`, response.body);
20
+ }));
21
+ }
@@ -39,14 +39,58 @@ export interface TowerVerb {
39
39
  export declare function towerNouns(): string[];
40
40
  /** Every Tower verb a person may type today. */
41
41
  export declare function towerVerbs(): TowerVerb[];
42
+ /**
43
+ * The tools the SECOND MCP server registers (BLI-4047).
44
+ *
45
+ * BLI Memory ships its own server, `bli-memory`
46
+ * (`packages/cockpit-memory-mcp`), because its hooks and its session handling
47
+ * are its own; the four memory tools live there and not on `bli-tower`. This
48
+ * package declares no dependency on it, so — exactly like the collector scan
49
+ * above — the names are read out of its source. A text scan is the weaker
50
+ * check, and it is still strictly better than a census that calls four verbs
51
+ * twinned against tools nothing verifies exist.
52
+ */
53
+ export declare const MEMORY_MCP_SERVER_FILE: string;
54
+ export declare function memoryMcpToolNames(): string[];
42
55
  export interface McpTwin {
43
56
  /** The MCP tool name. Must actually be registered — the suite checks. */
44
57
  tool: string;
45
58
  /** The API door it goes through: the SAME one the CLI verb calls. */
46
59
  door: string;
60
+ /**
61
+ * Which MCP server registers it. Absent means `bli-tower`, this package.
62
+ * BLI-4047: `bli-memory` is the second server we ship, and a twin naming a
63
+ * tool on it while the suite only ever read `bli-tower`'s registry would be
64
+ * a claim nothing checks.
65
+ */
66
+ server?: "bli-tower" | "bli-memory";
47
67
  }
48
68
  /** A CLI verb and the MCP tool that does the same thing, over the same door. */
49
69
  export declare const MCP_TWINS: Record<string, McpTwin>;
70
+ /**
71
+ * Census key → what a person actually types. (BLI-4048)
72
+ *
73
+ * The keys above are derived MECHANICALLY from each noun's `action` union, and
74
+ * for two nouns that union is not the grammar. `settings`'s positional is a
75
+ * SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
76
+ * the parser decides afterwards; `team`'s `device-list` is one action word
77
+ * spelled as two typed words, `team device list`. So `cockpit settings show`
78
+ * and `cockpit team device-list` were rendered into the README's "CLI verb"
79
+ * column and into `cli-caller-identity.md`, and the CLI refused all six.
80
+ *
81
+ * This is the display layer and nothing else: the keys, the twin rule and the
82
+ * suite are untouched. Every spelling here is proven typable —
83
+ * `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
84
+ * own parser, so a wrong entry fails the build rather than misleading a
85
+ * stranger. Where one tool serves several sections (`settings_set` writes any
86
+ * of them) the spelling names ONE of them and the door column names the rest.
87
+ *
88
+ * A verb whose key is already typable is absent from here on purpose; the
89
+ * shorter this map is, the closer the census keys are to the product.
90
+ */
91
+ export declare const TYPED_SPELLING: Record<string, string>;
92
+ /** How a census key is written when a person is being told to type it. */
93
+ export declare function typedSpelling(verb: string): string;
50
94
  /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
51
95
  export declare const TERMINAL_ONLY: Record<string, string>;
52
96
  /**
@@ -138,7 +138,10 @@ function resolveShapeAlias(variant, unionSource) {
138
138
  export function towerVerbs() {
139
139
  const nouns = new Set(towerNouns());
140
140
  const aliases = actionAliases();
141
- const verbs = [{ noun: "memory", action: "log", spelling: "memory log" }];
141
+ const verbs = [
142
+ { noun: "memory", action: "log", spelling: "memory log" },
143
+ ...memoryStoreVerbs(aliases),
144
+ ];
142
145
  const seenNouns = new Set();
143
146
  for (const variant of commandVariants()) {
144
147
  const kind = /kind:\s*"([a-z-]+)"/.exec(variant)?.[1];
@@ -169,8 +172,44 @@ export function towerVerbs() {
169
172
  }
170
173
  return verbs.sort((a, b) => a.spelling.localeCompare(b.spelling));
171
174
  }
175
+ /**
176
+ * `cockpit memory search|save|update|forget` (BLI-4047).
177
+ *
178
+ * `memory` is a COLLECTOR noun — `parseMemoryArgs` lives in
179
+ * `local-args-collector-status.ts` and is not re-exported from
180
+ * `local-args-tower.ts`, because `install` and `status` act on THIS MACHINE.
181
+ * Four of its verbs act on Tower, so the mechanical noun scan cannot see them
182
+ * and this reads them off `MemoryStoreAction` instead. Deliberately NOT a
183
+ * hardcoded list of four: a fifth store verb still has to face the census and
184
+ * name its MCP twin.
185
+ */
186
+ function memoryStoreVerbs(aliases) {
187
+ const actions = aliases.get("MemoryStoreAction") ?? [];
188
+ if (actions.length === 0) {
189
+ throw new Error("MemoryStoreAction not found in the collector's local-args-tower-* files — "
190
+ + "`cockpit memory`'s Tower verbs would drop out of the census silently.");
191
+ }
192
+ return actions.map((action) => ({ noun: "memory", action, spelling: `memory ${action}` }));
193
+ }
194
+ /**
195
+ * The tools the SECOND MCP server registers (BLI-4047).
196
+ *
197
+ * BLI Memory ships its own server, `bli-memory`
198
+ * (`packages/cockpit-memory-mcp`), because its hooks and its session handling
199
+ * are its own; the four memory tools live there and not on `bli-tower`. This
200
+ * package declares no dependency on it, so — exactly like the collector scan
201
+ * above — the names are read out of its source. A text scan is the weaker
202
+ * check, and it is still strictly better than a census that calls four verbs
203
+ * twinned against tools nothing verifies exist.
204
+ */
205
+ export const MEMORY_MCP_SERVER_FILE = resolve(HERE, "../../cockpit-memory-mcp/src/server.ts");
206
+ export function memoryMcpToolNames() {
207
+ const source = readFileSync(MEMORY_MCP_SERVER_FILE, "utf8");
208
+ return [...source.matchAll(/registerTool\(\s*"([a-z_]+)"/g)].map((match) => match[1]);
209
+ }
172
210
  /** A CLI verb and the MCP tool that does the same thing, over the same door. */
173
211
  export const MCP_TWINS = {
212
+ "usage people": { tool: "usage_people", door: "GET /api/usage/people" },
174
213
  "docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
175
214
  "docs tree": { tool: "docs_tree", door: "GET /api/docs/tree" },
176
215
  "docs read": { tool: "docs_read", door: "GET /api/docs/documents/[id]" },
@@ -221,7 +260,14 @@ export const MCP_TWINS = {
221
260
  "notes share": { tool: "notes_share", door: "POST /api/notes/share" },
222
261
  "notes unshare": { tool: "notes_unshare", door: "POST /api/notes/share (share=false)" },
223
262
  "notes move": { tool: "notes_move", door: "POST /api/notes/move" },
263
+ "notes place": { tool: "notes_place", door: "POST /api/notes/place" },
224
264
  "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
265
+ // BLI-4047: the memory STORE at a terminal. The twins live on the second
266
+ // server, `bli-memory`, over the same four doors these CLI verbs call.
267
+ "memory search": { tool: "search_memory", door: "POST /api/memory/search", server: "bli-memory" },
268
+ "memory save": { tool: "save_memory", door: "POST /api/memory/save", server: "bli-memory" },
269
+ "memory update": { tool: "update_memory", door: "POST /api/memory/update", server: "bli-memory" },
270
+ "memory forget": { tool: "forget_memory", door: "POST /api/memory/forget", server: "bli-memory" },
225
271
  "ops status": { tool: "ops_status", door: "GET /api/ops/status" },
226
272
  "ops recompile": { tool: "ops_recompile", door: "POST /api/ops/recompile" },
227
273
  "slack coverage": { tool: "slack_coverage", door: "GET /api/ops/slack/coverage" },
@@ -258,6 +304,39 @@ export const MCP_TWINS = {
258
304
  // header of this file).
259
305
  "jarvis --trace": { tool: "jarvis_trace", door: "GET /api/ops/trace/[id]" },
260
306
  };
307
+ /**
308
+ * Census key → what a person actually types. (BLI-4048)
309
+ *
310
+ * The keys above are derived MECHANICALLY from each noun's `action` union, and
311
+ * for two nouns that union is not the grammar. `settings`'s positional is a
312
+ * SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
313
+ * the parser decides afterwards; `team`'s `device-list` is one action word
314
+ * spelled as two typed words, `team device list`. So `cockpit settings show`
315
+ * and `cockpit team device-list` were rendered into the README's "CLI verb"
316
+ * column and into `cli-caller-identity.md`, and the CLI refused all six.
317
+ *
318
+ * This is the display layer and nothing else: the keys, the twin rule and the
319
+ * suite are untouched. Every spelling here is proven typable —
320
+ * `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
321
+ * own parser, so a wrong entry fails the build rather than misleading a
322
+ * stranger. Where one tool serves several sections (`settings_set` writes any
323
+ * of them) the spelling names ONE of them and the door column names the rest.
324
+ *
325
+ * A verb whose key is already typable is absent from here on purpose; the
326
+ * shorter this map is, the closer the census keys are to the product.
327
+ */
328
+ export const TYPED_SPELLING = {
329
+ "settings show": "settings",
330
+ "settings list": "settings env list",
331
+ "settings set": "settings switches set",
332
+ "settings delete": "settings env delete",
333
+ "team device-list": "team device list",
334
+ "team device-revoke": "team device revoke",
335
+ };
336
+ /** How a census key is written when a person is being told to type it. */
337
+ export function typedSpelling(verb) {
338
+ return TYPED_SPELLING[verb] ?? verb;
339
+ }
261
340
  /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
262
341
  export const TERMINAL_ONLY = {
263
342
  "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)",
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "private": false,
5
- "description": "bli-tower \u2014 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).",
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",
7
7
  "bin": {
8
8
  "bli-cockpit-mcp": "./dist/index.js"
@@ -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.42",
33
+ "@bli-cockpit/telemetry-core": "0.1.43",
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
35
  "zod": "^4.3.6"
36
36
  },