@thehammer/danx-dashboard-mcp 0.1.28 → 0.1.30
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 +2 -2
- package/dist/handlers.test.js +51 -0
- package/dist/index.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `m
|
|
|
26
26
|
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
|
|
27
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
|
-
| `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
|
|
29
|
+
| `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds). None of these are a cross-card ordering gate — a card held at Review via `keep` can still be readied independent of a sibling; use `issue_dependency` to sequence cards |
|
|
30
30
|
| `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
|
|
31
|
-
| `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"
|
|
31
|
+
| `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"`. The only mechanism the dispatch picker enforces to sequence one card after another — status alone is not a substitute |
|
|
32
32
|
| `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them |
|
|
33
33
|
| `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
|
|
34
34
|
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -260,7 +260,7 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issu
|
|
|
260
260
|
...boardField,
|
|
261
261
|
}, async (args) => jsonResult(await issueTransition(client, args)));
|
|
262
262
|
// ---------------- issue_triage ----------------
|
|
263
|
-
server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/triage. Verdicts: approve (stamps ready_at — moves to ToDo, clears triage TTL), cancel (stamps cancelled_at — terminal), keep (refreshes triage_expires_at by ttl_seconds — defaults 7 days, card stays at Review), defer (stamps archived_at, clears ready_at — parks to Backlog). ICE components optional but recorded when present (total = i
|
|
263
|
+
server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/triage. Verdicts: approve (stamps ready_at — moves to ToDo, clears triage TTL), cancel (stamps cancelled_at — terminal), keep (refreshes triage_expires_at by ttl_seconds — defaults 7 days, card stays at Review), defer (stamps archived_at, clears ready_at — parks to Backlog). ICE components optional but recorded when present (total = i*c*e). Reason is REQUIRED non-empty. REFUSES 409 on terminal cards. None of these verdicts create a cross-card ordering gate — a card left at Review (via keep) is invisible to the plan-dependency gate's live-partner comparison and can be readied independent of any sibling's status. To sequence one card after another, use issue_dependency (kind: depends_on) — that is the only mechanism the dispatch picker actually enforces regardless of status.", {
|
|
264
264
|
id: z.string().min(1),
|
|
265
265
|
verdict: z.enum(TRIAGE_VERDICTS),
|
|
266
266
|
reason: z.string().min(1),
|
|
@@ -308,7 +308,7 @@ server.tool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/check
|
|
|
308
308
|
...boardField,
|
|
309
309
|
}, async (args) => jsonResult(await issueChecklist(client, args)));
|
|
310
310
|
// ---------------- issue_dependency ----------------
|
|
311
|
-
server.tool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason="recorded_in_error" on removal (encodes "removal means NOT related, never satisfied") — this MCP boundary hardcodes it, so callers do not pass reason on remove.', {
|
|
311
|
+
server.tool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason="recorded_in_error" on removal (encodes "removal means NOT related, never satisfied") — this MCP boundary hardcodes it, so callers do not pass reason on remove. This is the ONLY mechanism the dispatch picker enforces to sequence one card after another — leaving a card at a Review/held status (e.g. an issue_triage "keep" verdict) is NOT a substitute and provides no cross-card ordering protection.', {
|
|
312
312
|
id: z.string().min(1),
|
|
313
313
|
action: z.enum(["add", "remove"]),
|
|
314
314
|
kind: z.enum(["depends_on", "conflict_on"]).optional(),
|
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.30",
|
|
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",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test:watch": "vitest"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@modelcontextprotocol/sdk": "
|
|
33
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
34
34
|
"zod": "^3.25.76"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|