@thehammer/danx-dashboard-mcp 0.1.48 → 0.1.51
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 +89 -0
- package/dist/index.js +51 -9
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -446,6 +446,17 @@ export async function issueRequiresHuman(client, args) {
|
|
|
446
446
|
*
|
|
447
447
|
* `effort_level` (DX-1760) is an independent sibling write: present (incl.
|
|
448
448
|
* `null`) sets `card_quality_gates.effort_level`; omitted leaves it untouched.
|
|
449
|
+
*
|
|
450
|
+
* **Effectiveness echo.** The server response body is `{issue, applied: true,
|
|
451
|
+
* effective, reason}`, not just `{issue}` — `client.request()` passes it
|
|
452
|
+
* through VERBATIM (see `http-client.ts`'s header doc: "the envelope
|
|
453
|
+
* passthrough is load-bearing"), so no transform is needed here for the
|
|
454
|
+
* calling agent to see `effective` (what `isGateEffectivelyRequired` resolves
|
|
455
|
+
* to right after this write, per the tri-state rule above) and `reason`
|
|
456
|
+
* (`null` when it matches the `required` value just sent, otherwise which
|
|
457
|
+
* board state overrode it). This closes the gap where a 200 alone could not
|
|
458
|
+
* tell a fully-honored write from one the board's `required`/`disabled`
|
|
459
|
+
* state made a complete no-op.
|
|
449
460
|
*/
|
|
450
461
|
export async function issueQualityGate(client, args) {
|
|
451
462
|
const body = { required: args.required };
|
|
@@ -651,3 +662,81 @@ export async function planSetArchitecture(client, args) {
|
|
|
651
662
|
body: { content: args.content, base_hash: args.base_hash },
|
|
652
663
|
});
|
|
653
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Create a new, empty plan via `POST /api/plans`. GLOBAL — a plan is not
|
|
667
|
+
* board-scoped, so this takes no board and creates no membership; the caller
|
|
668
|
+
* still owns zero cards, zero records and no architecture document until it
|
|
669
|
+
* adds them. This does NOT connect any session to the new plan — call
|
|
670
|
+
* `plan_connect` separately (mirroring how creating a card does not add it to
|
|
671
|
+
* a plan; these are two deliberately separate steps, same as everywhere else
|
|
672
|
+
* in this tool surface).
|
|
673
|
+
*/
|
|
674
|
+
export async function planCreate(client, args) {
|
|
675
|
+
return client.request({
|
|
676
|
+
method: "POST",
|
|
677
|
+
path: "",
|
|
678
|
+
basePath: PLANS_BASE_PATH,
|
|
679
|
+
body: { name: args.name },
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Read ONE goal/rule/caveat of the plan this session is connected to, via
|
|
684
|
+
* `GET /api/plans/mine/records/:rid`. TAKES NO PLAN ID: the plan is resolved
|
|
685
|
+
* from your connected session, same as `plan_add_record`. Useful both for an
|
|
686
|
+
* ordinary targeted read (skip pulling the whole plan via `plan_get` just to
|
|
687
|
+
* see one record) and for conflict recovery after a 409 `stale_plan_record`
|
|
688
|
+
* — though that refusal already carries `currentBody`, so a second read is
|
|
689
|
+
* rarely needed for that specific case. Not connected →
|
|
690
|
+
* `{error: "session_not_connected"}`. Unknown/foreign record id → 404.
|
|
691
|
+
*/
|
|
692
|
+
export async function planGetRecord(client, args) {
|
|
693
|
+
return client.request({
|
|
694
|
+
method: "GET",
|
|
695
|
+
path: `/mine/records/${args.record_id}`,
|
|
696
|
+
basePath: PLANS_BASE_PATH,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Edit a goal/rule/caveat of the plan this session is connected to, via
|
|
701
|
+
* `PATCH /api/plans/mine/records/:rid`. The reference (`G-1`, `R-4`,
|
|
702
|
+
* `CAV-12`) NEVER moves — only the text changes. `content_hash` MUST be the
|
|
703
|
+
* record's `contentHash` from the immediately-prior `plan_get`/
|
|
704
|
+
* `plan_get_record`; the server compares it against the row's current hash
|
|
705
|
+
* and, on a mismatch, refuses the write ENTIRELY and fails loud with
|
|
706
|
+
* `{ok: false, status: 409, body: {error: "stale_plan_record", currentHash,
|
|
707
|
+
* currentBody}}` rather than overwriting whoever wrote in between.
|
|
708
|
+
* `currentBody` rides the SAME refusal — unlike the architecture document's
|
|
709
|
+
* `stale_plan_architecture`, which carries only `currentHash` — so you can
|
|
710
|
+
* merge and retry in ONE round trip without a second `plan_get_record` call.
|
|
711
|
+
* TAKES NO PLAN ID: the plan is resolved from your connected session, same as
|
|
712
|
+
* `plan_add_record`. Not connected → `{error: "session_not_connected"}`.
|
|
713
|
+
*/
|
|
714
|
+
export async function planUpdateRecord(client, args) {
|
|
715
|
+
return client.request({
|
|
716
|
+
method: "PATCH",
|
|
717
|
+
path: `/mine/records/${args.record_id}`,
|
|
718
|
+
basePath: PLANS_BASE_PATH,
|
|
719
|
+
body: { body: args.body, content_hash: args.content_hash },
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Soft-delete a goal/rule/caveat of the plan this session is connected to,
|
|
724
|
+
* via `DELETE /api/plans/mine/records/:rid`. Its reference (`G-1`, `R-4`,
|
|
725
|
+
* `CAV-12`) is retired PERMANENTLY and never reused by a later record of the
|
|
726
|
+
* same kind. `content_hash` MUST be the record's `contentHash` from the
|
|
727
|
+
* immediately-prior `plan_get`/`plan_get_record`; a stale hash refuses the
|
|
728
|
+
* delete ENTIRELY (nothing is removed) with the same
|
|
729
|
+
* `{ok: false, status: 409, body: {error: "stale_plan_record", currentHash,
|
|
730
|
+
* currentBody}}` shape `plan_update_record` uses. TAKES NO PLAN ID: the plan
|
|
731
|
+
* is resolved from your connected session, same as `plan_add_record`. Not
|
|
732
|
+
* connected → `{error: "session_not_connected"}`. Unknown/foreign/already-
|
|
733
|
+
* deleted record id → 404.
|
|
734
|
+
*/
|
|
735
|
+
export async function planDeleteRecord(client, args) {
|
|
736
|
+
return client.request({
|
|
737
|
+
method: "DELETE",
|
|
738
|
+
path: `/mine/records/${args.record_id}`,
|
|
739
|
+
basePath: PLANS_BASE_PATH,
|
|
740
|
+
body: { content_hash: args.content_hash },
|
|
741
|
+
});
|
|
742
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -33,8 +33,12 @@
|
|
|
33
33
|
* - brief_set_page PUT /api/brief/page (DX-2083 / DX-2484)
|
|
34
34
|
* - plan_list GET /api/plans (DX-2683)
|
|
35
35
|
* - plan_get GET /api/plans/:id/full | /api/plans/mine
|
|
36
|
+
* - plan_create POST /api/plans
|
|
36
37
|
* - plan_connect POST /api/plan-sessions/me/plan
|
|
37
38
|
* - plan_add_record POST /api/plans/mine/records
|
|
39
|
+
* - plan_get_record GET /api/plans/mine/records/:rid (DX-2681 follow-up)
|
|
40
|
+
* - plan_update_record PATCH /api/plans/mine/records/:rid (DX-2681 follow-up)
|
|
41
|
+
* - plan_delete_record DELETE /api/plans/mine/records/:rid (DX-2681 follow-up)
|
|
38
42
|
* - plan_add_card POST /api/plans/mine/cards
|
|
39
43
|
* - plan_set_architecture PUT /api/plans/mine/architecture
|
|
40
44
|
*
|
|
@@ -75,7 +79,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
75
79
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
76
80
|
import { z } from "zod";
|
|
77
81
|
import { DashboardHttpClient } from "./http-client.js";
|
|
78
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddCard, planAddRecord, planConnect, planGet, planList, planSetArchitecture, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
82
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddCard, planAddRecord, planConnect, planCreate, planDeleteRecord, planGet, planGetRecord, planList, planSetArchitecture, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
79
83
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
80
84
|
function readEnvOrDie(name) {
|
|
81
85
|
const v = process.env[name];
|
|
@@ -215,12 +219,15 @@ const sortField = z
|
|
|
215
219
|
}))
|
|
216
220
|
.optional()
|
|
217
221
|
.describe("Multi-column sort — ordered list of {column, order}. Absent → the server's default order (priority desc, repo_name asc, with a numeric-id tiebreaker always appended).");
|
|
218
|
-
// DX-1290 — the uniform
|
|
222
|
+
// DX-1290 — the uniform checklist-item status, extended DX-2653 with
|
|
223
|
+
// `deferred` (a named real-world/post-deploy check still outstanding).
|
|
224
|
+
// Terminal = passing|cancelled|deferred.
|
|
219
225
|
const CHECKLIST_ITEM_STATUSES = [
|
|
220
226
|
"incomplete",
|
|
221
227
|
"failing",
|
|
222
228
|
"passing",
|
|
223
229
|
"cancelled",
|
|
230
|
+
"deferred",
|
|
224
231
|
];
|
|
225
232
|
const TRANSITION_ACTIONS = [
|
|
226
233
|
"ready",
|
|
@@ -329,7 +336,7 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
329
336
|
...boardField,
|
|
330
337
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
331
338
|
// ---------------- issue_edit ----------------
|
|
332
|
-
server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues/:id/edit. ALLOWED keys: title, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type. ANY OTHER KEY (lifecycle timestamps, triage state, dependencies, retro, requires_human, blocked/dispatch gates) returns 400 with offending_keys[] and a pointer to the dedicated semantic handler — use issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro instead. TYPE (DX-2484): change the card\'s `type` via the `type` key — the route recomputes `dispatchable_derived` immediately when it changes. Changing type TO `Story`, `Bug`, or `Chore` makes the card eligible for autonomous pickup (subject to every other dispatch gate); changing it TO `Task` or a container (`Epic`/`Feature`) REMOVES that eligibility — a planning record or container is never dispatched, in ANY status. This is the way to promote a planning item into real work. PRIORITY (DX-1532): set card priority via the `priority` key — a tier WORD ("lowest"/"low"/"medium"/"high"/"very_high"/"critical", resolved to the tier midpoint) OR a raw number in [0,6). This is the ONLY way to change priority: the numeric `issues.priority` column is what the Trello priority label AND the dashboard badge read — editing a "Priority: <x>" line in the DESCRIPTION changes nothing downstream (a silent false-positive). To honor a "set priority" request, write `priority` here, do NOT edit description prose. CHECKLISTS (DX-1290): a card carries 0..N named checklists, each item ONE
|
|
339
|
+
server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues/:id/edit. ALLOWED keys: title, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type. ANY OTHER KEY (lifecycle timestamps, triage state, dependencies, retro, requires_human, blocked/dispatch gates) returns 400 with offending_keys[] and a pointer to the dedicated semantic handler — use issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro instead. TYPE (DX-2484): change the card\'s `type` via the `type` key — the route recomputes `dispatchable_derived` immediately when it changes. Changing type TO `Story`, `Bug`, or `Chore` makes the card eligible for autonomous pickup (subject to every other dispatch gate); changing it TO `Task` or a container (`Epic`/`Feature`) REMOVES that eligibility — a planning record or container is never dispatched, in ANY status. This is the way to promote a planning item into real work. PRIORITY (DX-1532): set card priority via the `priority` key — a tier WORD ("lowest"/"low"/"medium"/"high"/"very_high"/"critical", resolved to the tier midpoint) OR a raw number in [0,6). This is the ONLY way to change priority: the numeric `issues.priority` column is what the Trello priority label AND the dashboard badge read — editing a "Priority: <x>" line in the DESCRIPTION changes nothing downstream (a silent false-positive). To honor a "set priority" request, write `priority` here, do NOT edit description prose. CHECKLISTS (DX-1290, extended DX-2653): a card carries 0..N named checklists, each item ONE status `incomplete|failing|passing|cancelled|deferred` (terminal = passing|cancelled|deferred). `deferred` is the honest disposition for a criterion whose work is done but names a real-world/post-deploy check still outstanding — REQUIRES a non-empty `detail`; a criterion marked with the `📡` post-deploy-evidence prefix can never be `passing` at all (refused 409 at completion, resolve to `deferred` instead). `ac` is the 2-state CONVENIENCE onto the default "Acceptance Criteria" checklist (checked:true ↔ passing, false ↔ incomplete BY DEFAULT — pass `status` alongside `checked` to reach `cancelled`/`failing`/`deferred` instead) — each incoming item is DIFFED against that checklist\'s current live items (matched by the optional `check_item_id`, else by exact title) so an unchanged item keeps its id; only changed items are updated in place, new titles are inserted, and items missing from the array are removed. `checklists` is the GENERIC wholesale write path: it REPLACES every named checklist on the card with full status control (each `{name, items:[{label, detail?, status}]}`) — use it to author named checklists like "Feature Tests". Send EITHER `ac` OR `checklists`, NOT both (400). list_id (DX-1192 / DX-1200) PINS the card to a specific board list. **Pass EITHER a board_lists id OR the list\'s display NAME (case-insensitive, e.g. a queue name like "⚙️ Fulfillment Queue") — the server resolves a name to its id.** Its type MUST match the card\'s CURRENT derived-status list-type (so to route a ToDo card into a `ready`-type queue, ready it first; mismatch / unknown name or id → 400); pass null to clear the pin (back to default-for-type).', {
|
|
333
340
|
id: z.string().min(1),
|
|
334
341
|
title: z.string().min(1).optional(),
|
|
335
342
|
description: z.string().optional(),
|
|
@@ -341,8 +348,21 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
|
|
|
341
348
|
.array(z.object({
|
|
342
349
|
title: z.string(),
|
|
343
350
|
checked: z.boolean().optional(),
|
|
351
|
+
status: z
|
|
352
|
+
.enum(CHECKLIST_ITEM_STATUSES)
|
|
353
|
+
.optional()
|
|
354
|
+
.describe('DX-2653 — optional full-status override. Omitted → derived from `checked` (`true`→passing, `false`→incomplete), same as before. Present → used verbatim, reaching `cancelled`/`failing`/`deferred` from this everyday convenience path instead of only via `checklists` or `issue_checklist`. `deferred` REQUIRES a non-empty `detail` naming the outstanding real-world/post-deploy check — a marker-prefixed ("📡") item can never be `passing` at all (refused 409 at completion).'),
|
|
355
|
+
detail: z
|
|
356
|
+
.string()
|
|
357
|
+
.optional()
|
|
358
|
+
.describe("DX-2653 — paired with `status`; required (non-empty) when `status` is `deferred`."),
|
|
359
|
+
check_item_id: z
|
|
360
|
+
.union([z.string(), z.number()])
|
|
361
|
+
.optional()
|
|
362
|
+
.describe("OPTIONAL — the item's real checklist-item id (the same value issue_get returns as ac[].check_item_id). Each incoming item is correlated to the checklist's current live rows by this id when supplied, else by an exact title match. An item that matches and is byte-identical (title + checked/status/detail) to its stored row is left untouched and KEEPS its id; only items that actually changed are updated in place, a title with no match is inserted fresh, and a live row missing from this array is removed. Supply check_item_id only to disambiguate two AC items that share an identical title — every other call can omit it and rely on title matching."),
|
|
344
363
|
}))
|
|
345
|
-
.optional()
|
|
364
|
+
.optional()
|
|
365
|
+
.describe("The 2-state convenience onto the default Acceptance Criteria checklist (checked:true ↔ passing, false ↔ incomplete BY DEFAULT — pass `status` to reach `cancelled`/`failing`/`deferred` instead). Diffed against the checklist's current live items — unchanged items keep their id, changed ones are updated in place, new titles are inserted, and existing items missing from this array are removed. See check_item_id below for disambiguating duplicate titles, and status/detail below for the full status vocabulary."),
|
|
346
366
|
checklists: z
|
|
347
367
|
.array(z.object({
|
|
348
368
|
name: z.string().min(1),
|
|
@@ -367,7 +387,7 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
|
|
|
367
387
|
...boardField,
|
|
368
388
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
369
389
|
// ---------------- issue_transition ----------------
|
|
370
|
-
server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issues/:id/transition. **THIS IS THE ONLY WAY TO MOVE A CARD'S LIFECYCLE STATE** — DX-835 separated card lifecycle (this tool) from dispatch finalization (`mcp__danxbot__danxbot_complete`). The worker no longer infers card moves from `danxbot_complete.status`; agents that want the card to move MUST call this tool BEFORE calling `danxbot_complete`. Actions: ready (Review→ToDo), pickup (ToDo→In Progress — server checks every dispatch gate: ready_at, blocked_at, requires_human_reason, depends_on partners terminal, conflict_on partners idle; refuses 409 with failed_gate naming the cause; pass manual:true for OPERATOR-SESSION self-pickup — stamps dispatch_kind 'manual', bypasses every card-flow gate except terminal/deleted/already-dispatched, and the worker NEVER auto-transitions the card: no orphan-heal rollback, no Epic auto-rollup — use this whenever the work happens in YOUR current session rather than a worker dispatch, DX-946), rollback_pickup, **complete** (stamps completed_at — moves card to Done; this is YOUR explicit decision, not a side effect of danxbot_complete; REFUSES 409 on Epic if any phase child non-terminal — see non_terminal_phases[]; ALSO refuses 409 with failed_gate 'quality_gate_post' + failed_post_gates[] while any required POST quality gate row != pass — DX-1177), cancel (stamps cancelled_at — terminal), **block** (requires non-empty reason — stamps blocked_at + blocked_reason + clears dispatch; USE THIS when the CARD itself cannot proceed without human intervention; distinct from env-fault dispatch failures which use `danxbot_complete({status:'failed'})`), unblock, archive (parks to Backlog, clears ready_at), reopen (terminal→active, clears completed_at/cancelled_at/archived_at). Terminal cards refuse every action except reopen. Ladder timestamps preserved — forward stamps never clear earlier ones (CLAUDE.md Core Principle 2). **DX-2282 — every path into In Progress now requires an identified claimer.** For `manual:true` pickup called from a DISPATCHED-AGENT session (this MCP tool, bearer = your dispatch token): you MUST pass `assigned_agent` set to YOUR resolved agent/profile name — omitting it, or passing the shared dispatch-token identity itself, is refused 409 `assigned_agent (required, distinguishing)`. A genuine human dashboard session may omit it (auto-resolved from the real logged-in user). Every pickup flavor (work/gate/manual) is refused 409 `assigned_agent (required)` if it would otherwise leave the card with no owner at all.", {
|
|
390
|
+
server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issues/:id/transition. **THIS IS THE ONLY WAY TO MOVE A CARD'S LIFECYCLE STATE** — DX-835 separated card lifecycle (this tool) from dispatch finalization (`mcp__danxbot__danxbot_complete`). The worker no longer infers card moves from `danxbot_complete.status`; agents that want the card to move MUST call this tool BEFORE calling `danxbot_complete`. Actions: ready (Review→ToDo), pickup (ToDo→In Progress — server checks every dispatch gate: ready_at, blocked_at, requires_human_reason, depends_on partners terminal, conflict_on partners idle; refuses 409 with failed_gate naming the cause; pass manual:true for OPERATOR-SESSION self-pickup — stamps dispatch_kind 'manual', bypasses every card-flow gate except terminal/deleted/already-dispatched, and the worker NEVER auto-transitions the card: no orphan-heal rollback, no Epic auto-rollup — use this whenever the work happens in YOUR current session rather than a worker dispatch, DX-946), rollback_pickup, **complete** (stamps completed_at — moves card to Done; this is YOUR explicit decision, not a side effect of danxbot_complete; REFUSES 409 on Epic if any phase child non-terminal — see non_terminal_phases[]; ALSO refuses 409 with failed_gate 'quality_gate_post' + failed_post_gates[] while any required POST quality gate row != pass — DX-1177), cancel (stamps cancelled_at — terminal), **block** (requires non-empty reason — stamps blocked_at + blocked_reason + clears dispatch; USE THIS when the CARD itself cannot proceed without human intervention; distinct from env-fault dispatch failures which use `danxbot_complete({status:'failed'})`), unblock, archive (parks to Backlog, clears ready_at), reopen (terminal→active, clears completed_at/cancelled_at/archived_at). Terminal cards refuse every action except reopen. Ladder timestamps preserved — forward stamps never clear earlier ones (CLAUDE.md Core Principle 2). **DX-2282 — every path into In Progress now requires an identified claimer.** For `manual:true` pickup called from a DISPATCHED-AGENT session (this MCP tool, bearer = your dispatch token): you MUST pass `assigned_agent` set to YOUR resolved agent/profile name — omitting it, or passing the shared dispatch-token identity itself, is refused 409 `assigned_agent (required, distinguishing)`. A genuine human dashboard session may omit it (auto-resolved from the real logged-in user). Every pickup flavor (work/gate/manual) is refused 409 `assigned_agent (required)` if it would otherwise leave the card with no owner at all. **A manual pickup can now also be refused 409 `failed_gate: \"dispatch_id\"` — \"pickup refused — card claimed by another actor between read and write\" — when it loses a genuine race against a concurrent manual pickup of the same idle card.** Before this, two overlapping manual pickups of the same card could both return 200: the second write silently overwrote the first winner's claim with no error and no distinguishing status code. The claiming write is now atomic (fenced on `dispatch_id IS NULL`), so the loser gets this 409 instead of a false success — on this response, do NOT retry blindly; re-check the card's current `assigned_agent`/`dispatch_id` first, since another actor already has it.", {
|
|
371
391
|
id: z.string().min(1),
|
|
372
392
|
action: z.enum(TRANSITION_ACTIONS),
|
|
373
393
|
reason: z.string().optional(),
|
|
@@ -403,7 +423,7 @@ const CHECKLIST_ITEM_INPUT = z.object({
|
|
|
403
423
|
detail: z.string().optional(),
|
|
404
424
|
status: z.enum(CHECKLIST_ITEM_STATUSES).optional(),
|
|
405
425
|
});
|
|
406
|
-
server.tool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/checklists[/:cid[/items[/:iid]]] (DX-1362). Mutates ONE checklist or item WITHOUT the wholesale `issue_edit({checklists})` replace — use this for the common case (flip an item's status, add/rename a checklist, add/edit/remove an item); the wholesale path silently DROPS any checklist you omit and churns every item id (orphaning its Trello mirror), so prefer this for single-item changes. Action-dispatched: add_list (POST :id/checklists {name, items?}) — create a named checklist, optionally with initial items; update_list (PATCH :id/checklists/:cid {name}) — rename; remove_list (DELETE :id/checklists/:cid) — soft-delete the checklist (audit trail preserved); add_item (POST :id/checklists/:cid/items {label, detail?, status?}) — append an item (status defaults `incomplete`); update_item (PATCH :id/checklists/:cid/items/:iid {label?, detail?, status?}) — change ONLY the fields you pass, in place (the item keeps its id + Trello linkage; at least one field required); remove_item (DELETE :id/checklists/:cid/items/:iid) — soft-delete one item.
|
|
426
|
+
server.tool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/checklists[/:cid[/items[/:iid]]] (DX-1362). Mutates ONE checklist or item WITHOUT the wholesale `issue_edit({checklists})` replace — use this for the common case (flip an item's status, add/rename a checklist, add/edit/remove an item); the wholesale path silently DROPS any checklist you omit and churns every item id (orphaning its Trello mirror), so prefer this for single-item changes. Action-dispatched: add_list (POST :id/checklists {name, items?}) — create a named checklist, optionally with initial items; update_list (PATCH :id/checklists/:cid {name}) — rename; remove_list (DELETE :id/checklists/:cid) — soft-delete the checklist (audit trail preserved); add_item (POST :id/checklists/:cid/items {label, detail?, status?}) — append an item (status defaults `incomplete`); update_item (PATCH :id/checklists/:cid/items/:iid {label?, detail?, status?}) — change ONLY the fields you pass, in place (the item keeps its id + Trello linkage; at least one field required); remove_item (DELETE :id/checklists/:cid/items/:iid) — soft-delete one item. Status: incomplete|failing|passing|cancelled|deferred (terminal = passing|cancelled|deferred; DX-2653 added `deferred` — the honest disposition for a criterion whose work is done but names a real-world/post-deploy check still outstanding, REQUIRES a non-empty `detail`). checklist_id is required for every action except add_list; item_id for update_item/remove_item. Each returns the {ok,status,body} envelope; unknown card/checklist/item → 404, invalid status → 400. ADDITIVE — the wholesale `issue_edit({checklists})` path stays for bulk authoring.", {
|
|
407
427
|
id: z.string().min(1),
|
|
408
428
|
action: z.enum([
|
|
409
429
|
"add_list",
|
|
@@ -441,7 +461,7 @@ server.tool("issue_requires_human", "Set or clear the requires_human dispatch ga
|
|
|
441
461
|
...boardField,
|
|
442
462
|
}, async (args) => jsonResult(await issueRequiresHuman(client, args)));
|
|
443
463
|
// ---------------- issue_quality_gate ----------------
|
|
444
|
-
server.tool("issue_quality_gate", "Toggle a single card's per-card quality-gate `required` flag via POST /api/issues/:id/quality-gates/:gate {required} — the SAME write the dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY post-create way to mark a gate required/not-required: `issue_create` carries `gate_decisions` at birth, and `issue_edit` REJECTS gate keys (400 offending_keys) — without this tool a card created without a gate can never have it turned on by an agent. `gate` is a registry name: `plan-dependency` | `plan-architecture` | `plan-tdd` | `code-test-quality` | `code-architecture` | `code-quality` (the PRE/plan- gates run before the work dispatch; the POST/code- gates block issue_transition complete). Unknown gate → 400 (never a silent no-op); a card with no seeded row for a registered gate → 500 (canonical corruption). NOTE board requirement is TRI-STATE per gate (`board_quality_gate_settings.default_state`, the Agents-tab surface), NOT a binary on/off: `required` = gate always runs (this flag irrelevant); `optional` = gate runs WHEN this per-card flag is true (per-card opt-in — `optional` is ENABLED, NOT off); `disabled` = never runs (this flag inert). So flipping `required:true` here LAUNCHES the gate when the board state is `required` OR `optional`; it is inert ONLY when the board state is `disabled`. Do not read `optional` as off. (Source of truth: `isGateEffectivelyRequired` in `src/issues/quality-gates/read.ts`.) DX-1760: optionally pass `effort_level` — a per-`(card, gate)` reviewer-rung override for a `plan-*` gate, written alongside `required`; omit to leave it untouched, pass `null` to clear a prior override.
|
|
464
|
+
server.tool("issue_quality_gate", "Toggle a single card's per-card quality-gate `required` flag via POST /api/issues/:id/quality-gates/:gate {required} — the SAME write the dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY post-create way to mark a gate required/not-required: `issue_create` carries `gate_decisions` at birth, and `issue_edit` REJECTS gate keys (400 offending_keys) — without this tool a card created without a gate can never have it turned on by an agent. `gate` is a registry name: `plan-dependency` | `plan-architecture` | `plan-tdd` | `code-test-quality` | `code-architecture` | `code-quality` (the PRE/plan- gates run before the work dispatch; the POST/code- gates block issue_transition complete). Unknown gate → 400 (never a silent no-op); a card with no seeded row for a registered gate → 500 (canonical corruption). NOTE board requirement is TRI-STATE per gate (`board_quality_gate_settings.default_state`, the Agents-tab surface), NOT a binary on/off: `required` = gate always runs (this flag irrelevant); `optional` = gate runs WHEN this per-card flag is true (per-card opt-in — `optional` is ENABLED, NOT off); `disabled` = never runs (this flag inert). So flipping `required:true` here LAUNCHES the gate when the board state is `required` OR `optional`; it is inert ONLY when the board state is `disabled`. Do not read `optional` as off. (Source of truth: `isGateEffectivelyRequired` in `src/issues/quality-gates/read.ts`.) DX-1760: optionally pass `effort_level` — a per-`(card, gate)` reviewer-rung override for a `plan-*` gate, written alongside `required`; omit to leave it untouched, pass `null` to clear a prior override. **The write itself ALWAYS succeeds and is echoed back** — the response is `{issue, applied: true, effective, reason}`, not just `{issue}`: `effective` is what `isGateEffectivelyRequired` resolves to for this (card, gate) right AFTER the write (accounts for the board tri-state above), and `reason` is `null` when `effective` matches the `required` value you just sent, or a plain-English explanation when the board's `required`/`disabled` state overrode it — READ `effective`/`reason`, not just the 200, to know whether this call actually changed whether the gate runs. Board-scoped; pass `board` (`<repo>:<slug>`) to target another board.", {
|
|
445
465
|
id: z.string().min(1),
|
|
446
466
|
gate: z.enum([
|
|
447
467
|
"plan-dependency",
|
|
@@ -551,11 +571,14 @@ server.tool("brief_set_page", 'Write one Brief page via PUT /api/brief/page?slug
|
|
|
551
571
|
}, async (args) => jsonResult(await briefSetPage(client, args)));
|
|
552
572
|
// ---------------- plans (DX-2683) ----------------
|
|
553
573
|
// THE BINDING ASYMMETRY IS IN THE SCHEMAS, NOT IN PROSE. `plan_get` takes an
|
|
554
|
-
// optional `plan_id`; `plan_add_record`, `
|
|
574
|
+
// optional `plan_id`; `plan_add_record`, `plan_get_record`,
|
|
575
|
+
// `plan_update_record`, `plan_delete_record`, `plan_add_card` and
|
|
555
576
|
// `plan_set_architecture` take NO plan id in any form, so a connected session
|
|
556
577
|
// cannot even express "write to that other plan". `plan_connect` takes one
|
|
557
578
|
// because binding a session to a plan is the one operation that is ABOUT a
|
|
558
|
-
// plan id — and it can only ever bind the caller's own session.
|
|
579
|
+
// plan id — and it can only ever bind the caller's own session. `plan_create`
|
|
580
|
+
// also takes no plan id, but for a different reason: it MAKES a plan rather
|
|
581
|
+
// than acting on one, so there is no existing plan for an id to name yet.
|
|
559
582
|
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}}`. `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). NOTE this is NOT the board Brief (`brief_list`), which is a different feature entirely.", {}, async () => jsonResult(await planList(client)));
|
|
560
583
|
server.tool("plan_get", "Read one plan WHOLE via GET /api/plans (DX-2683) — its member cards (with the boards they cover), its goals, rules and caveats, its architecture document, the sessions working on it, and your own session state. One call, not five. Pass `plan_id` to read ANY plan (browsing another plan is useful and changes nothing); OMIT it to read the plan this session is connected to. Omitting it while connected to no plan fails loud with `{error: \"session_not_connected\"}` — connect first. Returns `{plan, cards, boards, records: {goal: [], rule: [], caveat: []}, architecture: {content, contentHash, updatedAt, updatedBy}, sessions, session}`. ALWAYS `plan_get` immediately before `plan_set_architecture` and pass the returned `architecture.contentHash` back as `base_hash`.", {
|
|
561
584
|
plan_id: z
|
|
@@ -565,6 +588,9 @@ server.tool("plan_get", "Read one plan WHOLE via GET /api/plans (DX-2683) — it
|
|
|
565
588
|
.optional()
|
|
566
589
|
.describe("A plan id from `plan_list`. Omit to read the plan this session is connected to."),
|
|
567
590
|
}, async (args) => jsonResult(await planGet(client, args)));
|
|
591
|
+
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 document; 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}}}`. Use the returned `plan.id` with `plan_connect` to start working on it, or with `plan_get({plan_id})` to browse it.", {
|
|
592
|
+
name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
|
|
593
|
+
}, async (args) => jsonResult(await planCreate(client, args)));
|
|
568
594
|
server.tool("plan_connect", "Connect THIS session to a plan via POST /api/plan-sessions/me/plan (DX-2683) — the same binding the operator's Connect action writes, through the same server-side path. A session is connected to AT MOST ONE plan (enforced by the schema, not by convention); connecting while already on another plan MOVES you, and the response says which plan you left: `{ok, status, body: {session, movedFrom: {id, name} | null}}`. `movedFrom: null` means you were on no plan, or already on this one. It can only ever bind your OWN session — `me` is resolved from the session id this server forwards, never from anything you pass. After this, every plan WRITE tool acts on this plan, and no plan id is accepted anywhere.", {
|
|
569
595
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
570
596
|
}, async (args) => jsonResult(await planConnect(client, args)));
|
|
@@ -574,6 +600,22 @@ server.tool("plan_add_record", "Add a GOAL, RULE or CAVEAT to the plan this sess
|
|
|
574
600
|
.describe("Which standing record this is. Determines the reference prefix."),
|
|
575
601
|
body: z.string().min(1).describe("The record's text. Plain text, not markdown."),
|
|
576
602
|
}, async (args) => jsonResult(await planAddRecord(client, args)));
|
|
603
|
+
server.tool("plan_get_record", "Read ONE goal/rule/caveat of the plan this session is connected to, via GET /api/plans/mine/records/:rid (DX-2681 follow-up). Useful for an ordinary targeted read (skip pulling the whole plan via `plan_get` just to see one record) and for conflict recovery after a 409 `stale_plan_record` from `plan_update_record`/`plan_delete_record` — though that refusal already carries `currentBody`, so a second read is rarely needed for that specific case. TAKES NO PLAN ID: the plan is resolved from your connected session, same as `plan_add_record`. Not connected → `{error: \"session_not_connected\"}`. Unknown or another plan's record id → 404. Returns `{record: {id, planId, kind, refNum, ref, body, contentHash, createdAt, updatedAt}}`.", {
|
|
604
|
+
record_id: z.number().int().positive().describe("A record id, from `plan_add_record`, `plan_get` or `plan_get_record` itself."),
|
|
605
|
+
}, async (args) => jsonResult(await planGetRecord(client, args)));
|
|
606
|
+
server.tool("plan_update_record", 'Edit a goal/rule/caveat of the plan this session is connected to, via PATCH /api/plans/mine/records/:rid (DX-2681 follow-up). The reference (`G-1`, `R-4`, `CAV-12`) NEVER moves — only the text changes. `content_hash` MUST be the record\'s `contentHash` from the immediately-prior `plan_get`/`plan_get_record`/`plan_add_record`; the server compares it against the row\'s current hash and, on a mismatch, refuses the write ENTIRELY (nothing is changed) and fails loud with `{ok: false, body: {error: "stale_plan_record", currentHash, currentBody}}` rather than overwriting whoever wrote in between. `currentBody` rides that SAME refusal — unlike the architecture document\'s `stale_plan_architecture`, which carries only `currentHash` — so you can merge and retry in ONE round trip, no second read needed. On a refusal: merge your edit into `currentBody`, and retry with `content_hash: currentHash` — never retry blindly against the old hash. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: "session_not_connected"}`. Returns the updated record plus that kind\'s full list.', {
|
|
607
|
+
record_id: z.number().int().positive().describe("The record id to edit."),
|
|
608
|
+
content_hash: z
|
|
609
|
+
.string()
|
|
610
|
+
.describe("The record's `contentHash` from the immediately-prior read. Required — an absent hash is not read as a wildcard."),
|
|
611
|
+
body: z.string().min(1).describe("The record's new text, replacing what is stored. Plain text, not markdown."),
|
|
612
|
+
}, async (args) => jsonResult(await planUpdateRecord(client, args)));
|
|
613
|
+
server.tool("plan_delete_record", 'Soft-delete a goal/rule/caveat of the plan this session is connected to, via DELETE /api/plans/mine/records/:rid (DX-2681 follow-up). Its reference (`G-1`, `R-4`, `CAV-12`) is retired PERMANENTLY and never reused by a later record of the same kind. `content_hash` MUST be the record\'s `contentHash` from the immediately-prior `plan_get`/`plan_get_record`/`plan_add_record`; a stale hash refuses the delete ENTIRELY (nothing is removed) with the same `{ok: false, body: {error: "stale_plan_record", currentHash, currentBody}}` shape `plan_update_record` uses — merge and retry with `content_hash: currentHash` rather than retrying blindly. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: "session_not_connected"}`. Unknown, foreign, or already-deleted record id → 404. Returns that kind\'s remaining list.', {
|
|
614
|
+
record_id: z.number().int().positive().describe("The record id to delete."),
|
|
615
|
+
content_hash: z
|
|
616
|
+
.string()
|
|
617
|
+
.describe("The record's `contentHash` from the immediately-prior read. Required — an absent hash is not read as a wildcard."),
|
|
618
|
+
}, async (args) => jsonResult(await planDeleteRecord(client, args)));
|
|
577
619
|
server.tool("plan_add_card", "Add an existing card to the plan this session is connected to, via POST /api/plans/mine/cards (DX-2683). The card may live on ANY board — that is what a plan is for. Idempotent: re-adding a card already on the plan is a no-op, not an error, and a card may sit in several plans at once. This adds MEMBERSHIP only; it never edits the card. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns the plan's full member list.", {
|
|
578
620
|
card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
|
|
579
621
|
}, async (args) => jsonResult(await planAddCard(client, args)));
|
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.51",
|
|
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",
|