@thehammer/danx-dashboard-mcp 0.1.59 → 0.1.61

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 CHANGED
@@ -22,7 +22,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
22
22
  | Tool | HTTP | Notes |
23
23
  |---|---|---|
24
24
  | `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
25
- | `issue_get` | `GET /api/issues/:id` | Returns hydrated card + ancestor chain |
25
+ | `issue_get` | `GET /api/issues/:id` or `GET /api/issues/batch` | Pass `id` for one card, or `ids[]` (DX-2727) to resolve many across boards in ONE call — global, no board scope, per-id `not_found` rather than a whole-call 404. Minimal scalars by default; `fields` opts in |
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. A successful `block` also returns `solutions_reminder: {solution_count, instruction}` |
@@ -33,6 +33,10 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
33
33
  | `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them. A successful set also returns `solutions_reminder: {solution_count, instruction}` |
34
34
  | `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
35
35
 
36
+ ## `plan_get` — cheap by default, opt-in for the rest (DX-2727)
37
+
38
+ A bare `plan_get` (no `fields`) returns only the plan's cheap scalars — `plan`, `boards`, `cardCount`, `bucketCounts`, `session`, `sessionListenerAttached` — plus `available_field_groups` naming what else exists. Pass `fields` to opt into `cards` (capped at 200 members, with `cards_total` reporting the true count so a truncation is never silent), `records` (every goal/rule/caveat) or `records:goal` / `records:rule` / `records:caveat` (just one kind), `architecture`, and `sessions`. A `plan_get` made only to grab a hash before a one-line edit no longer pays for the architecture document or every member card.
39
+
36
40
  ## `listen` — a working session's event listener
37
41
 
38
42
  `plan_connect` connects the session to a plan AND returns `listener: {command, persistent: true, instruction}`. The agent arms `command` with Claude Code's Monitor tool (`persistent: true`); every line the command prints becomes a notification that wakes the session.
package/dist/handlers.js CHANGED
@@ -49,11 +49,35 @@ export async function issueList(client, args) {
49
49
  // DX-1171 — board-only: forward the qualified board id, no repo.
50
50
  return client.request({ method: "GET", path: "", query, board: args.board });
51
51
  }
52
+ /**
53
+ * Fetch one card via `GET /api/issues/:id`, or many via
54
+ * `GET /api/issues/batch?ids=...` (DX-2727) — a real batch read that keeps
55
+ * the single-id route's global, cross-board resolution rather than routing
56
+ * through `issue_list` (board-scoped per call, and its filter has no `id`
57
+ * property). Exactly one of `id` / `ids` must be given; the batch form
58
+ * reports a per-id `not_found` list rather than failing the whole call, and
59
+ * ignores `board` — the same no-scope contract the single-id form already has.
60
+ */
52
61
  export async function issueGet(client, args) {
62
+ if (args.id !== undefined && args.ids !== undefined) {
63
+ throw new Error("issue_get: pass exactly one of id or ids, not both");
64
+ }
53
65
  const query = {};
54
66
  if (args.fields !== undefined && args.fields.length > 0) {
55
67
  query.fields = args.fields.join(",");
56
68
  }
69
+ if (args.ids !== undefined) {
70
+ if (args.ids.length === 0) {
71
+ throw new Error("issue_get: ids must be a non-empty array");
72
+ }
73
+ query.ids = args.ids.join(",");
74
+ // DX-2727 — the batch route resolves globally, exactly like the
75
+ // single-id path (see its own doc comment): no `board` is forwarded.
76
+ return client.request({ method: "GET", path: "/batch", query });
77
+ }
78
+ if (args.id === undefined) {
79
+ throw new Error("issue_get: pass exactly one of id or ids");
80
+ }
57
81
  return client.request({
58
82
  method: "GET",
59
83
  path: `/${encodeURIComponent(args.id)}`,
@@ -709,16 +733,22 @@ export async function planList(client) {
709
733
  return client.request({ method: "GET", path: "", basePath: PLANS_BASE_PATH });
710
734
  }
711
735
  /**
712
- * One plan, entire — its cards, the boards they cover, its goals + rules +
713
- * caveats, its architecture sections, and the sessions working on it. ONE
714
- * call rather than five, which is what keeps this tool surface small enough
715
- * to be worth an agent's context.
736
+ * One plan — its cheap scalars by default, or opt into its cards, its goals
737
+ * + rules + caveats, its architecture sections, and the sessions working on
738
+ * it via `fields` (DX-2727). One call rather than several, which is what
739
+ * keeps this tool surface small enough to be worth an agent's context even
740
+ * once every part of a plan is opt-in rather than always-on.
716
741
  */
717
742
  export async function planGet(client, args = {}) {
743
+ const query = {};
744
+ if (args.fields !== undefined && args.fields.length > 0) {
745
+ query.fields = args.fields.join(",");
746
+ }
718
747
  return client.request({
719
748
  method: "GET",
720
749
  path: args.plan_id === undefined ? "/mine" : `/${args.plan_id}/full`,
721
750
  basePath: PLANS_BASE_PATH,
751
+ query,
722
752
  });
723
753
  }
724
754
  function readIssuedTicket(body) {
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * through the workspace `mcp.template.json`):
13
13
  *
14
14
  * - issue_list GET /api/issues
15
- * - issue_get GET /api/issues/:id
15
+ * - issue_get GET /api/issues/:id | /api/issues/batch (DX-2727 ids[] batch)
16
16
  * - issue_create POST /api/issues
17
17
  * - issue_edit PATCH /api/issues/:id/edit
18
18
  * - issue_transition POST /api/issues/:id/transition
@@ -33,7 +33,7 @@
33
33
  * - brief_get_page GET /api/brief/page (DX-2083 / DX-2484)
34
34
  * - brief_set_page PUT /api/brief/page (DX-2083 / DX-2484)
35
35
  * - plan_list GET /api/plans (DX-2683)
36
- * - plan_get GET /api/plans/:id/full | /api/plans/mine
36
+ * - plan_get GET /api/plans/:id/full | /api/plans/mine (DX-2727: bare = scalars only, fields= opts in)
37
37
  * - plan_create POST /api/plans
38
38
  * - plan_connect POST /api/plan-sessions/me/plan
39
39
  * - plan_add_record POST /api/plans/mine/records
@@ -231,6 +231,18 @@ const GET_FIELD_GROUPS = [
231
231
  "mirrors",
232
232
  "code_review_items",
233
233
  ];
234
+ // DX-2727 — `plan_get`'s field-group taxonomy, hand-copied from
235
+ // `src/issues/plan-field-groups.ts` (this package cannot import server
236
+ // source). `records` is every kind; `records:<kind>` narrows to just one.
237
+ const PLAN_FIELD_GROUPS = [
238
+ "cards",
239
+ "records",
240
+ "records:goal",
241
+ "records:rule",
242
+ "records:caveat",
243
+ "architecture",
244
+ "sessions",
245
+ ];
234
246
  const SORT_ORDERS = ["asc", "desc"];
235
247
  const sortField = z
236
248
  .array(z.object({
@@ -307,8 +319,13 @@ server.tool("issue_list", "List issues for the dispatch's board by default via G
307
319
  ...boardField,
308
320
  }, async (args) => jsonResult(await issueList(client, args)));
309
321
  // ---------------- issue_get ----------------
310
- server.tool("issue_get", "Fetch a single issue via GET /api/issues/:id. Board-scoped; defaults to the dispatch's board. Issue ids are globally unique, so this resolves from any dispatch regardless of `board`. DEFAULT RESPONSE IS MINIMAL (DX-935 / DX-937) — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash — DX-2741, the card's optimistic-concurrency token; pass it back as `content_hash` on an `issue_edit` touching title/description/checklists); no joined collections. Pass `fields` to opt into named field-GROUPS: description (full description body + the plain-language summary), solutions (the card's live candidate solutions, each with its content_hash, + every operator answer in decisions[]), ac (acceptance-criteria + checklists model), comments (comments[]), retro (retro good/bad/action_items/commits), dependencies (waiting_on/conflict_on/blocked gate state), triage (triage history + ICE), requires_human (the requires_human gate + steps), assignment (dispatch/assigned_agent/lifecycle timestamps), quality_gates (DX-1177 — one row per registered gate {gate, required, status pending|pass|fail, completed_at, message}; a required PRE gate not yet `pass` pre-empts the work dispatch, and `issue_transition complete` refuses while a required POST gate row != pass), children (child id list + rollups), mirrors (external mirror sync state), code_review_items (code-review findings). Point any heavy read at the matching `fields` entry rather than assuming it's already on the row. 404 envelope on unknown id.", {
311
- id: z.string().min(1),
322
+ server.tool("issue_get", "Fetch one card via GET /api/issues/:id, or MANY via GET /api/issues/batch?ids=... (DX-2727) — pass exactly one of `id` (single) or `ids` (batch, an array). Board-scoped for the single form; defaults to the dispatch's board. Issue ids are globally unique, so BOTH forms resolve from any dispatch regardless of `board` (the batch form ignores `board` entirely — it is a global, cross-board read). DEFAULT RESPONSE IS MINIMAL (DX-935 / DX-937) — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash — DX-2741, the card's optimistic-concurrency token; pass it back as `content_hash` on an `issue_edit` touching title/description/checklists); no joined collections. Pass `fields` to opt into named field-GROUPS, applied per card in the batch form too: description (full description body + the plain-language summary), solutions (the card's live candidate solutions, each with its content_hash, + every operator answer in decisions[]), ac (acceptance-criteria + checklists model), comments (comments[]), retro (retro good/bad/action_items/commits), dependencies (waiting_on/conflict_on/blocked gate state), triage (triage history + ICE), requires_human (the requires_human gate + steps), assignment (dispatch/assigned_agent/lifecycle timestamps), quality_gates (DX-1177 — one row per registered gate {gate, required, status pending|pass|fail, completed_at, message}; a required PRE gate not yet `pass` pre-empts the work dispatch, and `issue_transition complete` refuses while a required POST gate row != pass), children (child id list + rollups), mirrors (external mirror sync state), code_review_items (code-review findings). Point any heavy read at the matching `fields` entry rather than assuming it's already on the row. Single form: 404 envelope on unknown id. Batch form: returns `{issues: [...found cards...], not_found: [...ids that didn't resolve...]}` — an unknown or soft-deleted id never fails the whole call, it just lands in `not_found` alongside every card that DID resolve.", {
323
+ id: z.string().min(1).optional().describe("A single card id. Mutually exclusive with `ids`."),
324
+ ids: z
325
+ .array(z.string().min(1))
326
+ .min(1)
327
+ .optional()
328
+ .describe("Batch form (DX-2727): every requested id, resolved globally in ONE call. Mutually exclusive with `id`. Reports not_found per id rather than failing the whole call."),
312
329
  fields: z
313
330
  .array(z.enum(GET_FIELD_GROUPS))
314
331
  .optional()
@@ -643,13 +660,17 @@ server.tool("brief_set_page", 'Write one Brief page via PUT /api/brief/page?slug
643
660
  // also takes no plan id, but for a different reason: it MAKES a plan rather
644
661
  // than acting on one, so there is no existing plan for an id to name yet.
645
662
  server.tool("plan_list", "List every plan via GET /api/plans (DX-2683), and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped: a plan is a named, dated set of cards an operator assembled by hand, and its cards may come from any repository. Returns `{ok, status, body: {plans: [{id, name, createdAt, cardCount, boards}], session, sessionListenerAttached}}`. `session` is your own registration — `{sessionId, title, planId, planName, firstSeenAt, lastActiveAt}` — or `null` if this process is not running inside a Claude Code session. A `planId` of null means you are connected to no plan: read any plan with `plan_get`, then `plan_connect` to the one you are working on (or ask the operator to connect you from the Plans list). `sessionListenerAttached` says whether your event listener is running: `false` while connected to a plan means you will NOT hear about its cards — call `plan_connect` again and arm the Monitor it returns. NOTE this is NOT the board Brief (`brief_list`), which is a different feature entirely.", {}, async () => jsonResult(await planList(client)));
646
- server.tool("plan_get", "Read one plan WHOLE via GET /api/plans (DX-2683) — its member cards (with the boards they cover), its goals, rules and caveats, its architecture sections, the sessions working on it, and your own session state. One call, not five. Pass `plan_id` to read ANY plan (browsing another plan is useful and changes nothing); OMIT it to read the plan this session is connected to. Omitting it while connected to no plan fails loud with `{error: \"session_not_connected\"}` — connect first. Returns `{plan, cards, boards, records: {goal, rule, caveat} (each `[{id, ref, body, context, contentHash}]` — `context` is markdown detail or null), architecture: {sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}, sessions, session, sessionListenerAttached}` — `sessionListenerAttached: false` while connected means your event listener is not running; call `plan_connect` again and arm the Monitor it returns. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
663
+ server.tool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id` to read ANY plan (browsing another plan is useful and changes nothing); OMIT it to read the plan this session is connected to. Omitting it while connected to no plan fails loud with `{error: \"session_not_connected\"}` — connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, session, sessionListenerAttached, available_field_groups}` — no member cards, no goals/rules/caveats, no architecture body. Pass `fields` to opt into the rest, one call at a time: `cards` (every member card, capped at 200 with `cards_total` reporting the true count so a truncation is never silent), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (just that one kind — cheaper than the full union), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan). `session`/`sessionListenerAttached` (your own connection state) and `available_field_groups` ride EVERY response, gated or not. `sessionListenerAttached: false` while connected means your event listener is not running; call `plan_connect` again and arm the Monitor it returns. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
647
664
  plan_id: z
648
665
  .number()
649
666
  .int()
650
667
  .positive()
651
668
  .optional()
652
669
  .describe("A plan id from `plan_list`. Omit to read the plan this session is connected to."),
670
+ fields: z
671
+ .array(z.enum(PLAN_FIELD_GROUPS))
672
+ .optional()
673
+ .describe("Opt-in field-GROUPS: cards, records (every kind) or records:goal/records:rule/records:caveat (one kind), architecture, sessions. Absent/empty = cheap scalars only (plan, boards, cardCount, bucketCounts) plus session state."),
653
674
  }, async (args) => jsonResult(await planGet(client, args)));
654
675
  server.tool("plan_create", "Create a new, empty plan via POST /api/plans (DX-2531). GLOBAL — a plan is not board-scoped, and this call adds no cards, no records, and no architecture sections; it does NOT connect any session to the new plan (call `plan_connect` separately, exactly as adding a card to a plan is its own separate step). Returns `{ok, status, body: {plan: {id, name, createdAt}}}`. Use the returned `plan.id` with `plan_connect` to start working on it, or with `plan_get({plan_id})` to browse it.", {
655
676
  name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
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",