@bli-cockpit/mcp 0.1.103 → 0.1.105

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `tower_search` — one bar over five corpora, as an MCP tool (BLI-3728).
2
+ * `tower_search` — one bar over every corpus, as an MCP tool (BLI-3728).
3
3
  *
4
4
  * The same `GET /api/search` door the browser's search bar and `cockpit
5
5
  * search` press, over this machine's device token
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `tower_search` — one bar over five corpora, as an MCP tool (BLI-3728).
2
+ * `tower_search` — one bar over every corpus, as an MCP tool (BLI-3728).
3
3
  *
4
4
  * The same `GET /api/search` door the browser's search bar and `cockpit
5
5
  * search` press, over this machine's device token
@@ -25,18 +25,20 @@
25
25
  import { z } from "zod";
26
26
  import { loadAgentDoorSession } from "./agent-door-session.js";
27
27
  import { callAgentDoor } from "./agent-door.js";
28
- // Mirrors `SEARCH_KINDS` in apps/dashboard/src/lib/search/contracts.ts.
29
- // BLI-3880 added mail and cal; nothing in the type system ties the two
30
- // lists together, so `search-tool.test.ts` pins this one against the door.
31
- const KINDS = ["doc", "msg", "issue", "note", "mail", "cal", "memory"];
28
+ // What `kinds` may name is the SERVER's to say (BLI-13027): this used to be a
29
+ // z.enum copy of the dashboard's list, so an installed MCP server refused a
30
+ // corpus the day it shipped. Kinds are forwarded as typed; the door answers
31
+ // `unknown_kind` with the list, and `categories: true` asks for it up front.
32
32
  const KIND_LABELS = {
33
33
  doc: "Document",
34
34
  msg: "Message",
35
- issue: "Issue",
35
+ issue: "Ticket",
36
36
  note: "Meeting note",
37
37
  mail: "Email",
38
38
  cal: "Calendar event",
39
39
  memory: "Memory",
40
+ scout: "Scout card",
41
+ careers: "Applicant",
40
42
  };
41
43
  function textResult(text, structured) {
42
44
  return {
@@ -51,13 +53,22 @@ export function registerSearchTool(server, deps) {
51
53
  const register = server.registerTool.bind(server);
52
54
  register("tower_search", {
53
55
  title: "Search everything in Tower",
54
- description: "One search over documents, messages, issues, meeting notes, the caller's own mail, their calendar and BLI Memory. Use it when you know WHAT you are looking for but not WHERE it lives. Every result names the corpus it came from and where to open it; a memory has no page, so its text is the whole record. Mail and calendar are the caller's OWN — their mailboxes, their calendars and the ones shared with the organisation — never the whole company's. A calendar hit is the stored event, so a recurring meeting comes back once as its series rather than once per occurrence. A corpus that could not answer is reported separately from one that found nothing — do not read a failure as silence.",
56
+ description: "One search over Tower tickets, documents, meeting notes, messages, the caller's own mail, their calendar, BLI Memory, Scout and (only where the caller may read applications) Careers. Use it when you know WHAT you are looking for but not WHERE it lives. Every result names the corpus it came from and where to open it; a memory has no page, so its text is the whole record. Mail and calendar are the caller's OWN — their mailboxes, their calendars and the ones shared with the organisation — never the whole company's. A calendar hit is the stored event, so a recurring meeting comes back once as its series rather than once per occurrence. A corpus that could not answer is reported separately from one that found nothing — do not read a failure as silence.",
55
57
  inputSchema: {
56
- query: z.string().min(2).max(1000).describe("What to look for, in the words a person would type."),
58
+ query: z
59
+ .string()
60
+ .min(2)
61
+ .max(1000)
62
+ .optional()
63
+ .describe("What to look for, in the words a person would type. Required unless categories is true."),
57
64
  kinds: z
58
- .array(z.enum(KINDS))
65
+ .array(z.string().min(1).max(40))
66
+ .optional()
67
+ .describe("Narrow to these categories (issue, doc, note, msg, mail, cal, memory, scout, careers; ticket works for issue). Omit to search every one. Call with categories: true for the exact list this account is offered."),
68
+ categories: z
69
+ .boolean()
59
70
  .optional()
60
- .describe("Narrow to these corpora. Omit to search every one."),
71
+ .describe("True returns the categories this account can narrow to, instead of searching."),
61
72
  limit: z.number().int().min(1).max(50).optional().describe("Results to return (default 20)."),
62
73
  },
63
74
  }, async (args) => {
@@ -66,10 +77,15 @@ export function registerSearchTool(server, deps) {
66
77
  if (!loaded.ok) {
67
78
  return errorResult(`This machine is not paired with Tower (${loaded.reason}). ${loaded.message}`);
68
79
  }
80
+ if (args.categories === true)
81
+ return await listCategories(loaded.session, deps);
69
82
  const query = String(args.query ?? "");
83
+ if (query.trim().length < 2) {
84
+ return errorResult("tower_search needs a query of at least 2 characters, or categories: true to list the categories.");
85
+ }
70
86
  const params = new URLSearchParams({ q: query });
71
87
  if (Array.isArray(args.kinds) && args.kinds.length > 0) {
72
- params.set("kinds", args.kinds.join(","));
88
+ params.set("kinds", args.kinds.map((kind) => kind.trim().toLowerCase()).join(","));
73
89
  }
74
90
  if (typeof args.limit === "number")
75
91
  params.set("limit", String(args.limit));
@@ -94,6 +110,26 @@ export function registerSearchTool(server, deps) {
94
110
  });
95
111
  });
96
112
  }
113
+ /** `categories: true` — the list the browser's Command-K chips are drawn from. */
114
+ async function listCategories(session, deps) {
115
+ const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/search?categories=1");
116
+ if (!response.ok) {
117
+ if (response.transportError) {
118
+ return errorResult(`Tower could not be reached for tower_search (${response.transportError}). The categories were not read.`);
119
+ }
120
+ const reason = typeof response.body.reason === "string" ? response.body.reason : "unknown_error";
121
+ return errorResult(`Tower refused tower_search categories (${reason}).`);
122
+ }
123
+ const categories = (Array.isArray(response.body.categories) ? response.body.categories : []);
124
+ const lines = [
125
+ `${categories.length} categories this account can search. Name them in kinds; omit kinds to search all of them.`,
126
+ ...categories.map((entry) => {
127
+ const aliases = (entry.aliases ?? []).length > 0 ? ` (also: ${(entry.aliases ?? []).join(", ")})` : "";
128
+ return `- ${entry.kind}: ${entry.label}${aliases}`;
129
+ }),
130
+ ];
131
+ return textResult(lines.join("\n"), { categories });
132
+ }
97
133
  /**
98
134
  * The sentence a model reads.
99
135
  *
@@ -84,13 +84,13 @@ async function resolveParentId(deps, session, ref) {
84
84
  export function registerWorkTools(server, deps) {
85
85
  const register = registrarFor(server);
86
86
  register("work_list_issues", {
87
- title: "List Tower issues",
88
- description: "Issues you may see, most recently updated first: identifier, state, title, priority, assignee, project. "
87
+ title: "List Tower tickets",
88
+ description: "Tower tickets you may see, most recently updated first: identifier, state, title, priority, assignee, project. "
89
89
  + "Filter by state, assignee (\"me\", \"unassigned\", or a uuid) or project (name or uuid). "
90
- + `Default ${DEFAULT_LIST_LIMIT} issues; the answer says when more exist. `
91
- + "Never a description — call work_get_issue for one issue's full text.",
90
+ + `Default ${DEFAULT_LIST_LIMIT} tickets; the answer says when more exist. `
91
+ + "Never a description — call work_get_issue for one ticket's full text.",
92
92
  inputSchema: {
93
- state: z.enum(WORK_ISSUE_STATES).optional().describe("Only issues in this state."),
93
+ state: z.enum(WORK_ISSUE_STATES).optional().describe("Only tickets in this state."),
94
94
  assignee: z.string().min(1).max(100).optional().describe('"me", "unassigned", or a person\'s uuid.'),
95
95
  project: z.string().min(1).max(200).optional().describe("A project name or uuid."),
96
96
  limit: z.number().int().min(1).max(ISSUE_LIST_MAX).optional().describe(`At most this many issues. Default ${DEFAULT_LIST_LIMIT}.`),
@@ -144,8 +144,8 @@ export function registerWorkTools(server, deps) {
144
144
  });
145
145
  }));
146
146
  register("work_get_issue", {
147
- title: "Read a Tower issue",
148
- description: "One issue's title, state, priority, assignee (name and email, not just its id), description and every "
147
+ title: "Read a Tower ticket",
148
+ description: "One Tower ticket's title, state, priority, assignee (name and email, not just its id), description and every "
149
149
  + "comment on it. `ids` reads several in one call — still one request per issue plus its comments, "
150
150
  + "no bulk door exists yet. A single id answers `{ issue, comments }`; several answer "
151
151
  + "`{ issues: [{ ref, ok, issue, comments } | { ref, ok: false, error, detail }] }` so one bad id never "
@@ -189,8 +189,8 @@ export function registerWorkTools(server, deps) {
189
189
  });
190
190
  }));
191
191
  register("work_create_issue", {
192
- title: "File a Tower issue",
193
- description: "Files a new issue and returns the BLI-#### identifier it was given. Use this the way you would file a Linear ticket.",
192
+ title: "File a Tower ticket",
193
+ description: "Files a new Tower ticket and returns the BLI-#### identifier it was given. Use this the way you would file a Linear ticket.",
194
194
  inputSchema: {
195
195
  title: z.string().min(1).max(300).describe("The issue title."),
196
196
  description: z.string().max(200_000).optional().describe("The issue body, markdown."),
@@ -236,8 +236,8 @@ export function registerWorkTools(server, deps) {
236
236
  return textResult(`Filed ${issue?.identifier ?? ""} — ${issue?.title ?? ""} (${issue?.id ?? ""}).`, { issue: issue ? withSizeLabel(issue) : issue });
237
237
  }));
238
238
  register("work_update_issue", {
239
- title: "Update a Tower issue",
240
- description: "Changes an issue's title, description, priority, assignee or project. Only the fields you pass change. "
239
+ title: "Update a Tower ticket",
240
+ description: "Changes a Tower ticket's title, description, priority, assignee or project. Only the fields you pass change. "
241
241
  + "State does NOT move here — call work_move_issue, which records the move.",
242
242
  inputSchema: {
243
243
  id: ISSUE_REF,
@@ -285,8 +285,8 @@ export function registerWorkTools(server, deps) {
285
285
  return textResult(`Updated ${issue?.identifier ?? ref} — ${issue?.title ?? ""}.`, { issue: issue ? withSizeLabel(issue) : issue });
286
286
  }));
287
287
  register("work_move_issue", {
288
- title: "Move a Tower issue",
289
- description: "Moves an issue to a new state and records the move in its history. This is how an issue's state changes; "
288
+ title: "Move a Tower ticket",
289
+ description: "Moves a Tower ticket to a new state and records the move in its history. This is how a ticket's state changes; "
290
290
  + "there is no other door.",
291
291
  inputSchema: { id: ISSUE_REF, state: z.enum(WORK_ISSUE_STATES).describe("The state to move it to.") },
292
292
  }, async (args) => withSession(deps, async (session) => {
@@ -300,8 +300,8 @@ export function registerWorkTools(server, deps) {
300
300
  return textResult(`${issue?.identifier ?? ref} is now ${issue?.state ?? String(args.state)}.`, { issue });
301
301
  }));
302
302
  register("work_comment_issue", {
303
- title: "Comment on a Tower issue",
304
- description: "Posts a comment. Comments are append-only — nothing here edits or deletes one.",
303
+ title: "Comment on a Tower ticket",
304
+ description: "Posts a comment on a Tower ticket. Comments are append-only — nothing here edits or deletes one.",
305
305
  inputSchema: { id: ISSUE_REF, body_markdown: z.string().min(1).max(50_000).describe("The comment, markdown.") },
306
306
  }, async (args) => withSession(deps, async (session) => {
307
307
  const ref = String(args.id ?? "");
@@ -312,8 +312,8 @@ export function registerWorkTools(server, deps) {
312
312
  return textResult(`Commented on ${ref} (${comment?.id ?? "?"}).`, { comment });
313
313
  }));
314
314
  register("work_issue_history", {
315
- title: "Read a Tower issue's history",
316
- description: "Every recorded change to one issue — state moves, assignee changes — newest first, with who made it and "
315
+ title: "Read a Tower ticket's history",
316
+ description: "Every recorded change to one Tower ticket — state moves, assignee changes — newest first, with who made it and "
317
317
  + "when. History rows are never rewritten; a row imported without state names says so rather than showing "
318
318
  + "\"(none) -> (none)\".",
319
319
  inputSchema: {
@@ -336,7 +336,7 @@ export function registerWorkTools(server, deps) {
336
336
  }));
337
337
  register("work_list_projects", {
338
338
  title: "List Tower projects",
339
- description: "The projects issues are filed under. Use a project's name with work_list_issues or work_create_issue.",
339
+ description: "The projects Tower tickets are filed under. Use a project's name with work_list_issues or work_create_issue.",
340
340
  inputSchema: { include_archived: z.boolean().optional().describe("true: archived projects too.") },
341
341
  }, async (args) => withSession(deps, async (session) => {
342
342
  const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/work/projects${args.include_archived ? "?include_archived=true" : ""}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.103",
3
+ "version": "0.1.105",
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",