@thehammer/danx-dashboard-mcp 0.1.215 → 0.1.216
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/dist/http-client.js
CHANGED
|
@@ -1,65 +1,5 @@
|
|
|
1
|
+
import { isBoardScopedRoute } from "./route-board-scope.js";
|
|
1
2
|
import { assertVersionFloorSatisfied, MCP_MIN_VERSION_HEADER } from "./version-floor.js";
|
|
2
|
-
/**
|
|
3
|
-
* DX-3750 — route-path prefixes (`<basePath><path>`, BEFORE any query
|
|
4
|
-
* string) that are NEVER board-scoped: the dashboard route serving them
|
|
5
|
-
* takes no `?board=` at all — either because the resource is GLOBAL
|
|
6
|
-
* (plans — see `plan_list`'s own doc: "Plans are GLOBAL, not board-scoped")
|
|
7
|
-
* or because it scopes some other way a `board` param would be meaningless
|
|
8
|
-
* for (dispatches scope by `repo`; a board resolves from a PATH segment on
|
|
9
|
-
* every route but its own list, which itself scopes by `repo`; `/api/routes`
|
|
10
|
-
* and `/api/openapi` are cross-board discovery). Server-side, each of these
|
|
11
|
-
* families' LIST route validates its query params with its own
|
|
12
|
-
* `rejectUnknownParams(params, TOP_LEVEL_..._PARAM_KEYS)` allowlist
|
|
13
|
-
* (`src/issues/plans-routes.ts`, `src/dashboard/dispatches-routes.ts`,
|
|
14
|
-
* `src/issues/boards-routes.ts`) — none of those allowlists names `board`
|
|
15
|
-
* (DX-3725), so stamping it 400s. THIS list and `NON_BOARD_SCOPED_PATH_PATTERNS` below are the only
|
|
16
|
-
* classification; `isBoardScopedRoute` is the one function that reads them — no second, independently-drifting
|
|
17
|
-
* copy anywhere else in this package.
|
|
18
|
-
*
|
|
19
|
-
* Deliberately prefix-matched, not exact-matched: every sub-route of a
|
|
20
|
-
* non-board-scoped family (`/api/plans/:id/cards`, `/api/boards/:id/export`,
|
|
21
|
-
* `/api/dispatches/:id/logs`, …) is equally never board-scoped — a plan, a
|
|
22
|
-
* dispatch and a board-export all resolve their target from a PATH segment
|
|
23
|
-
* or a `repo`/global scope, never from `?board=`.
|
|
24
|
-
*/
|
|
25
|
-
const NON_BOARD_SCOPED_PATH_PREFIXES = [
|
|
26
|
-
"/api/plans",
|
|
27
|
-
"/api/dispatches",
|
|
28
|
-
"/api/boards",
|
|
29
|
-
"/api/routes",
|
|
30
|
-
"/api/openapi",
|
|
31
|
-
// DX-4261: install- or team-global families, whose list reads declare no `board` (the engine 400s an unknown key).
|
|
32
|
-
"/api/team",
|
|
33
|
-
"/api/users",
|
|
34
|
-
"/api/account",
|
|
35
|
-
"/api/platform",
|
|
36
|
-
"/api/failure-categories",
|
|
37
|
-
"/api/failure-signatures",
|
|
38
|
-
"/api/notifications",
|
|
39
|
-
"/api/self-repair",
|
|
40
|
-
"/api/audit-events",
|
|
41
|
-
"/api/docs",
|
|
42
|
-
"/api/repos",
|
|
43
|
-
"/api/sync-root",
|
|
44
|
-
"/api/workers",
|
|
45
|
-
"/api/tasks",
|
|
46
|
-
"/api/reminders",
|
|
47
|
-
];
|
|
48
|
-
/**
|
|
49
|
-
* DX-4261: non-board-scoped routes a prefix cannot express, because their family root (`/api/issues`, `/api/catalog`) is board-scoped.
|
|
50
|
-
* `/api/agents` stays stamped: `?board=` there selects the roster read (DX-4220).
|
|
51
|
-
*/
|
|
52
|
-
const NON_BOARD_SCOPED_PATH_PATTERNS = [
|
|
53
|
-
/^\/api\/issues\/[^/]+\/(dispatches|problems)$/,
|
|
54
|
-
/^\/api\/catalog\/[^/]+$/,
|
|
55
|
-
// The LIST read only: `/api/plan-sessions/me/plan` reads the stamped board to pin the operator dispatch (DX-2954).
|
|
56
|
-
/^\/api\/plan-sessions$/,
|
|
57
|
-
];
|
|
58
|
-
/** DX-3750 — does `routePath` (`<basePath><path>`, no query string) take a `?board=` at all? See `NON_BOARD_SCOPED_PATH_PREFIXES`'s own doc. */
|
|
59
|
-
export function isBoardScopedRoute(routePath) {
|
|
60
|
-
return !(NON_BOARD_SCOPED_PATH_PREFIXES.some((prefix) => routePath === prefix || routePath.startsWith(`${prefix}/`)) ||
|
|
61
|
-
NON_BOARD_SCOPED_PATH_PATTERNS.some((pattern) => pattern.test(routePath)));
|
|
62
|
-
}
|
|
63
3
|
export class DashboardHttpClient {
|
|
64
4
|
config;
|
|
65
5
|
fetchImpl;
|
|
@@ -71,7 +11,7 @@ export class DashboardHttpClient {
|
|
|
71
11
|
this.fetchImpl = fetchImpl;
|
|
72
12
|
}
|
|
73
13
|
async request(args) {
|
|
74
|
-
const url = this.buildUrl(args.path, args.query, args.board, args.basePath);
|
|
14
|
+
const url = this.buildUrl(args.method, args.path, args.query, args.board, args.basePath);
|
|
75
15
|
const headers = {
|
|
76
16
|
Authorization: `Bearer ${this.config.token}`,
|
|
77
17
|
Accept: "application/json",
|
|
@@ -147,7 +87,7 @@ export class DashboardHttpClient {
|
|
|
147
87
|
}
|
|
148
88
|
return { ok: false, status: res.status, body: parsed };
|
|
149
89
|
}
|
|
150
|
-
buildUrl(path, extraQuery, boardOverride, basePath = "/api/issues") {
|
|
90
|
+
buildUrl(method, path, extraQuery, boardOverride, basePath = "/api/issues") {
|
|
151
91
|
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
152
92
|
const [rawPath, existingQs] = path.split("?", 2);
|
|
153
93
|
let cleanPath;
|
|
@@ -158,10 +98,8 @@ export class DashboardHttpClient {
|
|
|
158
98
|
cleanPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
159
99
|
}
|
|
160
100
|
const params = new URLSearchParams(existingQs ?? "");
|
|
161
|
-
// DX-3750 — only a
|
|
162
|
-
|
|
163
|
-
// plans/dispatches/boards-list through `danxbot_api`.
|
|
164
|
-
if (isBoardScopedRoute(`${basePath}${cleanPath}`)) {
|
|
101
|
+
// DX-3750 / DX-4303 — only a route the dashboard takes `?board=` on gets the stamp; see `route-board-scope.ts`.
|
|
102
|
+
if (isBoardScopedRoute(method, `${basePath}${cleanPath}`)) {
|
|
165
103
|
params.set("board", boardOverride ?? this.config.board);
|
|
166
104
|
}
|
|
167
105
|
if (extraQuery) {
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
export const GET_ROUTE_BOARD_SCOPE = [
|
|
9
|
+
["/api/account/sessions", false],
|
|
10
|
+
["/api/audit-events", false],
|
|
11
|
+
["/api/boards", false],
|
|
12
|
+
["/api/boards/:boardId", false],
|
|
13
|
+
["/api/catalog/:catalogId", false],
|
|
14
|
+
["/api/dispatches", false],
|
|
15
|
+
["/api/dispatches/:dispatchId", true],
|
|
16
|
+
["/api/dispatches/:dispatchId/logs", false],
|
|
17
|
+
["/api/dispatches/:dispatchId/usage-events", false],
|
|
18
|
+
["/api/dispatches/dead-letter-ids", false],
|
|
19
|
+
["/api/docs/:repo/changes", false],
|
|
20
|
+
["/api/failure-categories", false],
|
|
21
|
+
["/api/failure-categories/ambiguous", false],
|
|
22
|
+
["/api/failure-categories/uncategorized", false],
|
|
23
|
+
["/api/failure-signatures", false],
|
|
24
|
+
["/api/issues/:id/problems", false],
|
|
25
|
+
["/api/issues/:id/usage-runs", false],
|
|
26
|
+
["/api/issues/:id/usage-series", false],
|
|
27
|
+
["/api/issues/:issueId/dispatches", false],
|
|
28
|
+
["/api/notifications", false],
|
|
29
|
+
["/api/plan-sessions", false],
|
|
30
|
+
["/api/plans", false],
|
|
31
|
+
["/api/plans/:planId/architecture/sections", false],
|
|
32
|
+
["/api/plans/:planId/cards", false],
|
|
33
|
+
["/api/plans/:planId/notes", false],
|
|
34
|
+
["/api/plans/:planId/records", false],
|
|
35
|
+
["/api/plans/:planId/usage-series", false],
|
|
36
|
+
["/api/plans/mine/architecture/sections", false],
|
|
37
|
+
["/api/platform/signup-codes", false],
|
|
38
|
+
["/api/platform/teams", false],
|
|
39
|
+
["/api/reminders", false],
|
|
40
|
+
["/api/repos", false],
|
|
41
|
+
["/api/self-repair/errors", false],
|
|
42
|
+
["/api/sync-root", false],
|
|
43
|
+
["/api/tasks", false],
|
|
44
|
+
["/api/team/invitations", false],
|
|
45
|
+
["/api/team/members", false],
|
|
46
|
+
["/api/team/roles", false],
|
|
47
|
+
["/api/users", false],
|
|
48
|
+
["/api/workers", false],
|
|
49
|
+
];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4303 - does the dashboard route a request reaches take a `?board=` query key?
|
|
3
|
+
*
|
|
4
|
+
* The answer comes from the dashboard's own route table, shipped in this package as `route-board-scope.generated.ts` (generated by
|
|
5
|
+
* the server's `src/dashboard/mcp-board-scope.ts`; a root test fails when it drifts). This file only matches a concrete request
|
|
6
|
+
* path against those templates, the way the server's `RouteTable` does: segments compared left to right, a literal beating a
|
|
7
|
+
* `:param` beating a trailing `*`. It holds no route knowledge of its own.
|
|
8
|
+
*
|
|
9
|
+
* Only GET routes are listed - the server refuses an unknown query key on a read alone - so every other method takes the key. A
|
|
10
|
+
* GET path no template matches takes it too: the server answers it with a 404 whatever the query.
|
|
11
|
+
*/
|
|
12
|
+
import { GET_ROUTE_BOARD_SCOPE } from "./route-board-scope.generated.js";
|
|
13
|
+
const RANK = { literal: 2, param: 1, rest: 0 };
|
|
14
|
+
function compile(template, acceptsBoard) {
|
|
15
|
+
const segments = template
|
|
16
|
+
.slice(1)
|
|
17
|
+
.split("/")
|
|
18
|
+
.map((s) => (s === "*" ? { kind: "rest" } : s.startsWith(":") ? { kind: "param" } : { kind: "literal", value: s }));
|
|
19
|
+
return { segments, rank: segments.map((s) => RANK[s.kind]), acceptsBoard };
|
|
20
|
+
}
|
|
21
|
+
function compareRank(a, b) {
|
|
22
|
+
const n = Math.max(a.length, b.length);
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const x = a[i] ?? -1;
|
|
25
|
+
const y = b[i] ?? -1;
|
|
26
|
+
if (x !== y)
|
|
27
|
+
return y - x;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
const ROUTES = GET_ROUTE_BOARD_SCOPE.map(([template, accepts]) => compile(template, accepts)).sort((a, b) => compareRank(a.rank, b.rank));
|
|
32
|
+
function matches(route, parts) {
|
|
33
|
+
for (let i = 0; i < route.segments.length; i++) {
|
|
34
|
+
const seg = route.segments[i];
|
|
35
|
+
if (seg.kind === "rest")
|
|
36
|
+
return true;
|
|
37
|
+
const part = parts[i];
|
|
38
|
+
if (part === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
if (seg.kind === "literal" ? part !== seg.value : part === "")
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return parts.length === route.segments.length;
|
|
44
|
+
}
|
|
45
|
+
/** Does the dashboard route `<method> <routePath>` (no query string) take a `?board=` query key? */
|
|
46
|
+
export function isBoardScopedRoute(method, routePath) {
|
|
47
|
+
if (method !== "GET")
|
|
48
|
+
return true;
|
|
49
|
+
const parts = routePath.slice(1).split("/");
|
|
50
|
+
return ROUTES.find((route) => matches(route, parts))?.acceptsBoard ?? true;
|
|
51
|
+
}
|
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.216",
|
|
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",
|