@thehammer/danx-dashboard-mcp 0.1.82 → 0.1.84
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 +85 -1
- package/dist/index.js +29 -2
- package/dist/listen.js +22 -4
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -392,7 +392,11 @@ export async function issueComment(client, args) {
|
|
|
392
392
|
return client.request({
|
|
393
393
|
method: "POST",
|
|
394
394
|
path: `/${idEnc}/comments`,
|
|
395
|
-
body: {
|
|
395
|
+
body: {
|
|
396
|
+
text,
|
|
397
|
+
...(args.metadata !== undefined ? { metadata: args.metadata } : {}),
|
|
398
|
+
...(args.problem_id !== undefined ? { problem_id: args.problem_id } : {}),
|
|
399
|
+
},
|
|
396
400
|
board,
|
|
397
401
|
});
|
|
398
402
|
}
|
|
@@ -844,6 +848,7 @@ export const PLAN_FIELD_GROUPS = [
|
|
|
844
848
|
"records:caveat",
|
|
845
849
|
"architecture",
|
|
846
850
|
"sessions",
|
|
851
|
+
"notes",
|
|
847
852
|
];
|
|
848
853
|
/**
|
|
849
854
|
* DX-2834 — the plan-status taxonomy, mirroring `PLAN_FIELD_GROUPS` just
|
|
@@ -1147,6 +1152,85 @@ export async function planDeleteRecord(client, args) {
|
|
|
1147
1152
|
body: { content_hash: args.content_hash },
|
|
1148
1153
|
});
|
|
1149
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Write a milestone note to a plan, via `POST /api/plans/:plan_id/notes`.
|
|
1157
|
+
* TAKES AN EXPLICIT `plan_id`, unlike `plan_add_record`/`plan_add_architecture_section`
|
|
1158
|
+
* — mirrors `plan_add_card`'s sibling `plan_remove_card`/`plan_rename`: a
|
|
1159
|
+
* dispatched worker has no plan connection when it finishes a card, and a
|
|
1160
|
+
* card can sit on several plans, so the writer names which one the note
|
|
1161
|
+
* belongs to. A note is a MILESTONE, not a log — write one when a card (or a
|
|
1162
|
+
* related group of cards) finishes, an important decision lands, or a
|
|
1163
|
+
* goal/rule/caveat/architecture section changes meaningfully; routine step
|
|
1164
|
+
* progress stays a card comment. Links resolve on read into what they point
|
|
1165
|
+
* at (a card's title, a record's ref + body, a section's title) — an unknown
|
|
1166
|
+
* card id, an unparseable or foreign record ref, or an unknown or foreign
|
|
1167
|
+
* section id is refused 400 naming exactly which one. A card link does NOT
|
|
1168
|
+
* require the card to be a member of this plan; a record/section link MUST
|
|
1169
|
+
* belong to THIS plan. `author` is stamped server-side from your identity,
|
|
1170
|
+
* never sent by you. Unknown plan → 404. Returns the new note plus the
|
|
1171
|
+
* plan's latest notes page.
|
|
1172
|
+
*/
|
|
1173
|
+
export async function planAddNote(client, args) {
|
|
1174
|
+
return client.request({
|
|
1175
|
+
method: "POST",
|
|
1176
|
+
path: `/${args.plan_id}/notes`,
|
|
1177
|
+
basePath: PLANS_BASE_PATH,
|
|
1178
|
+
body: {
|
|
1179
|
+
title: args.title,
|
|
1180
|
+
body: args.body,
|
|
1181
|
+
...(args.card_ids === undefined ? {} : { card_ids: args.card_ids }),
|
|
1182
|
+
...(args.record_refs === undefined ? {} : { record_refs: args.record_refs }),
|
|
1183
|
+
...(args.section_ids === undefined ? {} : { section_ids: args.section_ids }),
|
|
1184
|
+
},
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Edit a plan note, via `PATCH /api/plans/:plan_id/notes/:note_id`.
|
|
1189
|
+
* `content_hash` MUST be the note's `contentHash` from your last read; on a
|
|
1190
|
+
* mismatch nothing is written and you get `{error: "stale_plan_note",
|
|
1191
|
+
* currentHash, currentTitle, currentBody, currentLinks}` — merge into those
|
|
1192
|
+
* and retry with `content_hash: currentHash`, never blindly. `title`/`body`
|
|
1193
|
+
* are each optional and keep their stored value when omitted. The LINK
|
|
1194
|
+
* fields are all-or-nothing as a GROUP: omit all three to keep the stored
|
|
1195
|
+
* link set untouched; send ANY one of them to REPLACE THE WHOLE SET (never a
|
|
1196
|
+
* per-link add/remove — the set is the unit of change). The hash covers
|
|
1197
|
+
* title, body AND the link set, so a stale read of any of the three is
|
|
1198
|
+
* refused. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`.
|
|
1199
|
+
* Unknown plan or note id → 404. Returns the edited note plus the plan's
|
|
1200
|
+
* latest notes page.
|
|
1201
|
+
*/
|
|
1202
|
+
export async function planUpdateNote(client, args) {
|
|
1203
|
+
return client.request({
|
|
1204
|
+
method: "PATCH",
|
|
1205
|
+
path: `/${args.plan_id}/notes/${args.note_id}`,
|
|
1206
|
+
basePath: PLANS_BASE_PATH,
|
|
1207
|
+
body: {
|
|
1208
|
+
content_hash: args.content_hash,
|
|
1209
|
+
...(args.title === undefined ? {} : { title: args.title }),
|
|
1210
|
+
...(args.body === undefined ? {} : { body: args.body }),
|
|
1211
|
+
...(args.card_ids === undefined ? {} : { card_ids: args.card_ids }),
|
|
1212
|
+
...(args.record_refs === undefined ? {} : { record_refs: args.record_refs }),
|
|
1213
|
+
...(args.section_ids === undefined ? {} : { section_ids: args.section_ids }),
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Soft-delete a plan note, via `DELETE /api/plans/:plan_id/notes/:note_id`.
|
|
1219
|
+
* `content_hash` MUST be the note's `contentHash` from your last read; a
|
|
1220
|
+
* stale hash deletes nothing and returns the same `{error:
|
|
1221
|
+
* "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}`
|
|
1222
|
+
* shape `plan_update_note` uses. TAKES AN EXPLICIT `plan_id`, same reason as
|
|
1223
|
+
* `plan_add_note`. Unknown plan, unknown note, or an already-deleted note →
|
|
1224
|
+
* 404. Returns the plan's remaining latest notes page.
|
|
1225
|
+
*/
|
|
1226
|
+
export async function planDeleteNote(client, args) {
|
|
1227
|
+
return client.request({
|
|
1228
|
+
method: "DELETE",
|
|
1229
|
+
path: `/${args.plan_id}/notes/${args.note_id}`,
|
|
1230
|
+
basePath: PLANS_BASE_PATH,
|
|
1231
|
+
body: { content_hash: args.content_hash },
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1150
1234
|
// ---------------- failure_category_list / _create / _update (DX-2792) ----------------
|
|
1151
1235
|
/**
|
|
1152
1236
|
* DX-2792 (Failure evaluation 3/4) — wraps `src/dashboard/failure-categories-routes.ts`,
|
package/dist/index.js
CHANGED
|
@@ -101,7 +101,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
101
101
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
102
102
|
import { z } from "zod";
|
|
103
103
|
import { DashboardHttpClient } from "./http-client.js";
|
|
104
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_STATUSES, 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";
|
|
104
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
105
105
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
106
106
|
function readEnvOrDie(name) {
|
|
107
107
|
const v = process.env[name];
|
|
@@ -504,12 +504,13 @@ server.tool("issue_triage", "Record a triage confidence score via POST /api/issu
|
|
|
504
504
|
...boardField,
|
|
505
505
|
}, async (args) => jsonResult(await issueTriage(client, args)));
|
|
506
506
|
// ---------------- issue_comment ----------------
|
|
507
|
-
server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text, metadata?} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation). `metadata` (DX-2157, action=add only) is an OPTIONAL opaque JSON object a calling app attaches to the comment — e.g. a generated `{sql, explanation}` packet its own UI renders specially. danxbot stores + returns it verbatim and enforces NO shape on its contents; omit for a plain markdown-only comment (unaffected either way).", {
|
|
507
|
+
server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text, metadata?, problem_id?} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation). `metadata` (DX-2157, action=add only) is an OPTIONAL opaque JSON object a calling app attaches to the comment — e.g. a generated `{sql, explanation}` packet its own UI renders specially. danxbot stores + returns it verbatim and enforces NO shape on its contents; omit for a plain markdown-only comment (unaffected either way). `problem_id` (DX-2906, action=add only) OPTIONALLY threads the comment as a follow-up question under one of this SAME card's problems (from `issue_problem` list/add) WITHOUT answering it — commenting never changes open_problem_count, blocked, or records a decision; use issue_problem's answer route for that. An unknown id, another card's id, or a removed problem's id all 404 naming the problem id — an ANSWERED (but not removed) problem still accepts a follow-up comment, since the thread continues after a decision.", {
|
|
508
508
|
id: z.string().min(1),
|
|
509
509
|
action: z.enum(["add", "edit", "delete"]),
|
|
510
510
|
comment_id: z.number().int().positive().optional(),
|
|
511
511
|
text: z.string().min(1).optional(),
|
|
512
512
|
metadata: z.record(z.unknown()).optional(),
|
|
513
|
+
problem_id: z.number().int().positive().optional().describe("action=add only — thread this comment as a follow-up under a LIVE problem of this same card, without answering it"),
|
|
513
514
|
...boardField,
|
|
514
515
|
}, async (args) => jsonResult(await issueComment(client, args)));
|
|
515
516
|
// ---------------- issue_checklist ----------------
|
|
@@ -793,6 +794,32 @@ server.tool("plan_delete_record", 'Soft-delete a goal/rule/caveat of your connec
|
|
|
793
794
|
record_id: z.number().int().positive().describe("The record id to delete."),
|
|
794
795
|
content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
|
|
795
796
|
}, async (args) => jsonResult(await planDeleteRecord(client, args)));
|
|
797
|
+
server.tool("plan_add_note", "Write a milestone note to a plan's timeline, via POST /api/plans/:plan_id/notes (DX-2915). A note is a MILESTONE, not a log — write one for a card (or related group of cards) finishing, an important decision, or a meaningful goal/rule/caveat/architecture-section change; routine step progress stays a card comment, never a note. Terse tone: `title` at most 60 characters, `body` (the wrap-up) at most 250 (both 400 if too long, naming the limit and actual length). Links resolve on read into what they point at (a card's title, a record's ref+body, a section's title): an unknown card, an unparseable or foreign record ref, or an unknown or foreign section id is refused 400 naming exactly which one. A card link does NOT require the card to be a member of this plan; a record/section link MUST belong to THIS plan. `author` is stamped from your identity server-side — there is no field for it. 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 the plan's latest notes page.", {
|
|
798
|
+
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
799
|
+
title: z.string().min(1).describe("At most 60 characters."),
|
|
800
|
+
body: z.string().min(1).describe("The wrap-up, at most 250 characters."),
|
|
801
|
+
card_ids: z.array(z.string().min(1)).optional().describe("Card ids this note concerns, e.g. `[\"DX-2894\"]`."),
|
|
802
|
+
record_refs: z
|
|
803
|
+
.array(z.string().min(1))
|
|
804
|
+
.optional()
|
|
805
|
+
.describe("Goal/rule/caveat references this note announces, e.g. `[\"G-1\", \"R-3\", \"CAV-2\"]`."),
|
|
806
|
+
section_ids: z.array(z.number().int().positive()).optional().describe("Architecture section ids this note announces."),
|
|
807
|
+
}, async (args) => jsonResult(await planAddNote(client, args)));
|
|
808
|
+
server.tool("plan_update_note", 'Edit a plan note, via PATCH /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` MUST be the note\'s `contentHash` from your last read; a mismatch writes NOTHING and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — merge into those and retry with `content_hash: currentHash`, never blindly. `title`/`body` are each optional and keep their stored value when omitted. The link fields (`card_ids`/`record_refs`/`section_ids`) are all-or-nothing AS A GROUP: omit all three to leave the stored link set untouched; send ANY one of them to REPLACE THE WHOLE SET — there is no per-link add/remove. The hash covers title, body AND the link set. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan or note id → 404. Returns the edited note plus the plan\'s latest notes page.', {
|
|
809
|
+
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
810
|
+
note_id: z.number().int().positive().describe("The note id to edit."),
|
|
811
|
+
content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
|
|
812
|
+
title: z.string().min(1).optional().describe("New title, at most 60 characters. Omit to keep the stored title."),
|
|
813
|
+
body: z.string().min(1).optional().describe("New wrap-up, at most 250 characters. Omit to keep the stored body."),
|
|
814
|
+
card_ids: z.array(z.string().min(1)).optional().describe("REPLACES the whole link set when sent (with record_refs/section_ids)."),
|
|
815
|
+
record_refs: z.array(z.string().min(1)).optional().describe("REPLACES the whole link set when sent (with card_ids/section_ids)."),
|
|
816
|
+
section_ids: z.array(z.number().int().positive()).optional().describe("REPLACES the whole link set when sent (with card_ids/record_refs)."),
|
|
817
|
+
}, async (args) => jsonResult(await planUpdateNote(client, args)));
|
|
818
|
+
server.tool("plan_delete_note", 'Soft-delete a plan note, via DELETE /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` must be the note\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — the same shape `plan_update_note` uses. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan, unknown note, or an already-deleted note → 404. Returns the plan\'s remaining latest notes page.', {
|
|
819
|
+
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
820
|
+
note_id: z.number().int().positive().describe("The note id to delete."),
|
|
821
|
+
content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
|
|
822
|
+
}, async (args) => jsonResult(await planDeleteNote(client, args)));
|
|
796
823
|
server.tool("plan_add_card", "Add an existing card to the plan this session is connected to, via POST /api/plans/mine/cards (DX-2683). The card may live on ANY board — that is what a plan is for. Idempotent: re-adding a card already on the plan is a no-op, not an error, and a card may sit in several plans at once. This adds MEMBERSHIP only; it never edits the card. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns the plan's full member list.", {
|
|
797
824
|
card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
|
|
798
825
|
}, async (args) => jsonResult(await planAddCard(client, args)));
|
package/dist/listen.js
CHANGED
|
@@ -92,8 +92,18 @@ export function invalidEventReason(value) {
|
|
|
92
92
|
if (!isRecord(d))
|
|
93
93
|
return "detail is not an object";
|
|
94
94
|
switch (value.kind) {
|
|
95
|
-
case "comment_added":
|
|
96
|
-
|
|
95
|
+
case "comment_added": {
|
|
96
|
+
if (!isCappedText(d.excerpt))
|
|
97
|
+
return "detail.excerpt is not capped text";
|
|
98
|
+
// DX-2906 — `problem` is OPTIONAL (only present when the comment named
|
|
99
|
+
// one); absent is valid, but a present-and-malformed one is not.
|
|
100
|
+
if (d.problem === undefined)
|
|
101
|
+
return null;
|
|
102
|
+
const problem = d.problem;
|
|
103
|
+
return isRecord(problem) && typeof problem.id === "number" && isCappedText(problem.statement)
|
|
104
|
+
? null
|
|
105
|
+
: "detail.problem is not {id, statement}";
|
|
106
|
+
}
|
|
97
107
|
case "solution_answered": {
|
|
98
108
|
const problem = d.problem;
|
|
99
109
|
if (!isRecord(problem) || typeof problem.id !== "number" || !isCappedText(problem.statement)) {
|
|
@@ -122,8 +132,16 @@ export function invalidEventReason(value) {
|
|
|
122
132
|
function describe(event) {
|
|
123
133
|
const d = event.detail;
|
|
124
134
|
switch (event.kind) {
|
|
125
|
-
case "comment_added":
|
|
126
|
-
|
|
135
|
+
case "comment_added": {
|
|
136
|
+
// DX-2906 — a comment naming a problem (a follow-up question, without
|
|
137
|
+
// answering it) relays distinctly from a plain comment, so an agent
|
|
138
|
+
// listening to the stream can tell which of its open problems just
|
|
139
|
+
// got a reply.
|
|
140
|
+
const problem = d.problem;
|
|
141
|
+
if (problem === undefined)
|
|
142
|
+
return `${event.actor} commented: ${quoted(d.excerpt)}`;
|
|
143
|
+
return `${event.actor} commented on problem ${quoted(problem.statement)}: ${quoted(d.excerpt)}`;
|
|
144
|
+
}
|
|
127
145
|
case "solution_answered": {
|
|
128
146
|
// DX-2735: every answer answers ONE problem on a card that may carry several,
|
|
129
147
|
// so the line names the problem — otherwise the agent cannot tell which of its
|
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.84",
|
|
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",
|