@thehammer/danx-dashboard-mcp 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/handlers.js +53 -24
- package/dist/http-client.js +3 -3
- package/dist/index.js +73 -12
- package/package.json +1 -1
- package/dist/handlers.test.js +0 -51
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `m
|
|
|
26
26
|
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
|
|
27
27
|
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose + structured keys (`title`, `description`, `ac`, `checklists`, `effort_level`, `parent_id`, `priority`, `list_id`); semantic keys refused with 400 + pointer to dedicated handler. `priority` (DX-1532) takes a tier word (`low`/`high`/…) or a number in `[0,6)` — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
|
|
28
28
|
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
|
|
29
|
-
| `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds)
|
|
29
|
+
| `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
|
|
30
30
|
| `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
|
|
31
|
-
| `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"
|
|
31
|
+
| `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"` |
|
|
32
32
|
| `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them |
|
|
33
33
|
| `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
|
|
34
34
|
|
package/dist/handlers.js
CHANGED
|
@@ -24,48 +24,77 @@
|
|
|
24
24
|
import { readFile } from "node:fs/promises";
|
|
25
25
|
import { basename, extname, isAbsolute } from "node:path";
|
|
26
26
|
import { resolvePriority } from "./priority.js";
|
|
27
|
+
/**
|
|
28
|
+
* `filter`/`fields`/`sort` are JSON/CSV-encoded onto the query string (the
|
|
29
|
+
* route parses `?filter=<JSON>`, `?fields=a,b`, `?sort=<JSON>` — see
|
|
30
|
+
* `src/issues/read/reader.ts#parseEnvelope`); each is omitted entirely when
|
|
31
|
+
* empty so the wire carries no `{}`/`[]` noise. `board` stays a flat
|
|
32
|
+
* top-level param, resolved by the HTTP client exactly as before.
|
|
33
|
+
*/
|
|
27
34
|
export async function issueList(client, args) {
|
|
28
|
-
// status_derived is display-only on the server but the route accepts
|
|
29
|
-
// it as a projection filter — passthrough verbatim. parent_id=null is
|
|
30
|
-
// the legitimate "root cards only" filter; the server reader treats
|
|
31
|
-
// the literal string "null" identically.
|
|
32
35
|
const query = {};
|
|
33
|
-
if (args.
|
|
34
|
-
query.
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
if (args.filter !== undefined && Object.keys(args.filter).length > 0) {
|
|
37
|
+
query.filter = JSON.stringify(args.filter);
|
|
38
|
+
}
|
|
39
|
+
if (args.fields !== undefined && args.fields.length > 0) {
|
|
40
|
+
query.fields = args.fields.join(",");
|
|
37
41
|
}
|
|
38
|
-
if (args.
|
|
39
|
-
query.
|
|
42
|
+
if (args.sort !== undefined && args.sort.length > 0) {
|
|
43
|
+
query.sort = JSON.stringify(args.sort);
|
|
40
44
|
}
|
|
41
|
-
if (args.assigned_agent !== undefined)
|
|
42
|
-
query.assigned_agent = args.assigned_agent;
|
|
43
|
-
if (args.include_closed !== undefined)
|
|
44
|
-
query.include_closed = args.include_closed;
|
|
45
|
-
if (args.status_derived !== undefined)
|
|
46
|
-
query.status_derived = args.status_derived;
|
|
47
|
-
if (args.q !== undefined)
|
|
48
|
-
query.q = args.q;
|
|
49
45
|
if (args.limit !== undefined)
|
|
50
46
|
query.limit = args.limit;
|
|
51
47
|
if (args.offset !== undefined)
|
|
52
48
|
query.offset = args.offset;
|
|
53
|
-
// DX-1163 — the agent surface is lean by default: opt the route into the
|
|
54
|
-
// `IssueListRowLeanV2` projection (id/type/title/status/parent_id/
|
|
55
|
-
// assigned_agent/children-ids/priority). The SPA omits this and keeps the
|
|
56
|
-
// rich board row; `issue_get` still returns the full card body.
|
|
57
|
-
query.lean = true;
|
|
58
49
|
// DX-1171 — board-only: forward the qualified board id, no repo.
|
|
59
50
|
return client.request({ method: "GET", path: "", query, board: args.board });
|
|
60
51
|
}
|
|
61
|
-
// ---------------- issue_get ----------------
|
|
62
52
|
export async function issueGet(client, args) {
|
|
53
|
+
const query = {};
|
|
54
|
+
if (args.fields !== undefined && args.fields.length > 0) {
|
|
55
|
+
query.fields = args.fields.join(",");
|
|
56
|
+
}
|
|
63
57
|
return client.request({
|
|
64
58
|
method: "GET",
|
|
65
59
|
path: `/${encodeURIComponent(args.id)}`,
|
|
60
|
+
query,
|
|
61
|
+
board: args.board,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// ---------------- repo_knowledge_get / repo_knowledge_set ----------------
|
|
65
|
+
const REPO_KNOWLEDGE_BASE_PATH = "/api/repo-knowledge";
|
|
66
|
+
/**
|
|
67
|
+
* Fetch the board's working-knowledge doc via GET /api/repo-knowledge
|
|
68
|
+
* (DX-1128, Story 2). Mirrors `issueGet` — a bare board-scoped GET, no id
|
|
69
|
+
* (the doc is 1-per-board). Board resolves the same way every other tool's
|
|
70
|
+
* `board` arg does: per-call override, else the dispatch's env-derived board.
|
|
71
|
+
*/
|
|
72
|
+
export async function repoKnowledgeGet(client, args = {}) {
|
|
73
|
+
return client.request({
|
|
74
|
+
method: "GET",
|
|
75
|
+
path: "",
|
|
76
|
+
basePath: REPO_KNOWLEDGE_BASE_PATH,
|
|
66
77
|
board: args.board,
|
|
67
78
|
});
|
|
68
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Write the board's working-knowledge doc via PUT /api/repo-knowledge
|
|
82
|
+
* (DX-1128, Story 2). Mirrors `issueEdit`'s shape (a PATCH-like body write)
|
|
83
|
+
* but targets the repo-knowledge route family, not `/api/issues`. The
|
|
84
|
+
* server's optimistic-concurrency guard rejects a stale `base_hash` — the
|
|
85
|
+
* refusal envelope (`{ok: false, body: {error, currentHash}}`) passes
|
|
86
|
+
* through verbatim so the caller can re-get, re-merge, and retry.
|
|
87
|
+
*/
|
|
88
|
+
export async function repoKnowledgeSet(client, args) {
|
|
89
|
+
const { board, ...body } = args;
|
|
90
|
+
return client.request({
|
|
91
|
+
method: "PUT",
|
|
92
|
+
path: "",
|
|
93
|
+
basePath: REPO_KNOWLEDGE_BASE_PATH,
|
|
94
|
+
body,
|
|
95
|
+
board,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
69
98
|
export async function issueCreate(client, args, defaultBoard) {
|
|
70
99
|
// Resolve the target board ONCE: per-call `args.board` override (a
|
|
71
100
|
// qualified `<repo>:<slug>` id) wins, else the dispatch's env-derived
|
package/dist/http-client.js
CHANGED
|
@@ -6,7 +6,7 @@ export class DashboardHttpClient {
|
|
|
6
6
|
this.fetchImpl = fetchImpl;
|
|
7
7
|
}
|
|
8
8
|
async request(args) {
|
|
9
|
-
const url = this.buildUrl(args.path, args.query, args.board);
|
|
9
|
+
const url = this.buildUrl(args.path, args.query, args.board, args.basePath);
|
|
10
10
|
const headers = {
|
|
11
11
|
Authorization: `Bearer ${this.config.token}`,
|
|
12
12
|
Accept: "application/json",
|
|
@@ -54,7 +54,7 @@ export class DashboardHttpClient {
|
|
|
54
54
|
}
|
|
55
55
|
return { ok: false, status: res.status, body: parsed };
|
|
56
56
|
}
|
|
57
|
-
buildUrl(path, extraQuery, boardOverride) {
|
|
57
|
+
buildUrl(path, extraQuery, boardOverride, basePath = "/api/issues") {
|
|
58
58
|
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
59
59
|
const [rawPath, existingQs] = path.split("?", 2);
|
|
60
60
|
let cleanPath;
|
|
@@ -73,6 +73,6 @@ export class DashboardHttpClient {
|
|
|
73
73
|
params.set(k, String(v));
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
return `${base}
|
|
76
|
+
return `${base}${basePath}${cleanPath}?${params.toString()}`;
|
|
77
77
|
}
|
|
78
78
|
}
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* - issue_quality_gate POST /api/issues/:id/quality-gates/:gate
|
|
25
25
|
* - issue_retro PUT /api/issues/:id/retro
|
|
26
26
|
* - issue_attach POST /api/issues/:id/attachments (reads a local file)
|
|
27
|
+
* - repo_knowledge_get GET /api/repo-knowledge
|
|
28
|
+
* - repo_knowledge_set PUT /api/repo-knowledge (DX-1128, Story 2)
|
|
27
29
|
*
|
|
28
30
|
* BOARD-ONLY (DX-1171): board is the first-level concept; repo is
|
|
29
31
|
* DERIVED from board server-side, never passed. The package composes the
|
|
@@ -53,7 +55,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
53
55
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
54
56
|
import { z } from "zod";
|
|
55
57
|
import { DashboardHttpClient } from "./http-client.js";
|
|
56
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueRequiresHuman, issueRetro, issueTransition, issueTriage, } from "./handlers.js";
|
|
58
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueRequiresHuman, issueRetro, issueTransition, issueTriage, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
57
59
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
58
60
|
function readEnvOrDie(name) {
|
|
59
61
|
const v = process.env[name];
|
|
@@ -125,6 +127,36 @@ const EFFORT_VALUES = [
|
|
|
125
127
|
];
|
|
126
128
|
const ISSUE_TYPES = ["Epic", "Bug", "Feature", "Story", "Chore"];
|
|
127
129
|
const NON_EPIC_TYPES = ["Bug", "Feature", "Story", "Chore"];
|
|
130
|
+
// DX-935 / DX-937 — field-group taxonomy for the nested read envelope,
|
|
131
|
+
// hand-copied from `src/issues/read/field-groups.ts` (LIST_GROUPS / GET_GROUPS
|
|
132
|
+
// — this package cannot import server source). Drift surfaces at runtime as a
|
|
133
|
+
// server 400, not silently.
|
|
134
|
+
const LIST_FIELD_GROUPS = [
|
|
135
|
+
"description",
|
|
136
|
+
"ac",
|
|
137
|
+
"comments",
|
|
138
|
+
"retro",
|
|
139
|
+
"dependencies",
|
|
140
|
+
"triage",
|
|
141
|
+
"requires_human",
|
|
142
|
+
"assignment",
|
|
143
|
+
"quality_gates",
|
|
144
|
+
"children",
|
|
145
|
+
"effort",
|
|
146
|
+
];
|
|
147
|
+
const GET_FIELD_GROUPS = [
|
|
148
|
+
...LIST_FIELD_GROUPS,
|
|
149
|
+
"mirrors",
|
|
150
|
+
"code_review_items",
|
|
151
|
+
];
|
|
152
|
+
const SORT_ORDERS = ["asc", "desc"];
|
|
153
|
+
const sortField = z
|
|
154
|
+
.array(z.object({
|
|
155
|
+
column: z.string().min(1),
|
|
156
|
+
order: z.enum(SORT_ORDERS),
|
|
157
|
+
}))
|
|
158
|
+
.optional()
|
|
159
|
+
.describe("Multi-column sort — ordered list of {column, order}. Absent → the server's default order (priority desc, repo_name asc, with a numeric-id tiebreaker always appended).");
|
|
128
160
|
// DX-1290 — the uniform 4-state checklist-item status. Terminal = passing|cancelled.
|
|
129
161
|
const CHECKLIST_ITEM_STATUSES = [
|
|
130
162
|
"incomplete",
|
|
@@ -160,21 +192,37 @@ const boardField = {
|
|
|
160
192
|
.describe("Target another board by its qualified id `<repo>:<slug>` (e.g. `platform:the-supply-operations-hub`); omit to use this dispatch's board. Unknown board → 404."),
|
|
161
193
|
};
|
|
162
194
|
// ---------------- issue_list ----------------
|
|
163
|
-
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).
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
195
|
+
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.", {
|
|
196
|
+
filter: z
|
|
197
|
+
.object({
|
|
198
|
+
q: z.string().optional(),
|
|
199
|
+
type: z.enum(ISSUE_TYPES).optional(),
|
|
200
|
+
parent_id: z.string().nullable().optional(),
|
|
201
|
+
dispatchable_derived: z.boolean().optional(),
|
|
202
|
+
status_derived: z.array(z.string()).optional(),
|
|
203
|
+
self_dispatchable_derived: z.boolean().optional(),
|
|
204
|
+
assigned_agent: z.string().optional(),
|
|
205
|
+
include_closed: z.boolean().optional(),
|
|
206
|
+
include_deleted: z.boolean().optional(),
|
|
207
|
+
})
|
|
208
|
+
.optional()
|
|
209
|
+
.describe("Nested list filters (DX-935) — the hard-cut replacement for the old flat top-level params, including the former standalone `q` (now `filter.q`). Omit entirely for no filtering."),
|
|
210
|
+
fields: z
|
|
211
|
+
.array(z.enum(LIST_FIELD_GROUPS))
|
|
212
|
+
.optional()
|
|
213
|
+
.describe("Opt-in field-GROUPS to add to the minimal default row: description, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, effort. Absent/empty = minimal scalars only — no joins."),
|
|
214
|
+
sort: sortField,
|
|
171
215
|
limit: z.number().int().positive().max(1000).optional(),
|
|
172
216
|
offset: z.number().int().nonnegative().optional(),
|
|
173
217
|
...boardField,
|
|
174
218
|
}, async (args) => jsonResult(await issueList(client, args)));
|
|
175
219
|
// ---------------- issue_get ----------------
|
|
176
|
-
server.tool("issue_get",
|
|
220
|
+
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), 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.", {
|
|
177
221
|
id: z.string().min(1),
|
|
222
|
+
fields: z
|
|
223
|
+
.array(z.enum(GET_FIELD_GROUPS))
|
|
224
|
+
.optional()
|
|
225
|
+
.describe("Opt-in field-GROUPS to add to the minimal default row: description, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, mirrors, code_review_items. Absent/empty = minimal scalars only."),
|
|
178
226
|
...boardField,
|
|
179
227
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
180
228
|
// ---------------- issue_create ----------------
|
|
@@ -260,7 +308,7 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issu
|
|
|
260
308
|
...boardField,
|
|
261
309
|
}, async (args) => jsonResult(await issueTransition(client, args)));
|
|
262
310
|
// ---------------- issue_triage ----------------
|
|
263
|
-
server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/triage. Verdicts: approve (stamps ready_at — moves to ToDo, clears triage TTL), cancel (stamps cancelled_at — terminal), keep (refreshes triage_expires_at by ttl_seconds — defaults 7 days, card stays at Review), defer (stamps archived_at, clears ready_at — parks to Backlog). ICE components optional but recorded when present (total = i
|
|
311
|
+
server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/triage. Verdicts: approve (stamps ready_at — moves to ToDo, clears triage TTL), cancel (stamps cancelled_at — terminal), keep (refreshes triage_expires_at by ttl_seconds — defaults 7 days, card stays at Review), defer (stamps archived_at, clears ready_at — parks to Backlog). ICE components optional but recorded when present (total = i+c+e). Reason is REQUIRED non-empty. REFUSES 409 on terminal cards.", {
|
|
264
312
|
id: z.string().min(1),
|
|
265
313
|
verdict: z.enum(TRIAGE_VERDICTS),
|
|
266
314
|
reason: z.string().min(1),
|
|
@@ -308,7 +356,7 @@ server.tool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/check
|
|
|
308
356
|
...boardField,
|
|
309
357
|
}, async (args) => jsonResult(await issueChecklist(client, args)));
|
|
310
358
|
// ---------------- issue_dependency ----------------
|
|
311
|
-
server.tool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason="recorded_in_error" on removal (encodes "removal means NOT related, never satisfied") — this MCP boundary hardcodes it, so callers do not pass reason on remove.
|
|
359
|
+
server.tool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason="recorded_in_error" on removal (encodes "removal means NOT related, never satisfied") — this MCP boundary hardcodes it, so callers do not pass reason on remove.', {
|
|
312
360
|
id: z.string().min(1),
|
|
313
361
|
action: z.enum(["add", "remove"]),
|
|
314
362
|
kind: z.enum(["depends_on", "conflict_on"]).optional(),
|
|
@@ -379,6 +427,19 @@ server.tool("issue_attach", "Attach a LOCAL file to an issue card via POST /api/
|
|
|
379
427
|
.describe("Absolute path to a local file on the dispatch's shared filesystem (must start with `/`)."),
|
|
380
428
|
...boardField,
|
|
381
429
|
}, async (args) => jsonResult(await issueAttach(client, args)));
|
|
430
|
+
// ---------------- repo_knowledge_get ----------------
|
|
431
|
+
server.tool("repo_knowledge_get", "Fetch the board's working-knowledge markdown doc via GET /api/repo-knowledge (DX-1128, Story 2). Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to read another board's doc. Returns `{ok, status, body: {content, contentHash, updatedAt, updatedBy, boardId}}` — an unset doc reads as the empty view (`content: \"\"`, `contentHash: \"\"`), NOT a 404. Ground exploratory answers in `content`; before `repo_knowledge_set`, ALWAYS `repo_knowledge_get` immediately first and pass its `contentHash` back as `base_hash` — the server's optimistic-concurrency guard rejects a stale write.", {
|
|
432
|
+
...boardField,
|
|
433
|
+
}, async (args) => jsonResult(await repoKnowledgeGet(client, args)));
|
|
434
|
+
// ---------------- repo_knowledge_set ----------------
|
|
435
|
+
server.tool("repo_knowledge_set", 'Write the board\'s working-knowledge markdown doc via PUT /api/repo-knowledge (DX-1128, Story 2). Board-scoped; defaults to the dispatch\'s board. `base_hash` MUST be the `contentHash` from the immediately-prior `repo_knowledge_get` call ("" for the true first write, when the board has no doc yet) — the server compares it against the CURRENT hash and, on mismatch, fails loud with `{ok: false, body: {error: "stale_repo_knowledge", currentHash}}` rather than silently overwriting a concurrent write. On that refusal: re-`repo_knowledge_get`, re-merge your insight into the fresh content, and retry `repo_knowledge_set` with the new hash. On success, persists to the DB, publishes `repo-knowledge:updated` over SSE (live in the dashboard editor), and returns the new view.', {
|
|
436
|
+
content: z.string(),
|
|
437
|
+
base_hash: z
|
|
438
|
+
.string()
|
|
439
|
+
.optional()
|
|
440
|
+
.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.'),
|
|
441
|
+
...boardField,
|
|
442
|
+
}, async (args) => jsonResult(await repoKnowledgeSet(client, args)));
|
|
382
443
|
// ---------------- main ----------------
|
|
383
444
|
async function main() {
|
|
384
445
|
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.32",
|
|
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",
|
package/dist/handlers.test.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { DashboardHttpClient } from "./http-client.js";
|
|
3
|
-
import { issueList, issueTransition } from "./handlers.js";
|
|
4
|
-
/**
|
|
5
|
-
* `issue_list` query-param forwarding. A fake `fetch` captures the built
|
|
6
|
-
* URL so each filter is asserted at the wire, through the real client's
|
|
7
|
-
* URL builder (`?repo=` always stamped, extra query merged after).
|
|
8
|
-
*/
|
|
9
|
-
function clientCapturing() {
|
|
10
|
-
const urls = [];
|
|
11
|
-
const fetchImpl = (async (url) => {
|
|
12
|
-
urls.push(url);
|
|
13
|
-
return new Response(JSON.stringify({ issues: [] }), { status: 200 });
|
|
14
|
-
});
|
|
15
|
-
const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
|
|
16
|
-
return { client, urls };
|
|
17
|
-
}
|
|
18
|
-
describe("issueList — query forwarding", () => {
|
|
19
|
-
it("forwards q as the server-side search needle", async () => {
|
|
20
|
-
const { client, urls } = clientCapturing();
|
|
21
|
-
await issueList(client, { q: "retire" });
|
|
22
|
-
expect(urls[0]).toContain("q=retire");
|
|
23
|
-
});
|
|
24
|
-
it("omits q when not provided", async () => {
|
|
25
|
-
const { client, urls } = clientCapturing();
|
|
26
|
-
await issueList(client, { include_closed: true });
|
|
27
|
-
expect(urls[0]).not.toContain("q=");
|
|
28
|
-
expect(urls[0]).toContain("include_closed=true");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
describe("issueTransition — body forwarding", () => {
|
|
32
|
-
function clientCapturingBody() {
|
|
33
|
-
const bodies = [];
|
|
34
|
-
const fetchImpl = (async (_url, init) => {
|
|
35
|
-
bodies.push(JSON.parse(String(init?.body ?? "{}")));
|
|
36
|
-
return new Response(JSON.stringify({ issue: {} }), { status: 200 });
|
|
37
|
-
});
|
|
38
|
-
const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
|
|
39
|
-
return { client, bodies };
|
|
40
|
-
}
|
|
41
|
-
it("forwards manual: true on pickup (DX-946 operator self-pickup)", async () => {
|
|
42
|
-
const { client, bodies } = clientCapturingBody();
|
|
43
|
-
await issueTransition(client, { id: "DX-1", action: "pickup", manual: true });
|
|
44
|
-
expect(bodies[0]).toEqual({ action: "pickup", manual: true });
|
|
45
|
-
});
|
|
46
|
-
it("omits manual when not provided", async () => {
|
|
47
|
-
const { client, bodies } = clientCapturingBody();
|
|
48
|
-
await issueTransition(client, { id: "DX-1", action: "pickup" });
|
|
49
|
-
expect(bodies[0]).toEqual({ action: "pickup" });
|
|
50
|
-
});
|
|
51
|
-
});
|