@thehammer/danx-dashboard-mcp 0.1.310 → 0.1.312

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
@@ -50,7 +50,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
50
50
  | `danxbot_api_list` | No args. Lists the routes this caller's credential may reach as `{routes: ["METHOD /path"]}` (one bare string per route, exactly what `danxbot_api_spec` takes; a dispatch's own callbacks are in it) — call this first when you don't know the route for what you need to do. |
51
51
  | `danxbot_api_spec` | `{endpoints}` (comma-joined `METHOD /path` list, required, min 1). Returns the exact OpenAPI contract for those routes — call this when you know the route but not its exact request/response shape. |
52
52
  | `attach_file` | `{card_id, path, replaces?, primary?, filename?, content_type?}` — the server reads the LOCAL file at `path` (absolute, `/tmp/a.png` or `C:\x\a.png`) and uploads it, so its bytes never pass through the agent's context. `replaces: <attachment id>` swaps that attachment's file in place (`PUT /api/issues/:id/attachments/:aid`: same id, name and cover flag) instead of adding one; `primary: true` makes it the card's single cover, atomically with the write. Reply: `{issue: <compact row>, attachment_id, url, thumbnail_url, is_primary}`. Shown to a key holding `cards.edit` (DX-4523); a dispatch profile still receives it only when its catalog selection names it (`slack-agent` and `work`, DX-4386, through `extraTools`). A screenshot of browser-visible work: first `primary: true`, then `replaces` at each milestone. |
53
- | `design_publish` | `{target, dir, card_id?, board?, title?, summary?, description?}` — builds a design with the TARGET repo's own Vite (resolved from the target's root at run time: this package depends on neither `vite` nor `playwright`) and publishes it as a revision of a Design card (DX-4077). `target` names one of the repo's design targets, read from the dashboard (`GET /api/repos/<repo>`, edited in Settings > Board > General), never from a file; `dir` holds `Design.tsx` under the target's root. The repo is the board's `repo_name` (`GET /api/boards/<board>`). The build runs in `<root>/.danx-design/<build-id>/` (removed afterwards), in a short-lived `node` child process that exits when the build ends, so the server never holds the design's worktree files open and no esbuild outlives the publish (DX-4727), the page is written to `<tmpdir>/danx-design/<build-id>.html` before the upload, and a 1280x800 screenshot is taken when Playwright resolves from the target or this server's cwd (`screenshot: "none: <reason>"` otherwise, never a failed publish). Without `card_id` it creates the Design card first; a refused revision comes back verbatim with that `card_id`. Shown to a key holding `cards.edit`; opt-in, so a dispatch profile receives it only when its catalog selection names it (none does yet). |
53
+ | `design_publish` | `{target, note, dir, card_id, design_no | design_title}` — builds a design with the TARGET repo's own Vite (resolved from the target's root at run time: this package depends on neither `vite` nor `playwright`) and publishes it as a revision of one of an existing card's designs, or as a new design on it (DX-4077, DX-5164, DX-5193): a card holds several designs, so pass exactly one of `design_no` (add a revision to that design, `POST /api/issues/<card>/designs/<no>/revisions`) or `design_title` (start a new design, `POST /api/issues/<card>/designs`; the title is fixed for good), both-or-neither is refused before anything is built; the answer carries `{card_id, design_no, revision_no, dashboard_url, local_html_path, screenshot}`. `target` names one of the repo's design targets, read from the dashboard (`GET /api/repos/<repo>`, edited in Settings > Board > General), never from a file; `dir` holds `Design.tsx` under the target's root. The repo is the board's `repo_name` (`GET /api/boards/<board>`). The build runs in `<root>/.danx-design/<build-id>/` (removed afterwards), in a short-lived `node` child process that exits when the build ends, so the server never holds the design's worktree files open and no esbuild outlives the publish (DX-4727), the page is written to `<tmpdir>/danx-design/<build-id>.html` before the upload, and a 1280x800 screenshot is taken when Playwright resolves from the target or this server's cwd (`screenshot: "none: <reason>"` otherwise, never a failed publish). `card_id` is required and names an existing card (any but an Epic or Feature); the tool never creates a card, and a refused revision comes back verbatim with the `card_id`. Shown to a key holding `cards.edit`; opt-in, so a dispatch profile receives it only when its catalog selection names it (none does yet). |
54
54
  | `request_permission` | `{permissions, reason}` — asks a person to grant this key ONE MORE permission (see "Asking for one more permission" above). Answers at once with the approval URL and confirm code; the grant is picked up in the background. |
55
55
 
