@thehammer/danx-dashboard-mcp 0.1.226 → 0.1.228
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/bridge.js +43 -5
- package/dist/handlers.js +3 -1
- package/dist/index.js +24 -9
- package/dist/result-limit.js +25 -0
- package/dist/session-title.js +123 -0
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -76,6 +76,7 @@ import { readSessionConnection, sessionConnectionPath } from "./session-connecti
|
|
|
76
76
|
import { capResumeIds, DELIVERED_ID_MEMORY, HEALTHY_CONNECTION_MS, READ_IDLE_TIMEOUT_MS, runListener, } from "./listen.js";
|
|
77
77
|
import { isClaimState } from "@thehammer/danx-session-telemetry";
|
|
78
78
|
import { OperatorSessionStats, transcriptPathFromEnv, } from "./session-stats.js";
|
|
79
|
+
import { TranscriptTitleReader } from "./session-title.js";
|
|
79
80
|
/**
|
|
80
81
|
* DX-2730 D4 — WHICH EVENTS WAKE THE SESSION, NOW THAT EVERY ONE OF THEM CAN.
|
|
81
82
|
*
|
|
@@ -586,7 +587,9 @@ export function degradePolicyFor(reason) {
|
|
|
586
587
|
* heartbeat call itself was rejected". This gap is real and is recorded on
|
|
587
588
|
* the card rather than silently papered over.
|
|
588
589
|
*/
|
|
589
|
-
async function sendHeartbeat(options, deps, connection, stats
|
|
590
|
+
async function sendHeartbeat(options, deps, connection, stats,
|
|
591
|
+
// DX-4461 - the session's current Claude title, read from its transcript (`session-title.ts`).
|
|
592
|
+
title) {
|
|
590
593
|
const body = { state: connection.state };
|
|
591
594
|
if (connection.reason !== undefined)
|
|
592
595
|
body.reason = connection.reason;
|
|
@@ -594,7 +597,7 @@ async function sendHeartbeat(options, deps, connection, stats) {
|
|
|
594
597
|
body.inventory = connection.inventory;
|
|
595
598
|
const outcome = await request(options, deps, "heartbeat", `${options.dashboardUrl}${HEARTBEAT_PATH}`, {
|
|
596
599
|
method: "POST",
|
|
597
|
-
body: JSON.stringify(stats
|
|
600
|
+
body: JSON.stringify({ connection: body, ...(stats !== undefined ? { stats } : {}), ...(title !== undefined ? { title } : {}) }),
|
|
598
601
|
});
|
|
599
602
|
// DX-3099 — this is now the ONE place a heartbeat's own outcome is
|
|
600
603
|
// inspected. `request()` still never throws, and a 401/403 is still an
|
|
@@ -604,7 +607,7 @@ async function sendHeartbeat(options, deps, connection, stats) {
|
|
|
604
607
|
// request never landed at all, which is exactly the signal the silent-
|
|
605
608
|
// failure counter in `runBridge` below needs. Still never throws.
|
|
606
609
|
if (outcome.kind !== "answer")
|
|
607
|
-
return { landed: false, status: null, stats: null, revocation: null };
|
|
610
|
+
return { landed: false, status: null, stats: null, revocation: null, title: null };
|
|
608
611
|
// DX-2954 — the answer's `stats` (the operator dispatch's stored or
|
|
609
612
|
// just-written totals and cursors) feed the session's stats collector.
|
|
610
613
|
const ok = outcome.status >= 200 && outcome.status < 300;
|
|
@@ -623,7 +626,15 @@ async function sendHeartbeat(options, deps, connection, stats) {
|
|
|
623
626
|
? statsBody
|
|
624
627
|
: null;
|
|
625
628
|
// DX-4418 - the dashboard refuses the beat of a session whose key a person revoked, and says so.
|
|
626
|
-
|
|
629
|
+
// DX-4461 - the dashboard's stored title for this session; an answer without one is an older dashboard's.
|
|
630
|
+
const storedTitle = ok && isRecord(outcome.body) && typeof outcome.body.title === "string" ? outcome.body.title : null;
|
|
631
|
+
return {
|
|
632
|
+
landed: true,
|
|
633
|
+
status: outcome.status,
|
|
634
|
+
stats: answered,
|
|
635
|
+
revocation: keyRevocationOf({ ok: false, status: outcome.status, body: outcome.body }),
|
|
636
|
+
title: storedTitle,
|
|
637
|
+
};
|
|
627
638
|
}
|
|
628
639
|
function isRecord(value) {
|
|
629
640
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -669,11 +680,38 @@ export async function runBridge(options, deps) {
|
|
|
669
680
|
: connectionState === "stopped"
|
|
670
681
|
? { state: "stopped", reason: stoppedReason ?? undefined }
|
|
671
682
|
: { state: "reconnecting" };
|
|
683
|
+
// DX-4461 - a rename reaches the dashboard without the agent: the title in the session's transcript rides the heartbeat
|
|
684
|
+
// when it differs from the one the dashboard last answered with. Until an answer carries a title (an older dashboard
|
|
685
|
+
// never does) nothing is sent, since that dashboard refuses the `title` key and a refused heartbeat is a dead listener.
|
|
686
|
+
const titleReader = options.transcriptPath !== undefined ? new TranscriptTitleReader(options.transcriptPath) : null;
|
|
687
|
+
let dashboardTitle;
|
|
688
|
+
let lastTitleError = null;
|
|
689
|
+
const titleToReport = () => {
|
|
690
|
+
if (titleReader === null || dashboardTitle === undefined)
|
|
691
|
+
return undefined;
|
|
692
|
+
// The title is cosmetic and the heartbeat is liveness: a transcript the title cannot be read from costs the title
|
|
693
|
+
// (said once per distinct failure), never the beat.
|
|
694
|
+
try {
|
|
695
|
+
const title = titleReader.read();
|
|
696
|
+
lastTitleError = null;
|
|
697
|
+
return title !== null && title !== dashboardTitle ? title : undefined;
|
|
698
|
+
}
|
|
699
|
+
catch (err) {
|
|
700
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
701
|
+
if (message !== lastTitleError)
|
|
702
|
+
process.stderr.write(`session title: ${message}\n`);
|
|
703
|
+
lastTitleError = message;
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
672
707
|
const heartbeatTick = () => {
|
|
673
708
|
if (heartbeatSuppressed)
|
|
674
709
|
return;
|
|
710
|
+
const title = titleToReport();
|
|
675
711
|
void sessionStats.tick(async (stats) => {
|
|
676
|
-
const answer = await sendHeartbeat(options, deps, connectionNow(), stats);
|
|
712
|
+
const answer = await sendHeartbeat(options, deps, connectionNow(), stats, title);
|
|
713
|
+
if (answer.title !== null)
|
|
714
|
+
dashboardTitle = answer.title;
|
|
677
715
|
// DX-4418 - a person revoked this session's key: tell the session to stop, at once and only once, as an ordinary event
|
|
678
716
|
// line (the same delivery the silent-failure notice below uses). Its next dashboard call says the same.
|
|
679
717
|
if (answer.revocation !== null && !revocationAnnounced) {
|
package/dist/handlers.js
CHANGED
|
@@ -47,9 +47,11 @@ const PLAN_SESSIONS_BASE_PATH = "/api/plan-sessions";
|
|
|
47
47
|
* would end the bridge's stream.
|
|
48
48
|
*
|
|
49
49
|
* `title` (DX-2816) rides here, not on a header, because the AGENT — not this
|
|
50
|
-
* server —
|
|
50
|
+
* server — can read Claude's own session title at connect time (via a
|
|
51
51
|
* Desktop-side `get_session` tool no MCP server subprocess can call). Sent
|
|
52
52
|
* only when the caller supplied one; the server leaves an unset title alone.
|
|
53
|
+
* The event bridge keeps it current afterwards, from the transcript (DX-4461,
|
|
54
|
+
* `session-title.ts`).
|
|
53
55
|
*/
|
|
54
56
|
export async function planConnect(client, args) {
|
|
55
57
|
if (args.disconnect === true) {
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
87
87
|
import { z } from "zod";
|
|
88
88
|
import { DashboardHttpClient } from "./http-client.js";
|
|
89
89
|
import { planConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DANXBOT_API_METHODS, } from "./handlers.js";
|
|
90
|
+
import { boundedResultText } from "./result-limit.js";
|
|
90
91
|
import { bodyValueSchema, queryValueSchema } from "./types.js";
|
|
91
92
|
import { termsAcceptanceCallHalt, termsHaltFor } from "./terms-halt.js";
|
|
92
93
|
import { danxbotApiExamplesText } from "./danxbot-api-examples.js";
|
|
@@ -220,10 +221,14 @@ function resolveInitialCredential(dashboardUrl, sessionId) {
|
|
|
220
221
|
* MCP-server-reachable signal carries Claude's own session title — this
|
|
221
222
|
* SERVER genuinely cannot read it. The AGENT can, though (a Desktop-side
|
|
222
223
|
* `get_session` tool this server has no access to), and the agent is what
|
|
223
|
-
* calls `plan_connect` — so the title
|
|
224
|
-
*
|
|
225
|
-
* here.
|
|
226
|
-
*
|
|
224
|
+
* calls `plan_connect` — so the title travels as that tool's own `title`
|
|
225
|
+
* argument (see `handlers.ts#planConnect`) at connect time, not as a header
|
|
226
|
+
* stamped here. DX-4461: the session's TRANSCRIPT carries every rename as a
|
|
227
|
+
* `custom-title` record, so the event bridge (not this server) reads it from
|
|
228
|
+
* there and reports it on its heartbeat (`session-title.ts`) and a later rename needs no
|
|
229
|
+
* agent call. A session that never connects with a title keeps the
|
|
230
|
+
* dashboard's own placeholder (`session <id prefix>`) until its bridge reports
|
|
231
|
+
* one — an honest placeholder beats a guess.
|
|
227
232
|
*/
|
|
228
233
|
function readSessionConfig() {
|
|
229
234
|
const id = readEnvOptional("CLAUDE_CODE_SESSION_ID");
|
|
@@ -283,18 +288,28 @@ function accessReply(ready, note) {
|
|
|
283
288
|
if (ready.halt !== undefined)
|
|
284
289
|
return haltResult(ready.halt);
|
|
285
290
|
const answer = note ? { ...ready.answer, note } : ready.answer;
|
|
286
|
-
return ready.isError ? haltResult(JSON.stringify(answer
|
|
291
|
+
return ready.isError ? haltResult(JSON.stringify(answer)) : jsonResult(answer);
|
|
287
292
|
}
|
|
288
293
|
/** DX-4390 — a tool's dashboard call behind the sign-in halt (and the 401 sign-out). */
|
|
289
294
|
async function guardedJson(run) {
|
|
290
295
|
const guarded = await access.guard(run);
|
|
291
296
|
return "halt" in guarded ? haltResult(guarded.halt) : jsonResult(guarded.result);
|
|
292
297
|
}
|
|
298
|
+
/** DX-4400 — `guardedJson` for the read tools whose answer can be a whole document: bounded. */
|
|
299
|
+
async function guardedBoundedJson(run) {
|
|
300
|
+
const guarded = await access.guard(run);
|
|
301
|
+
return "halt" in guarded ? haltResult(guarded.halt) : boundedJsonResult(guarded.result);
|
|
302
|
+
}
|
|
293
303
|
function jsonResult(value) {
|
|
294
304
|
return {
|
|
295
|
-
|
|
305
|
+
// DX-4400: compact — nothing parses the indentation and it costs every call 16% to 55% more characters.
|
|
306
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
296
307
|
};
|
|
297
308
|
}
|
|
309
|
+
/** DX-4400 — a dashboard route's result, refused with the way to narrow it when over `MCP_MAX_RESULT_CHARS`. */
|
|
310
|
+
function boundedJsonResult(result) {
|
|
311
|
+
return { content: [{ type: "text", text: boundedResultText(result) }] };
|
|
312
|
+
}
|
|
298
313
|
/**
|
|
299
314
|
* DX-3238 — every tool on this server MUST reject an undeclared/mistyped
|
|
300
315
|
* argument instead of silently stripping it. `server.tool(name, description,
|
|
@@ -407,14 +422,14 @@ async (args) => {
|
|
|
407
422
|
const halt = termsHaltFor(result);
|
|
408
423
|
if (halt)
|
|
409
424
|
return haltResult(halt);
|
|
410
|
-
return
|
|
425
|
+
return boundedJsonResult(await listenerReminder.apply(result));
|
|
411
426
|
});
|
|
412
427
|
// ---------------- danxbot_api_list (DX-3465 A5, PLN-11 R-25) ----------------
|
|
413
428
|
strictTool("danxbot_api_list",
|
|
414
429
|
// DX-3528 — the list is filtered by the same permission check every request takes.
|
|
415
430
|
"Lists the dashboard API routes your credential may call: `[{method, path, summary, permission}]`, each " +
|
|
416
431
|
"`path` a route template (`/api/issues/:id/transition`), the form `danxbot_api_spec` takes. A route not " +
|
|
417
|
-
"listed is not callable by you.", {}, async () =>
|
|
432
|
+
"listed is not callable by you.", {}, async () => guardedBoundedJson(() => danxbotApiList(client)));
|
|
418
433
|
// ---------------- danxbot_api_spec (DX-3465, PLN-11 R-25) ----------------
|
|
419
434
|
strictTool("danxbot_api_spec",
|
|
420
435
|
// DX-3527 — the contract is generated from the route table.
|
|
@@ -423,7 +438,7 @@ strictTool("danxbot_api_spec",
|
|
|
423
438
|
'(e.g. `"POST /api/issues/:id/transition"`), never a real path with ids. An endpoint the dashboard does ' +
|
|
424
439
|
"not serve, or your credential may not call, is refused and named.", {
|
|
425
440
|
endpoints: z.array(z.string().min(1)).min(1),
|
|
426
|
-
}, async (args) =>
|
|
441
|
+
}, async (args) => guardedBoundedJson(() => danxbotApiSpec(client, args)));
|
|
427
442
|
// ---------------- attach_file (DX-3465 A4, checklist item 36140) ----------------
|
|
428
443
|
//
|
|
429
444
|
// The "25 MB" ceiling quoted below tracks the backend route's
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4400 — the most characters one `danxbot_api`, `danxbot_api_spec` or `danxbot_api_list` result may carry
|
|
3
|
+
* back to the agent. Every character is context the agent pays for on every later call, and Claude Code itself
|
|
4
|
+
* saves a larger result to a file with no hint how to narrow, after the cost is paid. A deliberate, narrow
|
|
5
|
+
* exception to PLAN-11 rule R-25 (verbatim forwarding): `plan_connect` and the lifecycle tools never pass here.
|
|
6
|
+
*/
|
|
7
|
+
export const MCP_MAX_RESULT_CHARS = 100_000;
|
|
8
|
+
/** A runtime result text, not a tool description: the DX-4318 text limit does not apply to it. */
|
|
9
|
+
const HOW_TO_NARROW = "Ask for less: `limit` and `offset` page a collection, `fields` names only the columns or relations you need, " +
|
|
10
|
+
"`filter` and `since` (epoch milliseconds) select rows. Then repeat the call.";
|
|
11
|
+
/**
|
|
12
|
+
* The text the agent receives for `result`: its compact JSON when that fits `MCP_MAX_RESULT_CHARS`, otherwise
|
|
13
|
+
* `{ok, status, body_omitted:{chars, limit, how_to_narrow}}`. Never truncates, never forwards a part. `ok`
|
|
14
|
+
* and `status` stay so a write that already committed is still reported as committed.
|
|
15
|
+
*/
|
|
16
|
+
export function boundedResultText(result) {
|
|
17
|
+
const text = JSON.stringify(result);
|
|
18
|
+
if (text.length <= MCP_MAX_RESULT_CHARS)
|
|
19
|
+
return text;
|
|
20
|
+
return JSON.stringify({
|
|
21
|
+
ok: result.ok,
|
|
22
|
+
status: result.status,
|
|
23
|
+
body_omitted: { chars: text.length, limit: MCP_MAX_RESULT_CHARS, how_to_narrow: HOW_TO_NARROW },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4461 - a session's live Claude title, read from its own transcript with no help from the agent.
|
|
3
|
+
*
|
|
4
|
+
* WHY THE TRANSCRIPT. The dashboard stores a plan session's title, and until now it arrived only as the `title`
|
|
5
|
+
* argument of `plan_connect`, so a session renamed afterwards showed a stale name until the agent happened to connect
|
|
6
|
+
* again. DX-2816 found no env var, hook input or MCP-reachable signal carrying the title, but Claude Code writes every
|
|
7
|
+
* title change into the session's transcript as a `{"type":"custom-title","customTitle":"..."}` line the moment the
|
|
8
|
+
* rename happens, even while the session is idle (observed on session 952f0739: 76 records of the old title, then the
|
|
9
|
+
* renamed one, no turn in between). The bridge already holds the transcript path (the plugin passes the hook input's
|
|
10
|
+
* `transcript_path` as `DANX_BRIDGE_TRANSCRIPT_PATH`, `session-stats.ts`), so it reads the last such record on each
|
|
11
|
+
* heartbeat and reports it when it differs from the title the dashboard answered with (`bridge.ts`); the dashboard
|
|
12
|
+
* stores it (`plan-sessions-routes.ts`).
|
|
13
|
+
*
|
|
14
|
+
* INCREMENTAL, NEVER A RE-READ. The reader remembers how far it has scanned (always a line boundary) and the title it
|
|
15
|
+
* found, so a heartbeat reads only the bytes appended since the last one. The first read of a long transcript is
|
|
16
|
+
* bounded to its last {@link TRANSCRIPT_TITLE_MAX_SCAN_BYTES}: Claude Code repeats the record on every turn, so the
|
|
17
|
+
* latest one is always near the end, and a transcript with none in that window has nothing to report.
|
|
18
|
+
*
|
|
19
|
+
* It reads synchronously on purpose: the bridge's heartbeat tick is synchronous up to its first request, and a read
|
|
20
|
+
* of a few appended kilobytes every 15 seconds costs less than the plumbing an async read would need.
|
|
21
|
+
*/
|
|
22
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
23
|
+
/**
|
|
24
|
+
* The longest title the dashboard accepts (`SESSION_TITLE_MAX_CHARS`, `src/issues/db/plan-sessions.ts`, counted in
|
|
25
|
+
* UTF-16 units). A longer one would 400 the whole heartbeat, which is liveness, so the reader cuts it first.
|
|
26
|
+
*/
|
|
27
|
+
export const SESSION_TITLE_MAX_CHARS = 200;
|
|
28
|
+
/** How much of a transcript's end the first read looks at. */
|
|
29
|
+
export const TRANSCRIPT_TITLE_MAX_SCAN_BYTES = 2 * 1024 * 1024;
|
|
30
|
+
/** The marker of a title record. A message quoting it carries escaped quotes, so it never matches. */
|
|
31
|
+
const TITLE_RECORD_MARKER = '"type":"custom-title"';
|
|
32
|
+
export class TranscriptTitleReader {
|
|
33
|
+
path;
|
|
34
|
+
/** Offset just past the last complete line already scanned; always 0 or a line boundary. */
|
|
35
|
+
scannedTo = 0;
|
|
36
|
+
title = null;
|
|
37
|
+
constructor(path) {
|
|
38
|
+
this.path = path;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The session's current title, or `null` when its transcript has none (yet). Throws on a transcript this cannot
|
|
42
|
+
* read, or a complete title record that is not JSON: the caller decides what a failed read costs.
|
|
43
|
+
*/
|
|
44
|
+
read() {
|
|
45
|
+
const fd = this.open();
|
|
46
|
+
if (fd === null)
|
|
47
|
+
return this.title;
|
|
48
|
+
try {
|
|
49
|
+
const size = fstatSync(fd).size;
|
|
50
|
+
if (size < this.scannedTo) {
|
|
51
|
+
// The file was rewritten shorter than what was already scanned; nothing remembered about it holds.
|
|
52
|
+
this.scannedTo = 0;
|
|
53
|
+
this.title = null;
|
|
54
|
+
}
|
|
55
|
+
if (size === this.scannedTo)
|
|
56
|
+
return this.title;
|
|
57
|
+
const start = Math.max(this.scannedTo, size - TRANSCRIPT_TITLE_MAX_SCAN_BYTES);
|
|
58
|
+
const buf = Buffer.alloc(size - start);
|
|
59
|
+
let filled = 0;
|
|
60
|
+
while (filled < buf.length) {
|
|
61
|
+
const n = readSync(fd, buf, filled, buf.length - filled, start + filled);
|
|
62
|
+
if (n === 0)
|
|
63
|
+
break;
|
|
64
|
+
filled += n;
|
|
65
|
+
}
|
|
66
|
+
const lastNewline = buf.subarray(0, filled).lastIndexOf(0x0a);
|
|
67
|
+
if (lastNewline === -1)
|
|
68
|
+
return this.title;
|
|
69
|
+
const lines = buf.subarray(0, lastNewline).toString("utf8").split("\n");
|
|
70
|
+
// A region that starts mid-file (the first read of a long transcript) begins inside a line.
|
|
71
|
+
if (start !== this.scannedTo)
|
|
72
|
+
lines.shift();
|
|
73
|
+
this.scannedTo = start + lastNewline + 1;
|
|
74
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
75
|
+
if (!lines[i].includes(TITLE_RECORD_MARKER))
|
|
76
|
+
continue;
|
|
77
|
+
this.title = parseTitleRecord(lines[i], this.path);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
return this.title;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
closeSync(fd);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The transcript's descriptor, or `null` when it does not exist yet (Claude Code creates it on the first turn). */
|
|
87
|
+
open() {
|
|
88
|
+
try {
|
|
89
|
+
return openSync(this.path, "r");
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err.code === "ENOENT")
|
|
93
|
+
return null;
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function parseTitleRecord(line, path) {
|
|
99
|
+
let record;
|
|
100
|
+
try {
|
|
101
|
+
record = JSON.parse(line);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
throw new Error(`a custom-title line in ${path} is not JSON: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
const customTitle = record?.customTitle;
|
|
107
|
+
if (typeof customTitle !== "string")
|
|
108
|
+
return null;
|
|
109
|
+
const trimmed = customTitle.trim();
|
|
110
|
+
return trimmed === "" ? null : boundTitle(trimmed);
|
|
111
|
+
}
|
|
112
|
+
/** At most {@link SESSION_TITLE_MAX_CHARS} UTF-16 units, cut on a code-point boundary. */
|
|
113
|
+
function boundTitle(title) {
|
|
114
|
+
if (title.length <= SESSION_TITLE_MAX_CHARS)
|
|
115
|
+
return title;
|
|
116
|
+
let out = "";
|
|
117
|
+
for (const point of title) {
|
|
118
|
+
if (out.length + point.length > SESSION_TITLE_MAX_CHARS)
|
|
119
|
+
break;
|
|
120
|
+
out += point;
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
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.228",
|
|
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",
|