@thehammer/danx-dashboard-mcp
v0.1.85
Published
Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).
Downloads
1,522
Readme
@thehammer/danx-dashboard-mcp
Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes. Replaces the legacy "agents Edit/Write .yml files directly" pattern from before DX-704 / DX-811.
Each tool is a thin envelope over one HTTP route — Zod-validated at the MCP boundary, fetch under the hood, server response passed back to the agent verbatim. Refusal envelopes ({error, ...extra} with failed_gate, non_terminal_phases, offending_keys, etc.) come through as {ok: false, status, body} so the agent can pick the right next action without guessing. 5xx and network failures throw.
Env vars (required, fail-loud at boot)
| Var | Purpose |
|---|---|
| DANXBOT_DASHBOARD_URL | Dashboard base, e.g. http://danxbot-dashboard:5555 (inside compose) or http://localhost:5555 (host) |
| DANXBOT_DASHBOARD_TOKEN_FILE or DANXBOT_DISPATCH_TOKEN | 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 DANXBOT_DISPATCH_TOKEN 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 |
| DANX_REPO_NAME | Repo half of the qualified board id |
| DANXBOT_BOARD_NAME | Board-slug half of the qualified board id (the dispatch injects board.slug here) |
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.
Tool surface
All exposed as mcp__danx-dashboard__<name> once wired through the workspace mcp.template.json.
| Tool | HTTP | Notes |
|---|---|---|
| issue_list | GET /api/issues | filters: type, parent_id (null → root-only), dispatchable_derived, assigned_agent, include_closed, limit, offset |
| issue_get | GET /api/issues/:id or GET /api/issues/batch | Pass id for one card, or ids[] (DX-2727, at most 100) to resolve many across boards in ONE call — global, so ids with board throws; per-id not_found rather than a whole-call 404. Minimal scalars by default; fields opts in |
| issue_create | POST /api/issues | Epic REQUIRES non-empty phase_children[] (atomic insert). title = short domain-naming label; summary = 1–3 plain-language sentences, always shown; description = the collapsed "Context" body. Root and every phase child take their own summary |
| issue_edit | PATCH /api/issues/:id/edit | Prose + structured keys (title, summary (null clears), description, ac, checklists, effort_level, parent_id, priority, list_id); semantic keys refused with 400 + pointer to dedicated handler. priority (DX-1532) takes a tier word (low/high/…) or a number in [0,6) — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
| issue_transition | POST /api/issues/:id/transition | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen. block is a dispatch hold only — it never marks the card as needing a human; use issue_problem add for that |
| issue_problem | GET/POST/PATCH/DELETE /api/issues/:id/problems[/:pid] | Actions list / add / edit / remove. A problem is one statement the operator must resolve (a question or a plan flaw) with its own solutions and answers; add takes statement + optional solutions[] in one transaction and IS what puts the card in front of a human (open_problem_count > 0) — a successful add also returns problems_reminder: {open_problem_count, instruction}. Edit + remove are hash-guarded (base_hash, 409 stale_problem); removing the card's last open problem is always allowed — it just means the card no longer needs a human |
| issue_solution | POST/PATCH/DELETE /api/issues/:id/problems/:pid/solutions[/:sid] | Actions add / edit / remove, problem_id required (list via issue_problem list). Edit + remove are hash-guarded (base_hash, 409 stale_solution with the current row); at most one live recommended per problem; a chosen option cannot be edited. No answer action on any tool — the operator answers in the dashboard |
| issue_triage | POST /api/issues/:id/triage | Send {confidence, reason} — an integer 0-5 score; the server computes the verdict (approve/cancel/keep/defer) against the board's configured thresholds (DX-2086). keep/defer now block the card. None of these are a cross-card ordering gate; use issue_dependency to sequence cards |
| issue_comment | POST/PATCH/DELETE /api/issues/:id/comments[/:cid] | Author server-stamped, soft-delete preserved |
| issue_dependency | POST/DELETE /api/issues/:id/dependencies[/:did] | depends_on cycle-checked; remove hardcodes reason: "recorded_in_error". The only mechanism the dispatch picker enforces to sequence one card after another — status alone is not a substitute |
| issue_retire_branch | POST /api/issues/:id/card-branch-retire | Mark this card's own card/<id> branch RETIRED (unsafe to merge) — {reason}, by server-stamped. NO status/terminal gate (DX-2845): settable the instant an audit/split judges the branch unsafe, whatever the card's status. The dispatched worker reads this at its next bootstrap and forces origin/main instead of re-attaching to the retired content |
| issue_retro | PUT /api/issues/:id/retro | Requires terminal card; replace semantics. correctable_danxbot_problem + correctable_danxbot_problem_description (DX-2794) are REQUIRED on every write, like tests[] — answer honestly whether THIS dispatch hit a fixable problem in danxbot's own code/configuration (not "the card was hard"); a true+described retro starts exactly one automated repair |
plan_get — cheap by default, opt-in for the rest (DX-2727)
A bare plan_get (no fields) returns only the plan's cheap scalars — plan, boards, cardCount, bucketCounts, status, session, sessionListenerAttached — plus available_field_groups naming what else exists. Pass fields to opt into cards (paged: cards_offset, default 0, and cards_limit, 1..1000, default 200, pick the page; the response carries cards_total and cards_offset, so page with cards_offset while cards_offset + cards.length < cards_total — either paging arg without cards in fields is a 400), records (every goal/rule/caveat) or records:goal / records:rule / records:caveat (just one kind), architecture, and sessions. A plan_get made only to grab a hash before a one-line edit no longer pays for the architecture document or every member card.
plan_list / plan_get — computed status (DX-2834)
Every plan carries a status, computed fresh on every read and never stored — no writer ever sets it:
| Status | Meaning |
|---|---|
| complete | At least one card, and every card Done or Cancelled. Wins even with no session — a finished plan needs nobody. |
| awaiting-session | Not complete, and no session is LIVE on the plan. A plan_sessions row is never released when a session merely ends (only when the plan itself is deleted), so this checks real liveness — last_active_at within the last minute — never just "has a session ever connected". A working session's event stream stamps that activity every 15 seconds on its own, so an open, idle session stays live with no tool calls. |
| building | Not complete, a live session is connected, and at least one card is ToDo/In Progress, or carries an unnamed active state (Blocked, Needs Help — i.e. an OPEN problem, DX-2830 — and, once it exists, Verify): work started and is either moving or stuck. |
| planning | Everything else: no cards, or only Review/Backlog/terminal cards with at least one not Done/Cancelled. |
Evaluated in that order (complete > awaiting-session > building > planning) — first match wins. plan_list takes an optional status arg to filter to one of them (GET /api/plans?status=); plan_get always returns the one plan's own status, ungated.
bridge — a working session's event stream client
A program runs this, not an agent: the danxbot Claude Code plugin's plan event bridge spawns bridge for a session, relays each event's text into the session, and stores the delivered ids. plan_connect only connects the session; it mints no ticket.
CLAUDE_CODE_SESSION_ID=<session> npx -y @thehammer/danx-dashboard-mcp@<version> bridge [--resume-ids <id>,<id>,...]- Credential — the session's own, never the ambient one (DX-2862). The session id is the only thing
bridgetakes from its environment;--resume-idsis the only argument and holds no secret. Everything else comes from the connection record this session's own danx-dashboard MCP server wrote onplan_connect(src/session-connection.ts): the dashboard URL, the credential's SOURCE, and its FINGERPRINT.bridgeresolves 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 whateverDANXBOT_DISPATCH_TOKENthe 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. - No client-side reach check (DX-2920).
bridgeused to prove board reach itself, before ever opening the stream (GET /api/plans/mine?fields=cards+ a per-boardGET /api/issues/:id/problems). That check is DELETED — the dashboard (DX-2863) now enforces the identical requirement server-side, and a second client-side copy of the same check was a duplicate source of truth, not a safety net: it refuses a mint (403 issuer_cannot_read_plan_boards), refuses stream admission (the SAME 403), and ends a live stream (event: end,{"reason":"scope_narrowed","boards":[…]}) the instant it stops being true.bridgeonly MAPS those refusals now — see "Stopping" below — it never independently verifies anything before streaming. Once a ticket is minted,bridgeemits, once,{"type":"ready"}— noboardsfield, since reach is no longer proven client-side. - Ticket.
bridgemints 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. - Output — JSON Lines. One
{"type":"event","id":<id|null>,"text":"…"}per event on the connected plan's cards, wheretextis[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 acould not read eventtext. Nothing for keep-alives, reconnects or re-mints. The session's own writes are never echoed back to it. - Stopping. Last, one
{"type":"stopped","reason":"…","detail":"…","fix":"…"}and exit, only on a terminal outcome.fixis the remedy shown to the session (STOP_FIXESinsrc/bridge.ts— never a log-only hint), so a session that hits a stop it cannot otherwise see still knows what to do about it. Terminal reasons:no_connection_record/credential_unavailable/credential_mismatch(the start-time checks above),not_connected(a409 session_not_connectedat mint OR at stream admission, or a liveend("not_connected")— all three mapped identically),unauthorized(401/403 for a reason OTHER than an unreadable board),mint_refused(any other non-transient mint refusal),mint_bad_response,board_unreadable(a403 issuer_cannot_read_plan_boardsat mint OR at stream admission, naming the unreadable boards — DX-2920),scope_narrowed(a LIVE stream'send("scope_narrowed", boards)— the server'send()REQUIRESboardsfor this reason at the type level,bridgeparses them off the wire and names them indetail/fix, never a generic message),bad_end_payload(ascope_narrowedend whoseboardsarray was missing or malformed — a protocol error surfaced loudly, never silently downgraded to a boards-lessscope_narrowed),superseded/replaced(exit 0 — a newer or replacing connection already serves this session, sofixsays no action is needed),revoked, orrefused(two freshly minted tickets refused in a row with no more specific classification). A transient mint failure (network, timeout, 408, 429, 5xx) backs off and retries; a lapsed ticket lease re-mints. - Reconnect and resume. A read-idle timeout (three missed keep-alives) turns a silently dead connection into a drop. Capped exponential backoff (1s → 30s) that resets only after a healthy connection, with
Last-Event-ID, so a dashboard restart replays what was missed and nothing is emitted twice.--resume-idscarries the same guarantee across a process restart: the ids already delivered seed the duplicate guard, and the highest is the firstLast-Event-ID. The dashboard floors that replay at the later of the session's first ticket and when it joined its current plan, so a restart loses nothing and a plan move replays nothing from before the move.
Build + test
cd packages/danx-dashboard-mcp
npm install
npm test
npm run build # → dist/index.js (executable)Publishing
Out of scope for this package's own code — the make publish-danx-dashboard-mcp target in the danxbot root Makefile owns version bump + npm publish + propagation wait. Standing operator authorization per ~/.claude/skills/thehammer-publish/.
