@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.
- package/README.md +168 -2
- package/dist/agent-door.d.ts +8 -1
- package/dist/agent-door.js +7 -2
- package/dist/brief-tools.d.ts +26 -0
- package/dist/brief-tools.js +160 -0
- package/dist/brief-write-tools.d.ts +37 -0
- package/dist/brief-write-tools.js +178 -0
- package/dist/docs-msg-tools.d.ts +13 -7
- package/dist/docs-msg-tools.js +141 -25
- package/dist/jarvis-answer-envelope.d.ts +13 -1
- package/dist/jarvis-answer-envelope.js +2 -0
- package/dist/jarvis-door.js +16 -1
- package/dist/jarvis-tools.d.ts +18 -10
- package/dist/jarvis-tools.js +28 -17
- package/dist/notes-tools.d.ts +33 -0
- package/dist/notes-tools.js +143 -0
- package/dist/notes-write-tools.d.ts +34 -0
- package/dist/notes-write-tools.js +211 -0
- package/dist/ops-tools.d.ts +23 -0
- package/dist/ops-tools.js +212 -0
- package/dist/pages-tools.d.ts +28 -0
- package/dist/pages-tools.js +190 -0
- package/dist/readme-census.d.ts +18 -0
- package/dist/readme-census.js +79 -0
- package/dist/search-tool.d.ts +54 -0
- package/dist/search-tool.js +134 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +34 -1
- package/dist/settings-tools.d.ts +29 -0
- package/dist/settings-tools.js +151 -0
- package/dist/settings-write-tools.d.ts +47 -0
- package/dist/settings-write-tools.js +183 -0
- package/dist/team-write-tools.d.ts +39 -0
- package/dist/team-write-tools.js +141 -0
- package/dist/tool-result.d.ts +86 -0
- package/dist/tool-result.js +105 -0
- package/dist/verb-census.d.ts +22 -0
- package/dist/verb-census.js +108 -6
- package/dist/work-tools.d.ts +2 -7
- package/dist/work-tools.js +35 -25
- 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
|
+
}
|
package/dist/verb-census.d.ts
CHANGED
|
@@ -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>;
|
package/dist/verb-census.js
CHANGED
|
@@ -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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 = {};
|
package/dist/work-tools.d.ts
CHANGED
|
@@ -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 {
|
|
18
|
-
|
|
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: {
|
package/dist/work-tools.js
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.29",
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
34
|
"zod": "^4.3.6"
|
|
34
35
|
},
|