@thehammer/danx-dashboard-mcp 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/handlers.js +20 -5
- package/dist/index.js +8 -3
- package/dist/priority.js +70 -0
- package/package.json +1 -1
- package/dist/handlers.test.js +0 -51
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `m
|
|
|
24
24
|
| `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
|
|
25
25
|
| `issue_get` | `GET /api/issues/:id` | Returns hydrated card + ancestor chain |
|
|
26
26
|
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
|
|
27
|
-
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose
|
|
27
|
+
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose + structured keys (`title`, `description`, `ac`, `checklists`, `effort_level`, `parent_id`, `priority`, `list_id`); semantic keys refused with 400 + pointer to dedicated handler. `priority` (DX-1532) takes a tier word (`low`/`high`/…) or a number in `[0,6)` — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
|
|
28
28
|
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
|
|
29
29
|
| `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
|
|
30
30
|
| `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
|
package/dist/handlers.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { readFile } from "node:fs/promises";
|
|
25
25
|
import { basename, extname, isAbsolute } from "node:path";
|
|
26
|
+
import { resolvePriority } from "./priority.js";
|
|
26
27
|
export async function issueList(client, args) {
|
|
27
28
|
// status_derived is display-only on the server but the route accepts
|
|
28
29
|
// it as a projection filter — passthrough verbatim. parent_id=null is
|
|
@@ -97,8 +98,9 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
97
98
|
export async function issueEdit(client, args) {
|
|
98
99
|
// `board` is a transport concern (query), NOT a patch field — pull it
|
|
99
100
|
// out of `rest` so it never leaks into the `/edit` body (the server
|
|
100
|
-
// 400s on non-allowlisted keys).
|
|
101
|
-
|
|
101
|
+
// 400s on non-allowlisted keys). `priority` is pulled out separately so it
|
|
102
|
+
// can be resolved (tier word → numeric) before it reaches the body.
|
|
103
|
+
const { id, board, priority, ...rest } = args;
|
|
102
104
|
// Strip undefined so we don't send `{title: undefined}` — JSON.stringify
|
|
103
105
|
// drops them anyway but explicit is clearer.
|
|
104
106
|
const body = {};
|
|
@@ -106,6 +108,12 @@ export async function issueEdit(client, args) {
|
|
|
106
108
|
if (v !== undefined)
|
|
107
109
|
body[k] = v;
|
|
108
110
|
}
|
|
111
|
+
// DX-1532 — resolve a tier WORD to its numeric midpoint (inverse of
|
|
112
|
+
// priorityTier); a raw number passes through for the route to range-validate.
|
|
113
|
+
// Omitted (undefined) → never sent, so a prose-only edit is unchanged.
|
|
114
|
+
if (priority !== undefined) {
|
|
115
|
+
body.priority = resolvePriority(priority);
|
|
116
|
+
}
|
|
109
117
|
return client.request({
|
|
110
118
|
method: "PATCH",
|
|
111
119
|
path: `/${encodeURIComponent(id)}/edit`,
|
|
@@ -343,9 +351,16 @@ export async function issueRequiresHuman(client, args) {
|
|
|
343
351
|
*
|
|
344
352
|
* `gate` is a registry name (`plan-dependency` | `plan-architecture` |
|
|
345
353
|
* `plan-tdd` | `code-test-quality` | `code-architecture` | `code-quality`).
|
|
346
|
-
* An unknown gate is a 400 from the server (never a silent no-op).
|
|
347
|
-
*
|
|
348
|
-
*
|
|
354
|
+
* An unknown gate is a 400 from the server (never a silent no-op).
|
|
355
|
+
*
|
|
356
|
+
* Board requirement is TRI-STATE per gate, NOT a binary on/off
|
|
357
|
+
* (`board_quality_gate_settings.default_state`): `required` = always runs
|
|
358
|
+
* (this per-card flag is irrelevant); `optional` = ENABLED, runs WHEN this
|
|
359
|
+
* per-card flag is true (per-card opt-in — `optional` is NOT "off");
|
|
360
|
+
* `disabled` = never runs (this flag is inert). So flipping `required:true`
|
|
361
|
+
* here launches the gate when the board state is `required` OR `optional`;
|
|
362
|
+
* it is inert ONLY when the board state is `disabled`. Source of truth:
|
|
363
|
+
* `isGateEffectivelyRequired` in `src/issues/quality-gates/read.ts`.
|
|
349
364
|
*/
|
|
350
365
|
export async function issueQualityGate(client, args) {
|
|
351
366
|
return client.request({
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
53
53
|
import { z } from "zod";
|
|
54
54
|
import { DashboardHttpClient } from "./http-client.js";
|
|
55
55
|
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueRequiresHuman, issueRetro, issueTransition, issueTriage, } from "./handlers.js";
|
|
56
|
+
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
56
57
|
function readEnvOrDie(name) {
|
|
57
58
|
const v = process.env[name];
|
|
58
59
|
if (typeof v !== "string" || v === "") {
|
|
@@ -175,7 +176,7 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
175
176
|
required_gates: z
|
|
176
177
|
.array(z.string().min(1))
|
|
177
178
|
.optional()
|
|
178
|
-
.describe('Optional array of quality-gate names to mark REQUIRED on this card (e.g. ["architecture","tdd"]).
|
|
179
|
+
.describe('Optional array of quality-gate names to mark REQUIRED on this card (e.g. ["architecture","tdd"]). Board requirement is TRI-STATE per gate (`board_quality_gate_settings.default_state`), NOT binary: `required` runs always; `optional` runs WHEN flagged on the card (per-card opt-in — `optional` is ENABLED, not off); `disabled` never runs (flag inert). So flagging makes the gate run unless the board state is `disabled`. Use this when you (the creating agent) judge the card needs that pre-dispatch review gate; omit for cards that don\'t. Known gates: dependency, architecture, tdd, code. The operator can also toggle these per-card later in the issue drawer.'),
|
|
179
180
|
phase_children: z
|
|
180
181
|
.array(z.object({
|
|
181
182
|
type: z.enum(NON_EPIC_TYPES),
|
|
@@ -188,7 +189,7 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
188
189
|
...boardField,
|
|
189
190
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
190
191
|
// ---------------- issue_edit ----------------
|
|
191
|
-
server.tool("issue_edit", 'Patch prose fields
|
|
192
|
+
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. 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. 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 4-state status `incomplete|failing|passing|cancelled` (terminal = passing|cancelled). `ac` is the 2-state CONVENIENCE onto the default "Acceptance Criteria" checklist (checked:true ↔ passing, false ↔ incomplete) — wholesale soft-delete + reinsert of that checklist\'s items. `checklists` is the GENERIC wholesale write path: it REPLACES every named checklist on the card with full 4-state 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).', {
|
|
192
193
|
id: z.string().min(1),
|
|
193
194
|
title: z.string().min(1).optional(),
|
|
194
195
|
description: z.string().optional(),
|
|
@@ -210,6 +211,10 @@ server.tool("issue_edit", 'Patch prose fields only via PATCH /api/issues/:id/edi
|
|
|
210
211
|
.optional(),
|
|
211
212
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
212
213
|
parent_id: z.string().nullable().optional(),
|
|
214
|
+
priority: z
|
|
215
|
+
.union([z.enum(PRIORITY_TIER_WORDS), z.number()])
|
|
216
|
+
.optional()
|
|
217
|
+
.describe('Card priority (DX-1532). A tier WORD ("lowest"/"low"/"medium"/"high"/"very_high"/"critical") resolved to the tier midpoint, OR a raw number in [0,6). Writes the numeric `issues.priority` column the Trello label + dashboard badge read — set priority HERE, never via description prose (which no system reads for priority).'),
|
|
213
218
|
list_id: z.string().min(1).nullable().optional(),
|
|
214
219
|
...boardField,
|
|
215
220
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
@@ -290,7 +295,7 @@ server.tool("issue_requires_human", "Set or clear the requires_human dispatch ga
|
|
|
290
295
|
...boardField,
|
|
291
296
|
}, async (args) => jsonResult(await issueRequiresHuman(client, args)));
|
|
292
297
|
// ---------------- issue_quality_gate ----------------
|
|
293
|
-
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 `required_gates[]` 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
|
|
298
|
+
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 `required_gates[]` 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`.) Returns the hydrated issue. Board-scoped; pass `board` (`<repo>:<slug>`) to target another board.", {
|
|
294
299
|
id: z.string().min(1),
|
|
295
300
|
gate: z.enum([
|
|
296
301
|
"plan-dependency",
|
package/dist/priority.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-word → numeric priority resolution for the MCP `issue_edit` tool
|
|
3
|
+
* (DX-1532).
|
|
4
|
+
*
|
|
5
|
+
* Priority is a STRUCTURED numeric column `issues.priority` (float in `[0,6)`).
|
|
6
|
+
* Both the Trello "priority" label and the dashboard priority badge read it via
|
|
7
|
+
* `priorityTier(issue.priority)` — description PROSE ("Priority: Low") is read
|
|
8
|
+
* by neither. So an agent that "sets priority" by editing description text
|
|
9
|
+
* produces a silent false-positive: nothing downstream changes. The fix is to
|
|
10
|
+
* let the agent write the numeric column through `issue_edit`.
|
|
11
|
+
*
|
|
12
|
+
* Agents (and operators) speak in tier WORDS ("low" / "high"), so the tool
|
|
13
|
+
* accepts a tier word and resolves it to the tier's MIDPOINT here — the inverse
|
|
14
|
+
* of `src/issue-tracker/priority-tier.ts#priorityTier`. A raw numeric is passed
|
|
15
|
+
* through unchanged for the server route to range-validate (it 400s on
|
|
16
|
+
* out-of-`[0,6)`), so the MCP boundary never has to duplicate that bound.
|
|
17
|
+
*
|
|
18
|
+
* This package is a standalone published artifact and cannot import the
|
|
19
|
+
* backend's `priority-tier` module, so the midpoint table is duplicated here.
|
|
20
|
+
* Each value IS the `[min,max)` bucket midpoint by construction and equals the
|
|
21
|
+
* backend's `PRIORITY_TIERS.defaultValue`; `src/issue-tracker/priority-tier.test.ts`
|
|
22
|
+
* asserts `priorityTier(defaultValue) === key` on the canonical side, pinning
|
|
23
|
+
* the round-trip these midpoints rely on.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The six priority tier words, low → high. A const tuple so it doubles as the
|
|
27
|
+
* `z.enum` source in `index.ts`.
|
|
28
|
+
*/
|
|
29
|
+
export const PRIORITY_TIER_WORDS = [
|
|
30
|
+
"lowest",
|
|
31
|
+
"low",
|
|
32
|
+
"medium",
|
|
33
|
+
"high",
|
|
34
|
+
"very_high",
|
|
35
|
+
"critical",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Tier word → bucket midpoint (inverse of `priorityTier`). Lands the numeric
|
|
39
|
+
* column squarely inside the requested tier so a later `priorityTier` read
|
|
40
|
+
* returns the same word.
|
|
41
|
+
*/
|
|
42
|
+
export const PRIORITY_TIER_MIDPOINTS = {
|
|
43
|
+
lowest: 0.5,
|
|
44
|
+
low: 1.5,
|
|
45
|
+
medium: 2.5,
|
|
46
|
+
high: 3.5,
|
|
47
|
+
very_high: 4.5,
|
|
48
|
+
critical: 5.5,
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a priority input — a tier WORD or a raw numeric — to the numeric
|
|
52
|
+
* value the `PATCH /api/issues/:id/edit` route stores in `issues.priority`.
|
|
53
|
+
*
|
|
54
|
+
* - tier word → its bucket midpoint (`PRIORITY_TIER_MIDPOINTS`).
|
|
55
|
+
* - number → passed through unchanged; the server route range-validates it to
|
|
56
|
+
* `[0,6)` and 400s otherwise (the MCP boundary deliberately does not
|
|
57
|
+
* re-validate the bound — single source of truth on the route).
|
|
58
|
+
* - unknown string → throws here at the MCP boundary (no round-trip). The Zod
|
|
59
|
+
* enum on the tool schema already rejects this for tool callers; the explicit
|
|
60
|
+
* throw keeps the helper safe for any direct caller.
|
|
61
|
+
*/
|
|
62
|
+
export function resolvePriority(input) {
|
|
63
|
+
if (typeof input === "number")
|
|
64
|
+
return input;
|
|
65
|
+
const midpoint = PRIORITY_TIER_MIDPOINTS[input];
|
|
66
|
+
if (midpoint === undefined) {
|
|
67
|
+
throw new Error(`issue_edit: unknown priority tier word "${input}" (expected one of ${PRIORITY_TIER_WORDS.join(", ")} or a number in [0,6))`);
|
|
68
|
+
}
|
|
69
|
+
return midpoint;
|
|
70
|
+
}
|
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.23",
|
|
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",
|
package/dist/handlers.test.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { DashboardHttpClient } from "./http-client.js";
|
|
3
|
-
import { issueList, issueTransition } from "./handlers.js";
|
|
4
|
-
/**
|
|
5
|
-
* `issue_list` query-param forwarding. A fake `fetch` captures the built
|
|
6
|
-
* URL so each filter is asserted at the wire, through the real client's
|
|
7
|
-
* URL builder (`?repo=` always stamped, extra query merged after).
|
|
8
|
-
*/
|
|
9
|
-
function clientCapturing() {
|
|
10
|
-
const urls = [];
|
|
11
|
-
const fetchImpl = (async (url) => {
|
|
12
|
-
urls.push(url);
|
|
13
|
-
return new Response(JSON.stringify({ issues: [] }), { status: 200 });
|
|
14
|
-
});
|
|
15
|
-
const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
|
|
16
|
-
return { client, urls };
|
|
17
|
-
}
|
|
18
|
-
describe("issueList — query forwarding", () => {
|
|
19
|
-
it("forwards q as the server-side search needle", async () => {
|
|
20
|
-
const { client, urls } = clientCapturing();
|
|
21
|
-
await issueList(client, { q: "retire" });
|
|
22
|
-
expect(urls[0]).toContain("q=retire");
|
|
23
|
-
});
|
|
24
|
-
it("omits q when not provided", async () => {
|
|
25
|
-
const { client, urls } = clientCapturing();
|
|
26
|
-
await issueList(client, { include_closed: true });
|
|
27
|
-
expect(urls[0]).not.toContain("q=");
|
|
28
|
-
expect(urls[0]).toContain("include_closed=true");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
describe("issueTransition — body forwarding", () => {
|
|
32
|
-
function clientCapturingBody() {
|
|
33
|
-
const bodies = [];
|
|
34
|
-
const fetchImpl = (async (_url, init) => {
|
|
35
|
-
bodies.push(JSON.parse(String(init?.body ?? "{}")));
|
|
36
|
-
return new Response(JSON.stringify({ issue: {} }), { status: 200 });
|
|
37
|
-
});
|
|
38
|
-
const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
|
|
39
|
-
return { client, bodies };
|
|
40
|
-
}
|
|
41
|
-
it("forwards manual: true on pickup (DX-946 operator self-pickup)", async () => {
|
|
42
|
-
const { client, bodies } = clientCapturingBody();
|
|
43
|
-
await issueTransition(client, { id: "DX-1", action: "pickup", manual: true });
|
|
44
|
-
expect(bodies[0]).toEqual({ action: "pickup", manual: true });
|
|
45
|
-
});
|
|
46
|
-
it("omits manual when not provided", async () => {
|
|
47
|
-
const { client, bodies } = clientCapturingBody();
|
|
48
|
-
await issueTransition(client, { id: "DX-1", action: "pickup" });
|
|
49
|
-
expect(bodies[0]).toEqual({ action: "pickup" });
|
|
50
|
-
});
|
|
51
|
-
});
|