@thehammer/danx-dashboard-mcp 0.1.220 → 0.1.222
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/index.js +61 -87
- package/dist/lifecycle.js +24 -30
- package/dist/listen.js +0 -45
- package/dist/text-limit.js +24 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -89,6 +89,7 @@ import { planConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DA
|
|
|
89
89
|
import { bodyValueSchema, queryValueSchema } from "./types.js";
|
|
90
90
|
import { termsAcceptanceCallHalt, termsHaltFor } from "./terms-halt.js";
|
|
91
91
|
import { danxbotApiExamplesText } from "./danxbot-api-examples.js";
|
|
92
|
+
import { assertWithinTextLimit } from "./text-limit.js";
|
|
92
93
|
import { registerLifecycleTools, applyLifecycleVisibility, readLifecycleUrlsFromEnv, } from "./lifecycle.js";
|
|
93
94
|
function readEnvOrDie(name) {
|
|
94
95
|
const v = process.env[name];
|
|
@@ -220,61 +221,39 @@ function readSessionConfig() {
|
|
|
220
221
|
return { id };
|
|
221
222
|
}
|
|
222
223
|
/**
|
|
223
|
-
* DX-3465 (PLN-11 R-25) — the
|
|
224
|
-
*
|
|
225
|
-
* server exposes, now that `danxbot_api` is the generic surface for
|
|
226
|
-
* everything that is not agent-specific (R-25). Read this before your first
|
|
227
|
-
* `danxbot_api` call.
|
|
224
|
+
* DX-3465 (PLN-11 R-25) — the conventions shared by every `danxbot_api` call,
|
|
225
|
+
* stated once here instead of on each tool.
|
|
228
226
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
227
|
+
* DX-4318 — this block is the ONE home of the stale-hash rule and of the
|
|
228
|
+
* writing standard (PLN-11 R-24): it is the only text every caller of these
|
|
229
|
+
* tools receives (operator sessions, their sub-agents, every dispatch
|
|
230
|
+
* profile). It was 3,916 characters and Claude Code delivered the first
|
|
231
|
+
* 2,048, so both rules were cut from every session. `assertWithinTextLimit`
|
|
232
|
+
* below refuses a block that would be cut again. Behaviour only, no rationale.
|
|
231
233
|
*/
|
|
232
234
|
const SERVER_INSTRUCTIONS = `\
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
Conventions for every \`danxbot_api\` call. Find a route with \`danxbot_api_list\` and its exact shape with
|
|
236
|
+
\`danxbot_api_spec\`.
|
|
235
237
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
- get GET <collection>/:id — one resource; \`fields\` opts into relations/heavy data the same way.
|
|
241
|
-
- create POST <collection> — the resource's own required fields; a 400 names any missing/invalid one.
|
|
242
|
-
- update PATCH/PUT <collection>/:id — send only the fields you are changing; most resources require the
|
|
243
|
-
concurrency token below when the edit touches contested text fields.
|
|
244
|
-
A card is the exception: PATCH /api/issues/<id>/edit.
|
|
245
|
-
- delete DELETE <collection>/:id — usually a soft-delete; the resource's own docs say so.
|
|
246
|
-
Don't know the route for what you need to do? Call \`danxbot_api_list()\` — it lists exactly the routes your
|
|
247
|
-
credential may call, one line each. Then call \`danxbot_api_spec({endpoints: ["METHOD /path/template"]})\` for the
|
|
248
|
-
EXACT current shape of the one(s) you picked, before calling them through \`danxbot_api\` — request/response
|
|
249
|
-
schema, required permission, and every documented error envelope. Never guess a field name or a route path.
|
|
238
|
+
RESOURCES. Most collections take GET (list) and POST (create); a single resource (\`<collection>/<id>\`) takes
|
|
239
|
+
GET, PATCH or PUT (send only the fields you change) and DELETE. A card is edited at
|
|
240
|
+
\`PATCH /api/issues/<id>/edit\`. A list or get answers scalars only: name each relation or heavy field you need in
|
|
241
|
+
the \`fields\` query tree. \`GET /api/resources/<resource>/fields\` lists the names.
|
|
250
242
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
243
|
+
STALE HASH. A write to contested text (a card's title, description or checklists, a plan record, a problem) sends
|
|
244
|
+
back the \`content_hash\` or \`base_hash\` you read from that resource. A stale hash is refused 409 with the current
|
|
245
|
+
hash and content: merge your change into that and retry with the fresh hash.
|
|
254
246
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
FIRST (from the same resource's own \`get\`/\`list\` response) and send back unchanged. A stale hash 409s with the
|
|
258
|
-
CURRENT hash and CURRENT content in the body — re-read, merge, retry with the fresh hash. Never retry blindly
|
|
259
|
-
with the same stale value, and never omit the hash hoping the write "just works" — omission is refused exactly
|
|
260
|
-
like a stale value on any route that requires one.
|
|
247
|
+
REFUSALS. A 4xx body is \`{error, ...}\`, and its other fields name the fix: \`offending_keys\`, \`failed_gate\`,
|
|
248
|
+
\`unresolved_items\`, \`did_you_mean\`. Read it before you retry.
|
|
261
249
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
you don't understand means re-checking the spec, not repeating the same call.
|
|
268
|
-
|
|
269
|
-
WRITING STANDARD — for any tool that writes prose onto a card, plan record, or problem (title, summary,
|
|
270
|
-
description, context, a problem's statement, a solution's body, ...). The reader is new to the subject and has
|
|
271
|
-
not read, and will not read, the code or docs: what you write IS their documentation. Define any term a
|
|
272
|
-
newcomer would not know where it first appears. Open with plain background — what the thing is, why it exists,
|
|
273
|
-
what is wrong or what the choice changes — before evidence or technical detail. Cite ids/SHAs/paths precisely
|
|
274
|
-
rather than describing them vaguely ("the migration file" → the actual filename). Be brief: the goal is less to
|
|
275
|
-
read, not more, not a transcript of your reasoning. A summary/title alone must let the reader decide without
|
|
276
|
-
opening the description.
|
|
250
|
+
WRITING STANDARD, for every title, summary, description, comment, problem, solution and plan record you write. The
|
|
251
|
+
reader is new to the subject and will not read the code or docs: what you write is their documentation. Open with
|
|
252
|
+
plain background (what the thing is, why it exists, what is wrong or what the choice changes) before evidence.
|
|
253
|
+
Define each term a newcomer would not know. Cite exact ids, SHAs and paths. Be brief: less to read, never a
|
|
254
|
+
transcript of your reasoning. A title or summary alone must let the reader decide.
|
|
277
255
|
`;
|
|
256
|
+
assertWithinTextLimit("the server instructions", SERVER_INSTRUCTIONS);
|
|
278
257
|
export const server = new McpServer({
|
|
279
258
|
name: "danx-dashboard-mcp",
|
|
280
259
|
version: "0.1.0",
|
|
@@ -309,6 +288,8 @@ function jsonResult(value) {
|
|
|
309
288
|
* set, exactly like the dashboard's own `parseFilterObject` refusal.
|
|
310
289
|
*/
|
|
311
290
|
function strictTool(name, description, shape, cb) {
|
|
291
|
+
// DX-4318 — the one registration path, so no tool can ship a description an agent would receive cut.
|
|
292
|
+
assertWithinTextLimit(`the "${name}" tool description`, description);
|
|
312
293
|
return server.registerTool(name, { description, inputSchema: z.object(shape).strict() }, cb);
|
|
313
294
|
}
|
|
314
295
|
// ---------------- lifecycle tool set (DX-3465 / PLN-11 R-25) ----------------
|
|
@@ -342,15 +323,11 @@ const boardField = {
|
|
|
342
323
|
// `danxbot_api({method:"GET", path:"/api/resources/<resource>/fields"})`
|
|
343
324
|
// instead; the route and its response shape are unchanged.
|
|
344
325
|
// ---------------- danxbot_api (DX-3465, PLN-11 R-25) ----------------
|
|
345
|
-
strictTool("danxbot_api",
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
"
|
|
350
|
-
"route attaches one. `board` overrides this dispatch's configured board for a board-scoped route only. " +
|
|
351
|
-
"See this server's own instructions (shown once, at connect) " +
|
|
352
|
-
"for the shared resource shape, the stale-hash rule, and how to read a refusal — don't know the route? call " +
|
|
353
|
-
"`danxbot_api_list` first. " +
|
|
326
|
+
strictTool("danxbot_api",
|
|
327
|
+
// DX-3997 — `reminders[]` also relays the dashboard's own listener reminder
|
|
328
|
+
// to a plan-connected session; the reminder's text explains itself.
|
|
329
|
+
"Call any dashboard API route your credential may reach. The answer is the route's JSON body: act on its " +
|
|
330
|
+
"`reminders[]` when present. " +
|
|
354
331
|
// DX-4015 — the ONE statement of which writes need a spec call first; the
|
|
355
332
|
// `work` profile's spec-first sentence defers to this list. A shown write
|
|
356
333
|
// is copied from its example (3 of 3 canary sessions did, with zero 4xx).
|
|
@@ -367,7 +344,7 @@ strictTool("danxbot_api", "Call any dashboard API route your credential may reac
|
|
|
367
344
|
.describe('Full path starting with "/api/" — never a route template.'),
|
|
368
345
|
// DX-3465 — a query value may be a scalar, a field TREE (a plain
|
|
369
346
|
// object, e.g. `{"description":true,"comments":{"limit":20}}` — see
|
|
370
|
-
// SERVER_INSTRUCTIONS
|
|
347
|
+
// SERVER_INSTRUCTIONS above and the OpenAPI spec's `fields` schema), or
|
|
371
348
|
// an array of scalars (e.g. `ids` on the batch-get route).
|
|
372
349
|
// `DashboardHttpClient#buildUrl` JSON-encodes a plain object but
|
|
373
350
|
// comma-joins an array, matching the two shapes the server actually
|
|
@@ -401,22 +378,18 @@ async (args) => {
|
|
|
401
378
|
return jsonResult(await listenerReminder.apply(result));
|
|
402
379
|
});
|
|
403
380
|
// ---------------- danxbot_api_list (DX-3465 A5, PLN-11 R-25) ----------------
|
|
404
|
-
strictTool("danxbot_api_list",
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"you can actually call). No arguments. Call this when you don't already know the route, then " +
|
|
410
|
-
"`danxbot_api_spec({endpoints})` for the exact contract of the one(s) you picked, then `danxbot_api` to " +
|
|
411
|
-
"call it.", {}, async () => jsonResult(await danxbotApiList(client)));
|
|
381
|
+
strictTool("danxbot_api_list",
|
|
382
|
+
// DX-3528 — the list is filtered by the same permission check every request takes.
|
|
383
|
+
"Lists the dashboard API routes your credential may call: `[{method, path, summary, permission}]`, each " +
|
|
384
|
+
"`path` a route template (`/api/issues/:id/transition`), the form `danxbot_api_spec` takes. A route not " +
|
|
385
|
+
"listed is not callable by you.", {}, async () => jsonResult(await danxbotApiList(client)));
|
|
412
386
|
// ---------------- danxbot_api_spec (DX-3465, PLN-11 R-25) ----------------
|
|
413
|
-
strictTool("danxbot_api_spec",
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
"
|
|
419
|
-
"endpoints to name? call `danxbot_api_list` first.", {
|
|
387
|
+
strictTool("danxbot_api_spec",
|
|
388
|
+
// DX-3527 — the contract is generated from the route table.
|
|
389
|
+
"The OpenAPI 3.1 contract for the routes you name: request and response shape, required permission and " +
|
|
390
|
+
'every documented error. `endpoints`: `"METHOD /path/template"` strings as `danxbot_api_list` reports them ' +
|
|
391
|
+
'(e.g. `"POST /api/issues/:id/transition"`), never a real path with ids. An endpoint the dashboard does ' +
|
|
392
|
+
"not serve, or your credential may not call, is refused and named.", {
|
|
420
393
|
endpoints: z.array(z.string().min(1)).min(1),
|
|
421
394
|
}, async (args) => jsonResult(await danxbotApiSpec(client, args)));
|
|
422
395
|
// ---------------- attach_file (DX-3465 A4, checklist item 36140) ----------------
|
|
@@ -429,18 +402,12 @@ strictTool("danxbot_api_spec", "The exact OpenAPI 3.1 contract (DX-3527) for one
|
|
|
429
402
|
// out plus a role-by-role grant, see `tool-defs.ts`'s `MCP_SERVER_OPT_IN_TOOLS`
|
|
430
403
|
// — a role needs local-file-upload only when it deals with files an agent
|
|
431
404
|
// might hold locally, e.g. the Slack recovery agent's CSV path).
|
|
432
|
-
strictTool("attach_file", "Attach a LOCAL file to a card
|
|
433
|
-
"
|
|
434
|
-
"
|
|
435
|
-
"
|
|
436
|
-
"
|
|
437
|
-
"
|
|
438
|
-
"(no partial S3 object or row). 25 MB decoded ceiling (413). Returns `{issue, attachment_id, s3_key}` — " +
|
|
439
|
-
"find the attachment in `issue.attachments` by `attachment_id` to read its permanent, non-expiring `url`. " +
|
|
440
|
-
"That url can be embedded in ANY markdown field (a problem's statement/context, a comment, a plan record, " +
|
|
441
|
-
"a solution body) as `` — the dashboard renders it inline as a thumbnail. Use whenever " +
|
|
442
|
-
"a screenshot or a local file judges something faster than prose, or replaces pasting a large CSV/log " +
|
|
443
|
-
"straight into a card field.", {
|
|
405
|
+
strictTool("attach_file", "Attach a LOCAL file to a card. This server reads and uploads the bytes, so they never pass through your " +
|
|
406
|
+
"context. The attachment mirrors to the card's linked Trello card and Slack thread. 25 MB limit (413). " +
|
|
407
|
+
"Returns `{issue, attachment_id, s3_key}`: the entry of `issue.attachments` with that `attachment_id` " +
|
|
408
|
+
"carries a permanent `url`: embed it in any markdown field as `` and it renders " +
|
|
409
|
+
"inline. Use it when a screenshot or file judges something faster than prose, or instead of pasting a " +
|
|
410
|
+
"large CSV or log into a card field.", {
|
|
444
411
|
card_id: z.string().min(1).describe("The card id to attach the file to."),
|
|
445
412
|
path: z
|
|
446
413
|
.string()
|
|
@@ -461,14 +428,21 @@ strictTool("attach_file", "Attach a LOCAL file to a card: `card_id` + `path` (an
|
|
|
461
428
|
// a different reason: it MAKES a plan rather than acting on one, so there is
|
|
462
429
|
// no existing plan for an id to name yet.
|
|
463
430
|
strictTool("plan_connect",
|
|
464
|
-
// DX-
|
|
465
|
-
|
|
431
|
+
// DX-4318 — states only what to do with the reply. What it used to add arrives where it is used: a rejected
|
|
432
|
+
// action's rule in the event line itself (`listen.ts`), the mantra in the reply's own `reminders[]` text.
|
|
433
|
+
"Connect THIS session to a plan; one call is enough to start. The reply carries `missedEvents` (operator " +
|
|
434
|
+
"events on the plan that no session had been told about, oldest first: read them first), " +
|
|
435
|
+
"`browserInstruction` (do it now), `briefing` (the plan's goals, rules, caveats, architecture section " +
|
|
436
|
+
"titles and a first page of open cards) and `listenerHealth` (when its `state` is not `healthy`, do its " +
|
|
437
|
+
"`nextStep`). A session is on at most one plan: connecting elsewhere moves it, and `movedFrom` names the " +
|
|
438
|
+
"plan it left. Afterwards a `danxbot_api` call to a `/api/plans/mine/...` route acts on this plan and " +
|
|
439
|
+
"takes no plan id.", {
|
|
466
440
|
plan_id: z.number().int().positive().describe("The plan id, from the plans list (`danxbot_api` `GET /api/plans`)."),
|
|
467
441
|
title: z
|
|
468
442
|
.string()
|
|
469
443
|
.min(1)
|
|
470
444
|
.optional()
|
|
471
|
-
.describe(
|
|
445
|
+
.describe('This session\'s own Claude title: `get_session({session_id:"self"}).title`, verbatim, never invented and never the plan\'s name. Omit to keep the stored title.'),
|
|
472
446
|
disconnect: z.boolean().optional().describe("true = leave `plan_id` (your current plan); no `title`."),
|
|
473
447
|
},
|
|
474
448
|
// DX-2862 — the connect is also the moment this session's event bridge learns
|
package/dist/lifecycle.js
CHANGED
|
@@ -181,10 +181,10 @@ export function registerLifecycleTools(strictTool) {
|
|
|
181
181
|
"cancelled = agent voluntarily abandons the dispatch (does NOT cancel the card — transition it first); " +
|
|
182
182
|
"critical_failure = environment broken in a way that affects every dispatch (MCP tools not loading, " +
|
|
183
183
|
"Bash unavailable, Claude auth missing) — halts the poller until a human clears it; " +
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"
|
|
187
|
-
"
|
|
184
|
+
// DX-4318 — the steps live in each profile that grants this ending (`work` and `failure-repair`,
|
|
185
|
+
// `required-profiles.yaml`), never repeated here.
|
|
186
|
+
"waiting_on_dependency = blocked on a card that must land on another board or repo: do your " +
|
|
187
|
+
"profile's steps for work that needs another repo or board's change first."),
|
|
188
188
|
summary: z
|
|
189
189
|
.string()
|
|
190
190
|
.describe("Brief summary of what happened. For failed, why the dispatch stopped, " +
|
|
@@ -213,11 +213,11 @@ export function registerLifecycleTools(strictTool) {
|
|
|
213
213
|
};
|
|
214
214
|
});
|
|
215
215
|
// ---------------- danxbot_slack_reply ----------------
|
|
216
|
-
const slackReply = strictTool("danxbot_slack_reply",
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"progress use danxbot_slack_post_update
|
|
216
|
+
const slackReply = strictTool("danxbot_slack_reply",
|
|
217
|
+
// DX-4253 — no ordering against another tool here: the Slack persona (`src/agent/persona.ts`) and the
|
|
218
|
+
// `slack-agent` profile own the order (reply, link, attach, complete).
|
|
219
|
+
"Post the final user-facing reply to the originating Slack thread, once per dispatch. Files in `files[]` " +
|
|
220
|
+
"post with the text as one thread message. For progress updates use danxbot_slack_post_update.", {
|
|
221
221
|
text: z.string().describe("The final reply text to post to the Slack thread."),
|
|
222
222
|
files: z
|
|
223
223
|
.array(z.string())
|
|
@@ -350,27 +350,21 @@ export function registerLifecycleTools(strictTool) {
|
|
|
350
350
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
351
351
|
});
|
|
352
352
|
// ---------------- quality_gate_complete (absorbs quality_gate_skip, DX-3394 problem 1682) ----------------
|
|
353
|
-
const qualityGateComplete = strictTool("quality_gate_complete",
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
|
|
368
|
-
"status='pass' releases the dispatch toward the next requested code gate or back to WORK to finalize; " +
|
|
369
|
-
"status='retry' is PLAN-phase only, refused here. status='skipped' — the gate is OBVIOUSLY " +
|
|
370
|
-
"UNNECESSARY for this diff (a single-line / docs-only change, or nothing in this gate's lane): use " +
|
|
371
|
-
`\`message\` as the mandatory justification (≥${QUALITY_GATE_SKIP_REASON_MIN_CHARS} chars, the ` +
|
|
372
|
-
"permanent accountability record a human will read) — never skip to dodge a real review. You never " +
|
|
373
|
-
"edit files and never transition the card yourself, on either phase.", {
|
|
353
|
+
const qualityGateComplete = strictTool("quality_gate_complete",
|
|
354
|
+
// DX-3741 — every gate runs as its own gate-reviewer dispatch, the only one that sees this tool. DX-4318 —
|
|
355
|
+
// what a status does, per phase; how to review (read-only, no transition) is the gate-reviewer profile's
|
|
356
|
+
// (`required-profiles.yaml`). The "3-pass cap" mirrors CODE_GATE_MAX_REVIEW_PASSES
|
|
357
|
+
// (`src/issues/quality-gates/registry.ts`), which this separately published package cannot import;
|
|
358
|
+
// `src/tokens/tool-defs.test.ts` compares the two.
|
|
359
|
+
"Sign off the quality gate you are reviewing. It is your dispatch's TERMINAL call: never follow it with " +
|
|
360
|
+
"danxbot_complete. PLAN gate: `pass` releases the card to the next gate or the work agent; `fail` blocks " +
|
|
361
|
+
"the card with `message` as the reason; `retry` means you edited the card into a sounder plan and want " +
|
|
362
|
+
"a fresh pass. CODE gate: report every finding in `findings[]`; `fail` returns the findings to the " +
|
|
363
|
+
"author, who fixes them and requests review again, and at the 3-pass cap blocks the card instead (sign " +
|
|
364
|
+
"on the findings alone; the dispatcher enforces the cap); `pass` releases toward the next code gate or " +
|
|
365
|
+
"the author's finalize; `retry` is refused. `skipped`: the gate obviously does not apply to this diff " +
|
|
366
|
+
"(one line, docs only, nothing in its lane), with `message` as the mandatory justification of at least " +
|
|
367
|
+
`${QUALITY_GATE_SKIP_REASON_MIN_CHARS} characters; never skip to dodge a real review.`, {
|
|
374
368
|
gate: z.string().describe("Registered gate name, e.g. 'plan-architecture' or 'code-quality'."),
|
|
375
369
|
status: z.enum(["pass", "fail", "retry", "skipped"]),
|
|
376
370
|
message: z
|
package/dist/listen.js
CHANGED
|
@@ -329,32 +329,6 @@ export function formatNudgeLine(event) {
|
|
|
329
329
|
"Work these cards; any you cannot work, update to its true state: open a problem, add depends_on or " +
|
|
330
330
|
"conflict_on, block it with the real reason, or rollback_pickup.");
|
|
331
331
|
}
|
|
332
|
-
/** Why a reminder cannot be read, or `null` when it can. */
|
|
333
|
-
export function invalidReminderReason(value) {
|
|
334
|
-
if (!isRecord(value))
|
|
335
|
-
return "the reminder is not an object";
|
|
336
|
-
if (value.kind !== "policy_reminder")
|
|
337
|
-
return "kind is not policy_reminder";
|
|
338
|
-
if (typeof value.text !== "string" || value.text.trim() === "")
|
|
339
|
-
return "text is not a non-empty string";
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* The label every reminder carries, so the session can never read one as the operator speaking
|
|
344
|
-
* (DX-2955: "labelled unmistakably as automated system reminders, never phrased as the operator").
|
|
345
|
-
*/
|
|
346
|
-
export const REMINDER_LABEL = "AUTOMATED SYSTEM REMINDER from the danxbot dashboard monitor (not from the operator):";
|
|
347
|
-
/**
|
|
348
|
-
* The text of a reminder: the label, then the reminder as the dashboard wrote it. Unlike an
|
|
349
|
-
* activity line the body keeps its line breaks, because a reminder with something off is the
|
|
350
|
-
* numbered checklist plus what is off. Throws, naming the fault, on a malformed reminder.
|
|
351
|
-
*/
|
|
352
|
-
export function formatReminderLine(event) {
|
|
353
|
-
const invalid = invalidReminderReason(event);
|
|
354
|
-
if (invalid !== null)
|
|
355
|
-
throw new Error(`malformed reminder event: ${invalid}`);
|
|
356
|
-
return `${LINE_PREFIX} ${REMINDER_LABEL}\n${event.text.trim()}`;
|
|
357
|
-
}
|
|
358
332
|
/**
|
|
359
333
|
* Incremental SSE parser. Comment lines (keep-alives, the connect marker) and
|
|
360
334
|
* field-less blocks produce no message, which is what keeps them out of the output.
|
|
@@ -495,25 +469,6 @@ export async function runListener(options, deps) {
|
|
|
495
469
|
deps.write({ type: "event", id: null, text, origin: null });
|
|
496
470
|
return null;
|
|
497
471
|
}
|
|
498
|
-
if (message.event === "reminder") {
|
|
499
|
-
// DX-2955 - like a nudge: no usable id, never replayed, never deduplicated, and
|
|
500
|
-
// `origin: null` so `bridge.ts` delivers it immediately instead of folding it
|
|
501
|
-
// into a digest (the generic null-origin exemption in `routeEvent`).
|
|
502
|
-
let parsed;
|
|
503
|
-
let invalid;
|
|
504
|
-
try {
|
|
505
|
-
parsed = JSON.parse(message.data);
|
|
506
|
-
invalid = invalidReminderReason(parsed);
|
|
507
|
-
}
|
|
508
|
-
catch (err) {
|
|
509
|
-
invalid = `not JSON: ${err.message}`;
|
|
510
|
-
}
|
|
511
|
-
const text = invalid === null
|
|
512
|
-
? formatReminderLine(parsed)
|
|
513
|
-
: `${LINE_PREFIX} could not read reminder (${invalid}): ${oneLine(message.data, 300)}`;
|
|
514
|
-
deps.write({ type: "event", id: null, text, origin: null });
|
|
515
|
-
return null;
|
|
516
|
-
}
|
|
517
472
|
if (message.event !== "activity")
|
|
518
473
|
return null;
|
|
519
474
|
// `Number(null)` is 0, so a frame with no id must never be read as event 0: that id
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4318 — Claude Code delivers at most 2,048 characters (string length, not
|
|
3
|
+
* bytes) of an MCP server's instructions and of each tool description, then
|
|
4
|
+
* appends `… [truncated]`; nothing fails. Measured 2026-10-03 on three texts
|
|
5
|
+
* of this server, each cut at index 2,048. The limit here keeps 48 characters
|
|
6
|
+
* of headroom under that cut.
|
|
7
|
+
*
|
|
8
|
+
* Whether Claude Code also cuts a parameter description is unverified: none
|
|
9
|
+
* is long enough to show it. The package test holds them to the same limit, so
|
|
10
|
+
* the question stays moot.
|
|
11
|
+
*/
|
|
12
|
+
export const MCP_TEXT_LIMIT_CHARS = 2000;
|
|
13
|
+
/**
|
|
14
|
+
* Refuses a text an agent would receive cut. Called where each text is
|
|
15
|
+
* registered, so an over-long one stops the module from loading: the package
|
|
16
|
+
* tests, the `tool-defs.json` generator and the publish boot check all fail
|
|
17
|
+
* on it.
|
|
18
|
+
*/
|
|
19
|
+
export function assertWithinTextLimit(label, text) {
|
|
20
|
+
if (text.length > MCP_TEXT_LIMIT_CHARS) {
|
|
21
|
+
throw new Error(`[danx-dashboard-mcp] ${label} is ${text.length} characters, over the ${MCP_TEXT_LIMIT_CHARS}-character limit: ` +
|
|
22
|
+
`Claude Code would cut it and the agent would never see the rest. Shorten it.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
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.222",
|
|
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",
|