@thehammer/danx-dashboard-mcp 0.1.148 → 0.1.149
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 +2 -2
- package/dist/handlers.js +93 -5
- package/dist/index.js +32 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,8 +26,8 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
|
|
|
26
26
|
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert). `title` = short domain-naming label; `summary` = 1–3 plain-language sentences, always shown; `description` = the collapsed "Context" body. Root and every phase child take their own `summary` |
|
|
27
27
|
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose + structured keys (`title`, `summary` (null clears), `description`, `ac`, `checklists`, `effort_level`, `parent_id`, `priority`, `list_id`); semantic keys refused with 400 + pointer to dedicated handler. `priority` (DX-1532) takes a tier word (`low`/`high`/…) or a number in `[0,6)` — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
|
|
28
28
|
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen. `block` is a dispatch hold only — it never marks the card as needing a human; use `issue_problem` add for that |
|
|
29
|
-
| `issue_problem` | `GET/POST/PATCH/DELETE /api/issues/:id/problems[/:pid]` | Actions list / add / edit / remove. A problem is one statement the operator must resolve (a question or a plan flaw) with its own solutions and answers; `add` takes `statement` + optional `solutions[]` in one transaction and IS what puts the card in front of a human (`open_problem_count > 0`) — a successful add also returns a `reminders: [{key, text}]` array (DX-3365 — the reminder registry's middleware, DB-driven and operator-overridable from the dashboard). Edit + remove are hash-guarded (`base_hash`, 409 `stale_problem`); removing the card's last open problem is always allowed — it just means the card no longer needs a human |
|
|
30
|
-
| `issue_solution` | `POST/PATCH/DELETE /api/issues/:id/problems/:pid/solutions[/:sid]` | Actions add / edit / remove, `problem_id` required (list via `issue_problem list`). Edit + remove are hash-guarded (`base_hash`, 409 `stale_solution` with the current row); at most one live `recommended` per problem; a chosen option cannot be edited. No answer action on
|
|
29
|
+
| `issue_problem` | `GET/POST/PATCH/DELETE /api/issues/:id/problems[/:pid]` + `POST .../answer`, `change-answer`, `unanswer` | Actions list / add / edit / remove / answer / change_answer / unanswer (DX-3471). A problem is one statement the operator must resolve (a question or a plan flaw) with its own solutions and answers; `add` takes `statement` + optional `solutions[]` in one transaction and IS what puts the card in front of a human (`open_problem_count > 0`) — a successful add also returns a `reminders: [{key, text}]` array (DX-3365 — the reminder registry's middleware, DB-driven and operator-overridable from the dashboard). Edit + remove are hash-guarded (`base_hash`, 409 `stale_problem`); removing the card's last open problem is always allowed — it just means the card no longer needs a human. `answer` `{solution_id, note?}` \| `{freeform}` and `change_answer` (same + `base_hash` naming the live DECISION) / `unanswer` (`{base_hash}`) record or retract an operator's decision — server-refused 403 for a dispatched agent's machine credential (DX-2830), reachable from an OPERATOR SESSION relaying an in-chat decision, which also durably records the relaying session (`relayed_by_session`) via the same session header every call already carries |
|
|
30
|
+
| `issue_solution` | `POST/PATCH/DELETE /api/issues/:id/problems/:pid/solutions[/:sid]` | Actions add / edit / remove, `problem_id` required (list via `issue_problem list`). Edit + remove are hash-guarded (`base_hash`, 409 `stale_solution` with the current row); at most one live `recommended` per problem; a chosen option cannot be edited. No answer action on THIS tool — see `issue_problem`'s answer / change_answer / unanswer actions |
|
|
31
31
|
| `issue_triage` | `POST /api/issues/:id/triage` | Send `{confidence, reason}` — an integer 0-5 score; the server computes the verdict (approve/cancel/keep/defer) against the board's configured thresholds (DX-2086). `keep`/`defer` now block the card. None of these are a cross-card ordering gate; use `issue_dependency` to sequence cards |
|
|
32
32
|
| `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
|
|
33
33
|
| `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"`. The only mechanism the dispatch picker enforces to sequence one card after another — status alone is not a substitute |
|
package/dist/handlers.js
CHANGED
|
@@ -568,7 +568,31 @@ export async function issueChecklist(client, args) {
|
|
|
568
568
|
* `.describe()` in `index.ts` — that description, not this comment, is what
|
|
569
569
|
* a calling agent actually reads.
|
|
570
570
|
*
|
|
571
|
-
*
|
|
571
|
+
* DX-3471 — `answer` {solution_id, note?} | {freeform} → POST :pid/answer;
|
|
572
|
+
* `change_answer` {base_hash, solution_id, note?} | {base_hash, freeform} →
|
|
573
|
+
* POST :pid/change-answer (retracts the live decision and records a new one
|
|
574
|
+
* in ONE server transaction); `unanswer` {base_hash} → POST :pid/unanswer
|
|
575
|
+
* (retracts with no new answer, reopening the problem). `base_hash` on these
|
|
576
|
+
* two names the live DECISION (`decisionContentHash`), NOT the problem —
|
|
577
|
+
* read it off a problem's `decisions[].content_hash` (from `list`), never
|
|
578
|
+
* off the problem's own `content_hash`. A stale one → 409 `stale_decision`
|
|
579
|
+
* with `currentHash` + `currentProblem`: merge, then retry.
|
|
580
|
+
*
|
|
581
|
+
* ALL THREE ARE OPERATOR-SESSION ONLY. The server's route policy admits only
|
|
582
|
+
* a `human` principal on these three routes (`route-policies/index.ts`) — a
|
|
583
|
+
* dispatched agent's machine credential is refused before it ever reaches a
|
|
584
|
+
* handler, for the DX-2830 reason: an agent that could answer its own
|
|
585
|
+
* question could release the very human gate it set to stop and wait for.
|
|
586
|
+
* An operator-session agent (running under the OPERATOR's own dashboard
|
|
587
|
+
* token, human-authenticated) CAN call these — this is what turns "the
|
|
588
|
+
* operator decided in chat" into a durable, correctly-attributed decision
|
|
589
|
+
* instead of a comment or a raw curl call. The decision records the human
|
|
590
|
+
* decider (`decided_by`) and, when this call carries the session id
|
|
591
|
+
* `@thehammer/danx-dashboard-mcp` already stamps on every request
|
|
592
|
+
* (`x-danx-session-id`, DX-2683 — no separate parameter needed here),
|
|
593
|
+
* which working session relayed it (`relayed_by_session` on the resulting
|
|
594
|
+
* decision, DX-3471) — so a decision entered from the dashboard's own UI
|
|
595
|
+
* (no header) stays distinguishable from one an agent relayed in chat.
|
|
572
596
|
*/
|
|
573
597
|
/**
|
|
574
598
|
* DX-3310 (nit) — per-action allow-list for `refuseInapplicableFields`.
|
|
@@ -583,6 +607,9 @@ const PROBLEM_ACTION_FIELDS = {
|
|
|
583
607
|
add: ["statement", "context", "type", "summary", "solutions"],
|
|
584
608
|
edit: ["problem_id", "base_hash", "statement", "context", "summary", "type"],
|
|
585
609
|
remove: ["problem_id", "base_hash"],
|
|
610
|
+
answer: ["problem_id", "solution_id", "note", "freeform"],
|
|
611
|
+
change_answer: ["problem_id", "base_hash", "solution_id", "note", "freeform"],
|
|
612
|
+
unanswer: ["problem_id", "base_hash"],
|
|
586
613
|
};
|
|
587
614
|
export async function issueProblem(client, args) {
|
|
588
615
|
const idEnc = encodeURIComponent(args.id);
|
|
@@ -652,8 +679,66 @@ export async function issueProblem(client, args) {
|
|
|
652
679
|
board,
|
|
653
680
|
});
|
|
654
681
|
}
|
|
682
|
+
// DX-3471 — answer/change_answer share the exact-one-of solution_id/freeform
|
|
683
|
+
// shape the server's own `validateAnswer` enforces (`write/problem-answer.ts`);
|
|
684
|
+
// checked here too so a caller gets the same clear boundary error every
|
|
685
|
+
// OTHER missing/conflicting-arg case in this tool already gets, rather than
|
|
686
|
+
// a round trip to the server for a 400 the client could have caught.
|
|
687
|
+
case "answer": {
|
|
688
|
+
const problemId = need.id(args.problem_id, "problem_id");
|
|
689
|
+
return client.request({
|
|
690
|
+
method: "POST",
|
|
691
|
+
path: `/${idEnc}/problems/${problemId}/answer`,
|
|
692
|
+
body: answerChoiceBody(args, "answer"),
|
|
693
|
+
board,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
case "change_answer": {
|
|
697
|
+
const problemId = need.id(args.problem_id, "problem_id");
|
|
698
|
+
const baseHash = need.string(args.base_hash, "base_hash");
|
|
699
|
+
return client.request({
|
|
700
|
+
method: "POST",
|
|
701
|
+
path: `/${idEnc}/problems/${problemId}/change-answer`,
|
|
702
|
+
body: { base_hash: baseHash, ...answerChoiceBody(args, "change_answer") },
|
|
703
|
+
board,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
case "unanswer": {
|
|
707
|
+
const problemId = need.id(args.problem_id, "problem_id");
|
|
708
|
+
const baseHash = need.string(args.base_hash, "base_hash");
|
|
709
|
+
return client.request({
|
|
710
|
+
method: "POST",
|
|
711
|
+
path: `/${idEnc}/problems/${problemId}/unanswer`,
|
|
712
|
+
body: { base_hash: baseHash },
|
|
713
|
+
board,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
655
716
|
}
|
|
656
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* DX-3471 — the either/or answer shape `answer`/`change_answer` both send:
|
|
720
|
+
* `{solution_id, note?}` XOR `{freeform}`, mirroring the server's own
|
|
721
|
+
* `validateAnswer` (`write/problem-answer.ts`) so a caller gets the SAME
|
|
722
|
+
* refusal at this client boundary that the server would otherwise 400 on,
|
|
723
|
+
* with no round trip.
|
|
724
|
+
*/
|
|
725
|
+
function answerChoiceBody(args, mode) {
|
|
726
|
+
const hasSolution = args.solution_id !== undefined;
|
|
727
|
+
const hasFreeform = args.freeform !== undefined;
|
|
728
|
+
if (hasSolution === hasFreeform) {
|
|
729
|
+
throw new Error(`issue_problem ${mode} requires either solution_id or freeform (never both, never neither)`);
|
|
730
|
+
}
|
|
731
|
+
if (hasFreeform) {
|
|
732
|
+
if (args.note !== undefined) {
|
|
733
|
+
throw new Error(`issue_problem ${mode}: note qualifies a chosen solution — it is not valid with freeform`);
|
|
734
|
+
}
|
|
735
|
+
return { freeform: args.freeform };
|
|
736
|
+
}
|
|
737
|
+
const body = { solution_id: args.solution_id };
|
|
738
|
+
if (args.note !== undefined)
|
|
739
|
+
body.note = args.note;
|
|
740
|
+
return body;
|
|
741
|
+
}
|
|
657
742
|
/**
|
|
658
743
|
* One problem's candidate solutions, AND (DX-3310) one solution's individual
|
|
659
744
|
* procedure steps, via `/api/issues/:id/problems/:pid/solutions[/:sid][/steps[/:stepId]]`
|
|
@@ -664,10 +749,13 @@ export async function issueProblem(client, args) {
|
|
|
664
749
|
* solution/step id from the wrong scope → 404, nesting past depth 3 → 400)
|
|
665
750
|
* pass through verbatim.
|
|
666
751
|
*
|
|
667
|
-
* There is deliberately NO answer action
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
752
|
+
* There is deliberately NO answer action on THIS tool — answering releases the
|
|
753
|
+
* human gate on a card, and this tool has no notion of "who is calling".
|
|
754
|
+
* `issue_problem`'s `answer` / `change_answer` / `unanswer` actions (DX-3471)
|
|
755
|
+
* are where that lives instead: server-side human-principal-only (a
|
|
756
|
+
* dispatched agent's machine credential is refused 403), so an operator
|
|
757
|
+
* session can record a decision but a dispatched worker cannot release its
|
|
758
|
+
* own gate.
|
|
671
759
|
*/
|
|
672
760
|
const SOLUTION_ACTION_FIELDS = {
|
|
673
761
|
add: ["title", "body", "pro", "con", "recommended", "steps"],
|
package/dist/index.js
CHANGED
|
@@ -657,11 +657,39 @@ const STEP_INPUT = z.lazy(() => z
|
|
|
657
657
|
steps: z.array(STEP_INPUT).optional().describe("this step's own children, nested — refused past 3 levels total"),
|
|
658
658
|
})
|
|
659
659
|
.strict());
|
|
660
|
-
strictTool("issue_problem", "A card's PROBLEMS: one statement the operator must resolve (a question, or a flaw in the plan) OR an action only a person can carry out, each with its own solutions/answers — one problem per question. OPEN = not yet answered; the card needs a human exactly while open_problem_count > 0 — adding a problem IS putting the card in front of a human, there is no separate flag. list → live problems in order, each {id, statement, context, type, summary, content_hash, open, solutions[], decisions[]}; add {statement, context?, type?, summary?, solutions?} → problem_id + solution_ids in one transaction (zero solutions is valid: the operator answers free-form) plus a `reminders: [{key, text}]` array (DX-3365 — every MCP-response reminder rides this ONE field now, DB-registry-driven and operator-overridable from the dashboard) naming each still-open problem's solution count; edit :pid {base_hash, statement, context?, summary?}; remove :pid {base_hash} — always allowed, even as the card's last open problem (removing it just means the card no longer needs a human). A stale base_hash → 409 `stale_problem` with currentHash + currentProblem: merge, then retry.
|
|
660
|
+
strictTool("issue_problem", "A card's PROBLEMS: one statement the operator must resolve (a question, or a flaw in the plan) OR an action only a person can carry out, each with its own solutions/answers — one problem per question. OPEN = not yet answered; the card needs a human exactly while open_problem_count > 0 — adding a problem IS putting the card in front of a human, there is no separate flag. list → live problems in order, each {id, statement, context, type, summary, content_hash, open, solutions[], decisions[]} (each decision carries `relayed_by_session`, DX-3471 — the working session that recorded it, or null for one entered directly in the dashboard); add {statement, context?, type?, summary?, solutions?} → problem_id + solution_ids in one transaction (zero solutions is valid: the operator answers free-form) plus a `reminders: [{key, text}]` array (DX-3365 — every MCP-response reminder rides this ONE field now, DB-registry-driven and operator-overridable from the dashboard) naming each still-open problem's solution count; edit :pid {base_hash, statement, context?, summary?}; remove :pid {base_hash} — always allowed, even as the card's last open problem (removing it just means the card no longer needs a human). A stale base_hash → 409 `stale_problem` with currentHash + currentProblem: merge, then retry. " +
|
|
661
|
+
"DX-3471 — answer :pid {solution_id, note?} | {freeform} records the operator's decision on a LIVE problem (exactly one of solution_id/freeform; note only qualifies a chosen solution); change_answer :pid {base_hash, solution_id, note?} | {base_hash, freeform} retracts the current decision and records a new one in ONE server transaction; unanswer :pid {base_hash} retracts with no new answer, reopening the problem. `base_hash` on these two names the live DECISION (from that problem's `decisions[].content_hash` in `list`), NOT the problem's own hash — a stale one → 409 `stale_decision` with currentHash + currentDecision: merge, then retry. " +
|
|
662
|
+
"**These three are OPERATOR-SESSION ONLY** — the server refuses a dispatched agent's machine credential 403 (DX-2830: an agent that could answer its own question could release the very human gate it set to stop and wait for); call them only when relaying a decision the OPERATOR actually made in this chat, never a decision you are making yourself. Session attribution is automatic (the same `x-danx-session-id` every call already carries, DX-2683) — no session parameter to pass here. " +
|
|
663
|
+
"`statement` is capped at 200 characters (400 names the actual length otherwise): one plain sentence, with any investigation detail in `context` (markdown, no cap) instead. Read `type`'s and `summary`'s own descriptions below before your first `add` — together they teach which type this is and the three-field split (`statement`/`summary`/`context`) an action needs.", {
|
|
661
664
|
id: z.string().min(1),
|
|
662
|
-
action: z.enum(["list", "add", "edit", "remove"]),
|
|
663
|
-
problem_id: z.number().int().positive().optional().describe("edit/remove"),
|
|
664
|
-
base_hash: z
|
|
665
|
+
action: z.enum(["list", "add", "edit", "remove", "answer", "change_answer", "unanswer"]),
|
|
666
|
+
problem_id: z.number().int().positive().optional().describe("edit/remove/answer/change_answer/unanswer"),
|
|
667
|
+
base_hash: z
|
|
668
|
+
.string()
|
|
669
|
+
.min(1)
|
|
670
|
+
.optional()
|
|
671
|
+
.describe("edit/remove: content_hash last read for the PROBLEM. change_answer/unanswer: content_hash last read for " +
|
|
672
|
+
"the live DECISION instead (that problem's `decisions[].content_hash` from `list`) — the two are never " +
|
|
673
|
+
"interchangeable."),
|
|
674
|
+
solution_id: z
|
|
675
|
+
.number()
|
|
676
|
+
.int()
|
|
677
|
+
.positive()
|
|
678
|
+
.optional()
|
|
679
|
+
.describe("answer/change_answer only. The chosen option's id, from this problem's own solutions[]. Exactly one of " +
|
|
680
|
+
"solution_id / freeform — never both, never neither (refused before any request)."),
|
|
681
|
+
note: z
|
|
682
|
+
.string()
|
|
683
|
+
.min(1)
|
|
684
|
+
.optional()
|
|
685
|
+
.describe("answer/change_answer only, solution_id answers only: an optional qualification (\"this, but…\"). Refused " +
|
|
686
|
+
"alongside freeform, which is already the operator's own words."),
|
|
687
|
+
freeform: z
|
|
688
|
+
.string()
|
|
689
|
+
.min(1)
|
|
690
|
+
.optional()
|
|
691
|
+
.describe("answer/change_answer only. The operator's own words, when no listed solution fits. Exactly one of " +
|
|
692
|
+
"solution_id / freeform — never both, never neither."),
|
|
665
693
|
statement: z
|
|
666
694
|
.string()
|
|
667
695
|
.min(1)
|
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.149",
|
|
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",
|