@thehammer/danx-dashboard-mcp 0.1.3 → 0.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @thehammer/danx-dashboard-mcp
2
2
 
3
- Stdio MCP server wrapping danxbot's dashboard `/api/v2/issues/*` normalized DB-backed HTTP routes. Replaces the legacy "agents Edit/Write `.yml` files directly" pattern from before DX-704 / DX-811.
3
+ Stdio MCP server wrapping danxbot's dashboard `/api/issues/*` normalized DB-backed HTTP routes. Replaces the legacy "agents Edit/Write `.yml` files directly" pattern from before DX-704 / DX-811.
4
4
 
5
5
  Each tool is a thin envelope over one HTTP route — Zod-validated at the MCP boundary, fetch under the hood, server response passed back to the agent verbatim. Refusal envelopes (`{error, ...extra}` with `failed_gate`, `non_terminal_phases`, `offending_keys`, etc.) come through as `{ok: false, status, body}` so the agent can pick the right next action without guessing. 5xx and network failures throw.
6
6
 
@@ -18,16 +18,16 @@ All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `.
18
18
 
19
19
  | Tool | HTTP | Notes |
20
20
  |---|---|---|
21
- | `issue_list` | `GET /api/v2/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
22
- | `issue_get` | `GET /api/v2/issues/:id` | Returns hydrated card + ancestor chain |
23
- | `issue_create` | `POST /api/v2/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
24
- | `issue_edit` | `PATCH /api/v2/issues/:id/edit` | Prose-only — semantic keys refused with 400 + pointer to dedicated handler |
25
- | `issue_transition` | `POST /api/v2/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
26
- | `issue_triage` | `POST /api/v2/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
27
- | `issue_comment` | `POST/PATCH/DELETE /api/v2/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
28
- | `issue_dependency` | `POST/DELETE /api/v2/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"` |
29
- | `issue_requires_human` | `POST/DELETE /api/v2/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them |
30
- | `issue_retro` | `PUT /api/v2/issues/:id/retro` | Requires terminal card; replace semantics |
21
+ | `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
22
+ | `issue_get` | `GET /api/issues/:id` | Returns hydrated card + ancestor chain |
23
+ | `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
24
+ | `issue_edit` | `PATCH /api/issues/:id/edit` | Prose-only — semantic keys refused with 400 + pointer to dedicated handler |
25
+ | `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
26
+ | `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
27
+ | `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
28
+ | `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"` |
29
+ | `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them |
30
+ | `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
31
31
 
32
32
  ## Build + test
33
33
 
@@ -66,6 +66,6 @@ export class DashboardHttpClient {
66
66
  params.set(k, String(v));
67
67
  }
68
68
  }
69
- return `${base}/api/v2/issues${cleanPath}?${params.toString()}`;
69
+ return `${base}/api/issues${cleanPath}?${params.toString()}`;
70
70
  }
71
71
  }
