@thehammer/danx-dashboard-mcp 0.1.322 → 0.1.323

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.
@@ -1,6 +1,13 @@
1
- import { boardStampOf } from "./route-board-scope.js";
1
+ import { boardStampOf, takesBoardInBody } from "./route-board-scope.js";
2
2
  import { assertVersionFloorSatisfied, MCP_MIN_VERSION_HEADER } from "./version-floor.js";
3
3
  import { PLUGIN_BACKGROUND_HEADER } from "./session-options.js";
4
+ /** `<basePath><path>` with the path's own query string cut off and its leading slash normalised (an empty or "/" path adds nothing). */
5
+ function routePathOf(path, basePath = "/api/issues") {
6
+ const [rawPath] = path.split("?", 2);
7
+ if (rawPath === "" || rawPath === "/")
8
+ return basePath;
9
+ return `${basePath}${rawPath.startsWith("/") ? rawPath : `/${rawPath}`}`;
10
+ }
4
11
  export class DashboardHttpClient {
5
12
  config;
6
13
  fetchImpl;
@@ -71,7 +78,7 @@ export class DashboardHttpClient {
71
78
  }
72
79
  else if (args.body !== undefined) {
73
80
  headers["Content-Type"] = "application/json";
74
- payload = JSON.stringify(args.body);
81
+ payload = JSON.stringify(this.withBodyBoard(args));
75
82
  }
76
83
  let res;
77
84
  try {
@@ -116,31 +123,44 @@ export class DashboardHttpClient {
116
123
  }
117
124
  return { ok: false, status: res.status, body: parsed };
118
125
  }
126
+ /**
127
+ * DX-5337 - the body of a request whose route reads the board from `body.board` (issue create/import): the board the caller wrote is kept;
128
+ * with none, the call's `board` argument or the dispatch's board is filled in. DX-3465 deleted the per-operation `issue_create` tool that
129
+ * put the dispatch's board in the body, and the create route does not read `?board=`, so a dispatched agent's create arrived with no board
130
+ * and was refused 400. A board-less server with no `board` argument fills nothing: the dashboard's own 400 names the missing board.
131
+ */
132
+ withBodyBoard(args) {
133
+ const body = args.body;
134
+ if (!takesBoardInBody(args.method, routePathOf(args.path, args.basePath)))
135
+ return body;
136
+ if (typeof body !== "object" || body === null || Array.isArray(body))
137
+ return body;
138
+ if (body.board !== undefined)
139
+ return body;
140
+ const board = args.board ?? this.config.board;
141
+ return board === undefined ? body : { ...body, board };
142
+ }
119
143
  buildUrl(method, path, extraQuery, boardOverride, basePath = "/api/issues") {
120
144
  const base = this.config.baseUrl.replace(/\/+$/, "");
121
- const [rawPath, existingQs] = path.split("?", 2);
122
- let cleanPath;
123
- if (rawPath === "" || rawPath === "/") {
124
- cleanPath = "";
125
- }
126
- else {
127
- cleanPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
128
- }
145
+ const [, existingQs] = path.split("?", 2);
129
146
  const params = new URLSearchParams(existingQs ?? "");
130
147
  // DX-3750 / DX-4303 — only a route the dashboard takes `?board=` on gets the stamp; see `route-board-scope.ts`.
131
148
  // DX-4542 — a route whose board is OPTIONAL (no board = the whole team) gets only the board the caller named: stamping the dispatch's
132
149
  // board there would make the team-wide list impossible to ask for.
133
- const stamp = boardStampOf(method, `${basePath}${cleanPath}`);
134
- if (stamp === "stamp") {
150
+ const routePath = routePathOf(path, basePath);
151
+ const stamp = boardStampOf(method, routePath);
152
+ // DX-5337: issue create/import take the board in the body only (`withBodyBoard`), so no `?board=` rides them: one path, never two.
153
+ const boardInQuery = !takesBoardInBody(method, routePath);
154
+ if (boardInQuery && stamp === "stamp") {
135
155
  const board = boardOverride ?? this.config.board;
136
156
  // DX-4592: a board-less server (the danxbot plugin's, no DANX_REPO_NAME/DANXBOT_BOARD_NAME) stamps nothing when the call names no
137
- // board. Whether the route NEEDS one is the dashboard's to say: a route that does (issue create/import, quality-gate and settings
138
- // routes, the dispatch verbs) answers 400 naming `board`, while a card-id, plan or plan-session route takes its board from the card
157
+ // board. Whether the route NEEDS one is the dashboard's to say: a route that does (quality-gate and settings routes, the dispatch
158
+ // verbs) answers 400 naming `board`, while a card-id, plan or plan-session route takes its board from the card
139
159
  // or the plan. Refusing here (DX-4555) wrongly stopped every plan route, `plan_connect` first.
140
160
  if (board !== undefined)
141
161
  params.set("board", board);
142
162
  }
143
- else if (stamp === "named" && boardOverride !== undefined) {
163
+ else if (boardInQuery && stamp === "named" && boardOverride !== undefined) {
144
164
  params.set("board", boardOverride);
145
165
  }
146
166
  if (extraQuery) {
@@ -163,6 +183,6 @@ export class DashboardHttpClient {
163
183
  }
164
184
  }
165
185
  const query = params.toString();
166
- return `${base}${basePath}${cleanPath}${query === "" ? "" : `?${query}`}`;
186
+ return `${base}${routePath}${query === "" ? "" : `?${query}`}`;
167
187
  }
168
188
  }
@@ -61,3 +61,8 @@ export const GET_ROUTE_BOARD_SCOPE = [
61
61
  ["/api/worker/dispatches/recent-signal-failures", "refuse"],
62
62
  ["/api/workers", "refuse"],
63
63
  ];
64
+ /** DX-5337 - the routes that read the card's board from `body.board` only: the client fills a dispatch's board in there and sends no `?board=`. */
65
+ export const BODY_BOARD_ROUTES = [
66
+ ["POST", "/api/issues"],
67
+ ["POST", "/api/issues/import"],
68
+ ];
@@ -11,7 +11,7 @@
11
11
  * Only GET routes are listed - the server refuses an unknown query key on a read alone - so every other method takes the key. A
12
12
  * GET path no template matches takes it too: the server answers it with a 404 whatever the query.
13
13
  */
14
- import { GET_ROUTE_BOARD_SCOPE } from "./route-board-scope.generated.js";
14
+ import { BODY_BOARD_ROUTES, GET_ROUTE_BOARD_SCOPE } from "./route-board-scope.generated.js";
15
15
  const RANK = { literal: 2, param: 1, rest: 0 };
16
16
  function compile(template, stamp) {
17
17
  const segments = template
@@ -55,3 +55,17 @@ export function boardStampOf(method, routePath) {
55
55
  const parts = routePath.slice(1).split("/");
56
56
  return ROUTES.find((route) => matches(route, parts))?.stamp ?? "stamp";
57
57
  }
58
+ /**
59
+ * DX-5337 - the routes that read the card's board ONLY from `body.board` (issue create and import), from the generated
60
+ * `BODY_BOARD_ROUTES` (the server's `RouteDef.boardInBody`). A dispatch's default board reaches them through the body and through
61
+ * nothing else: the client fills `body.board` when the caller named none and sends no `?board=` there.
62
+ */
63
+ const BODY_BOARD = BODY_BOARD_ROUTES.map(([method, template]) => ({
64
+ method,
65
+ route: compile(template, "stamp"),
66
+ }));
67
+ /** Whether `<method> <routePath>` (no query string) takes its board in `body.board`. */
68
+ export function takesBoardInBody(method, routePath) {
69
+ const parts = routePath.replace(/\/+$/, "").slice(1).split("/");
70
+ return BODY_BOARD.some((entry) => entry.method === method && matches(entry.route, parts));
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.322",
3
+ "version": "0.1.323",
4
4
  "description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, request_permission, attach_file, and danxbot_slack_reply) (DX-3465, PLN-11 R-25).",
5
5
  "license": "MIT",
6
6
  "type": "module",