@thehammer/danx-dashboard-mcp 0.1.154 → 0.1.157
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/field-tree.js +22 -13
- package/dist/handlers.js +28 -106
- package/dist/index.js +16 -64
- package/package.json +1 -1
package/dist/field-tree.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DX-3426 — the shared recursive FIELD-TREE shape.
|
|
3
3
|
*
|
|
4
|
-
* The dashboard's single-card
|
|
5
|
-
* `issue_list
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
4
|
+
* The dashboard's single-card, batch card and plan reads (`issue_get`,
|
|
5
|
+
* `issue_list`, `plan_get` — DX-3428) take a nested JSON tree instead of a
|
|
6
|
+
* flat field-GROUP list: each key names a field or relation of the resource
|
|
7
|
+
* at that level, and each value is either `true` (the field, or a relation's
|
|
8
|
+
* child with its default fields), a non-negative integer (a cursor-paged
|
|
9
|
+
* relation's own `limit` argument), an opaque cursor STRING (a paged
|
|
10
|
+
* relation's `before` — DX-3428: not every relation's cursor is a plain
|
|
11
|
+
* integer, e.g. a plan event's visibility-scanned row id), a non-empty
|
|
12
|
+
* STRING or ARRAY OF STRINGS (a relation's own declared, non-paging argument
|
|
13
|
+
* — e.g. `records`' `kind`, `events`' `kinds`/`origin`/`writer`), or another
|
|
12
14
|
* field tree (a relation's child with a NAMED field selection). Example:
|
|
13
15
|
*
|
|
14
|
-
* {"description": true, "ac": true, "problems": {"solutions": {"steps": true}, "decisions": true}, "comments": {"limit": 10}}
|
|
16
|
+
* {"description": true, "ac": true, "problems": {"solutions": {"steps": true}, "decisions": true}, "comments": {"limit": 10}, "records": {"kind": "goal"}}
|
|
15
17
|
*
|
|
16
18
|
* One schema, one module — every tool that accepts a field tree imports THIS
|
|
17
19
|
* export rather than redeclaring the shape, so the recursion is defined once
|
|
@@ -28,8 +30,15 @@
|
|
|
28
30
|
import { z } from "zod";
|
|
29
31
|
/**
|
|
30
32
|
* The shared schema. A record whose values are `true`, a non-negative
|
|
31
|
-
* integer,
|
|
32
|
-
*
|
|
33
|
-
*
|
|
33
|
+
* integer, a non-empty string, a non-empty array of non-empty strings, or
|
|
34
|
+
* the same schema recursively — generic on purpose (no issue/plan-specific
|
|
35
|
+
* field names baked in here) so every tool that reads a resource tree
|
|
36
|
+
* reuses it unchanged.
|
|
34
37
|
*/
|
|
35
|
-
export const fieldTreeSchema = z.lazy(() => z.record(z.union([
|
|
38
|
+
export const fieldTreeSchema = z.lazy(() => z.record(z.union([
|
|
39
|
+
z.literal(true),
|
|
40
|
+
z.number().int().nonnegative(),
|
|
41
|
+
z.string().min(1),
|
|
42
|
+
z.array(z.string().min(1)).min(1),
|
|
43
|
+
fieldTreeSchema,
|
|
44
|
+
])));
|
package/dist/handlers.js
CHANGED
|
@@ -65,21 +65,6 @@ export const ISSUE_BATCH_GET_MAX = 100;
|
|
|
65
65
|
* `__tests__/handlers.test.ts` asserts the two agree.
|
|
66
66
|
*/
|
|
67
67
|
export const LIST_PAGE_MAX_LIMIT = 1000;
|
|
68
|
-
/**
|
|
69
|
-
* `plan_get`'s `cards_limit` when omitted — mirrors the server's
|
|
70
|
-
* `src/issues/list-page.ts#PLAN_GET_CARDS_DEFAULT_LIMIT`;
|
|
71
|
-
* `__tests__/handlers.test.ts` asserts the two agree.
|
|
72
|
-
*/
|
|
73
|
-
export const PLAN_GET_CARDS_DEFAULT_LIMIT = 200;
|
|
74
|
-
/**
|
|
75
|
-
* DX-3027 — `plan_get`'s `events_limit` default/max — mirrors the server's
|
|
76
|
-
* `src/issues/plans-routes.ts#PLAN_GET_EVENTS_DEFAULT_LIMIT` /
|
|
77
|
-
* `PLAN_GET_EVENTS_MAX_LIMIT`. Deliberately smaller than `LIST_PAGE_MAX_LIMIT`
|
|
78
|
-
* above — an event page is read far more often, and a wide page defeats the
|
|
79
|
-
* point of a cursor. `__tests__/handlers.test.ts` asserts the two agree.
|
|
80
|
-
*/
|
|
81
|
-
export const PLAN_GET_EVENTS_DEFAULT_LIMIT = 50;
|
|
82
|
-
export const PLAN_GET_EVENTS_MAX_LIMIT = 200;
|
|
83
68
|
/**
|
|
84
69
|
* Fetch one card via `GET /api/issues/:id`, or many via
|
|
85
70
|
* `GET /api/issues/batch?ids=...` (DX-2727) — a real batch read that keeps
|
|
@@ -126,47 +111,11 @@ export async function issueGet(client, args) {
|
|
|
126
111
|
board: args.board,
|
|
127
112
|
});
|
|
128
113
|
}
|
|
129
|
-
// ---------------- repo_knowledge_get / repo_knowledge_set ----------------
|
|
130
|
-
const REPO_KNOWLEDGE_BASE_PATH = "/api/repo-knowledge";
|
|
131
|
-
/**
|
|
132
|
-
* Fetch the board's working-knowledge doc via GET /api/repo-knowledge
|
|
133
|
-
* (DX-1128, Story 2). Mirrors `issueGet` — a bare board-scoped GET, no id
|
|
134
|
-
* (the doc is 1-per-board). Board resolves the same way every other tool's
|
|
135
|
-
* `board` arg does: per-call override, else the dispatch's env-derived board.
|
|
136
|
-
*/
|
|
137
|
-
export async function repoKnowledgeGet(client, args = {}) {
|
|
138
|
-
return client.request({
|
|
139
|
-
method: "GET",
|
|
140
|
-
path: "",
|
|
141
|
-
basePath: REPO_KNOWLEDGE_BASE_PATH,
|
|
142
|
-
board: args.board,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Write the board's working-knowledge doc via PUT /api/repo-knowledge
|
|
147
|
-
* (DX-1128, Story 2). Mirrors `issueEdit`'s shape (a PATCH-like body write)
|
|
148
|
-
* but targets the repo-knowledge route family, not `/api/issues`. The
|
|
149
|
-
* server's optimistic-concurrency guard rejects a stale `base_hash` — the
|
|
150
|
-
* refusal envelope (`{ok: false, body: {error, currentHash}}`) passes
|
|
151
|
-
* through verbatim so the caller can re-get, re-merge, and retry.
|
|
152
|
-
*/
|
|
153
|
-
export async function repoKnowledgeSet(client, args) {
|
|
154
|
-
const { board, ...body } = args;
|
|
155
|
-
return client.request({
|
|
156
|
-
method: "PUT",
|
|
157
|
-
path: "",
|
|
158
|
-
basePath: REPO_KNOWLEDGE_BASE_PATH,
|
|
159
|
-
body,
|
|
160
|
-
board,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
114
|
// ---------------- brief_list / brief_get_page / brief_set_page ----------------
|
|
164
115
|
const BRIEF_BASE_PATH = "/api/brief";
|
|
165
116
|
/**
|
|
166
117
|
* List the board's named Brief pages via GET /api/brief (DX-2083).
|
|
167
|
-
*
|
|
168
|
-
* targets the list-shaped sibling surface — many named pages per board,
|
|
169
|
-
* not one document.
|
|
118
|
+
* A bare board-scoped GET, no id — many named pages per board.
|
|
170
119
|
*/
|
|
171
120
|
export async function briefList(client, args = {}) {
|
|
172
121
|
return client.request({
|
|
@@ -179,7 +128,7 @@ export async function briefList(client, args = {}) {
|
|
|
179
128
|
/**
|
|
180
129
|
* Fetch one Brief page by slug via GET /api/brief/page?slug=
|
|
181
130
|
* (DX-2083). A missing row (including a not-yet-created page) reads as the
|
|
182
|
-
*
|
|
131
|
+
* empty view — never a 404.
|
|
183
132
|
*/
|
|
184
133
|
export async function briefGetPage(client, args) {
|
|
185
134
|
return client.request({
|
|
@@ -192,10 +141,9 @@ export async function briefGetPage(client, args) {
|
|
|
192
141
|
}
|
|
193
142
|
/**
|
|
194
143
|
* Write one Brief page via PUT /api/brief/page?slug= (DX-2083).
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* query string (matching the route), never the body.
|
|
144
|
+
* Optimistic-concurrency `base_hash` with a verbatim 409 passthrough on a
|
|
145
|
+
* stale write. `slug` rides the query string (matching the route), never
|
|
146
|
+
* the body.
|
|
199
147
|
*/
|
|
200
148
|
export async function briefSetPage(client, args) {
|
|
201
149
|
const { slug, board, ...body } = args;
|
|
@@ -1061,32 +1009,17 @@ export async function issueAttach(client, args, deps = {}) {
|
|
|
1061
1009
|
const PLANS_BASE_PATH = "/api/plans";
|
|
1062
1010
|
const PLAN_SESSIONS_BASE_PATH = "/api/plan-sessions";
|
|
1063
1011
|
/**
|
|
1064
|
-
* DX-
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
* `
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
"records:goal",
|
|
1076
|
-
"records:rule",
|
|
1077
|
-
"records:caveat",
|
|
1078
|
-
"architecture",
|
|
1079
|
-
"sessions",
|
|
1080
|
-
"notes",
|
|
1081
|
-
"events",
|
|
1082
|
-
];
|
|
1083
|
-
/**
|
|
1084
|
-
* DX-3027 — every kind `plan_get`'s `events_kinds` filter accepts. The ONE
|
|
1085
|
-
* copy in this package (the `plan_get` zod enum in `index.ts` reads this
|
|
1086
|
-
* const), duplicated from the server's `src/issues/db/plan-events.ts#PLAN_EVENT_KINDS`
|
|
1087
|
-
* because the published package cannot import server source at runtime.
|
|
1088
|
-
* `__tests__/handlers.test.ts` asserts the two are equal, so drift fails a
|
|
1089
|
-
* test rather than a live call.
|
|
1012
|
+
* DX-3428 — every kind the plan resource's `events` relation accepts as a
|
|
1013
|
+
* `kinds` filter (`{"events": {"kinds": [...]}}`). The ONE copy in this
|
|
1014
|
+
* package (the `resource_fields`-describable `events` relation validates the
|
|
1015
|
+
* same set server-side), duplicated from the server's
|
|
1016
|
+
* `src/issues/db/plan-events.ts#PLAN_EVENT_KINDS` because the published
|
|
1017
|
+
* package cannot import server source at runtime — kept here purely for a
|
|
1018
|
+
* CONSUMER'S typed convenience (`PlanEventKind`); the MCP tool schema itself
|
|
1019
|
+
* no longer enumerates it (DX-3428 moved `plan_get` onto the generic,
|
|
1020
|
+
* resource-agnostic `fieldTreeSchema` every field-tree tool shares — see
|
|
1021
|
+
* `field-tree.ts`). `__tests__/handlers.test.ts` asserts the two are equal,
|
|
1022
|
+
* so drift fails a test rather than a live call.
|
|
1090
1023
|
*/
|
|
1091
1024
|
export const PLAN_EVENT_KINDS = [
|
|
1092
1025
|
"comment_added",
|
|
@@ -1124,10 +1057,11 @@ export const PLAN_EVENT_KINDS = [
|
|
|
1124
1057
|
"plan_auto_sign_off_changed",
|
|
1125
1058
|
];
|
|
1126
1059
|
/**
|
|
1127
|
-
* DX-
|
|
1060
|
+
* DX-3428 — every origin the `events` relation's `origin` filter accepts.
|
|
1128
1061
|
* Mirrors the server's `src/issues/db/plan-events.ts#PlanEventOrigin`
|
|
1129
1062
|
* (itself `ActivityOrigin`) — same drift protection as `PLAN_EVENT_KINDS`
|
|
1130
|
-
* above
|
|
1063
|
+
* above, and likewise kept only for a consumer's typed convenience (not read
|
|
1064
|
+
* by the MCP tool schema any more).
|
|
1131
1065
|
*/
|
|
1132
1066
|
export const PLAN_EVENT_ORIGINS = ["operator", "agent", "machine"];
|
|
1133
1067
|
/**
|
|
@@ -1150,31 +1084,19 @@ export async function planList(client, args = {}) {
|
|
|
1150
1084
|
}
|
|
1151
1085
|
/**
|
|
1152
1086
|
* One plan — its cheap scalars by default, or opt into its cards, its goals
|
|
1153
|
-
* + rules + caveats, its architecture sections,
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1087
|
+
* + rules + caveats, its architecture sections, its sessions and its event
|
|
1088
|
+
* ledger via a field TREE (DX-3428, replacing the retired flat field-GROUP
|
|
1089
|
+
* list). One call rather than several, which is what keeps this tool
|
|
1090
|
+
* surface small enough to be worth an agent's context even once every part
|
|
1091
|
+
* of a plan is opt-in rather than always-on.
|
|
1157
1092
|
*/
|
|
1158
1093
|
export async function planGet(client, args = {}) {
|
|
1159
1094
|
const query = {};
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
query.cards_offset = args.cards_offset;
|
|
1165
|
-
if (args.cards_limit !== undefined)
|
|
1166
|
-
query.cards_limit = args.cards_limit;
|
|
1167
|
-
if (args.events_limit !== undefined)
|
|
1168
|
-
query.events_limit = args.events_limit;
|
|
1169
|
-
if (args.events_before !== undefined)
|
|
1170
|
-
query.events_before = args.events_before;
|
|
1171
|
-
if (args.events_kinds !== undefined && args.events_kinds.length > 0) {
|
|
1172
|
-
query.events_kinds = args.events_kinds.join(",");
|
|
1095
|
+
// DX-3428 — the tree travels as ONE JSON-encoded query param, exactly like
|
|
1096
|
+
// `issue_get`/`issue_list`'s own `fields` (see `issueGet` above).
|
|
1097
|
+
if (args.fields !== undefined && Object.keys(args.fields).length > 0) {
|
|
1098
|
+
query.fields = JSON.stringify(args.fields);
|
|
1173
1099
|
}
|
|
1174
|
-
if (args.events_origin !== undefined)
|
|
1175
|
-
query.events_origin = args.events_origin;
|
|
1176
|
-
if (args.events_writer !== undefined)
|
|
1177
|
-
query.events_writer = args.events_writer;
|
|
1178
1100
|
return client.request({
|
|
1179
1101
|
method: "GET",
|
|
1180
1102
|
path: args.plan_id === undefined ? "/mine" : `/${args.plan_id}/full`,
|
package/dist/index.js
CHANGED
|
@@ -34,8 +34,6 @@
|
|
|
34
34
|
* - issue_attach POST /api/issues/:id/attachments (reads a local file)
|
|
35
35
|
* - quality_gate_instruction
|
|
36
36
|
* GET /api/quality-gates/:gate/instruction (DX-3341, not card-scoped)
|
|
37
|
-
* - repo_knowledge_get GET /api/repo-knowledge
|
|
38
|
-
* - repo_knowledge_set PUT /api/repo-knowledge (DX-1128, Story 2)
|
|
39
37
|
* - brief_list GET /api/brief
|
|
40
38
|
* - brief_get_page GET /api/brief/page (DX-2083 / DX-2484)
|
|
41
39
|
* - brief_set_page PUT /api/brief/page (DX-2083 / DX-2484)
|
|
@@ -110,7 +108,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
110
108
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
111
109
|
import { z } from "zod";
|
|
112
110
|
import { DashboardHttpClient } from "./http-client.js";
|
|
113
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet,
|
|
111
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, qualityGateInstruction, resourceFields, } from "./handlers.js";
|
|
114
112
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
115
113
|
import { fieldTreeSchema } from "./field-tree.js";
|
|
116
114
|
function readEnvOrDie(name) {
|
|
@@ -874,21 +872,8 @@ strictTool("issue_attach", "Attach a LOCAL file to an issue card: `id` (the card
|
|
|
874
872
|
.describe("Absolute path to a local file on the dispatch's shared filesystem (must start with `/`)."),
|
|
875
873
|
...boardField,
|
|
876
874
|
}, async (args) => jsonResult(await issueAttach(client, args)));
|
|
877
|
-
// ---------------- repo_knowledge_get ----------------
|
|
878
|
-
strictTool("repo_knowledge_get", "Fetch the board's working-knowledge markdown doc. Board-scoped; see `board`. Returns `{ok, status, body: {content, contentHash, updatedAt, updatedBy, boardId}}` — an unset doc reads as the empty view (content/contentHash `\"\"`), not a 404. Before `repo_knowledge_set`, always get immediately first and pass `contentHash` back as `base_hash` — the concurrency guard rejects a stale write.", {
|
|
879
|
-
...boardField,
|
|
880
|
-
}, async (args) => jsonResult(await repoKnowledgeGet(client, args)));
|
|
881
|
-
// ---------------- repo_knowledge_set ----------------
|
|
882
|
-
strictTool("repo_knowledge_set", 'Write the board\'s working-knowledge markdown doc. Board-scoped; see `board`. `base_hash` must be the `contentHash` from the immediately-prior `repo_knowledge_get` ("" for the true first write). On mismatch, fails loud with `{error: "stale_repo_knowledge", currentHash}` rather than overwriting — re-get, re-merge, retry with the new hash. On success persists to the DB, publishes `repo-knowledge:updated` over SSE, and returns the new view.', {
|
|
883
|
-
content: z.string(),
|
|
884
|
-
base_hash: z
|
|
885
|
-
.string()
|
|
886
|
-
.optional()
|
|
887
|
-
.describe('The contentHash last read via repo_knowledge_get ("" for a true first write). Omitted also normalizes to "" server-side, so it only succeeds against an empty/absent doc — always get immediately before set.'),
|
|
888
|
-
...boardField,
|
|
889
|
-
}, async (args) => jsonResult(await repoKnowledgeSet(client, args)));
|
|
890
875
|
// ---------------- brief_list ----------------
|
|
891
|
-
strictTool("brief_list", "List the board's named Brief pages. Board-scoped; see `board`. Returns `{boardId, pages: [{slug, title, contentHash, sortOrder, updatedAt, updatedBy}]}` — metadata only, no content (use `brief_get_page`).
|
|
876
|
+
strictTool("brief_list", "List the board's named Brief pages. Board-scoped; see `board`. Returns `{boardId, pages: [{slug, title, contentHash, sortOrder, updatedAt, updatedBy}]}` — metadata only, no content (use `brief_get_page`). Brief pages are MANY named pages per board (Goals/Architecture/Rules/Caveats tabs), keyed by (board, slug). The reserved `index` slug always exists.", {
|
|
892
877
|
...boardField,
|
|
893
878
|
}, async (args) => jsonResult(await briefList(client, args)));
|
|
894
879
|
// ---------------- brief_get_page ----------------
|
|
@@ -900,7 +885,7 @@ strictTool("brief_get_page", 'Fetch one Brief page by slug. Board-scoped; see `b
|
|
|
900
885
|
...boardField,
|
|
901
886
|
}, async (args) => jsonResult(await briefGetPage(client, args)));
|
|
902
887
|
// ---------------- brief_set_page ----------------
|
|
903
|
-
strictTool("brief_set_page", 'Write one Brief page. Board-scoped; see `board`. Body: `{content, title?, sortOrder?, base_hash?}` —
|
|
888
|
+
strictTool("brief_set_page", 'Write one Brief page. Board-scoped; see `board`. Body: `{content, title?, sortOrder?, base_hash?}` — optimistic-concurrency, targeting one named page. `base_hash` must be the `contentHash` from the immediately-prior `brief_get_page` (`""` for a true first write). On mismatch fails loud with `{error: "stale_brief_page", currentHash}` — re-get, re-merge, retry, never overwrite blindly. On success persists, publishes `brief:updated` over SSE, and returns the new view. No delete tool on this surface; the reserved `index` slug is never deletable — removing a non-index page is dashboard-UI-only.', {
|
|
904
889
|
slug: z
|
|
905
890
|
.string()
|
|
906
891
|
.min(1)
|
|
@@ -930,62 +915,29 @@ strictTool("plan_list", "List every plan, and learn which plan THIS session is c
|
|
|
930
915
|
.optional()
|
|
931
916
|
.describe("Filter to one computed status: awaiting-session, planning, building, awaiting-sign-off, complete. Omit for every plan."),
|
|
932
917
|
}, async (args) => jsonResult(await planList(client, args)));
|
|
933
|
-
strictTool("plan_get",
|
|
918
|
+
strictTool("plan_get",
|
|
919
|
+
// DX-3428 — the retired flat field-GROUP list (`cards`, `records:<kind>`,
|
|
920
|
+
// `cards_offset`/`cards_limit`, `events_*`) is replaced by a JSON field
|
|
921
|
+
// TREE against the plan resource — the SAME `fieldTreeSchema` `issue_get`/
|
|
922
|
+
// `issue_list` already take (`./field-tree.ts`). Every former query param
|
|
923
|
+
// now lives INSIDE the tree as a relation argument.
|
|
924
|
+
"Read a plan. Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{id, ref, name, created_at, signed_off_at, signed_off_by, auto_sign_off, boards, card_count, bucket_counts, status, session, sessionListenerAttached}` — no cards, records, or architecture body. `ref` is the plan's short reference (`PLN-<id>`) — cite that, not the bare id. `status` is computed fresh on every read, never stored — see `plan_list` for the four values and what each means. Pass a `fields` tree to opt into: `cards` (member cards — the ISSUE resource itself, so any of `issue_get`'s own fields/relations may be nested under it, e.g. `{\"cards\": {\"title\": true, \"status\": true}}`; cursor-paged via `{\"limit\": N, \"before\": \"<cursor>\"}`, response carries a sibling `cards_page: {limit, total, next_cursor}`), `records` (every goal+rule+caveat) or `{\"records\": {\"kind\": \"goal\"}}` / `{\"kind\": [\"goal\",\"rule\"]}` (narrow to one or more kinds, cheaper), `architecture_sections` (`[{id, plan_id, content_hash, title, content, sort_order, created_at, updated_at}]`), `sessions` (every session connected to the plan), `notes` (the latest milestone-timeline page), `events` (the plan's durable event ledger — every human action and bridge message; cursor-paged via `{\"limit\": N, \"before\": \"<cursor>\"}` plus the filter args `{\"kinds\": [...], \"origin\": \"...\", \"writer\": \"...\"}`; response carries `events` rows plus a sibling `events_page: {limit, total, next_cursor}`; `next_cursor` null on the last page; an event on a card whose board you cannot read is left out, plan-level events are always visible). Call `resource_fields({resource:\"plan\"})` for the full, current list of what a tree may name — never guess a name; unknown → 400 `unknown_field`. `session`/`sessionListenerAttached` ride every response regardless (not part of the tree — they describe YOUR session, not the plan). `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the plugin's event bridge starts; still `false` after that while connected means the plan's card events are not reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `content_hash`/`contentHash` back as `base_hash`.", {
|
|
934
925
|
plan_id: z
|
|
935
926
|
.number()
|
|
936
927
|
.int()
|
|
937
928
|
.positive()
|
|
938
929
|
.optional()
|
|
939
930
|
.describe("A plan id from `plan_list`. Omit to read the plan this session is connected to."),
|
|
940
|
-
fields:
|
|
941
|
-
.array(z.enum(PLAN_FIELD_GROUPS))
|
|
942
|
-
.optional()
|
|
943
|
-
.describe("Opt-in field-GROUPS: cards, records (every kind) or records:goal/records:rule/records:caveat (one kind), architecture, sessions, notes, events. Absent/empty = cheap scalars only (plan, boards, cardCount, bucketCounts) plus session state."),
|
|
944
|
-
cards_offset: z
|
|
945
|
-
.number()
|
|
946
|
-
.int()
|
|
947
|
-
.nonnegative()
|
|
948
|
-
.optional()
|
|
949
|
-
.describe("Where the `cards` page starts (default 0). Requires `fields:[\"cards\"]`. Page with cards_offset while cards_offset + cards.length < cards_total."),
|
|
950
|
-
cards_limit: z
|
|
951
|
-
.number()
|
|
952
|
-
.int()
|
|
953
|
-
.positive()
|
|
954
|
-
.max(LIST_PAGE_MAX_LIMIT)
|
|
955
|
-
.optional()
|
|
956
|
-
.describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields:[\"cards\"]`."),
|
|
957
|
-
events_limit: z
|
|
958
|
-
.number()
|
|
959
|
-
.int()
|
|
960
|
-
.positive()
|
|
961
|
-
.max(PLAN_GET_EVENTS_MAX_LIMIT)
|
|
962
|
-
.optional()
|
|
963
|
-
.describe("how many ledger events one page holds, 1.." + PLAN_GET_EVENTS_MAX_LIMIT + " (default " + PLAN_GET_EVENTS_DEFAULT_LIMIT + "). Requires `fields` to include `events`."),
|
|
964
|
-
events_before: z
|
|
965
|
-
.string()
|
|
966
|
-
.min(1)
|
|
967
|
-
.optional()
|
|
968
|
-
.describe("an opaque cursor from a previous page's `next_cursor`. Omit for the newest page. Requires `fields` to include `events`."),
|
|
969
|
-
events_kinds: z
|
|
970
|
-
.array(z.enum(PLAN_EVENT_KINDS))
|
|
971
|
-
.optional()
|
|
972
|
-
.describe("only these event kinds. Omit for every kind. Requires `fields` to include `events`."),
|
|
973
|
-
events_origin: z
|
|
974
|
-
.enum(PLAN_EVENT_ORIGINS)
|
|
975
|
-
.optional()
|
|
976
|
-
.describe("only events with this origin. Omit for every origin. Requires `fields` to include `events`."),
|
|
977
|
-
events_writer: z
|
|
978
|
-
.string()
|
|
979
|
-
.min(1)
|
|
931
|
+
fields: fieldTreeSchema
|
|
980
932
|
.optional()
|
|
981
|
-
.describe("
|
|
933
|
+
.describe("A field tree (see tool description); absent/empty = cheap scalars only. `resource_fields({resource:\"plan\"})` names every valid key."),
|
|
982
934
|
}, async (args) => jsonResult(await planGet(client, args)));
|
|
983
935
|
strictTool("plan_create", "Create a new, empty plan. Global — not board-scoped. Adds no cards, records, or architecture sections, and does not connect any session (call `plan_connect` separately). Returns `{plan: {id, ref, name, createdAt}}` — `ref` is the short reference (`PLN-<id>`). Use `plan.id` with `plan_connect` to start working on it, or `plan_get({plan_id})` to browse.", {
|
|
984
936
|
name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
|
|
985
937
|
}, async (args) => jsonResult(await planCreate(client, args)));
|
|
986
938
|
strictTool("plan_connect",
|
|
987
939
|
// DX-2735: trimmed with the problem tools to stay inside the work-profile budget.
|
|
988
|
-
"Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth}` — `browserInstruction` is a server-built action to take NOW: it names the plan's URL and tells you to open it (in-app browser if you have one, else default), keep that tab open for the whole session without navigating it away, and use a different tab for your own browsing — the operator's tab for following and talking to you. Returned on every connect, including a re-connect, so it doubles as the post-context-loss reminder. `briefing` is every goal/rule/caveat (ref+body), every architecture section (id+title), the plan's own ref/name/status, a first page of open cards (id/type/status/title/openProblemCount/assignedAgent) with a `morePagesHint` when more exist, and the closed-card count — usually replacing the `plan_get({fields:
|
|
940
|
+
"Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth}` — `browserInstruction` is a server-built action to take NOW: it names the plan's URL and tells you to open it (in-app browser if you have one, else default), keep that tab open for the whole session without navigating it away, and use a different tab for your own browsing — the operator's tab for following and talking to you. Returned on every connect, including a re-connect, so it doubles as the post-context-loss reminder. `briefing` is every goal/rule/caveat (ref+body), every architecture section (id+title), the plan's own ref/name/status, a first page of open cards (id/type/status/title/openProblemCount/assignedAgent) with a `morePagesHint` when more exist, and the closed-card count — usually replacing the `plan_get({fields: {...}})` + card batch-read a fresh session used to need. A session is on AT MOST ONE plan: connecting elsewhere MOVES it, and `movedFrom: {id, name} | null` says which plan it left (null = no plan, or already this one). Binds only your OWN session, resolved from the session id this server forwards; afterwards every plan WRITE tool acts on this plan and takes no plan id. `listenerHealth` reports the event bridge's state — `null` (no session), or `{attached, state: \"unattached\"|\"credential_mismatch\"|\"healthy\", nextStep}` naming a concrete fix per unhealthy state (same shape `plan_list`/`plan_get` report). Once healthy, every comment, answer, problem added and block/unblock on this plan's cards reaches the session unpolled, relayed by the plugin's event bridge as a line like `[DX-8 \"Title\" repo:board] newms87 answered \"<statement>\": chose \"Pause E2E\"`. Pass `title` (call `get_session({session_id:\"self\"})` first and forward its `title` verbatim) so the dashboard shows the same name Claude does — this server cannot read it itself.", {
|
|
989
941
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
990
942
|
title: z
|
|
991
943
|
.string()
|
|
@@ -1009,7 +961,7 @@ async (args) => {
|
|
|
1009
961
|
}),
|
|
1010
962
|
});
|
|
1011
963
|
});
|
|
1012
|
-
strictTool("plan_add_record", "Add a goal, rule or caveat to your connected plan (see `kind`'s own values below for what each means — never progress/status/session notes, those are card comments). `body` is one plain statement, at most 250 characters (400 if longer); detail goes in markdown `context`. Allocates a permanent reference (`G-1`, `R-4`, `CAV-12`). Takes no plan id; not connected → `plan_connect` first. Returns the created record plus `records_count` (that kind's live count, not the whole list) — read the list with `plan_get({fields:
|
|
964
|
+
strictTool("plan_add_record", "Add a goal, rule or caveat to your connected plan (see `kind`'s own values below for what each means — never progress/status/session notes, those are card comments). `body` is one plain statement, at most 250 characters (400 if longer); detail goes in markdown `context`. Allocates a permanent reference (`G-1`, `R-4`, `CAV-12`). Takes no plan id; not connected → `plan_connect` first. Returns the created record plus `records_count` (that kind's live count, not the whole list) — read the list with `plan_get({fields: {records: {kind: \"<kind>\"}}})`.", {
|
|
1013
965
|
kind: z.enum(["goal", "rule", "caveat"]).describe("goal = outcome, rule = constraint, caveat = architecture trade-off."),
|
|
1014
966
|
body: z.string().min(1).describe("One plain statement, at most 250 characters. Details go in `context`."),
|
|
1015
967
|
context: z.string().optional().describe("Markdown detail behind the statement: evidence, history, examples."),
|
|
@@ -1031,7 +983,7 @@ strictTool("plan_delete_record", 'Soft-delete a goal/rule/caveat; its reference
|
|
|
1031
983
|
record_id: z.number().int().positive().describe("The record id to delete."),
|
|
1032
984
|
content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
|
|
1033
985
|
}, async (args) => jsonResult(await planDeleteRecord(client, args)));
|
|
1034
|
-
strictTool("plan_add_note", "Write a milestone note to a plan's timeline — a MILESTONE, not a log: use for a card (or group) finishing, an important decision, or a meaningful goal/rule/caveat/architecture change; routine progress stays a card comment. Terse: `title` at most 60 characters, `body` at most 250 (400 if too long, naming the limit). Links resolve on read into their target (a card's title, a record's ref+body, a section's title); an unknown card, unparseable/foreign record ref, or unknown/foreign section id is refused 400 naming which one. A card link needn't be a plan member; a record/section link must belong to THIS plan. `author` is stamped server-side. Takes an explicit `plan_id` (like `plan_remove_card`/`plan_rename`) so a dispatched worker with no plan connection can still write. Unknown plan → 404. Returns the new note plus `notes_count` (the plan's total live count, not the latest page) — read the timeline with `plan_get({fields:
|
|
986
|
+
strictTool("plan_add_note", "Write a milestone note to a plan's timeline — a MILESTONE, not a log: use for a card (or group) finishing, an important decision, or a meaningful goal/rule/caveat/architecture change; routine progress stays a card comment. Terse: `title` at most 60 characters, `body` at most 250 (400 if too long, naming the limit). Links resolve on read into their target (a card's title, a record's ref+body, a section's title); an unknown card, unparseable/foreign record ref, or unknown/foreign section id is refused 400 naming which one. A card link needn't be a plan member; a record/section link must belong to THIS plan. `author` is stamped server-side. Takes an explicit `plan_id` (like `plan_remove_card`/`plan_rename`) so a dispatched worker with no plan connection can still write. Unknown plan → 404. Returns the new note plus `notes_count` (the plan's total live count, not the latest page) — read the timeline with `plan_get({fields: {notes: true}})`.", {
|
|
1035
987
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
1036
988
|
title: z.string().min(1).describe("At most 60 characters."),
|
|
1037
989
|
body: z.string().min(1).describe("The wrap-up, at most 250 characters."),
|
|
@@ -1057,7 +1009,7 @@ strictTool("plan_delete_note", 'Soft-delete a plan note. `content_hash` must be
|
|
|
1057
1009
|
note_id: z.number().int().positive().describe("The note id to delete."),
|
|
1058
1010
|
content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
|
|
1059
1011
|
}, async (args) => jsonResult(await planDeleteNote(client, args)));
|
|
1060
|
-
strictTool("plan_add_card", "Add an existing card, from ANY board, to the plan this session is connected to. Idempotent (re-adding is a no-op; a card may sit in several plans). Adds MEMBERSHIP only, never edits the card. Takes no plan id — resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns `{card_id, member: true, cards_count}` (the plan's total member-card count, not the full list — a plan can hold hundreds; read it with `plan_get({fields:
|
|
1012
|
+
strictTool("plan_add_card", "Add an existing card, from ANY board, to the plan this session is connected to. Idempotent (re-adding is a no-op; a card may sit in several plans). Adds MEMBERSHIP only, never edits the card. Takes no plan id — resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns `{card_id, member: true, cards_count}` (the plan's total member-card count, not the full list — a plan can hold hundreds; read it with `plan_get({fields: {cards: true}})`, cursor-paged).", {
|
|
1061
1013
|
card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
|
|
1062
1014
|
}, async (args) => jsonResult(await planAddCard(client, args)));
|
|
1063
1015
|
strictTool("plan_remove_card", "Remove a card from a plan — sibling of `plan_add_card`, any board. Idempotent (removing a non-member is a no-op). Removes MEMBERSHIP only — never touches the card itself or its membership in other plans. Unlike `plan_add_card`, takes an EXPLICIT `plan_id` — you may remove from any plan you can name. Unknown plan → 404. Returns `{card_id, member: false, cards_count}` (see `plan_add_card`).", {
|
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.157",
|
|
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",
|