@thehammer/danx-dashboard-mcp 0.1.17 → 0.1.18

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
@@ -1,3 +1,28 @@
1
+ /**
2
+ * Per-tool handlers for the dashboard MCP server.
3
+ *
4
+ * Each handler is a pure async function: validate args (Zod at the MCP
5
+ * boundary), build the HTTP request the corresponding `/api/issues/*`
6
+ * route expects, and pass the envelope back to the caller verbatim.
7
+ *
8
+ * BOARD-ONLY (DX-1171): board is the first-level concept; repo is
9
+ * DERIVED from board server-side, never passed. Every handler accepts an
10
+ * optional `board` arg — a qualified board id (`<repo>:<slug>`). When
11
+ * set, it overrides the dispatch's configured board for that single call
12
+ * (forwarded as `?board=<board>` — and into `body.board` for create — so
13
+ * an agent can target any board). Omitted → falls back to the dispatch's
14
+ * env-derived board. The dashboard route validates the board (owning
15
+ * board→repo) and 404s on unknown; the MCP forwards the refusal envelope
16
+ * verbatim, never duplicates validation.
17
+ *
18
+ * Zero transformation of the server response. Refusal payloads carry
19
+ * structured detail (`failed_gate`, `non_terminal_phases`,
20
+ * `offending_keys`, `received_reason`, etc.) that the agent uses to
21
+ * decide its next move; swallowing any of it would force the agent to
22
+ * guess.
23
+ */
24
+ import { readFile } from "node:fs/promises";
25
+ import { basename, extname, isAbsolute } from "node:path";
1
26
  export async function issueList(client, args) {
2
27
  // status_derived is display-only on the server but the route accepts
3
28
  // it as a projection filter — passthrough verbatim. parent_id=null is
@@ -212,3 +237,70 @@ export async function issueRetro(client, args) {
212
237
  board,
213
238
  });
214
239
  }
240
+ /**
241
+ * Extension → MIME map for the agent-attached file types that matter for
242
+ * downstream rendering (image previews in the dashboard drawer, Trello card,
243
+ * and Slack thread all key on the persisted `mime`). This package is a
244
+ * standalone published artifact and cannot import the backend's helpers, so
245
+ * the small map lives here. An unknown extension resolves to the generic
246
+ * octet-stream — a sensible default for "we don't know the type", NOT a
247
+ * silenced error (the bytes still upload + the row is still written).
248
+ */
249
+ const MIME_BY_EXT = {
250
+ ".png": "image/png",
251
+ ".jpg": "image/jpeg",
252
+ ".jpeg": "image/jpeg",
253
+ ".gif": "image/gif",
254
+ ".webp": "image/webp",
255
+ ".svg": "image/svg+xml",
256
+ ".pdf": "application/pdf",
257
+ ".csv": "text/csv",
258
+ ".txt": "text/plain",
259
+ ".log": "text/plain",
260
+ ".md": "text/markdown",
261
+ ".json": "application/json",
262
+ ".html": "text/html",
263
+ ".zip": "application/zip",
264
+ ".gz": "application/gzip",
265
+ ".tgz": "application/gzip",
266
+ };
267
+ export function inferAttachmentMime(filename) {
268
+ return MIME_BY_EXT[extname(filename).toLowerCase()] ?? "application/octet-stream";
269
+ }
270
+ /**
271
+ * Attach a local file to an issue card. Reads the bytes from the dispatch's
272
+ * shared filesystem (this MCP server runs co-located with the agent), then
273
+ * forwards to the dashboard's `POST /api/issues/:id/attachments` route — the
274
+ * SAME ingest the dashboard UI upload uses, which uploads to S3, inserts ONE
275
+ * danxbot-origin `issue_attachments` row (`source_type=''`), and publishes
276
+ * `issue:updated` so the DX-1122 outbound projection mirrors the file to both
277
+ * the linked Trello card and the Slack card-view thread.
278
+ *
279
+ * Fail-loud (`base:fail-loudly`): the path is validated (non-empty + absolute)
280
+ * and the file is read BEFORE any HTTP call, so a bad/missing/unreadable path
281
+ * throws here and NEVER reaches S3, the DB row, or the projection. An S3 /
282
+ * server fault on the route surfaces as a 5xx, which the HTTP client throws.
283
+ */
284
+ export async function issueAttach(client, args, deps = {}) {
285
+ if (typeof args.file_path !== "string" || args.file_path.trim() === "") {
286
+ throw new Error("issue_attach: file_path must be a non-empty string");
287
+ }
288
+ if (!isAbsolute(args.file_path)) {
289
+ throw new Error(`issue_attach: file_path must be an absolute path (got "${args.file_path}")`);
290
+ }
291
+ const read = deps.readFile ?? readFile;
292
+ // A missing / unreadable file rejects here — no HTTP call is made, so no
293
+ // S3 upload, no row insert, and no projection are ever triggered.
294
+ const bytes = await read(args.file_path);
295
+ const filename = basename(args.file_path);
296
+ return client.request({
297
+ method: "POST",
298
+ path: `/${encodeURIComponent(args.id)}/attachments`,
299
+ body: {
300
+ filename,
301
+ contentType: inferAttachmentMime(filename),
302
+ base64: bytes.toString("base64"),
303
+ },
304
+ board: args.board,
305
+ });
306
+ }
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * - issue_dependency POST/DELETE /api/issues/:id/dependencies[/:did]
22
22
  * - issue_requires_human POST/DELETE /api/issues/:id/requires-human
