@bli-cockpit/mcp 0.1.5 → 0.1.7
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 +10 -1
- package/dist/mail-tools.d.ts +31 -0
- package/dist/mail-tools.js +204 -0
- package/dist/server.js +6 -0
- package/dist/verb-census.js +9 -0
- package/package.json +2 -2
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
|
+
**64 of 66 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
|
|
@@ -61,6 +61,14 @@ three tables below.
|
|
|
61
61
|
| `cockpit issue update` | `work_update_issue` | `PATCH /api/work/issues/[id]` |
|
|
62
62
|
| `cockpit jarvis` | `jarvis_ask` | `POST /api/jarvis/cli` |
|
|
63
63
|
| `cockpit jarvis --trace` | `jarvis_trace` | `GET /api/ops/trace/[id]` |
|
|
64
|
+
| `cockpit mail accounts` | `mail_accounts` | `GET /api/mail/accounts` |
|
|
65
|
+
| `cockpit mail attachment` | `mail_attachment` | `GET /api/mail/attachments/[id]?meta=1` |
|
|
66
|
+
| `cockpit mail detach` | `mail_detach` | `DELETE /api/mail/accounts` |
|
|
67
|
+
| `cockpit mail inbox` | `mail_inbox` | `GET /api/mail/inbox` |
|
|
68
|
+
| `cockpit mail read` | `mail_read` | `GET /api/mail/threads/[id]` |
|
|
69
|
+
| `cockpit mail search` | `mail_search` | `GET /api/mail/search` |
|
|
70
|
+
| `cockpit mail send` | `mail_send` | `POST /api/mail/send` |
|
|
71
|
+
| `cockpit mail sync` | `mail_sync` | `POST /api/mail/accounts/[id]/sync` |
|
|
64
72
|
| `cockpit model set` | `model_set` | `POST /api/settings/jarvis-model` |
|
|
65
73
|
| `cockpit model show` | `model_show` | `GET /api/settings/jarvis-model` |
|
|
66
74
|
| `cockpit msg channels` | `msg_channels` | `GET /api/msg/channels` |
|
|
@@ -112,6 +120,7 @@ A claim about the verb's nature, not a backlog.
|
|
|
112
120
|
| CLI verb | Why it can never have a twin |
|
|
113
121
|
| --- | --- |
|
|
114
122
|
| `cockpit 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) |
|
|
123
|
+
| `cockpit 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) |
|
|
115
124
|
|
|
116
125
|
<!-- END GENERATED verb census -->
|
|
117
126
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mail_*` MCP tools (BLI-3708) — the mailboxes a person attached, on the
|
|
3
|
+
* `bli-tower` server, over the same `/api/mail/**` doors `cockpit mail` and
|
|
4
|
+
* the browser call, authenticated with this machine's collector device token.
|
|
5
|
+
*
|
|
6
|
+
* The point, in one sentence: an agent working on somebody's behalf should be
|
|
7
|
+
* able to READ the mail that explains a ticket — the invoice, the vendor's
|
|
8
|
+
* reply, the bank notice — and send a reply, without a browser and without a
|
|
9
|
+
* second copy of anybody's credentials.
|
|
10
|
+
*
|
|
11
|
+
* TWO DELIBERATE ASYMMETRIES with the CLI, both recorded in `verb-census.ts`:
|
|
12
|
+
*
|
|
13
|
+
* - **`cockpit mail add-imap` has NO MCP twin.** It carries a Google app
|
|
14
|
+
* password, and the CLI takes that on stdin precisely so it never lands in
|
|
15
|
+
* an argument list. An MCP tool argument IS an argument list — it travels
|
|
16
|
+
* through a model's context window and whatever transcript store sits behind
|
|
17
|
+
* it. Attaching a mailbox stays a thing a person does at a terminal.
|
|
18
|
+
* - **`mail_attachment` answers METADATA, not bytes.** The CLI writes the file
|
|
19
|
+
* to `--out <path>`; an MCP tool has nowhere to put ten megabytes but the
|
|
20
|
+
* conversation. It returns the same `?meta=1` read the CLI uses to describe
|
|
21
|
+
* a file before downloading it, and names the command that fetches it.
|
|
22
|
+
*
|
|
23
|
+
* Same session discipline as `work-tools.ts`: the session is loaded fresh per
|
|
24
|
+
* call, so a machine with no `cockpit login` pairing still serves this
|
|
25
|
+
* server's other tools and only a `mail_` call fails, by name.
|
|
26
|
+
*/
|
|
27
|
+
import { type ToolDeps } from "./tool-result.js";
|
|
28
|
+
export type MailDeps = ToolDeps;
|
|
29
|
+
export declare function registerMailTools(server: {
|
|
30
|
+
registerTool: (...args: never[]) => unknown;
|
|
31
|
+
}, deps: MailDeps): void;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mail_*` MCP tools (BLI-3708) — the mailboxes a person attached, on the
|
|
3
|
+
* `bli-tower` server, over the same `/api/mail/**` doors `cockpit mail` and
|
|
4
|
+
* the browser call, authenticated with this machine's collector device token.
|
|
5
|
+
*
|
|
6
|
+
* The point, in one sentence: an agent working on somebody's behalf should be
|
|
7
|
+
* able to READ the mail that explains a ticket — the invoice, the vendor's
|
|
8
|
+
* reply, the bank notice — and send a reply, without a browser and without a
|
|
9
|
+
* second copy of anybody's credentials.
|
|
10
|
+
*
|
|
11
|
+
* TWO DELIBERATE ASYMMETRIES with the CLI, both recorded in `verb-census.ts`:
|
|
12
|
+
*
|
|
13
|
+
* - **`cockpit mail add-imap` has NO MCP twin.** It carries a Google app
|
|
14
|
+
* password, and the CLI takes that on stdin precisely so it never lands in
|
|
15
|
+
* an argument list. An MCP tool argument IS an argument list — it travels
|
|
16
|
+
* through a model's context window and whatever transcript store sits behind
|
|
17
|
+
* it. Attaching a mailbox stays a thing a person does at a terminal.
|
|
18
|
+
* - **`mail_attachment` answers METADATA, not bytes.** The CLI writes the file
|
|
19
|
+
* to `--out <path>`; an MCP tool has nowhere to put ten megabytes but the
|
|
20
|
+
* conversation. It returns the same `?meta=1` read the CLI uses to describe
|
|
21
|
+
* a file before downloading it, and names the command that fetches it.
|
|
22
|
+
*
|
|
23
|
+
* Same session discipline as `work-tools.ts`: the session is loaded fresh per
|
|
24
|
+
* call, so a machine with no `cockpit login` pairing still serves this
|
|
25
|
+
* server's other tools and only a `mail_` call fails, by name.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
29
|
+
import { CONFIRM_INPUT, doorFailureText, errorResult, queryString, registrarFor, textResult, unconfirmed, withSession, } from "./tool-result.js";
|
|
30
|
+
function messageLine(message) {
|
|
31
|
+
const from = message.from_address?.address ?? "(no sender)";
|
|
32
|
+
return `${message.is_unread ? "*" : " "} ${message.internal_date.slice(0, 16)} ${from} ${message.subject ?? "(no subject)"} [thread ${message.thread_id ?? "?"}]`;
|
|
33
|
+
}
|
|
34
|
+
const ADDRESS = z.string().email().max(320);
|
|
35
|
+
export function registerMailTools(server, deps) {
|
|
36
|
+
const register = registrarFor(server);
|
|
37
|
+
register("mail_accounts", {
|
|
38
|
+
title: "List attached mailboxes",
|
|
39
|
+
description: "Every mailbox attached to Tower that you may see — address, provider (gmail_oauth or imap), health and last sync. "
|
|
40
|
+
+ "One person may have several; there is no default mailbox anywhere in this surface, so start here when you need an account id.",
|
|
41
|
+
inputSchema: {},
|
|
42
|
+
}, async () => withSession(deps, async (session) => {
|
|
43
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/mail/accounts");
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
return errorResult(doorFailureText("mail_accounts", response));
|
|
46
|
+
const accounts = (Array.isArray(response.body.accounts) ? response.body.accounts : []);
|
|
47
|
+
const lines = accounts
|
|
48
|
+
.map((account) => `${account.address} ${account.provider} ${account.status}${account.status_reason ? ` (${account.status_reason})` : ""} ${account.id}`)
|
|
49
|
+
.join("\n");
|
|
50
|
+
return textResult(accounts.length === 0
|
|
51
|
+
? "No mailboxes are attached. A person attaches one with `cockpit mail add-imap` (personal) or the browser (work)."
|
|
52
|
+
: `${accounts.length} mailbox(es).\n${lines}`, { accounts });
|
|
53
|
+
}));
|
|
54
|
+
register("mail_inbox", {
|
|
55
|
+
title: "Read the unified inbox",
|
|
56
|
+
description: "Messages across every mailbox you may read, newest first. Never a body — call mail_read on a thread for that. "
|
|
57
|
+
+ "`account_id` narrows to one mailbox; `unread` and `label` filter; `before` (an ISO instant) pages backwards.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
account_id: z.string().uuid().optional(),
|
|
60
|
+
unread: z.boolean().optional(),
|
|
61
|
+
label: z.string().min(1).max(100).optional(),
|
|
62
|
+
before: z.string().min(1).max(40).optional(),
|
|
63
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
64
|
+
},
|
|
65
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
66
|
+
const query = new URLSearchParams();
|
|
67
|
+
if (args.account_id)
|
|
68
|
+
query.set("account_id", String(args.account_id));
|
|
69
|
+
if (args.unread === true)
|
|
70
|
+
query.set("unread", "1");
|
|
71
|
+
if (args.label)
|
|
72
|
+
query.set("label", String(args.label));
|
|
73
|
+
if (args.before)
|
|
74
|
+
query.set("before", String(args.before));
|
|
75
|
+
if (args.limit)
|
|
76
|
+
query.set("limit", String(args.limit));
|
|
77
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/mail/inbox${queryString(query)}`);
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
return errorResult(doorFailureText("mail_inbox", response));
|
|
80
|
+
const messages = (Array.isArray(response.body.messages) ? response.body.messages : []);
|
|
81
|
+
return textResult(messages.length === 0
|
|
82
|
+
? "No mail matches that."
|
|
83
|
+
: `${messages.length} message(s).\n${messages.map(messageLine).join("\n")}`, { messages, accounts: response.body.accounts ?? [] });
|
|
84
|
+
}));
|
|
85
|
+
register("mail_read", {
|
|
86
|
+
title: "Read one conversation",
|
|
87
|
+
description: "One thread with every message's text body, oldest first. Thread ids come from mail_inbox or mail_search. "
|
|
88
|
+
+ "Read the thread before saying what somebody wrote — a snippet is the first 240 characters and nothing more.",
|
|
89
|
+
inputSchema: { thread_id: z.string().uuid() },
|
|
90
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
91
|
+
const threadId = String(args.thread_id ?? "");
|
|
92
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/mail/threads/${encodeURIComponent(threadId)}`);
|
|
93
|
+
if (!response.ok)
|
|
94
|
+
return errorResult(doorFailureText("mail_read", response));
|
|
95
|
+
const thread = response.body.thread;
|
|
96
|
+
const messages = (Array.isArray(response.body.messages) ? response.body.messages : []);
|
|
97
|
+
const rendered = messages
|
|
98
|
+
.map((message) => `--- ${message.internal_date} ${message.from_address?.address ?? "(no sender)"} [${message.id}]\n`
|
|
99
|
+
+ (message.body_text ?? message.snippet ?? "(no text body — this message is HTML only)").trim())
|
|
100
|
+
.join("\n\n");
|
|
101
|
+
return textResult(`${thread?.subject ?? "(no subject)"} — ${thread?.account_address ?? "?"}, ${messages.length} message(s)\n\n${rendered}`, { thread, messages });
|
|
102
|
+
}));
|
|
103
|
+
register("mail_search", {
|
|
104
|
+
title: "Search your mail",
|
|
105
|
+
description: "Full text over the subject and plain-text body of every message you may read. Quoted phrases and OR work the way "
|
|
106
|
+
+ "they do in a search box. Returns headers only; call mail_read on a hit's thread to read it.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
query: z.string().min(1).max(400),
|
|
109
|
+
account_id: z.string().uuid().optional(),
|
|
110
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
111
|
+
},
|
|
112
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
113
|
+
const query = new URLSearchParams({ q: String(args.query ?? "") });
|
|
114
|
+
if (args.account_id)
|
|
115
|
+
query.set("account_id", String(args.account_id));
|
|
116
|
+
if (args.limit)
|
|
117
|
+
query.set("limit", String(args.limit));
|
|
118
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/mail/search${queryString(query)}`);
|
|
119
|
+
if (!response.ok)
|
|
120
|
+
return errorResult(doorFailureText("mail_search", response));
|
|
121
|
+
const matches = (Array.isArray(response.body.matches) ? response.body.matches : []);
|
|
122
|
+
return textResult(matches.length === 0
|
|
123
|
+
? `Nothing in your mail matched "${String(args.query)}".`
|
|
124
|
+
: `${matches.length} match(es).\n${matches.map(messageLine).join("\n")}`, { matches });
|
|
125
|
+
}));
|
|
126
|
+
register("mail_send", {
|
|
127
|
+
title: "Send mail from one mailbox",
|
|
128
|
+
description: "Sends a message. `account_id` is REQUIRED and is the address it goes out from — with several mailboxes attached, "
|
|
129
|
+
+ "that is never something to guess. `in_reply_to_message_id` (a message id from mail_read) threads the reply. "
|
|
130
|
+
+ "This actually sends mail to real people: confirm must be true.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
account_id: z.string().uuid(),
|
|
133
|
+
to: z.array(ADDRESS).min(1).max(50),
|
|
134
|
+
cc: z.array(ADDRESS).max(50).optional(),
|
|
135
|
+
subject: z.string().max(998),
|
|
136
|
+
text: z.string().min(1).max(500_000),
|
|
137
|
+
in_reply_to_message_id: z.string().uuid().optional(),
|
|
138
|
+
confirm: CONFIRM_INPUT,
|
|
139
|
+
},
|
|
140
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
141
|
+
const refusal = unconfirmed(args, "Sending mail reaches real people and cannot be recalled.");
|
|
142
|
+
if (refusal)
|
|
143
|
+
return refusal;
|
|
144
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/mail/send", {
|
|
145
|
+
account_id: args.account_id,
|
|
146
|
+
to: args.to.map((address) => ({ address })),
|
|
147
|
+
...(Array.isArray(args.cc) && args.cc.length > 0
|
|
148
|
+
? { cc: args.cc.map((address) => ({ address })) }
|
|
149
|
+
: {}),
|
|
150
|
+
subject: args.subject ?? "",
|
|
151
|
+
text: args.text,
|
|
152
|
+
...(args.in_reply_to_message_id ? { in_reply_to_message_id: args.in_reply_to_message_id } : {}),
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok)
|
|
155
|
+
return errorResult(doorFailureText("mail_send", response));
|
|
156
|
+
const sent = response.body.sent;
|
|
157
|
+
return textResult(`Sent from ${sent?.address ?? "that mailbox"} to ${args.to.join(", ")}${sent?.threaded ? " (as a reply)" : ""}.`, { sent });
|
|
158
|
+
}));
|
|
159
|
+
register("mail_attachment", {
|
|
160
|
+
title: "Describe an attachment",
|
|
161
|
+
description: "The filename, mime type and size of one attachment. Attachments are POINTERS in Tower — nothing is stored — and "
|
|
162
|
+
+ "the bytes are not returned here: `cockpit mail attachment <id> --out <path>` downloads the file at a terminal.",
|
|
163
|
+
inputSchema: { attachment_id: z.string().uuid() },
|
|
164
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
165
|
+
const id = String(args.attachment_id ?? "");
|
|
166
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/mail/attachments/${encodeURIComponent(id)}?meta=1`);
|
|
167
|
+
if (!response.ok)
|
|
168
|
+
return errorResult(doorFailureText("mail_attachment", response));
|
|
169
|
+
const attachment = response.body.attachment;
|
|
170
|
+
return textResult(`${attachment?.filename ?? "(unnamed)"} — ${attachment?.mime_type ?? "unknown type"}, ${attachment?.size_bytes ?? "?"} bytes. `
|
|
171
|
+
+ `Download it with: cockpit mail attachment ${id} --out <path>`, { attachment });
|
|
172
|
+
}));
|
|
173
|
+
register("mail_sync", {
|
|
174
|
+
title: "Read a mailbox now",
|
|
175
|
+
description: "Runs one sync pass on one mailbox instead of waiting for the 15-minute cron, and answers with what landed or the "
|
|
176
|
+
+ "reason it did not (token_revoked, history_gap, auth_failed, quota…). Only the mailbox's OWNER may do this.",
|
|
177
|
+
inputSchema: { account_id: z.string().uuid() },
|
|
178
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
179
|
+
const id = String(args.account_id ?? "");
|
|
180
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", `/api/mail/accounts/${encodeURIComponent(id)}/sync`);
|
|
181
|
+
if (!response.ok)
|
|
182
|
+
return errorResult(doorFailureText("mail_sync", response));
|
|
183
|
+
const run = response.body.run;
|
|
184
|
+
if (run?.reason && run.reason !== "ok") {
|
|
185
|
+
return errorResult(`That mailbox did not sync (${run.reason})${run.detail ? `: ${run.detail}` : ""}.`);
|
|
186
|
+
}
|
|
187
|
+
return textResult(`Synced: ${run?.messagesAdded ?? 0} new, ${run?.messagesUpdated ?? 0} updated.`, { run: run ?? {} });
|
|
188
|
+
}));
|
|
189
|
+
register("mail_detach", {
|
|
190
|
+
title: "Detach a mailbox",
|
|
191
|
+
description: "Removes a mailbox from Tower along with its stored mail and its credential. The mail itself is untouched at the "
|
|
192
|
+
+ "provider — this unhooks Tower, it does not delete anybody's email. Irreversible here: confirm must be true.",
|
|
193
|
+
inputSchema: { account_id: z.string().uuid(), confirm: CONFIRM_INPUT },
|
|
194
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
195
|
+
const refusal = unconfirmed(args, "Detaching removes this mailbox's stored mail from Tower.");
|
|
196
|
+
if (refusal)
|
|
197
|
+
return refusal;
|
|
198
|
+
const id = String(args.account_id ?? "");
|
|
199
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "DELETE", `/api/mail/accounts?account_id=${encodeURIComponent(id)}`);
|
|
200
|
+
if (!response.ok)
|
|
201
|
+
return errorResult(doorFailureText("mail_detach", response));
|
|
202
|
+
return textResult(`Detached ${id}. Its stored mail and its credential went with it.`, { detached: id });
|
|
203
|
+
}));
|
|
204
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerBriefTools } from "./brief-tools.js";
|
|
|
10
10
|
import { registerBriefWriteTools } from "./brief-write-tools.js";
|
|
11
11
|
import { registerDocsMsgTools } from "./docs-msg-tools.js";
|
|
12
12
|
import { registerJarvisTools } from "./jarvis-tools.js";
|
|
13
|
+
import { registerMailTools } from "./mail-tools.js";
|
|
13
14
|
import { registerNotesTools } from "./notes-tools.js";
|
|
14
15
|
import { registerNotesWriteTools } from "./notes-write-tools.js";
|
|
15
16
|
import { registerOpsTools } from "./ops-tools.js";
|
|
@@ -386,6 +387,11 @@ export function createServer(deps) {
|
|
|
386
387
|
// the docs/msg tools above, for the same reason: a coding session should
|
|
387
388
|
// file and move a Tower issue the way it files and moves a Linear one.
|
|
388
389
|
registerWorkTools(server, { fetchImpl: deps.fetchImpl });
|
|
390
|
+
// BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
|
|
391
|
+
// the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
|
|
392
|
+
// has no twin (see mail-tools.ts's header — a credential is not a tool
|
|
393
|
+
// argument).
|
|
394
|
+
registerMailTools(server, { fetchImpl: deps.fetchImpl });
|
|
389
395
|
// BLI-3732: `jarvis_*` — the assistant itself, on the same device-token path
|
|
390
396
|
// as the three families above, over the doors `cockpit jarvis` already
|
|
391
397
|
// calls. It is the last Tower surface that had a CLI door and no MCP one.
|
package/dist/verb-census.js
CHANGED
|
@@ -148,6 +148,14 @@ export const MCP_TWINS = {
|
|
|
148
148
|
"issue comment": { tool: "work_comment_issue", door: "POST /api/work/issues/[id]/comments" },
|
|
149
149
|
"issue history": { tool: "work_issue_history", door: "GET /api/work/issues/[id]/history" },
|
|
150
150
|
"project list": { tool: "work_list_projects", door: "GET /api/work/projects" },
|
|
151
|
+
"mail accounts": { tool: "mail_accounts", door: "GET /api/mail/accounts" },
|
|
152
|
+
"mail inbox": { tool: "mail_inbox", door: "GET /api/mail/inbox" },
|
|
153
|
+
"mail read": { tool: "mail_read", door: "GET /api/mail/threads/[id]" },
|
|
154
|
+
"mail search": { tool: "mail_search", door: "GET /api/mail/search" },
|
|
155
|
+
"mail send": { tool: "mail_send", door: "POST /api/mail/send" },
|
|
156
|
+
"mail attachment": { tool: "mail_attachment", door: "GET /api/mail/attachments/[id]?meta=1" },
|
|
157
|
+
"mail sync": { tool: "mail_sync", door: "POST /api/mail/accounts/[id]/sync" },
|
|
158
|
+
"mail detach": { tool: "mail_detach", door: "DELETE /api/mail/accounts" },
|
|
151
159
|
"brief read": { tool: "brief_read", door: "GET /api/jarvis/brief" },
|
|
152
160
|
"brief history": { tool: "brief_history", door: "GET /api/jarvis/brief?history=1" },
|
|
153
161
|
"brief status": { tool: "brief_status", door: "GET /api/ops/brief-status" },
|
|
@@ -194,6 +202,7 @@ export const MCP_TWINS = {
|
|
|
194
202
|
};
|
|
195
203
|
/** Verbs that can never have an MCP twin, and why. A claim, not a backlog. */
|
|
196
204
|
export const TERMINAL_ONLY = {
|
|
205
|
+
"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)",
|
|
197
206
|
"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
207
|
};
|
|
199
208
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"readme": "npm run build && node scripts/write-readme-census.mjs"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.30",
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
34
34
|
"zod": "^4.3.6"
|
|
35
35
|
},
|