@thehammer/danx-dashboard-mcp 0.1.9 → 0.1.11

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
@@ -10,7 +10,10 @@ Each tool is a thin envelope over one HTTP route — Zod-validated at the MCP bo
10
10
  |---|---|
11
11
  | `DANXBOT_DASHBOARD_URL` | Dashboard base, e.g. `http://danxbot-dashboard:5555` (inside compose) or `http://localhost:5555` (host) |
12
12
  | `DANXBOT_DISPATCH_TOKEN` | Per-dispatch bearer — server validates via `requireUser`/dispatch-token checks |
13
- | `DANX_REPO_NAME` | Default repo scope appended as `?repo=<name>`; each tool's optional `repo` arg overrides it per-call for cross-repo reads/writes (unknown repo → 404) |
13
+ | `DANX_REPO_NAME` | Repo half of the qualified board id |
14
+ | `DANXBOT_BOARD_NAME` | Board-slug half of the qualified board id (the dispatch injects `board.slug` here) |
15
+
16
+ BOARD-ONLY (DX-1171): board is the first-level concept; repo is DERIVED from board server-side, never passed. At boot the package composes the dispatch's qualified board id `<repo>:<slug>` from `DANX_REPO_NAME` + `DANXBOT_BOARD_NAME` and appends it as `?board=<id>` (and `body.board` on create) to every `/api/issues/*` request. Each tool exposes an optional `board` arg — a FULL qualified id `<repo>:<slug>` (e.g. `platform:the-supply-operations-hub`) — that overrides it per-call for cross-board reads/writes (unknown board → 404). The dashboard owns board→repo resolution; this package never sends `repo`.
14
17
 
15
18
  ## Tool surface
16
19
 
package/dist/handlers.js CHANGED
@@ -24,26 +24,33 @@ export async function issueList(client, args) {
24
24
  query.limit = args.limit;
25
25
  if (args.offset !== undefined)
26
26
  query.offset = args.offset;
27
- return client.request({ method: "GET", path: "", query, repo: args.repo });
27
+ // DX-1163 the agent surface is lean by default: opt the route into the
28
+ // `IssueListRowLeanV2` projection (id/type/title/status/parent_id/
29
+ // assigned_agent/children-ids/priority). The SPA omits this and keeps the
30
+ // rich board row; `issue_get` still returns the full card body.
31
+ query.lean = true;
32
+ // DX-1171 — board-only: forward the qualified board id, no repo.
33
+ return client.request({ method: "GET", path: "", query, board: args.board });
28
34
  }
29
35
  // ---------------- issue_get ----------------
30
36
  export async function issueGet(client, args) {
31
37
  return client.request({
32
38
  method: "GET",
33
39
  path: `/${encodeURIComponent(args.id)}`,
34
- repo: args.repo,
40
+ board: args.board,
35
41
  });
36
42
  }
