@thehammer/danx-dashboard-mcp 0.1.232 → 0.1.234

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
@@ -36,8 +36,9 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
36
36
  | Tool | Purpose |
37
37
  |---|---|
38
38
  | `danxbot_api` | `{method, path, query?, body?}` — calls any dashboard route the caller's credential may reach (card/plan CRUD, comments, dependencies, problems/solutions, quality gates, retro, triage, transitions, …). Returns `{ok, status, body}`; `body` carries the route's own response, or its structured error verbatim when `ok:false`. While this session is plan-connected, the dashboard's own reminders for it (today: `plan_list.listener_unattached`, attached by the server to `GET /api/plans` when the event listener is unattached past the startup grace, DX-3997) are relayed into the body's `reminders[]`; the MCP carries no reminder text or state of its own and re-reads at most every 30s. |
39
- | `danxbot_api_list` | No args. Lists the routes this caller's credential may reach, one line each (`{method, path, summary, permission}`) — call this first when you don't know the route for what you need to do. |
39
+ | `danxbot_api_list` | No args. Lists the routes this caller's credential may reach as `{routes: ["METHOD /path"], external: [{route, served_by}]}` (one bare string per route, exactly what `danxbot_api_spec` takes) — call this first when you don't know the route for what you need to do. |
40
40
  | `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. |
41
+ | `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}`. Opt-in: granted to `slack-agent` and `work` (DX-4386) through `extraTools`. A screenshot of browser-visible work: first `primary: true`, then `replaces` at each milestone. |
41
42
  | `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. |
42
43
 
43
44
  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/handlers.js CHANGED