56
56
  There is no dedicated `resource_fields` tool (removed DX-3465 — it was a thin wrapper `danxbot_api` already reaches with no agent-specific behavior earning it its own tool). What an opt-in field TREE may name for one resource (`{resource, description, always, hashes, fields, relations}`) is `danxbot_api({method:"GET", path:"/api/resources/<resource>/fields"})` — install-global, no `board`; `resource: "issue"` is the root, and a relation's own `resource` in the response is what to call this again with, one level deeper.
@@ -1,16 +1,21 @@
1
1
  /**
2
2
  * DX-4077 - the `design_publish` flow: build a design against the repo's real components with the repo's own Vite, and publish it as
3
- * a revision of a Design card.
3
+ * a revision of one of an existing card's designs, or as the first revision of a new design on it (DX-5193).
4
4
  *
5
5
  * It lives in this package, not behind `danxbot_api`, because it is agent-specific: the build runs where the source is (the agent's
6
6
  * own checkout or worktree) and the dashboard holds no connected repo's source and no Vite. Every dashboard call goes through the same
7
7
  * client `danxbot_api` uses, so it carries the session's credential, trace and session headers.
8
8
  *
9
- * ORDER, so a failure leaves as little behind as it can: the arguments, then the board and card, then the repo and its targets (the
10
- * dashboard answers; a refusal comes back verbatim), then the checkout and the build, then the screenshot, and only THEN the card is
11
- * created and the revision posted. A failed build or screenshot never leaves a card. The two writes are separate calls, so a refused
12
- * revision leaves a Design card with no revision: that is not hidden or undone, the refusal comes back verbatim WITH the new
13
- * `card_id`, and a retry with it adds revision 1 to the same card.
9
+ * The design goes on an EXISTING card (`card_id`, any card but an Epic or Feature, DX-5164): this tool never creates a card. To show
10
+ * options for work that has no card yet, create the card first.
11
+ *
12
+ * DX-5193: a card holds SEVERAL designs. `design_no` adds a revision to that existing design (`POST .../designs/<no>/revisions`);
13
+ * `design_title` starts a new design (`POST .../designs`, the title fixed for good). Exactly one of the two, refused before any call.
14
+ *
15
+ * ORDER, so a failure leaves as little behind as it can: the arguments, then the card's board, then the repo and its targets (the
16
+ * dashboard answers; a refusal comes back verbatim), then the checkout and the build, then the screenshot, and only THEN the revision is
17
+ * posted. A failed build or screenshot never leaves anything on the card; a refused revision comes back verbatim with the `card_id`
18
+ * and the built page's path.
14
19
  */
15
20
  import path from "node:path";
16
21
  import { buildDesign, newBuildId, writeLocalHtml } from "./design-build.js";
@@ -26,42 +31,30 @@ function field(body, name, where) {
26
31
  fail(`${where} answered without "${name}"`);
27
32
  return value;
28
33
  }
29
- /** The board the card lives on: the card's own (read from it), else the named one, else the server's, else a refusal naming `board`. */
30
- async function resolveBoard(client, args, deps) {
31
- if (args.card_id === undefined) {
32
- const board = args.board ?? deps.serverBoard;
33
- if (board === undefined)
34
- fail("no board: this server has none of its own, so name one in `board` (`<repo>:<slug>`)");
35
- return { ok: true, board };
36
- }
34
+ /** The board the card lives on, read from the card: a revision goes on the card's own board. */
35
+ async function readCardBoard(client, cardId) {
37
36
  const card = await danxbotApi(client, {
38
37
  method: "GET",
39
- path: `/api/issues/${encodeURIComponent(args.card_id)}`,
38
+ path: `/api/issues/${encodeURIComponent(cardId)}`,
40
39
  query: { fields: { board_id: true } },
41
40
  });
42
41
  if (!card.ok)
43
42
  return card;
44
- const board = field(card.body, "board_id", `GET /api/issues/${args.card_id}`);
45
- if (args.board !== undefined && args.board !== board) {
46
- fail(`card ${args.card_id} lives on board ${board}, not ${args.board}: a revision goes on the card's own board, so omit \`board\` or name ${board}`);
47
- }
48
- return { ok: true, board };
43
+ return { ok: true, board: field(card.body, "board_id", `GET /api/issues/${cardId}`) };
49
44
  }
50
45
  export async function designPublish(client, args, deps) {
51
- // DX-4729: refused before any call, so a blank note never leaves a built page, a screenshot or a card behind.
46
+ // DX-4729: refused before any call, so a blank note never leaves a built page or a screenshot behind.
52
47
  if (args.note.trim() === "")
53
48
  fail("`note` is required: say what changed since the last revision, which decision note it answers and what is still open");
54
- // With `card_id` the card already exists: its title, summary and description are not this call's to set, and silently ignoring them would hide the mistake.
55
- if (args.card_id !== undefined) {
56
- const given = ["title", "summary", "description"].filter((k) => args[k] !== undefined);
57
- if (given.length > 0)
58
- fail(`${given.map((k) => `\`${k}\``).join(", ")} name the card this call CREATES, so they cannot be combined with \`card_id\`: drop them to add a revision to ${args.card_id}`);
59
- }
60
- // No card to add to: it is created here, and a card needs a title and a summary. Refused before any call.
61
- if (args.card_id === undefined && (args.title === undefined || args.summary === undefined)) {
62
- fail("`title` and `summary` are required without `card_id` (they name the Design card this creates)");
63
- }
64
- const boardOrRefusal = await resolveBoard(client, args, deps);
49
+ const { card_id: cardId } = args;
50
+ // DX-5193: exactly one of design_no / design_title, refused before any call so a bad pair never builds a page.
51
+ const hasNo = args.design_no !== undefined;
52
+ const hasTitle = args.design_title !== undefined;
53
+ if (hasNo === hasTitle)
54
+ fail("pass exactly one of `design_no` (add a revision to that design of the card) or `design_title` (start a new design on the card)");
55
+ if (args.design_title?.trim() === "")
56
+ fail("`design_title` must not be blank");
57
+ const boardOrRefusal = await readCardBoard(client, cardId);
65
58
  if (!boardOrRefusal.ok)
