@bli-cockpit/mcp 0.1.80 → 0.1.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,7 +103,7 @@ about what an agent can reach.
103
103
 
104
104
  <!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
105
105
 
106
- **119 of 124 Tower verbs have an MCP twin.**
106
+ **123 of 128 Tower verbs have an MCP twin.**
107
107
  Each tool goes through the SAME door its CLI verb calls, with the same
108
108
  collector device token — never a second route and never a service-role
109
109
  reader. `src/verb-census.test.ts` fails when a verb is in none of the
@@ -150,6 +150,10 @@ three tables below.
150
150
  | `cockpit docs sync-status` | `docs_sync_status` | `GET /api/docs/notion-sync` |
151
151
  | `cockpit docs tree` | `docs_tree` | `GET /api/docs/tree` |
152
152
  | `cockpit docs update` | `docs_update` | `PATCH /api/docs/documents/[id]` |
153
+ | `cockpit github merge` | `github_merge` | `POST /api/github/pull-request (merge)` |
154
+ | `cockpit github pr` | `github_pr` | `POST /api/github/pull-request (read)` |
155
+ | `cockpit github rerun` | `github_rerun` | `POST /api/github/pull-request (rerun)` |
156
+ | `cockpit github retitle` | `github_retitle` | `POST /api/github/pull-request (retitle)` |
153
157
  | `cockpit issue comment` | `work_comment_issue` | `POST /api/work/issues/[id]/comments` |
154
158
  | `cockpit issue create` | `work_create_issue` | `POST /api/work/issues` |
