@thehammer/danx-dashboard-mcp 0.1.41 → 0.1.45

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 CHANGED
@@ -95,6 +95,54 @@ export async function repoKnowledgeSet(client, args) {
95
95
  board,
96
96
  });
97
97
  }
98
+ // ---------------- master_plan_list / master_plan_get_page / master_plan_set_page ----------------
99
+ const MASTER_PLAN_BASE_PATH = "/api/master-plan";
100
+ /**
101
+ * List the board's named Master Plan pages via GET /api/master-plan (DX-2083).
102
+ * Mirrors `repoKnowledgeGet`'s shape (a bare board-scoped GET, no id) but
103
+ * targets the list-shaped sibling surface — many named pages per board,
104
+ * not one document.
105
+ */
106
+ export async function masterPlanList(client, args = {}) {
107
+ return client.request({
108
+ method: "GET",
109
+ path: "",
110
+ basePath: MASTER_PLAN_BASE_PATH,
111
+ board: args.board,
112
+ });
113
+ }
114
+ /**
115
+ * Fetch one Master Plan page by slug via GET /api/master-plan/page?slug=
116
+ * (DX-2083). A missing row (including a not-yet-created page) reads as the
117
+ * same empty-view convention `repoKnowledgeGet` uses — never a 404.
118
+ */
119
+ export async function masterPlanGetPage(client, args) {
120
+ return client.request({
121
+ method: "GET",
122
+ path: "/page",
123
+ basePath: MASTER_PLAN_BASE_PATH,
124
+ query: { slug: args.slug },
125
+ board: args.board,
126
+ });
127
+ }
128
+ /**
129
+ * Write one Master Plan page via PUT /api/master-plan/page?slug= (DX-2083).
130
+ * Mirrors `repoKnowledgeSet`'s shape (optimistic-concurrency `base_hash`,
131
+ * verbatim 409 passthrough on a stale write) but targets one named page
132
+ * instead of the board's single working-knowledge doc. `slug` rides the
133
+ * query string (matching the route), never the body.
134
+ */
135
+ export async function masterPlanSetPage(client, args) {
136
+ const { slug, board, ...body } = args;
137
+ return client.request({
138
+ method: "PUT",
139
+ path: "/page",
140
+ basePath: MASTER_PLAN_BASE_PATH,
141
+ query: { slug },
142
+ body,
143
+ board,
144
+ });
145
+ }
98
146
  export async function issueCreate(client, args, defaultBoard) {
99
147
  // Resolve the target board ONCE: per-call `args.board` override (a
100
148
  // qualified `<repo>:<slug>` id) wins, else the dispatch's env-derived
package/dist/index.js CHANGED
@@ -28,6 +28,9 @@
28
28
  * - issue_attach POST /api/issues/:id/attachments (reads a local file)
29
29
  * - repo_knowledge_get GET /api/repo-knowledge
30
30
  * - repo_knowledge_set PUT /api/repo-knowledge (DX-1128, Story 2)
31
+ * - master_plan_list GET /api/master-plan
32
+ * - master_plan_get_page GET /api/master-plan/page (DX-2083 / DX-2484)
33
+ * - master_plan_set_page PUT /api/master-plan/page (DX-2083 / DX-2484)
31
34
  *
32
35
  * BOARD-ONLY (DX-1171): board is the first-level concept; repo is
33
36
  * DERIVED from board server-side, never passed. The package composes the
@@ -57,7 +60,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
57
60
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
58
61
  import { z } from "zod";
59
62
  import { DashboardHttpClient } from "./http-client.js";
60
- import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueTransition, issueTriage, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
63
+ import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueTransition, issueTriage, masterPlanGetPage, masterPlanList, masterPlanSetPage, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
61
64
  import { PRIORITY_TIER_WORDS } from "./priority.js";
62
65
  function readEnvOrDie(name) {
63
66
  const v = process.env[name];
@@ -128,11 +131,9 @@ const EFFORT_VALUES = [
128
131
  "max",
129
132
  ];
130
133
  // DX-2479 — `Task` is a planning record: a leaf card the dispatcher can never
131
- // pick up. Convert it to a Story/Bug/Chore with the raw route
132
- // `PATCH /api/issues/:id/edit {"type":"Story"}` — the MCP `issue_edit` tool has
133
- // NO `type` parameter today, so an `issue_edit({type:"Story"})` call is silently
134
- // stripped by Zod and 400s as an empty patch. Adding `type` to that tool is
135
- // tracked as DX-2484. Accepted everywhere a type is, so a planning card can
134
+ // pick up. Convert it to a Story/Bug/Chore via `issue_edit({type:"Story"})`
135
+ // (DX-2484 wired `type` onto that tool's schema see the issue_edit
136
+ // registration below). Accepted everywhere a type is, so a planning card can
136
137
  // still be CREATED through this MCP.
137
138
  const ISSUE_TYPES = ["Epic", "Bug", "Feature", "Story", "Chore", "Task"];
138
139
  const NON_EPIC_TYPES = ["Bug", "Feature", "Story", "Chore", "Task"];
@@ -280,10 +281,14 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
280
281
  ...boardField,
281
282
  }, async (args) => jsonResult(await issueCreate(client, args, config.board)));
