@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 +92 -0
- package/dist/index.js +15 -1
- package/package.json +1 -1
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.
|
|
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",
|