@bli-cockpit/mcp 0.1.61 → 0.1.63
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 +5 -1
- package/dist/docs-msg-helpers.d.ts +89 -0
- package/dist/docs-msg-helpers.js +106 -0
- package/dist/docs-msg-tools.d.ts +3 -13
- package/dist/docs-msg-tools.js +2 -99
- package/dist/docs-notion-tools.d.ts +16 -0
- package/dist/docs-notion-tools.js +89 -0
- package/dist/server.js +3 -0
- package/dist/tool-catalog.d.ts +2 -2
- package/dist/tool-catalog.js +2 -0
- package/dist/verb-census.js +2 -0
- package/package.json +1 -1
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
|
-
**
|
|
106
|
+
**109 of 114 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
|
|
@@ -139,8 +139,10 @@ three tables below.
|
|
|
139
139
|
| `cockpit careers takehome show` | `careers_takehome_show` | `GET /api/careers/takehomes/[role]` |
|
|
140
140
|
| `cockpit correct` | `brief_correct` | `POST /api/jarvis/corrections` |
|
|
141
141
|
| `cockpit docs create` | `docs_create` | `POST /api/docs/documents` |
|
|
142
|
+
| `cockpit docs link` | `docs_link` | `PUT /api/docs/documents/[id]/notion-link` |
|
|
142
143
|
| `cockpit docs list` | `docs_list` | `GET /api/docs/documents` |
|
|
143
144
|
| `cockpit docs read` | `docs_read` | `GET /api/docs/documents/[id]` |
|
|
145
|
+
| `cockpit docs sync-status` | `docs_sync_status` | `GET /api/docs/notion-sync` |
|
|
144
146
|
| `cockpit docs tree` | `docs_tree` | `GET /api/docs/tree` |
|
|
145
147
|
| `cockpit docs update` | `docs_update` | `PATCH /api/docs/documents/[id]` |
|
|
146
148
|
| `cockpit issue comment` | `work_comment_issue` | `POST /api/work/issues/[id]/comments` |
|
|
@@ -382,6 +384,8 @@ narrowing who can read something.
|
|
|
382
384
|
| `docs_read` | `GET /api/docs/documents/[id]` | One document's title and full body, by id or slug (resolved against `docs_list` first). |
|
|
383
385
|
| `docs_create` | `POST /api/docs/documents` | Creates a document. Omitting `body_markdown` creates an empty one. |
|
|
384
386
|
| `docs_update` | `PATCH /api/docs/documents/[id]` | Updates title/body/visibility/parent (a parent change IS a move). Emptying a document that holds text needs `allow_empty: true` (BLI-3757's guard). |
|
|
387
|
+
| `docs_link` | `PUT`/`DELETE /api/docs/documents/[id]/notion-link` | Ties a document to a Notion page for the two-way sync (`notion_page`: a URL or page id), or unties it with `clear: true`. The page becomes the truth: the next sync pulls it over the copy and keeps the copy as a version. |
|
|
388
|
+
| `docs_sync_status` | `GET /api/docs/notion-sync` | Every linked document's sync state (`synced` / `tower_changed` / `never_synced`), or one's by `id`. Tower's half only; metadata, never a body. |
|
|
385
389
|
| `msg_channels` | `GET /api/msg/channels` | Every channel you are a member of. |
|
|
386
390
|
| `msg_read` | `GET /api/msg/channels/[id]/messages` | Recent messages of one channel, newest first. `channel` is an id or a `#name`. |
|
|
387
391
|
| `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` | One thread's replies, oldest first, so it reads as a conversation. |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers behind the `docs_*` and `msg_*` tools (split out of
|
|
3
|
+
* `docs-msg-tools.ts` to keep it under the file-size ratchet): id resolution,
|
|
4
|
+
* list shaping and one-line renderers. No tool is registered here.
|
|
5
|
+
*/
|
|
6
|
+
import { type ToolDeps } from "./tool-result.js";
|
|
7
|
+
/** A canonical uuid: a ref that already IS an id needs no list read to resolve. */
|
|
8
|
+
export declare const UUID_PATTERN: RegExp;
|
|
9
|
+
/** The door's own ceiling on `?limit=` (`/api/docs/documents` `DOCUMENT_LIST_MAX_LIMIT`). */
|
|
10
|
+
export declare const DOCS_LIST_MAX = 1000;
|
|
11
|
+
export type DocsMsgDeps = ToolDeps;
|
|
12
|
+
/** One node of `GET /api/docs/tree`. The door nests; nothing here re-nests. */
|
|
13
|
+
export interface DocTreeNode {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
slug: string | null;
|
|
17
|
+
children: DocTreeNode[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The tree as the text half prints it: id, title, slug, children. The door
|
|
21
|
+
* also sends visibility, source and a timestamp per node, which nothing here
|
|
22
|
+
* promised and which made the structured half 48 KB for 226 documents
|
|
23
|
+
* (BLI-10432). `docs_list` carries those fields for a flat read.
|
|
24
|
+
*/
|
|
25
|
+
export declare function slimTree(nodes: DocTreeNode[]): DocTreeNode[];
|
|
26
|
+
/** Indented lines, deepest last — the same shape `cockpit docs tree` prints. */
|
|
27
|
+
export declare function renderDocTree(nodes: DocTreeNode[], depth: number): string;
|
|
28
|
+
export declare function countTree(nodes: DocTreeNode[]): number;
|
|
29
|
+
export interface DocRow {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
slug: string | null;
|
|
33
|
+
visibility: string;
|
|
34
|
+
source?: string | null;
|
|
35
|
+
parent_id?: string | null;
|
|
36
|
+
updated_at: string;
|
|
37
|
+
/** BLI-3737: the body's length, the one fact about a body the LIST still carries. */
|
|
38
|
+
body_chars?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The fields `docs_list` promises, and only those. The door's row also carries
|
|
42
|
+
* `created_by`, `updated_by`, `created_at`, `import_truncated` and
|
|
43
|
+
* `missing_block_count`: 105 KB for 226 documents before this (BLI-10432).
|
|
44
|
+
*/
|
|
45
|
+
export declare function docListRow(row: DocRow): Record<string, unknown>;
|
|
46
|
+
export declare function docListLine(row: DocRow): string;
|
|
47
|
+
/**
|
|
48
|
+
* `docs_list`'s narrowing, as the door's own query parameters (BLI-3737).
|
|
49
|
+
* The list itself is metadata only — asking for a whole library used to cost
|
|
50
|
+
* every document's prose, so an agent that wants one folder or one phrase
|
|
51
|
+
* says so here and the database does the filtering.
|
|
52
|
+
*/
|
|
53
|
+
export declare function docsListPath(args: {
|
|
54
|
+
parent_id?: unknown;
|
|
55
|
+
query?: unknown;
|
|
56
|
+
limit?: unknown;
|
|
57
|
+
}): string;
|
|
58
|
+
export declare function resolveDocumentId(deps: DocsMsgDeps, session: {
|
|
59
|
+
dashboardUrl: string;
|
|
60
|
+
deviceToken: string;
|
|
61
|
+
}, ref: string): Promise<{
|
|
62
|
+
status: "ok";
|
|
63
|
+
id: string;
|
|
64
|
+
} | {
|
|
65
|
+
status: "not_found";
|
|
66
|
+
} | {
|
|
67
|
+
status: "list_failed";
|
|
68
|
+
text: string;
|
|
69
|
+
}>;
|
|
70
|
+
export interface ChannelRow {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string | null;
|
|
73
|
+
is_private: boolean;
|
|
74
|
+
is_dm: boolean;
|
|
75
|
+
}
|
|
76
|
+
export declare function resolveChannelId(deps: DocsMsgDeps, session: {
|
|
77
|
+
dashboardUrl: string;
|
|
78
|
+
deviceToken: string;
|
|
79
|
+
}, ref: string): Promise<{
|
|
80
|
+
status: "ok";
|
|
81
|
+
id: string;
|
|
82
|
+
} | {
|
|
83
|
+
status: "not_found";
|
|
84
|
+
} | {
|
|
85
|
+
status: "list_failed";
|
|
86
|
+
text: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** One message as a line: id (msg_thread's `thread_parent_id`), time, author, text. */
|
|
89
|
+
export declare function messageLine(message: Record<string, unknown>): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers behind the `docs_*` and `msg_*` tools (split out of
|
|
3
|
+
* `docs-msg-tools.ts` to keep it under the file-size ratchet): id resolution,
|
|
4
|
+
* list shaping and one-line renderers. No tool is registered here.
|
|
5
|
+
*/
|
|
6
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
7
|
+
import { DEFAULT_LIST_LIMIT, doorFailureText } from "./tool-result.js";
|
|
8
|
+
/** A canonical uuid: a ref that already IS an id needs no list read to resolve. */
|
|
9
|
+
export const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
10
|
+
/** The door's own ceiling on `?limit=` (`/api/docs/documents` `DOCUMENT_LIST_MAX_LIMIT`). */
|
|
11
|
+
export const DOCS_LIST_MAX = 1000;
|
|
12
|
+
/**
|
|
13
|
+
* The tree as the text half prints it: id, title, slug, children. The door
|
|
14
|
+
* also sends visibility, source and a timestamp per node, which nothing here
|
|
15
|
+
* promised and which made the structured half 48 KB for 226 documents
|
|
16
|
+
* (BLI-10432). `docs_list` carries those fields for a flat read.
|
|
17
|
+
*/
|
|
18
|
+
export function slimTree(nodes) {
|
|
19
|
+
return nodes.map((node) => ({
|
|
20
|
+
id: node.id,
|
|
21
|
+
title: node.title,
|
|
22
|
+
slug: node.slug ?? null,
|
|
23
|
+
children: slimTree(Array.isArray(node.children) ? node.children : []),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/** Indented lines, deepest last — the same shape `cockpit docs tree` prints. */
|
|
27
|
+
export function renderDocTree(nodes, depth) {
|
|
28
|
+
return nodes
|
|
29
|
+
.map((node) => {
|
|
30
|
+
const line = `${" ".repeat(depth)}${node.title} (${node.slug ?? node.id})`;
|
|
31
|
+
const children = node.children?.length ? `\n${renderDocTree(node.children, depth + 1)}` : "";
|
|
32
|
+
return `${line}${children}`;
|
|
33
|
+
})
|
|
34
|
+
.join("\n");
|
|
35
|
+
}
|
|
36
|
+
export function countTree(nodes) {
|
|
37
|
+
return nodes.reduce((count, node) => count + 1 + countTree(node.children ?? []), 0);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The fields `docs_list` promises, and only those. The door's row also carries
|
|
41
|
+
* `created_by`, `updated_by`, `created_at`, `import_truncated` and
|
|
42
|
+
* `missing_block_count`: 105 KB for 226 documents before this (BLI-10432).
|
|
43
|
+
*/
|
|
44
|
+
export function docListRow(row) {
|
|
45
|
+
return {
|
|
46
|
+
id: row.id,
|
|
47
|
+
slug: row.slug ?? null,
|
|
48
|
+
title: row.title,
|
|
49
|
+
visibility: row.visibility,
|
|
50
|
+
source: row.source ?? null,
|
|
51
|
+
parent_id: row.parent_id ?? null,
|
|
52
|
+
updated_at: row.updated_at,
|
|
53
|
+
body_chars: row.body_chars ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function docListLine(row) {
|
|
57
|
+
return `${row.id} ${row.slug ?? "-"} ${row.title} [${row.visibility}${row.source ? `, ${row.source}` : ""}, `
|
|
58
|
+
+ `${row.body_chars ?? "?"} chars, parent ${row.parent_id ?? "root"}, updated ${row.updated_at}]`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `docs_list`'s narrowing, as the door's own query parameters (BLI-3737).
|
|
62
|
+
* The list itself is metadata only — asking for a whole library used to cost
|
|
63
|
+
* every document's prose, so an agent that wants one folder or one phrase
|
|
64
|
+
* says so here and the database does the filtering.
|
|
65
|
+
*/
|
|
66
|
+
export function docsListPath(args) {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
if (typeof args.parent_id === "string" && args.parent_id.length > 0)
|
|
69
|
+
params.set("parent_id", args.parent_id);
|
|
70
|
+
if (typeof args.query === "string" && args.query.trim().length > 0)
|
|
71
|
+
params.set("query", args.query);
|
|
72
|
+
if (typeof args.limit === "number" && Number.isInteger(args.limit))
|
|
73
|
+
params.set("limit", String(args.limit));
|
|
74
|
+
// No limit named: one page plus one row, so "more exist" is read off the
|
|
75
|
+
// door instead of guessed from a full page (BLI-10432).
|
|
76
|
+
else if (args.limit === undefined)
|
|
77
|
+
params.set("limit", String(DEFAULT_LIST_LIMIT + 1));
|
|
78
|
+
const search = params.toString();
|
|
79
|
+
return search.length > 0 ? `/api/docs/documents?${search}` : "/api/docs/documents";
|
|
80
|
+
}
|
|
81
|
+
export async function resolveDocumentId(deps, session, ref) {
|
|
82
|
+
// An id needs no lookup: the read itself answers 404 by name when it is
|
|
83
|
+
// wrong. Only a slug costs the whole-library list read (BLI-10432).
|
|
84
|
+
if (UUID_PATTERN.test(ref))
|
|
85
|
+
return { status: "ok", id: ref };
|
|
86
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/docs/documents");
|
|
87
|
+
if (!response.ok)
|
|
88
|
+
return { status: "list_failed", text: doorFailureText("docs_list", response) };
|
|
89
|
+
const documents = (Array.isArray(response.body.documents) ? response.body.documents : []);
|
|
90
|
+
const match = documents.find((doc) => doc.id === ref || doc.slug === ref);
|
|
91
|
+
return match ? { status: "ok", id: match.id } : { status: "not_found" };
|
|
92
|
+
}
|
|
93
|
+
export async function resolveChannelId(deps, session, ref) {
|
|
94
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/msg/channels");
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
return { status: "list_failed", text: doorFailureText("msg_channels", response) };
|
|
97
|
+
const channels = (Array.isArray(response.body.channels) ? response.body.channels : []);
|
|
98
|
+
const bare = ref.startsWith("#") ? ref.slice(1) : ref;
|
|
99
|
+
const match = channels.find((channel) => channel.id === ref || channel.name === bare);
|
|
100
|
+
return match ? { status: "ok", id: match.id } : { status: "not_found" };
|
|
101
|
+
}
|
|
102
|
+
/** One message as a line: id (msg_thread's `thread_parent_id`), time, author, text. */
|
|
103
|
+
export function messageLine(message) {
|
|
104
|
+
return `${String(message.id ?? "?")} ${String(message.created_at ?? "?")} `
|
|
105
|
+
+ `${String(message.agent_label ?? message.user_id ?? "?")} ${String(message.content ?? "")}`;
|
|
106
|
+
}
|
package/dist/docs-msg-tools.d.ts
CHANGED
|
@@ -9,19 +9,9 @@
|
|
|
9
9
|
* on a machine with no collector pairing at all, so a missing session must
|
|
10
10
|
* fail the ONE call that needed it, never the whole process.
|
|
11
11
|
*/
|
|
12
|
-
import { type
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
* `docs_list`'s narrowing, as the door's own query parameters (BLI-3737).
|
|
16
|
-
* The list itself is metadata only — asking for a whole library used to cost
|
|
17
|
-
* every document's prose, so an agent that wants one folder or one phrase
|
|
18
|
-
* says so here and the database does the filtering.
|
|
19
|
-
*/
|
|
20
|
-
export declare function docsListPath(args: {
|
|
21
|
-
parent_id?: unknown;
|
|
22
|
-
query?: unknown;
|
|
23
|
-
limit?: unknown;
|
|
24
|
-
}): string;
|
|
12
|
+
import { type DocsMsgDeps } from "./docs-msg-helpers.js";
|
|
13
|
+
export { docsListPath } from "./docs-msg-helpers.js";
|
|
14
|
+
export type { DocsMsgDeps } from "./docs-msg-helpers.js";
|
|
25
15
|
export declare function registerDocsMsgTools(server: {
|
|
26
16
|
registerTool: (...args: never[]) => unknown;
|
|
27
17
|
}, deps: DocsMsgDeps): void;
|
package/dist/docs-msg-tools.js
CHANGED
|
@@ -12,105 +12,8 @@
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { callAgentDoor } from "./agent-door.js";
|
|
14
14
|
import { DEFAULT_LIST_LIMIT, doorFailureText, errorResult, moreNote, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/** The door's own ceiling on `?limit=` (`/api/docs/documents` `DOCUMENT_LIST_MAX_LIMIT`). */
|
|
18
|
-
const DOCS_LIST_MAX = 1000;
|
|
19
|
-
/**
|
|
20
|
-
* The tree as the text half prints it: id, title, slug, children. The door
|
|
21
|
-
* also sends visibility, source and a timestamp per node, which nothing here
|
|
22
|
-
* promised and which made the structured half 48 KB for 226 documents
|
|
23
|
-
* (BLI-10432). `docs_list` carries those fields for a flat read.
|
|
24
|
-
*/
|
|
25
|
-
function slimTree(nodes) {
|
|
26
|
-
return nodes.map((node) => ({
|
|
27
|
-
id: node.id,
|
|
28
|
-
title: node.title,
|
|
29
|
-
slug: node.slug ?? null,
|
|
30
|
-
children: slimTree(Array.isArray(node.children) ? node.children : []),
|
|
31
|
-
}));
|
|
32
|
-
}
|
|
33
|
-
/** Indented lines, deepest last — the same shape `cockpit docs tree` prints. */
|
|
34
|
-
function renderDocTree(nodes, depth) {
|
|
35
|
-
return nodes
|
|
36
|
-
.map((node) => {
|
|
37
|
-
const line = `${" ".repeat(depth)}${node.title} (${node.slug ?? node.id})`;
|
|
38
|
-
const children = node.children?.length ? `\n${renderDocTree(node.children, depth + 1)}` : "";
|
|
39
|
-
return `${line}${children}`;
|
|
40
|
-
})
|
|
41
|
-
.join("\n");
|
|
42
|
-
}
|
|
43
|
-
function countTree(nodes) {
|
|
44
|
-
return nodes.reduce((count, node) => count + 1 + countTree(node.children ?? []), 0);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* The fields `docs_list` promises, and only those. The door's row also carries
|
|
48
|
-
* `created_by`, `updated_by`, `created_at`, `import_truncated` and
|
|
49
|
-
* `missing_block_count`: 105 KB for 226 documents before this (BLI-10432).
|
|
50
|
-
*/
|
|
51
|
-
function docListRow(row) {
|
|
52
|
-
return {
|
|
53
|
-
id: row.id,
|
|
54
|
-
slug: row.slug ?? null,
|
|
55
|
-
title: row.title,
|
|
56
|
-
visibility: row.visibility,
|
|
57
|
-
source: row.source ?? null,
|
|
58
|
-
parent_id: row.parent_id ?? null,
|
|
59
|
-
updated_at: row.updated_at,
|
|
60
|
-
body_chars: row.body_chars ?? null,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function docListLine(row) {
|
|
64
|
-
return `${row.id} ${row.slug ?? "-"} ${row.title} [${row.visibility}${row.source ? `, ${row.source}` : ""}, `
|
|
65
|
-
+ `${row.body_chars ?? "?"} chars, parent ${row.parent_id ?? "root"}, updated ${row.updated_at}]`;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* `docs_list`'s narrowing, as the door's own query parameters (BLI-3737).
|
|
69
|
-
* The list itself is metadata only — asking for a whole library used to cost
|
|
70
|
-
* every document's prose, so an agent that wants one folder or one phrase
|
|
71
|
-
* says so here and the database does the filtering.
|
|
72
|
-
*/
|
|
73
|
-
export function docsListPath(args) {
|
|
74
|
-
const params = new URLSearchParams();
|
|
75
|
-
if (typeof args.parent_id === "string" && args.parent_id.length > 0)
|
|
76
|
-
params.set("parent_id", args.parent_id);
|
|
77
|
-
if (typeof args.query === "string" && args.query.trim().length > 0)
|
|
78
|
-
params.set("query", args.query);
|
|
79
|
-
if (typeof args.limit === "number" && Number.isInteger(args.limit))
|
|
80
|
-
params.set("limit", String(args.limit));
|
|
81
|
-
// No limit named: one page plus one row, so "more exist" is read off the
|
|
82
|
-
// door instead of guessed from a full page (BLI-10432).
|
|
83
|
-
else if (args.limit === undefined)
|
|
84
|
-
params.set("limit", String(DEFAULT_LIST_LIMIT + 1));
|
|
85
|
-
const search = params.toString();
|
|
86
|
-
return search.length > 0 ? `/api/docs/documents?${search}` : "/api/docs/documents";
|
|
87
|
-
}
|
|
88
|
-
async function resolveDocumentId(deps, session, ref) {
|
|
89
|
-
// An id needs no lookup: the read itself answers 404 by name when it is
|
|
90
|
-
// wrong. Only a slug costs the whole-library list read (BLI-10432).
|
|
91
|
-
if (UUID_PATTERN.test(ref))
|
|
92
|
-
return { status: "ok", id: ref };
|
|
93
|
-
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/docs/documents");
|
|
94
|
-
if (!response.ok)
|
|
95
|
-
return { status: "list_failed", text: doorFailureText("docs_list", response) };
|
|
96
|
-
const documents = (Array.isArray(response.body.documents) ? response.body.documents : []);
|
|
97
|
-
const match = documents.find((doc) => doc.id === ref || doc.slug === ref);
|
|
98
|
-
return match ? { status: "ok", id: match.id } : { status: "not_found" };
|
|
99
|
-
}
|
|
100
|
-
async function resolveChannelId(deps, session, ref) {
|
|
101
|
-
const response = await callAgentDoor(session, deps.fetchImpl, "GET", "/api/msg/channels");
|
|
102
|
-
if (!response.ok)
|
|
103
|
-
return { status: "list_failed", text: doorFailureText("msg_channels", response) };
|
|
104
|
-
const channels = (Array.isArray(response.body.channels) ? response.body.channels : []);
|
|
105
|
-
const bare = ref.startsWith("#") ? ref.slice(1) : ref;
|
|
106
|
-
const match = channels.find((channel) => channel.id === ref || channel.name === bare);
|
|
107
|
-
return match ? { status: "ok", id: match.id } : { status: "not_found" };
|
|
108
|
-
}
|
|
109
|
-
/** One message as a line: id (msg_thread's `thread_parent_id`), time, author, text. */
|
|
110
|
-
function messageLine(message) {
|
|
111
|
-
return `${String(message.id ?? "?")} ${String(message.created_at ?? "?")} `
|
|
112
|
-
+ `${String(message.agent_label ?? message.user_id ?? "?")} ${String(message.content ?? "")}`;
|
|
113
|
-
}
|
|
15
|
+
import { DOCS_LIST_MAX, slimTree, renderDocTree, countTree, docListRow, docListLine, resolveDocumentId, resolveChannelId, messageLine, docsListPath } from "./docs-msg-helpers.js";
|
|
16
|
+
export { docsListPath } from "./docs-msg-helpers.js";
|
|
114
17
|
export function registerDocsMsgTools(server, deps) {
|
|
115
18
|
const register = registrarFor(server);
|
|
116
19
|
register("docs_list", {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Notion half of the `docs_*` tools (BLI-10430) — the same door
|
|
3
|
+
* `cockpit docs link` calls, over this machine's device token. A sibling of
|
|
4
|
+
* `docs-msg-tools.ts` rather than more of it, the same split the collector
|
|
5
|
+
* makes (`commands/docs-notion.ts`).
|
|
6
|
+
*
|
|
7
|
+
* `docs_link` → `PUT /api/docs/documents/[id]/notion-link` (or `DELETE` with
|
|
8
|
+
* `clear: true`). A link makes the Notion page the truth: the next sync pulls
|
|
9
|
+
* it over the Tower copy and keeps the copy as a version.
|
|
10
|
+
* `docs_sync_status` → `GET /api/docs/notion-sync[?document_id=]`: Tower's half
|
|
11
|
+
* of the sync state, metadata only.
|
|
12
|
+
*/
|
|
13
|
+
import { type DocsMsgDeps } from "./docs-msg-helpers.js";
|
|
14
|
+
export declare function registerDocsNotionTools(server: {
|
|
15
|
+
registerTool: (...args: never[]) => unknown;
|
|
16
|
+
}, deps: DocsMsgDeps): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Notion half of the `docs_*` tools (BLI-10430) — the same door
|
|
3
|
+
* `cockpit docs link` calls, over this machine's device token. A sibling of
|
|
4
|
+
* `docs-msg-tools.ts` rather than more of it, the same split the collector
|
|
5
|
+
* makes (`commands/docs-notion.ts`).
|
|
6
|
+
*
|
|
7
|
+
* `docs_link` → `PUT /api/docs/documents/[id]/notion-link` (or `DELETE` with
|
|
8
|
+
* `clear: true`). A link makes the Notion page the truth: the next sync pulls
|
|
9
|
+
* it over the Tower copy and keeps the copy as a version.
|
|
10
|
+
* `docs_sync_status` → `GET /api/docs/notion-sync[?document_id=]`: Tower's half
|
|
11
|
+
* of the sync state, metadata only.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
15
|
+
import { resolveDocumentId } from "./docs-msg-helpers.js";
|
|
16
|
+
import { doorFailureText, errorResult, registrarFor, textResult, withSession } from "./tool-result.js";
|
|
17
|
+
export function registerDocsNotionTools(server, deps) {
|
|
18
|
+
const register = registrarFor(server);
|
|
19
|
+
register("docs_link", {
|
|
20
|
+
title: "Link a Tower document to a Notion page",
|
|
21
|
+
description: "Ties a document to a Notion page so the two-way sync moves it both ways, or unties it with clear: true. "
|
|
22
|
+
+ "The page becomes the truth: the next sync pulls it over this copy and keeps the copy as a version. "
|
|
23
|
+
+ "Refusals keep Tower's reason: invalid_notion_page, notion_page_already_linked, document_not_writable.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
id: z.string().min(1).max(200).describe("A document id, or its slug."),
|
|
26
|
+
notion_page: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(500)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("The Notion page URL (app.notion.com/p/… or notion.so/…) or its page id."),
|
|
32
|
+
clear: z.boolean().optional().describe("true to untie the document from Notion instead."),
|
|
33
|
+
},
|
|
34
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
35
|
+
const clear = args.clear === true;
|
|
36
|
+
const notionPage = typeof args.notion_page === "string" ? args.notion_page : undefined;
|
|
37
|
+
if (clear === Boolean(notionPage)) {
|
|
38
|
+
return errorResult("docs_link needs exactly one of notion_page (to link) or clear: true (to unlink).");
|
|
39
|
+
}
|
|
40
|
+
const ref = String(args.id ?? "");
|
|
41
|
+
const resolved = await resolveDocumentId(deps, session, ref);
|
|
42
|
+
if (resolved.status === "list_failed")
|
|
43
|
+
return errorResult(resolved.text);
|
|
44
|
+
if (resolved.status === "not_found")
|
|
45
|
+
return errorResult(`No document matches "${ref}" — check docs_list for the id or slug.`);
|
|
46
|
+
const path = `/api/docs/documents/${encodeURIComponent(resolved.id)}/notion-link`;
|
|
47
|
+
const response = clear
|
|
48
|
+
? await callAgentDoor(session, deps.fetchImpl, "DELETE", path)
|
|
49
|
+
: await callAgentDoor(session, deps.fetchImpl, "PUT", path, { notion_page: notionPage });
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
return errorResult(doorFailureText("docs_link", response));
|
|
52
|
+
const outcome = String(response.body.outcome ?? "");
|
|
53
|
+
const document = response.body.document;
|
|
54
|
+
return textResult(`${outcome || "done"}: "${document?.title ?? ""}" (${document?.id ?? resolved.id}).`, {
|
|
55
|
+
outcome,
|
|
56
|
+
document,
|
|
57
|
+
});
|
|
58
|
+
}));
|
|
59
|
+
register("docs_sync_status", {
|
|
60
|
+
title: "Read the Tower-Notion sync status",
|
|
61
|
+
description: "Every document linked to a Notion page (or one, by id or slug) with its sync state: synced (the body is what "
|
|
62
|
+
+ "both sides last agreed on), tower_changed (edited in Tower since; the next write-back pushes it), never_synced "
|
|
63
|
+
+ "(linked, no baseline yet). Tower's half only: a Notion edit shows up at the next pull. Never a body.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
id: z.string().min(1).max(200).optional().describe("A document id or slug, for one document's status."),
|
|
66
|
+
},
|
|
67
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
68
|
+
let path = "/api/docs/notion-sync";
|
|
69
|
+
if (typeof args.id === "string" && args.id.length > 0) {
|
|
70
|
+
const resolved = await resolveDocumentId(deps, session, args.id);
|
|
71
|
+
if (resolved.status === "list_failed")
|
|
72
|
+
return errorResult(resolved.text);
|
|
73
|
+
if (resolved.status === "not_found")
|
|
74
|
+
return errorResult(`No document matches "${args.id}" — check docs_list for the id or slug.`);
|
|
75
|
+
path = `${path}?document_id=${encodeURIComponent(resolved.id)}`;
|
|
76
|
+
}
|
|
77
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", path);
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
return errorResult(doorFailureText("docs_sync_status", response));
|
|
80
|
+
const counts = response.body.counts;
|
|
81
|
+
const summary = Object.entries(counts?.by_state ?? {})
|
|
82
|
+
.map(([state, count]) => `${count} ${state}`)
|
|
83
|
+
.join(", ");
|
|
84
|
+
return textResult(`${counts?.linked ?? 0} linked: ${summary}.`, {
|
|
85
|
+
counts,
|
|
86
|
+
documents: response.body.documents ?? [],
|
|
87
|
+
});
|
|
88
|
+
}));
|
|
89
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { registerBriefTools } from "./brief-tools.js";
|
|
11
11
|
import { registerBriefWriteTools } from "./brief-write-tools.js";
|
|
12
12
|
import { registerDocsMsgTools } from "./docs-msg-tools.js";
|
|
13
|
+
import { registerDocsNotionTools } from "./docs-notion-tools.js";
|
|
13
14
|
import { registerJarvisTools } from "./jarvis-tools.js";
|
|
14
15
|
import { registerCalTools } from "./cal-tools.js";
|
|
15
16
|
import { registerMailTools } from "./mail-tools.js";
|
|
@@ -415,6 +416,8 @@ export function createServer(deps) {
|
|
|
415
416
|
// no `cockpit login` pairing yet still serves the three event tools; only a
|
|
416
417
|
// docs/msg call on that machine fails, by name.
|
|
417
418
|
registerDocsMsgTools(server, { fetchImpl: deps.fetchImpl });
|
|
419
|
+
// BLI-10430: docs_link, the Notion half of the docs tools, on the same path.
|
|
420
|
+
registerDocsNotionTools(server, { fetchImpl: deps.fetchImpl });
|
|
418
421
|
// BLI-3716: `work_*` — the issue tracker, on the same device-token path as
|
|
419
422
|
// the docs/msg tools above, for the same reason: a coding session should
|
|
420
423
|
// file and move a Tower issue the way it files and moves a Linear one.
|
package/dist/tool-catalog.d.ts
CHANGED
|
@@ -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", "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", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "notes_folders", "notes_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "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"];
|
|
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", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "notes_folders", "notes_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "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"];
|
|
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", "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_takehome_set", "careers_settings_set", "mail_send", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "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_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_takehome_set", "careers_settings_set", "mail_send", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "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
|
/**
|
package/dist/tool-catalog.js
CHANGED
|
@@ -16,6 +16,7 @@ export const READ_TOOLS = [
|
|
|
16
16
|
"docs_list",
|
|
17
17
|
"docs_tree",
|
|
18
18
|
"docs_read",
|
|
19
|
+
"docs_sync_status",
|
|
19
20
|
"msg_channels",
|
|
20
21
|
"msg_read",
|
|
21
22
|
"msg_thread",
|
|
@@ -84,6 +85,7 @@ export const WRITE_TOOLS = [
|
|
|
84
85
|
"emit_event",
|
|
85
86
|
"docs_create",
|
|
86
87
|
"docs_update",
|
|
88
|
+
"docs_link",
|
|
87
89
|
"msg_create_channel",
|
|
88
90
|
"msg_dm",
|
|
89
91
|
"msg_send",
|
package/dist/verb-census.js
CHANGED
|
@@ -234,6 +234,8 @@ export const MCP_TWINS = {
|
|
|
234
234
|
"docs read": { tool: "docs_read", door: "GET /api/docs/documents/[id]" },
|
|
235
235
|
"docs create": { tool: "docs_create", door: "POST /api/docs/documents" },
|
|
236
236
|
"docs update": { tool: "docs_update", door: "PATCH /api/docs/documents/[id]" },
|
|
237
|
+
"docs link": { tool: "docs_link", door: "PUT /api/docs/documents/[id]/notion-link" },
|
|
238
|
+
"docs sync-status": { tool: "docs_sync_status", door: "GET /api/docs/notion-sync" },
|
|
237
239
|
"msg channels": { tool: "msg_channels", door: "GET /api/msg/channels" },
|
|
238
240
|
"msg create": { tool: "msg_create_channel", door: "POST /api/msg/channels" },
|
|
239
241
|
"msg dm": { tool: "msg_dm", door: "POST /api/msg/channels (dm)" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
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",
|