@bli-cockpit/mcp 0.1.29 → 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
- **81 of 84 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
@@ -97,6 +97,7 @@ three tables below.
97
97
  | `cockpit notes list` | `notes_list` | `GET /api/notes/library` |
98
98
  | `cockpit notes move` | `notes_move` | `POST /api/notes/move` |
99
99
  | `cockpit notes paste` | `notes_paste` | `POST /api/notes/upload (text)` |
100
+ | `cockpit notes place` | `notes_place` | `POST /api/notes/place` |
100
101
  | `cockpit notes share` | `notes_share` | `POST /api/notes/share` |
101
102
  | `cockpit notes shelf` | `notes_shelf` | `GET /api/notes/shelf` |
102
103
  | `cockpit notes shelves` | `notes_shelves` | `GET /api/notes/library` |
@@ -111,17 +112,18 @@ three tables below.
111
112
  | `cockpit scout start` | `scout_start` | `POST /api/cockpit/scout (start)` |
112
113
  | `cockpit scout undo` | `scout_undo` | `POST /api/cockpit/scout (undo_dismiss)` |
113
114
  | `cockpit search` | `tower_search` | `GET /api/search` |
114
- | `cockpit settings delete` | `settings_delete` | `DELETE /api/settings/env-blobs` |
115
- | `cockpit settings list` | `settings_list` | `GET /api/settings/env-blobs` |
116
- | `cockpit settings set` | `settings_set` | `POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor` |
117
- | `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` |
118
119
  | `cockpit slack coverage` | `slack_coverage` | `GET /api/ops/slack/coverage` |
119
120
  | `cockpit slack read` | `slack_read` | `POST /api/ops/slack/read` |
120
- | `cockpit team device-list` | `team_device_list` | `GET /api/team/devices` |
121
- | `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` |
122
123
  | `cockpit team invite` | `team_invite` | `POST /api/team/invite` |
123
124
  | `cockpit team members` | `team_members` | `GET /api/team/members` |
124
125
  | `cockpit team role` | `team_role` | `PATCH /api/team/members/[userId]/role` |
126
+ | `cockpit usage people` | `usage_people` | `GET /api/usage/people` |
125
127
  | `cockpit workbook` | `workbook_read` | `GET /api/cockpit/workbook` |
126
128
 
127
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
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
+ }
@@ -67,6 +67,30 @@ export interface McpTwin {
67
67
  }
68
68
  /** A CLI verb and the MCP tool that does the same thing, over the same door. */
69
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;
70
94
  /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
71
95
  export declare const TERMINAL_ONLY: Record<string, string>;
72
96
  /**
@@ -209,6 +209,7 @@ export function memoryMcpToolNames() {
209
209
  }
210
210
  /** A CLI verb and the MCP tool that does the same thing, over the same door. */
211
211
  export const MCP_TWINS = {
212
+ "usage people": { tool: "usage_people", door: "GET /api/usage/people" },
212
213
  "docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
213
214
  "docs tree": { tool: "docs_tree", door: "GET /api/docs/tree" },
214
215
  "docs read": { tool: "docs_read", door: "GET /api/docs/documents/[id]" },
@@ -259,6 +260,7 @@ export const MCP_TWINS = {
259
260
  "notes share": { tool: "notes_share", door: "POST /api/notes/share" },
260
261
  "notes unshare": { tool: "notes_unshare", door: "POST /api/notes/share (share=false)" },
261
262
  "notes move": { tool: "notes_move", door: "POST /api/notes/move" },
263
+ "notes place": { tool: "notes_place", door: "POST /api/notes/place" },
262
264
  "memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
263
265
  // BLI-4047: the memory STORE at a terminal. The twins live on the second
264
266
  // server, `bli-memory`, over the same four doors these CLI verbs call.
@@ -302,6 +304,39 @@ export const MCP_TWINS = {
302
304
  // header of this file).
303
305
  "jarvis --trace": { tool: "jarvis_trace", door: "GET /api/ops/trace/[id]" },
304
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
+ }
305
340
  /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
306
341
  export const TERMINAL_ONLY = {
307
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.29",
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
  },