66
59
  return boardOrRefusal;
67
60
  const { board } = boardOrRefusal;
@@ -87,25 +80,16 @@ export async function designPublish(client, args, deps) {
87
80
  const { html, buildId } = await buildDesign({ ...checked, buildId: newBuildId() });
88
81
  const localHtmlPath = await writeLocalHtml(buildId, html);
89
82
  const shot = await captureScreenshot(html, { rootAbs: checked.rootAbs, cwd: deps.cwd });
90
- let cardId = args.card_id;
91
- if (cardId === undefined) {
92
- const created = await danxbotApi(client, {
93
- method: "POST",
94
- path: "/api/issues",
95
- // `description` is a creation-time seed: the route requires a string.
96
- body: { type: "Design", plan: "mine", board, title: args.title, summary: args.summary, description: args.description ?? "" },
97
- board,
98
- });
99
- if (!created.ok)
100
- return { ...created, local_html_path: localHtmlPath };
101
- cardId = field(created.body?.issue, "id", "POST /api/issues");
102
- }
83
+ const designRoute = hasTitle
84
+ ? `/api/issues/${encodeURIComponent(cardId)}/designs`
85
+ : `/api/issues/${encodeURIComponent(cardId)}/designs/${args.design_no}/revisions`;
103
86
  let published;
104
87
  try {
105
88
  published = await danxbotApi(client, {
106
89
  method: "POST",
107
- path: `/api/issues/${encodeURIComponent(cardId)}/design/revisions`,
90
+ path: designRoute,
108
91
  body: {
92
+ ...(hasTitle ? { title: args.design_title } : {}),
109
93
  note: args.note,
110
94
  target: target.name,
111
95
  entry: repoRelative(repoRoot, path.join(checked.dirAbs, DESIGN_FILE)),
@@ -118,17 +102,20 @@ export async function designPublish(client, args, deps) {
118
102
  });
119
103
  }
120
104
  catch (err) {
121
- // A network or server fault after the card exists: the card id and the page still matter to the retry.
122
- throw new Error(`${err.message} (card ${cardId} exists with no revision from this call; retry with card_id "${cardId}"; the page is at ${localHtmlPath})`);
105
+ // A network or server fault: the page is built and still matters to the retry.
106
+ throw new Error(`${err.message} (no revision was published to ${cardId}; the page is at ${localHtmlPath})`);
123
107
  }
124
108
  if (!published.ok)
125
109
  return { ...published, card_id: cardId, local_html_path: localHtmlPath };
126
- const revisionNo = published.body?.revision_no;
127
- if (typeof revisionNo !== "number")
128
- fail(`POST /api/issues/${cardId}/design/revisions answered without a revision_no`);
110
+ const answer = published.body;
111
+ if (typeof answer?.design_no !== "number")
112
+ fail(`POST ${designRoute} answered without a design_no`);
113
+ if (typeof answer.revision_no !== "number")
114
+ fail(`POST ${designRoute} answered without a revision_no`);
129
115
  const body = {
130
116
  card_id: cardId,
131
- revision_no: revisionNo,
117
+ design_no: answer.design_no,
118
+ revision_no: answer.revision_no,
132
119
  dashboard_url: `${deps.baseUrl.replace(/\/+$/, "")}/board/${cardId}`,
133
120
  local_html_path: localHtmlPath,
134
121
  screenshot: shot.screenshot,
package/dist/index.js CHANGED
@@ -422,7 +422,7 @@ plain background (what the thing is, why it exists, what is wrong or what the ch
422
422
  Define each term a newcomer would not know. Cite exact ids, SHAs and paths. Be brief; never transcribe your
423
423
  reasoning. A title or summary alone must let the reader decide.
424
424
 
425
- DESIGN. UI designs and style options go on a Design card (\`design_publish\`), never an Artifact or HTML page.
425
+ DESIGN. UI designs and style options go on the card they concern (\`design_publish\`), never an Artifact or HTML page.
426
426
  Decide choices yourself; show alternatives as design states, never ask.
427
427
  `;
428
428
  assertWithinTextLimit("the server instructions", SERVER_INSTRUCTIONS);
@@ -646,16 +646,17 @@ strictTool("attach_file", "Attach a LOCAL file to a card. This server reads and
646
646
  // `MCP_SERVER_OPT_IN_TOOLS`) and granted to no profile here: a default tool would add its schema to every default-all role's first
647
647
  // turn, and a role with no worktree or toolchain can only fail it. The package takes no `vite` or `playwright` dependency: both are
648
648
  // resolved from the target repo at run time (`design-build.ts`, `design-screenshot.ts`).
649
- strictTool("design_publish", "Build a design against this repo's REAL components with the repo's own Vite and publish it as a revision of a Design card. " +
649
+ strictTool("design_publish", "Build a design against this repo's REAL components with the repo's own Vite and publish it as a revision of one of an existing card's designs, or as a new design on it. " +
650
650
  "The design is a directory holding `Design.tsx` (a default-exported React component) plus any helpers; it may be a whole " +
651
651
  "front-end app, and how to split its states is your call. Read the board's design prompt first: `danxbot_api` GET " +
652
652
  "/api/boards/<board> with fields {controls: true}, the row keyed `design.prompt`, field `value`. " +
653
- "Without `card_id` this creates the Design card (needs `title` and `summary`, and a connected plan: it files the card with plan \"mine\", so connect one with plan_connect first); with `card_id`, the " +
654
- "next revision goes on that card and `title`, `summary` and `description` are refused. Returns `{card_id, revision_no, dashboard_url, local_html_path, " +
653
+ "`card_id` names the card the design goes on (any card but an Epic or Feature); this tool never creates a card, so to show options for work that has no card yet, create the card first. " +
654
+ "A card holds several designs: pass `design_title` to start a NEW design, or `design_no` (from the card's `designs` relation) to add a revision to an existing one; exactly one. " +
655
+ "Returns `{card_id, design_no, revision_no, dashboard_url, local_html_path, " +
655
656
  "screenshot}`: `local_html_path` is the built page, written before the upload so you can open it yourself; `screenshot` is " +
656
657
  "`captured` (1280x800) or `none: <reason>` (no Playwright in the repo: the publish still succeeds). Build errors and a " +
657
- "refused revision (400, 413) come back verbatim; a refusal after the card was created carries its `card_id`: fix the " +
658
- "design and call again with it. Limits: the frame has no network, so a component that fetches its own data fails inside " +
658
+ "refused revision (400, 409, 413) come back verbatim with the `card_id`: fix the " +
659
+ "design and call again. Limits: the frame has no network, so a component that fetches its own data fails inside " +
659
660
  "it (use fixture props); React only; targets are a dashboard setting, read from `design_targets` of `GET /api/repos/<repo>`, never a file.", {
660
661
  target: z.string().min(1).describe("A design target's name, as listed in `design_targets` of `GET /api/repos/<repo>` (`danxbot_api`, fields {design_targets: true})."),
661
662
  note: z
@@ -666,16 +667,10 @@ strictTool("design_publish", "Build a design against this repo's REAL components
666
667
  .string()
667
668
  .min(1)
668
669
  .describe("The design directory: absolute, or relative to this server's cwd. Inside the target's root, holding `Design.tsx`."),
669
- card_id: z.string().min(1).optional().describe("Add a revision to this Design card; omit to create one."),
670
- title: z.string().min(1).optional().describe("The new card's title. Required without `card_id`; refused with it."),
671
- summary: z.string().min(1).optional().describe("The new card's summary. Required without `card_id`; refused with it."),
672
- description: z.string().optional().describe("The new card's description; default empty. Refused with `card_id`."),
673
- board: z
674
- .string()
675
- .min(1)
676
- .optional()
677
- .describe("Board id `<repo>:<slug>` for the new card; omit for this server's board. With `card_id` the card's own board is used."),
678
- }, async (args) => guardedJson(() => designPublish(client, args, { serverBoard: config.board, baseUrl: config.baseUrl, cwd: process.cwd() })));
670
+ card_id: z.string().min(1).describe("The card this design goes on: any existing card but an Epic or Feature."),
671
+ design_no: z.number().int().positive().optional().describe("Add a revision to this existing design of the card (its `designs` relation). Not with `design_title`."),
672
+ design_title: z.string().min(1).optional().describe("Start a NEW design on the card with this title, fixed for good. Not with `design_no`."),
673
+ }, async (args) => guardedJson(() => designPublish(client, args, { baseUrl: config.baseUrl, cwd: process.cwd() })));
679
674
  // ---------------- request_permission (DX-4434, parent DX-4432) ----------------
680
675
  strictTool("request_permission", "Ask a person to grant this key ONE MORE permission. Call it when a call was refused `403 forbidden` naming a " +
681
676
  "`permission`, or when you know you need one more. It returns AT ONCE with `{state, approvalUrl, confirmCode, " +
package/dist/listen.js CHANGED
@@ -195,6 +195,10 @@ export function invalidEventReason(value) {
195
195
  case "design_decided": {
196
196
  if (typeof d.decision_id !== "number")
197
197
  return "detail.decision_id is not a number";
198
+ if (typeof d.design_no !== "number")
199
+ return "detail.design_no is not a number";
200
+ if (typeof d.design_title !== "string")
201
+ return "detail.design_title is not a string";
198
202
  if (typeof d.revision_no !== "number")
199
203
  return "detail.revision_no is not a number";
200
204
  if (!isDesignOutcome(d.outcome))
@@ -286,11 +290,12 @@ function describe(event) {
286
290
  // DX-4078: names the outcome, the revision and the note, and what the agent does next, so it never has to re-read the card to learn the verdict.
287
291
  case "design_decided": {
288
292
  const note = d.note;
289
- const revision = `revision ${d.revision_no} of the design`;
293
+ // DX-5193: a card holds several designs, so the line names which one the verdict closes.
294
+ const revision = `revision ${d.revision_no} of design ${d.design_no} "${d.design_title}"`;
290
295
  if (d.outcome === "approved")
291
- return `${who} APPROVED ${revision}${note === null ? "" : ` — note: ${quoted(note)}`}. The design is closed; build from it`;
296
+ return `${who} APPROVED ${revision}${note === null ? "" : ` — note: ${quoted(note)}`}. That design is closed; build from it`;
292
297
  if (d.outcome === "rejected")
293
- return `${who} REJECTED ${revision} — reason: ${quoted(note)}. The design is closed; do not continue it`;
298
+ return `${who} REJECTED ${revision} — reason: ${quoted(note)}. That design is closed; do not continue it`;
294
299
  return `${who} requested changes on ${revision} — ${quoted(note)}. Publish a new revision that addresses it`;
295
300
  }
296
301
  default:
@@ -42,7 +42,7 @@ export const TOOL_ACCESS = {
42
42
  danxbot_api_spec: BOOTSTRAP,
43
43
  // `POST /api/issues/:id/attachments`
44
44
  attach_file: needs("cards.edit"),
45
- // DX-4077 - builds in the agent's own checkout, then `POST /api/issues/:id/design/revisions` (`cards.edit`, DX-4076). The routes it
45
+ // DX-4077 - builds in the agent's own checkout, then `POST /api/issues/:id/designs` (a new design) or `POST /api/issues/:id/designs/:designNo/revisions` (the next revision), both `cards.edit` (DX-4076, DX-5193). The routes it
46
46
  // reads on the way (`GET /api/boards/:id`, `GET /api/repos/:id`, `GET /api/issues/:id`) and the card it creates are separate
47
47
  // checks on the dashboard whose refusals come back verbatim.
48
48
  design_publish: needs("cards.edit"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.310",
3
+ "version": "0.1.312",
4
4
  "description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, request_permission, attach_file, and danxbot_slack_reply) (DX-3465, PLN-11 R-25).",
5
5
  "license": "MIT",
6
6
  "type": "module",