@bli-cockpit/mcp 0.1.69 → 0.1.70
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/dist/work-flag-census.d.ts +22 -0
- package/dist/work-flag-census.js +109 -0
- package/dist/work-issue-rows.d.ts +30 -0
- package/dist/work-issue-rows.js +41 -1
- package/dist/work-tools.js +56 -8
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Flags `parseIssueArgs` accepts a VALUE for (`--flag value`), read from its own `valueFlags` array. */
|
|
2
|
+
export declare function issueValueFlags(): string[];
|
|
3
|
+
/** The bare field names (`--parent` → `parent`) `cockpit issue create`/`update` actually write. */
|
|
4
|
+
export declare function issueWriteFieldNames(): string[];
|
|
5
|
+
export declare const WORK_WRITE_VERBS: readonly ["issue create", "issue update"];
|
|
6
|
+
export type WorkWriteVerb = (typeof WORK_WRITE_VERBS)[number];
|
|
7
|
+
export declare const MCP_TOOL_FOR_WRITE_VERB: Record<WorkWriteVerb, string>;
|
|
8
|
+
/** The field names an MCP tool's REGISTERED zod shape actually carries. */
|
|
9
|
+
export declare function mcpToolFieldNames(toolName: string): string[];
|
|
10
|
+
/**
|
|
11
|
+
* A field on exactly one side ON PURPOSE, with why — a claim about shape,
|
|
12
|
+
* never a backlog (the same discipline `TERMINAL_ONLY` keeps in
|
|
13
|
+
* `verb-census.ts`). Anything asymmetric that is NOT listed here fails the
|
|
14
|
+
* test in `work-flag-census.test.ts`.
|
|
15
|
+
*/
|
|
16
|
+
export interface OneSidedField {
|
|
17
|
+
verb: WorkWriteVerb;
|
|
18
|
+
side: "cli" | "mcp";
|
|
19
|
+
field: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const EXPECTED_ONE_SIDED_FIELDS: OneSidedField[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flag-level parity for `cockpit issue create|update` against their MCP
|
|
3
|
+
* twins, `work_create_issue`/`work_update_issue` (BLI-10575).
|
|
4
|
+
*
|
|
5
|
+
* `verb-census.ts` proves every VERB has a twin — it never looks at a verb's
|
|
6
|
+
* FLAGS, by its own header ("What it deliberately does NOT see"): the
|
|
7
|
+
* extraction there only ever reads a noun's `action` union, never a shape's
|
|
8
|
+
* own fields. That is exactly how `work_create_issue` shipped with no
|
|
9
|
+
* `parent` while `cockpit issue create --parent` already had one (BLI-3716)
|
|
10
|
+
* — the VERB existed on both sides, its FLAGS did not, and nothing checked
|
|
11
|
+
* because nothing looked. `--size`, added in this same ticket, would have
|
|
12
|
+
* been the next one to drift silently were this test not written alongside
|
|
13
|
+
* it.
|
|
14
|
+
*
|
|
15
|
+
* This reads both real sources rather than trusting a hand-written list on
|
|
16
|
+
* either side:
|
|
17
|
+
*
|
|
18
|
+
* - the collector's own `valueFlags` array for `parseIssueArgs`, a text
|
|
19
|
+
* read the same way `verb-census.ts` reads the collector (this package
|
|
20
|
+
* declares no dependency on it);
|
|
21
|
+
* - the MCP tool's REGISTERED zod shape — `createServer(...)`'s
|
|
22
|
+
* `_registeredTools[name].inputSchema.shape`, the same private field
|
|
23
|
+
* `server.test.ts` already introspects, not a text scan of the source.
|
|
24
|
+
*
|
|
25
|
+
* `cockpit issue` has ONE shared flag vocabulary across every verb (`list`,
|
|
26
|
+
* `create`, `update`, `move`, ... all parse from the same `valueFlags`
|
|
27
|
+
* array — `local-args-tower-work.ts` gates a flag's MEANING per action, not
|
|
28
|
+
* its presence), so the CLI side of this comparison is the same set for
|
|
29
|
+
* `create` and `update` alike; only the filter list below narrows it to the
|
|
30
|
+
* fields that name something an issue's ROW carries, as opposed to a filter
|
|
31
|
+
* (`--state`, `--limit`) or a global (`--home`, `--dashboard-url`).
|
|
32
|
+
*/
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
import { dirname, resolve } from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
import { createServer } from "./server.js";
|
|
37
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const WORK_ARGS_FILE = resolve(HERE, "../../cockpit-local-collector/src/commands/local-args-tower-work.ts");
|
|
39
|
+
/** Flags `parseIssueArgs` accepts a VALUE for (`--flag value`), read from its own `valueFlags` array. */
|
|
40
|
+
export function issueValueFlags() {
|
|
41
|
+
const source = readFileSync(WORK_ARGS_FILE, "utf8");
|
|
42
|
+
const start = source.indexOf("export function parseIssueArgs");
|
|
43
|
+
const end = source.indexOf("export type ProjectAction", start);
|
|
44
|
+
if (start < 0 || end < 0) {
|
|
45
|
+
throw new Error("parseIssueArgs (or the point where it ends, ProjectAction) moved in local-args-tower-work.ts — "
|
|
46
|
+
+ "work-flag-census.ts's extraction needs updating alongside it.");
|
|
47
|
+
}
|
|
48
|
+
const slice = source.slice(start, end);
|
|
49
|
+
const valueFlagsStart = slice.indexOf("valueFlags: [");
|
|
50
|
+
if (valueFlagsStart < 0)
|
|
51
|
+
throw new Error("parseIssueArgs's own valueFlags array not found.");
|
|
52
|
+
const valueFlagsEnd = slice.indexOf("]", valueFlagsStart);
|
|
53
|
+
return [...slice.slice(valueFlagsStart, valueFlagsEnd).matchAll(/"(--[a-z-]+)"/g)].map((match) => match[1]);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Flags that name a filter, a global, or a transport knob rather than a
|
|
57
|
+
* FIELD on the issue row itself — never counted as a "write field" either
|
|
58
|
+
* side owes the other. `--file` carries the same DESCRIPTION the MCP tools
|
|
59
|
+
* take as a plain `description` argument; see `EXPECTED_ONE_SIDED` below for
|
|
60
|
+
* why that one stays a documented asymmetry rather than a shared name.
|
|
61
|
+
*/
|
|
62
|
+
const NON_FIELD_FLAGS = new Set(["--home", "--dashboard-url", "--state", "--limit", "--file"]);
|
|
63
|
+
/** The bare field names (`--parent` → `parent`) `cockpit issue create`/`update` actually write. */
|
|
64
|
+
export function issueWriteFieldNames() {
|
|
65
|
+
return issueValueFlags()
|
|
66
|
+
.filter((flag) => !NON_FIELD_FLAGS.has(flag))
|
|
67
|
+
.map((flag) => flag.replace(/^--/, ""));
|
|
68
|
+
}
|
|
69
|
+
export const WORK_WRITE_VERBS = ["issue create", "issue update"];
|
|
70
|
+
export const MCP_TOOL_FOR_WRITE_VERB = {
|
|
71
|
+
"issue create": "work_create_issue",
|
|
72
|
+
"issue update": "work_update_issue",
|
|
73
|
+
};
|
|
74
|
+
/** The field names an MCP tool's REGISTERED zod shape actually carries. */
|
|
75
|
+
export function mcpToolFieldNames(toolName) {
|
|
76
|
+
const deps = {
|
|
77
|
+
resolveToken: async () => "token-not-used-by-registration",
|
|
78
|
+
baseUrl: "https://dashboard.example.test",
|
|
79
|
+
fetchImpl: (async () => new Response("{}")),
|
|
80
|
+
};
|
|
81
|
+
const server = createServer(deps);
|
|
82
|
+
const shape = server._registeredTools[toolName]?.inputSchema?.shape;
|
|
83
|
+
if (!shape) {
|
|
84
|
+
throw new Error(`${toolName} has no inputSchema shape — is it still registered under this name?`);
|
|
85
|
+
}
|
|
86
|
+
return Object.keys(shape);
|
|
87
|
+
}
|
|
88
|
+
export const EXPECTED_ONE_SIDED_FIELDS = [
|
|
89
|
+
{
|
|
90
|
+
verb: "issue update",
|
|
91
|
+
side: "mcp",
|
|
92
|
+
field: "id",
|
|
93
|
+
reason: "the issue being updated is a positional argument on the CLI (`cockpit issue update <id>`), never a flag — "
|
|
94
|
+
+ "there is nothing for it to be symmetric WITH.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
verb: "issue create",
|
|
98
|
+
side: "mcp",
|
|
99
|
+
field: "description",
|
|
100
|
+
reason: "the CLI never takes a description on argv, by the sourcing-secrets discipline every body follows here — "
|
|
101
|
+
+ "stdin or --file only. An MCP call has no stdin, so its tool takes description as a plain argument instead.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
verb: "issue update",
|
|
105
|
+
side: "mcp",
|
|
106
|
+
field: "description",
|
|
107
|
+
reason: "the same reason as issue create's: stdin/--file on the CLI, a plain argument on MCP.",
|
|
108
|
+
},
|
|
109
|
+
];
|
|
@@ -19,11 +19,40 @@ export interface IssueRow {
|
|
|
19
19
|
title: string;
|
|
20
20
|
state: string;
|
|
21
21
|
priority?: number | null;
|
|
22
|
+
estimate?: number | null;
|
|
22
23
|
assignee_id?: string | null;
|
|
23
24
|
project_id?: string | null;
|
|
24
25
|
parent_id?: string | null;
|
|
25
26
|
updated_at?: string | null;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* `size` on `work_create_issue`/`work_update_issue` (BLI-10575) maps onto the
|
|
30
|
+
* existing `work_issues.estimate` numeric column — the CLI twin of this,
|
|
31
|
+
* `issue-contracts.ts` in the collector, carries the identical table and
|
|
32
|
+
* comment; kept as a second copy rather than a shared import for the same
|
|
33
|
+
* reason `work-directory.ts` duplicates `issue-directory.ts` instead of
|
|
34
|
+
* depending on the collector package.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SIZE_ESTIMATES: {
|
|
37
|
+
readonly S: 1;
|
|
38
|
+
readonly M: 3;
|
|
39
|
+
readonly L: 5;
|
|
40
|
+
};
|
|
41
|
+
export type IssueSize = keyof typeof SIZE_ESTIMATES;
|
|
42
|
+
export declare function estimateForSize(size: string): number;
|
|
43
|
+
export declare function sizeForEstimate(estimate: number | null | undefined): IssueSize | null;
|
|
44
|
+
/**
|
|
45
|
+
* `T extends object` rather than `T extends { estimate?: ... }` on purpose:
|
|
46
|
+
* a caller here often has `result.issue ?? {}` in hand, and TypeScript's
|
|
47
|
+
* excess-property check on an all-optional ("weak") object type refuses a
|
|
48
|
+
* `{}` that shares NO property with `{ estimate?: ... }` at all (TS2559) —
|
|
49
|
+
* exactly the shape `withAssigneeIdentity(... ?? {}, directory)` hands back
|
|
50
|
+
* when the issue itself is absent. Reading `estimate` off an `object` cast
|
|
51
|
+
* keeps the same runtime behaviour without that false refusal.
|
|
52
|
+
*/
|
|
53
|
+
export declare function withSizeLabel<T extends object>(row: T): T & {
|
|
54
|
+
size: IssueSize | null;
|
|
55
|
+
};
|
|
27
56
|
/**
|
|
28
57
|
* The fields `work_list_issues` promises. The door's row carries the whole
|
|
29
58
|
* description and a dozen bookkeeping columns; with no limit that was 1,000
|
|
@@ -36,6 +65,7 @@ export interface IssueListRow {
|
|
|
36
65
|
title: string;
|
|
37
66
|
state: string;
|
|
38
67
|
priority: number | null;
|
|
68
|
+
estimate: number | null;
|
|
39
69
|
assignee_id: string | null;
|
|
40
70
|
project_id: string | null;
|
|
41
71
|
parent_id: string | null;
|
package/dist/work-issue-rows.js
CHANGED
|
@@ -13,6 +13,44 @@
|
|
|
13
13
|
* Every string moved verbatim.
|
|
14
14
|
*/
|
|
15
15
|
import { assigneeLabel } from "./work-directory.js";
|
|
16
|
+
/**
|
|
17
|
+
* `size` on `work_create_issue`/`work_update_issue` (BLI-10575) maps onto the
|
|
18
|
+
* existing `work_issues.estimate` numeric column — the CLI twin of this,
|
|
19
|
+
* `issue-contracts.ts` in the collector, carries the identical table and
|
|
20
|
+
* comment; kept as a second copy rather than a shared import for the same
|
|
21
|
+
* reason `work-directory.ts` duplicates `issue-directory.ts` instead of
|
|
22
|
+
* depending on the collector package.
|
|
23
|
+
*/
|
|
24
|
+
export const SIZE_ESTIMATES = { S: 1, M: 3, L: 5 };
|
|
25
|
+
export function estimateForSize(size) {
|
|
26
|
+
const key = size.trim().toUpperCase();
|
|
27
|
+
if (key !== "S" && key !== "M" && key !== "L") {
|
|
28
|
+
throw new Error("size must be S, M, or L.");
|
|
29
|
+
}
|
|
30
|
+
return SIZE_ESTIMATES[key];
|
|
31
|
+
}
|
|
32
|
+
export function sizeForEstimate(estimate) {
|
|
33
|
+
if (estimate === null || estimate === undefined)
|
|
34
|
+
return null;
|
|
35
|
+
for (const key of Object.keys(SIZE_ESTIMATES)) {
|
|
36
|
+
if (SIZE_ESTIMATES[key] === estimate)
|
|
37
|
+
return key;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `T extends object` rather than `T extends { estimate?: ... }` on purpose:
|
|
43
|
+
* a caller here often has `result.issue ?? {}` in hand, and TypeScript's
|
|
44
|
+
* excess-property check on an all-optional ("weak") object type refuses a
|
|
45
|
+
* `{}` that shares NO property with `{ estimate?: ... }` at all (TS2559) —
|
|
46
|
+
* exactly the shape `withAssigneeIdentity(... ?? {}, directory)` hands back
|
|
47
|
+
* when the issue itself is absent. Reading `estimate` off an `object` cast
|
|
48
|
+
* keeps the same runtime behaviour without that false refusal.
|
|
49
|
+
*/
|
|
50
|
+
export function withSizeLabel(row) {
|
|
51
|
+
const estimate = row.estimate;
|
|
52
|
+
return { ...row, size: sizeForEstimate(estimate ?? null) };
|
|
53
|
+
}
|
|
16
54
|
export function issueListRow(row) {
|
|
17
55
|
return {
|
|
18
56
|
id: row.id,
|
|
@@ -20,6 +58,7 @@ export function issueListRow(row) {
|
|
|
20
58
|
title: row.title,
|
|
21
59
|
state: row.state,
|
|
22
60
|
priority: row.priority ?? null,
|
|
61
|
+
estimate: row.estimate ?? null,
|
|
23
62
|
assignee_id: row.assignee_id ?? null,
|
|
24
63
|
project_id: row.project_id ?? null,
|
|
25
64
|
parent_id: row.parent_id ?? null,
|
|
@@ -55,8 +94,9 @@ export function outsideNote(outside) {
|
|
|
55
94
|
}
|
|
56
95
|
export function renderOneIssue(result, directory) {
|
|
57
96
|
const { issue, comments } = result;
|
|
97
|
+
const size = sizeForEstimate(issue?.estimate ?? null);
|
|
58
98
|
const commentText = comments.map((comment) => `[${comment.created_at ?? ""}] ${comment.body_markdown ?? ""}`).join("\n\n");
|
|
59
|
-
return `${issue?.identifier ?? result.ref} ${issue?.title ?? ""} (${issue?.state ?? "?"}) · assignee ${assigneeLabel(issue?.assignee_id, directory)}`
|
|
99
|
+
return `${issue?.identifier ?? result.ref} ${issue?.title ?? ""} (${issue?.state ?? "?"})${size ? ` · size ${size}` : ""} · assignee ${assigneeLabel(issue?.assignee_id, directory)}`
|
|
60
100
|
+ `\n\n${issue?.description ?? "(no description)"}`
|
|
61
101
|
+ (commentText ? `\n\n--- comments ---\n${commentText}` : "")
|
|
62
102
|
+ (result.commentsUnread ? "\n\n(comments could not be read on this call)" : "");
|
package/dist/work-tools.js
CHANGED
|
@@ -18,7 +18,7 @@ import { z } from "zod";
|
|
|
18
18
|
import { callAgentDoor } from "./agent-door.js";
|
|
19
19
|
import { DEFAULT_LIST_LIMIT, doorFailureText, errorResult, moreNote, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
20
20
|
import { assigneeLabel, resolveAssigneeDirectory, withAssigneeIdentity } from "./work-directory.js";
|
|
21
|
-
import { historyChange, issueListRow, listCompleteness, outsideNote, outsideProject, renderOneIssue, } from "./work-issue-rows.js";
|
|
21
|
+
import { estimateForSize, historyChange, issueListRow, listCompleteness, outsideNote, outsideProject, renderOneIssue, sizeForEstimate, withSizeLabel, } from "./work-issue-rows.js";
|
|
22
22
|
/** The closed state vocabulary, verbatim from `lib/work/api-doors.ts` `WORK_ISSUE_STATES`. */
|
|
23
23
|
export const WORK_ISSUE_STATES = [
|
|
24
24
|
"backlog",
|
|
@@ -63,6 +63,24 @@ const ISSUE_REF = z
|
|
|
63
63
|
.min(1)
|
|
64
64
|
.max(200)
|
|
65
65
|
.describe("A BLI-#### identifier (e.g. BLI-3654) or an issue's uuid. Both work.");
|
|
66
|
+
/**
|
|
67
|
+
* A parent issue's uuid from any issue reference — the `create_id`/`update_id`
|
|
68
|
+
* bodies below need a uuid, and `[issueId]`'s own ref resolution
|
|
69
|
+
* (`lib/work/issue-ref.ts`) only runs on the PATH segment of `GET
|
|
70
|
+
* /api/work/issues/[issueId]`, never on a `parent_id` in a POST/PATCH body.
|
|
71
|
+
* `issue-contracts.ts#resolveIssueUuid` in the collector is the CLI's own
|
|
72
|
+
* copy of the identical resolve-then-write two-step.
|
|
73
|
+
*/
|
|
74
|
+
async function resolveParentId(deps, session, ref) {
|
|
75
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/work/issues/${encodeURIComponent(ref)}`);
|
|
76
|
+
if (!response.ok)
|
|
77
|
+
return { status: "failed", text: doorFailureText("work_get_issue", response) };
|
|
78
|
+
const parent = response.body.issue;
|
|
79
|
+
if (!parent?.id) {
|
|
80
|
+
return { status: "failed", text: `No issue matches "${ref}" — it may not exist, or you cannot read it.` };
|
|
81
|
+
}
|
|
82
|
+
return { status: "ok", id: parent.id };
|
|
83
|
+
}
|
|
66
84
|
export function registerWorkTools(server, deps) {
|
|
67
85
|
const register = registrarFor(server);
|
|
68
86
|
register("work_list_issues", {
|
|
@@ -111,12 +129,15 @@ export function registerWorkTools(server, deps) {
|
|
|
111
129
|
// BLI-10450: one directory call for the whole page, never one per row.
|
|
112
130
|
const directory = await resolveAssigneeDirectory(deps, session, issues.map((issue) => issue.assignee_id));
|
|
113
131
|
const lines = issues
|
|
114
|
-
.map((issue) =>
|
|
115
|
-
|
|
132
|
+
.map((issue) => {
|
|
133
|
+
const size = sizeForEstimate(issue.estimate ?? null);
|
|
134
|
+
return `${issue.identifier} ${issue.state} ${issue.title}`
|
|
135
|
+
+ ` [p${issue.priority ?? 0}${size ? `, size ${size}` : ""}, assignee ${assigneeLabel(issue.assignee_id, directory)}, project ${issue.project_id ?? "none"}]`;
|
|
136
|
+
})
|
|
116
137
|
.join("\n");
|
|
117
138
|
const totalNote = completeness?.total != null && completeness.total > issues.length ? ` ${completeness.total} match in all.` : "";
|
|
118
139
|
return textResult(`${issues.length} issue(s).${totalNote}${moreNote(more, ISSUE_LIST_MAX)}${outsideNote(outside)}${lines ? `\n${lines}` : ""}`, {
|
|
119
|
-
issues: issues.map((issue) => withAssigneeIdentity(issueListRow(issue), directory)),
|
|
140
|
+
issues: issues.map((issue) => withSizeLabel(withAssigneeIdentity(issueListRow(issue), directory))),
|
|
120
141
|
more,
|
|
121
142
|
...(completeness ? { total: completeness.total } : {}),
|
|
122
143
|
...(outside ? { outside_project: outside } : {}),
|
|
@@ -146,7 +167,7 @@ export function registerWorkTools(server, deps) {
|
|
|
146
167
|
if (!result?.ok)
|
|
147
168
|
return errorResult(result?.text ?? "work_get_issue could not read that issue.");
|
|
148
169
|
return textResult(renderOneIssue(result, directory), {
|
|
149
|
-
issue: withAssigneeIdentity(result.issue ?? {}, directory),
|
|
170
|
+
issue: withSizeLabel(withAssigneeIdentity(result.issue ?? {}, directory)),
|
|
150
171
|
comments: result.comments,
|
|
151
172
|
...(result.commentsUnread ? { comments_unread: true } : {}),
|
|
152
173
|
});
|
|
@@ -160,7 +181,7 @@ export function registerWorkTools(server, deps) {
|
|
|
160
181
|
? {
|
|
161
182
|
ref: result.ref,
|
|
162
183
|
ok: true,
|
|
163
|
-
issue: withAssigneeIdentity(result.issue ?? {}, directory),
|
|
184
|
+
issue: withSizeLabel(withAssigneeIdentity(result.issue ?? {}, directory)),
|
|
164
185
|
comments: result.comments,
|
|
165
186
|
...(result.commentsUnread ? { comments_unread: true } : {}),
|
|
166
187
|
}
|
|
@@ -175,6 +196,11 @@ export function registerWorkTools(server, deps) {
|
|
|
175
196
|
description: z.string().max(200_000).optional().describe("The issue body, markdown."),
|
|
176
197
|
project: z.string().min(1).max(200).optional().describe("A project name or uuid."),
|
|
177
198
|
priority: z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low."),
|
|
199
|
+
// BLI-10575: the CLI twin of this tool, `cockpit issue create`, has
|
|
200
|
+
// had --parent since BLI-3716; this field was missing, and nothing
|
|
201
|
+
// checked for the gap — see work-flag-census.test.ts.
|
|
202
|
+
parent: ISSUE_REF.optional().describe("Another issue's BLI-#### identifier or uuid — this issue becomes its sub-issue."),
|
|
203
|
+
size: z.enum(["S", "M", "L"]).optional().describe("Tower's own size scale, stored in the existing estimate column: S=1, M=3, L=5 (BLI-10575)."),
|
|
178
204
|
assignee: z.string().min(1).max(100).optional().describe('"me" or a person\'s uuid.'),
|
|
179
205
|
},
|
|
180
206
|
}, async (args) => withSession(deps, async (session) => {
|
|
@@ -188,17 +214,26 @@ export function registerWorkTools(server, deps) {
|
|
|
188
214
|
}
|
|
189
215
|
projectId = resolved.id;
|
|
190
216
|
}
|
|
217
|
+
let parentId;
|
|
218
|
+
if (args.parent) {
|
|
219
|
+
const resolved = await resolveParentId(deps, session, String(args.parent));
|
|
220
|
+
if (resolved.status === "failed")
|
|
221
|
+
return errorResult(resolved.text);
|
|
222
|
+
parentId = resolved.id;
|
|
223
|
+
}
|
|
191
224
|
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/work/issues", {
|
|
192
225
|
title: args.title,
|
|
193
226
|
...(args.description === undefined ? {} : { description: args.description }),
|
|
194
227
|
...(projectId ? { project_id: projectId } : {}),
|
|
195
228
|
...(args.priority === undefined ? {} : { priority: args.priority }),
|
|
229
|
+
...(args.size === undefined ? {} : { estimate: estimateForSize(String(args.size)) }),
|
|
196
230
|
...(args.assignee ? { assignee_id: args.assignee } : {}),
|
|
231
|
+
...(parentId ? { parent_id: parentId } : {}),
|
|
197
232
|
});
|
|
198
233
|
if (!response.ok)
|
|
199
234
|
return errorResult(doorFailureText("work_create_issue", response));
|
|
200
235
|
const issue = response.body.issue;
|
|
201
|
-
return textResult(`Filed ${issue?.identifier ?? ""} — ${issue?.title ?? ""} (${issue?.id ?? ""}).`, { issue });
|
|
236
|
+
return textResult(`Filed ${issue?.identifier ?? ""} — ${issue?.title ?? ""} (${issue?.id ?? ""}).`, { issue: issue ? withSizeLabel(issue) : issue });
|
|
202
237
|
}));
|
|
203
238
|
register("work_update_issue", {
|
|
204
239
|
title: "Update a Tower issue",
|
|
@@ -209,6 +244,10 @@ export function registerWorkTools(server, deps) {
|
|
|
209
244
|
title: z.string().min(1).max(300).optional().describe("A new title."),
|
|
210
245
|
description: z.string().max(200_000).nullable().optional().describe("The WHOLE new body, markdown; null clears it."),
|
|
211
246
|
priority: z.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low."),
|
|
247
|
+
// BLI-10575: had no MCP twin at all — `cockpit issue update --parent`
|
|
248
|
+
// could re-parent a ticket and nothing on this server could.
|
|
249
|
+
parent: ISSUE_REF.optional().describe("Another issue's BLI-#### identifier or uuid — this issue becomes its sub-issue."),
|
|
250
|
+
size: z.enum(["S", "M", "L"]).optional().describe("Tower's own size scale, stored in the existing estimate column: S=1, M=3, L=5 (BLI-10575)."),
|
|
212
251
|
assignee: z.string().min(1).max(100).nullable().optional().describe('"me", a uuid, or null to unassign.'),
|
|
213
252
|
project: z.string().min(1).max(200).optional().describe("A project name or uuid."),
|
|
214
253
|
},
|
|
@@ -224,17 +263,26 @@ export function registerWorkTools(server, deps) {
|
|
|
224
263
|
}
|
|
225
264
|
projectId = resolved.id;
|
|
226
265
|
}
|
|
266
|
+
let parentId;
|
|
267
|
+
if (args.parent) {
|
|
268
|
+
const resolved = await resolveParentId(deps, session, String(args.parent));
|
|
269
|
+
if (resolved.status === "failed")
|
|
270
|
+
return errorResult(resolved.text);
|
|
271
|
+
parentId = resolved.id;
|
|
272
|
+
}
|
|
227
273
|
const response = await callAgentDoor(session, deps.fetchImpl, "PATCH", `/api/work/issues/${encodeURIComponent(ref)}`, {
|
|
228
274
|
...(args.title !== undefined ? { title: args.title } : {}),
|
|
229
275
|
...(args.description !== undefined ? { description: args.description } : {}),
|
|
230
276
|
...(args.priority !== undefined ? { priority: args.priority } : {}),
|
|
277
|
+
...(args.size === undefined ? {} : { estimate: estimateForSize(String(args.size)) }),
|
|
231
278
|
...(args.assignee !== undefined ? { assignee_id: args.assignee } : {}),
|
|
232
279
|
...(projectId ? { project_id: projectId } : {}),
|
|
280
|
+
...(parentId ? { parent_id: parentId } : {}),
|
|
233
281
|
});
|
|
234
282
|
if (!response.ok)
|
|
235
283
|
return errorResult(doorFailureText("work_update_issue", response));
|
|
236
284
|
const issue = response.body.issue;
|
|
237
|
-
return textResult(`Updated ${issue?.identifier ?? ref} — ${issue?.title ?? ""}.`, { issue });
|
|
285
|
+
return textResult(`Updated ${issue?.identifier ?? ref} — ${issue?.title ?? ""}.`, { issue: issue ? withSizeLabel(issue) : issue });
|
|
238
286
|
}));
|
|
239
287
|
register("work_move_issue", {
|
|
240
288
|
title: "Move a Tower issue",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
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",
|