37
- export async function issueCreate(client, args, defaultRepo) {
38
- // Resolve the target repo ONCE: per-call `args.repo` override (target
39
- // another registered repo) wins, else the dispatch's `DANX_REPO_NAME`.
40
- // `repo` lives in BOTH the query string (via http-client) AND the body
41
- // (server requires it inside the JSON envelope per
42
- // `write/create.ts::validate`). Sending only one fails-loud — the
43
- // duplication is intentional. The dashboard 404s an unknown repo.
44
- const repo = args.repo ?? defaultRepo;
43
+ export async function issueCreate(client, args, defaultBoard) {
44
+ // Resolve the target board ONCE: per-call `args.board` override (a
45
+ // qualified `<repo>:<slug>` id) wins, else the dispatch's env-derived
46
+ // board. `board` lives in BOTH the query string (via http-client) AND
47
+ // the body (the create route reads it from the JSON envelope, as
48
+ // `repo` did pre-DX-1171). Sending only one fails-loud — the
49
+ // duplication is intentional. The dashboard owns board→repo and 404s
50
+ // an unknown board.
51
+ const board = args.board ?? defaultBoard;
45
52
  const body = {
46
- repo,
53
+ board,
47
54
  type: args.type,
48
55
  title: args.title,
49
56
  description: args.description,
@@ -56,13 +63,13 @@ export async function issueCreate(client, args, defaultRepo) {
56
63
  body.effort_level = args.effort_level;
57
64
  if (args.phase_children !== undefined)
58
65
  body.phase_children = args.phase_children;
59
- return client.request({ method: "POST", path: "", body, repo });
66
+ return client.request({ method: "POST", path: "", body, board });
60
67
  }
61
68
  export async function issueEdit(client, args) {
62
- // `repo` is a transport concern (query), NOT a patch field — pull it
69
+ // `board` is a transport concern (query), NOT a patch field — pull it
63
70
  // out of `rest` so it never leaks into the `/edit` body (the server
64
71
  // 400s on non-allowlisted keys).
65
- const { id, repo, ...rest } = args;
72
+ const { id, board, ...rest } = args;
66
73
  // Strip undefined so we don't send `{title: undefined}` — JSON.stringify
67
74
  // drops them anyway but explicit is clearer.
68
75
  const body = {};
@@ -74,30 +81,30 @@ export async function issueEdit(client, args) {
74
81
  method: "PATCH",
75
82
  path: `/${encodeURIComponent(id)}/edit`,
76
83
  body,
77
- repo,
84
+ board,
78
85
  });
79
86
  }
80
87
  export async function issueTransition(client, args) {
81
- const { id, repo, ...body } = args;
88
+ const { id, board, ...body } = args;
82
89
  return client.request({
83
90
  method: "POST",
84
91
  path: `/${encodeURIComponent(id)}/transition`,
85
92
  body,
86
- repo,
93
+ board,
87
94
  });
88
95
  }
89
96
  export async function issueTriage(client, args) {
90
- const { id, repo, ...body } = args;
97
+ const { id, board, ...body } = args;
91
98
  return client.request({
92
99
  method: "POST",
93
100
  path: `/${encodeURIComponent(id)}/triage`,
94
101
  body,
95
- repo,
102
+ board,
96
103
  });
97
104
  }
98
105
  export async function issueComment(client, args) {
99
106
  const idEnc = encodeURIComponent(args.id);
100
- const repo = args.repo;
107
+ const board = args.board;
101
108
  if (args.action === "add") {
102
109
  if (typeof args.text !== "string") {
103
110
  throw new Error("issue_comment action=add requires text");
@@ -106,7 +113,7 @@ export async function issueComment(client, args) {
106
113
  method: "POST",
107
114
  path: `/${idEnc}/comments`,
108
115
  body: { text: args.text },
109
- repo,
116
+ board,
110
117
  });
111
118
  }
112
119
  if (args.action === "edit") {
@@ -120,7 +127,7 @@ export async function issueComment(client, args) {
120
127
  method: "PATCH",
121
128
  path: `/${idEnc}/comments/${args.comment_id}`,
122
129
  body: { text: args.text },
123
- repo,
130
+ board,
124
131
  });
125
132
  }
126
133
  // delete
@@ -130,12 +137,12 @@ export async function issueComment(client, args) {
130
137
  return client.request({
131
138
  method: "DELETE",
132
139
  path: `/${idEnc}/comments/${args.comment_id}`,
133
- repo,
140
+ board,
134
141
  });
135
142
  }
136
143
  export async function issueDependency(client, args) {
137
144
  const idEnc = encodeURIComponent(args.id);
138
- const repo = args.repo;
145
+ const board = args.board;
139
146
  if (args.action === "add") {
140
147
  if (args.kind === undefined) {
141
148
  throw new Error("issue_dependency action=add requires kind");
@@ -151,7 +158,7 @@ export async function issueDependency(client, args) {
151
158
  target_id: args.target_id,
152
159
  reason: args.reason ?? "",
153
160
  },
154
- repo,
161
+ board,
155
162
  });
156
163
  }
157
164
  // remove
@@ -166,12 +173,12 @@ export async function issueDependency(client, args) {
166
173
  // agent that types the wrong reason gets a refusal here at MCP
167
174
  // arg-validation, not a confusing 400 envelope.
168
175
  body: { reason: "recorded_in_error" },
169
- repo,
176
+ board,
170
177
  });
171
178
  }
172
179
  export async function issueRequiresHuman(client, args) {
173
180
  const idEnc = encodeURIComponent(args.id);
174
- const repo = args.repo;
181
+ const board = args.board;
175
182
  if (args.set) {
176
183
  if (typeof args.reason !== "string") {
177
184
  throw new Error("issue_requires_human set=true requires reason");
@@ -183,21 +190,21 @@ export async function issueRequiresHuman(client, args) {
183
190
  method: "POST",
184
191
  path: `/${idEnc}/requires-human`,
185
192
  body: { reason: args.reason, steps: args.steps },
186
- repo,
193
+ board,
187
194
  });
188
195
  }
189
196
  return client.request({
190
197
  method: "DELETE",
191
198
  path: `/${idEnc}/requires-human`,
192
- repo,
199
+ board,
193
200
  });
194
201
  }
