@thehammer/danx-dashboard-mcp 0.1.9 → 0.1.11
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 +4 -1
- package/dist/handlers.js +38 -31
- package/dist/http-client.js +3 -3
- package/dist/index.js +45 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,10 @@ Each tool is a thin envelope over one HTTP route — Zod-validated at the MCP bo
|
|
|
10
10
|
|---|---|
|
|
11
11
|
| `DANXBOT_DASHBOARD_URL` | Dashboard base, e.g. `http://danxbot-dashboard:5555` (inside compose) or `http://localhost:5555` (host) |
|
|
12
12
|
| `DANXBOT_DISPATCH_TOKEN` | Per-dispatch bearer — server validates via `requireUser`/dispatch-token checks |
|
|
13
|
-
| `DANX_REPO_NAME` |
|
|
13
|
+
| `DANX_REPO_NAME` | Repo half of the qualified board id |
|
|
14
|
+
| `DANXBOT_BOARD_NAME` | Board-slug half of the qualified board id (the dispatch injects `board.slug` here) |
|
|
15
|
+
|
|
16
|
+
BOARD-ONLY (DX-1171): board is the first-level concept; repo is DERIVED from board server-side, never passed. At boot the package composes the dispatch's qualified board id `<repo>:<slug>` from `DANX_REPO_NAME` + `DANXBOT_BOARD_NAME` and appends it as `?board=<id>` (and `body.board` on create) to every `/api/issues/*` request. Each tool exposes an optional `board` arg — a FULL qualified id `<repo>:<slug>` (e.g. `platform:the-supply-operations-hub`) — that overrides it per-call for cross-board reads/writes (unknown board → 404). The dashboard owns board→repo resolution; this package never sends `repo`.
|
|
14
17
|
|
|
15
18
|
## Tool surface
|
|
16
19
|
|
package/dist/handlers.js
CHANGED
|
@@ -24,26 +24,33 @@ export async function issueList(client, args) {
|
|
|
24
24
|
query.limit = args.limit;
|
|
25
25
|
if (args.offset !== undefined)
|
|
26
26
|
query.offset = args.offset;
|
|
27
|
-
|
|
27
|
+
// DX-1163 — the agent surface is lean by default: opt the route into the
|
|
28
|
+
// `IssueListRowLeanV2` projection (id/type/title/status/parent_id/
|
|
29
|
+
// assigned_agent/children-ids/priority). The SPA omits this and keeps the
|
|
30
|
+
// rich board row; `issue_get` still returns the full card body.
|
|
31
|
+
query.lean = true;
|
|
32
|
+
// DX-1171 — board-only: forward the qualified board id, no repo.
|
|
33
|
+
return client.request({ method: "GET", path: "", query, board: args.board });
|
|
28
34
|
}
|
|
29
35
|
// ---------------- issue_get ----------------
|
|
30
36
|
export async function issueGet(client, args) {
|
|
31
37
|
return client.request({
|
|
32
38
|
method: "GET",
|
|
33
39
|
path: `/${encodeURIComponent(args.id)}`,
|
|
34
|
-
|
|
40
|
+
board: args.board,
|
|
35
41
|
});
|
|
36
42
|
}
|
|
37
|
-
export async function issueCreate(client, args,
|
|
38
|
-
// Resolve the target
|
|
39
|
-
//
|
|
40
|
-
// `
|
|
41
|
-
// (
|
|
42
|
-
// `
|
|
43
|
-
// duplication is intentional. The dashboard
|
|
44
|
-
|
|
43
|
+
export async function issueCreate(client, args, defaultBoard) {
|
|
44
|
+
// Resolve the target board ONCE: per-call `args.board` override (a
|
|
45
|
+
// qualified `<repo>:<slug>` id) wins, else the dispatch's env-derived
|
|
46
|
+
// board. `board` lives in BOTH the query string (via http-client) AND
|
|
47
|
+
// the body (the create route reads it from the JSON envelope, as
|
|
48
|
+
// `repo` did pre-DX-1171). Sending only one fails-loud — the
|
|
49
|
+
// duplication is intentional. The dashboard owns board→repo and 404s
|
|
50
|
+
// an unknown board.
|
|
51
|
+
const board = args.board ?? defaultBoard;
|
|
45
52
|
const body = {
|
|
46
|
-
|
|
53
|
+
board,
|
|
47
54
|
type: args.type,
|
|
48
55
|
title: args.title,
|
|
49
56
|
description: args.description,
|
|
@@ -56,13 +63,13 @@ export async function issueCreate(client, args, defaultRepo) {
|
|
|
56
63
|
body.effort_level = args.effort_level;
|
|
57
64
|
if (args.phase_children !== undefined)
|
|
58
65
|
body.phase_children = args.phase_children;
|
|
59
|
-
return client.request({ method: "POST", path: "", body,
|
|
66
|
+
return client.request({ method: "POST", path: "", body, board });
|
|
60
67
|
}
|
|
61
68
|
export async function issueEdit(client, args) {
|
|
62
|
-
// `
|
|
69
|
+
// `board` is a transport concern (query), NOT a patch field — pull it
|
|
63
70
|
// out of `rest` so it never leaks into the `/edit` body (the server
|
|
64
71
|
// 400s on non-allowlisted keys).
|
|
65
|
-
const { id,
|
|
72
|
+
const { id, board, ...rest } = args;
|
|
66
73
|
// Strip undefined so we don't send `{title: undefined}` — JSON.stringify
|
|
67
74
|
// drops them anyway but explicit is clearer.
|
|
68
75
|
const body = {};
|
|
@@ -74,30 +81,30 @@ export async function issueEdit(client, args) {
|
|
|
74
81
|
method: "PATCH",
|
|
75
82
|
path: `/${encodeURIComponent(id)}/edit`,
|
|
76
83
|
body,
|
|
77
|
-
|
|
84
|
+
board,
|
|
78
85
|
});
|
|
79
86
|
}
|
|
80
87
|
export async function issueTransition(client, args) {
|
|
81
|
-
const { id,
|
|
88
|
+
const { id, board, ...body } = args;
|
|
82
89
|
return client.request({
|
|
83
90
|
method: "POST",
|
|
84
91
|
path: `/${encodeURIComponent(id)}/transition`,
|
|
85
92
|
body,
|
|
86
|
-
|
|
93
|
+
board,
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
96
|
export async function issueTriage(client, args) {
|
|
90
|
-
const { id,
|
|
97
|
+
const { id, board, ...body } = args;
|
|
91
98
|
return client.request({
|
|
92
99
|
method: "POST",
|
|
93
100
|
path: `/${encodeURIComponent(id)}/triage`,
|
|
94
101
|
body,
|
|
95
|
-
|
|
102
|
+
board,
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
export async function issueComment(client, args) {
|
|
99
106
|
const idEnc = encodeURIComponent(args.id);
|
|
100
|
-
const
|
|
107
|
+
const board = args.board;
|
|
101
108
|
if (args.action === "add") {
|
|
102
109
|
if (typeof args.text !== "string") {
|
|
103
110
|
throw new Error("issue_comment action=add requires text");
|
|
@@ -106,7 +113,7 @@ export async function issueComment(client, args) {
|
|
|
106
113
|
method: "POST",
|
|
107
114
|
path: `/${idEnc}/comments`,
|
|
108
115
|
body: { text: args.text },
|
|
109
|
-
|
|
116
|
+
board,
|
|
110
117
|
});
|
|
111
118
|
}
|
|
112
119
|
if (args.action === "edit") {
|
|
@@ -120,7 +127,7 @@ export async function issueComment(client, args) {
|
|
|
120
127
|
method: "PATCH",
|
|
121
128
|
path: `/${idEnc}/comments/${args.comment_id}`,
|
|
122
129
|
body: { text: args.text },
|
|
123
|
-
|
|
130
|
+
board,
|
|
124
131
|
});
|
|
125
132
|
}
|
|
126
133
|
// delete
|
|
@@ -130,12 +137,12 @@ export async function issueComment(client, args) {
|
|
|
130
137
|
return client.request({
|
|
131
138
|
method: "DELETE",
|
|
132
139
|
path: `/${idEnc}/comments/${args.comment_id}`,
|
|
133
|
-
|
|
140
|
+
board,
|
|
134
141
|
});
|
|
135
142
|
}
|
|
136
143
|
export async function issueDependency(client, args) {
|
|
137
144
|
const idEnc = encodeURIComponent(args.id);
|
|
138
|
-
const
|
|
145
|
+
const board = args.board;
|
|
139
146
|
if (args.action === "add") {
|
|
140
147
|
if (args.kind === undefined) {
|
|
141
148
|
throw new Error("issue_dependency action=add requires kind");
|
|
@@ -151,7 +158,7 @@ export async function issueDependency(client, args) {
|
|
|
151
158
|
target_id: args.target_id,
|
|
152
159
|
reason: args.reason ?? "",
|
|
153
160
|
},
|
|
154
|
-
|
|
161
|
+
board,
|
|
155
162
|
});
|
|
156
163
|
}
|
|
157
164
|
// remove
|
|
@@ -166,12 +173,12 @@ export async function issueDependency(client, args) {
|
|
|
166
173
|
// agent that types the wrong reason gets a refusal here at MCP
|
|
167
174
|
// arg-validation, not a confusing 400 envelope.
|
|
168
175
|
body: { reason: "recorded_in_error" },
|
|
169
|
-
|
|
176
|
+
board,
|
|
170
177
|
});
|
|
171
178
|
}
|
|
172
179
|
export async function issueRequiresHuman(client, args) {
|
|
173
180
|
const idEnc = encodeURIComponent(args.id);
|
|
174
|
-
const
|
|
181
|
+
const board = args.board;
|
|
175
182
|
if (args.set) {
|
|
176
183
|
if (typeof args.reason !== "string") {
|
|
177
184
|
throw new Error("issue_requires_human set=true requires reason");
|
|
@@ -183,21 +190,21 @@ export async function issueRequiresHuman(client, args) {
|
|
|
183
190
|
method: "POST",
|
|
184
191
|
path: `/${idEnc}/requires-human`,
|
|
185
192
|
body: { reason: args.reason, steps: args.steps },
|
|
186
|
-
|
|
193
|
+
board,
|
|
187
194
|
});
|
|
188
195
|
}
|
|
189
196
|
return client.request({
|
|
190
197
|
method: "DELETE",
|
|
191
198
|
path: `/${idEnc}/requires-human`,
|
|
192
|
-
|
|
199
|
+
board,
|
|
193
200
|
});
|
|
194
201
|
}
|
|
195
202
|
export async function issueRetro(client, args) {
|
|
196
|
-
const { id,
|
|
203
|
+
const { id, board, ...body } = args;
|
|
197
204
|
return client.request({
|
|
198
205
|
method: "PUT",
|
|
199
206
|
path: `/${encodeURIComponent(id)}/retro`,
|
|
200
207
|
body,
|
|
201
|
-
|
|
208
|
+
board,
|
|
202
209
|
});
|
|
203
210
|
}
|
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.
|
|
9
|
+
const url = this.buildUrl(args.path, args.query, args.board);
|
|
10
10
|
const headers = {
|
|
11
11
|
Authorization: `Bearer ${this.config.token}`,
|
|
12
12
|
Accept: "application/json",
|
|
@@ -47,7 +47,7 @@ export class DashboardHttpClient {
|
|
|
47
47
|
}
|
|
48
48
|
return { ok: false, status: res.status, body: parsed };
|
|
49
49
|
}
|
|
50
|
-
buildUrl(path, extraQuery,
|
|
50
|
+
buildUrl(path, extraQuery, boardOverride) {
|
|
51
51
|
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
52
52
|
const [rawPath, existingQs] = path.split("?", 2);
|
|
53
53
|
let cleanPath;
|
|
@@ -58,7 +58,7 @@ export class DashboardHttpClient {
|
|
|
58
58
|
cleanPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
|
|
59
59
|
}
|
|
60
60
|
const params = new URLSearchParams(existingQs ?? "");
|
|
61
|
-
params.set("
|
|
61
|
+
params.set("board", boardOverride ?? this.config.board);
|
|
62
62
|
if (extraQuery) {
|
|
63
63
|
for (const [k, v] of Object.entries(extraQuery)) {
|
|
64
64
|
if (v === undefined || v === null)
|
package/dist/index.js
CHANGED
|
@@ -22,12 +22,22 @@
|
|
|
22
22
|
* - issue_requires_human POST/DELETE /api/issues/:id/requires-human
|
|
23
23
|
* - issue_retro PUT /api/issues/:id/retro
|
|
24
24
|
*
|
|
25
|
+
* BOARD-ONLY (DX-1171): board is the first-level concept; repo is
|
|
26
|
+
* DERIVED from board server-side, never passed. The package composes the
|
|
27
|
+
* dispatch's qualified board id (`<repo>:<slug>`) at boot from
|
|
28
|
+
* DANX_REPO_NAME + DANXBOT_BOARD_NAME and sends it as `board` — the
|
|
29
|
+
* dashboard owns board→repo resolution.
|
|
30
|
+
*
|
|
25
31
|
* Env at boot (validated fail-loud — missing → process.exit(1)):
|
|
26
32
|
* DANXBOT_DASHBOARD_URL dashboard base (e.g. http://danxbot-dashboard:5555)
|
|
27
33
|
* DANXBOT_DISPATCH_TOKEN per-dispatch bearer
|
|
28
|
-
* DANX_REPO_NAME
|
|
29
|
-
*
|
|
30
|
-
*
|
|
34
|
+
* DANX_REPO_NAME repo half of the qualified board id
|
|
35
|
+
* DANXBOT_BOARD_NAME board-slug half of the qualified board id
|
|
36
|
+
* (the dispatch injects `board.slug` here);
|
|
37
|
+
* composed as `<repo>:<slug>` and appended as
|
|
38
|
+
* ?board=<id>. Each tool's optional `board` arg
|
|
39
|
+
* (a full qualified id) overrides it per-call
|
|
40
|
+
* for cross-board reads/writes.
|
|
31
41
|
*
|
|
32
42
|
* Envelope contract: every tool returns the dashboard's response body
|
|
33
43
|
* verbatim wrapped as `{ok, status, body}`. Refusals (4xx with the v2
|
|
@@ -48,10 +58,15 @@ function readEnvOrDie(name) {
|
|
|
48
58
|
}
|
|
49
59
|
return v;
|
|
50
60
|
}
|
|
61
|
+
// Compose the dispatch's qualified board id (`<repo>:<slug>`) from the
|
|
62
|
+
// two env halves the worker injects. DANXBOT_BOARD_NAME already carries
|
|
63
|
+
// the board SLUG at the spawn site (src/dispatch/core.ts sets it from
|
|
64
|
+
// `board.slug`), so a plain join yields the canonical id — no transform.
|
|
65
|
+
// Both halves are fail-loud required (Core Principle 1 — no fallback).
|
|
51
66
|
const config = {
|
|
52
67
|
baseUrl: readEnvOrDie("DANXBOT_DASHBOARD_URL"),
|
|
53
68
|
token: readEnvOrDie("DANXBOT_DISPATCH_TOKEN"),
|
|
54
|
-
|
|
69
|
+
board: `${readEnvOrDie("DANX_REPO_NAME")}:${readEnvOrDie("DANXBOT_BOARD_NAME")}`,
|
|
55
70
|
};
|
|
56
71
|
const client = new DashboardHttpClient(config);
|
|
57
72
|
const server = new McpServer({
|
|
@@ -90,20 +105,23 @@ const TRANSITION_ACTIONS = [
|
|
|
90
105
|
"reopen",
|
|
91
106
|
];
|
|
92
107
|
const TRIAGE_VERDICTS = ["approve", "cancel", "keep", "defer"];
|
|
93
|
-
// Optional per-call
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
// Optional per-call board override shared by every tool. Omitted → the
|
|
109
|
+
// dispatch's env-derived board (`<repo>:<slug>`) is used; provided → that
|
|
110
|
+
// board is targeted instead (the dashboard owns board→repo and 404s an
|
|
111
|
+
// unknown board). The override is a FULL qualified board id
|
|
112
|
+
// (`<repo>:<slug>`, e.g. `platform:the-supply-operations-hub`) — never a
|
|
113
|
+
// bare slug, since a slug alone is not globally unique. Spread into each
|
|
114
|
+
// tool's Zod input object so cross-board work is reachable from any
|
|
115
|
+
// dispatch without rebinding the MCP server.
|
|
116
|
+
const boardField = {
|
|
117
|
+
board: z
|
|
100
118
|
.string()
|
|
101
119
|
.min(1)
|
|
102
120
|
.optional()
|
|
103
|
-
.describe("Target another
|
|
121
|
+
.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."),
|
|
104
122
|
};
|
|
105
123
|
// ---------------- issue_list ----------------
|
|
106
|
-
server.tool("issue_list", "List issues for the dispatch's
|
|
124
|
+
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). Filters: q (free-text search matched case-insensitively over title + description — use this to find cards by keyword), type, parent_id (string id, null for root-only), dispatchable_derived (boolean — server-computed pickup-ready gate), assigned_agent, include_closed (default false — excludes completed_at/cancelled_at). Returns ALL matching cards (no row cap unless you pass limit). Response body shape: {issues: Issue[]} — LEAN agent rows (DX-1163): each row carries ONLY {id, type, title, status, parent_id, assigned_agent, children (ids), priority}. UI-only board fields (children_detail, counts, triage_ice_total, blocked_descendants, conflict_on, child_assignments, requires_human_child_count, latest_work_dispatch, primary_attachment) and the `description` body are NOT included — call issue_get for the full card. Server-side numeric-suffix ordering so DX-10 follows DX-9. Use this instead of grepping .danxbot/issues/ — the DB-backed route is the source of truth post-DX-704.", {
|
|
107
125
|
q: z.string().optional(),
|
|
108
126
|
status_derived: z.string().optional(),
|
|
109
127
|
type: z.enum(ISSUE_TYPES).optional(),
|
|
@@ -113,15 +131,15 @@ server.tool("issue_list", "List issues for the dispatch's repo by default via GE
|
|
|
113
131
|
include_closed: z.boolean().optional(),
|
|
114
132
|
limit: z.number().int().positive().max(1000).optional(),
|
|
115
133
|
offset: z.number().int().nonnegative().optional(),
|
|
116
|
-
...
|
|
134
|
+
...boardField,
|
|
117
135
|
}, async (args) => jsonResult(await issueList(client, args)));
|
|
118
136
|
// ---------------- issue_get ----------------
|
|
119
|
-
server.tool("issue_get", "Fetch a single hydrated issue via GET /api/issues/:id. Issue ids are globally unique
|
|
137
|
+
server.tool("issue_get", "Fetch a single hydrated 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`. Returns the full card (every joined child collection: ac, comments, dependencies, requires_human steps, retro action items + commits, triage history) plus the ancestor chain walked via parent_id. 404 envelope on unknown id.", {
|
|
120
138
|
id: z.string().min(1),
|
|
121
|
-
...
|
|
139
|
+
...boardField,
|
|
122
140
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
123
141
|
// ---------------- issue_create ----------------
|
|
124
|
-
server.tool("issue_create", "Create a fresh card via POST /api/issues.
|
|
142
|
+
server.tool("issue_create", "Create a fresh card via POST /api/issues. Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to create the card on another board (forwarded into body.board + ?board=; unknown board → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic's id as parent_id.", {
|
|
125
143
|
type: z.enum(ISSUE_TYPES),
|
|
126
144
|
title: z.string().min(1),
|
|
127
145
|
description: z.string(),
|
|
@@ -137,8 +155,8 @@ server.tool("issue_create", "Create a fresh card via POST /api/issues. Defaults
|
|
|
137
155
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
138
156
|
}))
|
|
139
157
|
.optional(),
|
|
140
|
-
...
|
|
141
|
-
}, async (args) => jsonResult(await issueCreate(client, args, config.
|
|
158
|
+
...boardField,
|
|
159
|
+
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
142
160
|
// ---------------- issue_edit ----------------
|
|
143
161
|
server.tool("issue_edit", "Patch prose fields only via PATCH /api/issues/:id/edit. ALLOWED keys: title, description, ac, effort_level, parent_id. ANY OTHER KEY (lifecycle timestamps, triage state, dependencies, retro, requires_human, blocked/dispatch gates) returns 400 with offending_keys[] and a pointer to the dedicated semantic handler — use issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro instead. AC replacement is wholesale soft-delete + reinsert with fresh ordinals; check_item_id linkage survives via title match.", {
|
|
144
162
|
id: z.string().min(1),
|
|
@@ -152,7 +170,7 @@ server.tool("issue_edit", "Patch prose fields only via PATCH /api/issues/:id/edi
|
|
|
152
170
|
.optional(),
|
|
153
171
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
154
172
|
parent_id: z.string().nullable().optional(),
|
|
155
|
-
...
|
|
173
|
+
...boardField,
|
|
156
174
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
157
175
|
// ---------------- issue_transition ----------------
|
|
158
176
|
server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issues/:id/transition. **THIS IS THE ONLY WAY TO MOVE A CARD'S LIFECYCLE STATE** — DX-835 separated card lifecycle (this tool) from dispatch finalization (`mcp__danxbot__danxbot_complete`). The worker no longer infers card moves from `danxbot_complete.status`; agents that want the card to move MUST call this tool BEFORE calling `danxbot_complete`. Actions: ready (Review→ToDo), pickup (ToDo→In Progress — server checks every dispatch gate: ready_at, blocked_at, requires_human_reason, depends_on partners terminal, conflict_on partners idle; refuses 409 with failed_gate naming the cause; pass manual:true for OPERATOR-SESSION self-pickup — stamps dispatch_kind 'manual', bypasses every card-flow gate except terminal/deleted/already-dispatched, and the worker NEVER auto-transitions the card: no orphan-heal rollback, no Epic auto-rollup — use this whenever the work happens in YOUR current session rather than a worker dispatch, DX-946), rollback_pickup, **complete** (stamps completed_at — moves card to Done; this is YOUR explicit decision, not a side effect of danxbot_complete; REFUSES 409 on Epic if any phase child non-terminal — see non_terminal_phases[]), cancel (stamps cancelled_at — terminal), **block** (requires non-empty reason — stamps blocked_at + blocked_reason + clears dispatch; USE THIS when the CARD itself cannot proceed without human intervention; distinct from env-fault dispatch failures which use `danxbot_complete({status:'failed'})`), unblock, archive (parks to Backlog, clears ready_at), reopen (terminal→active, clears completed_at/cancelled_at/archived_at). Terminal cards refuse every action except reopen. Ladder timestamps preserved — forward stamps never clear earlier ones (CLAUDE.md Core Principle 2).", {
|
|
@@ -162,7 +180,7 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issu
|
|
|
162
180
|
summary: z.string().optional(),
|
|
163
181
|
dispatch_id: z.string().min(1).optional(),
|
|
164
182
|
manual: z.boolean().optional(),
|
|
165
|
-
...
|
|
183
|
+
...boardField,
|
|
166
184
|
}, async (args) => jsonResult(await issueTransition(client, args)));
|
|
167
185
|
// ---------------- issue_triage ----------------
|
|
168
186
|
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.", {
|
|
@@ -177,7 +195,7 @@ server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/tr
|
|
|
177
195
|
})
|
|
178
196
|
.optional(),
|
|
179
197
|
ttl_seconds: z.number().positive().optional(),
|
|
180
|
-
...
|
|
198
|
+
...boardField,
|
|
181
199
|
}, async (args) => jsonResult(await issueTriage(client, args)));
|
|
182
200
|
// ---------------- issue_comment ----------------
|
|
183
201
|
server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text} (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).", {
|
|
@@ -185,7 +203,7 @@ server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid].
|
|
|
185
203
|
action: z.enum(["add", "edit", "delete"]),
|
|
186
204
|
comment_id: z.number().int().positive().optional(),
|
|
187
205
|
text: z.string().min(1).optional(),
|
|
188
|
-
...
|
|
206
|
+
...boardField,
|
|
189
207
|
}, async (args) => jsonResult(await issueComment(client, args)));
|
|
190
208
|
// ---------------- issue_dependency ----------------
|
|
191
209
|
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.", {
|
|
@@ -195,7 +213,7 @@ server.tool("issue_dependency", "Dependency CRUD via /api/issues/:id/dependencie
|
|
|
195
213
|
target_id: z.string().min(1).optional(),
|
|
196
214
|
reason: z.string().optional(),
|
|
197
215
|
dependency_id: z.number().int().positive().optional(),
|
|
198
|
-
...
|
|
216
|
+
...boardField,
|
|
199
217
|
}, async (args) => jsonResult(await issueDependency(client, args)));
|
|
200
218
|
// ---------------- issue_requires_human ----------------
|
|
201
219
|
server.tool("issue_requires_human", "Set or clear the requires_human dispatch gate via /api/issues/:id/requires-human. set=true → POST {reason, steps[]} — sets requires_human_reason (the dispatch gate per DX-704 — poller refuses pickup while non-null), set_by from bearer, set_at NOW(), REPLACES the step rows (prior soft-deleted, fresh ordinals). set=false → DELETE — clears the columns and soft-deletes every live step. Terminal cards refuse 409 on set.", {
|
|
@@ -203,7 +221,7 @@ server.tool("issue_requires_human", "Set or clear the requires_human dispatch ga
|
|
|
203
221
|
set: z.boolean(),
|
|
204
222
|
reason: z.string().optional(),
|
|
205
223
|
steps: z.array(z.string().min(1)).optional(),
|
|
206
|
-
...
|
|
224
|
+
...boardField,
|
|
207
225
|
}, async (args) => jsonResult(await issueRequiresHuman(client, args)));
|
|
208
226
|
// ---------------- issue_retro ----------------
|
|
209
227
|
server.tool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retro. Body: {good, bad, action_item_ids[], commits[]}. REFUSES 409 unless the card is terminal (completed_at OR cancelled_at) — retro ships when work concludes. Replace semantics: good/bad upsert; action_item_ids[] + commits[] soft-delete prior live rows and insert with fresh ordinals. action_item_ids[] entries MUST match <PREFIX>-N. commits[] entries take {sha, subject?}.", {
|
|
@@ -215,13 +233,13 @@ server.tool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retr
|
|
|
215
233
|
sha: z.string().min(1),
|
|
216
234
|
subject: z.string().optional(),
|
|
217
235
|
})),
|
|
218
|
-
...
|
|
236
|
+
...boardField,
|
|
219
237
|
}, async (args) => jsonResult(await issueRetro(client, args)));
|
|
220
238
|
// ---------------- main ----------------
|
|
221
239
|
async function main() {
|
|
222
240
|
const transport = new StdioServerTransport();
|
|
223
241
|
await server.connect(transport);
|
|
224
|
-
console.error(`danx-dashboard-mcp running on stdio (dashboard=${config.baseUrl},
|
|
242
|
+
console.error(`danx-dashboard-mcp running on stdio (dashboard=${config.baseUrl}, board=${config.board})`);
|
|
225
243
|
}
|
|
226
244
|
main().catch((err) => {
|
|
227
245
|
console.error(`[danx-dashboard-mcp] fatal: ${err.message}`);
|
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.11",
|
|
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",
|