@thehammer/danx-dashboard-mcp 0.1.97 → 0.1.98
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 +29 -28
- package/dist/index.js +20 -24
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -232,8 +232,8 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
232
232
|
body.effort_level = args.effort_level;
|
|
233
233
|
if (args.list_id !== undefined)
|
|
234
234
|
body.list_id = args.list_id;
|
|
235
|
-
if (args.
|
|
236
|
-
body.
|
|
235
|
+
if (args.gate_decisions !== undefined)
|
|
236
|
+
body.gate_decisions = args.gate_decisions;
|
|
237
237
|
// DX-1895 (explicit-only) — forward the caller's explicit auto-triage
|
|
238
238
|
// decision; the server stamps false when absent (never auto-triaged).
|
|
239
239
|
// Per-child flags ride along inside phase_children entries verbatim.
|
|
@@ -683,41 +683,42 @@ export async function issueSolution(client, args) {
|
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
/**
|
|
686
|
-
*
|
|
687
|
-
* POST /api/issues/:id/quality-gates/:gate {
|
|
686
|
+
* Flip a single card's per-card quality-gate `required` flag via
|
|
687
|
+
* POST /api/issues/:id/quality-gates/:gate {required} — the same write the
|
|
688
688
|
* dashboard drawer's Quality Gates tab performs (DX-1181). This is the ONLY
|
|
689
|
-
* post-create path to
|
|
690
|
-
*
|
|
689
|
+
* post-create path to mark a gate required/not — `issue_create` carries
|
|
690
|
+
* `gate_decisions` at birth, and `issue_edit` rejects gate keys; without
|
|
691
|
+
* this tool an agent that created a card cannot turn a gate on afterward.
|
|
691
692
|
*
|
|
692
693
|
* `gate` is a registry name (`plan-dependency` | `plan-architecture` |
|
|
693
694
|
* `plan-tdd` | `code-test-quality` | `code-architecture` | `code-quality`).
|
|
694
|
-
* An unknown gate is a 400 from the server (never a silent no-op)
|
|
695
|
-
* card whose type is never gated (`Epic` / `Feature` / `Task`).
|
|
695
|
+
* An unknown gate is a 400 from the server (never a silent no-op).
|
|
696
696
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
697
|
+
* Board requirement is TRI-STATE per gate, NOT a binary on/off
|
|
698
|
+
* (`board_quality_gate_settings.default_state`): `required` = always runs
|
|
699
|
+
* (this per-card flag is irrelevant); `optional` = ENABLED, runs WHEN this
|
|
700
|
+
* per-card flag is true (per-card opt-in — `optional` is NOT "off");
|
|
701
|
+
* `disabled` = never runs (this flag is inert). So flipping `required:true`
|
|
702
|
+
* here launches the gate when the board state is `required` OR `optional`;
|
|
703
|
+
* it is inert ONLY when the board state is `disabled`. Source of truth:
|
|
704
|
+
* `isGateEffectivelyRequired` in `src/issues/quality-gates/read.ts`.
|
|
702
705
|
*
|
|
703
|
-
*
|
|
704
|
-
* `
|
|
705
|
-
* three inputs and the board could silently make the write a no-op — with the
|
|
706
|
-
* tri-state gone, the write IS the outcome and those fields would be constants.
|
|
706
|
+
* `effort_level` (DX-1760) is an independent sibling write: present (incl.
|
|
707
|
+
* `null`) sets `card_quality_gates.effort_level`; omitted leaves it untouched.
|
|
707
708
|
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
709
|
+
* **Effectiveness echo.** The server response body is `{issue, applied: true,
|
|
710
|
+
* effective, reason}`, not just `{issue}` — `client.request()` passes it
|
|
711
|
+
* through VERBATIM (see `http-client.ts`'s header doc: "the envelope
|
|
712
|
+
* passthrough is load-bearing"), so no transform is needed here for the
|
|
713
|
+
* calling agent to see `effective` (what `isGateEffectivelyRequired` resolves
|
|
714
|
+
* to right after this write, per the tri-state rule above) and `reason`
|
|
715
|
+
* (`null` when it matches the `required` value just sent, otherwise which
|
|
716
|
+
* board state overrode it). This closes the gap where a 200 alone could not
|
|
717
|
+
* tell a fully-honored write from one the board's `required`/`disabled`
|
|
718
|
+
* state made a complete no-op.
|
|
716
719
|
*/
|
|
717
720
|
export async function issueQualityGate(client, args) {
|
|
718
|
-
const body = {
|
|
719
|
-
if (args.note !== undefined)
|
|
720
|
-
body.note = args.note;
|
|
721
|
+
const body = { required: args.required };
|
|
721
722
|
if (args.effort_level !== undefined)
|
|
722
723
|
body.effort_level = args.effort_level;
|
|
723
724
|
return client.request({
|
package/dist/index.js
CHANGED
|
@@ -363,7 +363,7 @@ server.tool("issue_get",
|
|
|
363
363
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
364
364
|
// ---------------- issue_create ----------------
|
|
365
365
|
server.tool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass "mine" to put the card on THIS session\'s connected plan, or null when it deliberately belongs to no plan. There is no default and no inference — a card that names no plan is one nobody following the work can see, which is why the answer has to be given rather than omitted. "mine" while this session is on no plan is refused (409 session_not_connected) and creates NO card; a plan id is not accepted (a card is only ever created onto your own connected plan). This replaces the plan_add_card follow-up at creation time; plan_add_card remains for putting an EXISTING card on a plan. ' +
|
|
366
|
-
'Create a card via POST /api/issues. Board-scoped; see `board`. type=Epic REQUIRES non-empty phase_children[] (epic and phases inserted in one transaction; children get the epic as parent); other types refuse phase_children[] (400). Status starts at Review. `list_id` places the card straight into a column — a board_lists id or the list\'s display NAME (case-insensitive, emoji-tolerant): a `ready`-type queue lands it in ToDo, a `completed` list in Done, with no follow-up transition. Not valid on Epic; unknown name/id → 400. `
|
|
366
|
+
'Create a card via POST /api/issues. Board-scoped; see `board`. type=Epic REQUIRES non-empty phase_children[] (epic and phases inserted in one transaction; children get the epic as parent); other types refuse phase_children[] (400). Status starts at Review. `list_id` places the card straight into a column — a board_lists id or the list\'s display NAME (case-insensitive, emoji-tolerant): a `ready`-type queue lands it in ToDo, a `completed` list in Done, with no follow-up transition. Not valid on Epic; unknown name/id → 400. `gate_decisions` is REQUIRED when the board has an OPTIONAL quality gate for the card\'s type: a missing one fails closed with 400 `{error, required_gate_decisions:[...]}` naming each gate — retry with one `{gate, enabled, note}` per listed gate. ALWAYS pass `triage_enabled` explicitly on the root card and every phase child: true only when it should enter automatic triage/dispatch without human review; absent → false.', {
|
|
367
367
|
type: z.enum(ISSUE_TYPES),
|
|
368
368
|
title: z.string().min(1).describe(TITLE_DESCRIBE),
|
|
369
369
|
summary: z.string().min(1).optional().describe(SUMMARY_DESCRIBE),
|
|
@@ -380,14 +380,15 @@ server.tool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass
|
|
|
380
380
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
381
381
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
382
382
|
list_id: z.string().min(1).nullable().optional(),
|
|
383
|
-
|
|
383
|
+
gate_decisions: z
|
|
384
384
|
.array(z.object({
|
|
385
385
|
gate: z.string().min(1),
|
|
386
|
-
|
|
386
|
+
enabled: z.boolean(),
|
|
387
|
+
note: z.string(),
|
|
387
388
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
388
389
|
}))
|
|
389
390
|
.optional()
|
|
390
|
-
.describe("
|
|
391
|
+
.describe("One {gate, enabled, note} per board-OPTIONAL gate of the card's type: `enabled` = does it run on this card, `note` = why. Board `required` gates always run and `disabled` never do; neither takes a decision (naming one → 400). Optional `effort_level` overrides a `plan-*` gate's reviewer rung."),
|
|
391
392
|
phase_children: z
|
|
392
393
|
.array(z.object({
|
|
393
394
|
type: z.enum(NON_EPIC_TYPES),
|
|
@@ -400,14 +401,15 @@ server.tool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass
|
|
|
400
401
|
description: z.string().describe(DESCRIPTION_DESCRIBE),
|
|
401
402
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
402
403
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
403
|
-
|
|
404
|
+
gate_decisions: z
|
|
404
405
|
.array(z.object({
|
|
405
406
|
gate: z.string().min(1),
|
|
406
|
-
|
|
407
|
+
enabled: z.boolean(),
|
|
408
|
+
note: z.string(),
|
|
407
409
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
408
410
|
}))
|
|
409
411
|
.optional()
|
|
410
|
-
.describe("Same as the root
|
|
412
|
+
.describe("Same as the root gate_decisions, resolved against THIS child's type."),
|
|
411
413
|
triage_enabled: z
|
|
412
414
|
.boolean()
|
|
413
415
|
.optional()
|
|
@@ -600,7 +602,7 @@ server.tool("issue_retire_branch", "Mark a card's own `card/<id>` branch RETIRED
|
|
|
600
602
|
...boardField,
|
|
601
603
|
}, async (args) => jsonResult(await issueRetireBranch(client, args)));
|
|
602
604
|
// ---------------- issue_quality_gate ----------------
|
|
603
|
-
server.tool("issue_quality_gate", "
|
|
605
|
+
server.tool("issue_quality_gate", "Set one card's per-gate `required` flag via POST /api/issues/:id/quality-gates/:gate {required} — the only post-create way (issue_create takes gate_decisions; issue_edit refuses gate keys). PRE `plan-*` gates run before the work dispatch; POST `code-*` gates block complete. Unknown gate → 400. The board state per gate is tri-state: `required` always runs, `optional` runs WHEN this flag is true (optional is NOT off), `disabled` never runs. Optional `effort_level` overrides a `plan-*` gate's reviewer rung (null clears it). The write always succeeds and returns `{issue, applied: true, effective, reason}` — read `effective` (does the gate now run) and `reason` (why the board overrode your value), not just the 200. Board-scoped; see `board`.", {
|
|
604
606
|
id: z.string().min(1),
|
|
605
607
|
gate: z.enum([
|
|
606
608
|
"plan-dependency",
|
|
@@ -610,18 +612,12 @@ server.tool("issue_quality_gate", "Put one quality gate ON a card, or take it OF
|
|
|
610
612
|
"code-architecture",
|
|
611
613
|
"code-quality",
|
|
612
614
|
]),
|
|
613
|
-
|
|
614
|
-
.enum(["add", "remove"])
|
|
615
|
-
.describe("add = put this gate on the card; remove = take it off."),
|
|
616
|
-
note: z
|
|
617
|
-
.string()
|
|
618
|
-
.optional()
|
|
619
|
-
.describe("Why this gate applies to this card. `add` only."),
|
|
615
|
+
required: z.boolean(),
|
|
620
616
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
621
617
|
...boardField,
|
|
622
618
|
}, async (args) => jsonResult(await issueQualityGate(client, args)));
|
|
623
619
|
// ---------------- issue_quality_gate_verdict ----------------
|
|
624
|
-
server.tool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate VERDICT via PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373). SIBLING of `issue_quality_gate`, not a replacement: that one
|
|
620
|
+
server.tool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate VERDICT via PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373). SIBLING of `issue_quality_gate`, not a replacement: that one flips the per-card `required` FLAG (does this gate run at all), THIS one records the VERDICT (did it pass) — POST vs PATCH on the same resource, neither substitutes for the other. **Use this to close out a card you picked up with `issue_transition pickup {manual:true}`** (DX-946 operator-session self-pickup): `issue_transition complete` REFUSES 409 (`failed_gate: \"quality_gate_post\"`, `failed_post_gates[]`) while any required POST gate (`code-quality` / `code-test-quality` / `code-architecture`) is not `pass`, so without a verdict a manually-claimed card can never reach Done — it strands In Progress and its `conflict_on` / `waiting_on` edges then stall OTHER cards' dispatch. `status`: `pass` | `fail` | `pending` (revert a prior verdict, clears the message). `message` is the accountability record for the override, REQUIRED at >= 20 characters for `pass`/`fail` (shorter → 400), ignored for `pending`. Record the REAL reviewer finding, not a rubber stamp — a human-attributed override, stamped with the operator actor, standing in for a reviewer dispatch. A manual verdict is a PURE row write: no side effects — a manual `fail` never blocks the card and a manual `pass` never releases a dispatch. Unknown gate → 400; bad status → 400; unknown card → 404. Board-scoped; see `board`.", {
|
|
625
621
|
id: z.string().min(1),
|
|
626
622
|
gate: z.enum([
|
|
627
623
|
"plan-dependency",
|
|
@@ -815,7 +811,7 @@ async (args) => {
|
|
|
815
811
|
}),
|
|
816
812
|
});
|
|
817
813
|
});
|
|
818
|
-
server.tool("plan_add_record", "Add a goal, rule or caveat to your connected plan (POST /api/plans/mine/records). A GOAL is an outcome the work is measured against. A RULE is a constraint that must hold while it is worked. A CAVEAT is a lasting trade-off or limitation of the ARCHITECTURE — never progress, status or a session note (those are comments on the card). `body` is ONE plain statement of at most 250 characters; the evidence, history and detail go in `context` (markdown). An overlong body is refused with a 400 naming its length. The server allocates a permanent reference (`G-1`, `R-4`, `CAV-12`). Takes no plan id; `session_not_connected` → `plan_connect` first.
|
|
814
|
+
server.tool("plan_add_record", "Add a goal, rule or caveat to your connected plan (POST /api/plans/mine/records). A GOAL is an outcome the work is measured against. A RULE is a constraint that must hold while it is worked. A CAVEAT is a lasting trade-off or limitation of the ARCHITECTURE — never progress, status or a session note (those are comments on the card). `body` is ONE plain statement of at most 250 characters; the evidence, history and detail go in `context` (markdown). An overlong body is refused with a 400 naming its length. The server allocates a permanent reference (`G-1`, `R-4`, `CAV-12`). Takes no plan id; `session_not_connected` → `plan_connect` first. DX-3072 — returns the created record plus `records_count` (that kind's live count, not the whole list, which can grow unboundedly over a plan's life); read the list itself with `plan_get({fields:[\"records:<kind>\"]})` or the dedicated GET.", {
|
|
819
815
|
kind: z.enum(["goal", "rule", "caveat"]).describe("goal = outcome, rule = constraint, caveat = architecture trade-off."),
|
|
820
816
|
body: z.string().min(1).describe("One plain statement, at most 250 characters. Details go in `context`."),
|
|
821
817
|
context: z.string().optional().describe("Markdown detail behind the statement: evidence, history, examples."),
|
|
@@ -823,7 +819,7 @@ server.tool("plan_add_record", "Add a goal, rule or caveat to your connected pla
|
|
|
823
819
|
server.tool("plan_get_record", "Read one goal/rule/caveat of your connected plan (GET /api/plans/mine/records/:rid) without pulling the whole plan. Takes no plan id; an unknown or another plan's record id → 404. Returns `{record: {id, planId, kind, refNum, ref, body, context, contentHash, createdAt, updatedAt}}` — `context` is the markdown detail, or null.", {
|
|
824
820
|
record_id: z.number().int().positive().describe("A record id, from `plan_add_record`, `plan_get` or `plan_get_record` itself."),
|
|
825
821
|
}, async (args) => jsonResult(await planGetRecord(client, args)));
|
|
826
|
-
server.tool("plan_update_record", 'Edit a goal/rule/caveat of your connected plan (PATCH /api/plans/mine/records/:rid). The reference never moves. `body` stays ONE plain statement of at most 250 characters (400 otherwise); detail belongs in markdown `context`. `content_hash` must be the record\'s `contentHash` from your last read, and covers body AND context. On a mismatch nothing is written and you get `{error: "stale_plan_record", currentHash, currentBody, currentContext}`: merge into those and retry with `content_hash: currentHash`, never blindly. Takes no plan id.
|
|
822
|
+
server.tool("plan_update_record", 'Edit a goal/rule/caveat of your connected plan (PATCH /api/plans/mine/records/:rid). The reference never moves. `body` stays ONE plain statement of at most 250 characters (400 otherwise); detail belongs in markdown `context`. `content_hash` must be the record\'s `contentHash` from your last read, and covers body AND context. On a mismatch nothing is written and you get `{error: "stale_plan_record", currentHash, currentBody, currentContext}`: merge into those and retry with `content_hash: currentHash`, never blindly. Takes no plan id. DX-3072 — returns the edited record plus `records_count` (that kind\'s live count, not the whole list — see `plan_add_record`).', {
|
|
827
823
|
record_id: z.number().int().positive().describe("The record id to edit."),
|
|
828
824
|
content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
|
|
829
825
|
body: z.string().min(1).describe("The new statement, at most 250 characters. Plain text."),
|
|
@@ -833,11 +829,11 @@ server.tool("plan_update_record", 'Edit a goal/rule/caveat of your connected pla
|
|
|
833
829
|
.optional()
|
|
834
830
|
.describe("New markdown detail. Omit to keep the stored context; null clears it."),
|
|
835
831
|
}, async (args) => jsonResult(await planUpdateRecord(client, args)));
|
|
836
|
-
server.tool("plan_delete_record", 'Soft-delete a goal/rule/caveat of your connected plan (DELETE /api/plans/mine/records/:rid). Its reference is retired permanently, never reused. `content_hash` must be the record\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_record", currentHash, currentBody, currentContext}`. Takes no plan id. Unknown or already-deleted id → 404.
|
|
832
|
+
server.tool("plan_delete_record", 'Soft-delete a goal/rule/caveat of your connected plan (DELETE /api/plans/mine/records/:rid). Its reference is retired permanently, never reused. `content_hash` must be the record\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_record", currentHash, currentBody, currentContext}`. Takes no plan id. Unknown or already-deleted id → 404. DX-3072 — returns `{records_count}`, that kind\'s remaining live count, not the whole list (see `plan_add_record`).', {
|
|
837
833
|
record_id: z.number().int().positive().describe("The record id to delete."),
|
|
838
834
|
content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
|
|
839
835
|
}, async (args) => jsonResult(await planDeleteRecord(client, args)));
|
|
840
|
-
server.tool("plan_add_note", "Write a milestone note to a plan's timeline, via POST /api/plans/:plan_id/notes (DX-2915). A note is a MILESTONE, not a log — write one for a card (or related group of cards) finishing, an important decision, or a meaningful goal/rule/caveat/architecture-section change; routine step progress stays a card comment, never a note. Terse tone: `title` at most 60 characters, `body` (the wrap-up) at most 250 (both 400 if too long, naming the limit and actual length). Links resolve on read into what they point at (a card's title, a record's ref+body, a section's title): an unknown card, an unparseable or foreign record ref, or an unknown or foreign section id is refused 400 naming exactly which one. A card link does NOT require the card to be a member of this plan; a record/section link MUST belong to THIS plan. `author` is stamped from your identity server-side — there is no field for it. TAKES AN EXPLICIT `plan_id` (like `plan_remove_card`/`plan_rename`), so a dispatched worker with no plan connection can still write. Unknown plan → 404.
|
|
836
|
+
server.tool("plan_add_note", "Write a milestone note to a plan's timeline, via POST /api/plans/:plan_id/notes (DX-2915). A note is a MILESTONE, not a log — write one for a card (or related group of cards) finishing, an important decision, or a meaningful goal/rule/caveat/architecture-section change; routine step progress stays a card comment, never a note. Terse tone: `title` at most 60 characters, `body` (the wrap-up) at most 250 (both 400 if too long, naming the limit and actual length). Links resolve on read into what they point at (a card's title, a record's ref+body, a section's title): an unknown card, an unparseable or foreign record ref, or an unknown or foreign section id is refused 400 naming exactly which one. A card link does NOT require the card to be a member of this plan; a record/section link MUST belong to THIS plan. `author` is stamped from your identity server-side — there is no field for it. TAKES AN EXPLICIT `plan_id` (like `plan_remove_card`/`plan_rename`), so a dispatched worker with no plan connection can still write. Unknown plan → 404. DX-3072 — returns the new note plus `notes_count` (the plan's total live note count, not the latest page); read the timeline itself with `plan_get({fields:[\"notes\"]})` or the dedicated GET.", {
|
|
841
837
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
842
838
|
title: z.string().min(1).describe("At most 60 characters."),
|
|
843
839
|
body: z.string().min(1).describe("The wrap-up, at most 250 characters."),
|
|
@@ -848,7 +844,7 @@ server.tool("plan_add_note", "Write a milestone note to a plan's timeline, via P
|
|
|
848
844
|
.describe("Goal/rule/caveat references this note announces, e.g. `[\"G-1\", \"R-3\", \"CAV-2\"]`."),
|
|
849
845
|
section_ids: z.array(z.number().int().positive()).optional().describe("Architecture section ids this note announces."),
|
|
850
846
|
}, async (args) => jsonResult(await planAddNote(client, args)));
|
|
851
|
-
server.tool("plan_update_note", 'Edit a plan note, via PATCH /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` MUST be the note\'s `contentHash` from your last read; a mismatch writes NOTHING and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — merge into those and retry with `content_hash: currentHash`, never blindly. `title`/`body` are each optional and keep their stored value when omitted. The link fields (`card_ids`/`record_refs`/`section_ids`) are all-or-nothing AS A GROUP: omit all three to leave the stored link set untouched; send ANY one of them to REPLACE THE WHOLE SET — there is no per-link add/remove. The hash covers title, body AND the link set. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan or note id → 404.
|
|
847
|
+
server.tool("plan_update_note", 'Edit a plan note, via PATCH /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` MUST be the note\'s `contentHash` from your last read; a mismatch writes NOTHING and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — merge into those and retry with `content_hash: currentHash`, never blindly. `title`/`body` are each optional and keep their stored value when omitted. The link fields (`card_ids`/`record_refs`/`section_ids`) are all-or-nothing AS A GROUP: omit all three to leave the stored link set untouched; send ANY one of them to REPLACE THE WHOLE SET — there is no per-link add/remove. The hash covers title, body AND the link set. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan or note id → 404. DX-3072 — returns the edited note plus `notes_count` (see `plan_add_note`), not the latest page.', {
|
|
852
848
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
853
849
|
note_id: z.number().int().positive().describe("The note id to edit."),
|
|
854
850
|
content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
|
|
@@ -858,15 +854,15 @@ server.tool("plan_update_note", 'Edit a plan note, via PATCH /api/plans/:plan_id
|
|
|
858
854
|
record_refs: z.array(z.string().min(1)).optional().describe("REPLACES the whole link set when sent (with card_ids/section_ids)."),
|
|
859
855
|
section_ids: z.array(z.number().int().positive()).optional().describe("REPLACES the whole link set when sent (with card_ids/record_refs)."),
|
|
860
856
|
}, async (args) => jsonResult(await planUpdateNote(client, args)));
|
|
861
|
-
server.tool("plan_delete_note", 'Soft-delete a plan note, via DELETE /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` must be the note\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — the same shape `plan_update_note` uses. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan, unknown note, or an already-deleted note → 404.
|
|
857
|
+
server.tool("plan_delete_note", 'Soft-delete a plan note, via DELETE /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` must be the note\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — the same shape `plan_update_note` uses. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan, unknown note, or an already-deleted note → 404. DX-3072 — returns `{notes_count}`, the plan\'s remaining live note count, not the latest page.', {
|
|
862
858
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
863
859
|
note_id: z.number().int().positive().describe("The note id to delete."),
|
|
864
860
|
content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
|
|
865
861
|
}, async (args) => jsonResult(await planDeleteNote(client, args)));
|
|
866
|
-
server.tool("plan_add_card", "Add an existing card to the plan this session is connected to, via POST /api/plans/mine/cards (DX-2683). The card may live on ANY board — that is what a plan is for. Idempotent: re-adding a card already on the plan is a no-op, not an error, and a card may sit in several plans at once. This adds MEMBERSHIP only; it never edits the card. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404.
|
|
862
|
+
server.tool("plan_add_card", "Add an existing card to the plan this session is connected to, via POST /api/plans/mine/cards (DX-2683). The card may live on ANY board — that is what a plan is for. Idempotent: re-adding a card already on the plan is a no-op, not an error, and a card may sit in several plans at once. This adds MEMBERSHIP only; it never edits the card. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. DX-3072 — returns `{card_id, member: true, cards_count}` (the attachment's own confirmation plus the plan's total member-card count), not the full member list — a plan can hold hundreds of cards, and the whole list was a ~500KB reply that a caller could not read. Read the list itself with `plan_get({fields:[\"cards\"]})` (paged) when you actually need it.", {
|
|
867
863
|
card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
|
|
868
864
|
}, async (args) => jsonResult(await planAddCard(client, args)));
|
|
869
|
-
server.tool("plan_remove_card", "Remove a card from a plan via DELETE /api/plans/:plan_id/cards/:card_id (DX-2740) — the sibling of `plan_add_card`. The card may live on ANY board. Idempotent: removing a card that was never a member is a no-op, not an error — the same idempotent-toggle contract `issue_dependency` add/remove uses. This removes MEMBERSHIP only; it never edits or deletes the card itself, and its membership in every OTHER plan is untouched. Unlike `plan_add_card`, this takes an EXPLICIT `plan_id` rather than acting on your connected session's plan — you may remove a card from any plan you can name. Unknown plan → 404.
|
|
865
|
+
server.tool("plan_remove_card", "Remove a card from a plan via DELETE /api/plans/:plan_id/cards/:card_id (DX-2740) — the sibling of `plan_add_card`. The card may live on ANY board. Idempotent: removing a card that was never a member is a no-op, not an error — the same idempotent-toggle contract `issue_dependency` add/remove uses. This removes MEMBERSHIP only; it never edits or deletes the card itself, and its membership in every OTHER plan is untouched. Unlike `plan_add_card`, this takes an EXPLICIT `plan_id` rather than acting on your connected session's plan — you may remove a card from any plan you can name. Unknown plan → 404. DX-3072 — returns `{card_id, member: false, cards_count}`, the plan's remaining member-card count, not the full list (see `plan_add_card`).", {
|
|
870
866
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
871
867
|
card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
|
|
872
868
|
}, async (args) => jsonResult(await planRemoveCard(client, args)));
|
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.98",
|
|
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",
|