@thehammer/danx-dashboard-mcp 0.1.189 → 0.1.192
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 +2 -2
- package/dist/bridge.js +40 -15
- package/dist/credential.js +5 -5
- package/dist/handlers.js +1 -1
- package/dist/index.js +3 -3
- package/dist/session-connection.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Stdio MCP server fronting danxbot's dashboard HTTP API. Since DX-3465 it exposes
|
|
|
9
9
|
| Var | Purpose |
|
|
10
10
|
|---|---|
|
|
11
11
|
| `DANXBOT_DASHBOARD_URL` | Dashboard base, e.g. `http://danxbot-dashboard:5555` (inside compose) or `http://localhost:5555` (host) |
|
|
12
|
-
| `DANXBOT_DASHBOARD_TOKEN_FILE` **or** `
|
|
12
|
+
| `DANXBOT_DASHBOARD_TOKEN_FILE` **or** `DANX_DASHBOARD_CREDENTIAL` | The credential, declared exactly one of two ways (DX-2862, `src/credential.ts`). A FILE holding the bearer (a leading `~` is the user's home) — how an interactive operator session declares one, since its `.mcp.json` is git-tracked and can never carry the token itself. Or the bearer VERBATIM — how a dispatch declares one, substituted from its overlay. A declared file is AUTHORITATIVE and its failure is FATAL: an inherited `DANX_DASHBOARD_CREDENTIAL` is never consulted instead, because on an operator machine that value routinely names a different (local-dev) dashboard, which answers healthily and empty. The same resolver serves the `bridge` subcommand below, so a session's tools and its event stream can never sign in as two different identities |
|
|
13
13
|
| `DANX_REPO_NAME` | Repo half of the qualified board id |
|
|
14
14
|
| `DANXBOT_BOARD_NAME` | Board-slug half of the qualified board id (the dispatch injects `board.slug` here) |
|
|
15
15
|
|
|
@@ -39,7 +39,7 @@ A program runs this, not an agent: the danxbot Claude Code plugin's plan event b
|
|
|
39
39
|
CLAUDE_CODE_SESSION_ID=<session> npx -y @thehammer/danx-dashboard-mcp@<version> bridge [--resume-ids <id>,<id>,...]
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
- **Credential — the session's own, never the ambient one (DX-2862).** The session id is the only thing `bridge` takes from its environment; `--resume-ids` is the only argument and holds no secret. Everything else comes from the connection record this session's own danx-dashboard MCP server wrote on `plan_connect` (`src/session-connection.ts`): the dashboard URL, the credential's SOURCE, and its FINGERPRINT. `bridge` resolves that source through the same resolver the server used and refuses to run unless the result fingerprints identically — `credential_mismatch`. Before that fix it used whatever `
|
|
42
|
+
- **Credential — the session's own, never the ambient one (DX-2862).** The session id is the only thing `bridge` takes from its environment; `--resume-ids` is the only argument and holds no secret. Everything else comes from the connection record this session's own danx-dashboard MCP server wrote on `plan_connect` (`src/session-connection.ts`): the dashboard URL, the credential's SOURCE, and its FINGERPRINT. `bridge` resolves that source through the same resolver the server used and refuses to run unless the result fingerprints identically — `credential_mismatch`. Before that fix it used whatever `DANX_DASHBOARD_CREDENTIAL` the session's environment held, and on a machine where that differed from the server's credential the dashboard admitted the stream and dropped every event, with nothing logged anywhere.
|
|
43
43
|
- **No client-side reach check (DX-2920).** `bridge` used to prove board reach itself, before ever opening the stream (`GET /api/plans/mine?fields=cards` + a per-board `GET /api/issues/:id/problems`). That check is DELETED. The dashboard briefly enforced the identical requirement server-side (DX-2863: refusing a mint or stream admission with `403 issuer_cannot_read_plan_boards`, and ending a live stream `{"reason":"scope_narrowed","boards":[…]}`), but DX-3468 removed that mechanism too — DX-3409 made every credential team-bound, so a plan session's issuer and every board its connected plan covers are always in the same team, and the check could never fire any more. `bridge` no longer independently verifies board reach at all, client- or server-side. Once a ticket is minted, `bridge` emits, once, `{"type":"ready"}` — no `boards` field, since reach is no longer proven client-side.
|
|
44
44
|
- **Ticket.** `bridge` mints the session's listener ticket (`POST /api/plan-sessions/me/stream-ticket`, 10 s timeout) and keeps it in the process. A ticket authorizes reading that one session's event stream and nothing else, and is only issued while the session is connected to a plan. Minting a new one ends the previous listener.
|
|
45
45
|
- **Output — JSON Lines.** One `{"type":"event","id":<id|null>,"text":"…"}` per event on the connected plan's cards, where `text` is `[DX-8 "Title" repo:board] newms87 answered "Which rollout order?": chose "Pause E2E" — note: "only this week"`, `… answered "…": "<free-form answer>"`, `… commented: "…"`, `… opened a problem: "…"`, `… blocked the card: "…"`, or `… unblocked the card`. An event it cannot read still produces one, with a `could not read event` text. Nothing for keep-alives, reconnects or re-mints. The session's own writes are never echoed back to it.
|
package/dist/bridge.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* 5. Streams through `runListener` (`listen.ts`), which reconnects within the
|
|
35
35
|
* ticket, and re-mints when that ticket's lease runs out or a fresh ticket
|
|
36
36
|
* is refused once.
|
|
37
|
-
* 6. Writes JSON Lines to stdout: ONE `{type:"ready", boards, cardCount, degraded}`
|
|
37
|
+
* 6. Writes JSON Lines to stdout: ONE `{type:"ready", boards, cardCount, degraded, inventoryError}`
|
|
38
38
|
* once a ticket has been minted and streaming is about to start —
|
|
39
39
|
* DX-2970's `fetchPlanInventory`, a single BEST-EFFORT (never a gate)
|
|
40
40
|
* read of the connected plan's real board/card counts, purely so a
|
|
@@ -140,6 +140,13 @@ function formatDigest(entries) {
|
|
|
140
140
|
return [header, ...entries.map((e) => e.text)].join("\n");
|
|
141
141
|
}
|
|
142
142
|
export const BRIDGE_SUBCOMMAND = "bridge";
|
|
143
|
+
/**
|
|
144
|
+
* DX-3912 — the longest heartbeat `reason` the bridge sends: one under the dashboard's own
|
|
145
|
+
* limit (`HEARTBEAT_REASON_MAX_CHARS` = 200, `src/issues/db/plan-session-heartbeat.ts`),
|
|
146
|
+
* because `oneLine` appends a "…" to a truncated string and a longer reason refuses the whole
|
|
147
|
+
* heartbeat. `bridge.test.ts` asserts the cap.
|
|
148
|
+
*/
|
|
149
|
+
const HEARTBEAT_REASON_MAX_CODE_POINTS = 199;
|
|
143
150
|
export const STREAM_TICKET_PATH = "/api/plan-sessions/me/stream-ticket";
|
|
144
151
|
// DX-2970 — the bare (no `fields=`) cheap-scalars read, used ONLY to report the
|
|
145
152
|
// connected plan's real `boards`/`cardCount` on the `ready` record (see
|
|
@@ -276,7 +283,7 @@ export class BridgeStartError extends Error {
|
|
|
276
283
|
/**
|
|
277
284
|
* The session's connection, as its own MCP server recorded it — and the proof
|
|
278
285
|
* that this process holds the SAME credential that server does. Never falls back
|
|
279
|
-
* to an ambient
|
|
286
|
+
* to an ambient env credential: that fallback is precisely DX-2862.
|
|
280
287
|
*/
|
|
281
288
|
const INSTANCE_ID_ENV = "DANX_BRIDGE_INSTANCE_ID";
|
|
282
289
|
/**
|
|
@@ -449,15 +456,29 @@ export async function mintTicket(options, deps) {
|
|
|
449
456
|
leaseMs: ticket.leaseMs,
|
|
450
457
|
};
|
|
451
458
|
}
|
|
452
|
-
/**
|
|
459
|
+
/**
|
|
460
|
+
* What is wrong with a 2xx bare `/api/plans/mine` body, or `null` when it carries a usable inventory.
|
|
461
|
+
*
|
|
462
|
+
* DX-3912 — the field names here are the plan resource's WIRE names (`boards`, `card_count`,
|
|
463
|
+
* snake_case since DX-3428), the same ones `src/resources/plan/plan-resource.ts` emits; the
|
|
464
|
+
* contract test `src/issues/__tests__/plan-inventory-bridge-contract.test.ts` runs this parser
|
|
465
|
+
* on the server's own response, so a rename there fails that test instead of silently
|
|
466
|
+
* degrading every bridge. A missing field lists the keys the body DID carry, so a rename is
|
|
467
|
+
* named in the first message anyone reads.
|
|
468
|
+
*/
|
|
453
469
|
function badInventoryReason(body) {
|
|
454
|
-
if (
|
|
470
|
+
if (!isRecord(body))
|
|
455
471
|
return "the body is not a JSON object";
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
472
|
+
if (!("boards" in body))
|
|
473
|
+
return `boards is missing (response keys: ${Object.keys(body).join(", ")})`;
|
|
474
|
+
if (!Array.isArray(body.boards) || body.boards.some((x) => typeof x !== "string")) {
|
|
475
|
+
return "boards is not an array of strings";
|
|
476
|
+
}
|
|
477
|
+
if (!("card_count" in body))
|
|
478
|
+
return `card_count is missing (response keys: ${Object.keys(body).join(", ")})`;
|
|
479
|
+
const count = body.card_count;
|
|
480
|
+
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) {
|
|
481
|
+
return `card_count is not a non-negative integer (got ${typeof count === "number" ? count : typeof count})`;
|
|
461
482
|
}
|
|
462
483
|
return null;
|
|
463
484
|
}
|
|
@@ -485,7 +506,7 @@ export async function fetchPlanInventory(options, deps) {
|
|
|
485
506
|
return { kind: "unavailable", detail: `plan inventory read HTTP ${status}: ${bad}` };
|
|
486
507
|
}
|
|
487
508
|
const b = body;
|
|
488
|
-
return { kind: "ok", boards: b.boards, cardCount: b.
|
|
509
|
+
return { kind: "ok", boards: b.boards, cardCount: b.card_count };
|
|
489
510
|
}
|
|
490
511
|
/**
|
|
491
512
|
* DX-2920 — a stream admission refusal (`listen.ts`'s STRUCTURED `refused`
|
|
@@ -604,7 +625,8 @@ export async function runBridge(options, deps) {
|
|
|
604
625
|
let connectionState = "reconnecting";
|
|
605
626
|
let stoppedReason = null;
|
|
606
627
|
let heartbeatSuppressed = false;
|
|
607
|
-
|
|
628
|
+
// DX-3912 — `null` is "the inventory read succeeded"; a string is why it did not.
|
|
629
|
+
let lastInventoryError = null;
|
|
608
630
|
// DX-3099 — consecutive heartbeat POSTs that failed to LAND while this
|
|
609
631
|
// process still believed `connectionState === "streaming"`. Reset to 0 the
|
|
610
632
|
// moment a heartbeat lands, or the moment state leaves "streaming" for any
|
|
@@ -619,7 +641,9 @@ export async function runBridge(options, deps) {
|
|
|
619
641
|
// process, so an in-process re-mint never touches its state.
|
|
620
642
|
const sessionStats = new OperatorSessionStats(options.transcriptPath ?? null, deps.makeStatsWatcher);
|
|
621
643
|
const connectionNow = () => connectionState === "streaming"
|
|
622
|
-
?
|
|
644
|
+
? lastInventoryError === null
|
|
645
|
+
? { state: "streaming", inventory: "ok" }
|
|
646
|
+
: { state: "streaming", inventory: "unavailable", reason: oneLine(lastInventoryError, HEARTBEAT_REASON_MAX_CODE_POINTS) }
|
|
623
647
|
: connectionState === "stopped"
|
|
624
648
|
? { state: "stopped", reason: stoppedReason ?? undefined }
|
|
625
649
|
: { state: "reconnecting" };
|
|
@@ -822,23 +846,24 @@ export async function runBridge(options, deps) {
|
|
|
822
846
|
readyEmitted = true;
|
|
823
847
|
const inventory = await fetchPlanInventory(options, deps);
|
|
824
848
|
// DX-3028 (AC1, code-review + architecture-review finding) — this is
|
|
825
|
-
// the ONE place `
|
|
849
|
+
// the ONE place `lastInventoryError` is ever written; the 15s heartbeat
|
|
826
850
|
// reads it on every "streaming" tick (see `heartbeatTick` above). Must
|
|
827
851
|
// agree with the SAME `ready` record's own `degraded` decision below —
|
|
828
852
|
// a heartbeat reporting `inventory:"ok"` while the `ready` record just
|
|
829
853
|
// told the operator `degraded:true` would be exactly the untruthful
|
|
830
854
|
// state AC1 exists to prevent.
|
|
831
|
-
|
|
855
|
+
lastInventoryError = inventory.kind === "ok" ? null : inventory.detail;
|
|
832
856
|
if (inventory.kind === "ok") {
|
|
833
857
|
deps.write({
|
|
834
858
|
type: "ready",
|
|
835
859
|
boards: inventory.boards,
|
|
836
860
|
cardCount: inventory.cardCount,
|
|
837
861
|
degraded: inventory.boards.length === 0 || inventory.cardCount === 0,
|
|
862
|
+
inventoryError: null,
|
|
838
863
|
});
|
|
839
864
|
}
|
|
840
865
|
else {
|
|
841
|
-
deps.write({ type: "ready", boards: null, cardCount: null, degraded: true });
|
|
866
|
+
deps.write({ type: "ready", boards: null, cardCount: null, degraded: true, inventoryError: inventory.detail });
|
|
842
867
|
}
|
|
843
868
|
}
|
|
844
869
|
backoff = MINT_INITIAL_BACKOFF_MS;
|
package/dist/credential.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and `danx-dashboard-mcp bridge` (the session's event stream, run by the
|
|
7
7
|
* danxbot plugin's plan event bridge). Before this module they resolved their
|
|
8
8
|
* credential independently — the server through danxbot's operator launcher,
|
|
9
|
-
* which read a token file, and the bridge from whatever
|
|
9
|
+
* which read a token file, and the bridge from whatever credential variable
|
|
10
10
|
* happened to be in the session's environment. On a machine where those two
|
|
11
11
|
* differ (DX-2862: `ffe9f8a882f0…` in the environment, `6f9bc6e94b83…` in
|
|
12
12
|
* `~/.config/danxbot/dashboard-token-gpt`) the session's tools worked, the
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
* home). The file's trimmed contents are the credential. This is how an
|
|
22
22
|
* interactive operator session declares one, because its `.mcp.json` is
|
|
23
23
|
* git-tracked and can never carry the bearer itself.
|
|
24
|
-
* - `
|
|
24
|
+
* - `DANX_DASHBOARD_CREDENTIAL` — the credential verbatim. This is how a
|
|
25
25
|
* DISPATCH declares one: the worker substitutes it into the materialized
|
|
26
26
|
* `.mcp.json` from the dispatch overlay.
|
|
27
27
|
*
|
|
28
28
|
* A DECLARED FILE IS AUTHORITATIVE, AND ITS FAILURE IS FATAL. When the file
|
|
29
|
-
* variable is set, `
|
|
29
|
+
* variable is set, `DANX_DASHBOARD_CREDENTIAL` is never consulted — not even when
|
|
30
30
|
* the file is missing or empty. That is the DX-2483 rule, moved here from the
|
|
31
31
|
* launcher it used to live in: an operator machine routinely carries an
|
|
32
|
-
* inherited
|
|
32
|
+
* inherited env credential naming the LOCAL dev dashboard, which
|
|
33
33
|
* answers healthily and empty for a board it does not carry, so preferring it
|
|
34
34
|
* after a failed file read would turn a loud, fixable error into exactly the
|
|
35
35
|
* silent wrong-dashboard answer this whole module exists to prevent.
|
|
@@ -39,7 +39,7 @@ import { readFileSync } from "node:fs";
|
|
|
39
39
|
import { homedir } from "node:os";
|
|
40
40
|
import { isAbsolute, join, resolve } from "node:path";
|
|
41
41
|
export const CREDENTIAL_FILE_ENV = "DANXBOT_DASHBOARD_TOKEN_FILE";
|
|
42
|
-
export const CREDENTIAL_TOKEN_ENV = "
|
|
42
|
+
export const CREDENTIAL_TOKEN_ENV = "DANX_DASHBOARD_CREDENTIAL";
|
|
43
43
|
/**
|
|
44
44
|
* A credential that cannot be resolved. Carries the remedy beside the reason,
|
|
45
45
|
* because both of this module's callers report failures to a human who is not
|
package/dist/handlers.js
CHANGED
|
@@ -81,7 +81,7 @@ export function isDanxbotApiMethod(value) {
|
|
|
81
81
|
*
|
|
82
82
|
* The credential lives inside `client` (resolved once at boot — the
|
|
83
83
|
* operator's declared token file for a session, the per-dispatch
|
|
84
|
-
* `
|
|
84
|
+
* `DANX_DASHBOARD_CREDENTIAL` for a worker) and never appears in this
|
|
85
85
|
* function's arguments or return value — nothing in `DanxbotApiArgs` can
|
|
86
86
|
* carry one. The session id (`x-danx-session-id`) rides every call the SAME
|
|
87
87
|
* way it already does for every other tool on this server
|
package/dist/index.js
CHANGED
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
* session, whose `.mcp.json` is git-tracked and so
|
|
56
56
|
* can never carry one); authoritative when set, and
|
|
57
57
|
* its failure is fatal — an inherited
|
|
58
|
-
*
|
|
59
|
-
*
|
|
58
|
+
* DANX_DASHBOARD_CREDENTIAL is never consulted instead
|
|
59
|
+
* DANX_DASHBOARD_CREDENTIAL the bearer verbatim (a dispatch, substituted into
|
|
60
60
|
* the materialized `.mcp.json` from its overlay)
|
|
61
61
|
* DANX_REPO_NAME repo half of the qualified board id
|
|
62
62
|
* DANXBOT_BOARD_NAME board-slug half of the qualified board id
|
|
@@ -466,7 +466,7 @@ async function main() {
|
|
|
466
466
|
// the dashboard URL, credential source, and credential itself come from the
|
|
467
467
|
// session-connection record this server's own `plan_connect` wrote
|
|
468
468
|
// (`session-connection.ts`), never from the bridge's own environment
|
|
469
|
-
// (DX-2862: a bridge trusting its own ambient
|
|
469
|
+
// (DX-2862: a bridge trusting its own ambient env credential is the
|
|
470
470
|
// exact bug this repo fixed).
|
|
471
471
|
//
|
|
472
472
|
// Any OTHER argument is refused rather than ignored, so a typo cannot
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* in the SESSION's environment — not in the MCP server's, which is the only
|
|
7
7
|
* place the server's `.mcp.json` env block exists. So the bridge cannot see
|
|
8
8
|
* what the server was configured with, and before this it simply used its own
|
|
9
|
-
* ambient
|
|
9
|
+
* ambient env credential. When that was a different credential from
|
|
10
10
|
* the server's, every event was dropped silently (the card's incident).
|
|
11
11
|
*
|
|
12
12
|
* WHAT IS WRITTEN, AND WHAT IS NOT. On a successful `plan_connect` the server
|
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.192",
|
|
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",
|