23
23
  * - issue_retro PUT /api/issues/:id/retro
24
+ * - issue_attach POST /api/issues/:id/attachments (reads a local file)
24
25
  *
25
26
  * BOARD-ONLY (DX-1171): board is the first-level concept; repo is
26
27
  * DERIVED from board server-side, never passed. The package composes the
@@ -49,7 +50,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
49
50
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
50
51
  import { z } from "zod";
51
52
  import { DashboardHttpClient } from "./http-client.js";
52
- import { issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueRequiresHuman, issueRetro, issueTransition, issueTriage, } from "./handlers.js";
53
+ import { issueAttach, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueRequiresHuman, issueRetro, issueTransition, issueTriage, } from "./handlers.js";
53
54
  function readEnvOrDie(name) {
54
55
  const v = process.env[name];
55
56
  if (typeof v !== "string" || v === "") {
@@ -258,6 +259,19 @@ server.tool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retr
258
259
  })),
259
260
  ...boardField,
260
261
  }, async (args) => jsonResult(await issueRetro(client, args)));
262
+ // ---------------- issue_attach ----------------
263
+ // The "25 MB" ceiling quoted in the description below tracks the backend
264
+ // route's MAX_DECODED_BYTES (src/issues/write/attachments.ts). This package is
265
+ // a separate published artifact and cannot import that constant, so the number
266
+ // is restated here as prose — keep the two in sync if the backend ceiling moves.
267
+ server.tool("issue_attach", "Attach a LOCAL file to an issue card via POST /api/issues/:id/attachments. Pass `id` (the card) and `file_path` (an ABSOLUTE path to a file on the dispatch's shared filesystem — e.g. a screenshot, exported CSV, or diagram you wrote). This MCP server reads the bytes, infers the MIME type from the extension, and uploads through the dashboard, which: stores the bytes in S3, inserts ONE danxbot-origin issue_attachments row, and auto-mirrors the file to the card's linked Trello card AND its Slack card-view thread (DX-1122 outbound projection) — no extra step needed. Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to attach on another board (unknown board → 404). Fail-loud: a relative/empty path is rejected at the MCP boundary, and a missing/unreadable file throws BEFORE any upload (no partial S3 object, no row). 25 MB decoded ceiling (413). Returns the hydrated issue plus the new attachment id.", {
268
+ id: z.string().min(1),
269
+ file_path: z
270
+ .string()
271
+ .min(1)
272
+ .describe("Absolute path to a local file on the dispatch's shared filesystem (must start with `/`)."),
273
+ ...boardField,
274
+ }, async (args) => jsonResult(await issueAttach(client, args)));
261
275
  // ---------------- main ----------------
262
276
  async function main() {
263
277
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
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",