@thehammer/danx-dashboard-mcp 0.1.157 → 0.1.158

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,6 +1,10 @@
1
+ import { assertVersionFloorSatisfied, MCP_MIN_VERSION_HEADER } from "./version-floor.js";
1
2
  export class DashboardHttpClient {
2
3
  config;
3
4
  fetchImpl;
5
+ // DX-3597 — true once this instance has made ONE request, regardless of
6
+ // outcome. Drives the "first call OR any 4xx" version-floor check below.
7
+ hasCheckedVersionFloor = false;
4
8
  constructor(config, fetchImpl = fetch) {
5
9
  this.config = config;
6
10
  this.fetchImpl = fetchImpl;
@@ -50,6 +54,18 @@ export class DashboardHttpClient {
50
54
  catch (err) {
51
55
  throw new Error(`[danx-dashboard-mcp] network failure on ${args.method} ${url}: ${err instanceof Error ? err.message : String(err)}`);
52
56
  }
57
+ // DX-3597 — proactive on the first call this instance ever makes
58
+ // (regardless of outcome: a stale client can otherwise look fine for a
59
+ // while on routes whose shape didn't change), and on every subsequent
60
+ // 4xx (a response that might otherwise read as an ordinary caller
61
+ // error when it's really "you're running an old MCP"). Throws
62
+ // McpOutdatedError instead of returning below when this package is
63
+ // behind the floor — see version-floor.ts's own doc.
64
+ const isFirstCall = !this.hasCheckedVersionFloor;
65
+ this.hasCheckedVersionFloor = true;
66
+ if (isFirstCall || (res.status >= 400 && res.status < 500)) {
67
+ assertVersionFloorSatisfied(res.headers.get(MCP_MIN_VERSION_HEADER));
68
+ }
53
69
  const text = await res.text();
54
70
  let parsed = null;
55
71
  if (text !== "") {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * DX-3597 — this package's half of the MCP-version-floor guard whose
3
+ * server-side half shipped in DX-3525 (`src/mcp-contract.ts`,
4
+ * `src/dashboard/serve-request.ts`, `src/resources/field-tree.ts`, all in
5
+ * the main danxbot repo — this package cannot import them, see below).
6
+ *
7
+ * The dashboard stamps `X-Danx-Mcp-Min-Version` on every routed `/api/*`
8
+ * response with its currently-advertised floor (`MIN_MCP_VERSION` in
9
+ * `src/mcp-contract.ts` — DX-3597 made that floor track the version where
10
+ * an MCP-exposed contract last changed, not "whatever's currently
11
+ * published"; see that file's module doc for the full decision).
12
+ *
13
+ * `DashboardHttpClient.request()` (`./http-client.ts`) reads this header on
14
+ * the FIRST call this process makes (regardless of outcome — a proactive
15
+ * check, since a stale client can otherwise appear to work for a while on
16
+ * routes whose shape didn't change) and on every subsequent 4xx (a response
17
+ * that might otherwise read as an ordinary caller error, when it's really
18
+ * "you're running an old MCP"). When this package's own version is behind
19
+ * the advertised floor, it throws `McpOutdatedError` instead of returning
20
+ * the ordinary envelope — the same "throw rather than fabricate/silently
21
+ * pass through" contract `http-client.ts` already applies to a 5xx/network
22
+ * failure (see that file's own doc): an outdated MCP is exactly that class
23
+ * of failure, nothing the calling tool can meaningfully work around.
24
+ *
25
+ * Everything here is duplicated, not imported, from the server's
26
+ * `src/mcp-contract.ts` — this package is an INDEPENDENTLY PUBLISHED npm
27
+ * artifact (DX-2103) and cannot depend on the danxbot service's own `src/`
28
+ * tree. Keep the header name, the package name, and the version-compare
29
+ * behavior in agreement with that file by hand when either changes.
30
+ */
31
+ import { readFileSync } from "node:fs";
32
+ import { dirname, join } from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+ /** Must match `MCP_MIN_VERSION_HEADER` in danxbot's `src/mcp-contract.ts` verbatim. */
35
+ export const MCP_MIN_VERSION_HEADER = "X-Danx-Mcp-Min-Version";
36
+ /** Must match `MCP_PACKAGE_NAME` in danxbot's `src/mcp-contract.ts` verbatim. */
37
+ export const MCP_PACKAGE_NAME = "@thehammer/danx-dashboard-mcp";
38
+ /**
39
+ * Numeric-dotted version compare (major.minor.patch, ...) — mirrors
40
+ * `compareMcpVersions` in danxbot's `src/mcp-contract.ts` behavior
41
+ * byte-for-byte (duplicated, not imported — see module doc). Both sides of
42
+ * this contract have only ever used plain numeric dotted versions, so this
43
+ * throws rather than silently falling back to a lexical compare, which
44
+ * would misorder "0.1.9" vs "0.1.10".
45
+ */
46
+ export function compareMcpVersions(a, b) {
47
+ const parse = (v) => {
48
+ if (!/^\d+(\.\d+)*$/.test(v)) {
49
+ throw new Error(`[danx-dashboard-mcp] "${v}" is not a plain numeric dotted version (major.minor.patch)`);
50
+ }
51
+ return v.split(".").map(Number);
52
+ };
53
+ const pa = parse(a);
54
+ const pb = parse(b);
55
+ const len = Math.max(pa.length, pb.length);
56
+ for (let i = 0; i < len; i++) {
57
+ const da = pa[i] ?? 0;
58
+ const db = pb[i] ?? 0;
59
+ if (da !== db)
60
+ return da - db;
61
+ }
62
+ return 0;
63
+ }
64
+ let cachedOwnVersion;
65
+ /**
66
+ * Reads this package's OWN version from its `package.json` — always present
67
+ * next to `dist/` (or `src/`, under `npm run dev`'s `tsx`) at runtime even
68
+ * though `files: ["dist","README.md"]` excludes it from the tarball
69
+ * MANIFEST: npm always includes `package.json` in what it publishes and
70
+ * what it installs, regardless of `files` (the same guarantee `README` /
71
+ * `LICENSE` / the `main` entry get) — this reads the copy npm actually
72
+ * installed next to this module, not danxbot's own source tree. Both
73
+ * `dist/version-floor.js` and `src/version-floor.ts` sit exactly one
74
+ * directory below the package root, so `../package.json` resolves
75
+ * correctly under both the published (dist) and dev (`tsx src/index.ts`)
76
+ * shapes. Memoized — the version cannot change mid-process.
77
+ */
78
+ export function readOwnVersion() {
79
+ if (cachedOwnVersion !== undefined)
80
+ return cachedOwnVersion;
81
+ const here = dirname(fileURLToPath(import.meta.url));
82
+ const pkgJsonPath = join(here, "..", "package.json");
83
+ const raw = readFileSync(pkgJsonPath, "utf8");
84
+ const parsed = JSON.parse(raw);
85
+ if (typeof parsed.version !== "string") {
86
+ throw new Error(`[danx-dashboard-mcp] could not read this package's own version from ${pkgJsonPath}`);
87
+ }
88
+ cachedOwnVersion = parsed.version;
89
+ return cachedOwnVersion;
90
+ }
91
+ /** Test-only escape hatch — real callers never need to override the memoized read. */
92
+ export function __resetOwnVersionCacheForTests() {
93
+ cachedOwnVersion = undefined;
94
+ }
95
+ /**
96
+ * Thrown by `assertVersionFloorSatisfied` when this package is running
97
+ * behind the server's advertised floor. Carries structured `have`/`need`
98
+ * fields (not just a prose message) so a caller that wants to branch on
99
+ * "is this specifically an outdated-MCP failure" can check `error.code`
100
+ * rather than pattern-match the message text.
101
+ */
102
+ export class McpOutdatedError extends Error {
103
+ code = "mcp_outdated";
104
+ have;
105
+ need;
106
+ constructor(have, need) {
107
+ super(`[danx-dashboard-mcp] mcp_outdated: this MCP is running ${MCP_PACKAGE_NAME}@${have}, but the dashboard ` +
108
+ `requires >= ${need}. Restart your Claude Code session to pick up the published update.`);
109
+ this.name = "McpOutdatedError";
110
+ this.have = have;
111
+ this.need = need;
112
+ }
113
+ }
114
+ /**
115
+ * Compares this package's own version against the floor a response header
116
+ * advertised. No-ops (returns) when compatible, or when `headerValue` is
117
+ * `null`/empty (an old-enough server, or a test stub, that never stamped
118
+ * one — nothing to compare against). Throws `McpOutdatedError` when this
119
+ * package is behind.
120
+ */
121
+ export function assertVersionFloorSatisfied(headerValue) {
122
+ if (!headerValue)
123
+ return;
124
+ const have = readOwnVersion();
125
+ if (compareMcpVersions(have, headerValue) < 0) {
126
+ throw new McpOutdatedError(have, headerValue);
127
+ }
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.157",
3
+ "version": "0.1.158",
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",