195
202
  export async function issueRetro(client, args) {
196
- const { id, repo, ...body } = args;
203
+ const { id, board, ...body } = args;
197
204
  return client.request({
198
205
  method: "PUT",
199
206
  path: `/${encodeURIComponent(id)}/retro`,
200
207
  body,
201
- repo,
208
+ board,
202
209
  });
203
210
  }
@@ -6,7 +6,7 @@ export class DashboardHttpClient {
6
6
  this.fetchImpl = fetchImpl;
7
7
  }
8
8
  async request(args) {
9
- const url = this.buildUrl(args.path, args.query, args.repo);
9
+ const url = this.buildUrl(args.path, args.query, args.board);
10
10
  const headers = {
11
11
  Authorization: `Bearer ${this.config.token}`,
12
12
  Accept: "application/json",
@@ -47,7 +47,7 @@ export class DashboardHttpClient {
47
47
  }
48
48
  return { ok: false, status: res.status, body: parsed };
49
49
  }
50
- buildUrl(path, extraQuery, repoOverride) {
50
+ buildUrl(path, extraQuery, boardOverride) {
51
51
  const base = this.config.baseUrl.replace(/\/+$/, "");
52
52
  const [rawPath, existingQs] = path.split("?", 2);
53
53
  let cleanPath;
@@ -58,7 +58,7 @@ export class DashboardHttpClient {
58
58
  cleanPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
59
59
  }
60
60
  const params = new URLSearchParams(existingQs ?? "");
61
- params.set("repo", repoOverride ?? this.config.repo);
61
+ params.set("board", boardOverride ?? this.config.board);
62
62
  if (extraQuery) {
63
63
  for (const [k, v] of Object.entries(extraQuery)) {
64
64
  if (v === undefined || v === null)
package/dist/index.js CHANGED
@@ -22,12 +22,22 @@
22
22
  * - issue_requires_human POST/DELETE /api/issues/:id/requires-human
23
23
  * - issue_retro PUT /api/issues/:id/retro
24
24
  *
25
+ * BOARD-ONLY (DX-1171): board is the first-level concept; repo is
26
+ * DERIVED from board server-side, never passed. The package composes the
27
+ * dispatch's qualified board id (`<repo>:<slug>`) at boot from
28
+ * DANX_REPO_NAME + DANXBOT_BOARD_NAME and sends it as `board` — the
29
+ * dashboard owns board→repo resolution.
30
+ *
25
31
  * Env at boot (validated fail-loud — missing → process.exit(1)):
26
32
  * DANXBOT_DASHBOARD_URL dashboard base (e.g. http://danxbot-dashboard:5555)
27
33
  * DANXBOT_DISPATCH_TOKEN per-dispatch bearer
28
- * DANX_REPO_NAME default repo scope, appended as ?repo=<name>;
29
- * each tool's optional `repo` arg overrides it
30
- * per-call for cross-repo reads/writes
34
+ * DANX_REPO_NAME repo half of the qualified board id
35
+ * DANXBOT_BOARD_NAME board-slug half of the qualified board id
36
+ * (the dispatch injects `board.slug` here);
37
+ * composed as `<repo>:<slug>` and appended as
38
+ * ?board=<id>. Each tool's optional `board` arg
39
+ * (a full qualified id) overrides it per-call
40
+ * for cross-board reads/writes.
31
41
  *
32
42
  * Envelope contract: every tool returns the dashboard's response body
33
43
  * verbatim wrapped as `{ok, status, body}`. Refusals (4xx with the v2
@@ -48,10 +58,15 @@ function readEnvOrDie(name) {
48
58
  }
49
59
  return v;
50
60
  }
61
+ // Compose the dispatch's qualified board id (`<repo>:<slug>`) from the
62
+ // two env halves the worker injects. DANXBOT_BOARD_NAME already carries
63
+ // the board SLUG at the spawn site (src/dispatch/core.ts sets it from
64
+ // `board.slug`), so a plain join yields the canonical id — no transform.
65
+ // Both halves are fail-loud required (Core Principle 1 — no fallback).
51
66
  const config = {
52
67
  baseUrl: readEnvOrDie("DANXBOT_DASHBOARD_URL"),
53
68
  token: readEnvOrDie("DANXBOT_DISPATCH_TOKEN"),
54
- repo: readEnvOrDie("DANX_REPO_NAME"),
69
+ board: `${readEnvOrDie("DANX_REPO_NAME")}:${readEnvOrDie("DANXBOT_BOARD_NAME")}`,
55
70
  };
56
71
  const client = new DashboardHttpClient(config);
57
72
  const server = new McpServer({
@@ -90,20 +105,23 @@ const TRANSITION_ACTIONS = [
90
105
  "reopen",
91
106
  ];
92
107
  const TRIAGE_VERDICTS = ["approve", "cancel", "keep", "defer"];
93
- // Optional per-call repo override shared by every tool. Omitted → the
94
- // server binding's DANX_REPO_NAME is used; provided → that registered
95
- // repo is targeted instead (the dashboard 404s an unknown repo). Spread
96
- // into each tool's Zod input object so cross-repo work is reachable from
97
- // any dispatch without rebinding the MCP server.
98
- const repoField = {
99
- repo: z
108
+ // Optional per-call board override shared by every tool. Omitted → the
109
+ // dispatch's env-derived board (`<repo>:<slug>`) is used; provided → that
110
+ // board is targeted instead (the dashboard owns board→repo and 404s an
111
+ // unknown board). The override is a FULL qualified board id
112
+ // (`<repo>:<slug>`, e.g. `platform:the-supply-operations-hub`) never a
113
+ // bare slug, since a slug alone is not globally unique. Spread into each
114
+ // tool's Zod input object so cross-board work is reachable from any
115
+ // dispatch without rebinding the MCP server.
116
+ const boardField = {
117
+ board: z
100
118
  .string()
101
119
  .min(1)
102
120
  .optional()
103
- .describe("Target another registered repo; omit to use this dispatch's repo. Unknown repo → 404."),
121
+ .describe("Target another board by its qualified id `<repo>:<slug>` (e.g. `platform:the-supply-operations-hub`); omit to use this dispatch's board. Unknown board → 404."),
104
122
  };
105
123
  // ---------------- issue_list ----------------
106
- server.tool("issue_list", "List issues for the dispatch's repo by default via GET /api/issues. Pass `repo` to list another registered repo instead (unknown repo → 404). Filters: q (free-text search matched case-insensitively over title + description — use this to find cards by keyword), type, parent_id (string id, null for root-only), dispatchable_derived (boolean — server-computed pickup-ready gate), assigned_agent, include_closed (default false — excludes completed_at/cancelled_at). Returns ALL matching cards (no row cap unless you pass limit). Response body shape: {issues: Issue[]} — lean board rows that do NOT carry the `description` body; call issue_get for a card's full description. Server-side numeric-suffix ordering so DX-10 follows DX-9. Use this instead of grepping .danxbot/issues/ — the DB-backed route is the source of truth post-DX-704.", {
124
+ server.tool("issue_list", "List issues for the dispatch's board by default via GET /api/issues. Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to list another board instead (unknown board → 404). Filters: q (free-text search matched case-insensitively over title + description — use this to find cards by keyword), type, parent_id (string id, null for root-only), dispatchable_derived (boolean — server-computed pickup-ready gate), assigned_agent, include_closed (default false — excludes completed_at/cancelled_at). Returns ALL matching cards (no row cap unless you pass limit). Response body shape: {issues: Issue[]} — LEAN agent rows (DX-1163): each row carries ONLY {id, type, title, status, parent_id, assigned_agent, children (ids), priority}. UI-only board fields (children_detail, counts, triage_ice_total, blocked_descendants, conflict_on, child_assignments, requires_human_child_count, latest_work_dispatch, primary_attachment) and the `description` body are NOT included — call issue_get for the full card. Server-side numeric-suffix ordering so DX-10 follows DX-9. Use this instead of grepping .danxbot/issues/ — the DB-backed route is the source of truth post-DX-704.", {
107
125
  q: z.string().optional(),
108
126
  status_derived: z.string().optional(),
109
127
  type: z.enum(ISSUE_TYPES).optional(),
@@ -113,15 +131,15 @@ server.tool("issue_list", "List issues for the dispatch's repo by default via GE
113
131
  include_closed: z.boolean().optional(),
114
132
  limit: z.number().int().positive().max(1000).optional(),
115
133
  offset: z.number().int().nonnegative().optional(),
116
- ...repoField,
134
+ ...boardField,
117
135
  }, async (args) => jsonResult(await issueList(client, args)));
118
136
  // ---------------- issue_get ----------------
119
- server.tool("issue_get", "Fetch a single hydrated issue via GET /api/issues/:id. Issue ids are globally unique across repos, so this resolves from any dispatch regardless of `repo`. Returns the full card (every joined child collection: ac, comments, dependencies, requires_human steps, retro action items + commits, triage history) plus the ancestor chain walked via parent_id. 404 envelope on unknown id.", {
137
+ server.tool("issue_get", "Fetch a single hydrated 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`. Returns the full card (every joined child collection: ac, comments, dependencies, requires_human steps, retro action items + commits, triage history) plus the ancestor chain walked via parent_id. 404 envelope on unknown id.", {
120
138
  id: z.string().min(1),
121
- ...repoField,
139
+ ...boardField,
122
140
  }, async (args) => jsonResult(await issueGet(client, args)));
123
141
  // ---------------- issue_create ----------------
124
- server.tool("issue_create", "Create a fresh card via POST /api/issues. Defaults to the dispatch's repo; pass `repo` to create the card in another registered repo (forwarded into body.repo + ?repo=; unknown repo → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic's id as parent_id.", {
142
+ server.tool("issue_create", "Create a fresh card via POST /api/issues. Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to create the card on another board (forwarded into body.board + ?board=; unknown board → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic's id as parent_id.", {
125
143
  type: z.enum(ISSUE_TYPES),
126
144
  title: z.string().min(1),
127
145
  description: z.string(),
@@ -137,8 +155,8 @@ server.tool("issue_create", "Create a fresh card via POST /api/issues. Defaults
137
155
  effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
138
156
  }))
139
157
  .optional(),
140
- ...repoField,
141
- }, async (args) => jsonResult(await issueCreate(client, args, config.repo)));
158
+ ...boardField,
159
+ }, async (args) => jsonResult(await issueCreate(client, args, config.board)));
142
160
  // ---------------- issue_edit ----------------
143
161
  server.tool("issue_edit", "Patch prose fields only via PATCH /api/issues/:id/edit. ALLOWED keys: title, description, ac, effort_level, parent_id. ANY OTHER KEY (lifecycle timestamps, triage state, dependencies, retro, requires_human, blocked/dispatch gates) returns 400 with offending_keys[] and a pointer to the dedicated semantic handler — use issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro instead. AC replacement is wholesale soft-delete + reinsert with fresh ordinals; check_item_id linkage survives via title match.", {
144
162
  id: z.string().min(1),
@@ -152,7 +170,7 @@ server.tool("issue_edit", "Patch prose fields only via PATCH /api/issues/:id/edi
152
170
  .optional(),
153
171
  effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
154
172
  parent_id: z.string().nullable().optional(),
155
- ...repoField,
173
+ ...boardField,
156
174
  }, async (args) => jsonResult(await issueEdit(client, args)));
157
175
  // ---------------- issue_transition ----------------
158
176
  server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issues/:id/transition. **THIS IS THE ONLY WAY TO MOVE A CARD'S LIFECYCLE STATE** — DX-835 separated card lifecycle (this tool) from dispatch finalization (`mcp__danxbot__danxbot_complete`). The worker no longer infers card moves from `danxbot_complete.status`; agents that want the card to move MUST call this tool BEFORE calling `danxbot_complete`. Actions: ready (Review→ToDo), pickup (ToDo→In Progress — server checks every dispatch gate: ready_at, blocked_at, requires_human_reason, depends_on partners terminal, conflict_on partners idle; refuses 409 with failed_gate naming the cause; pass manual:true for OPERATOR-SESSION self-pickup — stamps dispatch_kind 'manual', bypasses every card-flow gate except terminal/deleted/already-dispatched, and the worker NEVER auto-transitions the card: no orphan-heal rollback, no Epic auto-rollup — use this whenever the work happens in YOUR current session rather than a worker dispatch, DX-946), rollback_pickup, **complete** (stamps completed_at — moves card to Done; this is YOUR explicit decision, not a side effect of danxbot_complete; REFUSES 409 on Epic if any phase child non-terminal — see non_terminal_phases[]), cancel (stamps cancelled_at — terminal), **block** (requires non-empty reason — stamps blocked_at + blocked_reason + clears dispatch; USE THIS when the CARD itself cannot proceed without human intervention; distinct from env-fault dispatch failures which use `danxbot_complete({status:'failed'})`), unblock, archive (parks to Backlog, clears ready_at), reopen (terminal→active, clears completed_at/cancelled_at/archived_at). Terminal cards refuse every action except reopen. Ladder timestamps preserved — forward stamps never clear earlier ones (CLAUDE.md Core Principle 2).", {
@@ -162,7 +180,7 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issu
162
180
  summary: z.string().optional(),
163
181
  dispatch_id: z.string().min(1).optional(),
164
182
  manual: z.boolean().optional(),
165
- ...repoField,
183
+ ...boardField,
166
184
  }, async (args) => jsonResult(await issueTransition(client, args)));
167
185
  // ---------------- issue_triage ----------------
168
186
  server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/triage. Verdicts: approve (stamps ready_at — moves to ToDo, clears triage TTL), cancel (stamps cancelled_at — terminal), keep (refreshes triage_expires_at by ttl_seconds — defaults 7 days, card stays at Review), defer (stamps archived_at, clears ready_at — parks to Backlog). ICE components optional but recorded when present (total = i+c+e). Reason is REQUIRED non-empty. REFUSES 409 on terminal cards.", {
@@ -177,7 +195,7 @@ server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/tr
177
195
  })
178
196
  .optional(),
179
197
  ttl_seconds: z.number().positive().optional(),
180
- ...repoField,
198
+ ...boardField,
181
199
  }, async (args) => jsonResult(await issueTriage(client, args)));
182
200
  // ---------------- issue_comment ----------------
183
201
  server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation).", {
@@ -185,7 +203,7 @@ server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid].
185
203
  action: z.enum(["add", "edit", "delete"]),
186
204
  comment_id: z.number().int().positive().optional(),
187
205
  text: z.string().min(1).optional(),
188
- ...repoField,
206
+ ...boardField,
189
207
  }, async (args) => jsonResult(await issueComment(client, args)));
190
208
  // ---------------- issue_dependency ----------------
191
209
  server.tool("issue_dependency", "Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason=\"recorded_in_error\" on removal (encodes \"removal means NOT related, never satisfied\") — this MCP boundary hardcodes it, so callers do not pass reason on remove.", {
@@ -195,7 +213,7 @@ server.tool("issue_dependency", "Dependency CRUD via /api/issues/:id/dependencie
195
213
  target_id: z.string().min(1).optional(),
196
214
  reason: z.string().optional(),
197
215
  dependency_id: z.number().int().positive().optional(),
198
- ...repoField,
216
+ ...boardField,
199
217
  }, async (args) => jsonResult(await issueDependency(client, args)));
200
218
  // ---------------- issue_requires_human ----------------
201
219
  server.tool("issue_requires_human", "Set or clear the requires_human dispatch gate via /api/issues/:id/requires-human. set=true → POST {reason, steps[]} — sets requires_human_reason (the dispatch gate per DX-704 — poller refuses pickup while non-null), set_by from bearer, set_at NOW(), REPLACES the step rows (prior soft-deleted, fresh ordinals). set=false → DELETE — clears the columns and soft-deletes every live step. Terminal cards refuse 409 on set.", {
@@ -203,7 +221,7 @@ server.tool("issue_requires_human", "Set or clear the requires_human dispatch ga
203
221
  set: z.boolean(),
204
222
  reason: z.string().optional(),
205
223
  steps: z.array(z.string().min(1)).optional(),
206
- ...repoField,
224
+ ...boardField,
207
225
  }, async (args) => jsonResult(await issueRequiresHuman(client, args)));
208
226
  // ---------------- issue_retro ----------------
209
227
  server.tool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retro. Body: {good, bad, action_item_ids[], commits[]}. REFUSES 409 unless the card is terminal (completed_at OR cancelled_at) — retro ships when work concludes. Replace semantics: good/bad upsert; action_item_ids[] + commits[] soft-delete prior live rows and insert with fresh ordinals. action_item_ids[] entries MUST match <PREFIX>-N. commits[] entries take {sha, subject?}.", {
@@ -215,13 +233,13 @@ server.tool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retr
215
233
  sha: z.string().min(1),
216
234
  subject: z.string().optional(),
217
235
  })),
218
- ...repoField,
236
+ ...boardField,
219
237
  }, async (args) => jsonResult(await issueRetro(client, args)));
220
238
  // ---------------- main ----------------
221
239
  async function main() {
222
240
  const transport = new StdioServerTransport();
223
241
  await server.connect(transport);
224
- console.error(`danx-dashboard-mcp running on stdio (dashboard=${config.baseUrl}, repo=${config.repo})`);
242
+ console.error(`danx-dashboard-mcp running on stdio (dashboard=${config.baseUrl}, board=${config.board})`);
225
243
  }
226
244
  main().catch((err) => {
227
245
  console.error(`[danx-dashboard-mcp] fatal: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",