@thehammer/danx-dashboard-mcp 0.1.57 → 0.1.58
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 +92 -13
- package/dist/index.js +37 -21
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -710,7 +710,7 @@ export async function planList(client) {
|
|
|
710
710
|
}
|
|
711
711
|
/**
|
|
712
712
|
* One plan, entire — its cards, the boards they cover, its goals + rules +
|
|
713
|
-
* caveats, its architecture
|
|
713
|
+
* caveats, its architecture sections, and the sessions working on it. ONE
|
|
714
714
|
* call rather than five, which is what keeps this tool surface small enough
|
|
715
715
|
* to be worth an agent's context.
|
|
716
716
|
*/
|
|
@@ -861,25 +861,104 @@ export async function planRename(client, args) {
|
|
|
861
861
|
});
|
|
862
862
|
}
|
|
863
863
|
/**
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
864
|
+
* Read ONE section of the plan this session is connected to, via
|
|
865
|
+
* `GET /api/plans/mine/architecture/sections/:sid`. TAKES NO PLAN ID: the
|
|
866
|
+
* plan is resolved from your connected session, same as `plan_add_record`.
|
|
867
|
+
* Useful for a targeted read (skip pulling the whole plan via `plan_get`)
|
|
868
|
+
* and for conflict recovery after a 409 `stale_plan_architecture_section` —
|
|
869
|
+
* though that refusal already carries `currentTitle`/`currentContent`, so a
|
|
870
|
+
* second read is rarely needed for that specific case. Not connected →
|
|
871
|
+
* `{error: "session_not_connected"}`. Unknown/foreign section id → 404.
|
|
869
872
|
*/
|
|
870
|
-
export async function
|
|
873
|
+
export async function planGetArchitectureSection(client, args) {
|
|
874
|
+
return client.request({
|
|
875
|
+
method: "GET",
|
|
876
|
+
path: `/mine/architecture/sections/${args.section_id}`,
|
|
877
|
+
basePath: PLANS_BASE_PATH,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Append a section to the connected plan's architecture, via
|
|
882
|
+
* `POST /api/plans/mine/architecture/sections`. TAKES NO PLAN ID, same as
|
|
883
|
+
* `plan_add_record`. The new section sorts after every existing live
|
|
884
|
+
* section — see `plan_reorder_architecture_section` to move it.
|
|
885
|
+
*/
|
|
886
|
+
export async function planAddArchitectureSection(client, args) {
|
|
887
|
+
return client.request({
|
|
888
|
+
method: "POST",
|
|
889
|
+
path: "/mine/architecture/sections",
|
|
890
|
+
basePath: PLANS_BASE_PATH,
|
|
891
|
+
body: { title: args.title, content: args.content },
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Edit a section's title and/or content, via
|
|
896
|
+
* `PATCH /api/plans/mine/architecture/sections/:sid`. Both `title` and
|
|
897
|
+
* `content` are OPTIONAL — send only whichever changed. `base_hash` MUST be
|
|
898
|
+
* the section's `contentHash` from the immediately-prior `plan_get`/
|
|
899
|
+
* `plan_get_architecture_section`; the server compares it against the row's
|
|
900
|
+
* current hash and, on a mismatch, refuses the write ENTIRELY and fails loud
|
|
901
|
+
* with `{ok: false, status: 409, body: {error:
|
|
902
|
+
* "stale_plan_architecture_section", currentHash, currentTitle,
|
|
903
|
+
* currentContent}}` rather than overwriting whoever wrote in between —
|
|
904
|
+
* `currentTitle`/`currentContent` ride the SAME refusal so you can merge and
|
|
905
|
+
* retry in ONE round trip. TAKES NO PLAN ID: the plan is resolved from your
|
|
906
|
+
* connected session, same as `plan_add_record`.
|
|
907
|
+
*/
|
|
908
|
+
export async function planUpdateArchitectureSection(client, args) {
|
|
909
|
+
return client.request({
|
|
910
|
+
method: "PATCH",
|
|
911
|
+
path: `/mine/architecture/sections/${args.section_id}`,
|
|
912
|
+
basePath: PLANS_BASE_PATH,
|
|
913
|
+
body: {
|
|
914
|
+
base_hash: args.base_hash,
|
|
915
|
+
...(args.title === undefined ? {} : { title: args.title }),
|
|
916
|
+
...(args.content === undefined ? {} : { content: args.content }),
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Soft-delete a section of the connected plan's architecture, via
|
|
922
|
+
* `DELETE /api/plans/mine/architecture/sections/:sid`. `base_hash` MUST be
|
|
923
|
+
* the section's `contentHash` from the immediately-prior read; a stale hash
|
|
924
|
+
* refuses the delete ENTIRELY (nothing is removed) with the same
|
|
925
|
+
* `{ok: false, status: 409, body: {error: "stale_plan_architecture_section",
|
|
926
|
+
* currentHash, currentTitle, currentContent}}` shape
|
|
927
|
+
* `plan_update_architecture_section` uses — on a mismatch, re-fetch and
|
|
928
|
+
* confirm this is still the section you meant to remove before retrying,
|
|
929
|
+
* never blindly re-send with the fresh hash. TAKES NO PLAN ID. Unknown/
|
|
930
|
+
* foreign/already-deleted section id → 404.
|
|
931
|
+
*/
|
|
932
|
+
export async function planDeleteArchitectureSection(client, args) {
|
|
933
|
+
return client.request({
|
|
934
|
+
method: "DELETE",
|
|
935
|
+
path: `/mine/architecture/sections/${args.section_id}`,
|
|
936
|
+
basePath: PLANS_BASE_PATH,
|
|
937
|
+
body: { base_hash: args.base_hash },
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Reassign the connected plan's section display order, via
|
|
942
|
+
* `PUT /api/plans/mine/architecture/sections/reorder`. UNGUARDED by content
|
|
943
|
+
* hash, by design: moving a section never changes its (or any other
|
|
944
|
+
* section's) `contentHash`. `order` must name exactly the plan's current
|
|
945
|
+
* live section ids, each once — a partial or foreign list is refused with a
|
|
946
|
+
* 400 rather than silently reordering a subset or dropping a section from
|
|
947
|
+
* view. TAKES NO PLAN ID.
|
|
948
|
+
*/
|
|
949
|
+
export async function planReorderArchitectureSection(client, args) {
|
|
871
950
|
return client.request({
|
|
872
951
|
method: "PUT",
|
|
873
|
-
path: "/mine/architecture",
|
|
952
|
+
path: "/mine/architecture/sections/reorder",
|
|
874
953
|
basePath: PLANS_BASE_PATH,
|
|
875
|
-
body: {
|
|
954
|
+
body: { order: args.order },
|
|
876
955
|
});
|
|
877
956
|
}
|
|
878
957
|
/**
|
|
879
958
|
* Create a new, empty plan via `POST /api/plans`. GLOBAL — a plan is not
|
|
880
959
|
* board-scoped, so this takes no board and creates no membership; the caller
|
|
881
|
-
* still owns zero cards, zero records and
|
|
882
|
-
* adds them. This does NOT connect any session to the new plan — call
|
|
960
|
+
* still owns zero cards, zero records and zero architecture sections until
|
|
961
|
+
* it adds them. This does NOT connect any session to the new plan — call
|
|
883
962
|
* `plan_connect` separately (mirroring how creating a card does not add it to
|
|
884
963
|
* a plan; these are two deliberately separate steps, same as everywhere else
|
|
885
964
|
* in this tool surface).
|
|
@@ -920,8 +999,8 @@ export async function planGetRecord(client, args) {
|
|
|
920
999
|
* currentBody, currentContext}}` rather than overwriting whoever wrote in
|
|
921
1000
|
* between. DX-2734: the hash covers body AND context, and `context` is sent
|
|
922
1001
|
* only when given (omitted keeps the stored context, `null` clears it).
|
|
923
|
-
* `currentBody` rides the SAME refusal —
|
|
924
|
-
* `
|
|
1002
|
+
* `currentBody` rides the SAME refusal — the same richer shape
|
|
1003
|
+
* `stale_plan_architecture_section` carries for a section — so you can
|
|
925
1004
|
* merge and retry in ONE round trip without a second `plan_get_record` call.
|
|
926
1005
|
* TAKES NO PLAN ID: the plan is resolved from your connected session, same as
|
|
927
1006
|
* `plan_add_record`. Not connected → `{error: "session_not_connected"}`.
|
package/dist/index.js
CHANGED
|
@@ -43,7 +43,11 @@
|
|
|
43
43
|
* - plan_add_card POST /api/plans/mine/cards
|
|
44
44
|
* - plan_remove_card DELETE /api/plans/:plan_id/cards/:card_id (DX-2740)
|
|
45
45
|
* - plan_rename PATCH /api/plans/:plan_id (DX-2740)
|
|
46
|
-
* -
|
|
46
|
+
* - plan_get_architecture_section GET /api/plans/mine/architecture/sections/:sid (DX-2726)
|
|
47
|
+
* - plan_add_architecture_section POST /api/plans/mine/architecture/sections (DX-2726)
|
|
48
|
+
* - plan_update_architecture_section PATCH /api/plans/mine/architecture/sections/:sid (DX-2726)
|
|
49
|
+
* - plan_delete_architecture_section DELETE /api/plans/mine/architecture/sections/:sid (DX-2726)
|
|
50
|
+
* - plan_reorder_architecture_section PUT /api/plans/mine/architecture/sections/reorder (DX-2726)
|
|
47
51
|
*
|
|
48
52
|
* DX-2683 — THE PLAN TOOLS ARE SESSION-BOUND, and asymmetrically so. Reads
|
|
49
53
|
* may name any plan; WRITES take no plan id at all and act on the plan this
|
|
@@ -84,7 +88,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
84
88
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
85
89
|
import { z } from "zod";
|
|
86
90
|
import { DashboardHttpClient } from "./http-client.js";
|
|
87
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddCard, planAddRecord, planConnect, planCreate, planDeleteRecord, planGet, planGetRecord, planList, planRemoveCard, planRename,
|
|
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";
|
|
88
92
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
89
93
|
function readEnvOrDie(name) {
|
|
90
94
|
const v = process.env[name];
|
|
@@ -278,7 +282,7 @@ const TITLE_DESCRIBE = 'Short, specific label that names the domain, so a reader
|
|
|
278
282
|
const SUMMARY_DESCRIBE = "1–3 plain-language sentences — no markdown, no jargon — for someone who has never seen this codebase: what the card is about and why it matters. Always shown, never collapsed. It must stand on its own: not a second title, not a teaser for the description.";
|
|
279
283
|
const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, technical detail. Long and markdown is normal; UIs collapse it by default. Candidate answers to the question a card is stopped on do NOT go here — list each one with issue_solution.';
|
|
280
284
|
// ---------------- issue_list ----------------
|
|
281
|
-
server.tool("issue_list", "List issues for the dispatch's board by default via GET /api/issues. Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to list another board instead (unknown board → 404). Nested envelope (DX-935 / DX-937 — hard-cut, no flat params): `filter` — the OLD flat filters, now nested (type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, and `q` — free-text over id+title+description, the former standalone `q` param now lives at `filter.q`). `fields` — opt-in named field-GROUPS (description, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, effort); THE DEFAULT RESPONSE (no `fields`) IS MINIMAL — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent
|
|
285
|
+
server.tool("issue_list", "List issues for the dispatch's board by default via GET /api/issues. Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to list another board instead (unknown board → 404). Nested envelope (DX-935 / DX-937 — hard-cut, no flat params): `filter` — the OLD flat filters, now nested (type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, and `q` — free-text over id+title+description, the former standalone `q` param now lives at `filter.q`). `fields` — opt-in named field-GROUPS (description, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, effort); THE DEFAULT RESPONSE (no `fields`) IS MINIMAL — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent), zero joins. Point any heavy read (full description, comments[], retro, ac items, dependency edges, triage history, quality-gate rows, children ids) at the matching `fields` entry rather than assuming it's already on the row. `sort` — ordered [{column, order}] (id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at); absent → default order (priority desc, repo_name asc) with an always-appended numeric-id tiebreaker (DX-10 follows DX-9). `limit`/`offset` — optional paging (no cap by default). Use issue_get for a single fully-detailed card.", {
|
|
282
286
|
filter: z
|
|
283
287
|
.object({
|
|
284
288
|
q: z.string().optional(),
|
|
@@ -303,7 +307,7 @@ server.tool("issue_list", "List issues for the dispatch's board by default via G
|
|
|
303
307
|
...boardField,
|
|
304
308
|
}, async (args) => jsonResult(await issueList(client, args)));
|
|
305
309
|
// ---------------- issue_get ----------------
|
|
306
|
-
server.tool("issue_get", "Fetch a single issue via GET /api/issues/:id. Board-scoped; defaults to the dispatch's board. Issue ids are globally unique, so this resolves from any dispatch regardless of `board`. 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
|
|
310
|
+
server.tool("issue_get", "Fetch a single issue via GET /api/issues/:id. Board-scoped; defaults to the dispatch's board. Issue ids are globally unique, so this resolves from any dispatch regardless of `board`. 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); no joined collections. Pass `fields` to opt into named field-GROUPS: 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. 404 envelope on unknown id.", {
|
|
307
311
|
id: z.string().min(1),
|
|
308
312
|
fields: z
|
|
309
313
|
.array(z.enum(GET_FIELD_GROUPS))
|
|
@@ -364,7 +368,7 @@ server.tool("issue_create", 'Create a card via POST /api/issues on this dispatch
|
|
|
364
368
|
...boardField,
|
|
365
369
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
366
370
|
// ---------------- issue_edit ----------------
|
|
367
|
-
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
|
|
371
|
+
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.', {
|
|
368
372
|
id: z.string().min(1),
|
|
369
373
|
title: z.string().min(1).optional().describe(TITLE_DESCRIBE),
|
|
370
374
|
summary: z
|
|
@@ -418,11 +422,6 @@ server.tool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED
|
|
|
418
422
|
.boolean()
|
|
419
423
|
.optional()
|
|
420
424
|
.describe("Per-card opt-in to the automatic triage dispatcher (default false = never auto-selected). Operator POST /api/triage and issue_triage ignore it."),
|
|
421
|
-
content_hash: z
|
|
422
|
-
.string()
|
|
423
|
-
.min(1)
|
|
424
|
-
.optional()
|
|
425
|
-
.describe("The card's content_hash last read via issue_get/issue_list. REQUIRED whenever this edit touches title/description/checklists (NOT ac); a stale value 409s stale_issue_content."),
|
|
426
425
|
...boardField,
|
|
427
426
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
428
427
|
// ---------------- issue_transition ----------------
|
|
@@ -631,15 +630,15 @@ server.tool("brief_set_page", 'Write one Brief page via PUT /api/brief/page?slug
|
|
|
631
630
|
// ---------------- plans (DX-2683) ----------------
|
|
632
631
|
// THE BINDING ASYMMETRY IS IN THE SCHEMAS, NOT IN PROSE. `plan_get` takes an
|
|
633
632
|
// optional `plan_id`; `plan_add_record`, `plan_get_record`,
|
|
634
|
-
// `plan_update_record`, `plan_delete_record`, `plan_add_card` and
|
|
635
|
-
// `
|
|
636
|
-
// cannot even express "write to that other plan". `plan_connect` takes one
|
|
633
|
+
// `plan_update_record`, `plan_delete_record`, `plan_add_card` and the five
|
|
634
|
+
// `plan_*_architecture_section` tools take NO plan id in any form, so a
|
|
635
|
+
// connected session cannot even express "write to that other plan". `plan_connect` takes one
|
|
637
636
|
// because binding a session to a plan is the one operation that is ABOUT a
|
|
638
637
|
// plan id — and it can only ever bind the caller's own session. `plan_create`
|
|
639
638
|
// also takes no plan id, but for a different reason: it MAKES a plan rather
|
|
640
639
|
// than acting on one, so there is no existing plan for an id to name yet.
|
|
641
640
|
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)));
|
|
642
|
-
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
|
|
641
|
+
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 sections, 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: {sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}, 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`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
|
|
643
642
|
plan_id: z
|
|
644
643
|
.number()
|
|
645
644
|
.int()
|
|
@@ -647,7 +646,7 @@ server.tool("plan_get", "Read one plan WHOLE via GET /api/plans (DX-2683) — it
|
|
|
647
646
|
.optional()
|
|
648
647
|
.describe("A plan id from `plan_list`. Omit to read the plan this session is connected to."),
|
|
649
648
|
}, async (args) => jsonResult(await planGet(client, args)));
|
|
650
|
-
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
|
|
649
|
+
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.", {
|
|
651
650
|
name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
|
|
652
651
|
}, async (args) => jsonResult(await planCreate(client, args)));
|
|
653
652
|
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.", {
|
|
@@ -686,12 +685,29 @@ server.tool("plan_rename", "Rename a plan via PATCH /api/plans/:plan_id (DX-2740
|
|
|
686
685
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
687
686
|
name: z.string().min(1).describe("The plan's new name."),
|
|
688
687
|
}, async (args) => jsonResult(await planRename(client, args)));
|
|
689
|
-
server.tool("
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
688
|
+
server.tool("plan_get_architecture_section", "Read ONE section of the plan this session is connected to, via GET /api/plans/mine/architecture/sections/:sid (DX-2726). TAKES NO PLAN ID: the plan is resolved from your connected session, same as `plan_add_record`. Not connected → `{error: \"session_not_connected\"}`. Unknown or another plan's section id → 404. Returns `{section: {id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}}`.", {
|
|
689
|
+
section_id: z.number().int().positive().describe("A section id, from `plan_get` or `plan_add_architecture_section`."),
|
|
690
|
+
}, async (args) => jsonResult(await planGetArchitectureSection(client, args)));
|
|
691
|
+
server.tool("plan_add_architecture_section", "Append a section to your connected plan's architecture, via POST /api/plans/mine/architecture/sections (DX-2726). Architecture is SECTIONS, not one document — each section is independently editable and hash-guarded, so fixing one never stales a concurrent edit to another. The new section sorts after every existing live section; use `plan_reorder_architecture_section` to move it. `title` is the heading shown in the auto-generated navigation index; `content` is its markdown. Takes no plan id; `session_not_connected` → `plan_connect` first. Returns the new section plus the plan's full live section list.", {
|
|
692
|
+
title: z.string().min(1).describe("The section's heading, shown in the navigation index."),
|
|
693
|
+
content: z.string().describe("The section's markdown. May be empty — a section awaiting its first draft is a real state."),
|
|
694
|
+
}, async (args) => jsonResult(await planAddArchitectureSection(client, args)));
|
|
695
|
+
server.tool("plan_update_architecture_section", 'Edit a section\'s title and/or content, via PATCH /api/plans/mine/architecture/sections/:sid (DX-2726). Both `title` and `content` are OPTIONAL — send only whichever changed. `base_hash` MUST be the section\'s `contentHash` from your last read; the server compares it against the current hash and, on a mismatch, fails loud with `{ok: false, body: {error: "stale_plan_architecture_section", currentHash, currentTitle, currentContent}}` rather than overwriting whoever wrote in between — merge into those and retry with `base_hash: currentHash`, never blindly. Takes no plan id. Returns the edited section plus the plan\'s full live section list.', {
|
|
696
|
+
section_id: z.number().int().positive().describe("The section id to edit."),
|
|
697
|
+
base_hash: z.string().describe("The section's `contentHash` from your last read. Required."),
|
|
698
|
+
title: z.string().min(1).optional().describe("New heading. Omit to keep the stored title."),
|
|
699
|
+
content: z.string().optional().describe("New markdown. Omit to keep the stored content."),
|
|
700
|
+
}, async (args) => jsonResult(await planUpdateArchitectureSection(client, args)));
|
|
701
|
+
server.tool("plan_delete_architecture_section", 'Soft-delete a section of your connected plan\'s architecture, via DELETE /api/plans/mine/architecture/sections/:sid (DX-2726). `base_hash` must be the section\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_architecture_section", currentHash, currentTitle, currentContent}` — on that refusal, re-fetch and confirm this is still the section you meant to remove before retrying, never blindly re-send with the fresh hash. Takes no plan id. Unknown or already-deleted section id → 404. Returns the plan\'s remaining live section list.', {
|
|
702
|
+
section_id: z.number().int().positive().describe("The section id to delete."),
|
|
703
|
+
base_hash: z.string().describe("The section's `contentHash` from your last read. Required."),
|
|
704
|
+
}, async (args) => jsonResult(await planDeleteArchitectureSection(client, args)));
|
|
705
|
+
server.tool("plan_reorder_architecture_section", "Reassign your connected plan's section display order, via PUT /api/plans/mine/architecture/sections/reorder (DX-2726). UNGUARDED by content hash, by design: moving a section never changes its (or any other section's) `contentHash`, so no `base_hash` is needed. `order` must name EXACTLY the plan's current live section ids, each once — a partial or foreign list is refused with a 400 rather than silently reordering a subset or dropping a section from view. Takes no plan id. Returns the plan's full live section list in its new order.", {
|
|
706
|
+
order: z
|
|
707
|
+
.array(z.number().int().positive())
|
|
708
|
+
.min(1)
|
|
709
|
+
.describe("Every live section id of the connected plan, in the desired order — exactly once each."),
|
|
710
|
+
}, async (args) => jsonResult(await planReorderArchitectureSection(client, args)));
|
|
695
711
|
// ---------------- main ----------------
|
|
696
712
|
async function main() {
|
|
697
713
|
boot();
|
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.58",
|
|
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",
|