@bli-cockpit/mcp 0.1.104 → 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,10 +25,10 @@
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",
@@ -37,6 +37,8 @@ const KIND_LABELS = {
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, Tower tickets, 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, "ticket"]))
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.map((kind) => (kind === "ticket" ? "issue" : kind)).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
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/mcp",
3
- "version": "0.1.104",
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",