282
283
  // ---------------- issue_edit ----------------
283
- 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. 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).', {
284
+ 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 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).', {
284
285
  id: z.string().min(1),
285
286
  title: z.string().min(1).optional(),
286
287
  description: z.string().optional(),
288
+ type: z
289
+ .enum(ISSUE_TYPES)
290
+ .optional()
291
+ .describe("DX-2484 — change the card's type. Changing TO `Story`, `Bug`, or `Chore` makes the card eligible for autonomous pickup (dispatchable); changing TO `Task` or a container (`Epic`/`Feature`) removes that eligibility — a planning record or container is never dispatched, in ANY status. The route recomputes `dispatchable_derived` immediately."),
287
292
  ac: z
288
293
  .array(z.object({
289
294
  title: z.string(),
@@ -464,6 +469,33 @@ server.tool("repo_knowledge_set", 'Write the board\'s working-knowledge markdown
464
469
  .describe('The contentHash last read via repo_knowledge_get ("" for a true first write). Omitted also normalizes to "" server-side, so it only succeeds against an empty/absent doc — always get immediately before set.'),
465
470
  ...boardField,
466
471
  }, async (args) => jsonResult(await repoKnowledgeSet(client, args)));
472
+ // ---------------- master_plan_list ----------------
473
+ server.tool("master_plan_list", "List the board's named Master Plan pages via GET /api/master-plan (DX-2083 / DX-2484). Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to list another board's pages. Returns `{boardId, pages: [{slug, title, contentHash, sortOrder, updatedAt, updatedBy}]}` — metadata only, no page content (use `master_plan_get_page` for that). This is the list+page-shaped sibling of `repo_knowledge_get`/`repo_knowledge_set` (one board-level doc) — Master Plan pages are MANY named pages per board (the Goals / Architecture / Rules / Caveats tabs), keyed by `(board, slug)`. The reserved `index` slug always exists — every board carries exactly one.", {
474
+ ...boardField,
475
+ }, async (args) => jsonResult(await masterPlanList(client, args)));
476
+ // ---------------- master_plan_get_page ----------------
477
+ server.tool("master_plan_get_page", 'Fetch one Master Plan page by slug via GET /api/master-plan/page?slug=<slug> (DX-2083 / DX-2484). Board-scoped; defaults to the dispatch\'s board. Returns `{boardId, slug, title, content, contentHash, sortOrder, updatedAt, updatedBy}` — a missing/not-yet-created page reads as the empty view (`content: ""`, `contentHash: ""`), NOT a 404, matching `repo_knowledge_get`\'s convention. Before `master_plan_set_page`, ALWAYS `master_plan_get_page` immediately first and pass its `contentHash` back as `base_hash` — the server\'s optimistic-concurrency guard rejects a stale write.', {
478
+ slug: z
479
+ .string()
480
+ .min(1)
481
+ .describe("The page's slug. The reserved `index` slug always exists on every board."),
482
+ ...boardField,
483
+ }, async (args) => jsonResult(await masterPlanGetPage(client, args)));
484
+ // ---------------- master_plan_set_page ----------------
485
+ server.tool("master_plan_set_page", 'Write one Master Plan page via PUT /api/master-plan/page?slug=<slug> (DX-2083 / DX-2484). Board-scoped; defaults to the dispatch\'s board. Body: `{content, title?, sortOrder?, base_hash?}` — mirrors `repo_knowledge_set`\'s optimistic-concurrency shape but targets one named page instead of the board\'s single working-knowledge doc. `base_hash` MUST be the `contentHash` from the immediately-prior `master_plan_get_page` call ("" for a true first write, when the page doesn\'t exist yet) — the server compares it against the CURRENT hash and, on mismatch, fails loud with `{ok: false, body: {error: "stale_master_plan_page", currentHash}}` rather than silently overwriting a concurrent write — re-get, re-merge, and retry on that refusal, never retry blindly or overwrite. On success, persists to the DB, publishes `master-plan:updated` over SSE, and returns the new view. NO delete tool is exposed on this surface — the reserved `index` slug can never be deleted through the tool surface, matching the route\'s own refusal; deleting a non-index page is dashboard-UI-only for now.', {
486
+ slug: z
487
+ .string()
488
+ .min(1)
489
+ .describe("The page's slug. Writing the reserved `index` slug is allowed (it always exists); only deletion of `index` is refused."),
490
+ content: z.string(),
491
+ title: z.string().optional(),
492
+ sortOrder: z.number().optional(),
493
+ base_hash: z
494
+ .string()
495
+ .optional()
496
+ .describe('The contentHash last read via master_plan_get_page ("" for a true first write). Omitted also normalizes to "" server-side, so it only succeeds against an empty/absent page — always get immediately before set.'),
497
+ ...boardField,
498
+ }, async (args) => jsonResult(await masterPlanSetPage(client, args)));
467
499
  // ---------------- main ----------------
468
500
  async function main() {
469
501
  boot();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.41",
3
+ "version": "0.1.45",
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",