@thehammer/danx-dashboard-mcp 0.1.171 → 0.1.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/background-work.js +34 -12
- package/dist/index.js +11 -2
- package/dist/types.js +30 -0
- package/package.json +1 -1
package/dist/background-work.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `danx-dashboard-mcp background-work <count|clear>` — the ONE-SHOT client of
|
|
2
|
+
* `danx-dashboard-mcp background-work <count|clear> <event-at>` — the ONE-SHOT client of
|
|
3
3
|
* `PUT /api/plan-sessions/me/background-work` (DX-3367).
|
|
4
4
|
*
|
|
5
5
|
* WHO RUNS IT. The danxbot Claude Code plugin's `Stop` / `SubagentStop` /
|
|
@@ -29,10 +29,15 @@
|
|
|
29
29
|
* no-op — `{"ok":false,"reason":"no_connection_record"}` — the plugin
|
|
30
30
|
* hook simply does not forward anything further in this case, exactly
|
|
31
31
|
* what "no-op when the session isn't plan-connected" means in practice;
|
|
32
|
-
* - on success, `{"ok":true,"count":<number|null>}`
|
|
33
|
-
*
|
|
34
|
-
* `
|
|
35
|
-
*
|
|
32
|
+
* - on success, `{"ok":true,"count":<number|null>,"applied":<boolean>}`
|
|
33
|
+
* echoes the SUBMITTED `count`; `applied` is the server's answer to
|
|
34
|
+
* whether it was stored (DX-3676: `false` for an out-of-order report
|
|
35
|
+
* older than the one already on file — the caller must not treat it as
|
|
36
|
+
* the dashboard's current count);
|
|
37
|
+
* - DX-3676 — `<event-at>` is the ISO-8601 UTC time the reporting HOOK
|
|
38
|
+
* fired, captured by the caller, never this process's own clock: the
|
|
39
|
+
* hooks are async and can finish out of order, and the server orders
|
|
40
|
+
* reports by this time;
|
|
36
41
|
* - human-readable diagnostics go to stderr only, never stdout.
|
|
37
42
|
*
|
|
38
43
|
* THE CREDENTIAL — the SAME resolver `bridge` and `plan-state` use, via
|
|
@@ -56,7 +61,7 @@ export const BACKGROUND_WORK_SUBCOMMAND = "background-work";
|
|
|
56
61
|
export const BACKGROUND_WORK_PATH = "/api/plan-sessions/me/background-work";
|
|
57
62
|
/** How long this one-shot call may take before it counts as a timeout. */
|
|
58
63
|
export const BACKGROUND_WORK_REQUEST_TIMEOUT_MS = 5_000;
|
|
59
|
-
const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${BACKGROUND_WORK_SUBCOMMAND} <non-negative-integer|clear>`;
|
|
64
|
+
const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${BACKGROUND_WORK_SUBCOMMAND} <non-negative-integer|clear> <event-at ISO-8601 UTC>`;
|
|
60
65
|
/**
|
|
61
66
|
* Parse the ONE positional argument: `clear`, or a non-negative integer
|
|
62
67
|
* string. Anything else is a usage error — never guessed or floored.
|
|
@@ -68,6 +73,15 @@ export function parseCountArg(raw) {
|
|
|
68
73
|
return { error: `count must be "clear" or a non-negative integer, got "${raw}"` };
|
|
69
74
|
return Number.parseInt(raw, 10);
|
|
70
75
|
}
|
|
76
|
+
/** DX-3676 — an ISO-8601 UTC instant, as `Date#toISOString` writes it (the server accepts exactly this). */
|
|
77
|
+
const ISO_UTC_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
|
|
78
|
+
/** Parse the event-time argument (DX-3676). Never substituted with this process's clock. */
|
|
79
|
+
export function parseEventAtArg(raw) {
|
|
80
|
+
if (!ISO_UTC_INSTANT.test(raw) || Number.isNaN(Date.parse(raw))) {
|
|
81
|
+
return { error: `event-at must be an ISO-8601 UTC timestamp (e.g. 2026-09-30T00:00:00.000Z), got "${raw}"` };
|
|
82
|
+
}
|
|
83
|
+
return raw;
|
|
84
|
+
}
|
|
71
85
|
/**
|
|
72
86
|
* One authenticated PUT, with a hard timeout, classified into the output
|
|
73
87
|
* shape this subcommand always prints — never a throw, never a non-2xx
|
|
@@ -81,8 +95,8 @@ export function parseCountArg(raw) {
|
|
|
81
95
|
* (no `409 session_not_connected` equivalent), so status alone decides
|
|
82
96
|
* whether a body read is even attempted.
|
|
83
97
|
*/
|
|
84
|
-
export async function putBackgroundWork(options,
|
|
85
|
-
const sent = await sendSessionRequest(options, BACKGROUND_WORK_PATH, { method: "PUT", body: JSON.stringify({ count }) }, deps);
|
|
98
|
+
export async function putBackgroundWork(options, report, deps) {
|
|
99
|
+
const sent = await sendSessionRequest(options, BACKGROUND_WORK_PATH, { method: "PUT", body: JSON.stringify({ count: report.count, eventAt: report.eventAt }) }, deps);
|
|
86
100
|
if ("reason" in sent)
|
|
87
101
|
return sent;
|
|
88
102
|
const { status, text } = sent;
|
|
@@ -99,10 +113,12 @@ export async function putBackgroundWork(options, count, deps) {
|
|
|
99
113
|
if (typeof body !== "object" ||
|
|
100
114
|
body === null ||
|
|
101
115
|
!("count" in body) ||
|
|
102
|
-
(body.count !== null && typeof body.count !== "number")
|
|
116
|
+
(body.count !== null && typeof body.count !== "number") ||
|
|
117
|
+
typeof body.applied !== "boolean") {
|
|
103
118
|
return { ok: false, reason: "bad_response" };
|
|
104
119
|
}
|
|
105
|
-
|
|
120
|
+
const shaped = body;
|
|
121
|
+
return { ok: true, count: shaped.count, applied: shaped.applied };
|
|
106
122
|
}
|
|
107
123
|
/** The bin's `background-work` subcommand, wired to the real process. Always resolves to exit code 0. */
|
|
108
124
|
export async function runBackgroundWorkCommand(argv, env = process.env,
|
|
@@ -111,7 +127,7 @@ resolveFrom = {}) {
|
|
|
111
127
|
const write = (output) => {
|
|
112
128
|
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
113
129
|
};
|
|
114
|
-
if (argv.length !==
|
|
130
|
+
if (argv.length !== 2) {
|
|
115
131
|
write({ ok: false, reason: "usage" });
|
|
116
132
|
process.stderr.write(`${USAGE}\n`);
|
|
117
133
|
return 0;
|
|
@@ -123,6 +139,12 @@ resolveFrom = {}) {
|
|
|
123
139
|
return 0;
|
|
124
140
|
}
|
|
125
141
|
const count = parsed;
|
|
142
|
+
const eventAt = parseEventAtArg(argv[1]);
|
|
143
|
+
if (typeof eventAt === "object") {
|
|
144
|
+
write({ ok: false, reason: "usage" });
|
|
145
|
+
process.stderr.write(`${eventAt.error}\n${USAGE}\n`);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
126
148
|
const resolved = await resolveSessionOptions(env, resolveFrom, USAGE);
|
|
127
149
|
if (!resolved.ok) {
|
|
128
150
|
// A session that never `plan_connect`-ed has no connection record — this
|
|
@@ -132,7 +154,7 @@ resolveFrom = {}) {
|
|
|
132
154
|
process.stderr.write(`${resolved.detail}\n`);
|
|
133
155
|
return 0;
|
|
134
156
|
}
|
|
135
|
-
const output = await putBackgroundWork(resolved.options, count, {
|
|
157
|
+
const output = await putBackgroundWork(resolved.options, { count, eventAt }, {
|
|
136
158
|
fetch: (input, init) => fetch(input, init),
|
|
137
159
|
requestTimeoutMs: BACKGROUND_WORK_REQUEST_TIMEOUT_MS,
|
|
138
160
|
});
|
package/dist/index.js
CHANGED
|
@@ -84,7 +84,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
84
84
|
import { z } from "zod";
|
|
85
85
|
import { DashboardHttpClient } from "./http-client.js";
|
|
86
86
|
import { planConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DANXBOT_API_METHODS, } from "./handlers.js";
|
|
87
|
-
import { queryValueSchema } from "./types.js";
|
|
87
|
+
import { bodyValueSchema, queryValueSchema } from "./types.js";
|
|
88
88
|
import { registerLifecycleTools, applyLifecycleVisibility, readLifecycleUrlsFromEnv, } from "./lifecycle.js";
|
|
89
89
|
function readEnvOrDie(name) {
|
|
90
90
|
const v = process.env[name];
|
|
@@ -348,7 +348,16 @@ strictTool("danxbot_api", "Call any dashboard API route your credential may reac
|
|
|
348
348
|
// elements are restricted to scalars, the only thing a comma-join can
|
|
349
349
|
// represent losslessly.
|
|
350
350
|
query: queryValueSchema.optional(),
|
|
351
|
-
|
|
351
|
+
// DX-3685 — was `z.unknown()` (JSON Schema `{}`), which gave the model
|
|
352
|
+
// no structural hint that a body must be an OBJECT, so it sometimes
|
|
353
|
+
// sent `body` as a JSON-encoded string; `http-client.ts` then
|
|
354
|
+
// `JSON.stringify`'d that string again, and every dashboard route
|
|
355
|
+
// refused the resulting JSON-string-literal body with `body: must be an
|
|
356
|
+
// object` (every route's body schema is object-only — see
|
|
357
|
+
// `bodyValueSchema`'s own doc in `types.ts`). A string/array/etc body
|
|
358
|
+
// now fails zod validation at this boundary with a clear message
|
|
359
|
+
// instead of reaching the dashboard malformed.
|
|
360
|
+
body: bodyValueSchema.optional(),
|
|
352
361
|
...boardField,
|
|
353
362
|
}, async (args) => jsonResult(await danxbotApi(client, args)));
|
|
354
363
|
// ---------------- danxbot_api_list (DX-3465 A5, PLN-11 R-25) ----------------
|
package/dist/types.js
CHANGED
|
@@ -14,3 +14,33 @@ export const queryValueSchema = z.record(z.union([
|
|
|
14
14
|
z.record(z.unknown()),
|
|
15
15
|
z.array(z.union([z.string(), z.number(), z.boolean()])),
|
|
16
16
|
]));
|
|
17
|
+
/**
|
|
18
|
+
* DX-3685 — the zod schema for `danxbot_api`'s `body` argument. Every
|
|
19
|
+
* dashboard route that takes a body validates it against a
|
|
20
|
+
* `BodyObjectSchema` (`src/http/body-schema.ts`, this repo) whose top-level
|
|
21
|
+
* `type` is ALWAYS `"object"` — no route ever accepts a body that is a
|
|
22
|
+
* string, an array, or any other JSON type at the top level (a plain-object
|
|
23
|
+
* request is the ONLY shape `validateBody`'s `"object"` case accepts; every
|
|
24
|
+
* other top-level shape hits its `body: must be an object` refusal).
|
|
25
|
+
*
|
|
26
|
+
* The previous schema, `z.unknown()`, compiles to `{}` in the tool's
|
|
27
|
+
* published JSON Schema — "any value, no structural hint at all" — which
|
|
28
|
+
* gave the calling model zero signal that an OBJECT was expected. In
|
|
29
|
+
* production this meant the model sometimes serialized `body` as a
|
|
30
|
+
* JSON-encoded STRING (e.g. `'{"text":"test"}'` instead of
|
|
31
|
+
* `{text:"test"}`); `http-client.ts#request` then ran `JSON.stringify` on
|
|
32
|
+
* that already-stringified value, producing a JSON STRING LITERAL as the
|
|
33
|
+
* wire body instead of a JSON object, which the dashboard's shared body
|
|
34
|
+
* validator correctly rejected with `body: must be an object` on every
|
|
35
|
+
* write (DX-3685).
|
|
36
|
+
*
|
|
37
|
+
* `z.record(z.unknown())` (the same idiom `queryValueSchema` above uses for
|
|
38
|
+
* its own field-tree branch) both (a) advertises `type: "object"` in the
|
|
39
|
+
* published JSON Schema, giving the model the structural hint it was
|
|
40
|
+
* missing, and (b) makes zod itself refuse a string/array/number/etc body
|
|
41
|
+
* at the MCP validation boundary with a clear, actionable
|
|
42
|
+
* `Expected object, received string`-class error — never silently
|
|
43
|
+
* forwarded, and never auto-`JSON.parse`d back into shape (Core Principle 1
|
|
44
|
+
* forbids tolerating a second input shape).
|
|
45
|
+
*/
|
|
46
|
+
export const bodyValueSchema = z.record(z.unknown());
|
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.173",
|
|
4
4
|
"description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|