@bli-cockpit/mcp 0.1.105 → 0.1.107

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
@@ -109,7 +109,7 @@ about what an agent can reach.
109
109
 
110
110
  <!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
111
111
 
112
- **136 of 141 Tower verbs have an MCP twin.**
112
+ **138 of 143 Tower verbs have an MCP twin.**
113
113
  Each tool goes through the SAME door its CLI verb calls, with the same
114
114
  collector device token — never a second route and never a service-role
115
115
  reader. `src/verb-census.test.ts` fails when a verb is in none of the
@@ -244,6 +244,8 @@ three tables below.
244
244
  | `cockpit setup` | `setup_read` | `GET /api/setup` |
245
245
  | `cockpit slack coverage` | `slack_coverage` | `GET /api/ops/slack/coverage` |
246
246
  | `cockpit slack read` | `slack_read` | `POST /api/ops/slack/read` |
247
+ | `cockpit speed` | `speed_read` | `GET /api/* (every rail page's data door, see speed-tools.ts)` |
248
+ | `cockpit speed --seen` | `speed_seen` | `GET /api/speed/seen` |
247
249
  | `cockpit team device doctor` | `team_device_doctor` | `POST /api/ambient/devices/[deviceId]/doctor` |
248
250
  | `cockpit team device list` | `team_device_list` | `GET /api/team/devices` |
