@bli-cockpit/mcp 0.1.29 → 0.1.31
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 +16 -7
- package/dist/careers-tools.d.ts +4 -0
- package/dist/careers-tools.js +19 -0
- package/dist/note-folder-tools.d.ts +4 -0
- package/dist/note-folder-tools.js +57 -0
- package/dist/notes-tools.d.ts +0 -17
- package/dist/notes-tools.js +3 -1
- package/dist/notes-write-tools.js +45 -5
- package/dist/readme-census.js +8 -3
- package/dist/server.js +4 -0
- package/dist/usage-tools.d.ts +4 -0
- package/dist/usage-tools.js +21 -0
- package/dist/verb-census.d.ts +24 -0
- package/dist/verb-census.js +42 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ about what an agent can reach.
|
|
|
34
34
|
|
|
35
35
|
<!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**90 of 93 Tower verbs have an MCP twin.**
|
|
38
38
|
Each tool goes through the SAME door its CLI verb calls, with the same
|
|
39
39
|
collector device token — never a second route and never a service-role
|
|
40
40
|
reader. `src/verb-census.test.ts` fails when a verb is in none of the
|
|
@@ -55,6 +55,9 @@ three tables below.
|
|
|
55
55
|
| `cockpit cal sync` | `cal_sync` | `POST /api/cal/calendars/[id]/sync` |
|
|
56
56
|
| `cockpit cal today` | `cal_today` | `GET /api/cal/today` |
|
|
57
57
|
| `cockpit cal week` | `cal_week` | `GET /api/cal/week` |
|
|
58
|
+
| `cockpit careers list` | `careers_list` | `GET /api/careers/applications` |
|
|
59
|
+
| `cockpit careers rescreen` | `careers_rescreen` | `POST /api/careers/applications/[id]/rescreen` |
|
|
60
|
+
| `cockpit careers show` | `careers_show` | `GET /api/careers/applications/[id]` |
|
|
58
61
|
| `cockpit correct` | `brief_correct` | `POST /api/jarvis/corrections` |
|
|
59
62
|
| `cockpit docs create` | `docs_create` | `POST /api/docs/documents` |
|
|
60
63
|
| `cockpit docs list` | `docs_list` | `GET /api/docs/documents` |
|
|
@@ -94,9 +97,14 @@ three tables below.
|
|
|
94
97
|
| `cockpit msg read` | `msg_read` | `GET /api/msg/channels/[id]/messages` |
|
|
95
98
|
| `cockpit msg send` | `msg_send` | `POST /api/msg/channels/[id]/messages` |
|
|
96
99
|
| `cockpit msg thread` | `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` |
|
|
100
|
+
| `cockpit notes folders` | `notes_folders` | `GET /api/notes/folders` |
|
|
97
101
|
| `cockpit notes list` | `notes_list` | `GET /api/notes/library` |
|
|
102
|
+
| `cockpit notes mkdir` | `notes_mkdir` | `POST /api/notes/folders` |
|
|
98
103
|
| `cockpit notes move` | `notes_move` | `POST /api/notes/move` |
|
|
99
104
|
| `cockpit notes paste` | `notes_paste` | `POST /api/notes/upload (text)` |
|
|
105
|
+
| `cockpit notes place` | `notes_place` | `POST /api/notes/place` |
|
|
106
|
+
| `cockpit notes rename` | `notes_rename` | `PATCH /api/notes/folders/[id]` |
|
|
107
|
+
| `cockpit notes rmdir` | `notes_rmdir` | `DELETE /api/notes/folders/[id]` |
|
|
100
108
|
| `cockpit notes share` | `notes_share` | `POST /api/notes/share` |
|
|
101
109
|
| `cockpit notes shelf` | `notes_shelf` | `GET /api/notes/shelf` |
|
|
102
110
|
| `cockpit notes shelves` | `notes_shelves` | `GET /api/notes/library` |
|
|
@@ -111,17 +119,18 @@ three tables below.
|
|
|
111
119
|
| `cockpit scout start` | `scout_start` | `POST /api/cockpit/scout (start)` |
|
|
112
120
|
| `cockpit scout undo` | `scout_undo` | `POST /api/cockpit/scout (undo_dismiss)` |
|
|
113
121
|
| `cockpit search` | `tower_search` | `GET /api/search` |
|
|
114
|
-
| `cockpit settings delete` | `settings_delete` | `DELETE /api/settings/env-blobs` |
|
|
115
|
-
| `cockpit settings list` | `settings_list` | `GET /api/settings/env-blobs` |
|
|
116
|
-
| `cockpit settings set` | `settings_set` | `POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor` |
|
|
117
|
-
| `cockpit settings
|
|
122
|
+
| `cockpit settings env delete` | `settings_delete` | `DELETE /api/settings/env-blobs` |
|
|
123
|
+
| `cockpit settings env list` | `settings_list` | `GET /api/settings/env-blobs` |
|
|
124
|
+
| `cockpit settings switches set` | `settings_set` | `POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor` |
|
|
125
|
+
| `cockpit settings` | `settings_show` | `GET /api/settings/* + /api/team/members` |
|
|
118
126
|
| `cockpit slack coverage` | `slack_coverage` | `GET /api/ops/slack/coverage` |
|
|
119
127
|
| `cockpit slack read` | `slack_read` | `POST /api/ops/slack/read` |
|
|
120
|
-
| `cockpit team device
|
|
121
|
-
| `cockpit team device
|
|
128
|
+
| `cockpit team device list` | `team_device_list` | `GET /api/team/devices` |
|
|
129
|
+
| `cockpit team device revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
|
|
122
130
|
| `cockpit team invite` | `team_invite` | `POST /api/team/invite` |
|
|
123
131
|
| `cockpit team members` | `team_members` | `GET /api/team/members` |
|
|
124
132
|
| `cockpit team role` | `team_role` | `PATCH /api/team/members/[userId]/role` |
|
|
133
|
+
| `cockpit usage people` | `usage_people` | `GET /api/usage/people` |
|
|
125
134
|
| `cockpit workbook` | `workbook_read` | `GET /api/cockpit/workbook` |
|
|
126
135
|
|
|
127
136
|
### Owed a twin
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { callAgentDoor } from './agent-door.js';
|
|
3
|
+
import { doorFailureText, errorResult, registrarFor, textResult, withSession } from './tool-result.js';
|
|
4
|
+
export function registerCareersTools(server, deps) {
|
|
5
|
+
const register = registrarFor(server);
|
|
6
|
+
register('careers_list', { title: 'List applications', description: 'Super-admin only. Newest applications, bounded to 100 with total and has_more.', inputSchema: { role: z.string().optional(), min_score: z.number().min(0).max(100).optional(), since: z.string().optional() } }, async (args) => withSession(deps, async (session) => {
|
|
7
|
+
const query = new URLSearchParams();
|
|
8
|
+
for (const key of ['role', 'min_score', 'since'])
|
|
9
|
+
if (args[key] !== undefined)
|
|
10
|
+
query.set(key, String(args[key]));
|
|
11
|
+
const answer = await callAgentDoor(session, deps.fetchImpl, 'GET', `/api/careers/applications?${query}`);
|
|
12
|
+
return answer.ok ? textResult('Applications', answer.body) : errorResult(doorFailureText('careers_list', answer));
|
|
13
|
+
}));
|
|
14
|
+
for (const action of ['show', 'rescreen'])
|
|
15
|
+
register(`careers_${action}`, { title: `${action} application`, description: `Super-admin only. ${action} one application.`, inputSchema: { id: z.string().uuid() } }, async (args) => withSession(deps, async (session) => {
|
|
16
|
+
const answer = await callAgentDoor(session, deps.fetchImpl, action === 'show' ? 'GET' : 'POST', `/api/careers/applications/${encodeURIComponent(String(args.id))}${action === 'rescreen' ? '/rescreen' : ''}`, undefined, 60_000);
|
|
17
|
+
return answer.ok ? textResult('Application', answer.body) : errorResult(doorFailureText(`careers_${action}`, answer));
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
3
|
+
import { registrarFor, withSession, textResult, errorResult, doorFailureText } from "./tool-result.js";
|
|
4
|
+
export function registerNoteFolderTools(server, deps) {
|
|
5
|
+
const register = registrarFor(server);
|
|
6
|
+
register("notes_folders", {
|
|
7
|
+
title: "List note folders", description: "The folder tree with caller-visible note counts.",
|
|
8
|
+
inputSchema: { tree: z.boolean().optional() },
|
|
9
|
+
}, async () => withSession(deps, async (session) => {
|
|
10
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
11
|
+
if (!response.ok)
|
|
12
|
+
return errorResult(doorFailureText("notes_folders", response));
|
|
13
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
14
|
+
}));
|
|
15
|
+
register("notes_mkdir", {
|
|
16
|
+
title: "Create a note folder", description: "Create a path, including missing ancestors. Existing paths are reused.",
|
|
17
|
+
inputSchema: { path: z.string().min(1).max(2000) },
|
|
18
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
19
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/folders", { path: args.path });
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
return errorResult(doorFailureText("notes_mkdir", response));
|
|
22
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
23
|
+
}));
|
|
24
|
+
register("notes_rmdir", {
|
|
25
|
+
title: "Delete an empty note folder", description: "Delete a folder by path. Refuses any folder with notes or children.",
|
|
26
|
+
inputSchema: { path: z.string().min(1).max(2000) },
|
|
27
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
28
|
+
const listed = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
29
|
+
if (!listed.ok)
|
|
30
|
+
return errorResult(doorFailureText("notes_rmdir", listed));
|
|
31
|
+
const body = listed.body;
|
|
32
|
+
const path = String(args.path).split("/").map(part => part.trim()).join("/");
|
|
33
|
+
const folder = body.folders.find(folder => folder.path === path);
|
|
34
|
+
if (!folder)
|
|
35
|
+
return errorResult("That folder could not be found.");
|
|
36
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/notes/folders/${encodeURIComponent(folder.id)}`);
|
|
37
|
+
if (!response.ok)
|
|
38
|
+
return errorResult(doorFailureText("notes_rmdir", response));
|
|
39
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
40
|
+
}));
|
|
41
|
+
register("notes_rename", {
|
|
42
|
+
title: "Rename a note folder", description: "Rename a folder by path, updating every descendant path.",
|
|
43
|
+
inputSchema: { path: z.string().min(1).max(2000), name: z.string().min(1).max(120) },
|
|
44
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
45
|
+
const listed = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/notes/folders");
|
|
46
|
+
if (!listed.ok)
|
|
47
|
+
return errorResult(doorFailureText("notes_rename", listed));
|
|
48
|
+
const body = listed.body;
|
|
49
|
+
const folder = body.folders.find(folder => folder.path === String(args.path).split("/").map(part => part.trim()).join("/"));
|
|
50
|
+
if (!folder)
|
|
51
|
+
return errorResult("That folder could not be found.");
|
|
52
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "PATCH", `/api/notes/folders/${encodeURIComponent(folder.id)}`, { name: args.name });
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
return errorResult(doorFailureText("notes_rename", response));
|
|
55
|
+
return textResult(JSON.stringify(response.body), response.body);
|
|
56
|
+
}));
|
|
57
|
+
}
|
package/dist/notes-tools.d.ts
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `notes_*` MCP tools (BLI-3756) — the meeting-notes library, read by an
|
|
3
|
-
* agent, over the same `/api/notes/**` doors `cockpit notes` calls with the
|
|
4
|
-
* same device token.
|
|
5
|
-
*
|
|
6
|
-
* **The narrowing is always said out loud.** `lib/notes/api-doors.ts` gives a
|
|
7
|
-
* caller with no signed-in browser session a NARROWER read and names it in the
|
|
8
|
-
* answer's own `scope`/`degradedBecause` — a device token is exactly such a
|
|
9
|
-
* caller, so an agent reading this library is routinely seeing less than a
|
|
10
|
-
* person would in a browser. Every tool here appends that reason to its text.
|
|
11
|
-
* Swallowing it would let an agent conclude a note was never taken when in
|
|
12
|
-
* fact it simply was not this caller's to read, which is a fact invented out
|
|
13
|
-
* of a permission.
|
|
14
|
-
*
|
|
15
|
-
* Reads only. `notes move|share|unshare|paste|upload` are batch 2 — sharing is
|
|
16
|
-
* a deliberate act and the paste door takes a body on stdin by rule.
|
|
17
|
-
*/
|
|
18
1
|
import { type ToolDeps } from "./tool-result.js";
|
|
19
2
|
export type NotesDeps = ToolDeps;
|
|
20
3
|
interface ScopedBody {
|
package/dist/notes-tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerNoteFolderTools } from "./note-folder-tools.js";
|
|
1
2
|
/**
|
|
2
3
|
* `notes_*` MCP tools (BLI-3756) — the meeting-notes library, read by an
|
|
3
4
|
* agent, over the same `/api/notes/**` doors `cockpit notes` calls with the
|
|
@@ -50,6 +51,7 @@ const LIBRARY_FILTERS = {
|
|
|
50
51
|
limit: z.number().int().min(1).max(500).optional(),
|
|
51
52
|
};
|
|
52
53
|
export function registerNotesTools(server, deps) {
|
|
54
|
+
registerNoteFolderTools(server, deps);
|
|
53
55
|
const register = registrarFor(server);
|
|
54
56
|
register("notes_list", {
|
|
55
57
|
title: "List Tower meeting notes",
|
|
@@ -92,7 +94,7 @@ export function registerNotesTools(server, deps) {
|
|
|
92
94
|
}
|
|
93
95
|
const room = note.participants.length > 0 ? `\nIn the room: ${note.participants.join(", ")}` : "";
|
|
94
96
|
return textResult(`${note.title}\n${note.meetingDate} · ${note.shelf} · ${note.fileName} · ${note.lineCount} lines`
|
|
95
|
-
+ `${room}\n${note.visibility}\n\n${note.content}${narrowingNote(body)}`, { scope: body.scope ?? null, note });
|
|
97
|
+
+ `${room}\n${note.visibility}${body.audio ? "\nTranscription: " + body.audio.transcription_status : ""}\n\n${note.content}${narrowingNote(body)}`, { scope: body.scope ?? null, note, audio: body.audio ?? null });
|
|
96
98
|
}));
|
|
97
99
|
register("notes_shelf", {
|
|
98
100
|
title: "Your own Tower notes shelf",
|
|
@@ -77,6 +77,9 @@ async function readNoteFile(filePath) {
|
|
|
77
77
|
}
|
|
78
78
|
if (size === 0)
|
|
79
79
|
return refuse("file_empty");
|
|
80
|
+
if (/\.(mp3|m4a|wav|webm|ogg)$/i.test(fileName) && size > 4_000_000) {
|
|
81
|
+
return { ok: false, refusal: "file_too_big", sentence: "file_too_large: Audio is limited to 4 MB (4,000,000 bytes)." };
|
|
82
|
+
}
|
|
80
83
|
if (size > NOTE_FILE_MAX_BYTES)
|
|
81
84
|
return refuse("file_too_big");
|
|
82
85
|
try {
|
|
@@ -102,18 +105,19 @@ async function postForm(deps, session, tool, form) {
|
|
|
102
105
|
// `stored: false` is the door saying it did not keep the note. That is a
|
|
103
106
|
// refusal with a reason, not a success with a caveat.
|
|
104
107
|
return body.stored === true
|
|
105
|
-
? textResult(text, { ok: true, noteId: body.noteId ?? null, scope: body.scope ?? null })
|
|
108
|
+
? textResult(text, { ok: true, noteId: body.noteId ?? null, scope: body.scope ?? null, transcription: body.transcription ?? null })
|
|
106
109
|
: errorResult(text);
|
|
107
110
|
}
|
|
108
111
|
export function registerNotesWriteTools(server, deps) {
|
|
109
112
|
const register = registrarFor(server);
|
|
110
113
|
register("notes_upload", {
|
|
111
114
|
title: "Put a local file in as a Tower meeting note",
|
|
112
|
-
description: "Uploads a transcript or
|
|
115
|
+
description: "Uploads a transcript, note, or MP3/M4A/WAV/WebM/OGG audio file from this machine. Audio is limited to 4 MB and returns a note id plus transcription status immediately. Poll notes_show for completion. "
|
|
113
116
|
+ "A name that looks like a key or credential file is refused without being opened; so is a missing, empty or "
|
|
114
117
|
+ "over-20-MB file. Tower reads the whole note with one model call, so a large file can take a couple of "
|
|
115
118
|
+ "minutes and your client may give up first — the upload keeps going; notes_list will show it.",
|
|
116
119
|
inputSchema: {
|
|
120
|
+
folder: z.string().min(1).max(2000).optional().describe("Folder path; creates missing ancestors. / means Root."),
|
|
117
121
|
path: z.string().min(1).max(4096).describe("An absolute path on this machine."),
|
|
118
122
|
exclude: z
|
|
119
123
|
.string()
|
|
@@ -128,9 +132,13 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
128
132
|
if (!read.ok)
|
|
129
133
|
return errorResult(`Refused (${read.refusal}): ${read.sentence} Nothing was sent.`);
|
|
130
134
|
const form = new FormData();
|
|
131
|
-
|
|
135
|
+
const audioTypes = { mp3: "audio/mpeg", m4a: "audio/mp4", wav: "audio/wav", webm: "audio/webm", ogg: "audio/ogg" };
|
|
136
|
+
const mediaType = audioTypes[path.extname(read.fileName).slice(1).toLowerCase()] ?? "";
|
|
137
|
+
form.set("file", new File([new Uint8Array(read.bytes)], read.fileName, { type: mediaType }));
|
|
132
138
|
if (args.exclude)
|
|
133
139
|
form.set("exclusions", String(args.exclude));
|
|
140
|
+
if (args.folder)
|
|
141
|
+
form.set("folder_path", String(args.folder));
|
|
134
142
|
return postForm(deps, session, "notes_upload", form);
|
|
135
143
|
}));
|
|
136
144
|
register("notes_paste", {
|
|
@@ -139,6 +147,7 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
139
147
|
+ "`cockpit notes paste` posts to. The CLI takes the body on stdin; here it is an argument, so the text "
|
|
140
148
|
+ "travels in the request and is never echoed back to you.",
|
|
141
149
|
inputSchema: {
|
|
150
|
+
folder: z.string().min(1).max(2000).optional().describe("Folder path; creates missing ancestors."),
|
|
142
151
|
text: z.string().min(1).max(PASTE_MAX_CHARS).describe("The note itself."),
|
|
143
152
|
name: z.string().min(1).max(300).optional().describe("What to call it. Tower names it by date if you do not."),
|
|
144
153
|
exclude: z.string().min(1).max(2000).optional().describe("Anything to leave out, in plain words."),
|
|
@@ -154,6 +163,8 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
154
163
|
form.set("name", String(args.name));
|
|
155
164
|
if (args.exclude)
|
|
156
165
|
form.set("exclusions", String(args.exclude));
|
|
166
|
+
if (args.folder)
|
|
167
|
+
form.set("folder_path", String(args.folder));
|
|
157
168
|
return postForm(deps, session, "notes_paste", form);
|
|
158
169
|
}));
|
|
159
170
|
register("notes_share", {
|
|
@@ -182,15 +193,19 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
182
193
|
+ "own move box sends. A shelf is free text; notes_shelves lists the ones in use.",
|
|
183
194
|
inputSchema: {
|
|
184
195
|
note_id: z.string().min(1).max(200).describe("A note id."),
|
|
196
|
+
folder: z.string().min(1).max(2000).optional().describe("Destination folder path; / means Root. Leaves the shelf unchanged."),
|
|
185
197
|
to: z.string().min(1).max(200).optional().describe("The shelf to move it to."),
|
|
186
198
|
clear: z.boolean().optional().describe("Take it off every shelf instead."),
|
|
187
199
|
},
|
|
188
200
|
}, async (args) => withSession(deps, async (session) => {
|
|
189
201
|
const clearing = args.clear === true;
|
|
190
|
-
if (!clearing && !args.to) {
|
|
202
|
+
if (!clearing && !args.to && !args.folder) {
|
|
191
203
|
return errorResult("Refused (no_destination): say which shelf with `to`, or pass `clear: true`. Nothing was moved.");
|
|
192
204
|
}
|
|
193
|
-
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/move", { note_id: String(args.note_id ?? ""),
|
|
205
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/move", { note_id: String(args.note_id ?? ""),
|
|
206
|
+
...(args.folder ? { folder_path: String(args.folder) } : {}),
|
|
207
|
+
...(clearing || args.to ? { category: clearing ? "" : String(args.to) } : {}),
|
|
208
|
+
}, WRITE_DEADLINE_MS);
|
|
194
209
|
if (!response.ok)
|
|
195
210
|
return errorResult(doorFailureText("notes_move", response));
|
|
196
211
|
const body = response.body;
|
|
@@ -199,6 +214,31 @@ export function registerNotesWriteTools(server, deps) {
|
|
|
199
214
|
? textResult(text, { ok: true, shelf: body.shelf ?? null })
|
|
200
215
|
: errorResult(text);
|
|
201
216
|
}));
|
|
217
|
+
register("notes_place", {
|
|
218
|
+
title: "Say which shelf a Tower meeting note belongs on",
|
|
219
|
+
description: "Answers where one note files itself and WHY, from the meeting series it is part of, who was in the room "
|
|
220
|
+
+ "and its own name — no model call. Read-only unless `apply: true`, which files it there. It never "
|
|
221
|
+
+ "overrules a shelf a person typed: a note that already has one comes back as `already_shelved`.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
note_id: z.string().min(1).max(200).describe("A note id."),
|
|
224
|
+
apply: z.boolean().optional().describe("Actually put it there. Default: only say where."),
|
|
225
|
+
},
|
|
226
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
227
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/notes/place", { note_id: String(args.note_id ?? ""), apply: args.apply === true }, WRITE_DEADLINE_MS);
|
|
228
|
+
if (!response.ok)
|
|
229
|
+
return errorResult(doorFailureText("notes_place", response));
|
|
230
|
+
const body = response.body;
|
|
231
|
+
const text = [body.headline ?? "Tower answered without a sentence.", ...(body.lines ?? [])].join("\n");
|
|
232
|
+
return body.ok === true
|
|
233
|
+
? textResult(text, {
|
|
234
|
+
ok: true,
|
|
235
|
+
shelf: body.shelf ?? null,
|
|
236
|
+
suggested_shelf: body.suggestedShelf ?? null,
|
|
237
|
+
decided_by: body.decidedBy ?? null,
|
|
238
|
+
applied: body.applied === true,
|
|
239
|
+
})
|
|
240
|
+
: errorResult(text);
|
|
241
|
+
}));
|
|
202
242
|
}
|
|
203
243
|
/** Share and unshare are one door and one body; only the boolean differs. */
|
|
204
244
|
async function shareCall(deps, session, args, share) {
|
package/dist/readme-census.js
CHANGED
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
* and returns a string. No file writing here — that is the script's job, so
|
|
12
12
|
* this can be asserted without touching a disk.
|
|
13
13
|
*/
|
|
14
|
-
import { AWAITING_TWIN, MCP_TWINS, TERMINAL_ONLY, towerVerbs } from "./verb-census.js";
|
|
14
|
+
import { AWAITING_TWIN, MCP_TWINS, TERMINAL_ONLY, towerVerbs, typedSpelling } from "./verb-census.js";
|
|
15
15
|
export const CENSUS_BEGIN = "<!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->";
|
|
16
16
|
export const CENSUS_END = "<!-- END GENERATED verb census -->";
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* `cockpit docs read` → `` `cockpit docs read` ``; `jarvis --trace` keeps its
|
|
19
|
+
* flag. A census key whose grammar differs from the typed command is rendered
|
|
20
|
+
* as the command (`settings show` → `cockpit settings`), because this column is
|
|
21
|
+
* headed "CLI verb" and a reader takes it literally (BLI-4048).
|
|
22
|
+
*/
|
|
18
23
|
function spell(verb) {
|
|
19
|
-
return `\`cockpit ${verb}\``;
|
|
24
|
+
return `\`cockpit ${typedSpelling(verb)}\``;
|
|
20
25
|
}
|
|
21
26
|
export function renderCensusMarkdown() {
|
|
22
27
|
// Every verb the collector's own tables name, plus the one hand-entered mode
|
package/dist/server.js
CHANGED
|
@@ -23,6 +23,8 @@ import { registerSettingsTools } from "./settings-tools.js";
|
|
|
23
23
|
import { registerSettingsWriteTools } from "./settings-write-tools.js";
|
|
24
24
|
import { registerTeamWriteTools } from "./team-write-tools.js";
|
|
25
25
|
import { registerWorkTools } from "./work-tools.js";
|
|
26
|
+
import { registerCareersTools } from "./careers-tools.js";
|
|
27
|
+
import { registerUsageTools } from "./usage-tools.js";
|
|
26
28
|
export const PACKAGE_NAME = "@bli-cockpit/mcp";
|
|
27
29
|
export const PACKAGE_VERSION = "0.1.4";
|
|
28
30
|
// ---- input schemas (Zod raw shapes) -----------------------------------------
|
|
@@ -390,6 +392,8 @@ export function createServer(deps) {
|
|
|
390
392
|
// the docs/msg tools above, for the same reason: a coding session should
|
|
391
393
|
// file and move a Tower issue the way it files and moves a Linear one.
|
|
392
394
|
registerWorkTools(server, { fetchImpl: deps.fetchImpl });
|
|
395
|
+
registerCareersTools(server, deps);
|
|
396
|
+
registerUsageTools(server, { fetchImpl: deps.fetchImpl });
|
|
393
397
|
// BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
|
|
394
398
|
// the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
|
|
395
399
|
// has no twin (see mail-tools.ts's header — a credential is not a tool
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
3
|
+
import { doorFailureText, errorResult, registrarFor, textResult, withSession } from "./tool-result.js";
|
|
4
|
+
export function registerUsageTools(server, deps) {
|
|
5
|
+
registrarFor(server)("usage_people", {
|
|
6
|
+
title: "Read usage by person",
|
|
7
|
+
description: "Per-person Claude Code and Codex token usage, list-price equivalent, and extraction coverage.",
|
|
8
|
+
inputSchema: { since: z.string().optional().describe("30d or an ISO timestamp."), until: z.string().optional(), include_automated: z.boolean().optional() },
|
|
9
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
10
|
+
const query = new URLSearchParams({ since: String(args.since ?? "30d") });
|
|
11
|
+
if (args.until)
|
|
12
|
+
query.set("until", String(args.until));
|
|
13
|
+
if (args.include_automated)
|
|
14
|
+
query.set("include_automated", "1");
|
|
15
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/usage/people?${query}`);
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
return errorResult(doorFailureText("usage_people", response));
|
|
18
|
+
const coverage = response.body.coverage;
|
|
19
|
+
return textResult(`${coverage?.sessions_extracted ?? 0} of ${coverage?.sessions_observed ?? 0} sessions extracted.`, response.body);
|
|
20
|
+
}));
|
|
21
|
+
}
|
package/dist/verb-census.d.ts
CHANGED
|
@@ -67,6 +67,30 @@ export interface McpTwin {
|
|
|
67
67
|
}
|
|
68
68
|
/** A CLI verb and the MCP tool that does the same thing, over the same door. */
|
|
69
69
|
export declare const MCP_TWINS: Record<string, McpTwin>;
|
|
70
|
+
/**
|
|
71
|
+
* Census key → what a person actually types. (BLI-4048)
|
|
72
|
+
*
|
|
73
|
+
* The keys above are derived MECHANICALLY from each noun's `action` union, and
|
|
74
|
+
* for two nouns that union is not the grammar. `settings`'s positional is a
|
|
75
|
+
* SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
|
|
76
|
+
* the parser decides afterwards; `team`'s `device-list` is one action word
|
|
77
|
+
* spelled as two typed words, `team device list`. So `cockpit settings show`
|
|
78
|
+
* and `cockpit team device-list` were rendered into the README's "CLI verb"
|
|
79
|
+
* column and into `cli-caller-identity.md`, and the CLI refused all six.
|
|
80
|
+
*
|
|
81
|
+
* This is the display layer and nothing else: the keys, the twin rule and the
|
|
82
|
+
* suite are untouched. Every spelling here is proven typable —
|
|
83
|
+
* `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
|
|
84
|
+
* own parser, so a wrong entry fails the build rather than misleading a
|
|
85
|
+
* stranger. Where one tool serves several sections (`settings_set` writes any
|
|
86
|
+
* of them) the spelling names ONE of them and the door column names the rest.
|
|
87
|
+
*
|
|
88
|
+
* A verb whose key is already typable is absent from here on purpose; the
|
|
89
|
+
* shorter this map is, the closer the census keys are to the product.
|
|
90
|
+
*/
|
|
91
|
+
export declare const TYPED_SPELLING: Record<string, string>;
|
|
92
|
+
/** How a census key is written when a person is being told to type it. */
|
|
93
|
+
export declare function typedSpelling(verb: string): string;
|
|
70
94
|
/** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
|
|
71
95
|
export declare const TERMINAL_ONLY: Record<string, string>;
|
|
72
96
|
/**
|
package/dist/verb-census.js
CHANGED
|
@@ -209,6 +209,10 @@ export function memoryMcpToolNames() {
|
|
|
209
209
|
}
|
|
210
210
|
/** A CLI verb and the MCP tool that does the same thing, over the same door. */
|
|
211
211
|
export const MCP_TWINS = {
|
|
212
|
+
"careers list": { tool: "careers_list", door: "GET /api/careers/applications" },
|
|
213
|
+
"careers show": { tool: "careers_show", door: "GET /api/careers/applications/[id]" },
|
|
214
|
+
"careers rescreen": { tool: "careers_rescreen", door: "POST /api/careers/applications/[id]/rescreen" },
|
|
215
|
+
"usage people": { tool: "usage_people", door: "GET /api/usage/people" },
|
|
212
216
|
"docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
|
|
213
217
|
"docs tree": { tool: "docs_tree", door: "GET /api/docs/tree" },
|
|
214
218
|
"docs read": { tool: "docs_read", door: "GET /api/docs/documents/[id]" },
|
|
@@ -250,6 +254,10 @@ export const MCP_TWINS = {
|
|
|
250
254
|
"brief status": { tool: "brief_status", door: "GET /api/ops/brief-status" },
|
|
251
255
|
"brief rewrite": { tool: "brief_rewrite", door: "POST /api/jarvis/recompile" },
|
|
252
256
|
correct: { tool: "brief_correct", door: "POST /api/jarvis/corrections" },
|
|
257
|
+
"notes folders": { tool: "notes_folders", door: "GET /api/notes/folders" },
|
|
258
|
+
"notes mkdir": { tool: "notes_mkdir", door: "POST /api/notes/folders" },
|
|
259
|
+
"notes rename": { tool: "notes_rename", door: "PATCH /api/notes/folders/[id]" },
|
|
260
|
+
"notes rmdir": { tool: "notes_rmdir", door: "DELETE /api/notes/folders/[id]" },
|
|
253
261
|
"notes list": { tool: "notes_list", door: "GET /api/notes/library" },
|
|
254
262
|
"notes show": { tool: "notes_show", door: "GET /api/notes/library/[id]" },
|
|
255
263
|
"notes shelf": { tool: "notes_shelf", door: "GET /api/notes/shelf" },
|
|
@@ -259,6 +267,7 @@ export const MCP_TWINS = {
|
|
|
259
267
|
"notes share": { tool: "notes_share", door: "POST /api/notes/share" },
|
|
260
268
|
"notes unshare": { tool: "notes_unshare", door: "POST /api/notes/share (share=false)" },
|
|
261
269
|
"notes move": { tool: "notes_move", door: "POST /api/notes/move" },
|
|
270
|
+
"notes place": { tool: "notes_place", door: "POST /api/notes/place" },
|
|
262
271
|
"memory log": { tool: "memory_experience", door: "POST /api/memory/experience" },
|
|
263
272
|
// BLI-4047: the memory STORE at a terminal. The twins live on the second
|
|
264
273
|
// server, `bli-memory`, over the same four doors these CLI verbs call.
|
|
@@ -302,6 +311,39 @@ export const MCP_TWINS = {
|
|
|
302
311
|
// header of this file).
|
|
303
312
|
"jarvis --trace": { tool: "jarvis_trace", door: "GET /api/ops/trace/[id]" },
|
|
304
313
|
};
|
|
314
|
+
/**
|
|
315
|
+
* Census key → what a person actually types. (BLI-4048)
|
|
316
|
+
*
|
|
317
|
+
* The keys above are derived MECHANICALLY from each noun's `action` union, and
|
|
318
|
+
* for two nouns that union is not the grammar. `settings`'s positional is a
|
|
319
|
+
* SECTION (`personal|switches|models|env|cli-floor`) and the `action` is what
|
|
320
|
+
* the parser decides afterwards; `team`'s `device-list` is one action word
|
|
321
|
+
* spelled as two typed words, `team device list`. So `cockpit settings show`
|
|
322
|
+
* and `cockpit team device-list` were rendered into the README's "CLI verb"
|
|
323
|
+
* column and into `cli-caller-identity.md`, and the CLI refused all six.
|
|
324
|
+
*
|
|
325
|
+
* This is the display layer and nothing else: the keys, the twin rule and the
|
|
326
|
+
* suite are untouched. Every spelling here is proven typable —
|
|
327
|
+
* `scripts/doc-doors.test.mjs` runs the rendered table through the collector's
|
|
328
|
+
* own parser, so a wrong entry fails the build rather than misleading a
|
|
329
|
+
* stranger. Where one tool serves several sections (`settings_set` writes any
|
|
330
|
+
* of them) the spelling names ONE of them and the door column names the rest.
|
|
331
|
+
*
|
|
332
|
+
* A verb whose key is already typable is absent from here on purpose; the
|
|
333
|
+
* shorter this map is, the closer the census keys are to the product.
|
|
334
|
+
*/
|
|
335
|
+
export const TYPED_SPELLING = {
|
|
336
|
+
"settings show": "settings",
|
|
337
|
+
"settings list": "settings env list",
|
|
338
|
+
"settings set": "settings switches set",
|
|
339
|
+
"settings delete": "settings env delete",
|
|
340
|
+
"team device-list": "team device list",
|
|
341
|
+
"team device-revoke": "team device revoke",
|
|
342
|
+
};
|
|
343
|
+
/** How a census key is written when a person is being told to type it. */
|
|
344
|
+
export function typedSpelling(verb) {
|
|
345
|
+
return TYPED_SPELLING[verb] ?? verb;
|
|
346
|
+
}
|
|
305
347
|
/** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
|
|
306
348
|
export const TERMINAL_ONLY = {
|
|
307
349
|
"mail add-imap": "carries a Google app password. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a mailbox stays a thing a person does at a terminal (BLI-3708)",
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "bli-tower
|
|
5
|
+
"description": "bli-tower — an MCP server over BLI Cockpit's agent doors: JARVIS (jarvis_*), documents (docs_*), channels (msg_*), issues (work_*), the daily page (brief_*), meeting notes (notes_*), the ops board (ops_status/slack_*), settings/team/model, Scout and the workbook, plus the legacy event-stream tools (emit_event, get_ticket_timeline, get_active_tickets).",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"bli-cockpit-mcp": "./dist/index.js"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"readme": "npm run build && node scripts/write-readme-census.mjs"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
33
|
+
"@bli-cockpit/telemetry-core": "0.1.43",
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
35
|
"zod": "^4.3.6"
|
|
36
36
|
},
|