@thehammer/danx-dashboard-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/handlers.js +72 -0
- package/dist/index.js +54 -6
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -1140,3 +1140,75 @@ export async function planDeleteRecord(client, args) {
|
|
|
1140
1140
|
body: { content_hash: args.content_hash },
|
|
1141
1141
|
});
|
|
1142
1142
|
}
|
|
1143
|
+
// ---------------- failure_category_list / _create / _update (DX-2792) ----------------
|
|
1144
|
+
/**
|
|
1145
|
+
* DX-2792 (Failure evaluation 3/4) — wraps `src/dashboard/failure-categories-routes.ts`,
|
|
1146
|
+
* the DX-2791 (Failure evaluation 2/4) category registry's REST API. INSTALL-
|
|
1147
|
+
* GLOBAL, not board-scoped: `failure_categories` carries no `board_id`
|
|
1148
|
+
* column (every category applies across the whole install), so — unlike
|
|
1149
|
+
* every `/api/issues/*`-backed tool above — these three never send a
|
|
1150
|
+
* `board` query param and the `board` override field is simply absent from
|
|
1151
|
+
* their schemas (mirrors the `plan_*` family's own board-less rationale in
|
|
1152
|
+
* `index.ts`'s `boardField` comment, for the same underlying reason: nothing
|
|
1153
|
+
* server-side would read it).
|
|
1154
|
+
*/
|
|
1155
|
+
const FAILURE_CATEGORIES_BASE_PATH = "/api/failure-categories";
|
|
1156
|
+
/**
|
|
1157
|
+
* List every failure category with its live matched-occurrence count and
|
|
1158
|
+
* last-seen time, via `GET /api/failure-categories`. Returns
|
|
1159
|
+
* `{categories: [{id, name, description, matchers, ignore, ignoreReason,
|
|
1160
|
+
* expectedRate, matchedCount, lastSeenMs, ...}]}`. A fresh install returns
|
|
1161
|
+
* `{categories: []}`.
|
|
1162
|
+
*/
|
|
1163
|
+
export async function failureCategoryList(client) {
|
|
1164
|
+
return client.request({
|
|
1165
|
+
method: "GET",
|
|
1166
|
+
path: "",
|
|
1167
|
+
basePath: FAILURE_CATEGORIES_BASE_PATH,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Create a new failure category via `POST /api/failure-categories`. At
|
|
1172
|
+
* least one matcher, each with at least one of `sourceKind`/`tool`/
|
|
1173
|
+
* `regexPattern` set, is required (400 otherwise). `ignore: true` REQUIRES a
|
|
1174
|
+
* non-empty `ignoreReason` (400 otherwise). A matcher set that would overlap
|
|
1175
|
+
* an EXISTING category's matchers is refused 400 naming the conflicting
|
|
1176
|
+
* category — expand that category instead of creating a near-duplicate. On
|
|
1177
|
+
* success, the dashboard re-matches every existing uncategorized occurrence
|
|
1178
|
+
* against the new category before responding, so `matchedCount` in the
|
|
1179
|
+
* response already reflects any newly-covered signatures.
|
|
1180
|
+
*/
|
|
1181
|
+
export async function failureCategoryCreate(client, args) {
|
|
1182
|
+
return client.request({
|
|
1183
|
+
method: "POST",
|
|
1184
|
+
path: "",
|
|
1185
|
+
basePath: FAILURE_CATEGORIES_BASE_PATH,
|
|
1186
|
+
body: {
|
|
1187
|
+
name: args.name,
|
|
1188
|
+
description: args.description ?? "",
|
|
1189
|
+
matchers: args.matchers,
|
|
1190
|
+
ignore: args.ignore ?? false,
|
|
1191
|
+
ignoreReason: args.ignoreReason ?? null,
|
|
1192
|
+
expectedRate: args.expectedRate ?? null,
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Patch an existing failure category via `PATCH /api/failure-categories/:id`
|
|
1198
|
+
* — the tool for BOTH "expand an existing category's matchers" (send the
|
|
1199
|
+
* full replacement `matchers` array) and "mark a category ignored" (send
|
|
1200
|
+
* `ignore: true` + a non-empty `ignoreReason`). At least one field is
|
|
1201
|
+
* required (400 otherwise). Same overlap refusal as create (excluding this
|
|
1202
|
+
* category's own prior matchers). On success, re-matches every
|
|
1203
|
+
* uncategorized occurrence against the updated matcher set before
|
|
1204
|
+
* responding.
|
|
1205
|
+
*/
|
|
1206
|
+
export async function failureCategoryUpdate(client, args) {
|
|
1207
|
+
const { id, ...patch } = args;
|
|
1208
|
+
return client.request({
|
|
1209
|
+
method: "PATCH",
|
|
1210
|
+
path: `/${id}`,
|
|
1211
|
+
basePath: FAILURE_CATEGORIES_BASE_PATH,
|
|
1212
|
+
body: patch,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
* - plan_update_architecture_section PATCH /api/plans/mine/architecture/sections/:sid (DX-2726)
|
|
50
50
|
* - plan_delete_architecture_section DELETE /api/plans/mine/architecture/sections/:sid (DX-2726)
|
|
51
51
|
* - plan_reorder_architecture_section PUT /api/plans/mine/architecture/sections/reorder (DX-2726)
|
|
52
|
+
* - failure_category_list GET /api/failure-categories (DX-2791/DX-2792, board-less)
|
|
53
|
+
* - failure_category_create POST /api/failure-categories (DX-2791/DX-2792, board-less)
|
|
54
|
+
* - failure_category_update PATCH /api/failure-categories/:id (DX-2791/DX-2792, board-less)
|
|
52
55
|
*
|
|
53
56
|
* DX-2683 — THE PLAN TOOLS ARE SESSION-BOUND, and asymmetrically so. Reads
|
|
54
57
|
* may name any plan; WRITES take no plan id at all and act on the plan this
|
|
@@ -88,7 +91,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
88
91
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
89
92
|
import { z } from "zod";
|
|
90
93
|
import { DashboardHttpClient } from "./http-client.js";
|
|
91
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
94
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
92
95
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
93
96
|
function readEnvOrDie(name) {
|
|
94
97
|
const v = process.env[name];
|
|
@@ -429,7 +432,7 @@ server.tool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED
|
|
|
429
432
|
// ---------------- issue_transition ----------------
|
|
430
433
|
server.tool("issue_transition",
|
|
431
434
|
// DX-2735: trimmed with the problem tools to stay inside the work-profile budget.
|
|
432
|
-
"Move a card's lifecycle via POST /api/issues/:id/transition — the ONLY way; `danxbot_complete` never moves a card, so call this first. Actions: ready (Review→ToDo); pickup (ToDo→In Progress; checks every dispatch gate — ready, blocked, requires_human, depends_on terminal, conflict_on idle — and refuses 409 with failed_gate naming the cause; `manual:true` is a self-pickup for work in YOUR session: it bypasses card-flow gates and is never auto-rolled-back); rollback_pickup; complete (your explicit decision; 409 on an Epic with non-terminal children (non_terminal_phases[]) or while a required POST quality gate is not pass (failed_gate 'quality_gate_post' + failed_post_gates[])); cancel (terminal); block (non-empty reason; only holds dispatch, never asks a human — for that use issue_problem then issue_requires_human; env faults use `danxbot_complete({status:'failed'})`); unblock; archive (to Backlog, clears ready_at); reopen (terminal→active). Terminal cards refuse all but reopen; forward stamps never clear earlier ones. A dispatched agent's manual pickup MUST pass `assigned_agent` = your agent/profile name (409 otherwise); one that loses a race is refused 409 `failed_gate: \"dispatch_id\"` — re-read assigned_agent/dispatch_id, never retry blindly.", {
|
|
435
|
+
"Move a card's lifecycle via POST /api/issues/:id/transition — the ONLY way; `danxbot_complete` never moves a card, so call this first. Actions: ready (Review→ToDo); pickup (ToDo→In Progress; checks every dispatch gate — ready, blocked, requires_human, depends_on terminal, conflict_on idle — and refuses 409 with failed_gate naming the cause; `manual:true` is a self-pickup for work in YOUR session: it bypasses card-flow gates and is never auto-rolled-back); rollback_pickup (`keep_assignment:true` releases the card to ready WITHOUT clearing its assignment — use this to hand a manually-held card back to `ready` while you keep holding it, instead of a follow-up assigned-agent call); complete (your explicit decision; 409 on an Epic with non-terminal children (non_terminal_phases[]) or while a required POST quality gate is not pass (failed_gate 'quality_gate_post' + failed_post_gates[])); cancel (terminal); block (non-empty reason; only holds dispatch, never asks a human — for that use issue_problem then issue_requires_human; env faults use `danxbot_complete({status:'failed'})`); unblock; archive (to Backlog, clears ready_at); reopen (terminal→active). Terminal cards refuse all but reopen; forward stamps never clear earlier ones. A dispatched agent's manual pickup MUST pass `assigned_agent` = your agent/profile name (409 otherwise); one that loses a race is refused 409 `failed_gate: \"dispatch_id\"` — re-read assigned_agent/dispatch_id, never retry blindly.", {
|
|
433
436
|
id: z.string().min(1),
|
|
434
437
|
action: z.enum(TRANSITION_ACTIONS),
|
|
435
438
|
reason: z.string().optional(),
|
|
@@ -441,6 +444,10 @@ server.tool("issue_transition",
|
|
|
441
444
|
.min(1)
|
|
442
445
|
.optional()
|
|
443
446
|
.describe("Required for a dispatched agent's manual:true pickup: your agent/profile name, never the shared dispatch-token identity. Optional for a human session; ignored by other actions."),
|
|
447
|
+
keep_assignment: z
|
|
448
|
+
.boolean()
|
|
449
|
+
.optional()
|
|
450
|
+
.describe("rollback_pickup-only. true preserves assigned_agent + assignment_mode across the release (the card lands ready still manually held, off the automated dispatcher) instead of the default clear-and-hand-back-to-automation."),
|
|
444
451
|
...boardField,
|
|
445
452
|
}, async (args) => jsonResult(await issueTransition(client, args)));
|
|
446
453
|
// ---------------- issue_triage ----------------
|
|
@@ -654,8 +661,8 @@ server.tool("brief_set_page", 'Write one Brief page via PUT /api/brief/page?slug
|
|
|
654
661
|
// plan id — and it can only ever bind the caller's own session. `plan_create`
|
|
655
662
|
// also takes no plan id, but for a different reason: it MAKES a plan rather
|
|
656
663
|
// than acting on one, so there is no existing plan for an id to name yet.
|
|
657
|
-
server.tool("plan_list", "List every plan via GET /api/plans (DX-2683), and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped: a plan is a named, dated set of cards an operator assembled by hand, and its cards may come from any repository. Returns `{ok, status, body: {plans: [{id, name, createdAt, cardCount, boards}], session, sessionListenerAttached}}
|
|
658
|
-
server.tool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, session, sessionListenerAttached, available_field_groups}` — no cards, records, or architecture body. Pass `fields` to opt into: `cards` (member cards, PAGED: `cards_offset` (default 0) and `cards_limit` (1.." + LIST_PAGE_MAX_LIMIT + ", default " + PLAN_GET_CARDS_DEFAULT_LIMIT + ") pick the page, in stable card-reference order (board prefix, then card number — pages never repeat/skip unless membership changes between reads); response carries `cards_total`/`cards_offset` — page while cards_offset + cards.length < cards_total; either paging arg without `fields: [\"cards\"]` is a 400), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (one kind, cheaper), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan). `session`/`sessionListenerAttached` and `available_field_groups` ride every response regardless. `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the plugin's event bridge starts; still `false` after that while connected means the plan's card events are not reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
|
|
664
|
+
server.tool("plan_list", "List every plan via GET /api/plans (DX-2683), and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped: a plan is a named, dated set of cards an operator assembled by hand, and its cards may come from any repository. Returns `{ok, status, body: {plans: [{id, ref, name, createdAt, cardCount, boards}], session, sessionListenerAttached}}` — `ref` is the plan's short reference (`PLN-<id>`), the same thing a card's own id is for a card; cite it rather than a bare id. `session` is your own registration — `{sessionId, title, planId, planName, firstSeenAt, lastActiveAt}` — or `null` if this process is not running inside a Claude Code session. A `planId` of null means you are connected to no plan: read any plan with `plan_get`, then `plan_connect` to the one you are working on (or ask the operator to connect you from the Plans list). `sessionListenerAttached` says whether your session's event stream is attached (the danxbot plugin's plan event bridge holds it). It is `false` for a few seconds right after `plan_connect` while the bridge starts; still `false` after that while connected means its card events are NOT reaching you — tell the operator. There is nothing to arm. NOTE this is NOT the board Brief (`brief_list`), which is a different feature entirely.", {}, async () => jsonResult(await planList(client)));
|
|
665
|
+
server.tool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, session, sessionListenerAttached, available_field_groups}` — no cards, records, or architecture body. `plan` carries `{id, ref, name, createdAt}`; `ref` is the plan's short reference (`PLN-<id>`) — cite that, not the bare id. Pass `fields` to opt into: `cards` (member cards, PAGED: `cards_offset` (default 0) and `cards_limit` (1.." + LIST_PAGE_MAX_LIMIT + ", default " + PLAN_GET_CARDS_DEFAULT_LIMIT + ") pick the page, in stable card-reference order (board prefix, then card number — pages never repeat/skip unless membership changes between reads); response carries `cards_total`/`cards_offset` — page while cards_offset + cards.length < cards_total; either paging arg without `fields: [\"cards\"]` is a 400), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (one kind, cheaper), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan). `session`/`sessionListenerAttached` and `available_field_groups` ride every response regardless. `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the plugin's event bridge starts; still `false` after that while connected means the plan's card events are not reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
|
|
659
666
|
plan_id: z
|
|
660
667
|
.number()
|
|
661
668
|
.int()
|
|
@@ -680,7 +687,7 @@ server.tool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id
|
|
|
680
687
|
.optional()
|
|
681
688
|
.describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields` to include `cards`."),
|
|
682
689
|
}, async (args) => jsonResult(await planGet(client, args)));
|
|
683
|
-
server.tool("plan_create", "Create a new, empty plan via POST /api/plans (DX-2531). GLOBAL — a plan is not board-scoped, and this call adds no cards, no records, and no architecture sections; it does NOT connect any session to the new plan (call `plan_connect` separately, exactly as adding a card to a plan is its own separate step). Returns `{ok, status, body: {plan: {id, name, createdAt}}}
|
|
690
|
+
server.tool("plan_create", "Create a new, empty plan via POST /api/plans (DX-2531). GLOBAL — a plan is not board-scoped, and this call adds no cards, no records, and no architecture sections; it does NOT connect any session to the new plan (call `plan_connect` separately, exactly as adding a card to a plan is its own separate step). Returns `{ok, status, body: {plan: {id, ref, name, createdAt}}}` — `ref` is the plan's short reference (`PLN-<id>`). Use the returned `plan.id` with `plan_connect` to start working on it, or with `plan_get({plan_id})` to browse it.", {
|
|
684
691
|
name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
|
|
685
692
|
}, async (args) => jsonResult(await planCreate(client, args)));
|
|
686
693
|
server.tool("plan_connect",
|
|
@@ -717,7 +724,7 @@ server.tool("plan_remove_card", "Remove a card from a plan via DELETE /api/plans
|
|
|
717
724
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
718
725
|
card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
|
|
719
726
|
}, async (args) => jsonResult(await planRemoveCard(client, args)));
|
|
720
|
-
server.tool("plan_rename", "Rename a plan via PATCH /api/plans/:plan_id (DX-2740) — the ONLY way to change a plan's `name`; nothing else in this tool surface can fix a stale name. Takes an EXPLICIT `plan_id`, not your connected session's plan, so you may rename any plan you can name. `name` must be a non-empty string (400 otherwise). The new name is visible immediately in a follow-up `plan_list` or `plan_get`. Unknown plan → 404. Returns the renamed plan `{id, name, createdAt}`.", {
|
|
727
|
+
server.tool("plan_rename", "Rename a plan via PATCH /api/plans/:plan_id (DX-2740) — the ONLY way to change a plan's `name`; nothing else in this tool surface can fix a stale name. Takes an EXPLICIT `plan_id`, not your connected session's plan, so you may rename any plan you can name. `name` must be a non-empty string (400 otherwise). The new name is visible immediately in a follow-up `plan_list` or `plan_get`. Unknown plan → 404. Returns the renamed plan `{id, ref, name, createdAt}`.", {
|
|
721
728
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
722
729
|
name: z.string().min(1).describe("The plan's new name."),
|
|
723
730
|
}, async (args) => jsonResult(await planRename(client, args)));
|
|
@@ -744,6 +751,47 @@ server.tool("plan_reorder_architecture_section", "Reassign your connected plan's
|
|
|
744
751
|
.min(1)
|
|
745
752
|
.describe("Every live section id of the connected plan, in the desired order — exactly once each."),
|
|
746
753
|
}, async (args) => jsonResult(await planReorderArchitectureSection(client, args)));
|
|
754
|
+
// ---------------- failure_category_list / _create / _update (DX-2792) ----------------
|
|
755
|
+
const matcherField = z
|
|
756
|
+
.object({
|
|
757
|
+
sourceKind: z
|
|
758
|
+
.enum(["tool-error", "hook-refusal", "api-error", "usage-limit", "session-result"])
|
|
759
|
+
.optional()
|
|
760
|
+
.describe("Absent matches any source kind."),
|
|
761
|
+
tool: z.string().min(1).optional().describe("Exact tool name (e.g. \"Bash\"); absent matches any tool, including null."),
|
|
762
|
+
regexPattern: z
|
|
763
|
+
.string()
|
|
764
|
+
.min(1)
|
|
765
|
+
.optional()
|
|
766
|
+
.describe("Regex source tested against the normalized excerpt (paths/UUIDs/timestamps/ports already stripped)."),
|
|
767
|
+
})
|
|
768
|
+
.describe("At least one of sourceKind/tool/regexPattern must be set — an empty matcher is refused.");
|
|
769
|
+
const expectedRateField = z
|
|
770
|
+
.object({
|
|
771
|
+
count: z.number().int().min(0).describe("N — how many failures are expected."),
|
|
772
|
+
overDispatches: z.number().int().positive().describe("X — over how many dispatches."),
|
|
773
|
+
})
|
|
774
|
+
.nullable()
|
|
775
|
+
.optional()
|
|
776
|
+
.describe("Both fields together, or omit/null entirely — never a half-specified rate.");
|
|
777
|
+
server.tool("failure_category_list", "List every failure category via GET /api/failure-categories (DX-2791/DX-2792). Board-less — a category applies across the whole install, not one board. Returns `{categories: [{id, name, description, matchers, ignore, ignoreReason, expectedRate, matchedCount, lastSeenMs, createdAtMs, createdBy, updatedAtMs, updatedBy}]}`, id ascending. A fresh install returns `{categories: []}`. Read this immediately before `failure_category_create`/`failure_category_update` so your overlap/expand decision is against the CURRENT set, not a stale snapshot from earlier in the dispatch.", {}, async () => jsonResult(await failureCategoryList(client)));
|
|
778
|
+
server.tool("failure_category_create", 'Create a new failure category via POST /api/failure-categories (DX-2791/DX-2792). Board-less. `matchers` (at least one) is an OR-across-matchers set — a category matches an occurrence when ANY ONE matcher\'s fields all hold. `ignore: true` REQUIRES a non-empty `ignoreReason` (400 otherwise) — use this for an expected, non-actionable failure rather than leaving it uncategorized. A matcher set overlapping an EXISTING category is refused 400 naming the conflict — call `failure_category_list` first and EXPAND that category (`failure_category_update`) instead of creating a near-duplicate. On success, the dashboard re-matches every existing uncategorized occurrence against the new category before responding.', {
|
|
779
|
+
name: z.string().min(1).describe("The category's name."),
|
|
780
|
+
description: z.string().optional().describe("Optional free-text description. Defaults to empty."),
|
|
781
|
+
matchers: z.array(matcherField).min(1).describe("At least one matcher; a category matches an occurrence when ANY ONE matches (OR across matchers)."),
|
|
782
|
+
ignore: z.boolean().optional().describe("Mark this category as expected/non-actionable noise. Requires ignoreReason. Defaults to false."),
|
|
783
|
+
ignoreReason: z.string().nullable().optional().describe("Required (non-empty) when ignore is true."),
|
|
784
|
+
expectedRate: expectedRateField,
|
|
785
|
+
}, async (args) => jsonResult(await failureCategoryCreate(client, args)));
|
|
786
|
+
server.tool("failure_category_update", "Patch an existing failure category via PATCH /api/failure-categories/:id (DX-2791/DX-2792). Board-less. This is the tool for BOTH actions: expanding an existing category's matchers (send the FULL replacement `matchers` array — it REPLACES, not appends, so include every matcher you want to keep alongside the new one) and marking a category ignored (`ignore: true` + a non-empty `ignoreReason`). At least one field besides `id` is required (400 otherwise). Same overlap refusal as create, excluding this category's own prior matchers. On success, re-matches every uncategorized occurrence against the updated matcher set before responding.", {
|
|
787
|
+
id: z.number().int().positive().describe("The category id to patch — from failure_category_list."),
|
|
788
|
+
name: z.string().min(1).optional(),
|
|
789
|
+
description: z.string().optional(),
|
|
790
|
+
matchers: z.array(matcherField).min(1).optional().describe("REPLACES the full matcher set when sent — never a partial append."),
|
|
791
|
+
ignore: z.boolean().optional(),
|
|
792
|
+
ignoreReason: z.string().nullable().optional(),
|
|
793
|
+
expectedRate: expectedRateField,
|
|
794
|
+
}, async (args) => jsonResult(await failureCategoryUpdate(client, args)));
|
|
747
795
|
// ---------------- main ----------------
|
|
748
796
|
async function main() {
|
|
749
797
|
boot();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
4
4
|
"description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|