@thehammer/danx-dashboard-mcp 0.1.152 → 0.1.156

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
@@ -21,7 +21,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
21
21
 
22
22
  | Tool | HTTP | Notes |
23
23
  |---|---|---|
24
- | `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
24
+ | `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset`. `fields` (DX-3427) is the SAME recursive field tree `issue_get` takes, against the same issue resource — list-row fields (`ac_total`, `comments_count`, `waiting_on`, `quality_gates`, `next_pre_gate`, …) sit alongside every detail field. See `resource_fields` |
25
25
  | `issue_get` | `GET /api/issues/:id` or `GET /api/issues/batch` | Pass `id` for one card, or `ids[]` (DX-2727, at most 100) to resolve many across boards in ONE call — global, so `ids` with `board` throws; per-id `not_found` rather than a whole-call 404. Minimal scalars by default; `fields` (DX-3426) opts in via a recursive field TREE — `{"description": true, "problems": {"solutions": true}, "comments": {"limit": 10}}` — not a flat group list; a cursor-paged relation (today only `comments`) takes its page args (`limit`/`before`) inside its own nested object, so paging now works on the batch form too. See `resource_fields` |
26
26
  | `resource_fields` | `GET /api/resources/:resource/fields` | DX-3426 — what an `issue_get`-style field tree may name for one resource: `{resource, description, always, hashes, fields, relations}`. Install-global, no `board`. `resource: "issue"` is the root; a relation's own `resource` in the response is what to call this again with, one level deeper |
27
27
  | `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` |
package/dist/handlers.js CHANGED
@@ -25,19 +25,21 @@ import { readFile } from "node:fs/promises";
25
25
  import { basename, extname, isAbsolute } from "node:path";
26
26
  import { resolvePriority } from "./priority.js";
27
27
  /**
28
- * `filter`/`fields`/`sort` are JSON/CSV-encoded onto the query string (the
29
- * route parses `?filter=<JSON>`, `?fields=a,b`, `?sort=<JSON>` — see
30
- * `src/issues/read/reader.ts#parseEnvelope`); each is omitted entirely when
31
- * empty so the wire carries no `{}`/`[]` noise. `board` stays a flat
32
- * top-level param, resolved by the HTTP client exactly as before.
28
+ * `filter`/`fields`/`sort` are JSON-encoded onto the query string (the route
29
+ * parses `?filter=<JSON>`, `?fields=<JSON>`, `?sort=<JSON>` — see
30
+ * `src/issues/read/reader.ts#parseEnvelope` + `handleGetIssues`); each is
31
+ * omitted entirely when empty so the wire carries no `{}`/`[]` noise. `board`
32
+ * stays a flat top-level param, resolved by the HTTP client exactly as
33
+ * before.
33
34
  */
