@bli-cockpit/mcp 0.1.2 → 0.1.4

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.
Files changed (41) hide show
  1. package/README.md +168 -2
  2. package/dist/agent-door.d.ts +8 -1
  3. package/dist/agent-door.js +7 -2
  4. package/dist/brief-tools.d.ts +26 -0
  5. package/dist/brief-tools.js +160 -0
  6. package/dist/brief-write-tools.d.ts +37 -0
  7. package/dist/brief-write-tools.js +178 -0
  8. package/dist/docs-msg-tools.d.ts +13 -7
  9. package/dist/docs-msg-tools.js +141 -25
  10. package/dist/jarvis-answer-envelope.d.ts +13 -1
  11. package/dist/jarvis-answer-envelope.js +2 -0
  12. package/dist/jarvis-door.js +16 -1
  13. package/dist/jarvis-tools.d.ts +18 -10
  14. package/dist/jarvis-tools.js +28 -17
  15. package/dist/notes-tools.d.ts +33 -0
  16. package/dist/notes-tools.js +143 -0
  17. package/dist/notes-write-tools.d.ts +34 -0
  18. package/dist/notes-write-tools.js +211 -0
  19. package/dist/ops-tools.d.ts +23 -0
  20. package/dist/ops-tools.js +212 -0
  21. package/dist/pages-tools.d.ts +28 -0
  22. package/dist/pages-tools.js +190 -0
  23. package/dist/readme-census.d.ts +18 -0
  24. package/dist/readme-census.js +79 -0
  25. package/dist/search-tool.d.ts +54 -0
  26. package/dist/search-tool.js +134 -0
  27. package/dist/server.d.ts +1 -1
  28. package/dist/server.js +34 -1
  29. package/dist/settings-tools.d.ts +29 -0
  30. package/dist/settings-tools.js +151 -0
  31. package/dist/settings-write-tools.d.ts +47 -0
  32. package/dist/settings-write-tools.js +183 -0
  33. package/dist/team-write-tools.d.ts +39 -0
  34. package/dist/team-write-tools.js +141 -0
  35. package/dist/tool-result.d.ts +86 -0
  36. package/dist/tool-result.js +105 -0
  37. package/dist/verb-census.d.ts +22 -0
  38. package/dist/verb-census.js +108 -6
  39. package/dist/work-tools.d.ts +2 -7
  40. package/dist/work-tools.js +35 -25
  41. package/package.json +5 -4
