@thehammer/danx-dashboard-mcp 0.1.310 → 0.1.311

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}` — 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 on an existing card (DX-4077, DX-5164). `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,18 @@
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 an existing card's design.
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
+ * 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
13
+ * dashboard answers; a refusal comes back verbatim), then the checkout and the build, then the screenshot, and only THEN the revision is
14
+ * posted. A failed build or screenshot never leaves anything on the card; a refused revision comes back verbatim with the `card_id`
15
+ * and the built page's path.
14
16
  */
15
17
  import path from "node:path";
16
18
  import { buildDesign, newBuildId, writeLocalHtml } from "./design-build.js";
@@ -26,42 +28,23 @@ function field(body, name, where) {
26
28
  fail(`${where} answered without "${name}"`);
27
29
  return value;
28
30
  }
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
- }
31
+ /** The board the card lives on, read from the card: a revision goes on the card's own board. */
32
+ async function readCardBoard(client, cardId) {
37
33
  const card = await danxbotApi(client, {
38
34
  method: "GET",
39
- path: `/api/issues/${encodeURIComponent(args.card_id)}`,
35
+ path: `/api/issues/${encodeURIComponent(cardId)}`,
40
36
  query: { fields: { board_id: true } },
41
37
  });
42
38
  if (!card.ok)
43
39
  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 };
40
+ return { ok: true, board: field(card.body, "board_id", `GET /api/issues/${cardId}`) };
49
41
  }
50
42
  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.
43
+ // DX-4729: refused before any call, so a blank note never leaves a built page or a screenshot behind.
52
44
  if (args.note.trim() === "")
53
45
  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);
46
+ const { card_id: cardId } = args;
47
+ const boardOrRefusal = await readCardBoard(client, cardId);
65
48
  if (!boardOrRefusal.ok)
66
49
  return boardOrRefusal;
67
50
  const { board } = boardOrRefusal;
@@ -87,19 +70,6 @@ export async function designPublish(client, args, deps) {
87
70
  const { html, buildId } = await buildDesign({ ...checked, buildId: newBuildId() });
88
71
  const localHtmlPath = await writeLocalHtml(buildId, html);
89
72
  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
- }
103
73
  let published;
104
74
  try {
105
75
  published = await danxbotApi(client, {
@@ -118,8 +88,8 @@ export async function designPublish(client, args, deps) {
118
88
  });
119
89
  }
120
90
  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})`);
91
+ // A network or server fault: the page is built and still matters to the retry.
92
+ throw new Error(`${err.message} (no revision was published to ${cardId}; the page is at ${localHtmlPath})`);
123
93
  }
124
94
  if (!published.ok)
125
95
  return { ...published, card_id: cardId, local_html_path: localHtmlPath };
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,16 @@ 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 a design on an existing card. " +
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
+ "Returns `{card_id, revision_no, dashboard_url, local_html_path, " +
655
655
  "screenshot}`: `local_html_path` is the built page, written before the upload so you can open it yourself; `screenshot` is " +
656
656
  "`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 " +
657
+ "refused revision (400, 409, 413) come back verbatim with the `card_id`: fix the " +
658
+ "design and call again. Limits: the frame has no network, so a component that fetches its own data fails inside " +
659
659
  "it (use fixture props); React only; targets are a dashboard setting, read from `design_targets` of `GET /api/repos/<repo>`, never a file.", {
660
660
  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
661
  note: z
@@ -666,16 +666,8 @@ strictTool("design_publish", "Build a design against this repo's REAL components
666
666
  .string()
667
667
  .min(1)
668
668
  .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() })));
669
+ card_id: z.string().min(1).describe("The card this design goes on: any existing card but an Epic or Feature."),
670
+ }, async (args) => guardedJson(() => designPublish(client, args, { baseUrl: config.baseUrl, cwd: process.cwd() })));
679
671
  // ---------------- request_permission (DX-4434, parent DX-4432) ----------------
680
672
  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
673
  "`permission`, or when you know you need one more. It returns AT ONCE with `{state, approvalUrl, confirmCode, " +
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.311",
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",