249
251
  | `cockpit team device revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
package/dist/server.js CHANGED
@@ -36,6 +36,7 @@ import { registerWorkTools } from "./work-tools.js";
36
36
  import { registerCareersTools } from "./careers-tools.js";
37
37
  import { registerRenameTools } from "./rename-tools.js";
38
38
  import { registerSetupTools } from "./setup-tools.js";
39
+ import { registerSpeedTools } from "./speed-tools.js";
39
40
  import { registerUsageTools } from "./usage-tools.js";
40
41
  import { registerLogsTools } from "./logs-tools.js";
41
42
  import { registerBrainTools } from "./brain-tools.js";
@@ -440,6 +441,8 @@ export function createServer(deps, options = {}) {
440
441
  // BLI-12385: GitHub PR read, and merge / retitle / re-run behind the two-step code.
441
442
  registerGithubTools(server, deps);
442
443
  registerSetupTools(server, { fetchImpl: deps.fetchImpl });
444
+ // BLI-13033: how long each rail page's data door takes, the twin of `cockpit speed`.
445
+ registerSpeedTools(server, { fetchImpl: deps.fetchImpl });
443
446
  registerUsageTools(server, { fetchImpl: deps.fetchImpl });
444
447
  // BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
445
448
  // the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
@@ -5,7 +5,7 @@ export function registerSetupTools(server, deps) {
5
5
  registrarFor(server)("setup_read", {
6
6
  title: "Compare agent setups and outcomes",
7
7
  description: "Read machine setup names and windowed (default 30-day) merged PR volume, tokens per merged PR and session yield. Compare exact emails. Differences are observations, not evidence that a setting causes better results. Missing or stale setup and incomplete token coverage are labeled. Members see their own outcomes; team comparisons require super_admin.",
8
- inputSchema: { since: z.string().max(40).optional().describe("7d, 30d, or an ISO date before now."), person: z.string().max(200).optional().describe("Only this person, by exact email address."), compare: z.string().max(401).optional().describe("Two exact email addresses separated by a comma.") },
8
+ inputSchema: { since: z.string().max(40).optional().describe("7d, 30d, or an ISO date before now."), person: z.string().max(200).optional().describe("Only this person, by exact first name, display name or email address."), compare: z.string().max(401).optional().describe("Two people, by exact first name, display name or email address, separated by a comma (for example edward,ian). An ambiguous name is refused with its candidates.") },
9
9
  }, async (args) => withSession(deps, async (session) => {
10
10
  const query = new URLSearchParams();
11
11
  if (args.since)
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `speed_read` (BLI-13033) — the MCP twin of `cockpit speed`: how long each
3
+ * Tower rail page's data door takes, measured from where this server runs,
4
+ * with this machine's device token.
5
+ *
6
+ * This package declares no dependency on `@bli-cockpit/local-collector`, so
7
+ * the page list and the arithmetic below are COPIES of
8
+ * `cockpit-local-collector/src/commands/speed-pages.ts` and `speed-math.ts`.
9
+ * `speed-tools.test.ts` fails when the two page lists drift.
10
+ *
11
+ * Reads only. Every request is a plain GET on a door the browser also uses;
12
+ * nothing is cached on this side, so the number is Tower's, not ours.
13
+ */
14
+ import { type ToolDeps } from "./tool-result.js";
15
+ export interface SpeedPage {
16
+ name: string;
17
+ label: string;
18
+ path: string;
19
+ note?: string;
20
+ }
21
+ export declare function speedPages(memoryContainer: string): SpeedPage[];
22
+ export declare const SPEED_PAGE_NAMES: string[];
23
+ export interface SpeedSample {
24
+ status: number | null;
25
+ ms: number;
26
+ bytes: number;
27
+ serverTiming: string | null;
28
+ failure?: string;
29
+ }
30
+ export type SpeedOutcome = "ok" | "not_permitted" | "failed";
31
+ export interface PageSpeed {
32
+ page: string;
33
+ label: string;
34
+ door: string;
35
+ outcome: SpeedOutcome;
36
+ cold_ms: number | null;
37
+ p50_ms: number | null;
38
+ p95_ms: number | null;
39
+ runs: number;
40
+ failed_runs: number;
41
+ http_status: number | null;
42
+ bytes: number | null;
43
+ server_timing: string | null;
44
+ server_total_ms: number | null;
45
+ reason?: string;
46
+ note?: string;
47
+ }
48
+ export declare function classifyStatus(status: number | null): SpeedOutcome;
49
+ export declare function percentile(values: readonly number[], p: number): number | null;
50
+ export declare function serverTotalMs(header: string | null): number | null;
51
+ export declare function summarizeSamples(page: {
52
+ name: string;
53
+ label: string;
54
+ path: string;
55
+ note?: string;
56
+ }, samples: readonly SpeedSample[]): PageSpeed;
57
+ export declare function rankPages(pages: readonly PageSpeed[]): PageSpeed[];
58
+ export declare function registerSpeedTools(server: {
59
+ registerTool: (...args: never[]) => unknown;
60
+ }, deps: ToolDeps): void;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * `speed_read` (BLI-13033) — the MCP twin of `cockpit speed`: how long each
3
+ * Tower rail page's data door takes, measured from where this server runs,
4
+ * with this machine's device token.
5
+ *
6
+ * This package declares no dependency on `@bli-cockpit/local-collector`, so
7
+ * the page list and the arithmetic below are COPIES of
8
+ * `cockpit-local-collector/src/commands/speed-pages.ts` and `speed-math.ts`.
9
+ * `speed-tools.test.ts` fails when the two page lists drift.
10
+ *
11
+ * Reads only. Every request is a plain GET on a door the browser also uses;
12
+ * nothing is cached on this side, so the number is Tower's, not ours.
13
+ */
14
+ import { z } from "zod";
15
+ import { callAgentDoor } from "./agent-door.js";
16
+ import { dataResult, doorFailureText, errorResult, registrarFor, withSession } from "./tool-result.js";
17
+ export function speedPages(memoryContainer) {
18
+ return [
19
+ { name: "today", label: "Today (JARVIS home)", path: "/api/jarvis/brief" },
20
+ { name: "meeting-notes", label: "Meeting notes", path: "/api/notes/library" },
21
+ { name: "careers", label: "Careers", path: "/api/careers/applications" },
22
+ { name: "scout", label: "Scout", path: "/api/cockpit/scout" },
23
+ { name: "messages", label: "Messages", path: "/api/msg/channels" },
24
+ { name: "mail", label: "Mail", path: "/api/mail/inbox" },
25
+ { name: "docs", label: "Docs", path: "/api/docs/tree" },
26
+ {
27
+ name: "memory",
28
+ label: "Memory",
29
+ path: `/api/memory/profile?container=${encodeURIComponent(memoryContainer)}`,
30
+ note: "the page reads agent_memories on the server; the profile door is the nearest read",
31
+ },
32
+ { name: "tickets", label: "Tickets", path: "/api/work/issues" },
33
+ { name: "setup", label: "Setup", path: "/api/setup" },
34
+ { name: "usage", label: "Usage", path: "/api/usage/people?since=30d" },
35
+ { name: "calendar", label: "Calendar", path: "/api/cal/week" },
36
+ { name: "workbook", label: "Workbook", path: "/api/cockpit/workbook" },
37
+ { name: "settings", label: "Settings", path: "/api/settings/personal" },
38
+ ];
39
+ }
40
+ // END copied
41
+ export const SPEED_PAGE_NAMES = speedPages("x").map((page) => page.name);
42
+ export function classifyStatus(status) {
43
+ if (status === 403)
44
+ return "not_permitted";
45
+ if (status !== null && status >= 200 && status < 300)
46
+ return "ok";
47
+ return "failed";
48
+ }
49
+ export function percentile(values, p) {
50
+ if (values.length === 0)
51
+ return null;
52
+ const sorted = [...values].sort((a, b) => a - b);
53
+ const rank = Math.max(1, Math.ceil((p / 100) * sorted.length));
54
+ return sorted[Math.min(rank, sorted.length) - 1] ?? null;
55
+ }
56
+ export function serverTotalMs(header) {
57
+ if (!header)
58
+ return null;
59
+ for (const part of header.split(",")) {
60
+ const [name, ...params] = part.trim().split(";");
61
+ if (name?.trim() !== "total")
62
+ continue;
63
+ for (const param of params) {
64
+ const [key, value] = param.trim().split("=");
65
+ if (key === "dur" && value !== undefined && Number.isFinite(Number(value)))
66
+ return Number(value);
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ const round1 = (value) => (value === null ? null : Math.round(value * 10) / 10);
72
+ export function summarizeSamples(page, samples) {
73
+ const base = { page: page.name, label: page.label, door: page.path, ...(page.note ? { note: page.note } : {}) };
74
+ const last = samples[samples.length - 1];
75
+ const good = samples.filter((sample) => classifyStatus(sample.status) === "ok");
76
+ const denied = samples.find((sample) => classifyStatus(sample.status) === "not_permitted");
77
+ if (good.length === 0) {
78
+ const first = samples[0];
79
+ const outcome = denied ? "not_permitted" : "failed";
80
+ return {
81
+ ...base,
82
+ outcome,
83
+ cold_ms: null,
84
+ p50_ms: null,
85
+ p95_ms: null,
86
+ runs: samples.length,
87
+ failed_runs: denied ? 0 : samples.length,
88
+ http_status: (denied ?? first)?.status ?? null,
89
+ bytes: null,
90
+ server_timing: null,
91
+ server_total_ms: null,
92
+ reason: denied
93
+ ? "not permitted for this account"
94
+ : (first?.failure ?? (first?.status === null || first === undefined ? "no_response" : `http_${first.status}`)),
95
+ };
96
+ }
97
+ const cold = samples[0] && classifyStatus(samples[0].status) === "ok" ? samples[0] : null;
98
+ const warm = good.filter((sample) => sample !== cold);
99
+ const timed = (warm.length > 0 ? warm : good).map((sample) => sample.ms);
100
+ const lastGood = good[good.length - 1] ?? last;
101
+ return {
102
+ ...base,
103
+ outcome: "ok",
104
+ cold_ms: round1(cold ? cold.ms : null),
105
+ p50_ms: round1(percentile(timed, 50)),
106
+ p95_ms: round1(percentile(timed, 95)),
107
+ runs: samples.length,
108
+ failed_runs: samples.length - good.length,
109
+ http_status: lastGood?.status ?? null,
110
+ bytes: lastGood?.bytes ?? null,
111
+ server_timing: lastGood?.serverTiming ?? null,
112
+ server_total_ms: serverTotalMs(lastGood?.serverTiming ?? null),
113
+ };
114
+ }
115
+ export function rankPages(pages) {
116
+ const tier = (page) => (page.outcome === "ok" ? 0 : page.outcome === "failed" ? 1 : 2);
117
+ return [...pages].sort((a, b) => {
118
+ if (tier(a) !== tier(b))
119
+ return tier(a) - tier(b);
120
+ return (b.p50_ms ?? 0) - (a.p50_ms ?? 0) || a.page.localeCompare(b.page);
121
+ });
122
+ }
123
+ // END copied
124
+ const REQUEST_DEADLINE_MS = 60_000;
125
+ const MAX_RUNS = 50;
126
+ const DEFAULT_RUNS = 5;
127
+ async function timeOneRead(fetchImpl, session, page) {
128
+ const startedAt = performance.now();
129
+ const controller = new AbortController();
130
+ const timer = setTimeout(() => controller.abort(), REQUEST_DEADLINE_MS);
131
+ timer.unref?.();
132
+ try {
133
+ const response = await fetchImpl(`${session.dashboardUrl}${page.path}`, {
134
+ method: "GET",
135
+ headers: { authorization: `Bearer ${session.deviceToken}`, accept: "application/json" },
136
+ signal: controller.signal,
137
+ });
138
+ const bytes = (await response.arrayBuffer()).byteLength;
139
+ return {
140
+ status: response.status,
141
+ ms: performance.now() - startedAt,
142
+ bytes,
143
+ serverTiming: response.headers.get("server-timing"),
144
+ };
145
+ }
146
+ catch (error) {
147
+ const aborted = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
148
+ return {
149
+ status: null,
150
+ ms: performance.now() - startedAt,
151
+ bytes: 0,
152
+ serverTiming: null,
153
+ failure: aborted ? "turn_timed_out" : "gateway_unreachable",
154
+ };
155
+ }
156
+ finally {
157
+ clearTimeout(timer);
158
+ }
159
+ }
160
+ /**
161
+ * `speed_seen` (BLI-13054) — the MCP twin of `cockpit speed --seen`: what real
162
+ * visits experienced, per page, from the page-view beacons the browser sends.
163
+ * One GET on `/api/speed/seen`; the door counts in the database on the caller's
164
+ * own row policies, so this side only relays and words the answer.
165
+ */
166
+ const SEEN_WINDOW = /^\d{1,4}[mhd]$/;
167
+ export function registerSpeedTools(server, deps) {
168
+ registrarFor(server)("speed_seen", {
169
+ title: "What real visits saw on each Tower page",
170
+ description: "Per Tower page, from real page views: visit count, p50 and p95 milliseconds until real content first showed "
171
+ + "(the last-known screen or the fresh one), the share painted from the last-known screen (snapshot), and median "
172
+ + "HTML and script transfer where the browser reported them. Slowest first. A person sees their own visits; "
173
+ + "`everyone` needs super_admin and is refused for anyone else. Window `since` is <n>m, <n>h or <n>d (default 24h, "
174
+ + "at most 30d). Read only. Twin of `cockpit speed --seen`.",
175
+ inputSchema: {
176
+ since: z.string().max(10).optional().describe("Window such as 90m, 24h or 7d (default 24h, at most 30d)."),
177
+ everyone: z.boolean().optional().describe("Every person's visits (super_admin only)."),
178
+ pages: z.array(z.enum(SPEED_PAGE_NAMES)).max(14).optional()
179
+ .describe("Only these pages. Omit for every page."),
180
+ },
181
+ }, async (args) => withSession(deps, async (session) => {
182
+ const since = typeof args.since === "string" ? args.since.trim().toLowerCase() : "24h";
183
+ if (!SEEN_WINDOW.test(since))
184
+ return errorResult("speed_seen: `since` takes <n>m, <n>h or <n>d, for example 24h.");
185
+ const everyone = args.everyone === true;
186
+ const query = new URLSearchParams({ since });
187
+ if (everyone)
188
+ query.set("everyone", "1");
189
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/speed/seen?${query}`);
190
+ if (!response.ok)
191
+ return errorResult(doorFailureText("speed_seen", response));
192
+ const asked = Array.isArray(args.pages) ? args.pages : [];
193
+ const body = response.body;
194
+ const pages = (body.pages ?? [])
195
+ .filter((page) => asked.length === 0 || asked.includes(page.page))
196
+ .sort((a, b) => b.p50_ms - a.p50_ms || a.page.localeCompare(b.page));
197
+ const lines = pages.map((page, index) => `${index + 1}. ${page.page} ${page.visits} visit${page.visits === 1 ? "" : "s"} p50 ${Math.round(page.p50_ms)} ms `
198
+ + `p95 ${Math.round(page.p95_ms)} ms snapshot ${Math.round(page.snapshot_share * 100)}%`);
199
+ const who = everyone ? "everyone's visits" : "your visits";
200
+ return dataResult(pages.length === 0
201
+ ? `No page views recorded in the last ${since} (${who}).`
202
+ : `Real page views over the last ${since} (${who}), slowest first:\n${lines.join("\n")}`, { ok: true, since, everyone, pages });
203
+ }));
204
+ registrarFor(server)("speed_read", {
205
+ title: "Time every Tower page's data door",
206
+ description: "How long each Tower rail page's data door takes, measured from where this server runs with this machine's "
207
+ + "device token: per page the first (cold) run, p50 and p95 over the warm runs, HTTP status, response bytes and "
208
+ + "the door's own Server-Timing total, slowest first. A page the account may not open reads `not_permitted`, "
209
+ + "not a failure. Times the data door, not the page's HTML or paint. Read only. Twin of `cockpit speed`.",
210
+ inputSchema: {
211
+ pages: z.array(z.enum(SPEED_PAGE_NAMES)).max(14).optional()
212
+ .describe("Only these pages. Omit for every page."),
213
+ runs: z.number().int().min(1).max(MAX_RUNS).optional().describe("Requests per page (default 5)."),
214
+ container: z.string().min(1).max(200).optional()
215
+ .describe("The memory container tag to time the Memory page with. Without it the Memory page is left out."),
216
+ },
217
+ }, async (args) => withSession(deps, async (session) => {
218
+ const runs = typeof args.runs === "number" ? args.runs : DEFAULT_RUNS;
219
+ const asked = Array.isArray(args.pages) ? args.pages : [];
220
+ const container = typeof args.container === "string" ? args.container : null;
221
+ const skipped = [];
222
+ const chosen = speedPages(container ?? "-").filter((page) => {
223
+ if (asked.length > 0 && !asked.includes(page.name))
224
+ return false;
225
+ if (page.name === "memory" && container === null) {
226
+ skipped.push("memory");
227
+ return false;
228
+ }
229
+ return true;
230
+ });
231
+ if (chosen.length === 0) {
232
+ return errorResult("speed_read has nothing to time: the Memory page needs a `container` tag.");
233
+ }
234
+ const results = [];
235
+ for (const page of chosen) {
236
+ const samples = [];
237
+ for (let run = 0; run < runs; run += 1) {
238
+ const sample = await timeOneRead(deps.fetchImpl, session, page);
239
+ samples.push(sample);
240
+ if (classifyStatus(sample.status) === "not_permitted")
241
+ break;
242
+ }
243
+ results.push(summarizeSamples(page, samples));
244
+ }
245
+ const ranked = rankPages(results);
246
+ const failed = ranked.filter((page) => page.outcome === "failed").length;
247
+ const lines = ranked.map((page, index) => page.outcome === "ok"
248
+ ? `${index + 1}. ${page.page} cold ${Math.round(page.cold_ms ?? 0)} ms p50 ${Math.round(page.p50_ms ?? 0)} ms p95 ${Math.round(page.p95_ms ?? 0)} ms HTTP ${page.http_status} ${page.bytes} B${page.server_total_ms === null ? "" : ` server ${Math.round(page.server_total_ms)} ms`}`
249
+ : `${index + 1}. ${page.page} ${page.outcome === "not_permitted" ? "not permitted" : `FAILED (${page.reason})`}`);
250
+ return dataResult(`Tower page speed, slowest first (${runs} run${runs === 1 ? "" : "s"} per page; first is cold, p50/p95 over warm runs):\n${lines.join("\n")}`
251
+ + (skipped.length ? `\nLeft out: ${skipped.join(", ")} (pass container).` : ""), { ok: failed === 0, runs, pages: ranked, skipped });
252
+ }));
253
+ }
@@ -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", "rename_pending", "github_pr", "setup_read", "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", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_rules", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
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", "rename_pending", "github_pr", "setup_read", "speed_read", "speed_seen", "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", "glossary_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_recordings", "notes_recording", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_rules", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status"];
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).
@@ -32,6 +32,8 @@ export const READ_TOOLS = [
32
32
  "rename_pending",
33
33
  "github_pr",
34
34
  "setup_read",
35
+ "speed_read",
36
+ "speed_seen",
35
37
  "usage_people",
36
38
  "usage_sessions",
37
39
  "mail_accounts",
@@ -242,6 +242,10 @@ export const MCP_TWINS = {
242
242
  "brain status": { tool: "brain_status", door: "GET /api/brain/incidents" },
243
243
  "usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
244
244
  "setup": { tool: "setup_read", door: "GET /api/setup" },
245
+ // BLI-13033: times the data door of every rail page; the twin runs the same GETs.
246
+ "speed": { tool: "speed_read", door: "GET /api/* (every rail page's data door, see speed-tools.ts)" },
247
+ // BLI-13054: a mode flag, entered by hand like `jarvis --trace`.
248
+ "speed --seen": { tool: "speed_seen", door: "GET /api/speed/seen" },
245
249
  "usage people": { tool: "usage_people", door: "GET /api/usage/people" },
246
250
  "glossary list": { tool: "glossary_list", door: "GET /api/glossary" },
247
251
  "glossary add": { tool: "glossary_add", door: "POST /api/glossary" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.105",
3
+ "version": "0.1.107",
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",
@@ -37,7 +37,7 @@
37
37
  "readme": "npm run build && node scripts/write-readme-census.mjs"
38
38
  },
39
39
  "dependencies": {
40
- "@bli-cockpit/telemetry-core": "0.1.66",
40
+ "@bli-cockpit/telemetry-core": "0.1.67",
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
42
  "zod": "^4.3.6"
43
43
  },