@thehammer/danx-dashboard-mcp 0.1.56 → 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 +34 -13
- 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];
|
|
@@ -626,15 +630,15 @@ server.tool("brief_set_page", 'Write one Brief page via PUT /api/brief/page?slug
|
|
|
626
630
|
// ---------------- plans (DX-2683) ----------------
|
|
627
631
|
// THE BINDING ASYMMETRY IS IN THE SCHEMAS, NOT IN PROSE. `plan_get` takes an
|
|
628
632
|
// optional `plan_id`; `plan_add_record`, `plan_get_record`,
|
|
629
|
-
// `plan_update_record`, `plan_delete_record`, `plan_add_card` and
|
|
630
|
-
// `
|
|
631
|
-
// 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
|
|
632
636
|
// because binding a session to a plan is the one operation that is ABOUT a
|
|
633
637
|
// plan id — and it can only ever bind the caller's own session. `plan_create`
|
|
634
638
|
// also takes no plan id, but for a different reason: it MAKES a plan rather
|
|
635
639
|
// than acting on one, so there is no existing plan for an id to name yet.
|
|
636
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)));
|
|
637
|
-
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`.", {
|
|
638
642
|
plan_id: z
|
|
639
643
|
.number()
|
|
640
644
|
.int()
|
|
@@ -642,7 +646,7 @@ server.tool("plan_get", "Read one plan WHOLE via GET /api/plans (DX-2683) — it
|
|
|
642
646
|
.optional()
|
|
643
647
|
.describe("A plan id from `plan_list`. Omit to read the plan this session is connected to."),
|
|
644
648
|
}, async (args) => jsonResult(await planGet(client, args)));
|
|
645
|
-
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.", {
|
|
646
650
|
name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
|
|
647
651
|
}, async (args) => jsonResult(await planCreate(client, args)));
|
|
648
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.", {
|
|
@@ -681,12 +685,29 @@ server.tool("plan_rename", "Rename a plan via PATCH /api/plans/:plan_id (DX-2740
|
|
|
681
685
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
682
686
|
name: z.string().min(1).describe("The plan's new name."),
|
|
683
687
|
}, async (args) => jsonResult(await planRename(client, args)));
|
|
684
|
-
server.tool("
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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)));
|
|
690
711
|
// ---------------- main ----------------
|
|
691
712
|
async function main() {
|
|
692
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",
|