@bli-cockpit/mcp 0.1.115 → 0.1.117
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 +12 -1
- package/dist/approvals-tools.d.ts +4 -0
- package/dist/approvals-tools.js +191 -0
- package/dist/image-tools.d.ts +4 -0
- package/dist/image-tools.js +114 -0
- package/dist/memory-twins.d.ts +2 -0
- package/dist/memory-twins.js +5 -0
- package/dist/server.js +6 -0
- package/dist/tool-annotations.d.ts +1 -1
- package/dist/tool-annotations.js +1 -0
- package/dist/tool-catalog.d.ts +2 -2
- package/dist/tool-catalog.js +15 -0
- package/dist/verb-census-spellings.js +2 -0
- package/dist/verb-census.js +12 -3
- package/package.json +2 -2
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
|
-
**
|
|
112
|
+
**171 of 176 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
|
|
@@ -117,6 +117,15 @@ three tables below.
|
|
|
117
117
|
|
|
118
118
|
| CLI verb | MCP tool | Door |
|
|
119
119
|
| --- | --- | --- |
|
|
120
|
+
| `cockpit approvals approve` | `approvals_approve` | `POST /api/approvals/[id]/approve` |
|
|
121
|
+
| `cockpit approvals create` | `approvals_create` | `POST /api/approvals` |
|
|
122
|
+
| `cockpit approvals edit` | `approvals_edit` | `PATCH /api/approvals/[id]` |
|
|
123
|
+
| `cockpit approvals list` | `approvals_list` | `GET /api/approvals` |
|
|
124
|
+
| `cockpit approvals reject` | `approvals_reject` | `POST /api/approvals/[id]/reject` |
|
|
125
|
+
| `cockpit approvals rules` | `approvals_rules` | `GET /api/approvals/rules` |
|
|
126
|
+
| `cockpit approvals rules create` | `approvals_rules_create` | `POST /api/approvals/rules` |
|
|
127
|
+
| `cockpit approvals rules revoke` | `approvals_rules_revoke` | `POST /api/approvals/rules/[id]/revoke` |
|
|
128
|
+
| `cockpit approvals show` | `approvals_show` | `GET /api/approvals/[id]` |
|
|
120
129
|
| `cockpit brain retry` | `brain_retry` | `POST /api/brain/incidents/retry` |
|
|
121
130
|
| `cockpit brain status` | `brain_status` | `GET /api/brain/incidents` |
|
|
122
131
|
| `cockpit brief done` | `brief_done` | `POST /api/jarvis/ticks` |
|
|
@@ -167,6 +176,8 @@ three tables below.
|
|
|
167
176
|
| `cockpit glossary add` | `glossary_add` | `POST /api/glossary` |
|
|
168
177
|
| `cockpit glossary list` | `glossary_list` | `GET /api/glossary` |
|
|
169
178
|
| `cockpit glossary remove` | `glossary_remove` | `POST /api/glossary` |
|
|
179
|
+
| `cockpit image describe` | `image_describe` | `POST /api/ambient/images` |
|
|
180
|
+
| `cockpit image show` | `image_show` | `GET /api/ambient/images` |
|
|
170
181
|
| `cockpit issue board` | `work_board` | `GET /api/work/board` |
|
|
171
182
|
| `cockpit issue comment` | `work_comment_issue` | `POST /api/work/issues/[id]/comments` |
|
|
172
183
|
| `cockpit issue create` | `work_create_issue` | `POST /api/work/issues` |
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approvals inbox over MCP (BLI-16945): the nine twins of `cockpit approvals`.
|
|
3
|
+
*
|
|
4
|
+
* Plain words: an agent PREPARES an outward act (a mail, a message) as a card; the person
|
|
5
|
+
* approves it, edits it and approves it, or rejects it. After a few approvals of one kind the
|
|
6
|
+
* person can turn it into a standing rule ("send without asking"). Every run leaves a receipt.
|
|
7
|
+
* These tools ride the SAME `/api/approvals` doors the browser page and the CLI call, on this
|
|
8
|
+
* machine's device token, never a second route.
|
|
9
|
+
*
|
|
10
|
+
* Three of them are deliberate acts and refuse without `confirm: true`, the same gate as
|
|
11
|
+
* `mail_send` and the CLI's `--yes`: `approvals_approve` (the guarded act runs and cannot be
|
|
12
|
+
* recalled), `approvals_rules_create` (future cards send without asking) and
|
|
13
|
+
* `approvals_rules_revoke` (a standing rule stops). Creating a card, editing one and rejecting one
|
|
14
|
+
* reach nobody, so they need none.
|
|
15
|
+
*
|
|
16
|
+
* A refused send answers 502 with the door's reason and its "Not sent" sentence; this relays both
|
|
17
|
+
* verbatim, as an error, and approving again retries.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
21
|
+
import { CONFIRM_INPUT, EMAIL_INPUT, dataResult, doorFailureText, errorResult, queryString, registrarFor, unconfirmed, withSession, } from "./tool-result.js";
|
|
22
|
+
const LIST_STATUSES = ["open", "done", "pending", "executing", "executed", "failed", "rejected"];
|
|
23
|
+
const SOURCE_KINDS = ["session", "meeting", "ticket", "message", "mail", "jarvis", "schedule", "manual"];
|
|
24
|
+
const CARD_ID = z.string().uuid().describe("The card id, from approvals_list.");
|
|
25
|
+
const ADDRESS = z.object({
|
|
26
|
+
name: z.string().max(200).optional().describe("The person's display name."),
|
|
27
|
+
address: EMAIL_INPUT.describe("The email address."),
|
|
28
|
+
});
|
|
29
|
+
/** Every field a card's `payload` can carry today (mail.send and msg.send); the door checks the fields of the kind named. */
|
|
30
|
+
const PAYLOAD = z.looseObject({
|
|
31
|
+
account: z.string().max(100).optional().describe("mail.send: the exact connected mailbox label to send from. Omit for the default."),
|
|
32
|
+
account_id: z.string().uuid().optional().describe("mail.send: the mailbox id to send from, instead of a label."),
|
|
33
|
+
to: z.array(ADDRESS).max(50).optional().describe("mail.send: recipients as { name?, address }."),
|
|
34
|
+
cc: z.array(ADDRESS).max(50).optional().describe("mail.send: copied recipients as { name?, address }."),
|
|
35
|
+
subject: z.string().max(998).optional().describe("mail.send: the subject line."),
|
|
36
|
+
text: z.string().max(500_000).optional().describe("mail.send: the plain-text body."),
|
|
37
|
+
in_reply_to_message_id: z.string().uuid().optional().describe("mail.send: a message id from mail_read, to send as a reply in that thread."),
|
|
38
|
+
channel_id: z.string().uuid().optional().describe("msg.send: the channel id from msg_channels."),
|
|
39
|
+
content: z.string().max(8000).optional().describe("msg.send: the message text."),
|
|
40
|
+
thread_parent_id: z.string().uuid().optional().describe("msg.send: a message id to reply under."),
|
|
41
|
+
});
|
|
42
|
+
const EDIT = z.looseObject({
|
|
43
|
+
subject: z.string().max(998).optional().describe("mail.send: the new subject line."),
|
|
44
|
+
text: z.string().max(500_000).optional().describe("mail.send: the new plain-text body."),
|
|
45
|
+
content: z.string().max(8000).optional().describe("msg.send: the new message text."),
|
|
46
|
+
});
|
|
47
|
+
export function registerApprovalsTools(server, deps) {
|
|
48
|
+
const register = registrarFor(server);
|
|
49
|
+
/** One door call, relayed: the data on success, the door's own reason and sentence on a refusal. */
|
|
50
|
+
async function ask(tool, method, path, body, headline) {
|
|
51
|
+
return withSession(deps, async (session) => {
|
|
52
|
+
const response = await callAgentDoor(session, deps.fetchImpl, method, path, body);
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
return errorResult(doorFailureText(tool, response));
|
|
55
|
+
return dataResult(headline(response.body), response.body);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const titleOf = (answer) => {
|
|
59
|
+
const card = answer.card;
|
|
60
|
+
return typeof card?.title === "string" ? card.title : "that card";
|
|
61
|
+
};
|
|
62
|
+
register("approvals_list", {
|
|
63
|
+
title: "List approval cards",
|
|
64
|
+
description: "The approvals inbox: outward acts (a mail, a message) that an agent prepared and a person has yet to decide. `status` open (default) is everything still waiting "
|
|
65
|
+
+ "(pending, executing, failed); done is everything decided; or name one status. Each card has its title, who it goes to, why, where it came from, the exact content and its "
|
|
66
|
+
+ "latest receipt. Newest first; `has_more` and `next_before` page backwards. Read-only. `cockpit approvals`, same door.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
status: z.enum(LIST_STATUSES).optional().describe("open (default), done, or one of pending, executing, executed, failed, rejected."),
|
|
69
|
+
limit: z.number().int().min(1).max(100).optional().describe("How many cards, 1 to 100. Default 50."),
|
|
70
|
+
before: z.string().min(1).max(100).optional().describe("The next_before cursor of the previous page, to read older cards."),
|
|
71
|
+
},
|
|
72
|
+
}, async (args) => {
|
|
73
|
+
const query = new URLSearchParams();
|
|
74
|
+
if (typeof args.status === "string")
|
|
75
|
+
query.set("status", args.status);
|
|
76
|
+
if (typeof args.limit === "number")
|
|
77
|
+
query.set("limit", String(args.limit));
|
|
78
|
+
if (typeof args.before === "string")
|
|
79
|
+
query.set("before", args.before);
|
|
80
|
+
return ask("approvals_list", "GET", `/api/approvals${queryString(query)}`, undefined, (answer) => `${String(answer.returned ?? 0)} card(s) shown, ${String(answer.waiting ?? 0)} waiting for a decision.${answer.has_more === true ? " More exist: pass next_before as before." : ""}`);
|
|
81
|
+
});
|
|
82
|
+
register("approvals_show", {
|
|
83
|
+
title: "Read one approval card",
|
|
84
|
+
description: "One card in full: who it goes to, why, where it came from, the exact content that will be (or was) sent, every receipt with the content the act door was given, "
|
|
85
|
+
+ "and whether a standing rule is worth offering. Read-only. `cockpit approvals show`, same door.",
|
|
86
|
+
inputSchema: { id: CARD_ID },
|
|
87
|
+
}, async (args) => ask("approvals_show", "GET", `/api/approvals/${encodeURIComponent(String(args.id))}`, undefined, (answer) => `${titleOf(answer)}.`));
|
|
88
|
+
register("approvals_create", {
|
|
89
|
+
title: "Prepare an approval card",
|
|
90
|
+
description: "Prepares an outward act as a card for the person to approve; nothing is sent. `act_kind` is mail.send (payload: to, subject, text, and optionally account, cc, in_reply_to_message_id) "
|
|
91
|
+
+ "or msg.send (payload: channel_id, content, optionally thread_parent_id). `reason` is why, in a sentence the person reads. `dedupe_key` makes a retry return the same card. "
|
|
92
|
+
+ "If a standing rule already covers the act it is sent at once and the answer says so. `cockpit approvals create`, same door.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
act_kind: z.enum(["mail.send", "msg.send"]).describe("Which kind of act: mail.send or msg.send."),
|
|
95
|
+
payload: PAYLOAD.describe("The act's exact content; the fields of the kind named."),
|
|
96
|
+
reason: z.string().trim().min(1).max(2000).describe("Why this should go out, in a sentence the person reads."),
|
|
97
|
+
kind_label: z.string().trim().min(1).max(120).optional().describe("A short name for this sort of card, like Client follow-up email."),
|
|
98
|
+
source_kind: z.enum(SOURCE_KINDS).optional().describe("Where the act came from. Default manual."),
|
|
99
|
+
source_ref: z.string().trim().min(1).max(200).optional().describe("An id or link for the source, like a ticket identifier."),
|
|
100
|
+
source_label: z.string().trim().min(1).max(300).optional().describe("A short human label for the source, shown on the card."),
|
|
101
|
+
prepared_by: z.string().trim().min(1).max(80).optional().describe("Who prepared it, like Claude. Default an agent on a paired machine."),
|
|
102
|
+
dedupe_key: z.string().trim().min(1).max(200).optional().describe("A stable key so preparing the same card twice returns the first."),
|
|
103
|
+
},
|
|
104
|
+
}, async (args) => ask("approvals_create", "POST", "/api/approvals", args, (answer) => {
|
|
105
|
+
if (answer.deduped === true)
|
|
106
|
+
return `${titleOf(answer)} was already prepared. Nothing new was created.`;
|
|
107
|
+
if (answer.executed_by_rule === true)
|
|
108
|
+
return `${titleOf(answer)}: a standing rule sent it without asking.`;
|
|
109
|
+
return `${titleOf(answer)} is prepared and waiting for the person's decision.`;
|
|
110
|
+
}));
|
|
111
|
+
register("approvals_approve", {
|
|
112
|
+
title: "Approve an approval card",
|
|
113
|
+
description: "Approves a card: the guarded act RUNS (the mail goes out, the message posts) and cannot be taken back, so confirm must be true. `edit` ({ subject, text } or { content }) "
|
|
114
|
+
+ "is applied first, in one step. If the send is refused the answer is an error carrying the door's reason and its \"Not sent\" sentence, the card is marked failed, and approving again retries. "
|
|
115
|
+
+ "A card already decided is refused with already_decided. `cockpit approvals approve`, same door.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
id: CARD_ID,
|
|
118
|
+
edit: EDIT.optional().describe("An edit to apply before approving: { subject, text } for mail, { content } for a message."),
|
|
119
|
+
confirm: CONFIRM_INPUT,
|
|
120
|
+
},
|
|
121
|
+
}, async (args) => {
|
|
122
|
+
const refusal = unconfirmed(args, "Approving lets the guarded act run, and a sent mail or message cannot be recalled.");
|
|
123
|
+
if (refusal)
|
|
124
|
+
return refusal;
|
|
125
|
+
return ask("approvals_approve", "POST", `/api/approvals/${encodeURIComponent(String(args.id))}/approve`, args.edit ? { edit: args.edit } : {}, (answer) => {
|
|
126
|
+
const receipt = answer.receipt;
|
|
127
|
+
return `Approved. ${typeof receipt?.summary === "string" ? receipt.summary : "Done."}`;
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
register("approvals_edit", {
|
|
131
|
+
title: "Edit an approval card",
|
|
132
|
+
description: "Saves an edit to a waiting card ({ subject, text } for mail, { content } for a message). The card stays waiting and nothing is sent; to edit and send in one step, call "
|
|
133
|
+
+ "approvals_approve with `edit`. Recipients cannot be edited (field_not_editable). `cockpit approvals edit`, same door.",
|
|
134
|
+
inputSchema: { id: CARD_ID, edit: EDIT.describe("The edit: { subject, text } for mail, { content } for a message.") },
|
|
135
|
+
}, async (args) => ask("approvals_edit", "PATCH", `/api/approvals/${encodeURIComponent(String(args.id))}`, { edit: args.edit }, (answer) => `Saved your edit to ${titleOf(answer)}. It is still waiting for a decision.`));
|
|
136
|
+
register("approvals_reject", {
|
|
137
|
+
title: "Reject an approval card",
|
|
138
|
+
description: "Declines a waiting card. Nothing is sent and the card is closed; a card already decided is refused with already_decided. `cockpit approvals reject`, same door.",
|
|
139
|
+
inputSchema: { id: CARD_ID },
|
|
140
|
+
}, async (args) => ask("approvals_reject", "POST", `/api/approvals/${encodeURIComponent(String(args.id))}/reject`, {}, (answer) => `Rejected ${titleOf(answer)}. Nothing was sent.`));
|
|
141
|
+
register("approvals_rules", {
|
|
142
|
+
title: "List standing approval rules",
|
|
143
|
+
description: "The person's standing rules: kinds of card that send without asking, who they cover, how often each has fired and whether it is still active. Read-only. `cockpit approvals rules`, same door.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
active: z.boolean().optional().describe("true for rules still in force, false for revoked ones. Omit for both."),
|
|
146
|
+
limit: z.number().int().min(1).max(100).optional().describe("How many rules, 1 to 100. Default 50."),
|
|
147
|
+
},
|
|
148
|
+
}, async (args) => {
|
|
149
|
+
const query = new URLSearchParams();
|
|
150
|
+
if (typeof args.active === "boolean")
|
|
151
|
+
query.set("active", String(args.active));
|
|
152
|
+
if (typeof args.limit === "number")
|
|
153
|
+
query.set("limit", String(args.limit));
|
|
154
|
+
return ask("approvals_rules", "GET", `/api/approvals/rules${queryString(query)}`, undefined, (answer) => `${String(answer.returned ?? 0)} standing rule(s).`);
|
|
155
|
+
});
|
|
156
|
+
register("approvals_rules_create", {
|
|
157
|
+
title: "Create a standing approval rule",
|
|
158
|
+
description: "Turns a card the person approved into a standing rule: from now on this kind of card sends WITHOUT asking, so confirm must be true. `audience` same (default) covers the same people; "
|
|
159
|
+
+ "any covers every audience of this kind. Refused unless the person approved the card themselves (card_not_approved) and when a rule already exists (rule_exists). `cockpit approvals rules create`, same door.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
card_id: z.string().uuid().describe("The id of a card the person approved."),
|
|
162
|
+
audience: z.enum(["same", "any"]).optional().describe("same (default): the same people. any: every audience of this kind."),
|
|
163
|
+
label: z.string().trim().min(1).max(400).optional().describe("A name for the rule, shown in the rules list."),
|
|
164
|
+
confirm: CONFIRM_INPUT,
|
|
165
|
+
},
|
|
166
|
+
}, async (args) => {
|
|
167
|
+
const refusal = unconfirmed(args, "A standing rule makes this kind of card send without asking from now on.");
|
|
168
|
+
if (refusal)
|
|
169
|
+
return refusal;
|
|
170
|
+
const { confirm: _confirm, ...body } = args;
|
|
171
|
+
return ask("approvals_rules_create", "POST", "/api/approvals/rules", body, (answer) => {
|
|
172
|
+
const rule = answer.rule;
|
|
173
|
+
return `Standing rule created: ${typeof rule?.label === "string" ? rule.label : "see the rule"}.`;
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
register("approvals_rules_revoke", {
|
|
177
|
+
title: "Revoke a standing approval rule",
|
|
178
|
+
description: "Takes a standing rule back: matching cards wait for a decision again. The rule stays in the list as revoked, with the reason. Confirm must be true. "
|
|
179
|
+
+ "`cockpit approvals rules revoke`, same door.",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
id: z.string().uuid().describe("The rule id, from approvals_rules."),
|
|
182
|
+
reason: z.string().trim().min(1).max(300).optional().describe("Why the rule is taken back."),
|
|
183
|
+
confirm: CONFIRM_INPUT,
|
|
184
|
+
},
|
|
185
|
+
}, async (args) => {
|
|
186
|
+
const refusal = unconfirmed(args, "Revoking a standing rule changes what Tower sends without asking.");
|
|
187
|
+
if (refusal)
|
|
188
|
+
return refusal;
|
|
189
|
+
return ask("approvals_rules_revoke", "POST", `/api/approvals/rules/${encodeURIComponent(String(args.id))}/revoke`, args.reason ? { reason: args.reason } : {}, () => "Standing rule revoked.");
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image descriptions over MCP (BLI-16977): the two twins of `cockpit image describe|show`.
|
|
3
|
+
*
|
|
4
|
+
* Plain words: Tower swaps the base64 pictures inside collected transcripts for a short written
|
|
5
|
+
* description kept against the image's sha256. `image_describe` sends one image through that same
|
|
6
|
+
* door by hand; `image_show` looks a stored description up by the sha256 or the 16-character
|
|
7
|
+
* prefix a transcript marker carries. Both ride the ONE `/api/ambient/images` door (POST describes,
|
|
8
|
+
* GET reads) on this machine's device token, never a second route.
|
|
9
|
+
*
|
|
10
|
+
* `image_describe` takes an absolute `path` to a local image file OR `content_base64` (the bytes
|
|
11
|
+
* themselves). It refuses, by name, an image over the door's size limit and bytes that are not a
|
|
12
|
+
* PNG, JPEG, GIF or WebP (judged by their first bytes, never the name or a declared type), and it
|
|
13
|
+
* sends nothing in either case. A hosted server has no caller's disk, so `path` is refused there.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { readFile, stat } from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { IMAGE_DESCRIBE_MAX_BYTES, IMAGE_MEDIA_TYPES, ImageHashQuerySchema, formatImageMarker, imageSizeLabel, sniffImageMediaType, } from "@bli-cockpit/telemetry-core";
|
|
20
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
21
|
+
import { inRemoteAgentDoorScope } from "./agent-door-session.js";
|
|
22
|
+
import { dataResult, doorFailureText, errorResult, queryString, registrarFor, withSession, } from "./tool-result.js";
|
|
23
|
+
const DOOR_PATH = "/api/ambient/images";
|
|
24
|
+
const refuse = (reason, sentence) => ({ ok: false, result: errorResult(`image_describe refused (${reason}): ${sentence} Nothing was sent.`) });
|
|
25
|
+
async function pickBytes(args) {
|
|
26
|
+
const filePath = typeof args.path === "string" ? args.path : undefined;
|
|
27
|
+
const base64 = typeof args.content_base64 === "string" ? args.content_base64 : undefined;
|
|
28
|
+
if ((filePath === undefined) === (base64 === undefined))
|
|
29
|
+
return refuse("give_path_or_content", "Give exactly one of `path` (an absolute path to an image file) or `content_base64`.");
|
|
30
|
+
if (base64 !== undefined) {
|
|
31
|
+
const bytes = Buffer.from(base64, "base64");
|
|
32
|
+
if (bytes.length === 0)
|
|
33
|
+
return refuse("invalid_base64", "`content_base64` decoded to nothing.");
|
|
34
|
+
return { ok: true, bytes };
|
|
35
|
+
}
|
|
36
|
+
if (inRemoteAgentDoorScope())
|
|
37
|
+
return refuse("path_not_available_here", "This server is hosted and has no access to your files; send `content_base64` instead.");
|
|
38
|
+
if (!path.isAbsolute(filePath))
|
|
39
|
+
return refuse("path_not_absolute", "`path` must be an absolute path.");
|
|
40
|
+
try {
|
|
41
|
+
const info = await stat(filePath);
|
|
42
|
+
if (!info.isFile())
|
|
43
|
+
return refuse("not_a_file", "`path` is not a file.");
|
|
44
|
+
if (info.size > IMAGE_DESCRIBE_MAX_BYTES)
|
|
45
|
+
return refuse("image_too_large", `That image is ${imageSizeLabel(info.size)}; the describe door takes at most ${imageSizeLabel(IMAGE_DESCRIBE_MAX_BYTES)}.`);
|
|
46
|
+
return { ok: true, bytes: await readFile(filePath) };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return refuse("file_unreadable", "Could not read the file at `path`.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function registerImageTools(server, deps) {
|
|
53
|
+
const register = registrarFor(server);
|
|
54
|
+
register("image_describe", {
|
|
55
|
+
title: "Describe an image",
|
|
56
|
+
description: "Sends ONE image to Tower's describe door and returns its short written description, the way the collector does for every picture in a transcript. Give an absolute `path` to a local "
|
|
57
|
+
+ "PNG, JPEG, GIF or WebP file (up to 3 MB), or `content_base64` with the bytes. An image Tower already described answers from storage (status known). The answer carries the description, "
|
|
58
|
+
+ "the sha256 and the `marker` a transcript would hold in place of the picture. Refuses by name, without sending, an image that is too large or not an image. `cockpit image describe`, same door.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
path: z.string().min(1).max(4096).optional().describe("Absolute path to a local image file. Not available on a hosted server."),
|
|
61
|
+
content_base64: z.string().min(8).max(Math.ceil((IMAGE_DESCRIBE_MAX_BYTES * 4) / 3) + 16).optional().describe("The image bytes, base64. Use instead of `path`."),
|
|
62
|
+
media_type: z.enum(IMAGE_MEDIA_TYPES).optional().describe("With content_base64: the type you believe it is. Checked against the bytes; a mismatch is refused."),
|
|
63
|
+
session_id: z.string().min(1).max(200).optional().describe("The agent session the image came from, an id only."),
|
|
64
|
+
},
|
|
65
|
+
}, async (args) => {
|
|
66
|
+
const picked = await pickBytes(args);
|
|
67
|
+
if (!picked.ok)
|
|
68
|
+
return picked.result;
|
|
69
|
+
const bytes = picked.bytes;
|
|
70
|
+
if (bytes.length > IMAGE_DESCRIBE_MAX_BYTES)
|
|
71
|
+
return refuse("image_too_large", `That image is ${imageSizeLabel(bytes.length)}; the describe door takes at most ${imageSizeLabel(IMAGE_DESCRIBE_MAX_BYTES)}.`).result;
|
|
72
|
+
const mediaType = sniffImageMediaType(bytes);
|
|
73
|
+
if (!mediaType)
|
|
74
|
+
return refuse("not_an_image", "Those bytes are not a PNG, JPEG, GIF or WebP image (judged by their first bytes, not the name).").result;
|
|
75
|
+
if (typeof args.media_type === "string" && args.media_type !== mediaType)
|
|
76
|
+
return refuse("media_type_mismatch", `You said ${args.media_type} but the bytes are ${mediaType}.`).result;
|
|
77
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
78
|
+
const request = {
|
|
79
|
+
schema_version: "image-describe.v1",
|
|
80
|
+
sha256,
|
|
81
|
+
media_type: mediaType,
|
|
82
|
+
byte_size: bytes.length,
|
|
83
|
+
content_base64: Buffer.from(bytes).toString("base64"),
|
|
84
|
+
source: "manual",
|
|
85
|
+
...(typeof args.session_id === "string" ? { session_id: args.session_id } : {}),
|
|
86
|
+
};
|
|
87
|
+
const marker = formatImageMarker({ sha256, mediaType, byteSize: bytes.length });
|
|
88
|
+
return withSession(deps, async (session) => {
|
|
89
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", DOOR_PATH, request);
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
return errorResult(doorFailureText("image_describe", response));
|
|
92
|
+
const description = response.body.description;
|
|
93
|
+
const said = response.body.status === "known" ? "Tower already had a description for this image." : "Described.";
|
|
94
|
+
return dataResult(`${said} ${typeof description?.description === "string" ? description.description : ""}`.trim(), { ...response.body, marker });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
register("image_show", {
|
|
98
|
+
title: "Read an image description",
|
|
99
|
+
description: "Looks up the stored description of an image by its sha256, or by the first 12 to 64 characters of it (a transcript's `[image sha256:ab12cd34ef567890… png 412KB]` marker carries 16). "
|
|
100
|
+
+ "Answers the descriptions found; an empty list means nothing is stored for that image yet. Read-only. `cockpit image show`, same door.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
sha256: ImageHashQuerySchema.describe("The image's sha256 or a prefix of it: 12 to 64 lowercase hex characters."),
|
|
103
|
+
},
|
|
104
|
+
}, async (args) => {
|
|
105
|
+
const query = new URLSearchParams({ sha256: String(args.sha256) });
|
|
106
|
+
return withSession(deps, async (session) => {
|
|
107
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `${DOOR_PATH}${queryString(query)}`, undefined);
|
|
108
|
+
if (!response.ok)
|
|
109
|
+
return errorResult(doorFailureText("image_show", response));
|
|
110
|
+
const found = Array.isArray(response.body.descriptions) ? response.body.descriptions.length : 0;
|
|
111
|
+
return dataResult(found === 0 ? "No description stored for that image yet." : `${found} description(s) found.`, response.body);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
package/dist/memory-twins.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* BLI Memory ships its own MCP server, so its store verbs are twinned against
|
|
5
5
|
* tools on THAT server, over the same `/api/memory/*` doors the CLI verbs call.
|
|
6
6
|
* Split out of `verb-census.ts` to keep that file under the readability band.
|
|
7
|
+
* The image door's two twins (BLI-16977) sit here for the same reason.
|
|
7
8
|
*/
|
|
8
9
|
import type { McpTwin } from "./verb-census.js";
|
|
9
10
|
export declare const MEMORY_STORE_TWINS: Record<string, McpTwin>;
|
|
11
|
+
export declare const IMAGE_TWINS: Record<string, McpTwin>;
|
package/dist/memory-twins.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* BLI Memory ships its own MCP server, so its store verbs are twinned against
|
|
5
5
|
* tools on THAT server, over the same `/api/memory/*` doors the CLI verbs call.
|
|
6
6
|
* Split out of `verb-census.ts` to keep that file under the readability band.
|
|
7
|
+
* The image door's two twins (BLI-16977) sit here for the same reason.
|
|
7
8
|
*/
|
|
8
9
|
export const MEMORY_STORE_TWINS = {
|
|
9
10
|
"memory search": { tool: "search_memory", door: "POST /api/memory/search", server: "bli-memory" },
|
|
@@ -15,3 +16,7 @@ export const MEMORY_STORE_TWINS = {
|
|
|
15
16
|
"memory private": { tool: "set_memory_visibility", door: "POST /api/memory/visibility", server: "bli-memory" },
|
|
16
17
|
"memory public": { tool: "set_memory_visibility", door: "POST /api/memory/visibility", server: "bli-memory" },
|
|
17
18
|
};
|
|
19
|
+
export const IMAGE_TWINS = {
|
|
20
|
+
"image describe": { tool: "image_describe", door: "POST /api/ambient/images" },
|
|
21
|
+
"image show": { tool: "image_show", door: "GET /api/ambient/images" },
|
|
22
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -42,6 +42,8 @@ import { registerSpeedTools } from "./speed-tools.js";
|
|
|
42
42
|
import { registerUsageTools } from "./usage-tools.js";
|
|
43
43
|
import { registerLogsTools } from "./logs-tools.js";
|
|
44
44
|
import { registerBrainTools } from "./brain-tools.js";
|
|
45
|
+
import { registerApprovalsTools } from "./approvals-tools.js";
|
|
46
|
+
import { registerImageTools } from "./image-tools.js";
|
|
45
47
|
import { registerSourcesTools } from "./sources-tools.js";
|
|
46
48
|
import { TOWER_SERVER_ICONS } from "./server-icons.js";
|
|
47
49
|
import { installToolRegistration } from "./tool-registration.js";
|
|
@@ -522,5 +524,9 @@ export function createServer(deps, options = {}) {
|
|
|
522
524
|
// BLI-12859: Tower's incident queue (one incident, one issue, per recurring log problem), over
|
|
523
525
|
// the same GET /api/brain/incidents door `cockpit brain status` reads. Read-only.
|
|
524
526
|
registerBrainTools(server, { fetchImpl: deps.fetchImpl });
|
|
527
|
+
// BLI-16945: the approvals inbox, over the same /api/approvals doors the page and `cockpit approvals` call.
|
|
528
|
+
registerApprovalsTools(server, { fetchImpl: deps.fetchImpl });
|
|
529
|
+
// BLI-16977: image descriptions, over the same /api/ambient/images door `cockpit image` calls.
|
|
530
|
+
registerImageTools(server, { fetchImpl: deps.fetchImpl });
|
|
525
531
|
return server;
|
|
526
532
|
}
|
|
@@ -17,7 +17,7 @@ export interface TowerToolAnnotations {
|
|
|
17
17
|
* Writes a person cannot take back by writing again: deletes, revokes,
|
|
18
18
|
* detaches and a merge. These also carry `destructiveHint: true`.
|
|
19
19
|
*/
|
|
20
|
-
export declare const DESTRUCTIVE_TOOLS: readonly ["docs_delete", "cal_delete", "cal_detach", "mail_detach", "notes_detach", "notes_rmdir", "glossary_remove", "settings_delete", "team_device_revoke", "github_merge"];
|
|
20
|
+
export declare const DESTRUCTIVE_TOOLS: readonly ["docs_delete", "cal_delete", "cal_detach", "mail_detach", "notes_detach", "notes_rmdir", "glossary_remove", "settings_delete", "team_device_revoke", "github_merge", "approvals_rules_revoke"];
|
|
21
21
|
/**
|
|
22
22
|
* Tools the HOSTED server leaves out, each with the reason. A hosted process
|
|
23
23
|
* is one machine answering many people: a tool that reads that machine's
|
package/dist/tool-annotations.js
CHANGED
package/dist/tool-catalog.d.ts
CHANGED
|
@@ -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_board", "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", "notes_bot_upcoming", "ops_status", "slack_coverage", "slack_connect", "slack_disconnect", "slack_read", "memory_map", "memory_rules", "memory_knowledge", "memory_profile", "memory_consolidation", "memory_chain", "settings_show", "settings_list", "model_show", "team_members", "team_github_login_add", "team_github_login_remove", "team_github_login_list", "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_board", "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", "notes_bot_upcoming", "ops_status", "slack_coverage", "slack_connect", "slack_disconnect", "slack_read", "memory_map", "memory_rules", "memory_knowledge", "memory_profile", "memory_consolidation", "memory_chain", "settings_show", "settings_list", "model_show", "team_members", "team_github_login_add", "team_github_login_remove", "team_github_login_list", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check", "logs_digest", "brain_status", "approvals_list", "approvals_show", "approvals_rules", "image_show"];
|
|
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).
|
|
@@ -21,7 +21,7 @@ export declare const DRY_RUN_TOOLS: readonly ["ops_recompile", "memory_requeue"]
|
|
|
21
21
|
* candidates, share calendars. Never called by the smoke; covered by the unit
|
|
22
22
|
* suites (`write-tools.test.ts` and each family's own).
|
|
23
23
|
*/
|
|
24
|
-
export declare const WRITE_TOOLS: readonly ["logs_key_mint", "brain_retry", "emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "github_merge", "github_retitle", "github_rerun", "mail_send", "mail_draft", "mail_rename", "mail_enable", "mail_disable", "mail_default", "mail_remove", "cal_change", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "glossary_add", "glossary_remove", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_grant", "team_revoke", "team_device_revoke", "team_device_doctor"];
|
|
24
|
+
export declare const WRITE_TOOLS: readonly ["image_describe", "approvals_create", "approvals_approve", "approvals_edit", "approvals_reject", "approvals_rules_create", "approvals_rules_revoke", "logs_key_mint", "brain_retry", "emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "github_merge", "github_retitle", "github_rerun", "mail_send", "mail_draft", "mail_rename", "mail_enable", "mail_disable", "mail_default", "mail_remove", "cal_change", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "glossary_add", "glossary_remove", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_grant", "team_revoke", "team_device_revoke", "team_device_doctor"];
|
|
25
25
|
/** Reads that spend a model turn. The smoke's `--no-model` leaves these out. */
|
|
26
26
|
export declare const MODEL_SPENDING_TOOLS: readonly ["jarvis_ask", "jarvis_check"];
|
|
27
27
|
/**
|
package/dist/tool-catalog.js
CHANGED
|
@@ -101,6 +101,12 @@ export const READ_TOOLS = [
|
|
|
101
101
|
"logs_digest",
|
|
102
102
|
// BLI-12859: Tower's incident queue, per project. Reads only.
|
|
103
103
|
"brain_status",
|
|
104
|
+
// BLI-16945: the approvals inbox and its standing rules. Reads only.
|
|
105
|
+
"approvals_list",
|
|
106
|
+
"approvals_show",
|
|
107
|
+
"approvals_rules",
|
|
108
|
+
// BLI-16977: a stored image description, by sha256. Reads only.
|
|
109
|
+
"image_show",
|
|
104
110
|
];
|
|
105
111
|
/**
|
|
106
112
|
* Tools that write, but answer a dry run that writes nothing. The smoke calls
|
|
@@ -113,6 +119,15 @@ export const DRY_RUN_TOOLS = ["ops_recompile", "memory_requeue"];
|
|
|
113
119
|
* suites (`write-tools.test.ts` and each family's own).
|
|
114
120
|
*/
|
|
115
121
|
export const WRITE_TOOLS = [
|
|
122
|
+
// BLI-16977: sends one image to the describe door (a vision-model call, stored against its sha256). Never smoked.
|
|
123
|
+
"image_describe",
|
|
124
|
+
// BLI-16945: approve runs the guarded act; a rule sends without asking. Never smoked.
|
|
125
|
+
"approvals_create",
|
|
126
|
+
"approvals_approve",
|
|
127
|
+
"approvals_edit",
|
|
128
|
+
"approvals_reject",
|
|
129
|
+
"approvals_rules_create",
|
|
130
|
+
"approvals_rules_revoke",
|
|
116
131
|
"logs_key_mint",
|
|
117
132
|
"brain_retry",
|
|
118
133
|
"emit_event",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
export const TYPED_SPELLING = {
|
|
23
23
|
// The action is `invite`; the button, the help wall and the docs all say
|
|
24
24
|
// send-takehome, so that is what a stranger is told to type.
|
|
25
|
+
"approvals rules-create": "approvals rules create",
|
|
26
|
+
"approvals rules-revoke": "approvals rules revoke",
|
|
25
27
|
"careers invite": "careers send-takehome",
|
|
26
28
|
"careers takehome-show": "careers takehome show",
|
|
27
29
|
"careers takehome-set": "careers takehome set",
|
package/dist/verb-census.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { readFileSync, readdirSync } from "node:fs";
|
|
30
30
|
import { dirname, join, resolve } from "node:path";
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
|
-
import { MEMORY_STORE_TWINS } from "./memory-twins.js";
|
|
32
|
+
import { IMAGE_TWINS, MEMORY_STORE_TWINS } from "./memory-twins.js";
|
|
33
33
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
export const COLLECTOR_COMMANDS_DIR = resolve(HERE, "../../cockpit-local-collector/src/commands");
|
|
35
35
|
function read(file) {
|
|
@@ -244,6 +244,15 @@ export const MCP_TWINS = {
|
|
|
244
244
|
"logs": { tool: "logs_digest", door: "LOCAL @bli-cockpit/telemetry-core/log-digest (this machine's logs); `project` reads GET /api/logs (the stored digests)" },
|
|
245
245
|
"brain retry": { tool: "brain_retry", door: "POST /api/brain/incidents/retry" },
|
|
246
246
|
"brain status": { tool: "brain_status", door: "GET /api/brain/incidents" },
|
|
247
|
+
"approvals list": { tool: "approvals_list", door: "GET /api/approvals" },
|
|
248
|
+
"approvals show": { tool: "approvals_show", door: "GET /api/approvals/[id]" },
|
|
249
|
+
"approvals create": { tool: "approvals_create", door: "POST /api/approvals" },
|
|
250
|
+
"approvals approve": { tool: "approvals_approve", door: "POST /api/approvals/[id]/approve" },
|
|
251
|
+
"approvals edit": { tool: "approvals_edit", door: "PATCH /api/approvals/[id]" },
|
|
252
|
+
"approvals reject": { tool: "approvals_reject", door: "POST /api/approvals/[id]/reject" },
|
|
253
|
+
"approvals rules": { tool: "approvals_rules", door: "GET /api/approvals/rules" },
|
|
254
|
+
"approvals rules-create": { tool: "approvals_rules_create", door: "POST /api/approvals/rules" },
|
|
255
|
+
"approvals rules-revoke": { tool: "approvals_rules_revoke", door: "POST /api/approvals/rules/[id]/revoke" },
|
|
247
256
|
"usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
|
|
248
257
|
"setup": { tool: "setup_read", door: "GET /api/setup" },
|
|
249
258
|
// BLI-13033: times the data door of every rail page; the twin runs the same GETs.
|
|
@@ -341,8 +350,8 @@ export const MCP_TWINS = {
|
|
|
341
350
|
"notes granola-status": { tool: "notes_granola_status", door: "GET /api/notes/granola-status" },
|
|
342
351
|
"memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
|
|
343
352
|
// BLI-4047, BLI-12927: the memory STORE at a terminal. Its twins live on the
|
|
344
|
-
// second server, `bli-memory` (see `memory-twins.ts
|
|
345
|
-
...MEMORY_STORE_TWINS,
|
|
353
|
+
// second server, `bli-memory` (see `memory-twins.ts`, which also holds the image twins).
|
|
354
|
+
...MEMORY_STORE_TWINS, ...IMAGE_TWINS,
|
|
346
355
|
// BLI-4533: the compiled one-page profile. Its twin is on `bli-tower`, not
|
|
347
356
|
// `bli-memory`: that server's four tools are the vendor-compatible set the
|
|
348
357
|
// Claude Code plugin expects, and this page is ours.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.117",
|
|
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.
|
|
40
|
+
"@bli-cockpit/telemetry-core": "0.1.71",
|
|
41
41
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
42
|
"zod": "^4.3.6"
|
|
43
43
|
},
|