package/dist/index.js CHANGED
@@ -2,25 +2,25 @@
2
2
  /**
3
3
  * @thehammer/danx-dashboard-mcp
4
4
  *
5
- * Stdio MCP server wrapping danxbot's dashboard `/api/v2/issues/*`
5
+ * Stdio MCP server wrapping danxbot's dashboard `/api/issues/*`
6
6
  * normalized DB-backed HTTP routes (DX-704 Phase 2 / DX-811). Replaces
7
7
  * the legacy "agents Edit/Write YAML files directly" pattern: every
8
8
  * tool here POSTs/GETs against the dashboard, which atomically applies
9
- * the change via the v2 transactional layer and publishes SSE.
9
+ * the change via the transactional layer and publishes SSE.
10
10
  *
11
11
  * Tool surface (all exposed as `mcp__danx_dashboard__<name>` once wired
12
12
  * through the workspace `.mcp.json`):
13
13
  *
14
- * - issue_list GET /api/v2/issues
15
- * - issue_get GET /api/v2/issues/:id
16
- * - issue_create POST /api/v2/issues
17
- * - issue_edit PATCH /api/v2/issues/:id/edit
18
- * - issue_transition POST /api/v2/issues/:id/transition
19
- * - issue_triage POST /api/v2/issues/:id/triage
20
- * - issue_comment POST/PATCH/DELETE /api/v2/issues/:id/comments[/:cid]
21
- * - issue_dependency POST/DELETE /api/v2/issues/:id/dependencies[/:did]
22
- * - issue_requires_human POST/DELETE /api/v2/issues/:id/requires-human
23
- * - issue_retro PUT /api/v2/issues/:id/retro
14
+ * - issue_list GET /api/issues
15
+ * - issue_get GET /api/issues/:id
16
+ * - issue_create POST /api/issues
17
+ * - issue_edit PATCH /api/issues/:id/edit
18
+ * - issue_transition POST /api/issues/:id/transition
19
+ * - issue_triage POST /api/issues/:id/triage
20
+ * - issue_comment POST/PATCH/DELETE /api/issues/:id/comments[/:cid]
21
+ * - issue_dependency POST/DELETE /api/issues/:id/dependencies[/:did]
22
+ * - issue_requires_human POST/DELETE /api/issues/:id/requires-human
23
+ * - issue_retro PUT /api/issues/:id/retro
24
24
  *
25
25
  * Env at boot (validated fail-loud — missing → process.exit(1)):
26
26
  * DANXBOT_DASHBOARD_URL dashboard base (e.g. http://danxbot-dashboard:5555)
@@ -103,7 +103,7 @@ const repoField = {
103
103
  .describe("Target another registered repo; omit to use this dispatch's repo. Unknown repo → 404."),
104
104
  };
105
105
  // ---------------- issue_list ----------------
106
- server.tool("issue_list", "List issues for the dispatch's repo by default via GET /api/v2/issues. Pass `repo` to list another registered repo instead (unknown repo → 404). Filters: type, parent_id (string id, null for root-only), dispatchable_derived (boolean — server-computed pickup-ready gate), assigned_agent, include_closed (default false — excludes completed_at/cancelled_at). Response body shape: {issues: IssueV2[]}. Server-side numeric-suffix ordering so DX-10 follows DX-9. Use this instead of grepping .danxbot/issues/ — the DB-backed route is the source of truth post-DX-704.", {
106
+ server.tool("issue_list", "List issues for the dispatch's repo by default via GET /api/issues. Pass `repo` to list another registered repo instead (unknown repo → 404). Filters: type, parent_id (string id, null for root-only), dispatchable_derived (boolean — server-computed pickup-ready gate), assigned_agent, include_closed (default false — excludes completed_at/cancelled_at). Response body shape: {issues: Issue[]}. Server-side numeric-suffix ordering so DX-10 follows DX-9. Use this instead of grepping .danxbot/issues/ — the DB-backed route is the source of truth post-DX-704.", {
107
107
  status_derived: z.string().optional(),
108
108
  type: z.enum(ISSUE_TYPES).optional(),
109
109
  parent_id: z.string().nullable().optional(),
@@ -115,12 +115,12 @@ server.tool("issue_list", "List issues for the dispatch's repo by default via GE
115
115
  ...repoField,
116
116
  }, async (args) => jsonResult(await issueList(client, args)));
117
117
  // ---------------- issue_get ----------------
118
- server.tool("issue_get", "Fetch a single hydrated issue via GET /api/v2/issues/:id. Issue ids are globally unique across repos, so this resolves from any dispatch regardless of `repo`. Returns the full card (every joined child collection: ac, comments, dependencies, requires_human steps, retro action items + commits, triage history) plus the ancestor chain walked via parent_id. 404 envelope on unknown id.", {
118
+ server.tool("issue_get", "Fetch a single hydrated issue via GET /api/issues/:id. Issue ids are globally unique across repos, so this resolves from any dispatch regardless of `repo`. Returns the full card (every joined child collection: ac, comments, dependencies, requires_human steps, retro action items + commits, triage history) plus the ancestor chain walked via parent_id. 404 envelope on unknown id.", {
119
119
  id: z.string().min(1),
120
120
  ...repoField,
121
121
  }, async (args) => jsonResult(await issueGet(client, args)));
122
122
  // ---------------- issue_create ----------------
123
- server.tool("issue_create", "Create a fresh card via POST /api/v2/issues. Defaults to the dispatch's repo; pass `repo` to create the card in another registered repo (forwarded into body.repo + ?repo=; unknown repo → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic's id as parent_id.", {
123
+ server.tool("issue_create", "Create a fresh card via POST /api/issues. Defaults to the dispatch's repo; pass `repo` to create the card in another registered repo (forwarded into body.repo + ?repo=; unknown repo → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic's id as parent_id.", {
124
124
  type: z.enum(ISSUE_TYPES),
125
125
  title: z.string().min(1),
126
126
  description: z.string(),
@@ -139,7 +139,7 @@ server.tool("issue_create", "Create a fresh card via POST /api/v2/issues. Defaul
139
139
  ...repoField,
140
140
  }, async (args) => jsonResult(await issueCreate(client, args, config.repo)));
141
141
  // ---------------- issue_edit ----------------
142
- server.tool("issue_edit", "Patch prose fields only via PATCH /api/v2/issues/:id/edit. ALLOWED keys: title, description, ac, effort_level, parent_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. AC replacement is wholesale soft-delete + reinsert with fresh ordinals; check_item_id linkage survives via title match.", {
142
+ server.tool("issue_edit", "Patch prose fields only via PATCH /api/issues/:id/edit. ALLOWED keys: title, description, ac, effort_level, parent_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. AC replacement is wholesale soft-delete + reinsert with fresh ordinals; check_item_id linkage survives via title match.", {
143
143
  id: z.string().min(1),
144
144
  title: z.string().min(1).optional(),
145
145
  description: z.string().optional(),
@@ -154,7 +154,7 @@ server.tool("issue_edit", "Patch prose fields only via PATCH /api/v2/issues/:id/
154
154
  ...repoField,
155
155
  }, async (args) => jsonResult(await issueEdit(client, args)));
156
156
  // ---------------- issue_transition ----------------
157
- server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/v2/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), 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[]), 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).", {
157
+ 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), 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[]), 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).", {
158
158
  id: z.string().min(1),
159
159
  action: z.enum(TRANSITION_ACTIONS),
160
160
  reason: z.string().optional(),
@@ -163,7 +163,7 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/v2/i
163
163
  ...repoField,
164
164
  }, async (args) => jsonResult(await issueTransition(client, args)));
165
165
  // ---------------- issue_triage ----------------
166
- server.tool("issue_triage", "Record a triage verdict via POST /api/v2/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.", {
166
+ 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.", {
167
167
  id: z.string().min(1),
168
168
  verdict: z.enum(TRIAGE_VERDICTS),
169
169
  reason: z.string().min(1),
@@ -178,7 +178,7 @@ server.tool("issue_triage", "Record a triage verdict via POST /api/v2/issues/:id
178
178
  ...repoField,
179
179
  }, async (args) => jsonResult(await issueTriage(client, args)));
180
180
  // ---------------- issue_comment ----------------
181
- server.tool("issue_comment", "Comment CRUD via /api/v2/issues/:id/comments[/:cid]. action=add → POST {text} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation).", {
181
+ server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation).", {
182
182
  id: z.string().min(1),
183
183
  action: z.enum(["add", "edit", "delete"]),
184
184
  comment_id: z.number().int().positive().optional(),
@@ -186,7 +186,7 @@ server.tool("issue_comment", "Comment CRUD via /api/v2/issues/:id/comments[/:cid
186
186
  ...repoField,
187
187
  }, async (args) => jsonResult(await issueComment(client, args)));
188
188
  // ---------------- issue_dependency ----------------
189
- server.tool("issue_dependency", "Dependency CRUD via /api/v2/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.", {
189
+ 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.", {
190
190
  id: z.string().min(1),
191
191
  action: z.enum(["add", "remove"]),
192
192
  kind: z.enum(["depends_on", "conflict_on"]).optional(),
@@ -196,7 +196,7 @@ server.tool("issue_dependency", "Dependency CRUD via /api/v2/issues/:id/dependen
196
196
  ...repoField,
197
197
  }, async (args) => jsonResult(await issueDependency(client, args)));
198
198
  // ---------------- issue_requires_human ----------------
199
- server.tool("issue_requires_human", "Set or clear the requires_human dispatch gate via /api/v2/issues/:id/requires-human. set=true → POST {reason, steps[]} — sets requires_human_reason (the dispatch gate per DX-704 — poller refuses pickup while non-null), set_by from bearer, set_at NOW(), REPLACES the step rows (prior soft-deleted, fresh ordinals). set=false → DELETE — clears the columns and soft-deletes every live step. Terminal cards refuse 409 on set.", {
199
+ server.tool("issue_requires_human", "Set or clear the requires_human dispatch gate via /api/issues/:id/requires-human. set=true → POST {reason, steps[]} — sets requires_human_reason (the dispatch gate per DX-704 — poller refuses pickup while non-null), set_by from bearer, set_at NOW(), REPLACES the step rows (prior soft-deleted, fresh ordinals). set=false → DELETE — clears the columns and soft-deletes every live step. Terminal cards refuse 409 on set.", {
200
200
  id: z.string().min(1),
201
201
  set: z.boolean(),
202
202
  reason: z.string().optional(),
@@ -204,7 +204,7 @@ server.tool("issue_requires_human", "Set or clear the requires_human dispatch ga
204
204
  ...repoField,
205
205
  }, async (args) => jsonResult(await issueRequiresHuman(client, args)));
206
206
  // ---------------- issue_retro ----------------
207
- server.tool("issue_retro", "Replace the retro block via PUT /api/v2/issues/:id/retro. Body: {good, bad, action_item_ids[], commits[]}. REFUSES 409 unless the card is terminal (completed_at OR cancelled_at) — retro ships when work concludes. Replace semantics: good/bad upsert; action_item_ids[] + commits[] soft-delete prior live rows and insert with fresh ordinals. action_item_ids[] entries MUST match <PREFIX>-N. commits[] entries take {sha, subject?}.", {
207
+ server.tool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retro. Body: {good, bad, action_item_ids[], commits[]}. REFUSES 409 unless the card is terminal (completed_at OR cancelled_at) — retro ships when work concludes. Replace semantics: good/bad upsert; action_item_ids[] + commits[] soft-delete prior live rows and insert with fresh ordinals. action_item_ids[] entries MUST match <PREFIX>-N. commits[] entries take {sha, subject?}.", {
208
208
  id: z.string().min(1),
209
209
  good: z.string(),
210
210
  bad: z.string(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.3",
4
- "description": "Stdio MCP server wrapping danxbot's dashboard /api/v2/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
3
+ "version": "0.1.5",
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",
7
7
  "main": "dist/index.js",