@thehammer/danx-dashboard-mcp 0.1.30 → 0.1.31
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 +34 -0
- package/dist/http-client.js +3 -3
- package/dist/index.js +18 -3
- 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
|
@@ -66,6 +66,40 @@ export async function issueGet(client, args) {
|
|
|
66
66
|
board: args.board,
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
// ---------------- repo_knowledge_get / repo_knowledge_set ----------------
|
|
70
|
+
const REPO_KNOWLEDGE_BASE_PATH = "/api/repo-knowledge";
|
|
71
|
+
/**
|
|
72
|
+
* Fetch the board's working-knowledge doc via GET /api/repo-knowledge
|
|
73
|
+
* (DX-1128, Story 2). Mirrors `issueGet` — a bare board-scoped GET, no id
|
|
74
|
+
* (the doc is 1-per-board). Board resolves the same way every other tool's
|
|
75
|
+
* `board` arg does: per-call override, else the dispatch's env-derived board.
|
|
76
|
+
*/
|
|
77
|
+
export async function repoKnowledgeGet(client, args = {}) {
|
|
78
|
+
return client.request({
|
|
79
|
+
method: "GET",
|
|
80
|
+
path: "",
|
|
81
|
+
basePath: REPO_KNOWLEDGE_BASE_PATH,
|
|
82
|
+
board: args.board,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Write the board's working-knowledge doc via PUT /api/repo-knowledge
|
|
87
|
+
* (DX-1128, Story 2). Mirrors `issueEdit`'s shape (a PATCH-like body write)
|
|
88
|
+
* but targets the repo-knowledge route family, not `/api/issues`. The
|
|
89
|
+
* server's optimistic-concurrency guard rejects a stale `base_hash` — the
|
|
90
|
+
* refusal envelope (`{ok: false, body: {error, currentHash}}`) passes
|
|
91
|
+
* through verbatim so the caller can re-get, re-merge, and retry.
|
|
92
|
+
*/
|
|
93
|
+
export async function repoKnowledgeSet(client, args) {
|
|
94
|
+
const { board, ...body } = args;
|
|
95
|
+
return client.request({
|
|
96
|
+
method: "PUT",
|
|
97
|
+
path: "",
|
|
98
|
+
basePath: REPO_KNOWLEDGE_BASE_PATH,
|
|
99
|
+
body,
|
|
100
|
+
board,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
69
103
|
export async function issueCreate(client, args, defaultBoard) {
|
|
70
104
|
// Resolve the target board ONCE: per-call `args.board` override (a
|
|
71
105
|
// 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];
|
|
@@ -260,7 +262,7 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issu
|
|
|
260
262
|
...boardField,
|
|
261
263
|
}, async (args) => jsonResult(await issueTransition(client, args)));
|
|
262
264
|
// ---------------- 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
|
|
265
|
+
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
266
|
id: z.string().min(1),
|
|
265
267
|
verdict: z.enum(TRIAGE_VERDICTS),
|
|
266
268
|
reason: z.string().min(1),
|
|
@@ -308,7 +310,7 @@ server.tool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/check
|
|
|
308
310
|
...boardField,
|
|
309
311
|
}, async (args) => jsonResult(await issueChecklist(client, args)));
|
|
310
312
|
// ---------------- 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.
|
|
313
|
+
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
314
|
id: z.string().min(1),
|
|
313
315
|
action: z.enum(["add", "remove"]),
|
|
314
316
|
kind: z.enum(["depends_on", "conflict_on"]).optional(),
|
|
@@ -379,6 +381,19 @@ server.tool("issue_attach", "Attach a LOCAL file to an issue card via POST /api/
|
|
|
379
381
|
.describe("Absolute path to a local file on the dispatch's shared filesystem (must start with `/`)."),
|
|
380
382
|
...boardField,
|
|
381
383
|
}, async (args) => jsonResult(await issueAttach(client, args)));
|
|
384
|
+
// ---------------- repo_knowledge_get ----------------
|
|
385
|
+
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.", {
|
|
386
|
+
...boardField,
|
|
387
|
+
}, async (args) => jsonResult(await repoKnowledgeGet(client, args)));
|
|
388
|
+
// ---------------- repo_knowledge_set ----------------
|
|
389
|
+
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.', {
|
|
390
|
+
content: z.string(),
|
|
391
|
+
base_hash: z
|
|
392
|
+
.string()
|
|
393
|
+
.optional()
|
|
394
|
+
.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.'),
|
|
395
|
+
...boardField,
|
|
396
|
+
}, async (args) => jsonResult(await repoKnowledgeSet(client, args)));
|
|
382
397
|
// ---------------- main ----------------
|
|
383
398
|
async function main() {
|
|
384
399
|
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.31",
|
|
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
|
-
});
|