@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 +61 -0
- package/dist/index.js +8 -7
- package/package.json +1 -1
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
|
|
638
|
-
"
|
|
639
|
-
"created new.
|
|
640
|
-
"
|
|
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
|
|
718
|
-
"
|
|
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.
|
|
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",
|