34
35
  export async function issueList(client, args) {
35
36
  const query = {};
36
37
  if (args.filter !== undefined && Object.keys(args.filter).length > 0) {
37
38
  query.filter = JSON.stringify(args.filter);
38
39
  }
39
- if (args.fields !== undefined && args.fields.length > 0) {
40
- query.fields = args.fields.join(",");
40
+ // DX-3427 — a JSON-encoded field tree, mirroring `issue_get` below.
41
+ if (args.fields !== undefined && Object.keys(args.fields).length > 0) {
42
+ query.fields = JSON.stringify(args.fields);
41
43
  }
42
44
  if (args.sort !== undefined && args.sort.length > 0) {
43
45
  query.sort = JSON.stringify(args.sort);
@@ -124,47 +126,11 @@ export async function issueGet(client, args) {
124
126
  board: args.board,
125
127
  });
126
128
  }
127
- // ---------------- repo_knowledge_get / repo_knowledge_set ----------------
128
- const REPO_KNOWLEDGE_BASE_PATH = "/api/repo-knowledge";
129
- /**
130
- * Fetch the board's working-knowledge doc via GET /api/repo-knowledge
131
- * (DX-1128, Story 2). Mirrors `issueGet` — a bare board-scoped GET, no id
132
- * (the doc is 1-per-board). Board resolves the same way every other tool's
133
- * `board` arg does: per-call override, else the dispatch's env-derived board.
134
- */
135
- export async function repoKnowledgeGet(client, args = {}) {
136
- return client.request({
137
- method: "GET",
138
- path: "",
139
- basePath: REPO_KNOWLEDGE_BASE_PATH,
140
- board: args.board,
141
- });
142
- }
143
- /**
144
- * Write the board's working-knowledge doc via PUT /api/repo-knowledge
145
- * (DX-1128, Story 2). Mirrors `issueEdit`'s shape (a PATCH-like body write)
146
- * but targets the repo-knowledge route family, not `/api/issues`. The
147
- * server's optimistic-concurrency guard rejects a stale `base_hash` — the
148
- * refusal envelope (`{ok: false, body: {error, currentHash}}`) passes
149
- * through verbatim so the caller can re-get, re-merge, and retry.
150
- */
151
- export async function repoKnowledgeSet(client, args) {
152
- const { board, ...body } = args;
153
- return client.request({
154
- method: "PUT",
155
- path: "",
156
- basePath: REPO_KNOWLEDGE_BASE_PATH,
157
- body,
158
- board,
159
- });
160
- }
161
129
  // ---------------- brief_list / brief_get_page / brief_set_page ----------------
162
130
  const BRIEF_BASE_PATH = "/api/brief";
163
131
  /**
164
132
  * List the board's named Brief pages via GET /api/brief (DX-2083).
165
- * Mirrors `repoKnowledgeGet`'s shape (a bare board-scoped GET, no id) but
166
- * targets the list-shaped sibling surface — many named pages per board,
167
- * not one document.
133
+ * A bare board-scoped GET, no id — many named pages per board.
168
134
  */
169
135
  export async function briefList(client, args = {}) {
170
136
  return client.request({
@@ -177,7 +143,7 @@ export async function briefList(client, args = {}) {
177
143
  /**
178
144
  * Fetch one Brief page by slug via GET /api/brief/page?slug=
179
145
  * (DX-2083). A missing row (including a not-yet-created page) reads as the
180
- * same empty-view convention `repoKnowledgeGet` uses — never a 404.
146
+ * empty view — never a 404.
181
147
  */
182
148
  export async function briefGetPage(client, args) {
183
149
  return client.request({
@@ -190,10 +156,9 @@ export async function briefGetPage(client, args) {
190
156
  }
191
157
  /**
192
158
  * Write one Brief page via PUT /api/brief/page?slug= (DX-2083).
193
- * Mirrors `repoKnowledgeSet`'s shape (optimistic-concurrency `base_hash`,
194
- * verbatim 409 passthrough on a stale write) but targets one named page
195
- * instead of the board's single working-knowledge doc. `slug` rides the
196
- * query string (matching the route), never the body.
159
+ * Optimistic-concurrency `base_hash` with a verbatim 409 passthrough on a
160
+ * stale write. `slug` rides the query string (matching the route), never
161
+ * the body.
197
162
  */
198
163
  export async function briefSetPage(client, args) {
199
164
  const { slug, board, ...body } = args;
package/dist/index.js CHANGED
@@ -34,8 +34,6 @@
34
34
  * - issue_attach POST /api/issues/:id/attachments (reads a local file)
35
35
  * - quality_gate_instruction
36
36
  * GET /api/quality-gates/:gate/instruction (DX-3341, not card-scoped)
37
- * - repo_knowledge_get GET /api/repo-knowledge
38
- * - repo_knowledge_set PUT /api/repo-knowledge (DX-1128, Story 2)
39
37
  * - brief_list GET /api/brief
40
38
  * - brief_get_page GET /api/brief/page (DX-2083 / DX-2484)
41
39
  * - brief_set_page PUT /api/brief/page (DX-2083 / DX-2484)
@@ -110,7 +108,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
110
108
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
111
109
  import { z } from "zod";
112
110
  import { DashboardHttpClient } from "./http-client.js";
113
- import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_EVENT_KINDS, PLAN_EVENT_ORIGINS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, PLAN_GET_EVENTS_DEFAULT_LIMIT, PLAN_GET_EVENTS_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, qualityGateInstruction, resourceFields, } from "./handlers.js";
111
+ import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_EVENT_KINDS, PLAN_EVENT_ORIGINS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, PLAN_GET_EVENTS_DEFAULT_LIMIT, PLAN_GET_EVENTS_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, qualityGateInstruction, resourceFields, } from "./handlers.js";
114
112
  import { PRIORITY_TIER_WORDS } from "./priority.js";
115
113
  import { fieldTreeSchema } from "./field-tree.js";
116
114
  function readEnvOrDie(name) {
@@ -277,25 +275,6 @@ const EFFORT_VALUES = [
277
275
  // still be CREATED through this MCP.
278
276
  const ISSUE_TYPES = ["Epic", "Bug", "Feature", "Story", "Chore", "Task"];
279
277
  const NON_EPIC_TYPES = ["Bug", "Feature", "Story", "Chore", "Task"];
280
- // DX-935 / DX-937 — field-group taxonomy for the LIST read envelope,
281
- // hand-copied from `src/issues/read/field-groups.ts` (`LIST_GROUPS` — this
282
- // package cannot import server source). Drift surfaces at runtime as a server
283
- // 400, not silently. DX-3426 moved `issue_get` to the field tree
284
- // (`field-tree.ts`); DX-3427 moves the list the same way and deletes this.
285
- const LIST_FIELD_GROUPS = [
286
- "description",
287
- // DX-2735: replaced the flat "solutions" group (hard cut, no alias).
288
- "problems",
289
- "ac",
290
- "comments",
291
- "retro",
292
- "dependencies",
293
- "triage",
294
- "assignment",
295
- "quality_gates",
296
- "children",
297
- "effort",
298
- ];
299
278
  const SORT_ORDERS = ["asc", "desc"];
300
279
  const sortField = z
301
280
  .array(z.object({
@@ -360,7 +339,7 @@ const MARKDOWN_STYLE_DESCRIBE = "Renders as markdown here. Use `##`/`###` header
360
339
  strictTool("issue_list",
361
340
  // DX-2735: trimmed to pay for the problem tools inside the work-profile
362
341
  // injected-surface budget — same facts, no repeated prose.
363
- "List cards. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins; opt into heavy data with `fields` groups: description (+ summary), problems (open_problem_count — a card needs a human exactly when this is > 0), ac, comments, retro, dependencies, triage, assignment, quality_gates, children, effort. `sort`: [{column, order}] over id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at (default order: see `sort`'s own field description). `limit`/`offset` page (uncapped by default). `include_closed` defaults to FALSE: a bare call silently excludes every Done/Cancelled card (leaf AND container alike). The response always carries `total` (the full count matching every filter except limit/offset — compare against `issues.length` to tell an exhausted list from a truncated one) and, whenever `include_closed` was not explicitly `true`, `closed_excluded` (how many additional terminal cards the default withheld — re-call with `include_closed: true` to see them). issue_get reads one card in full.", {
342
+ "List cards. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins; `fields` opts into more — the SAME recursive field-tree JSON `issue_get` takes, against the SAME issue resource (list-row fields like `ac_total`/`comments_count`/`waiting_on`/`quality_gates`/`next_pre_gate` sit alongside every detail field `issue_get` can name). Call `resource_fields({resource:\"issue\"})` for the full, current list of what `fields` may name — never guess a name; unknown → 400 `unknown_field`. `sort`: [{column, order}] over id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at (default order: see `sort`'s own field description). `limit`/`offset` page (uncapped by default). `include_closed` defaults to FALSE: a bare call silently excludes every Done/Cancelled card (leaf AND container alike). The response always carries `total` (the full count matching every filter except limit/offset — compare against `issues.length` to tell an exhausted list from a truncated one) and, whenever `include_closed` was not explicitly `true`, `closed_excluded` (how many additional terminal cards the default withheld — re-call with `include_closed: true` to see them). issue_get reads one card in full.", {
364
343
  filter: z
365
344
  .object({
366
345
  q: z.string().optional(),
@@ -381,10 +360,12 @@ strictTool("issue_list",
381
360
  // its own `.strict()`.
382
361
  .strict()
383
362
  .optional(),
384
- fields: z
385
- .array(z.enum(LIST_FIELD_GROUPS))
363
+ // DX-3427 — a field TREE, exactly like `issue_get`'s `fields` below
364
+ // (against the same shared issue resource) — replaces the retired
365
+ // CSV field-GROUP list.
366
+ fields: fieldTreeSchema
386
367
  .optional()
387
- .describe("Field groups to add; absent = minimal scalars."),
368
+ .describe("A field tree (see tool description); absent/empty = minimal scalars. `resource_fields({resource:\"issue\"})` names every valid key."),
388
369
  sort: sortField,
389
370
  limit: z.number().int().positive().max(LIST_PAGE_MAX_LIMIT).optional(),
390
371
  offset: z.number().int().nonnegative().optional(),
@@ -891,21 +872,8 @@ strictTool("issue_attach", "Attach a LOCAL file to an issue card: `id` (the card
891
872
  .describe("Absolute path to a local file on the dispatch's shared filesystem (must start with `/`)."),
892
873
  ...boardField,
893
874
  }, async (args) => jsonResult(await issueAttach(client, args)));
894
- // ---------------- repo_knowledge_get ----------------
895
- strictTool("repo_knowledge_get", "Fetch the board's working-knowledge markdown doc. Board-scoped; see `board`. Returns `{ok, status, body: {content, contentHash, updatedAt, updatedBy, boardId}}` — an unset doc reads as the empty view (content/contentHash `\"\"`), not a 404. Before `repo_knowledge_set`, always get immediately first and pass `contentHash` back as `base_hash` — the concurrency guard rejects a stale write.", {
896
- ...boardField,
897
- }, async (args) => jsonResult(await repoKnowledgeGet(client, args)));
898
- // ---------------- repo_knowledge_set ----------------
899
- strictTool("repo_knowledge_set", 'Write the board\'s working-knowledge markdown doc. Board-scoped; see `board`. `base_hash` must be the `contentHash` from the immediately-prior `repo_knowledge_get` ("" for the true first write). On mismatch, fails loud with `{error: "stale_repo_knowledge", currentHash}` rather than overwriting — re-get, re-merge, retry with the new hash. On success persists to the DB, publishes `repo-knowledge:updated` over SSE, and returns the new view.', {
900
- content: z.string(),
901
- base_hash: z
902
- .string()
903
- .optional()
904
- .describe('The contentHash last read via repo_knowledge_get ("" for a true first write). Omitted also normalizes to "" server-side, so it only succeeds against an empty/absent doc — always get immediately before set.'),
905
- ...boardField,
906
- }, async (args) => jsonResult(await repoKnowledgeSet(client, args)));
907
875
  // ---------------- brief_list ----------------
908
- strictTool("brief_list", "List the board's named Brief pages. Board-scoped; see `board`. Returns `{boardId, pages: [{slug, title, contentHash, sortOrder, updatedAt, updatedBy}]}` — metadata only, no content (use `brief_get_page`). Unlike `repo_knowledge_get`/`_set` (one board-level doc), Brief pages are MANY named pages per board (Goals/Architecture/Rules/Caveats tabs), keyed by (board, slug). The reserved `index` slug always exists.", {
876
+ strictTool("brief_list", "List the board's named Brief pages. Board-scoped; see `board`. Returns `{boardId, pages: [{slug, title, contentHash, sortOrder, updatedAt, updatedBy}]}` — metadata only, no content (use `brief_get_page`). Brief pages are MANY named pages per board (Goals/Architecture/Rules/Caveats tabs), keyed by (board, slug). The reserved `index` slug always exists.", {
909
877
  ...boardField,
910
878
  }, async (args) => jsonResult(await briefList(client, args)));
911
879
  // ---------------- brief_get_page ----------------
@@ -917,7 +885,7 @@ strictTool("brief_get_page", 'Fetch one Brief page by slug. Board-scoped; see `b
917
885
  ...boardField,
918
886
  }, async (args) => jsonResult(await briefGetPage(client, args)));
919
887
  // ---------------- brief_set_page ----------------
920
- strictTool("brief_set_page", 'Write one Brief page. Board-scoped; see `board`. Body: `{content, title?, sortOrder?, base_hash?}` — same optimistic-concurrency shape as `repo_knowledge_set`, targeting one named page. `base_hash` must be the `contentHash` from the immediately-prior `brief_get_page` (`""` for a true first write). On mismatch fails loud with `{error: "stale_brief_page", currentHash}` — re-get, re-merge, retry, never overwrite blindly. On success persists, publishes `brief:updated` over SSE, and returns the new view. No delete tool on this surface; the reserved `index` slug is never deletable — removing a non-index page is dashboard-UI-only.', {
888
+ strictTool("brief_set_page", 'Write one Brief page. Board-scoped; see `board`. Body: `{content, title?, sortOrder?, base_hash?}` — optimistic-concurrency, targeting one named page. `base_hash` must be the `contentHash` from the immediately-prior `brief_get_page` (`""` for a true first write). On mismatch fails loud with `{error: "stale_brief_page", currentHash}` — re-get, re-merge, retry, never overwrite blindly. On success persists, publishes `brief:updated` over SSE, and returns the new view. No delete tool on this surface; the reserved `index` slug is never deletable — removing a non-index page is dashboard-UI-only.', {
921
889
  slug: z
922
890
  .string()
923
891
  .min(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.152",
3
+ "version": "0.1.156",
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",