@thehammer/danx-dashboard-mcp 0.1.314 → 0.1.315

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
@@ -51,6 +51,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
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
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
+ | `plugin_publish` | `{dir, plugin, note, name?}` — builds a card-type plugin project with its own `npm run build` in a short-lived child process, archives its source (`source.tar.gz`: git-tracked plus untracked-unignored files, else what the top-level `.gitignore` leaves, minus `node_modules`, `.git`, `dist`; never `.env*`; a lockfile is required) and uploads both as a new revision of the plugin `plugin` (created on the first publish), `POST /api/plugins/<id>/revisions` (DX-5237). The build, archive and upload are `@danxbot/plugin-sdk/publish`, the code `danx-plugin publish` runs too. The answer carries `{plugin, plugin_id, revision_id, revision_no, approval_url, screenshot}`; `approval_url` is Settings > Plugins, where a team admin approves the revision. A 1280x800 screenshot of the project's `preview.html` is taken when it has one and Playwright resolves from the project or this server's cwd (`screenshot: "none: <reason>"` otherwise, never a failed publish). A refused upload comes back verbatim with the `step` refused. Shown to a key holding `plugins.publish`; opt-in like `design_publish`. |
54
55
  | `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
56
 
56
57
  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.
package/dist/index.js CHANGED
@@ -105,6 +105,7 @@ import { z } from "zod";
105
105
  import { DashboardHttpClient } from "./http-client.js";
106
106
  import { planConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DANXBOT_API_METHODS, } from "./handlers.js";
107
107
  import { designPublish } from "./design-publish.js";
108
+ import { pluginPublish } from "./plugin-publish.js";
108
109
  import { boundedResultText } from "./result-limit.js";
109
110
  import { bodyValueSchema, queryValueSchema } from "./types.js";
110
111
  import { termsAcceptanceCallHalt, termsHaltFor } from "./terms-halt.js";
@@ -671,6 +672,20 @@ strictTool("design_publish", "Build a design against this repo's REAL components
671
672
  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
673
  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
674
  }, async (args) => guardedJson(() => designPublish(client, args, { baseUrl: config.baseUrl, cwd: process.cwd() })));
675
+ // ---------------- plugin_publish (DX-5237, parent DX-4679) ----------------
676
+ //
677
+ // Agent-specific like design_publish: it builds in the agent's OWN checkout (`npm run build` in a short-lived child process) and uploads the
678
+ // result, which the dashboard cannot do. The build, archive and upload are `@danxbot/plugin-sdk/publish`, shared with the `danx-plugin`
679
+ // CLI; the screenshot is this tool's alone. Opt-in for the same reason as design_publish (`tool-defs.ts`'s `MCP_SERVER_OPT_IN_TOOLS`).
680
+ strictTool("plugin_publish", "Build a card-type plugin project (Vite, `danxPlugin()` from `@danxbot/plugin-sdk/vite`, a lockfile) with its own `npm run build` and publish it as a new revision of the team's plugin " +
681
+ "`plugin` (a new key creates it): uploads `dist/`, the source `.tar.gz` and a screenshot of the project's `preview.html` when it has one. " +
682
+ "Returns `{ok: true, status, body: {plugin, plugin_id, revision_id, revision_no, approval_url, screenshot}}`: `approval_url` is Settings > Plugins, where a team admin approves the revision; you cannot. " +
683
+ "`screenshot` is `captured` or `none: <reason>` (the publish still succeeds). A build error throws with the build's output; a refused upload comes back verbatim as `{ok: false, status, body, plugin, step}`.", {
684
+ dir: z.string().min(1).describe("The project directory: absolute, or relative to this server's cwd."),
685
+ plugin: z.string().min(1).describe("The plugin's key: lowercase letters, digits, - and _, at most 64."),
686
+ note: z.string().min(1).describe("What this revision changes, shown to the approver."),
687
+ name: z.string().min(1).optional().describe("Display name for a plugin this call creates; default: the key."),
688
+ }, async (args) => guardedJson(() => pluginPublish(client, args, { baseUrl: config.baseUrl, cwd: process.cwd() })));
674
689
  // ---------------- request_permission (DX-4434, parent DX-4432) ----------------
675
690
  strictTool("request_permission", "Ask a person to grant this key ONE MORE permission. Call it when a call was refused `403 forbidden` naming a " +
676
691
  "`permission`, or when you know you need one more. It returns AT ONCE with `{state, approvalUrl, confirmCode, " +
@@ -0,0 +1,55 @@
1
+ /**
2
+ * DX-5237 (DX-4679 section 6) - the `plugin_publish` flow: build a card-type plugin project with its own `npm run build`, archive its source,
3
+ * take a screenshot, and upload all of it as a revision of one of the team's plugins (created on the first publish).
4
+ *
5
+ * The build, the archive and the upload are `@danxbot/plugin-sdk/publish`, the same code the `danx-plugin publish` CLI runs; this module adds
6
+ * what only an agent's tool has: the dashboard client it already holds (its credential, trace and session headers) as the transport, and the
7
+ * screenshot. The screenshot machinery is `design-screenshot.ts`, for the one page a plugin can show without the app around it: the project's
8
+ * optional `preview.html` (a plugin's own bundle imports the app's React and kit, so it cannot render alone). No preview page, no browser, or a
9
+ * failed capture publishes WITHOUT a screenshot and says why in the result (`none: <reason>`), the same stated degraded mode as `design_publish`.
10
+ *
11
+ * ORDER, so a failure leaves as little behind as it can: the arguments (a blank note is refused here, before the build), then the build (a failed build uploads nothing), the archive, the
12
+ * screenshot, and only then the upload. The dashboard is the only validator; its refusal comes back verbatim with the step it refused.
13
+ */
14
+ import { readFile } from "node:fs/promises";
15
+ import path from "node:path";
16
+ import { archiveSource, assertNote, buildPlugin, uploadRevision, withScreenshot } from "@danxbot/plugin-sdk/publish";
17
+ import { captureScreenshot } from "./design-screenshot.js";
18
+ /** The page in a plugin project's root that is screenshotted at publish. */
19
+ export const PREVIEW_FILE = "preview.html";
20
+ /** The agent's authenticated dashboard client as the publish flow's transport. */
21
+ function clientTransport(client) {
22
+ return { request: ({ method, path: route, query, body }) => client.request({ method, path: route, basePath: "", query, body }) };
23
+ }
24
+ async function readPreview(projectDir) {
25
+ try {
26
+ return await readFile(path.join(projectDir, PREVIEW_FILE), "utf8");
27
+ }
28
+ catch (err) {
29
+ if (err.code === "ENOENT")
30
+ return null;
31
+ throw err;
32
+ }
33
+ }
34
+ export async function pluginPublish(client, args, deps) {
35
+ assertNote(args.note);
36
+ const projectDir = path.resolve(deps.cwd, args.dir);
37
+ const built = await buildPlugin(projectDir);
38
+ const source = await archiveSource(projectDir);
39
+ const preview = await readPreview(projectDir);
40
+ const shot = preview === null
41
+ ? { png_base64: null, screenshot: `none: the project has no ${PREVIEW_FILE}` }
42
+ : await captureScreenshot(preview, { rootAbs: projectDir, cwd: deps.cwd });
43
+ const toUpload = shot.png_base64 === null ? built : withScreenshot(built, Buffer.from(shot.png_base64, "base64"));
44
+ const result = await uploadRevision(clientTransport(client), {
45
+ plugin: args.plugin,
46
+ name: args.name,
47
+ note: args.note,
48
+ built: toUpload,
49
+ source,
50
+ dashboardUrl: deps.baseUrl,
51
+ });
52
+ if (!result.ok)
53
+ return { ok: false, status: result.status, body: result.body, plugin: args.plugin, step: result.step };
54
+ return { ok: true, status: result.status, body: { ...result.body, screenshot: shot.screenshot } };
55
+ }
@@ -46,6 +46,10 @@ export const TOOL_ACCESS = {
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"),
49
+ // DX-5237 - builds in the agent's own checkout, then `GET /api/plugins` (to find the plugin by key), `POST /api/plugins` (a new key) and
50
+ // `POST /api/plugins/:id/revisions`; both writes are `plugins.publish`, which the default agent role holds. The lookup is a read the key's
51
+ // role already has (`boards.view`); a refusal of any step comes back verbatim.
52
+ plugin_publish: needs("plugins.publish"),
49
53
  // DX-4233 - the plugin's relay waits on the session's plan events here: it opens the listener ticket
50
54
  // (`POST /api/plan-sessions/me/stream-ticket`) and heartbeats (`.../me/heartbeat`), both `plans.view`.
51
55
  // DX-4667 - not operator-session-only: an orchestrator's dispatch calls it too, through a background waiter sub-agent that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.314",
3
+ "version": "0.1.315",
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",
@@ -31,6 +31,7 @@
31
31
  "test:watch": "vitest"
32
32
  },
33
33
  "dependencies": {
34
+ "@danxbot/plugin-sdk": "0.1.4",
34
35
  "@modelcontextprotocol/sdk": "1.29.0",
35
36
  "@thehammer/danx-session-telemetry": "0.1.52",
36
37
  "zod": "^3.25.76"