@thehammer/danx-dashboard-mcp 0.1.61 → 0.1.63
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/handlers.js +48 -2
- package/dist/index.js +19 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
|
|
|
22
22
|
| Tool | HTTP | Notes |
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
|
|
25
|
-
| `issue_get` | `GET /api/issues/:id` or `GET /api/issues/batch` | Pass `id` for one card, or `ids[]` (DX-2727) to resolve many across boards in ONE call — global,
|
|
25
|
+
| `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 |
|
|
26
26
|
| `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` |
|
|
27
27
|
| `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 |
|
|
28
28
|
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen. A successful `block` also returns `solutions_reminder: {solution_count, instruction}` |
|
|
@@ -35,7 +35,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
|
|
|
35
35
|
|
|
36
36
|
## `plan_get` — cheap by default, opt-in for the rest (DX-2727)
|
|
37
37
|
|
|
38
|
-
A bare `plan_get` (no `fields`) returns only the plan's cheap scalars — `plan`, `boards`, `cardCount`, `bucketCounts`, `session`, `sessionListenerAttached` — plus `available_field_groups` naming what else exists. Pass `fields` to opt into `cards` (
|
|
38
|
+
A bare `plan_get` (no `fields`) returns only the plan's cheap scalars — `plan`, `boards`, `cardCount`, `bucketCounts`, `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.
|
|
39
39
|
|
|
40
40
|
## `listen` — a working session's event listener
|
|
41
41
|
|
package/dist/handlers.js
CHANGED
|
@@ -49,19 +49,43 @@ export async function issueList(client, args) {
|
|
|
49
49
|
// DX-1171 — board-only: forward the qualified board id, no repo.
|
|
50
50
|
return client.request({ method: "GET", path: "", query, board: args.board });
|
|
51
51
|
}
|
|
52
|
+
// ---------------- issue_get ----------------
|
|
53
|
+
/**
|
|
54
|
+
* The most distinct ids one batch `issue_get` may name — mirrors the server's
|
|
55
|
+
* `src/issues/list-page.ts#ISSUE_BATCH_GET_MAX` (the batch route 400s above it);
|
|
56
|
+
* `__tests__/handlers.test.ts` asserts the two agree.
|
|
57
|
+
*/
|
|
58
|
+
export const ISSUE_BATCH_GET_MAX = 100;
|
|
59
|
+
/**
|
|
60
|
+
* The largest page any paged list read accepts (`issue_list`'s `limit`,
|
|
61
|
+
* `plan_get`'s `cards_limit`) — mirrors the server's
|
|
62
|
+
* `src/issues/list-page.ts#MAX_LIMIT` (it 400s above it);
|
|
63
|
+
* `__tests__/handlers.test.ts` asserts the two agree.
|
|
64
|
+
*/
|
|
65
|
+
export const LIST_PAGE_MAX_LIMIT = 1000;
|
|
66
|
+
/**
|
|
67
|
+
* `plan_get`'s `cards_limit` when omitted — mirrors the server's
|
|
68
|
+
* `src/issues/list-page.ts#PLAN_GET_CARDS_DEFAULT_LIMIT`;
|
|
69
|
+
* `__tests__/handlers.test.ts` asserts the two agree.
|
|
70
|
+
*/
|
|
71
|
+
export const PLAN_GET_CARDS_DEFAULT_LIMIT = 200;
|
|
52
72
|
/**
|
|
53
73
|
* Fetch one card via `GET /api/issues/:id`, or many via
|
|
54
74
|
* `GET /api/issues/batch?ids=...` (DX-2727) — a real batch read that keeps
|
|
55
75
|
* the single-id route's global, cross-board resolution rather than routing
|
|
56
76
|
* through `issue_list` (board-scoped per call, and its filter has no `id`
|
|
57
77
|
* property). Exactly one of `id` / `ids` must be given; the batch form
|
|
58
|
-
* reports a per-id `not_found` list rather than failing the whole call
|
|
59
|
-
*
|
|
78
|
+
* reports a per-id `not_found` list rather than failing the whole call. The
|
|
79
|
+
* batch form is a global read with no board scope, so `ids` together with
|
|
80
|
+
* `board` is REFUSED here rather than `board` being silently dropped.
|
|
60
81
|
*/
|
|
61
82
|
export async function issueGet(client, args) {
|
|
62
83
|
if (args.id !== undefined && args.ids !== undefined) {
|
|
63
84
|
throw new Error("issue_get: pass exactly one of id or ids, not both");
|
|
64
85
|
}
|
|
86
|
+
if (args.ids !== undefined && args.board !== undefined) {
|
|
87
|
+
throw new Error("issue_get: board does not apply to the ids batch form — it resolves every id globally; drop board");
|
|
88
|
+
}
|
|
65
89
|
const query = {};
|
|
66
90
|
if (args.fields !== undefined && args.fields.length > 0) {
|
|
67
91
|
query.fields = args.fields.join(",");
|
|
@@ -728,6 +752,24 @@ export async function issueAttach(client, args, deps = {}) {
|
|
|
728
752
|
/* ── Plans: what a connected session may read and add to (DX-2683) ────────── */
|
|
729
753
|
const PLANS_BASE_PATH = "/api/plans";
|
|
730
754
|
const PLAN_SESSIONS_BASE_PATH = "/api/plan-sessions";
|
|
755
|
+
/**
|
|
756
|
+
* DX-2727 — the plan_get field-group taxonomy: the ONE copy in this package
|
|
757
|
+
* (the `plan_get` zod enum in `index.ts` reads this const, and the type is
|
|
758
|
+
* derived from it). The published package cannot import server source at
|
|
759
|
+
* runtime, so it stays a literal; `__tests__/handlers.test.ts` imports the
|
|
760
|
+
* server's `src/issues/plan-field-groups.ts#PLAN_FIELD_GROUPS` and asserts
|
|
761
|
+
* the two are equal, so drift fails a test rather than a live call.
|
|
762
|
+
* `records` is the union of every kind; `records:<kind>` narrows to one.
|
|
763
|
+
*/
|
|
764
|
+
export const PLAN_FIELD_GROUPS = [
|
|
765
|
+
"cards",
|
|
766
|
+
"records",
|
|
767
|
+
"records:goal",
|
|
768
|
+
"records:rule",
|
|
769
|
+
"records:caveat",
|
|
770
|
+
"architecture",
|
|
771
|
+
"sessions",
|
|
772
|
+
];
|
|
731
773
|
/** Every plan, plus which one THIS session is connected to. */
|
|
732
774
|
export async function planList(client) {
|
|
733
775
|
return client.request({ method: "GET", path: "", basePath: PLANS_BASE_PATH });
|
|
@@ -744,6 +786,10 @@ export async function planGet(client, args = {}) {
|
|
|
744
786
|
if (args.fields !== undefined && args.fields.length > 0) {
|
|
745
787
|
query.fields = args.fields.join(",");
|
|
746
788
|
}
|
|
789
|
+
if (args.cards_offset !== undefined)
|
|
790
|
+
query.cards_offset = args.cards_offset;
|
|
791
|
+
if (args.cards_limit !== undefined)
|
|
792
|
+
query.cards_limit = args.cards_limit;
|
|
747
793
|
return client.request({
|
|
748
794
|
method: "GET",
|
|
749
795
|
path: args.plan_id === undefined ? "/mine" : `/${args.plan_id}/full`,
|
package/dist/index.js
CHANGED
|
@@ -88,7 +88,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
88
88
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
89
89
|
import { z } from "zod";
|
|
90
90
|
import { DashboardHttpClient } from "./http-client.js";
|
|
91
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
91
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
92
92
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
93
93
|
function readEnvOrDie(name) {
|
|
94
94
|
const v = process.env[name];
|
|
@@ -231,18 +231,6 @@ const GET_FIELD_GROUPS = [
|
|
|
231
231
|
"mirrors",
|
|
232
232
|
"code_review_items",
|
|
233
233
|
];
|
|
234
|
-
// DX-2727 — `plan_get`'s field-group taxonomy, hand-copied from
|
|
235
|
-
// `src/issues/plan-field-groups.ts` (this package cannot import server
|
|
236
|
-
// source). `records` is every kind; `records:<kind>` narrows to just one.
|
|
237
|
-
const PLAN_FIELD_GROUPS = [
|
|
238
|
-
"cards",
|
|
239
|
-
"records",
|
|
240
|
-
"records:goal",
|
|
241
|
-
"records:rule",
|
|
242
|
-
"records:caveat",
|
|
243
|
-
"architecture",
|
|
244
|
-
"sessions",
|
|
245
|
-
];
|
|
246
234
|
const SORT_ORDERS = ["asc", "desc"];
|
|
247
235
|
const sortField = z
|
|
248
236
|
.array(z.object({
|
|
@@ -314,18 +302,19 @@ server.tool("issue_list", "List issues for the dispatch's board by default via G
|
|
|
314
302
|
.optional()
|
|
315
303
|
.describe("Opt-in field-GROUPS to add to the minimal default row: description (description + summary), solutions (solutions_count), ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, effort. Absent/empty = minimal scalars only — no joins."),
|
|
316
304
|
sort: sortField,
|
|
317
|
-
limit: z.number().int().positive().max(
|
|
305
|
+
limit: z.number().int().positive().max(LIST_PAGE_MAX_LIMIT).optional(),
|
|
318
306
|
offset: z.number().int().nonnegative().optional(),
|
|
319
307
|
...boardField,
|
|
320
308
|
}, async (args) => jsonResult(await issueList(client, args)));
|
|
321
309
|
// ---------------- issue_get ----------------
|
|
322
|
-
server.tool("issue_get", "Fetch one card via GET /api/issues/:id, or MANY via GET /api/issues/batch?ids=... (DX-2727) — pass exactly one of `id` (single) or `ids` (batch, an array). Board-scoped for the single form; defaults to the dispatch's board. Issue ids are globally unique, so BOTH forms resolve from any dispatch regardless of `board
|
|
310
|
+
server.tool("issue_get", "Fetch one card via GET /api/issues/:id, or MANY via GET /api/issues/batch?ids=... (DX-2727) — pass exactly one of `id` (single) or `ids` (batch, an array). Board-scoped for the single form; defaults to the dispatch's board. Issue ids are globally unique, so BOTH forms resolve from any dispatch regardless of `board`; the batch form is a global, cross-board read and REFUSES `board` (passing `ids` with `board` throws). The batch form takes at most " + ISSUE_BATCH_GET_MAX + " ids per call — split a larger set. DEFAULT RESPONSE IS MINIMAL (DX-935 / DX-937) — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash — DX-2741, the card's optimistic-concurrency token; pass it back as `content_hash` on an `issue_edit` touching title/description/checklists); no joined collections. Pass `fields` to opt into named field-GROUPS, applied per card in the batch form too: description (full description body + the plain-language summary), solutions (the card's live candidate solutions, each with its content_hash, + every operator answer in decisions[]), ac (acceptance-criteria + checklists model), comments (comments[]), retro (retro good/bad/action_items/commits), dependencies (waiting_on/conflict_on/blocked gate state), triage (triage history + ICE), requires_human (the requires_human gate + steps), assignment (dispatch/assigned_agent/lifecycle timestamps), quality_gates (DX-1177 — one row per registered gate {gate, required, status pending|pass|fail, completed_at, message}; a required PRE gate not yet `pass` pre-empts the work dispatch, and `issue_transition complete` refuses while a required POST gate row != pass), children (child id list + rollups), mirrors (external mirror sync state), code_review_items (code-review findings). Point any heavy read at the matching `fields` entry rather than assuming it's already on the row. Single form: 404 envelope on unknown id. Batch form: returns `{issues: [...found cards...], not_found: [...ids that didn't resolve...]}` — an unknown or soft-deleted id never fails the whole call, it just lands in `not_found` alongside every card that DID resolve.", {
|
|
323
311
|
id: z.string().min(1).optional().describe("A single card id. Mutually exclusive with `ids`."),
|
|
324
312
|
ids: z
|
|
325
313
|
.array(z.string().min(1))
|
|
326
314
|
.min(1)
|
|
315
|
+
.max(ISSUE_BATCH_GET_MAX)
|
|
327
316
|
.optional()
|
|
328
|
-
.describe(
|
|
317
|
+
.describe(`Batch form (DX-2727): every requested id (at most ${ISSUE_BATCH_GET_MAX}), resolved globally in ONE call. Mutually exclusive with \`id\`, and never combined with \`board\` (the batch is global; passing both throws). Reports not_found per id rather than failing the whole call.`),
|
|
329
318
|
fields: z
|
|
330
319
|
.array(z.enum(GET_FIELD_GROUPS))
|
|
331
320
|
.optional()
|
|
@@ -660,7 +649,7 @@ server.tool("brief_set_page", 'Write one Brief page via PUT /api/brief/page?slug
|
|
|
660
649
|
// also takes no plan id, but for a different reason: it MAKES a plan rather
|
|
661
650
|
// than acting on one, so there is no existing plan for an id to name yet.
|
|
662
651
|
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)));
|
|
663
|
-
server.tool("plan_get", "Read a plan via GET /api/plans (DX-2683). 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. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, session, sessionListenerAttached, available_field_groups}` — no member cards, no goals/rules/caveats, no architecture body. Pass `fields` to opt into the rest, one call at a time: `cards` (
|
|
652
|
+
server.tool("plan_get", "Read a plan via GET /api/plans (DX-2683). 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. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, session, sessionListenerAttached, available_field_groups}` — no member cards, no goals/rules/caveats, no architecture body. Pass `fields` to opt into the rest, one call at a time: `cards` (member cards, PAGED: `cards_offset` (default 0) and `cards_limit` (1.." + LIST_PAGE_MAX_LIMIT + ", default " + PLAN_GET_CARDS_DEFAULT_LIMIT + ") pick the page, in card-reference order (board prefix, then card number — stable while cards are edited, so pages never repeat or skip a card unless the plan's membership changes between reads), and the response carries `cards_total` and `cards_offset` — page with cards_offset while cards_offset + cards.length < cards_total; either paging arg without `fields: [\"cards\"]` is a 400), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (just that one kind — cheaper than the full union), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan). `session`/`sessionListenerAttached` (your own connection state) and `available_field_groups` ride EVERY response, gated or not. `sessionListenerAttached: false` while connected means your event listener is not running; call `plan_connect` again and arm the Monitor it returns. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
|
|
664
653
|
plan_id: z
|
|
665
654
|
.number()
|
|
666
655
|
.int()
|
|
@@ -671,6 +660,19 @@ server.tool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id
|
|
|
671
660
|
.array(z.enum(PLAN_FIELD_GROUPS))
|
|
672
661
|
.optional()
|
|
673
662
|
.describe("Opt-in field-GROUPS: cards, records (every kind) or records:goal/records:rule/records:caveat (one kind), architecture, sessions. Absent/empty = cheap scalars only (plan, boards, cardCount, bucketCounts) plus session state."),
|
|
663
|
+
cards_offset: z
|
|
664
|
+
.number()
|
|
665
|
+
.int()
|
|
666
|
+
.nonnegative()
|
|
667
|
+
.optional()
|
|
668
|
+
.describe("Where the `cards` page starts (default 0). Requires `fields` to include `cards`. Page with cards_offset while cards_offset + cards.length < cards_total."),
|
|
669
|
+
cards_limit: z
|
|
670
|
+
.number()
|
|
671
|
+
.int()
|
|
672
|
+
.positive()
|
|
673
|
+
.max(LIST_PAGE_MAX_LIMIT)
|
|
674
|
+
.optional()
|
|
675
|
+
.describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields` to include `cards`."),
|
|
674
676
|
}, async (args) => jsonResult(await planGet(client, args)));
|
|
675
677
|
server.tool("plan_create", "Create a new, empty plan via POST /api/plans (DX-2531). GLOBAL — a plan is not board-scoped, and this call adds no cards, no records, and no architecture sections; it does NOT connect any session to the new plan (call `plan_connect` separately, exactly as adding a card to a plan is its own separate step). Returns `{ok, status, body: {plan: {id, name, createdAt}}}`. Use the returned `plan.id` with `plan_connect` to start working on it, or with `plan_get({plan_id})` to browse it.", {
|
|
676
678
|
name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
|
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.63",
|
|
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",
|