@thehammer/danx-dashboard-mcp 0.1.123 → 0.1.125

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.
Files changed (2) hide show
  1. package/dist/handlers.js +55 -0
  2. package/package.json +1 -1
package/dist/handlers.js CHANGED
@@ -413,6 +413,33 @@ 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
+ function refuseInapplicableFields(tool, mode, args, allowed) {
436
+ const offending = Object.keys(args)
437
+ .filter((key) => args[key] !== undefined && !STRUCTURAL_ACTION_FIELDS.includes(key) && !allowed.has(key))
438
+ .sort();
439
+ if (offending.length > 0) {
440
+ throw new Error(`${tool} ${mode} refuses fields that do not apply to this action: ${offending.join(", ")}`);
441
+ }
442
+ }
416
443
  export async function issueComment(client, args) {
417
444
  const idEnc = encodeURIComponent(args.id);
418
445
  const board = args.board;
@@ -614,10 +641,25 @@ export async function issueChecklist(client, args) {
614
641
  *
615
642
  * No answer action, for the reason `issueSolution` gives.
616
643
  */
644
+ /**
645
+ * DX-3310 (nit) — per-action allow-list for `refuseInapplicableFields`.
646
+ * `id`/`action`/`board` are structural and never listed (every action takes
647
+ * them). `type` is listed under `edit` even though it can never actually be
648
+ * SENT there (see the dedicated throw below) — that keeps the generic
649
+ * refusal from firing first with a less specific message than the one that
650
+ * already names the real reason.
651
+ */
652
+ const PROBLEM_ACTION_FIELDS = {
653
+ list: [],
654
+ add: ["statement", "context", "type", "summary", "solutions"],
655
+ edit: ["problem_id", "base_hash", "statement", "context", "summary", "type"],
656
+ remove: ["problem_id", "base_hash"],
657
+ };
617
658
  export async function issueProblem(client, args) {
618
659
  const idEnc = encodeURIComponent(args.id);
619
660
  const board = args.board;
620
661
  const need = argCheckers("issue_problem", `action=${args.action}`);
662
+ refuseInapplicableFields("issue_problem", `action=${args.action}`, args, new Set(PROBLEM_ACTION_FIELDS[args.action]));
621
663
  switch (args.action) {
622
664
  case "list":
623
665
  return client.request({ method: "GET", path: `/${idEnc}/problems`, board });
@@ -693,11 +735,24 @@ export async function issueProblem(client, args) {
693
735
  * question could release the very stop it set to wait for a human. The operator
694
736
  * answers in the dashboard.
695
737
  */
738
+ const SOLUTION_ACTION_FIELDS = {
739
+ add: ["title", "body", "pro", "con", "recommended", "steps"],
740
+ edit: ["solution_id", "base_hash", "title", "body", "pro", "con", "recommended", "steps", "steps_base_hash"],
741
+ remove: ["solution_id", "base_hash"],
742
+ add_step: ["solution_id", "title", "description", "parent_step_id", "position"],
743
+ edit_step: ["solution_id", "step_id", "base_hash", "title", "description"],
744
+ remove_step: ["solution_id", "step_id", "base_hash"],
745
+ };
696
746
  export async function issueSolution(client, args) {
697
747
  const need = argCheckers("issue_solution", `action=${args.action}`);
698
748
  // DX-2735: checked at runtime too, not only by the schema — a caller that skips
699
749
  // the MCP boundary must never build `/problems/undefined/solutions`.
700
750
  const problemId = need.id(args.problem_id, "problem_id");
751
+ // DX-3310 (nit) — `problem_id` is unioned in here rather than listed in
752
+ // every `SOLUTION_ACTION_FIELDS` entry: unlike `issue_problem` (where it
753
+ // only applies to edit/remove), `issue_solution` requires it for every
754
+ // action (checked above), so it is structural FOR THIS TOOL specifically.
755
+ refuseInapplicableFields("issue_solution", `action=${args.action}`, args, new Set(["problem_id", ...SOLUTION_ACTION_FIELDS[args.action]]));
701
756
  const base = `/${encodeURIComponent(args.id)}/problems/${problemId}/solutions`;
702
757
  const board = args.board;
703
758
  const content = {};
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.125",
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",