@thehammer/danx-dashboard-mcp 0.1.2 → 0.1.5
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 +12 -12
- package/dist/handlers.js +35 -11
- package/dist/http-client.js +4 -4
- package/dist/index.js +48 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @thehammer/danx-dashboard-mcp
|
|
2
2
|
|
|
3
|
-
Stdio MCP server wrapping danxbot's dashboard `/api/
|
|
3
|
+
Stdio MCP server wrapping danxbot's dashboard `/api/issues/*` normalized DB-backed HTTP routes. Replaces the legacy "agents Edit/Write `.yml` files directly" pattern from before DX-704 / DX-811.
|
|
4
4
|
|
|
5
5
|
Each tool is a thin envelope over one HTTP route — Zod-validated at the MCP boundary, fetch under the hood, server response passed back to the agent verbatim. Refusal envelopes (`{error, ...extra}` with `failed_gate`, `non_terminal_phases`, `offending_keys`, etc.) come through as `{ok: false, status, body}` so the agent can pick the right next action without guessing. 5xx and network failures throw.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ 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` | Default repo scope appended as `?repo=<name>`; each tool's optional `repo` arg overrides it per-call for cross-repo reads/writes (unknown repo → 404) |
|
|
14
14
|
|
|
15
15
|
## Tool surface
|
|
16
16
|
|
|
@@ -18,16 +18,16 @@ All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `.
|
|
|
18
18
|
|
|
19
19
|
| Tool | HTTP | Notes |
|
|
20
20
|
|---|---|---|
|
|
21
|
-
| `issue_list` | `GET /api/
|
|
22
|
-
| `issue_get` | `GET /api/
|
|
23
|
-
| `issue_create` | `POST /api/
|
|
24
|
-
| `issue_edit` | `PATCH /api/
|
|
25
|
-
| `issue_transition` | `POST /api/
|
|
26
|
-
| `issue_triage` | `POST /api/
|
|
27
|
-
| `issue_comment` | `POST/PATCH/DELETE /api/
|
|
28
|
-
| `issue_dependency` | `POST/DELETE /api/
|
|
29
|
-
| `issue_requires_human` | `POST/DELETE /api/
|
|
30
|
-
| `issue_retro` | `PUT /api/
|
|
21
|
+
| `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
|
|
22
|
+
| `issue_get` | `GET /api/issues/:id` | Returns hydrated card + ancestor chain |
|
|
23
|
+
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
|
|
24
|
+
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose-only — semantic keys refused with 400 + pointer to dedicated handler |
|
|
25
|
+
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
|
|
26
|
+
| `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
|
|
27
|
+
| `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
|
|
28
|
+
| `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"` |
|
|
29
|
+
| `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them |
|
|
30
|
+
| `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
|
|
31
31
|
|
|
32
32
|
## Build + test
|
|
33
33
|
|
package/dist/handlers.js
CHANGED
|
@@ -22,17 +22,24 @@ export async function issueList(client, args) {
|
|
|
22
22
|
query.limit = args.limit;
|
|
23
23
|
if (args.offset !== undefined)
|
|
24
24
|
query.offset = args.offset;
|
|
25
|
-
return client.request({ method: "GET", path: "", query });
|
|
25
|
+
return client.request({ method: "GET", path: "", query, repo: args.repo });
|
|
26
26
|
}
|
|
27
27
|
// ---------------- issue_get ----------------
|
|
28
28
|
export async function issueGet(client, args) {
|
|
29
|
-
return client.request({
|
|
29
|
+
return client.request({
|
|
30
|
+
method: "GET",
|
|
31
|
+
path: `/${encodeURIComponent(args.id)}`,
|
|
32
|
+
repo: args.repo,
|
|
33
|
+
});
|
|
30
34
|
}
|
|
31
|
-
export async function issueCreate(client, args,
|
|
32
|
-
//
|
|
33
|
-
//
|
|
35
|
+
export async function issueCreate(client, args, defaultRepo) {
|
|
36
|
+
// Resolve the target repo ONCE: per-call `args.repo` override (target
|
|
37
|
+
// another registered repo) wins, else the dispatch's `DANX_REPO_NAME`.
|
|
38
|
+
// `repo` lives in BOTH the query string (via http-client) AND the body
|
|
39
|
+
// (server requires it inside the JSON envelope per
|
|
34
40
|
// `write/create.ts::validate`). Sending only one fails-loud — the
|
|
35
|
-
// duplication is intentional.
|
|
41
|
+
// duplication is intentional. The dashboard 404s an unknown repo.
|
|
42
|
+
const repo = args.repo ?? defaultRepo;
|
|
36
43
|
const body = {
|
|
37
44
|
repo,
|
|
38
45
|
type: args.type,
|
|
@@ -47,10 +54,13 @@ export async function issueCreate(client, args, repo) {
|
|
|
47
54
|
body.effort_level = args.effort_level;
|
|
48
55
|
if (args.phase_children !== undefined)
|
|
49
56
|
body.phase_children = args.phase_children;
|
|
50
|
-
return client.request({ method: "POST", path: "", body });
|
|
57
|
+
return client.request({ method: "POST", path: "", body, repo });
|
|
51
58
|
}
|
|
52
59
|
export async function issueEdit(client, args) {
|
|
53
|
-
|
|
60
|
+
// `repo` is a transport concern (query), NOT a patch field — pull it
|
|
61
|
+
// out of `rest` so it never leaks into the `/edit` body (the server
|
|
62
|
+
// 400s on non-allowlisted keys).
|
|
63
|
+
const { id, repo, ...rest } = args;
|
|
54
64
|
// Strip undefined so we don't send `{title: undefined}` — JSON.stringify
|
|
55
65
|
// drops them anyway but explicit is clearer.
|
|
56
66
|
const body = {};
|
|
@@ -62,26 +72,30 @@ export async function issueEdit(client, args) {
|
|
|
62
72
|
method: "PATCH",
|
|
63
73
|
path: `/${encodeURIComponent(id)}/edit`,
|
|
64
74
|
body,
|
|
75
|
+
repo,
|
|
65
76
|
});
|
|
66
77
|
}
|
|
67
78
|
export async function issueTransition(client, args) {
|
|
68
|
-
const { id, ...body } = args;
|
|
79
|
+
const { id, repo, ...body } = args;
|
|
69
80
|
return client.request({
|
|
70
81
|
method: "POST",
|
|
71
82
|
path: `/${encodeURIComponent(id)}/transition`,
|
|
72
83
|
body,
|
|
84
|
+
repo,
|
|
73
85
|
});
|
|
74
86
|
}
|
|
75
87
|
export async function issueTriage(client, args) {
|
|
76
|
-
const { id, ...body } = args;
|
|
88
|
+
const { id, repo, ...body } = args;
|
|
77
89
|
return client.request({
|
|
78
90
|
method: "POST",
|
|
79
91
|
path: `/${encodeURIComponent(id)}/triage`,
|
|
80
92
|
body,
|
|
93
|
+
repo,
|
|
81
94
|
});
|
|
82
95
|
}
|
|
83
96
|
export async function issueComment(client, args) {
|
|
84
97
|
const idEnc = encodeURIComponent(args.id);
|
|
98
|
+
const repo = args.repo;
|
|
85
99
|
if (args.action === "add") {
|
|
86
100
|
if (typeof args.text !== "string") {
|
|
87
101
|
throw new Error("issue_comment action=add requires text");
|
|
@@ -90,6 +104,7 @@ export async function issueComment(client, args) {
|
|
|
90
104
|
method: "POST",
|
|
91
105
|
path: `/${idEnc}/comments`,
|
|
92
106
|
body: { text: args.text },
|
|
107
|
+
repo,
|
|
93
108
|
});
|
|
94
109
|
}
|
|
95
110
|
if (args.action === "edit") {
|
|
@@ -103,6 +118,7 @@ export async function issueComment(client, args) {
|
|
|
103
118
|
method: "PATCH",
|
|
104
119
|
path: `/${idEnc}/comments/${args.comment_id}`,
|
|
105
120
|
body: { text: args.text },
|
|
121
|
+
repo,
|
|
106
122
|
});
|
|
107
123
|
}
|
|
108
124
|
// delete
|
|
@@ -112,10 +128,12 @@ export async function issueComment(client, args) {
|
|
|
112
128
|
return client.request({
|
|
113
129
|
method: "DELETE",
|
|
114
130
|
path: `/${idEnc}/comments/${args.comment_id}`,
|
|
131
|
+
repo,
|
|
115
132
|
});
|
|
116
133
|
}
|
|
117
134
|
export async function issueDependency(client, args) {
|
|
118
135
|
const idEnc = encodeURIComponent(args.id);
|
|
136
|
+
const repo = args.repo;
|
|
119
137
|
if (args.action === "add") {
|
|
120
138
|
if (args.kind === undefined) {
|
|
121
139
|
throw new Error("issue_dependency action=add requires kind");
|
|
@@ -131,6 +149,7 @@ export async function issueDependency(client, args) {
|
|
|
131
149
|
target_id: args.target_id,
|
|
132
150
|
reason: args.reason ?? "",
|
|
133
151
|
},
|
|
152
|
+
repo,
|
|
134
153
|
});
|
|
135
154
|
}
|
|
136
155
|
// remove
|
|
@@ -145,10 +164,12 @@ export async function issueDependency(client, args) {
|
|
|
145
164
|
// agent that types the wrong reason gets a refusal here at MCP
|
|
146
165
|
// arg-validation, not a confusing 400 envelope.
|
|
147
166
|
body: { reason: "recorded_in_error" },
|
|
167
|
+
repo,
|
|
148
168
|
});
|
|
149
169
|
}
|
|
150
170
|
export async function issueRequiresHuman(client, args) {
|
|
151
171
|
const idEnc = encodeURIComponent(args.id);
|
|
172
|
+
const repo = args.repo;
|
|
152
173
|
if (args.set) {
|
|
153
174
|
if (typeof args.reason !== "string") {
|
|
154
175
|
throw new Error("issue_requires_human set=true requires reason");
|
|
@@ -160,18 +181,21 @@ export async function issueRequiresHuman(client, args) {
|
|
|
160
181
|
method: "POST",
|
|
161
182
|
path: `/${idEnc}/requires-human`,
|
|
162
183
|
body: { reason: args.reason, steps: args.steps },
|
|
184
|
+
repo,
|
|
163
185
|
});
|
|
164
186
|
}
|
|
165
187
|
return client.request({
|
|
166
188
|
method: "DELETE",
|
|
167
189
|
path: `/${idEnc}/requires-human`,
|
|
190
|
+
repo,
|
|
168
191
|
});
|
|
169
192
|
}
|
|
170
193
|
export async function issueRetro(client, args) {
|
|
171
|
-
const { id, ...body } = args;
|
|
194
|
+
const { id, repo, ...body } = args;
|
|
172
195
|
return client.request({
|
|
173
196
|
method: "PUT",
|
|
174
197
|
path: `/${encodeURIComponent(id)}/retro`,
|
|
175
198
|
body,
|
|
199
|
+
repo,
|
|
176
200
|
});
|
|
177
201
|
}
|
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);
|
|
9
|
+
const url = this.buildUrl(args.path, args.query, args.repo);
|
|
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, repoOverride) {
|
|
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("repo", this.config.repo);
|
|
61
|
+
params.set("repo", repoOverride ?? this.config.repo);
|
|
62
62
|
if (extraQuery) {
|
|
63
63
|
for (const [k, v] of Object.entries(extraQuery)) {
|
|
64
64
|
if (v === undefined || v === null)
|
|
@@ -66,6 +66,6 @@ export class DashboardHttpClient {
|
|
|
66
66
|
params.set(k, String(v));
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
return `${base}/api/
|
|
69
|
+
return `${base}/api/issues${cleanPath}?${params.toString()}`;
|
|
70
70
|
}
|
|
71
71
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,30 +2,32 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @thehammer/danx-dashboard-mcp
|
|
4
4
|
*
|
|
5
|
-
* Stdio MCP server wrapping danxbot's dashboard `/api/
|
|
5
|
+
* Stdio MCP server wrapping danxbot's dashboard `/api/issues/*`
|
|
6
6
|
* normalized DB-backed HTTP routes (DX-704 Phase 2 / DX-811). Replaces
|
|
7
7
|
* the legacy "agents Edit/Write YAML files directly" pattern: every
|
|
8
8
|
* tool here POSTs/GETs against the dashboard, which atomically applies
|
|
9
|
-
* the change via the
|
|
9
|
+
* the change via the transactional layer and publishes SSE.
|
|
10
10
|
*
|
|
11
11
|
* Tool surface (all exposed as `mcp__danx_dashboard__<name>` once wired
|
|
12
12
|
* through the workspace `.mcp.json`):
|
|
13
13
|
*
|
|
14
|
-
* - issue_list GET /api/
|
|
15
|
-
* - issue_get GET /api/
|
|
16
|
-
* - issue_create POST /api/
|
|
17
|
-
* - issue_edit PATCH /api/
|
|
18
|
-
* - issue_transition POST /api/
|
|
19
|
-
* - issue_triage POST /api/
|
|
20
|
-
* - issue_comment POST/PATCH/DELETE /api/
|
|
21
|
-
* - issue_dependency POST/DELETE /api/
|
|
22
|
-
* - issue_requires_human POST/DELETE /api/
|
|
23
|
-
* - issue_retro PUT /api/
|
|
14
|
+
* - issue_list GET /api/issues
|
|
15
|
+
* - issue_get GET /api/issues/:id
|
|
16
|
+
* - issue_create POST /api/issues
|
|
17
|
+
* - issue_edit PATCH /api/issues/:id/edit
|
|
18
|
+
* - issue_transition POST /api/issues/:id/transition
|
|
19
|
+
* - issue_triage POST /api/issues/:id/triage
|
|
20
|
+
* - issue_comment POST/PATCH/DELETE /api/issues/:id/comments[/:cid]
|
|
21
|
+
* - issue_dependency POST/DELETE /api/issues/:id/dependencies[/:did]
|
|
22
|
+
* - issue_requires_human POST/DELETE /api/issues/:id/requires-human
|
|
23
|
+
* - issue_retro PUT /api/issues/:id/retro
|
|
24
24
|
*
|
|
25
25
|
* Env at boot (validated fail-loud — missing → process.exit(1)):
|
|
26
26
|
* DANXBOT_DASHBOARD_URL dashboard base (e.g. http://danxbot-dashboard:5555)
|
|
27
27
|
* DANXBOT_DISPATCH_TOKEN per-dispatch bearer
|
|
28
|
-
* DANX_REPO_NAME repo scope, appended as ?repo=<name
|
|
28
|
+
* DANX_REPO_NAME default repo scope, appended as ?repo=<name>;
|
|
29
|
+
* each tool's optional `repo` arg overrides it
|
|
30
|
+
* per-call for cross-repo reads/writes
|
|
29
31
|
*
|
|
30
32
|
* Envelope contract: every tool returns the dashboard's response body
|
|
31
33
|
* verbatim wrapped as `{ok, status, body}`. Refusals (4xx with the v2
|
|
@@ -88,8 +90,20 @@ const TRANSITION_ACTIONS = [
|
|
|
88
90
|
"reopen",
|
|
89
91
|
];
|
|
90
92
|
const TRIAGE_VERDICTS = ["approve", "cancel", "keep", "defer"];
|
|
93
|
+
// Optional per-call repo override shared by every tool. Omitted → the
|
|
94
|
+
// server binding's DANX_REPO_NAME is used; provided → that registered
|
|
95
|
+
// repo is targeted instead (the dashboard 404s an unknown repo). Spread
|
|
96
|
+
// into each tool's Zod input object so cross-repo work is reachable from
|
|
97
|
+
// any dispatch without rebinding the MCP server.
|
|
98
|
+
const repoField = {
|
|
99
|
+
repo: z
|
|
100
|
+
.string()
|
|
101
|
+
.min(1)
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Target another registered repo; omit to use this dispatch's repo. Unknown repo → 404."),
|
|
104
|
+
};
|
|
91
105
|
// ---------------- issue_list ----------------
|
|
92
|
-
server.tool("issue_list", "List issues for the
|
|
106
|
+
server.tool("issue_list", "List issues for the dispatch's repo by default via GET /api/issues. Pass `repo` to list another registered repo instead (unknown repo → 404). Filters: 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). Response body shape: {issues: Issue[]}. 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.", {
|
|
93
107
|
status_derived: z.string().optional(),
|
|
94
108
|
type: z.enum(ISSUE_TYPES).optional(),
|
|
95
109
|
parent_id: z.string().nullable().optional(),
|
|
@@ -98,13 +112,15 @@ server.tool("issue_list", "List issues for the current repo via GET /api/v2/issu
|
|
|
98
112
|
include_closed: z.boolean().optional(),
|
|
99
113
|
limit: z.number().int().positive().max(1000).optional(),
|
|
100
114
|
offset: z.number().int().nonnegative().optional(),
|
|
115
|
+
...repoField,
|
|
101
116
|
}, async (args) => jsonResult(await issueList(client, args)));
|
|
102
117
|
// ---------------- issue_get ----------------
|
|
103
|
-
server.tool("issue_get", "Fetch a single hydrated issue via GET /api/
|
|
118
|
+
server.tool("issue_get", "Fetch a single hydrated issue via GET /api/issues/:id. Issue ids are globally unique across repos, so this resolves from any dispatch regardless of `repo`. 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.", {
|
|
104
119
|
id: z.string().min(1),
|
|
105
|
-
|
|
120
|
+
...repoField,
|
|
121
|
+
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
106
122
|
// ---------------- issue_create ----------------
|
|
107
|
-
server.tool("issue_create", "Create a fresh card via POST /api/
|
|
123
|
+
server.tool("issue_create", "Create a fresh card via POST /api/issues. Defaults to the dispatch's repo; pass `repo` to create the card in another registered repo (forwarded into body.repo + ?repo=; unknown repo → 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.", {
|
|
108
124
|
type: z.enum(ISSUE_TYPES),
|
|
109
125
|
title: z.string().min(1),
|
|
110
126
|
description: z.string(),
|
|
@@ -120,9 +136,10 @@ server.tool("issue_create", "Create a fresh card via POST /api/v2/issues. INVARI
|
|
|
120
136
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
121
137
|
}))
|
|
122
138
|
.optional(),
|
|
139
|
+
...repoField,
|
|
123
140
|
}, async (args) => jsonResult(await issueCreate(client, args, config.repo)));
|
|
124
141
|
// ---------------- issue_edit ----------------
|
|
125
|
-
server.tool("issue_edit", "Patch prose fields only via PATCH /api/
|
|
142
|
+
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.", {
|
|
126
143
|
id: z.string().min(1),
|
|
127
144
|
title: z.string().min(1).optional(),
|
|
128
145
|
description: z.string().optional(),
|
|
@@ -134,17 +151,19 @@ server.tool("issue_edit", "Patch prose fields only via PATCH /api/v2/issues/:id/
|
|
|
134
151
|
.optional(),
|
|
135
152
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
136
153
|
parent_id: z.string().nullable().optional(),
|
|
154
|
+
...repoField,
|
|
137
155
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
138
156
|
// ---------------- issue_transition ----------------
|
|
139
|
-
server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/
|
|
157
|
+
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), 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).", {
|
|
140
158
|
id: z.string().min(1),
|
|
141
159
|
action: z.enum(TRANSITION_ACTIONS),
|
|
142
160
|
reason: z.string().optional(),
|
|
143
161
|
summary: z.string().optional(),
|
|
144
162
|
dispatch_id: z.string().min(1).optional(),
|
|
163
|
+
...repoField,
|
|
145
164
|
}, async (args) => jsonResult(await issueTransition(client, args)));
|
|
146
165
|
// ---------------- issue_triage ----------------
|
|
147
|
-
server.tool("issue_triage", "Record a triage verdict via POST /api/
|
|
166
|
+
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.", {
|
|
148
167
|
id: z.string().min(1),
|
|
149
168
|
verdict: z.enum(TRIAGE_VERDICTS),
|
|
150
169
|
reason: z.string().min(1),
|
|
@@ -156,32 +175,36 @@ server.tool("issue_triage", "Record a triage verdict via POST /api/v2/issues/:id
|
|
|
156
175
|
})
|
|
157
176
|
.optional(),
|
|
158
177
|
ttl_seconds: z.number().positive().optional(),
|
|
178
|
+
...repoField,
|
|
159
179
|
}, async (args) => jsonResult(await issueTriage(client, args)));
|
|
160
180
|
// ---------------- issue_comment ----------------
|
|
161
|
-
server.tool("issue_comment", "Comment CRUD via /api/
|
|
181
|
+
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).", {
|
|
162
182
|
id: z.string().min(1),
|
|
163
183
|
action: z.enum(["add", "edit", "delete"]),
|
|
164
184
|
comment_id: z.number().int().positive().optional(),
|
|
165
185
|
text: z.string().min(1).optional(),
|
|
186
|
+
...repoField,
|
|
166
187
|
}, async (args) => jsonResult(await issueComment(client, args)));
|
|
167
188
|
// ---------------- issue_dependency ----------------
|
|
168
|
-
server.tool("issue_dependency", "Dependency CRUD via /api/
|
|
189
|
+
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.", {
|
|
169
190
|
id: z.string().min(1),
|
|
170
191
|
action: z.enum(["add", "remove"]),
|
|
171
192
|
kind: z.enum(["depends_on", "conflict_on"]).optional(),
|
|
172
193
|
target_id: z.string().min(1).optional(),
|
|
173
194
|
reason: z.string().optional(),
|
|
174
195
|
dependency_id: z.number().int().positive().optional(),
|
|
196
|
+
...repoField,
|
|
175
197
|
}, async (args) => jsonResult(await issueDependency(client, args)));
|
|
176
198
|
// ---------------- issue_requires_human ----------------
|
|
177
|
-
server.tool("issue_requires_human", "Set or clear the requires_human dispatch gate via /api/
|
|
199
|
+
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.", {
|
|
178
200
|
id: z.string().min(1),
|
|
179
201
|
set: z.boolean(),
|
|
180
202
|
reason: z.string().optional(),
|
|
181
203
|
steps: z.array(z.string().min(1)).optional(),
|
|
204
|
+
...repoField,
|
|
182
205
|
}, async (args) => jsonResult(await issueRequiresHuman(client, args)));
|
|
183
206
|
// ---------------- issue_retro ----------------
|
|
184
|
-
server.tool("issue_retro", "Replace the retro block via PUT /api/
|
|
207
|
+
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?}.", {
|
|
185
208
|
id: z.string().min(1),
|
|
186
209
|
good: z.string(),
|
|
187
210
|
bad: z.string(),
|
|
@@ -190,6 +213,7 @@ server.tool("issue_retro", "Replace the retro block via PUT /api/v2/issues/:id/r
|
|
|
190
213
|
sha: z.string().min(1),
|
|
191
214
|
subject: z.string().optional(),
|
|
192
215
|
})),
|
|
216
|
+
...repoField,
|
|
193
217
|
}, async (args) => jsonResult(await issueRetro(client, args)));
|
|
194
218
|
// ---------------- main ----------------
|
|
195
219
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Stdio MCP server wrapping danxbot's dashboard /api/
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
7
7
|
"main": "dist/index.js",
|