@@ -137,7 +137,7 @@ export async function danxbotApi(client, args) {
137
137
  const ROUTES_BASE_PATH = "/api/routes";
138
138
  /**
139
139
  * `danxbot_api_list()` — the routes YOUR credential may currently call,
140
- * `[{method, path, summary, permission}]`, one line each. Backed by DX-3528's
140
+ * `{routes: ["METHOD /path"], external: [{route, served_by}]}`, one string each (DX-4401). Backed by DX-3528's
141
141
  * `GET /api/routes`, which filters the served route table through the SAME
142
142
  * `isRouteAuthorizedForPrincipal` check `authorize()` enforces on every
143
143
  * request — a route listed here is always callable, and a route this
@@ -212,8 +212,9 @@ export function inferAttachmentMime(filename) {
212
212
  * Attach a local file to a card. Reads the bytes from the dispatch's
213
213
  * shared filesystem (this MCP server runs co-located with the agent that
214
214
  * calls it), then forwards to the dashboard's `POST /api/issues/:id/attachments`
215
- * route — the SAME ingest the dashboard UI's own upload uses: stores the
216
- * bytes in S3, inserts one attachment row, and (via the dashboard's
215
+ * route (or, with `replaces`, `PUT /api/issues/:id/attachments/:aid`, which swaps that row's bytes in place) — the
216
+ * SAME ingest the dashboard UI's own upload uses: stores the
217
+ * bytes in S3, inserts or updates one attachment row, and (via the dashboard's
217
218
  * existing outbound projection) auto-mirrors the file to the card's
218
219
  * linked Trello card and Slack thread. Board-scoped; see `board`.
219
220
  *
@@ -234,21 +235,33 @@ export async function attachFile(client, args, deps = {}) {
234
235
  if (!isAbsolute(args.path)) {
235
236
  throw new Error(`attach_file: path must be an absolute path (got "${args.path}")`);
236
237
  }
238
+ // DX-4386: the replaced row keeps its stored name, so a filename override has nothing to apply to - refuse it loudly, before any read.
239
+ if (args.replaces !== undefined && args.filename !== undefined) {
240
+ throw new Error("attach_file: filename cannot be set with replaces (the replaced attachment keeps its name)");
241
+ }
237
242
  const read = deps.readFile ?? readFile;
238
243
  // A missing / unreadable file rejects here — no HTTP call is made, so no
239
244
  // S3 upload, no row insert, and no projection are ever triggered.
240
245
  const bytes = await read(args.path);
246
+ const contentTypeOf = (name) => (args.content_type?.trim() ? args.content_type.trim() : inferAttachmentMime(name));
247
+ const issuePath = `/${encodeURIComponent(args.card_id)}/attachments`;
248
+ // DX-4386: `primary` is the dashboard's own atomic option (one live cover, set in the write's own transaction); an absent flag sends no key.
249
+ const primary = args.primary === undefined ? {} : { primary: args.primary };
250
+ if (args.replaces !== undefined) {
251
+ return client.request({
252
+ method: "PUT",
253
+ basePath: "/api/issues",
254
+ path: `${issuePath}/${args.replaces}`,
255
+ body: { contentType: contentTypeOf(basename(args.path)), base64: bytes.toString("base64"), ...primary },
256
+ board: args.board,
257
+ });
258
+ }
241
259
  const filename = args.filename?.trim() ? args.filename.trim() : basename(args.path);
242
- const contentType = args.content_type?.trim() ? args.content_type.trim() : inferAttachmentMime(filename);
243
260
  return client.request({
244
261
  method: "POST",
245
262
  basePath: "/api/issues",
246
- path: `/${encodeURIComponent(args.card_id)}/attachments`,
247
- body: {
248
- filename,
249
- contentType,
250
- base64: bytes.toString("base64"),
251
- },
263
+ path: issuePath,
264
+ body: { filename, contentType: contentTypeOf(filename), base64: bytes.toString("base64"), ...primary },
252
265
  board: args.board,
253
266
  });
254
267
  }
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * brief_*, repo_knowledge_*, failure_category_*,
19
19
  * dispatch_transcript_search, quality_gate_instruction — all deleted
20
20
  * here). See `SERVER_INSTRUCTIONS` below for the calling convention.
21
- * - danxbot_api_list lists the routes your credential may call, one line each (DX-3465
21
+ * - danxbot_api_list lists the routes your credential may call, one string each (DX-3465
22
22
  * A5, backed by DX-3528's permission-aware discovery). Call this
23
23
  * first to find the route for something you need to do.
24
24
  * - danxbot_api_spec discovers the exact request/response shape of any route
@@ -449,8 +449,8 @@ async (args) => {
449
449
  // ---------------- danxbot_api_list (DX-3465 A5, PLN-11 R-25) ----------------
450
450
  strictTool("danxbot_api_list",
451
451
  // DX-3528 — the list is filtered by the same permission check every request takes.
452
- "Lists the dashboard API routes your credential may call: `[{method, path, summary, permission}]`, each " +
453
- "`path` a route template (`/api/issues/:id/transition`), the form `danxbot_api_spec` takes. A route not " +
452
+ "Lists the dashboard API routes your credential may call: `{routes: [\"METHOD /path/template\", ...], external: [...]}`, the " +
453
+ "strings `danxbot_api_spec` takes (`POST /api/issues/:id/transition`); `external` are worker-served routes. A route not " +
454
454
  "listed is not callable by you.", {}, async () => guardedBoundedJson(() => danxbotApiList(client)));
455
455
  // ---------------- danxbot_api_spec (DX-3465, PLN-11 R-25) ----------------
456
456
  strictTool("danxbot_api_spec",
@@ -475,14 +475,18 @@ strictTool("attach_file", "Attach a LOCAL file to a card. This server reads and
475
475
  "context. The attachment mirrors to the card's linked Trello card and Slack thread. 25 MB limit (413). " +
476
476
  "Returns `{issue, attachment_id, url, thumbnail_url, is_primary}`: `url` is permanent, embed it in any " +
477
477
  "markdown field as `![description](url)` and it renders inline. Use it when a screenshot or file judges something faster than prose, or instead of pasting a " +
478
- "large CSV or log into a card field.", {
478
+ "large CSV or log into a card field. A screenshot: first with `primary: true`, then `replaces: <id>` each milestone.", {
479
479
  card_id: z.string().min(1).describe("The card id to attach the file to."),
480
- path: z
481
- .string()
482
- .min(1)
483
- .describe("Absolute path to a local file on the dispatch's shared filesystem (must start with `/`)."),
480
+ path: z.string().min(1).describe("Absolute local path on the machine this server runs on (`/tmp/a.png` or `C:\\x\\a.png`)."),
484
481
  filename: z.string().min(1).optional().describe("Override for the stored filename; default: path's basename."),
485
482
  content_type: z.string().min(1).optional().describe("Override for the stored MIME type; default: an extension guess."),
483
+ replaces: z
484
+ .number()
485
+ .int()
486
+ .positive()
487
+ .optional()
488
+ .describe("Id of this card's attachment to replace in place: it keeps its id, name and cover flag. Not with `filename`."),
489
+ primary: z.boolean().optional().describe("Make this attachment the card's single cover (lead image)."),
486
490
  ...boardField,
487
491
  }, async (args) => guardedJson(() => attachFile(client, args)));
488
492
  // ---------------- request_permission (DX-4434, parent DX-4432) ----------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.232",
3
+ "version": "0.1.234",
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, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
5
5
  "license": "MIT",
6
6
  "type": "module",