@@ -0,0 +1,105 @@
1
+ /**
2
+ * What every agent-door tool on this server needs, written once (BLI-3756).
3
+ *
4
+ * `docs-msg-tools.ts` and `work-tools.ts` each carried their own copy of these
5
+ * four helpers. Batch 1 of the MCP twin build added five more surfaces, and
6
+ * seven copies of "what does a refused door say?" is seven places for the
7
+ * answer to drift — so the copies became this module and nothing else decides
8
+ * how a tool names a refusal.
9
+ *
10
+ * The one rule worth stating out loud: **a refusal keeps the DOOR's own
11
+ * words.** The reason label and the sentence are both computed on the server,
12
+ * where the outcome is known, and relayed here verbatim. A second wording in
13
+ * this package would be a second thing to keep in step, and the agent reading
14
+ * it would have no way to tell which one was true.
15
+ */
16
+ import { z } from "zod";
17
+ import { loadAgentDoorSession } from "./agent-door-session.js";
18
+ export function textResult(text, structured) {
19
+ return { content: [{ type: "text", text }], ...(structured ? { structuredContent: structured } : {}) };
20
+ }
21
+ export function errorResult(text) {
22
+ return { isError: true, content: [{ type: "text", text }] };
23
+ }
24
+ /**
25
+ * The session is loaded fresh per call (a cheap file read) rather than once at
26
+ * server startup: this package still serves the legacy event-stream tools on a
27
+ * machine with no collector pairing at all, so a missing session must fail the
28
+ * ONE call that needed it, by name, and never the whole process.
29
+ */
30
+ export async function withSession(deps, run) {
31
+ const loadSession = deps.loadSession ?? loadAgentDoorSession;
32
+ const loaded = loadSession();
33
+ if (!loaded.ok) {
34
+ return errorResult(`This machine is not paired with Tower (${loaded.reason}). ${loaded.message}`);
35
+ }
36
+ return run(loaded.session);
37
+ }
38
+ /**
39
+ * The door's own reason label and its own sentence.
40
+ *
41
+ * Two answer shapes are read because two exist on the server and both are
42
+ * deliberate: the ingest-style routes answer `{reason, message}`, while every
43
+ * `/api/notes/**` door answers in the shape the BROWSER renders — a `headline`
44
+ * plus `lines` — because the page and the terminal read the same body
45
+ * (`commands/notes-door.ts` says why). Reading both here rather than
46
+ * rephrasing either keeps one meaning per field.
47
+ */
48
+ export function doorFailureText(door, response) {
49
+ if (response.transportError) {
50
+ return `Tower could not be reached for ${door} (${response.transportError}). Nothing was read or written.`;
51
+ }
52
+ return `Tower refused ${door} (${doorReason(response.body)}): ${doorSentence(response.body, response.status)}`;
53
+ }
54
+ /** The label a script should branch on. `unknown_error` only when the door named none. */
55
+ export function doorReason(body) {
56
+ for (const key of ["reason", "error", "code"]) {
57
+ const value = body[key];
58
+ if (typeof value === "string" && value.trim() !== "")
59
+ return value;
60
+ }
61
+ return "unknown_error";
62
+ }
63
+ function doorSentence(body, status) {
64
+ const headline = typeof body["headline"] === "string" ? body["headline"] : null;
65
+ const message = typeof body["message"] === "string" ? body["message"] : null;
66
+ const lines = Array.isArray(body["lines"])
67
+ ? body["lines"].filter((line) => typeof line === "string")
68
+ : [];
69
+ const said = [headline ?? message, ...lines].filter(Boolean);
70
+ return said.length > 0 ? said.join(" ") : `Tower answered ${status}.`;
71
+ }
72
+ /** The one cast, so no tool family repeats it. */
73
+ export function registrarFor(server) {
74
+ return server.registerTool.bind(server);
75
+ }
76
+ /** `?a=1&b=2`, or "" when nothing was asked for. Never a bare `?`. */
77
+ export function queryString(params) {
78
+ const rendered = params.toString();
79
+ return rendered === "" ? "" : `?${rendered}`;
80
+ }
81
+ // ─────────────────────────────────────────────────────────────────────────
82
+ // Deliberate acts (BLI-3756 batch 2)
83
+ // ─────────────────────────────────────────────────────────────────────────
84
+ /**
85
+ * The `--yes` of this surface.
86
+ *
87
+ * `cockpit` refuses a share, a revoke, a role change or a delete that nobody
88
+ * confirmed: `--yes`, or a person at a keyboard, and nothing else
89
+ * (`commands/settings.ts` `confirmDestructive`). An MCP client has no
90
+ * keyboard, so the only half that survives is the explicit flag — and it must
91
+ * survive, or the surface a model reaches would be the one surface where a
92
+ * revoke needs no second thought. The reason label is the CLI's own,
93
+ * `confirmation_required`, so a script branching on it reads the same word
94
+ * from either door.
95
+ */
96
+ export const CONFIRM_INPUT = z
97
+ .boolean()
98
+ .optional()
99
+ .describe("Must be true. This act is deliberate and is refused without it — the same rule as the CLI's --yes.");
100
+ /** `null` when the caller confirmed; the refusal itself when they did not. */
101
+ export function unconfirmed(args, sentence) {
102
+ if (args.confirm === true)
103
+ return null;
104
+ return errorResult(`Refused (confirmation_required): ${sentence} Nothing was changed.`);
105
+ }
@@ -39,3 +39,25 @@ 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
+ export interface McpTwin {
43
+ /** The MCP tool name. Must actually be registered — the suite checks. */
44
+ tool: string;
45
+ /** The API door it goes through: the SAME one the CLI verb calls. */
46
+ door: string;
47
+ }
48
+ /** A CLI verb and the MCP tool that does the same thing, over the same door. */
49
+ export declare const MCP_TWINS: Record<string, McpTwin>;
50
+ /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
51
+ export declare const TERMINAL_ONLY: Record<string, string>;
52
+ /**
53
+ * Verbs owed a twin, with who owes it. This list should only ever shrink.
54
+ *
55
+ * **It is empty, and that is the point** (BLI-3756, batch 2, 2026-09-05).
56
+ * Every `cockpit <noun> <verb>` that talks to Tower now has an MCP tool over
57
+ * the same door — reads in batch 1, writes in batch 2 — so BLI-3706's rule
58
+ * holds without an exception: a Tower surface an agent cannot reach is as good
59
+ * as dead, and there is no surface left in that state. A new entry here is a
60
+ * new debt, not a normal state; it needs a ticket in its reason and it must
61
+ * come back out.
62
+ */
63
+ export declare const AWAITING_TWIN: Record<string, string>;
@@ -63,13 +63,34 @@ function actionAliases() {
63
63
  * The `LocalCommand` variants, split the way the union is written: one chunk
64
64
  * per ` | {` at the top level of the type.
65
65
  */
66
+ /**
67
+ * The `LocalCommand` union, wherever it currently lives.
68
+ *
69
+ * It moved from `local-args.ts` to `local-command-shapes.ts` in BLI-3728, when
70
+ * `local-args.ts` crossed the 700-line readability ceiling and was split into
71
+ * "which parser does this argv go to" (there) and "what does a parsed command
72
+ * look like" (the sibling). Both files are read rather than one, because a
73
+ * census that silently finds fewer verbs than exist is worse than no census —
74
+ * this function throwing when it can find the union in NEITHER file is the
75
+ * whole point, and hardcoding one filename is how it would stop throwing while
76
+ * being wrong.
77
+ */
66
78
  function commandVariants() {
67
- const source = readFileSync(join(COLLECTOR_COMMANDS_DIR, "local-args.ts"), "utf8");
68
- const start = source.indexOf("export type LocalCommand =");
69
- if (start < 0)
70
- throw new Error("LocalCommand union not found in local-args.ts");
71
- const end = source.indexOf("\nexport ", start + 10);
72
- return source.slice(start, end < 0 ? undefined : end).split(/\n\s{2}\|\s/);
79
+ for (const file of ["local-command-shapes.ts", "local-args.ts"]) {
80
+ let source;
81
+ try {
82
+ source = readFileSync(join(COLLECTOR_COMMANDS_DIR, file), "utf8");
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ const start = source.indexOf("export type LocalCommand =");
88
+ if (start < 0)
89
+ continue;
90
+ const end = source.indexOf("\nexport ", start + 10);
91
+ return source.slice(start, end < 0 ? undefined : end).split(/\n\s{2}\|\s/);
92
+ }
93
+ throw new Error("LocalCommand union not found in local-command-shapes.ts or local-args.ts");
73
94
  }
74
95
  /** Every Tower verb a person may type today. */
75
96
  export function towerVerbs() {
@@ -106,3 +127,84 @@ export function towerVerbs() {
106
127
  }
107
128
  return verbs.sort((a, b) => a.spelling.localeCompare(b.spelling));
108
129
  }
130
+ /** A CLI verb and the MCP tool that does the same thing, over the same door. */
131
+ export const MCP_TWINS = {
132
+ "docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
133
+ "docs tree": { tool: "docs_tree", door: "GET /api/docs/tree" },
134
+ "docs read": { tool: "docs_read", door: "GET /api/docs/documents/[id]" },
135
+ "docs create": { tool: "docs_create", door: "POST /api/docs/documents" },
136
+ "docs update": { tool: "docs_update", door: "PATCH /api/docs/documents/[id]" },
137
+ "msg channels": { tool: "msg_channels", door: "GET /api/msg/channels" },
138
+ "msg create": { tool: "msg_create_channel", door: "POST /api/msg/channels" },
139
+ "msg dm": { tool: "msg_dm", door: "POST /api/msg/channels (dm)" },
140
+ "msg read": { tool: "msg_read", door: "GET /api/msg/channels/[id]/messages" },
141
+ "msg thread": { tool: "msg_thread", door: "GET /api/msg/channels/[id]/messages?thread_parent_id=" },
142
+ "msg send": { tool: "msg_send", door: "POST /api/msg/channels/[id]/messages" },
143
+ "issue list": { tool: "work_list_issues", door: "GET /api/work/issues" },
144
+ "issue show": { tool: "work_get_issue", door: "GET /api/work/issues/[id]" },
145
+ "issue create": { tool: "work_create_issue", door: "POST /api/work/issues" },
146
+ "issue update": { tool: "work_update_issue", door: "PATCH /api/work/issues/[id]" },
147
+ "issue move": { tool: "work_move_issue", door: "POST /api/work/issues/[id]/state" },
148
+ "issue comment": { tool: "work_comment_issue", door: "POST /api/work/issues/[id]/comments" },
149
+ "issue history": { tool: "work_issue_history", door: "GET /api/work/issues/[id]/history" },
150
+ "project list": { tool: "work_list_projects", door: "GET /api/work/projects" },
151
+ "brief read": { tool: "brief_read", door: "GET /api/jarvis/brief" },
152
+ "brief history": { tool: "brief_history", door: "GET /api/jarvis/brief?history=1" },
153
+ "brief status": { tool: "brief_status", door: "GET /api/ops/brief-status" },
154
+ "brief rewrite": { tool: "brief_rewrite", door: "POST /api/jarvis/recompile" },
155
+ correct: { tool: "brief_correct", door: "POST /api/jarvis/corrections" },
156
+ "notes list": { tool: "notes_list", door: "GET /api/notes/library" },
157
+ "notes show": { tool: "notes_show", door: "GET /api/notes/library/[id]" },
158
+ "notes shelf": { tool: "notes_shelf", door: "GET /api/notes/shelf" },
159
+ "notes shelves": { tool: "notes_shelves", door: "GET /api/notes/library" },
160
+ "notes upload": { tool: "notes_upload", door: "POST /api/notes/upload" },
161
+ "notes paste": { tool: "notes_paste", door: "POST /api/notes/upload (text)" },
162
+ "notes share": { tool: "notes_share", door: "POST /api/notes/share" },
163
+ "notes unshare": { tool: "notes_unshare", door: "POST /api/notes/share (share=false)" },
164
+ "notes move": { tool: "notes_move", door: "POST /api/notes/move" },
165
+ "ops status": { tool: "ops_status", door: "GET /api/ops/status" },
166
+ "ops recompile": { tool: "ops_recompile", door: "POST /api/ops/recompile" },
167
+ "slack coverage": { tool: "slack_coverage", door: "GET /api/ops/slack/coverage" },
168
+ "slack read": { tool: "slack_read", door: "POST /api/ops/slack/read" },
169
+ "settings show": { tool: "settings_show", door: "GET /api/settings/* + /api/team/members" },
170
+ "settings list": { tool: "settings_list", door: "GET /api/settings/env-blobs" },
171
+ "settings set": { tool: "settings_set", door: "POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor" },
172
+ "settings delete": { tool: "settings_delete", door: "DELETE /api/settings/env-blobs" },
173
+ "team members": { tool: "team_members", door: "GET /api/team/members" },
174
+ "team device-list": { tool: "team_device_list", door: "GET /api/team/devices" },
175
+ "team invite": { tool: "team_invite", door: "POST /api/team/invite" },
176
+ "team role": { tool: "team_role", door: "PATCH /api/team/members/[userId]/role" },
177
+ "team device-revoke": { tool: "team_device_revoke", door: "POST /api/ambient/devices/[deviceId]/revoke" },
178
+ "model show": { tool: "model_show", door: "GET /api/settings/jarvis-model" },
179
+ "model set": { tool: "model_set", door: "POST /api/settings/jarvis-model" },
180
+ "scout board": { tool: "scout_board", door: "GET /api/cockpit/scout" },
181
+ "scout start": { tool: "scout_start", door: "POST /api/cockpit/scout (start)" },
182
+ "scout dismiss": { tool: "scout_dismiss", door: "POST /api/cockpit/scout (dismiss)" },
183
+ "scout undo": { tool: "scout_undo", door: "POST /api/cockpit/scout (undo_dismiss)" },
184
+ workbook: { tool: "workbook_read", door: "GET /api/cockpit/workbook" },
185
+ // BLI-3728: one search over documents, messages, issues, meeting notes and
186
+ // memory. `cockpit search` takes no action word — the whole verb is the noun
187
+ // — so it is keyed on the noun alone, the same way `jarvis` and `workbook`
188
+ // are.
189
+ search: { tool: "tower_search", door: "GET /api/search" },
190
+ jarvis: { tool: "jarvis_ask", door: "POST /api/jarvis/cli" },
191
+ // Entered by hand: a mode flag, not an `action` the parser names (see the
192
+ // header of this file).
193
+ "jarvis --trace": { tool: "jarvis_trace", door: "GET /api/ops/trace/[id]" },
194
+ };
195
+ /** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
196
+ export const TERMINAL_ONLY = {
197
+ "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)",
198
+ };
199
+ /**
200
+ * Verbs owed a twin, with who owes it. This list should only ever shrink.
201
+ *
202
+ * **It is empty, and that is the point** (BLI-3756, batch 2, 2026-09-05).
203
+ * Every `cockpit <noun> <verb>` that talks to Tower now has an MCP tool over
204
+ * the same door — reads in batch 1, writes in batch 2 — so BLI-3706's rule
205
+ * holds without an exception: a Tower surface an agent cannot reach is as good
206
+ * as dead, and there is no surface left in that state. A new entry here is a
207
+ * new debt, not a normal state; it needs a ticket in its reason and it must
208
+ * come back out.
209
+ */
210
+ export const AWAITING_TWIN = {};
@@ -14,13 +14,8 @@
14
14
  * per call, so a machine with no `cockpit login` pairing still serves this
15
15
  * server's other tools and only a `work_` call fails, by name.
16
16
  */
17
- import { loadAgentDoorSession } from "./agent-door-session.js";
18
- import { type FetchImpl } from "./agent-door.js";
19
- export interface WorkDeps {
20
- fetchImpl: FetchImpl;
21
- /** Injectable for tests; defaults to reading `~/.config/bli-cockpit/session.json`. */
22
- loadSession?: typeof loadAgentDoorSession;
23
- }
17
+ import { type ToolDeps } from "./tool-result.js";
18
+ export type WorkDeps = ToolDeps;
24
19
  /** The closed state vocabulary, verbatim from `lib/work/api-doors.ts` `WORK_ISSUE_STATES`. */
25
20
  export declare const WORK_ISSUE_STATES: readonly ["backlog", "todo", "in_progress", "in_review", "done", "canceled"];
26
21
  export declare function registerWorkTools(server: {
@@ -15,8 +15,8 @@
15
15
  * server's other tools and only a `work_` call fails, by name.
16
16
  */
17
17
  import { z } from "zod";
18
- import { loadAgentDoorSession } from "./agent-door-session.js";
19
18
  import { callAgentDoor } from "./agent-door.js";
19
+ import { doorFailureText, errorResult, registrarFor, textResult, withSession, } from "./tool-result.js";
20
20
  /** The closed state vocabulary, verbatim from `lib/work/api-doors.ts` `WORK_ISSUE_STATES`. */
21
21
  export const WORK_ISSUE_STATES = [
22
22
  "backlog",
@@ -26,30 +26,17 @@ export const WORK_ISSUE_STATES = [
26
26
  "done",
27
27
  "canceled",
28
28
  ];
29
- function textResult(text, structured) {
30
- return { content: [{ type: "text", text }], ...(structured ? { structuredContent: structured } : {}) };
31
- }
32
- function errorResult(text) {
33
- return { isError: true, content: [{ type: "text", text }] };
34
- }
35
- async function withSession(deps, run) {
36
- const loadSession = deps.loadSession ?? loadAgentDoorSession;
37
- const loaded = loadSession();
38
- if (!loaded.ok)
39
- return errorResult(`This machine is not paired with Tower (${loaded.reason}). ${loaded.message}`);
40
- return run(loaded.session);
41
- }
42
- function doorFailureText(door, response) {
43
- if (response.transportError) {
44
- return `Tower could not be reached for ${door} (${response.transportError}). Nothing was read or written.`;
29
+ /**
30
+ * A row the Linear import could not give state names to says which it is.
31
+ * Printing "(none) -> (none)" is honest and useless; naming the gap keeps
32
+ * nobody reading a real gap as a broken tool (`commands/issue.ts` says the
33
+ * same thing in the same words).
34
+ */
35
+ function historyChange(row) {
36
+ if (row.from_value === null && row.to_value === null) {
37
+ return `(no state names recorded — imported from ${row.source ?? "elsewhere"})`;
45
38
  }
46
- const reason = typeof response.body.reason === "string"
47
- ? response.body.reason
48
- : typeof response.body.error === "string"
49
- ? response.body.error
50
- : "unknown_error";
51
- const message = typeof response.body.message === "string" ? response.body.message : `Tower answered ${response.status}.`;
52
- return `Tower refused ${door} (${reason}): ${message}`;
39
+ return `${row.from_value ?? "(none)"} -> ${row.to_value ?? "(none)"}`;
53
40
  }
54
41
  /** A project id from a project NAME or a uuid — exact after case-folding, never fuzzy. */
55
42
  async function resolveProjectId(deps, session, ref) {
@@ -67,7 +54,7 @@ const ISSUE_REF = z
67
54
  .max(200)
68
55
  .describe("A BLI-#### identifier (e.g. BLI-3654) or an issue's uuid. Both work.");
69
56
  export function registerWorkTools(server, deps) {
70
- const register = server.registerTool.bind(server);
57
+ const register = registrarFor(server);
71
58
  register("work_list_issues", {
72
59
  title: "List Tower issues",
73
60
  description: "Issues you may see, most recently updated first. Filter by state, assignee (\"me\", \"unassigned\", or a uuid) "
@@ -219,6 +206,29 @@ export function registerWorkTools(server, deps) {
219
206
  const comment = response.body.comment;
220
207
  return textResult(`Commented on ${ref} (${comment?.id ?? "?"}).`, { comment });
221
208
  }));
209
+ register("work_issue_history", {
210
+ title: "Read a Tower issue's history",
211
+ description: "Every recorded change to one issue — state moves, assignee changes — newest first, with who made it and "
212
+ + "when. History rows are never rewritten; a row imported without state names says so rather than showing "
213
+ + "\"(none) -> (none)\".",
214
+ inputSchema: {
215
+ id: ISSUE_REF,
216
+ limit: z.number().int().min(1).max(500).optional(),
217
+ },
218
+ }, async (args) => withSession(deps, async (session) => {
219
+ const ref = String(args.id ?? "");
220
+ const query = typeof args.limit === "number" ? `?limit=${args.limit}` : "";
221
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/work/issues/${encodeURIComponent(ref)}/history${query}`);
222
+ if (!response.ok)
223
+ return errorResult(doorFailureText("work_issue_history", response));
224
+ const history = (Array.isArray(response.body.history) ? response.body.history : []);
225
+ const lines = history
226
+ .map((row) => `${row.occurred_at ?? "?"} ${(row.change_type ?? "?").padEnd(9)} ${historyChange(row)} `
227
+ + `${row.actor_name ?? ""}`)
228
+ .map((line) => line.trimEnd())
229
+ .join("\n");
230
+ return textResult(`${history.length} change(s) on ${ref}.${lines ? `\n${lines}` : ""}`, { history });
231
+ }));
222
232
  register("work_list_projects", {
223
233
  title: "List Tower projects",
224
234
  description: "The projects issues are filed under. Use a project's name with work_list_issues or work_create_issue.",
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
- "description": "bli-tower — an MCP server over BLI Cockpit's agent doors: JARVIS (jarvis_*), documents (docs_*), channels (msg_*), issues (work_*), 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"
@@ -25,10 +25,11 @@
25
25
  "typecheck": "tsc --noEmit",
26
26
  "pretest": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/telemetry-core",
27
27
  "test": "vitest run",
28
- "start": "node dist/index.js"
28
+ "start": "node dist/index.js",
29
+ "readme": "npm run build && node scripts/write-readme-census.mjs"
29
30
  },
30
31
  "dependencies": {
31
- "@bli-cockpit/telemetry-core": "0.1.28",
32
+ "@bli-cockpit/telemetry-core": "0.1.29",
32
33
  "@modelcontextprotocol/sdk": "^1.29.0",
33
34
  "zod": "^4.3.6"
34
35
  },