@thehammer/danx-dashboard-mcp 0.1.53 → 0.1.54
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/handlers.js +14 -4
- package/dist/index.js +30 -30
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -808,13 +808,17 @@ export async function planConnect(client, args, listener) {
|
|
|
808
808
|
},
|
|
809
809
|
};
|
|
810
810
|
}
|
|
811
|
-
/** Add a goal, rule or caveat to the connected plan. */
|
|
811
|
+
/** Add a goal, rule or caveat to the connected plan. `context` is sent only when given. */
|
|
812
812
|
export async function planAddRecord(client, args) {
|
|
813
813
|
return client.request({
|
|
814
814
|
method: "POST",
|
|
815
815
|
path: "/mine/records",
|
|
816
816
|
basePath: PLANS_BASE_PATH,
|
|
817
|
-
body: {
|
|
817
|
+
body: {
|
|
818
|
+
kind: args.kind,
|
|
819
|
+
body: args.body,
|
|
820
|
+
...(args.context === undefined ? {} : { context: args.context }),
|
|
821
|
+
},
|
|
818
822
|
});
|
|
819
823
|
}
|
|
820
824
|
/** Add an existing card to the connected plan. */
|
|
@@ -883,7 +887,9 @@ export async function planGetRecord(client, args) {
|
|
|
883
887
|
* `plan_get_record`; the server compares it against the row's current hash
|
|
884
888
|
* and, on a mismatch, refuses the write ENTIRELY and fails loud with
|
|
885
889
|
* `{ok: false, status: 409, body: {error: "stale_plan_record", currentHash,
|
|
886
|
-
* currentBody}}` rather than overwriting whoever wrote in
|
|
890
|
+
* currentBody, currentContext}}` rather than overwriting whoever wrote in
|
|
891
|
+
* between. DX-2734: the hash covers body AND context, and `context` is sent
|
|
892
|
+
* only when given (omitted keeps the stored context, `null` clears it).
|
|
887
893
|
* `currentBody` rides the SAME refusal — unlike the architecture document's
|
|
888
894
|
* `stale_plan_architecture`, which carries only `currentHash` — so you can
|
|
889
895
|
* merge and retry in ONE round trip without a second `plan_get_record` call.
|
|
@@ -895,7 +901,11 @@ export async function planUpdateRecord(client, args) {
|
|
|
895
901
|
method: "PATCH",
|
|
896
902
|
path: `/mine/records/${args.record_id}`,
|
|
897
903
|
basePath: PLANS_BASE_PATH,
|
|
898
|
-
body: {
|
|
904
|
+
body: {
|
|
905
|
+
body: args.body,
|
|
906
|
+
content_hash: args.content_hash,
|
|
907
|
+
...(args.context === undefined ? {} : { context: args.context }),
|
|
908
|
+
},
|
|
899
909
|
});
|
|
900
910
|
}
|
|
901
911
|
/**
|
package/dist/index.js
CHANGED
|
@@ -310,7 +310,7 @@ server.tool("issue_get", "Fetch a single issue via GET /api/issues/:id. Board-sc
|
|
|
310
310
|
...boardField,
|
|
311
311
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
312
312
|
// ---------------- issue_create ----------------
|
|
313
|
-
server.tool("issue_create", 'Create a
|
|
313
|
+
server.tool("issue_create", 'Create a card via POST /api/issues on this dispatch\'s board, or another via `board` (`<repo>:<slug>`; unknown → 404). type=Epic REQUIRES non-empty phase_children[] (epic and phases inserted in one transaction; children get the epic as parent); other types refuse phase_children[] (400). Status starts at Review. `list_id` places the card straight into a column — a board_lists id or the list\'s display NAME (case-insensitive, emoji-tolerant): a `ready`-type queue lands it in ToDo, a `completed` list in Done, with no follow-up transition. Not valid on Epic; unknown name/id → 400. `gate_decisions` is REQUIRED when the board has an OPTIONAL quality gate for the card\'s type: a missing one fails closed with 400 `{error, required_gate_decisions:[...]}` naming each gate — retry with one `{gate, enabled, note}` per listed gate. ALWAYS pass `triage_enabled` explicitly on the root card and every phase child: true only when it should enter automatic triage/dispatch without human review; absent → false.', {
|
|
314
314
|
type: z.enum(ISSUE_TYPES),
|
|
315
315
|
title: z.string().min(1).describe(TITLE_DESCRIBE),
|
|
316
316
|
summary: z.string().min(1).optional().describe(SUMMARY_DESCRIBE),
|
|
@@ -327,7 +327,7 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
327
327
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
328
328
|
}))
|
|
329
329
|
.optional()
|
|
330
|
-
.describe(
|
|
330
|
+
.describe("One {gate, enabled, note} per board-OPTIONAL gate of the card's type: `enabled` = does it run on this card, `note` = why. Board `required` gates always run and `disabled` never do; neither takes a decision (naming one → 400). Optional `effort_level` overrides a `plan-*` gate's reviewer rung."),
|
|
331
331
|
phase_children: z
|
|
332
332
|
.array(z.object({
|
|
333
333
|
type: z.enum(NON_EPIC_TYPES),
|
|
@@ -348,21 +348,21 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
348
348
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
349
349
|
}))
|
|
350
350
|
.optional()
|
|
351
|
-
.describe("
|
|
351
|
+
.describe("Same as the root gate_decisions, resolved against THIS child's type."),
|
|
352
352
|
triage_enabled: z
|
|
353
353
|
.boolean()
|
|
354
354
|
.optional()
|
|
355
|
-
.describe("
|
|
355
|
+
.describe("ALWAYS pass per child; absent → false (never auto-triaged). Not inherited from the root."),
|
|
356
356
|
}))
|
|
357
357
|
.optional(),
|
|
358
358
|
triage_enabled: z
|
|
359
359
|
.boolean()
|
|
360
360
|
.optional()
|
|
361
|
-
.describe("
|
|
361
|
+
.describe("ALWAYS pass explicitly. true = enters automatic triage/dispatch without further human review; absent → false. Operator POST /api/triage and issue_triage ignore it."),
|
|
362
362
|
...boardField,
|
|
363
363
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
364
364
|
// ---------------- issue_edit ----------------
|
|
365
|
-
server.tool("issue_edit", 'Patch
|
|
365
|
+
server.tool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED keys: title, summary, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type. Any other key (lifecycle, triage, dependencies, retro, requires_human, blocked) is refused 400 with offending_keys[] naming the right tool: issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro. `type`: Story/Bug/Chore makes a card eligible for autonomous pickup; Task or a container (Epic/Feature) removes that eligibility — how a planning item becomes work. `priority` (a tier word or a number) is the ONLY way to set priority; a "Priority:" line in the description changes nothing. CHECKLISTS: each item has one status `incomplete|failing|passing|cancelled|deferred`; `deferred` (work done, a real-world/post-deploy check outstanding) REQUIRES `detail`, and a `📡`-prefixed item can never be `passing`. `ac` edits the default "Acceptance Criteria" checklist (items matched by check_item_id, else exact title); `checklists` REPLACES every named checklist with full status control (`{name, items:[{label, detail?, status}]}`). Send `ac` OR `checklists`, not both (400). `list_id` pins the card to a list by id or display NAME; its type must match the card\'s current derived status (400 otherwise — ready a card before pinning it to a `ready` queue); null clears the pin.', {
|
|
366
366
|
id: z.string().min(1),
|
|
367
367
|
title: z.string().min(1).optional().describe(TITLE_DESCRIBE),
|
|
368
368
|
summary: z
|
|
@@ -375,7 +375,7 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
|
|
|
375
375
|
type: z
|
|
376
376
|
.enum(ISSUE_TYPES)
|
|
377
377
|
.optional()
|
|
378
|
-
.describe("
|
|
378
|
+
.describe("Story/Bug/Chore = dispatchable; Task/Epic/Feature = never dispatched, in any status."),
|
|
379
379
|
ac: z
|
|
380
380
|
.array(z.object({
|
|
381
381
|
title: z.string(),
|
|
@@ -383,7 +383,7 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
|
|
|
383
383
|
status: z
|
|
384
384
|
.enum(CHECKLIST_ITEM_STATUSES)
|
|
385
385
|
.optional()
|
|
386
|
-
.describe(
|
|
386
|
+
.describe("Optional full status; omitted → from `checked`. `deferred` REQUIRES a non-empty `detail`; a 📡 item can never be `passing`."),
|
|
387
387
|
detail: z
|
|
388
388
|
.string()
|
|
389
389
|
.optional()
|
|
@@ -391,10 +391,10 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
|
|
|
391
391
|
check_item_id: z
|
|
392
392
|
.union([z.string(), z.number()])
|
|
393
393
|
.optional()
|
|
394
|
-
.describe("
|
|
394
|
+
.describe("Optional id from issue_get's ac[].check_item_id. Only needed to tell apart two items with identical titles; otherwise items match by title."),
|
|
395
395
|
}))
|
|
396
396
|
.optional()
|
|
397
|
-
.describe("The
|
|
397
|
+
.describe("The default Acceptance Criteria checklist: checked true → passing, false → incomplete, or pass `status`. Diffed against live items — unchanged items keep their id, new titles are inserted, missing ones removed."),
|
|
398
398
|
checklists: z
|
|
399
399
|
.array(z.object({
|
|
400
400
|
name: z.string().min(1),
|
|
@@ -410,16 +410,16 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
|
|
|
410
410
|
priority: z
|
|
411
411
|
.union([z.enum(PRIORITY_TIER_WORDS), z.number()])
|
|
412
412
|
.optional()
|
|
413
|
-
.describe('
|
|
413
|
+
.describe('A tier word ("lowest"…"critical", resolved to the tier midpoint) or a number in [0,6). The only way to set priority.'),
|
|
414
414
|
list_id: z.string().min(1).nullable().optional(),
|
|
415
415
|
triage_enabled: z
|
|
416
416
|
.boolean()
|
|
417
417
|
.optional()
|
|
418
|
-
.describe("
|
|
418
|
+
.describe("Per-card opt-in to the automatic triage dispatcher (default false = never auto-selected). Operator POST /api/triage and issue_triage ignore it."),
|
|
419
419
|
...boardField,
|
|
420
420
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
421
421
|
// ---------------- issue_transition ----------------
|
|
422
|
-
server.tool("issue_transition", "
|
|
422
|
+
server.tool("issue_transition", "Move a card's lifecycle via POST /api/issues/:id/transition — the ONLY way to; `danxbot_complete` never moves a card, so call this BEFORE it. Actions: ready (Review→ToDo); pickup (ToDo→In Progress; checks every dispatch gate — ready, blocked, requires_human, depends_on terminal, conflict_on idle — and refuses 409 with failed_gate naming the cause; `manual:true` is an operator-session self-pickup that bypasses card-flow gates and is never auto-rolled-back: use it when the work happens in YOUR session); rollback_pickup; complete (your explicit decision; refuses 409 on an Epic with non-terminal children, non_terminal_phases[], or while a required POST quality gate is not pass, failed_gate 'quality_gate_post' + failed_post_gates[]); cancel (terminal); block (non-empty reason; the CARD needs a human — env faults use `danxbot_complete({status:'failed'})` instead); unblock; archive (to Backlog, clears ready_at); reopen (terminal→active). Terminal cards refuse everything but reopen; forward stamps never clear earlier ones. Every path into In Progress needs an identified claimer: a dispatched agent's manual pickup MUST pass `assigned_agent` = your agent/profile name (409 otherwise). A manual pickup that loses a race is refused 409 `failed_gate: \"dispatch_id\"` — re-read the card's assigned_agent/dispatch_id, never retry blindly. A successful block returns `solutions_reminder: {solution_count, instruction}`: before stopping, list EVERY viable solution with issue_solution; zero means you listed none.", {
|
|
423
423
|
id: z.string().min(1),
|
|
424
424
|
action: z.enum(TRANSITION_ACTIONS),
|
|
425
425
|
reason: z.string().optional(),
|
|
@@ -430,7 +430,7 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issu
|
|
|
430
430
|
.string()
|
|
431
431
|
.min(1)
|
|
432
432
|
.optional()
|
|
433
|
-
.describe("
|
|
433
|
+
.describe("Required for a dispatched agent's manual:true pickup: your agent/profile name, never the shared dispatch-token identity. Optional for a human session; ignored by other actions."),
|
|
434
434
|
...boardField,
|
|
435
435
|
}, async (args) => jsonResult(await issueTransition(client, args)));
|
|
436
436
|
// ---------------- issue_triage ----------------
|
|
@@ -513,7 +513,7 @@ server.tool("issue_requires_human", "Set or clear the requires_human dispatch ga
|
|
|
513
513
|
...boardField,
|
|
514
514
|
}, async (args) => jsonResult(await issueRequiresHuman(client, args)));
|
|
515
515
|
// ---------------- issue_quality_gate ----------------
|
|
516
|
-
server.tool("issue_quality_gate", "
|
|
516
|
+
server.tool("issue_quality_gate", "Set one card's per-gate `required` flag via POST /api/issues/:id/quality-gates/:gate {required} — the only post-create way (issue_create takes gate_decisions; issue_edit refuses gate keys). PRE `plan-*` gates run before the work dispatch; POST `code-*` gates block complete. Unknown gate → 400. The board state per gate is tri-state: `required` always runs, `optional` runs WHEN this flag is true (optional is NOT off), `disabled` never runs. Optional `effort_level` overrides a `plan-*` gate's reviewer rung (null clears it). The write always succeeds and returns `{issue, applied: true, effective, reason}` — read `effective` (does the gate now run) and `reason` (why the board overrode your value), not just the 200. Pass `board` to target another board.", {
|
|
517
517
|
id: z.string().min(1),
|
|
518
518
|
gate: z.enum([
|
|
519
519
|
"plan-dependency",
|
|
@@ -632,7 +632,7 @@ server.tool("brief_set_page", 'Write one Brief page via PUT /api/brief/page?slug
|
|
|
632
632
|
// also takes no plan id, but for a different reason: it MAKES a plan rather
|
|
633
633
|
// than acting on one, so there is no existing plan for an id to name yet.
|
|
634
634
|
server.tool("plan_list", "List every plan via GET /api/plans (DX-2683), and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped: a plan is a named, dated set of cards an operator assembled by hand, and its cards may come from any repository. Returns `{ok, status, body: {plans: [{id, name, createdAt, cardCount, boards}], session, sessionListenerAttached}}`. `session` is your own registration — `{sessionId, title, planId, planName, firstSeenAt, lastActiveAt}` — or `null` if this process is not running inside a Claude Code session. A `planId` of null means you are connected to no plan: read any plan with `plan_get`, then `plan_connect` to the one you are working on (or ask the operator to connect you from the Plans list). `sessionListenerAttached` says whether your event listener is running: `false` while connected to a plan means you will NOT hear about its cards — call `plan_connect` again and arm the Monitor it returns. NOTE this is NOT the board Brief (`brief_list`), which is a different feature entirely.", {}, async () => jsonResult(await planList(client)));
|
|
635
|
-
server.tool("plan_get", "Read one plan WHOLE via GET /api/plans (DX-2683) — its member cards (with the boards they cover), its goals, rules and caveats, its architecture document, the sessions working on it, and your own session state. One call, not five. Pass `plan_id` to read ANY plan (browsing another plan is useful and changes nothing); OMIT it to read the plan this session is connected to. Omitting it while connected to no plan fails loud with `{error: \"session_not_connected\"}` — connect first. Returns `{plan, cards, boards, records: {goal
|
|
635
|
+
server.tool("plan_get", "Read one plan WHOLE via GET /api/plans (DX-2683) — its member cards (with the boards they cover), its goals, rules and caveats, its architecture document, the sessions working on it, and your own session state. One call, not five. Pass `plan_id` to read ANY plan (browsing another plan is useful and changes nothing); OMIT it to read the plan this session is connected to. Omitting it while connected to no plan fails loud with `{error: \"session_not_connected\"}` — connect first. Returns `{plan, cards, boards, records: {goal, rule, caveat} (each `[{id, ref, body, context, contentHash}]` — `context` is markdown detail or null), architecture: {content, contentHash, updatedAt, updatedBy}, sessions, session, sessionListenerAttached}` — `sessionListenerAttached: false` while connected means your event listener is not running; call `plan_connect` again and arm the Monitor it returns. ALWAYS `plan_get` immediately before `plan_set_architecture` and pass the returned `architecture.contentHash` back as `base_hash`.", {
|
|
636
636
|
plan_id: z
|
|
637
637
|
.number()
|
|
638
638
|
.int()
|
|
@@ -646,27 +646,27 @@ server.tool("plan_create", "Create a new, empty plan via POST /api/plans (DX-253
|
|
|
646
646
|
server.tool("plan_connect", "Connect THIS session to a plan via POST /api/plan-sessions/me/plan (DX-2683) — the same binding the operator's Connect action writes, through the same server-side path. A session is connected to AT MOST ONE plan (enforced by the schema, not by convention); connecting while already on another plan MOVES you, and the response says which plan you left: `{ok, status, body: {session, movedFrom: {id, name} | null}}`. `movedFrom: null` means you were on no plan, or already on this one. It can only ever bind your OWN session — `me` is resolved from the session id this server forwards, never from anything you pass. After this, every plan WRITE tool acts on this plan, and no plan id is accepted anywhere. THE REPLY ALSO CARRIES `listener: {command, persistent: true, instruction}` — arm it IMMEDIATELY with the Monitor tool (`command` as given, `persistent: true`): from then on every comment, answer, requires_human change and block/unblock on this plan's cards arrives as a notification line like `[DX-8 \"Title\" repo:board] newms87 answered: chose \"Pause E2E\" — note: \"…\"`. Never poll for these. The command carries a narrow stream ticket, not a credential; calling plan_connect again (same plan is fine) issues a new one and ends the old listener, which is how you re-arm after a session restart or after the Monitor reports it gave up. If the ticket cannot be issued the call fails with `listener_not_armed` even though the connect itself happened.", {
|
|
647
647
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
648
648
|
}, async (args) => jsonResult(await planConnect(client, args, { baseUrl: config.baseUrl, packageSpec: PACKAGE_SPEC })));
|
|
649
|
-
server.tool("plan_add_record", "Add a
|
|
650
|
-
kind: z
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
body: z.string().min(1).describe("The record's text. Plain text, not markdown."),
|
|
649
|
+
server.tool("plan_add_record", "Add a goal, rule or caveat to your connected plan (POST /api/plans/mine/records). A GOAL is an outcome the work is measured against. A RULE is a constraint that must hold while it is worked. A CAVEAT is a lasting trade-off or limitation of the ARCHITECTURE — never progress, status or a session note (those are comments on the card). `body` is ONE plain statement of at most 250 characters; the evidence, history and detail go in `context` (markdown). An overlong body is refused with a 400 naming its length. The server allocates a permanent reference (`G-1`, `R-4`, `CAV-12`). Takes no plan id; `session_not_connected` → `plan_connect` first. Returns the record plus that kind's list.", {
|
|
650
|
+
kind: z.enum(["goal", "rule", "caveat"]).describe("goal = outcome, rule = constraint, caveat = architecture trade-off."),
|
|
651
|
+
body: z.string().min(1).describe("One plain statement, at most 250 characters. Details go in `context`."),
|
|
652
|
+
context: z.string().optional().describe("Markdown detail behind the statement: evidence, history, examples."),
|
|
654
653
|
}, async (args) => jsonResult(await planAddRecord(client, args)));
|
|
655
|
-
server.tool("plan_get_record", "Read
|
|
654
|
+
server.tool("plan_get_record", "Read one goal/rule/caveat of your connected plan (GET /api/plans/mine/records/:rid) without pulling the whole plan. Takes no plan id; an unknown or another plan's record id → 404. Returns `{record: {id, planId, kind, refNum, ref, body, context, contentHash, createdAt, updatedAt}}` — `context` is the markdown detail, or null.", {
|
|
656
655
|
record_id: z.number().int().positive().describe("A record id, from `plan_add_record`, `plan_get` or `plan_get_record` itself."),
|
|
657
656
|
}, async (args) => jsonResult(await planGetRecord(client, args)));
|
|
658
|
-
server.tool("plan_update_record", 'Edit a goal/rule/caveat of
|
|
657
|
+
server.tool("plan_update_record", 'Edit a goal/rule/caveat of your connected plan (PATCH /api/plans/mine/records/:rid). The reference never moves. `body` stays ONE plain statement of at most 250 characters (400 otherwise); detail belongs in markdown `context`. `content_hash` must be the record\'s `contentHash` from your last read, and covers body AND context. On a mismatch nothing is written and you get `{error: "stale_plan_record", currentHash, currentBody, currentContext}`: merge into those and retry with `content_hash: currentHash`, never blindly. Takes no plan id. Returns the record plus that kind\'s list.', {
|
|
659
658
|
record_id: z.number().int().positive().describe("The record id to edit."),
|
|
660
|
-
content_hash: z
|
|
659
|
+
content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
|
|
660
|
+
body: z.string().min(1).describe("The new statement, at most 250 characters. Plain text."),
|
|
661
|
+
context: z
|
|
661
662
|
.string()
|
|
662
|
-
.
|
|
663
|
-
|
|
663
|
+
.nullable()
|
|
664
|
+
.optional()
|
|
665
|
+
.describe("New markdown detail. Omit to keep the stored context; null clears it."),
|
|
664
666
|
}, async (args) => jsonResult(await planUpdateRecord(client, args)));
|
|
665
|
-
server.tool("plan_delete_record", 'Soft-delete a goal/rule/caveat of
|
|
667
|
+
server.tool("plan_delete_record", 'Soft-delete a goal/rule/caveat of your connected plan (DELETE /api/plans/mine/records/:rid). Its reference is retired permanently, never reused. `content_hash` must be the record\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_record", currentHash, currentBody, currentContext}`. Takes no plan id. Unknown or already-deleted id → 404. Returns that kind\'s remaining list.', {
|
|
666
668
|
record_id: z.number().int().positive().describe("The record id to delete."),
|
|
667
|
-
content_hash: z
|
|
668
|
-
.string()
|
|
669
|
-
.describe("The record's `contentHash` from the immediately-prior read. Required — an absent hash is not read as a wildcard."),
|
|
669
|
+
content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
|
|
670
670
|
}, async (args) => jsonResult(await planDeleteRecord(client, args)));
|
|
671
671
|
server.tool("plan_add_card", "Add an existing card to the plan this session is connected to, via POST /api/plans/mine/cards (DX-2683). The card may live on ANY board — that is what a plan is for. Idempotent: re-adding a card already on the plan is a no-op, not an error, and a card may sit in several plans at once. This adds MEMBERSHIP only; it never edits the card. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns the plan's full member list.", {
|
|
672
672
|
card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
|
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.54",
|
|
4
4
|
"description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|