@thehammer/danx-dashboard-mcp 0.1.123 → 0.1.126

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
@@ -413,6 +413,39 @@ function argCheckers(tool, mode) {
413
413
  },
414
414
  };
415
415
  }
416
+ /**
417
+ * Every action-dispatched tool's args carry these three regardless of
418
+ * `action` — never action-specific, so every per-action allow-list passed to
419
+ * `refuseInapplicableFields` is unioned with this rather than repeating it.
420
+ */
421
+ const STRUCTURAL_ACTION_FIELDS = ["id", "action", "board"];
422
+ /**
423
+ * DX-3310 (code-review nit) — a field the caller sent that means nothing for
424
+ * the chosen `action` used to be silently ignored: `issue_solution`'s `add`
425
+ * built its `content` object from `title`/`body`/`pro`/`con`/`recommended`
426
+ * regardless of action, then simply never read it on `remove`/`add_step`/etc,
427
+ * and `issue_problem`'s `remove` never looked at `statement`/`context`/
428
+ * `solutions` at all. Either way the caller got a 200 with the field quietly
429
+ * discarded — no different from a typo. Refused loud instead, mirroring
430
+ * `argCheckers`' own wording: every ACTUALLY-SUPPLIED (`!== undefined`) key
431
+ * outside the action's own allow-list (plus `STRUCTURAL_ACTION_FIELDS`,
432
+ * always allowed) is named in one error, so a caller fixes every offending
433
+ * field in one round trip rather than one at a time.
434
+ */
435
+ // A9 (code-review fix) — takes `object` rather than `Record<string, unknown>`
436
+ // so every caller (each with its own specific `*Args` interface, no index
437
+ // signature) passes `args` directly, with no `as unknown as Record<...>`
438
+ // bridge cast at the call site. `Object.entries` reads the key/value pairs
439
+ // without needing an index signature on the input type either.
440
+ function refuseInapplicableFields(tool, mode, args, allowed) {
441
+ const offending = Object.entries(args)
442
+ .filter(([key, value]) => value !== undefined && !STRUCTURAL_ACTION_FIELDS.includes(key) && !allowed.has(key))
443
+ .map(([key]) => key)
444
+ .sort();
445
+ if (offending.length > 0) {
446
+ throw new Error(`${tool} ${mode} refuses fields that do not apply to this action: ${offending.join(", ")}`);
447
+ }
448
+ }
416
449
  export async function issueComment(client, args) {
417
450
  const idEnc = encodeURIComponent(args.id);
418
451
  const board = args.board;
@@ -614,10 +647,25 @@ export async function issueChecklist(client, args) {
614
647
  *
615
648
  * No answer action, for the reason `issueSolution` gives.
616
649
  */
650
+ /**
651
+ * DX-3310 (nit) — per-action allow-list for `refuseInapplicableFields`.
652
+ * `id`/`action`/`board` are structural and never listed (every action takes
653
+ * them). `type` is listed under `edit` even though it can never actually be
654
+ * SENT there (see the dedicated throw below) — that keeps the generic
655
+ * refusal from firing first with a less specific message than the one that
656
+ * already names the real reason.
657
+ */
658
+ const PROBLEM_ACTION_FIELDS = {
659
+ list: [],
660
+ add: ["statement", "context", "type", "summary", "solutions"],
661
+ edit: ["problem_id", "base_hash", "statement", "context", "summary", "type"],
662
+ remove: ["problem_id", "base_hash"],
663
+ };
617
664
  export async function issueProblem(client, args) {
618
665
  const idEnc = encodeURIComponent(args.id);
619
666
  const board = args.board;
620
667
  const need = argCheckers("issue_problem", `action=${args.action}`);
668
+ refuseInapplicableFields("issue_problem", `action=${args.action}`, args, new Set(PROBLEM_ACTION_FIELDS[args.action]));
621
669
  switch (args.action) {
622
670
  case "list":
623
671
  return client.request({ method: "GET", path: `/${idEnc}/problems`, board });
@@ -693,11 +741,24 @@ export async function issueProblem(client, args) {
693
741
  * question could release the very stop it set to wait for a human. The operator
694
742
  * answers in the dashboard.
695
743
  */
744
+ const SOLUTION_ACTION_FIELDS = {
745
+ add: ["title", "body", "pro", "con", "recommended", "steps"],
746
+ edit: ["solution_id", "base_hash", "title", "body", "pro", "con", "recommended", "steps", "steps_base_hash"],
747
+ remove: ["solution_id", "base_hash"],
748
+ add_step: ["solution_id", "title", "description", "parent_step_id", "position"],
749
+ edit_step: ["solution_id", "step_id", "base_hash", "title", "description"],
750
+ remove_step: ["solution_id", "step_id", "base_hash"],
751
+ };
696
752
  export async function issueSolution(client, args) {
697
753
  const need = argCheckers("issue_solution", `action=${args.action}`);
698
754
  // DX-2735: checked at runtime too, not only by the schema — a caller that skips
699
755
  // the MCP boundary must never build `/problems/undefined/solutions`.
700
756
  const problemId = need.id(args.problem_id, "problem_id");
757
+ // DX-3310 (nit) — `problem_id` is unioned in here rather than listed in
758
+ // every `SOLUTION_ACTION_FIELDS` entry: unlike `issue_problem` (where it
759
+ // only applies to edit/remove), `issue_solution` requires it for every
760
+ // action (checked above), so it is structural FOR THIS TOOL specifically.
761
+ refuseInapplicableFields("issue_solution", `action=${args.action}`, args, new Set(["problem_id", ...SOLUTION_ACTION_FIELDS[args.action]]));
701
762
  const base = `/${encodeURIComponent(args.id)}/problems/${problemId}/solutions`;
702
763
  const board = args.board;
703
764
  const content = {};
package/dist/index.js CHANGED
@@ -634,11 +634,11 @@ const STEP_INPUT = z.lazy(() => z
634
634
  .positive()
635
635
  .optional()
636
636
  .describe("present -> this node IS an existing step (matched on this id, 400 if it does not resolve to a live " +
637
- "child of this exact parent scope — a supplied id is NEVER claimed by the positional/title fallback " +
638
- "below); absent -> matched against unclaimed siblings by TITLE (first unclaimed match, in order), else " +
639
- "created new. An id-less MIDDLE insert recreates every later id-less sibling's row unless you send ids " +
640
- "for the ones you want preserved — send ids whenever an existing step's identity matters (checked " +
641
- "progress, a later single-step edit)."),
637
+ "child of this exact parent scope — a supplied id is NEVER claimed by the fallback below); absent -> " +
638
+ "id-less nodes match the first unclaimed sibling with the same title under the same parent, else " +
639
+ "created new. This title-based match (never positional) is what lets an id-less MIDDLE insert keep " +
640
+ "every later id-less sibling's id — send ids anyway whenever an existing step's identity matters " +
641
+ "(checked progress, a later single-step edit), or when two siblings under the same parent share a title."),
642
642
  title: z
643
643
  .string()
644
644
  .min(1)
@@ -714,8 +714,9 @@ strictTool("issue_solution", "One problem's options via /api/issues/:id/problems
714
714
  "changed fields, steps?, steps_base_hash?}: `steps` omitted leaves the stored procedure untouched, an explicit " +
715
715
  "array (including [], refused if it would leave an action's solution with zero steps) DIFFS against it — send " +
716
716
  "back an existing step's `id` (read from the card) to keep it (even while retitling/reordering it) and omit " +
717
- "`id` for a genuinely new step; any existing step you don't include gets removed, and an id-less MIDDLE insert " +
718
- "recreates every later id-less sibling's row unless you send ids for the ones whose identity matters. " +
717
+ "`id` for a genuinely new step; any existing step you don't include gets removed. An id-less node matches the " +
718
+ "first unclaimed sibling with the same title under the same parent (never positional), so an id-less MIDDLE " +
719
+ "insert keeps every later id-less sibling's id — send ids anyway whenever an existing step's identity matters. " +
719
720
  "`steps_base_hash` is REQUIRED whenever `steps` is sent — the solution's current `steps_hash` (read it off the " +
720
721
  "card first); a stale value → 409 `stale_steps` carrying the current tree, so a step added/removed since your " +
721
722
  "read is never silently dropped. NEVER resend the whole tree just to fix one word — see add_step/edit_step/" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.123",
3
+ "version": "0.1.126",
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",