@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.
@@ -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>}` echoes the `count`
33
- * that was SUBMITTED for storage, not a re-read of the row — see
34
- * `recordBackgroundWork`'s own doc (`db/plan-session-background-work.ts`)
35
- * for why a plain no-op write is indistinguishable from a real one here;
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, count, deps) {
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
- return { ok: true, count: body.count };
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 !== 1) {
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
- body: z.unknown().optional(),
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.171",
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",