@thehammer/danx-dashboard-mcp 0.1.259 → 0.1.261
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 +1 -1
- package/dist/http-client.js +10 -3
- package/dist/index.js +1 -1
- package/dist/listen.js +20 -2
- package/dist/route-board-scope.generated.js +49 -56
- package/dist/route-board-scope.js +14 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Stdio MCP server fronting danxbot's dashboard HTTP API. Since DX-3465 it exposes
|
|
|
14
14
|
| `DANX_REPO_NAME` | OPTIONAL (DX-4555): repo half of the qualified board id; sent as the access request's repo label |
|
|
15
15
|
| `DANXBOT_BOARD_NAME` | OPTIONAL (DX-4555; needs `DANX_REPO_NAME`): board-slug half of the qualified board id (the dispatch injects `board.slug` here). With neither variable set (the danxbot plugin's own server) the server has no default board: a board-scoped call must carry its `board` argument or it fails loud |
|
|
16
16
|
|
|
17
|
-
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`.
|
|
17
|
+
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, except `GET /api/issues` (the list), whose board is OPTIONAL (DX-4542): there it sends a board only when the caller names one, because no board is the team's whole list across every board. 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`.
|
|
18
18
|
|
|
19
19
|
## Signing in (DX-4390): request and approve
|
|
20
20
|
|
package/dist/http-client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { boardStampOf } from "./route-board-scope.js";
|
|
2
2
|
import { assertVersionFloorSatisfied, MCP_MIN_VERSION_HEADER } from "./version-floor.js";
|
|
3
3
|
export class DashboardHttpClient {
|
|
4
4
|
config;
|
|
@@ -110,7 +110,10 @@ export class DashboardHttpClient {
|
|
|
110
110
|
}
|
|
111
111
|
const params = new URLSearchParams(existingQs ?? "");
|
|
112
112
|
// DX-3750 / DX-4303 — only a route the dashboard takes `?board=` on gets the stamp; see `route-board-scope.ts`.
|
|
113
|
-
|
|
113
|
+
// DX-4542 — a route whose board is OPTIONAL (no board = the whole team) gets only the board the caller named: stamping the dispatch's
|
|
114
|
+
// board there would make the team-wide list impossible to ask for.
|
|
115
|
+
const stamp = boardStampOf(method, `${basePath}${cleanPath}`);
|
|
116
|
+
if (stamp === "stamp") {
|
|
114
117
|
const board = boardOverride ?? this.config.board;
|
|
115
118
|
// DX-4555: a board-less server (the danxbot plugin's, no DANX_REPO_NAME/DANXBOT_BOARD_NAME) has no board to stamp.
|
|
116
119
|
if (board === undefined) {
|
|
@@ -118,6 +121,9 @@ export class DashboardHttpClient {
|
|
|
118
121
|
}
|
|
119
122
|
params.set("board", board);
|
|
120
123
|
}
|
|
124
|
+
else if (stamp === "named" && boardOverride !== undefined) {
|
|
125
|
+
params.set("board", boardOverride);
|
|
126
|
+
}
|
|
121
127
|
if (extraQuery) {
|
|
122
128
|
for (const [k, v] of Object.entries(extraQuery)) {
|
|
123
129
|
if (v === undefined || v === null)
|
|
@@ -137,6 +143,7 @@ export class DashboardHttpClient {
|
|
|
137
143
|
: String(v));
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
|
-
|
|
146
|
+
const query = params.toString();
|
|
147
|
+
return `${base}${basePath}${cleanPath}${query === "" ? "" : `?${query}`}`;
|
|
141
148
|
}
|
|
142
149
|
}
|
package/dist/index.js
CHANGED
|
@@ -495,7 +495,7 @@ const boardField = {
|
|
|
495
495
|
.string()
|
|
496
496
|
.min(1)
|
|
497
497
|
.optional()
|
|
498
|
-
.describe("Board id `<repo>:<slug>`; omit for this dispatch's board. Unknown → 404."),
|
|
498
|
+
.describe("Board id `<repo>:<slug>`; omit for this dispatch's board, except on `GET /api/issues`, where omitting it lists the whole team across every board. Unknown → 404."),
|
|
499
499
|
};
|
|
500
500
|
// DX-3465 (code-architecture gate finding, at 275f82c0d) — `resource_fields`
|
|
501
501
|
// (DX-3426) was deleted from here: it was a dedicated wrapper around a plain
|
package/dist/listen.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* `text` is what the session reads; an event it cannot read still produces one
|
|
13
13
|
* (`could not read event …`), never silence. `id` is `null` only for a frame
|
|
14
14
|
* that carried no usable id — which includes DX-2885's `nudge` frame
|
|
15
|
-
* (`formatNudgeLine`)
|
|
16
|
-
*
|
|
15
|
+
* (`formatNudgeLine`), DX-4340's `pacing` frame (`formatPacingLine`) and
|
|
16
|
+
* DX-4342's `resume` frame (`formatResumeLine`): all are advisory, written fresh by the dashboard's own keep-alive tick,
|
|
17
17
|
* and never replayed, so they ride this same shape with `id: null` rather
|
|
18
18
|
* than growing a THIRD output variant.
|
|
19
19
|
* - `{type:"stopped", reason, detail, refusal?}` — once, last. `reason`
|
|
@@ -352,6 +352,21 @@ export function formatPacingLine(event) {
|
|
|
352
352
|
throw new Error(`malformed pacing message: ${invalid}`);
|
|
353
353
|
return pacingRelayLine(event.text);
|
|
354
354
|
}
|
|
355
|
+
/** Why a resume message cannot be read, or `null` when it can. */
|
|
356
|
+
export function invalidResumeReason(value) {
|
|
357
|
+
if (!isRecord(value))
|
|
358
|
+
return "the resume message is not an object";
|
|
359
|
+
if (typeof value.text !== "string" || value.text.trim() === "")
|
|
360
|
+
return "text is empty";
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
/** The one readable line for a resume message: the dashboard's text, on one line, cut like a pacing line. Throws, naming the fault, on a malformed message. */
|
|
364
|
+
export function formatResumeLine(event) {
|
|
365
|
+
const invalid = invalidResumeReason(event);
|
|
366
|
+
if (invalid !== null)
|
|
367
|
+
throw new Error(`malformed resume message: ${invalid}`);
|
|
368
|
+
return pacingRelayLine(event.text);
|
|
369
|
+
}
|
|
355
370
|
/**
|
|
356
371
|
* Incremental SSE parser. Comment lines (keep-alives, the connect marker) and
|
|
357
372
|
* field-less blocks produce no message, which is what keeps them out of the output.
|
|
@@ -487,6 +502,9 @@ export async function runListener(options, deps) {
|
|
|
487
502
|
// DX-4340 - the pacing tick's one message for this session's Claude account (level or budget changed): advisory like a nudge.
|
|
488
503
|
if (message.event === "pacing")
|
|
489
504
|
return emitAdvisory(message, "pacing message", invalidPacingReason, (p) => formatPacingLine(p));
|
|
505
|
+
// DX-4342 - the auto-resume's message once the limit that paused or stopped this session has reset: advisory like a pacing message.
|
|
506
|
+
if (message.event === "resume")
|
|
507
|
+
return emitAdvisory(message, "resume message", invalidResumeReason, (p) => formatResumeLine(p));
|
|
490
508
|
if (message.event !== "activity")
|
|
491
509
|
return null;
|
|
492
510
|
// `Number(null)` is 0, so a frame with no id must never be read as event 0: that id
|
|
@@ -1,58 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GENERATED by src/dashboard/mcp-board-scope.ts (DX-4303) - do not edit by hand.
|
|
3
|
-
* Regenerate: DANX_REGEN_MCP_BOARD_SCOPE=1 npx vitest run src/dashboard/mcp-board-scope.test.ts
|
|
4
|
-
*
|
|
5
|
-
* The GET /api/* route templates the client must know to stamp `?board=` correctly, and whether each takes it. The server refuses an
|
|
6
|
-
* unknown key on a list read whose vocabulary omits `board` and on a route declaring `strictQuery`; a path no template names is stamped.
|
|
7
|
-
*/
|
|
8
1
|
export const GET_ROUTE_BOARD_SCOPE = [
|
|
9
|
-
["/api/access-requests",
|
|
10
|
-
["/api/account/sessions",
|
|
11
|
-
["/api/agents/:agentId/halt-history",
|
|
12
|
-
["/api/audit-events",
|
|
13
|
-
["/api/boards",
|
|
14
|
-
["/api/boards/:boardId",
|
|
15
|
-
["/api/catalog/:catalogId",
|
|
16
|
-
["/api/dispatches",
|
|
17
|
-
["/api/dispatches/:dispatchId",
|
|
18
|
-
["/api/dispatches/:dispatchId/logs",
|
|
19
|
-
["/api/dispatches/:dispatchId/usage-events",
|
|
20
|
-
["/api/dispatches/dead-letter-ids",
|
|
21
|
-
["/api/docs/:repo/changes",
|
|
22
|
-
["/api/docs/:repo/progress/:pathId",
|
|
23
|
-
["/api/failure-categories",
|
|
24
|
-
["/api/failure-categories/ambiguous",
|
|
25
|
-
["/api/failure-categories/uncategorized",
|
|
26
|
-
["/api/failure-signatures",
|
|
27
|
-
["/api/issues
|
|
28
|
-
["/api/issues/:id/
|
|
29
|
-
["/api/issues/:id/usage-
|
|
30
|
-
["/api/issues/:
|
|
31
|
-
["/api/
|
|
32
|
-
["/api/
|
|
33
|
-
["/api/
|
|
34
|
-
["/api/plans
|
|
35
|
-
["/api/plans/:planId/
|
|
36
|
-
["/api/plans/:planId/
|
|
37
|
-
["/api/plans/:planId/
|
|
38
|
-
["/api/plans/:planId/
|
|
39
|
-
["/api/plans/
|
|
40
|
-
["/api/
|
|
41
|
-
["/api/platform/
|
|
42
|
-
["/api/
|
|
43
|
-
["/api/
|
|
44
|
-
["/api/
|
|
45
|
-
["/api/
|
|
46
|
-
["/api/
|
|
47
|
-
["/api/
|
|
48
|
-
["/api/team/invitations",
|
|
49
|
-
["/api/team/members",
|
|
50
|
-
["/api/team/roles",
|
|
51
|
-
["/api/users",
|
|
52
|
-
["/api/worker/catalog-mcp-servers",
|
|
53
|
-
["/api/worker/dispatches/:dispatchId",
|
|
54
|
-
["/api/worker/dispatches/:dispatchId/usage-predecessor",
|
|
55
|
-
["/api/worker/dispatches/non-terminal",
|
|
56
|
-
["/api/worker/dispatches/recent-signal-failures",
|
|
57
|
-
["/api/workers",
|
|
2
|
+
["/api/access-requests", "refuse"],
|
|
3
|
+
["/api/account/sessions", "refuse"],
|
|
4
|
+
["/api/agents/:agentId/halt-history", "refuse"],
|
|
5
|
+
["/api/audit-events", "refuse"],
|
|
6
|
+
["/api/boards", "refuse"],
|
|
7
|
+
["/api/boards/:boardId", "refuse"],
|
|
8
|
+
["/api/catalog/:catalogId", "refuse"],
|
|
9
|
+
["/api/dispatches", "refuse"],
|
|
10
|
+
["/api/dispatches/:dispatchId", "stamp"],
|
|
11
|
+
["/api/dispatches/:dispatchId/logs", "refuse"],
|
|
12
|
+
["/api/dispatches/:dispatchId/usage-events", "refuse"],
|
|
13
|
+
["/api/dispatches/dead-letter-ids", "refuse"],
|
|
14
|
+
["/api/docs/:repo/changes", "refuse"],
|
|
15
|
+
["/api/docs/:repo/progress/:pathId", "refuse"],
|
|
16
|
+
["/api/failure-categories", "refuse"],
|
|
17
|
+
["/api/failure-categories/ambiguous", "refuse"],
|
|
18
|
+
["/api/failure-categories/uncategorized", "refuse"],
|
|
19
|
+
["/api/failure-signatures", "refuse"],
|
|
20
|
+
["/api/issues", "named"],
|
|
21
|
+
["/api/issues/:id/problems", "refuse"],
|
|
22
|
+
["/api/issues/:id/usage-runs", "refuse"],
|
|
23
|
+
["/api/issues/:id/usage-series", "refuse"],
|
|
24
|
+
["/api/issues/:issueId/dispatches", "refuse"],
|
|
25
|
+
["/api/notifications", "refuse"],
|
|
26
|
+
["/api/plan-sessions", "refuse"],
|
|
27
|
+
["/api/plans", "refuse"],
|
|
28
|
+
["/api/plans/:planId/architecture/sections", "refuse"],
|
|
29
|
+
["/api/plans/:planId/cards", "refuse"],
|
|
30
|
+
["/api/plans/:planId/notes", "refuse"],
|
|
31
|
+
["/api/plans/:planId/records", "refuse"],
|
|
32
|
+
["/api/plans/:planId/usage-series", "refuse"],
|
|
33
|
+
["/api/plans/mine/architecture/sections", "refuse"],
|
|
34
|
+
["/api/platform/signup-codes", "refuse"],
|
|
35
|
+
["/api/platform/teams", "refuse"],
|
|
36
|
+
["/api/reminders", "refuse"],
|
|
37
|
+
["/api/repos", "refuse"],
|
|
38
|
+
["/api/self-repair/errors", "refuse"],
|
|
39
|
+
["/api/sync-root", "refuse"],
|
|
40
|
+
["/api/tasks", "refuse"],
|
|
41
|
+
["/api/team/invitations", "refuse"],
|
|
42
|
+
["/api/team/members", "refuse"],
|
|
43
|
+
["/api/team/roles", "refuse"],
|
|
44
|
+
["/api/users", "refuse"],
|
|
45
|
+
["/api/worker/catalog-mcp-servers", "refuse"],
|
|
46
|
+
["/api/worker/dispatches/:dispatchId", "stamp"],
|
|
47
|
+
["/api/worker/dispatches/:dispatchId/usage-predecessor", "refuse"],
|
|
48
|
+
["/api/worker/dispatches/non-terminal", "refuse"],
|
|
49
|
+
["/api/worker/dispatches/recent-signal-failures", "refuse"],
|
|
50
|
+
["/api/workers", "refuse"],
|
|
58
51
|
];
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DX-4303 - does the dashboard route a request reaches
|
|
2
|
+
* DX-4303 / DX-4542 - what the client does with a `?board=` query key on the dashboard route a request reaches: `stamp` it (the route
|
|
3
|
+
* takes it, so the dispatch's board is filled in), `refuse` it (the server 400s it) or `named` (the board is optional and no board is the
|
|
4
|
+
* whole team: sent only when the caller names one).
|
|
3
5
|
*
|
|
4
6
|
* The answer comes from the dashboard's own route table, shipped in this package as `route-board-scope.generated.ts` (generated by
|
|
5
7
|
* the server's `src/dashboard/mcp-board-scope.ts`; a root test fails when it drifts). This file only matches a concrete request
|
|
@@ -11,12 +13,12 @@
|
|
|
11
13
|
*/
|
|
12
14
|
import { GET_ROUTE_BOARD_SCOPE } from "./route-board-scope.generated.js";
|
|
13
15
|
const RANK = { literal: 2, param: 1, rest: 0 };
|
|
14
|
-
function compile(template,
|
|
16
|
+
function compile(template, stamp) {
|
|
15
17
|
const segments = template
|
|
16
18
|
.slice(1)
|
|
17
19
|
.split("/")
|
|
18
20
|
.map((s) => (s === "*" ? { kind: "rest" } : s.startsWith(":") ? { kind: "param" } : { kind: "literal", value: s }));
|
|
19
|
-
return { segments, rank: segments.map((s) => RANK[s.kind]),
|
|
21
|
+
return { segments, rank: segments.map((s) => RANK[s.kind]), stamp };
|
|
20
22
|
}
|
|
21
23
|
function compareRank(a, b) {
|
|
22
24
|
const n = Math.max(a.length, b.length);
|
|
@@ -28,7 +30,7 @@ function compareRank(a, b) {
|
|
|
28
30
|
}
|
|
29
31
|
return 0;
|
|
30
32
|
}
|
|
31
|
-
const ROUTES = GET_ROUTE_BOARD_SCOPE.map(([template,
|
|
33
|
+
const ROUTES = GET_ROUTE_BOARD_SCOPE.map(([template, stamp]) => compile(template, stamp)).sort((a, b) => compareRank(a.rank, b.rank));
|
|
32
34
|
function matches(route, parts) {
|
|
33
35
|
for (let i = 0; i < route.segments.length; i++) {
|
|
34
36
|
const seg = route.segments[i];
|
|
@@ -42,10 +44,14 @@ function matches(route, parts) {
|
|
|
42
44
|
}
|
|
43
45
|
return parts.length === route.segments.length;
|
|
44
46
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* What the client does with `?board=` on `<method> <routePath>` (no query string): `stamp` the dispatch's board when the caller names none,
|
|
49
|
+
* `refuse` to send it (the server 400s it), or (DX-4542) `named`: the route takes it OPTIONALLY and no board is the whole team, so it is sent
|
|
50
|
+
* only when the caller names one.
|
|
51
|
+
*/
|
|
52
|
+
export function boardStampOf(method, routePath) {
|
|
47
53
|
if (method !== "GET")
|
|
48
|
-
return
|
|
54
|
+
return "stamp";
|
|
49
55
|
const parts = routePath.slice(1).split("/");
|
|
50
|
-
return ROUTES.find((route) => matches(route, parts))?.
|
|
56
|
+
return ROUTES.find((route) => matches(route, parts))?.stamp ?? "stamp";
|
|
51
57
|
}
|
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.261",
|
|
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, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|