155
159
  | `cockpit issue history` | `work_issue_history` | `GET /api/work/issues/[id]/history` |
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `github_pr` / `github_merge` / `github_retitle` / `github_rerun` (BLI-12385):
3
+ * the MCP twins of `cockpit github pr|merge|retitle|rerun`, over the same door
4
+ * (`POST /api/github/pull-request`) with the same device token.
5
+ *
6
+ * The door runs JARVIS's own GitHub implementations, so the lock is the same:
7
+ * the caller's OWN GitHub login (on the roster) must hold write, maintain or
8
+ * admin on the repo, and a change is TWO STEPS. The first call (no `confirm`)
9
+ * changes nothing and returns what would happen plus an eight-character code;
10
+ * the second carries `confirm`. The code is signed to the caller, repo, pull
11
+ * request and exact change. An agent must pass a code only after the PERSON has
12
+ * sent it back: whether to merge is theirs, and the descriptions say so.
13
+ * A refusal keeps the door's reason label and sentence verbatim.
14
+ */
15
+ import { type ToolDeps } from "./tool-result.js";
16
+ export declare function registerGithubTools(server: {
17
+ registerTool: (...args: never[]) => unknown;
18
+ }, deps: ToolDeps): void;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `github_pr` / `github_merge` / `github_retitle` / `github_rerun` (BLI-12385):
3
+ * the MCP twins of `cockpit github pr|merge|retitle|rerun`, over the same door
4
+ * (`POST /api/github/pull-request`) with the same device token.
5
+ *
6
+ * The door runs JARVIS's own GitHub implementations, so the lock is the same:
7
+ * the caller's OWN GitHub login (on the roster) must hold write, maintain or
8
+ * admin on the repo, and a change is TWO STEPS. The first call (no `confirm`)
9
+ * changes nothing and returns what would happen plus an eight-character code;
10
+ * the second carries `confirm`. The code is signed to the caller, repo, pull
11
+ * request and exact change. An agent must pass a code only after the PERSON has
12
+ * sent it back: whether to merge is theirs, and the descriptions say so.
13
+ * A refusal keeps the door's reason label and sentence verbatim.
14
+ */
15
+ import { z } from "zod";
16
+ import { callAgentDoor } from "./agent-door.js";
17
+ import { dataResult, doorFailureText, errorResult, registrarFor, textResult, withSession, } from "./tool-result.js";
18
+ const PATH = "/api/github/pull-request";
19
+ const TWO_STEPS = "TWO STEPS: call it WITHOUT confirm first. That changes nothing and returns what would happen plus an "
20
+ + "eight-character confirmation code; show the person exactly that and ask them to send the code back. Only then "
21
+ + "call again with the SAME arguments plus confirm set to the code THE PERSON gave you. Never invent a code, "
22
+ + "never reuse one the person did not send, and never change the arguments between the two calls (the code is "
23
+ + "signed to the exact change). It runs only if the person's OWN GitHub login has write, maintain or admin on the "
24
+ + "repo; otherwise it refuses with github_login_unknown or github_permission_insufficient: relay that as written. "
25
+ + "Only BLI-INC repositories.";
26
+ const repoField = z.string().min(1).max(100).describe("`owner/repo` or a bare repo name in BLI-INC, e.g. bli-cockpit.");
27
+ const numberField = z.number().int().min(1).describe("The pull request number.");
28
+ const confirmField = z
29
+ .string()
30
+ .min(1)
31
+ .max(64)
32
+ .optional()
33
+ .describe("The code the preview returned, which the PERSON sent back. Omit on the first call.");
34
+ export function registerGithubTools(server, deps) {
35
+ const register = registrarFor(server);
36
+ async function press(tool, session, body) {
37
+ const response = await callAgentDoor(session, deps.fetchImpl, "POST", PATH, body);
38
+ if (!response.ok)
39
+ return errorResult(doorFailureText(tool, response));
40
+ const result = (response.body["result"] ?? {});
41
+ if (result["status"] === "needs_confirmation") {
42
+ return textResult(`Nothing has been changed. Would ${String(result["preview"] ?? "make this change")} `
43
+ + `To go ahead, the person must send back the code ${String(result["confirmationCode"] ?? "")}; `
44
+ + `then call ${tool} again with the same arguments and confirm set to that code.`, result);
45
+ }
46
+ if (result["status"] === "done")
47
+ return textResult(String(result["sayThis"] ?? "Done."), result);
48
+ return dataResult("GitHub pull request", result);
49
+ }
50
+ register("github_pr", {
51
+ title: "Read one pull request from GitHub",
52
+ description: "GitHub's own live record of ONE pull request in BLI-INC: merged (and by whom), open/closed, draft, mergeable, "
53
+ + "title and CI. GitHub is the source of truth: when somebody asserts a state, read it here. Pass `claim` "
54
+ + "(merged, open, closed, ci_green or mergeable) to get GitHub's verdict on that assertion with the correction "
55
+ + "sentence. `cockpit github pr`, same door.",
56
+ inputSchema: {
57
+ repo: repoField,
58
+ pull_number: numberField,
59
+ claim: z.enum(["merged", "open", "closed", "ci_green", "mergeable"]).optional().describe("The state somebody asserted."),
60
+ },
61
+ }, async (args) => withSession(deps, (session) => press("github_pr", session, {
62
+ action: "read",
63
+ repo: String(args.repo),
64
+ pullNumber: args.pull_number,
65
+ ...(args.claim ? { claim: args.claim } : {}),
66
+ })));
67
+ register("github_merge", {
68
+ title: "Merge a pull request (two steps, caller's own GitHub access)",
69
+ description: `Merge ONE pull request in BLI-INC, squash by default. \`cockpit github merge\`, same door. ${TWO_STEPS}`,
70
+ inputSchema: {
71
+ repo: repoField,
72
+ pull_number: numberField,
73
+ method: z.enum(["squash", "merge", "rebase"]).optional().describe("Defaults to squash."),
74
+ confirm: confirmField,
75
+ },
76
+ }, async (args) => withSession(deps, (session) => press("github_merge", session, {
77
+ action: "merge",
78
+ repo: String(args.repo),
79
+ pullNumber: args.pull_number,
80
+ ...(args.method ? { method: args.method } : {}),
81
+ ...(args.confirm ? { confirm: String(args.confirm) } : {}),
82
+ })));
83
+ register("github_retitle", {
84
+ title: "Retitle a pull request (two steps, caller's own GitHub access)",
85
+ description: `Change the title of ONE pull request in BLI-INC. \`cockpit github retitle\`, same door. ${TWO_STEPS}`,
86
+ inputSchema: {
87
+ repo: repoField,
88
+ pull_number: numberField,
89
+ title: z.string().min(1).max(256).describe("The new title."),
90
+ confirm: confirmField,
91
+ },
92
+ }, async (args) => withSession(deps, (session) => press("github_retitle", session, {
93
+ action: "retitle",
94
+ repo: String(args.repo),
95
+ pullNumber: args.pull_number,
96
+ title: String(args.title),
97
+ ...(args.confirm ? { confirm: String(args.confirm) } : {}),
98
+ })));
99
+ register("github_rerun", {
100
+ title: "Re-run failed workflow jobs (two steps, caller's own GitHub access)",
101
+ description: "Re-run the FAILED jobs of workflow runs in BLI-INC: pass pull_number for every failed workflow on that pull "
102
+ + `request's head commit, or run_id for one run (exactly one). \`cockpit github rerun\`, same door. ${TWO_STEPS}`,
103
+ inputSchema: {
104
+ repo: repoField,
105
+ pull_number: numberField.optional(),
106
+ run_id: z.number().int().min(1).optional().describe("One workflow run, instead of pull_number."),
107
+ confirm: confirmField,
108
+ },
109
+ }, async (args) => withSession(deps, (session) => press("github_rerun", session, {
110
+ action: "rerun",
111
+ repo: String(args.repo),
112
+ ...(args.pull_number ? { pullNumber: args.pull_number } : {}),
113
+ ...(args.run_id ? { runId: args.run_id } : {}),
114
+ ...(args.confirm ? { confirm: String(args.confirm) } : {}),
115
+ })));
116
+ }
package/dist/ops-tools.js CHANGED
@@ -286,8 +286,15 @@ export function registerOpsTools(server, deps) {
286
286
  // anything" — and the messages/note print exactly once, not twice.
287
287
  const rawSummary = typeof read["summary"] === "string" ? read["summary"] : `${messages.length} message(s).`;
288
288
  const summary = rawSummary.split("\n")[0] ?? rawSummary;
289
- const note = typeof read["note"] === "string" && read["note"] ? `\n\n${read["note"]}` : "";
290
- return textResult(`${summary}${lines ? `\n\n${lines}` : ""}${note}`, {
289
+ // BLI-12381: a refusal's summary IS its note; print it once.
290
+ const noteText = typeof read["note"] === "string" && read["note"] && read["note"] !== summary ? read["note"] : null;
291
+ const note = noteText ? `\n\n${noteText}` : "";
292
+ // BLI-12381: an unknown, uncovered or empty read leads with its reason
293
+ // label, so a zero never reads as "nobody said anything".
294
+ const label = typeof read["status"] === "string" && (read["status"] !== "ok" || messages.length === 0)
295
+ ? `[slack_read status=${read["status"]} reason=${String(read["reason"] ?? "?")}] `
296
+ : "";
297
+ return textResult(`${label}${summary}${lines ? `\n\n${lines}` : ""}${note}`, {
291
298
  status: read["status"] ?? null,
292
299
  reason: read["reason"] ?? null,
293
300
  messages,
package/dist/server.js CHANGED
@@ -23,6 +23,7 @@ import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
23
23
  import { registerMemoryProfileTools } from "./memory-profile-tools.js";
24
24
  import { registerMemoryRequeueTools } from "./memory-requeue-tools.js";
25
25
  import { registerModelsTools } from "./models-tools.js";
26
+ import { registerGithubTools } from "./github-tools.js";
26
27
  import { registerOpsTools } from "./ops-tools.js";
27
28
  import { registerPagesTools } from "./pages-tools.js";
28
29
  import { registerSearchTool } from "./search-tool.js";
@@ -429,6 +430,8 @@ export function createServer(deps) {
429
430
  // BLI-12378: the twins of `cockpit rename list|resolve`, the human's call on
430
431
  // whether a correction becomes a rule.
431
432
  registerRenameTools(server, deps);
433
+ // BLI-12385: GitHub PR read, and merge / retitle / re-run behind the two-step code.
434
+ registerGithubTools(server, deps);
432
435
  registerUsageTools(server, { fetchImpl: deps.fetchImpl });
433
436
  // BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
434
437
  // the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
@@ -10,7 +10,7 @@
10
10
  * from `src/` under Node's type stripping, so it may import nothing.
11
11
  */
12
12
  /** Tools that change nothing. The smoke calls every one. */
13
- export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "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", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check"];
13
+ export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "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", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check"];
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 ["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", "mail_send", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "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_device_revoke"];
24
+ export declare const WRITE_TOOLS: readonly ["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_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "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_device_revoke"];
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
  /**
@@ -30,6 +30,7 @@ export const READ_TOOLS = [
30
30
  "careers_takehome_show",
31
31
  "careers_settings_show",
32
32
  "rename_pending",
33
+ "github_pr",
33
34
  "usage_people",
34
35
  "usage_sessions",
35
36
  "mail_accounts",
@@ -111,6 +112,9 @@ export const WRITE_TOOLS = [
111
112
  "careers_takehome_set",
112
113
  "careers_settings_set",
113
114
  "rename_resolve",
115
+ "github_merge",
116
+ "github_retitle",
117
+ "github_rerun",
114
118
  "mail_send",
115
119
  "mail_sync",
116
120
  "mail_detach",
@@ -234,6 +234,11 @@ export const MCP_TWINS = {
234
234
  // BLI-12378: the human's call on whether a correction becomes a rule.
235
235
  "rename list": { tool: "rename_pending", door: "GET /api/jarvis/rename-scope" },
236
236
  "rename resolve": { tool: "rename_resolve", door: "POST /api/jarvis/rename-scope" },
237
+ // BLI-12385: GitHub PR read and the two-step merge / retitle / re-run.
238
+ "github pr": { tool: "github_pr", door: "POST /api/github/pull-request (read)" },
239
+ "github merge": { tool: "github_merge", door: "POST /api/github/pull-request (merge)" },
240
+ "github retitle": { tool: "github_retitle", door: "POST /api/github/pull-request (retitle)" },
241
+ "github rerun": { tool: "github_rerun", door: "POST /api/github/pull-request (rerun)" },
237
242
  "usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
238
243
  "usage people": { tool: "usage_people", door: "GET /api/usage/people" },
239
244
  "docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.80",
3
+ "version": "0.1.82",
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",