@thehammer/danx-dashboard-mcp 0.1.156 → 0.1.157

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,17 +1,19 @@
1
1
  /**
2
2
  * DX-3426 — the shared recursive FIELD-TREE shape.
3
3
  *
4
- * The dashboard's single-card and batch card reads (`issue_get` today;
5
- * `issue_list` / `plan_get` in later cards, per DX-3426's own follow-up scope)
6
- * take a nested JSON tree instead of a flat field-GROUP list: each key names a
7
- * field or relation of the resource at that level, and each value is either
8
- * `true` (the field, or a relation's child with its default fields), a
9
- * non-negative integer (a cursor-paged relation's own page ARGUMENT — e.g.
10
- * `{"comments": {"limit": 10, "before": 812}}`, where `limit`/`before` are
11
- * themselves plain integer values inside the nested object), or another
4
+ * The dashboard's single-card, batch card and plan reads (`issue_get`,
5
+ * `issue_list`, `plan_get` — DX-3428) take a nested JSON tree instead of a
6
+ * flat field-GROUP list: each key names a field or relation of the resource
7
+ * at that level, and each value is either `true` (the field, or a relation's
8
+ * child with its default fields), a non-negative integer (a cursor-paged
9
+ * relation's own `limit` argument), an opaque cursor STRING (a paged
10
+ * relation's `before` — DX-3428: not every relation's cursor is a plain
11
+ * integer, e.g. a plan event's visibility-scanned row id), a non-empty
12
+ * STRING or ARRAY OF STRINGS (a relation's own declared, non-paging argument
13
+ * — e.g. `records`' `kind`, `events`' `kinds`/`origin`/`writer`), or another
12
14
  * field tree (a relation's child with a NAMED field selection). Example:
13
15
  *
14
- * {"description": true, "ac": true, "problems": {"solutions": {"steps": true}, "decisions": true}, "comments": {"limit": 10}}
16
+ * {"description": true, "ac": true, "problems": {"solutions": {"steps": true}, "decisions": true}, "comments": {"limit": 10}, "records": {"kind": "goal"}}
15
17
  *
16
18
  * One schema, one module — every tool that accepts a field tree imports THIS
17
19
  * export rather than redeclaring the shape, so the recursion is defined once
@@ -28,8 +30,15 @@
28
30
  import { z } from "zod";
29
31
  /**
30
32
  * The shared schema. A record whose values are `true`, a non-negative
31
- * integer, or the same schema recursively — generic on purpose (no
32
- * issue-specific field names baked in here) so `issue_list` / `plan_get` can
33
- * reuse it unchanged in their own later cards.
33
+ * integer, a non-empty string, a non-empty array of non-empty strings, or
34
+ * the same schema recursively — generic on purpose (no issue/plan-specific
35
+ * field names baked in here) so every tool that reads a resource tree
36
+ * reuses it unchanged.
34
37
  */
35
- export const fieldTreeSchema = z.lazy(() => z.record(z.union([z.literal(true), z.number().int().nonnegative(), fieldTreeSchema])));
38
+ export const fieldTreeSchema = z.lazy(() => z.record(z.union([
39
+ z.literal(true),
40
+ z.number().int().nonnegative(),
41
+ z.string().min(1),
42
+ z.array(z.string().min(1)).min(1),
43
+ fieldTreeSchema,
44
+ ])));
package/dist/handlers.js CHANGED
@@ -65,21 +65,6 @@ export const ISSUE_BATCH_GET_MAX = 100;
65
65
  * `__tests__/handlers.test.ts` asserts the two agree.
66
66
  */
67
67
  export const LIST_PAGE_MAX_LIMIT = 1000;
68
- /**
69
- * `plan_get`'s `cards_limit` when omitted — mirrors the server's
70
- * `src/issues/list-page.ts#PLAN_GET_CARDS_DEFAULT_LIMIT`;
71
- * `__tests__/handlers.test.ts` asserts the two agree.
72
- */
73
- export const PLAN_GET_CARDS_DEFAULT_LIMIT = 200;
74
- /**
75
- * DX-3027 — `plan_get`'s `events_limit` default/max — mirrors the server's
76
- * `src/issues/plans-routes.ts#PLAN_GET_EVENTS_DEFAULT_LIMIT` /
77
- * `PLAN_GET_EVENTS_MAX_LIMIT`. Deliberately smaller than `LIST_PAGE_MAX_LIMIT`
78
- * above — an event page is read far more often, and a wide page defeats the
79
- * point of a cursor. `__tests__/handlers.test.ts` asserts the two agree.
80
- */
81
- export const PLAN_GET_EVENTS_DEFAULT_LIMIT = 50;
82
- export const PLAN_GET_EVENTS_MAX_LIMIT = 200;
83
68
  /**
84
69
  * Fetch one card via `GET /api/issues/:id`, or many via
85
70
  * `GET /api/issues/batch?ids=...` (DX-2727) — a real batch read that keeps
@@ -1024,32 +1009,17 @@ export async function issueAttach(client, args, deps = {}) {
1024
1009
  const PLANS_BASE_PATH = "/api/plans";
1025
1010
  const PLAN_SESSIONS_BASE_PATH = "/api/plan-sessions";
1026
1011
  /**
1027
- * DX-2727 — the plan_get field-group taxonomy: the ONE copy in this package
1028
- * (the `plan_get` zod enum in `index.ts` reads this const, and the type is
1029
- * derived from it). The published package cannot import server source at
1030
- * runtime, so it stays a literal; `__tests__/handlers.test.ts` imports the
1031
- * server's `src/issues/plan-field-groups.ts#PLAN_FIELD_GROUPS` and asserts
1032
- * the two are equal, so drift fails a test rather than a live call.
1033
- * `records` is the union of every kind; `records:<kind>` narrows to one.
1034
- */
1035
- export const PLAN_FIELD_GROUPS = [
1036
- "cards",
1037
- "records",
1038
- "records:goal",
1039
- "records:rule",
1040
- "records:caveat",
1041
- "architecture",
1042
- "sessions",
1043
- "notes",
1044
- "events",
1045
- ];
1046
- /**
1047
- * DX-3027 — every kind `plan_get`'s `events_kinds` filter accepts. The ONE
1048
- * copy in this package (the `plan_get` zod enum in `index.ts` reads this
1049
- * const), duplicated from the server's `src/issues/db/plan-events.ts#PLAN_EVENT_KINDS`
1050
- * because the published package cannot import server source at runtime.
1051
- * `__tests__/handlers.test.ts` asserts the two are equal, so drift fails a
1052
- * test rather than a live call.
1012
+ * DX-3428 — every kind the plan resource's `events` relation accepts as a
1013
+ * `kinds` filter (`{"events": {"kinds": [...]}}`). The ONE copy in this
1014
+ * package (the `resource_fields`-describable `events` relation validates the
1015
+ * same set server-side), duplicated from the server's
1016
+ * `src/issues/db/plan-events.ts#PLAN_EVENT_KINDS` because the published
1017
+ * package cannot import server source at runtime — kept here purely for a
1018
+ * CONSUMER'S typed convenience (`PlanEventKind`); the MCP tool schema itself
1019
+ * no longer enumerates it (DX-3428 moved `plan_get` onto the generic,
1020
+ * resource-agnostic `fieldTreeSchema` every field-tree tool shares — see
1021
+ * `field-tree.ts`). `__tests__/handlers.test.ts` asserts the two are equal,
1022
+ * so drift fails a test rather than a live call.
1053
1023
  */
1054
1024
  export const PLAN_EVENT_KINDS = [
1055
1025
  "comment_added",
@@ -1087,10 +1057,11 @@ export const PLAN_EVENT_KINDS = [
1087
1057
  "plan_auto_sign_off_changed",
1088
1058
  ];
1089
1059
  /**
1090
- * DX-3027 — every origin `plan_get`'s `events_origin` filter accepts.
1060
+ * DX-3428 — every origin the `events` relation's `origin` filter accepts.
1091
1061
  * Mirrors the server's `src/issues/db/plan-events.ts#PlanEventOrigin`
1092
1062
  * (itself `ActivityOrigin`) — same drift protection as `PLAN_EVENT_KINDS`
1093
- * above.
1063
+ * above, and likewise kept only for a consumer's typed convenience (not read
1064
+ * by the MCP tool schema any more).
1094
1065
  */
1095
1066
  export const PLAN_EVENT_ORIGINS = ["operator", "agent", "machine"];
1096
1067
  /**
@@ -1113,31 +1084,19 @@ export async function planList(client, args = {}) {
1113
1084
  }
1114
1085
  /**
1115
1086
  * One plan — its cheap scalars by default, or opt into its cards, its goals
1116
- * + rules + caveats, its architecture sections, and the sessions working on
1117
- * it via `fields` (DX-2727). One call rather than several, which is what
1118
- * keeps this tool surface small enough to be worth an agent's context even
1119
- * once every part of a plan is opt-in rather than always-on.
1087
+ * + rules + caveats, its architecture sections, its sessions and its event
1088
+ * ledger via a field TREE (DX-3428, replacing the retired flat field-GROUP
1089
+ * list). One call rather than several, which is what keeps this tool
1090
+ * surface small enough to be worth an agent's context even once every part
1091
+ * of a plan is opt-in rather than always-on.
1120
1092
  */
1121
1093
  export async function planGet(client, args = {}) {
1122
1094
  const query = {};
1123
- if (args.fields !== undefined && args.fields.length > 0) {
1124
- query.fields = args.fields.join(",");
1125
- }
1126
- if (args.cards_offset !== undefined)
1127
- query.cards_offset = args.cards_offset;
1128
- if (args.cards_limit !== undefined)
1129
- query.cards_limit = args.cards_limit;
1130
- if (args.events_limit !== undefined)
1131
- query.events_limit = args.events_limit;
1132
- if (args.events_before !== undefined)
1133
- query.events_before = args.events_before;
1134
- if (args.events_kinds !== undefined && args.events_kinds.length > 0) {
1135
- query.events_kinds = args.events_kinds.join(",");
1095
+ // DX-3428 — the tree travels as ONE JSON-encoded query param, exactly like
1096
+ // `issue_get`/`issue_list`'s own `fields` (see `issueGet` above).
1097
+ if (args.fields !== undefined && Object.keys(args.fields).length > 0) {
1098
+ query.fields = JSON.stringify(args.fields);
1136
1099
  }
1137
- if (args.events_origin !== undefined)
1138
- query.events_origin = args.events_origin;
1139
- if (args.events_writer !== undefined)
1140
- query.events_writer = args.events_writer;
1141
1100
  return client.request({
1142
1101
  method: "GET",
1143
1102
  path: args.plan_id === undefined ? "/mine" : `/${args.plan_id}/full`,
package/dist/index.js CHANGED
@@ -108,7 +108,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
108
108
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
109
109
  import { z } from "zod";
110
110
  import { DashboardHttpClient } from "./http-client.js";
111
- import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_EVENT_KINDS, PLAN_EVENT_ORIGINS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, PLAN_GET_EVENTS_DEFAULT_LIMIT, PLAN_GET_EVENTS_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, qualityGateInstruction, resourceFields, } from "./handlers.js";
111
+ import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, qualityGateInstruction, resourceFields, } from "./handlers.js";
112
112
  import { PRIORITY_TIER_WORDS } from "./priority.js";
113
113
  import { fieldTreeSchema } from "./field-tree.js";
114
114
  function readEnvOrDie(name) {
@@ -915,62 +915,29 @@ strictTool("plan_list", "List every plan, and learn which plan THIS session is c
915
915
  .optional()
916
916
  .describe("Filter to one computed status: awaiting-session, planning, building, awaiting-sign-off, complete. Omit for every plan."),
917
917
  }, async (args) => jsonResult(await planList(client, args)));
918
- strictTool("plan_get", "Read a plan. Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, status, session, sessionListenerAttached, available_field_groups}` — no cards, records, or architecture body. `plan` carries `{id, ref, name, createdAt}`; `ref` is the plan's short reference (`PLN-<id>`) — cite that, not the bare id. `status` is computed fresh on every read, never stored — see `plan_list` for the four values and what each means. Pass `fields` to opt into: `cards` (member cards, paged via `cards_offset`/`cards_limit` — see their own field descriptions — in stable card-reference order: board prefix then card number, pages never repeat/skip unless membership changes between reads; response carries `cards_total`), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (one kind, cheaper), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan), `events` (the plan's durable event ledger — every human action and bridge message, paged/filtered via `events_limit`/`events_before`/`events_kinds`/`events_origin`/`events_writer` — see their own field descriptions; response carries `events: {items: [{id, at, kind, writer, origin, originSessionId, targetSessionId, cardId, cardTitle, boardId, detail}], next_cursor}`, `next_cursor` null on the last page; an event on a card whose board you cannot read is left out, plan-level events are always visible). `session`/`sessionListenerAttached` and `available_field_groups` ride every response regardless. `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the plugin's event bridge starts; still `false` after that while connected means the plan's card events are not reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
918
+ strictTool("plan_get",
919
+ // DX-3428 — the retired flat field-GROUP list (`cards`, `records:<kind>`,
920
+ // `cards_offset`/`cards_limit`, `events_*`) is replaced by a JSON field
921
+ // TREE against the plan resource — the SAME `fieldTreeSchema` `issue_get`/
922
+ // `issue_list` already take (`./field-tree.ts`). Every former query param
923
+ // now lives INSIDE the tree as a relation argument.
924
+ "Read a plan. Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{id, ref, name, created_at, signed_off_at, signed_off_by, auto_sign_off, boards, card_count, bucket_counts, status, session, sessionListenerAttached}` — no cards, records, or architecture body. `ref` is the plan's short reference (`PLN-<id>`) — cite that, not the bare id. `status` is computed fresh on every read, never stored — see `plan_list` for the four values and what each means. Pass a `fields` tree to opt into: `cards` (member cards — the ISSUE resource itself, so any of `issue_get`'s own fields/relations may be nested under it, e.g. `{\"cards\": {\"title\": true, \"status\": true}}`; cursor-paged via `{\"limit\": N, \"before\": \"<cursor>\"}`, response carries a sibling `cards_page: {limit, total, next_cursor}`), `records` (every goal+rule+caveat) or `{\"records\": {\"kind\": \"goal\"}}` / `{\"kind\": [\"goal\",\"rule\"]}` (narrow to one or more kinds, cheaper), `architecture_sections` (`[{id, plan_id, content_hash, title, content, sort_order, created_at, updated_at}]`), `sessions` (every session connected to the plan), `notes` (the latest milestone-timeline page), `events` (the plan's durable event ledger — every human action and bridge message; cursor-paged via `{\"limit\": N, \"before\": \"<cursor>\"}` plus the filter args `{\"kinds\": [...], \"origin\": \"...\", \"writer\": \"...\"}`; response carries `events` rows plus a sibling `events_page: {limit, total, next_cursor}`; `next_cursor` null on the last page; an event on a card whose board you cannot read is left out, plan-level events are always visible). Call `resource_fields({resource:\"plan\"})` for the full, current list of what a tree may name — never guess a name; unknown → 400 `unknown_field`. `session`/`sessionListenerAttached` ride every response regardless (not part of the tree — they describe YOUR session, not the plan). `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the plugin's event bridge starts; still `false` after that while connected means the plan's card events are not reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `content_hash`/`contentHash` back as `base_hash`.", {
919
925
  plan_id: z
920
926
  .number()
921
927
  .int()
922
928
  .positive()
923
929
  .optional()
924
930
  .describe("A plan id from `plan_list`. Omit to read the plan this session is connected to."),
925
- fields: z
926
- .array(z.enum(PLAN_FIELD_GROUPS))
927
- .optional()
928
- .describe("Opt-in field-GROUPS: cards, records (every kind) or records:goal/records:rule/records:caveat (one kind), architecture, sessions, notes, events. Absent/empty = cheap scalars only (plan, boards, cardCount, bucketCounts) plus session state."),
929
- cards_offset: z
930
- .number()
931
- .int()
932
- .nonnegative()
933
- .optional()
934
- .describe("Where the `cards` page starts (default 0). Requires `fields:[\"cards\"]`. Page with cards_offset while cards_offset + cards.length < cards_total."),
935
- cards_limit: z
936
- .number()
937
- .int()
938
- .positive()
939
- .max(LIST_PAGE_MAX_LIMIT)
940
- .optional()
941
- .describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields:[\"cards\"]`."),
942
- events_limit: z
943
- .number()
944
- .int()
945
- .positive()
946
- .max(PLAN_GET_EVENTS_MAX_LIMIT)
947
- .optional()
948
- .describe("how many ledger events one page holds, 1.." + PLAN_GET_EVENTS_MAX_LIMIT + " (default " + PLAN_GET_EVENTS_DEFAULT_LIMIT + "). Requires `fields` to include `events`."),
949
- events_before: z
950
- .string()
951
- .min(1)
952
- .optional()
953
- .describe("an opaque cursor from a previous page's `next_cursor`. Omit for the newest page. Requires `fields` to include `events`."),
954
- events_kinds: z
955
- .array(z.enum(PLAN_EVENT_KINDS))
956
- .optional()
957
- .describe("only these event kinds. Omit for every kind. Requires `fields` to include `events`."),
958
- events_origin: z
959
- .enum(PLAN_EVENT_ORIGINS)
960
- .optional()
961
- .describe("only events with this origin. Omit for every origin. Requires `fields` to include `events`."),
962
- events_writer: z
963
- .string()
964
- .min(1)
931
+ fields: fieldTreeSchema
965
932
  .optional()
966
- .describe("only events with this exact writer name. Omit for every writer. Requires `fields` to include `events`."),
933
+ .describe("A field tree (see tool description); absent/empty = cheap scalars only. `resource_fields({resource:\"plan\"})` names every valid key."),
967
934
  }, async (args) => jsonResult(await planGet(client, args)));
968
935
  strictTool("plan_create", "Create a new, empty plan. Global — not board-scoped. Adds no cards, records, or architecture sections, and does not connect any session (call `plan_connect` separately). Returns `{plan: {id, ref, name, createdAt}}` — `ref` is the short reference (`PLN-<id>`). Use `plan.id` with `plan_connect` to start working on it, or `plan_get({plan_id})` to browse.", {
969
936
  name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
970
937
  }, async (args) => jsonResult(await planCreate(client, args)));
971
938
  strictTool("plan_connect",
972
939
  // DX-2735: trimmed with the problem tools to stay inside the work-profile budget.
973
- "Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth}` — `browserInstruction` is a server-built action to take NOW: it names the plan's URL and tells you to open it (in-app browser if you have one, else default), keep that tab open for the whole session without navigating it away, and use a different tab for your own browsing — the operator's tab for following and talking to you. Returned on every connect, including a re-connect, so it doubles as the post-context-loss reminder. `briefing` is every goal/rule/caveat (ref+body), every architecture section (id+title), the plan's own ref/name/status, a first page of open cards (id/type/status/title/openProblemCount/assignedAgent) with a `morePagesHint` when more exist, and the closed-card count — usually replacing the `plan_get({fields:[...]})` + card batch-read a fresh session used to need. A session is on AT MOST ONE plan: connecting elsewhere MOVES it, and `movedFrom: {id, name} | null` says which plan it left (null = no plan, or already this one). Binds only your OWN session, resolved from the session id this server forwards; afterwards every plan WRITE tool acts on this plan and takes no plan id. `listenerHealth` reports the event bridge's state — `null` (no session), or `{attached, state: \"unattached\"|\"credential_mismatch\"|\"healthy\", nextStep}` naming a concrete fix per unhealthy state (same shape `plan_list`/`plan_get` report). Once healthy, every comment, answer, problem added and block/unblock on this plan's cards reaches the session unpolled, relayed by the plugin's event bridge as a line like `[DX-8 \"Title\" repo:board] newms87 answered \"<statement>\": chose \"Pause E2E\"`. Pass `title` (call `get_session({session_id:\"self\"})` first and forward its `title` verbatim) so the dashboard shows the same name Claude does — this server cannot read it itself.", {
940
+ "Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth}` — `browserInstruction` is a server-built action to take NOW: it names the plan's URL and tells you to open it (in-app browser if you have one, else default), keep that tab open for the whole session without navigating it away, and use a different tab for your own browsing — the operator's tab for following and talking to you. Returned on every connect, including a re-connect, so it doubles as the post-context-loss reminder. `briefing` is every goal/rule/caveat (ref+body), every architecture section (id+title), the plan's own ref/name/status, a first page of open cards (id/type/status/title/openProblemCount/assignedAgent) with a `morePagesHint` when more exist, and the closed-card count — usually replacing the `plan_get({fields: {...}})` + card batch-read a fresh session used to need. A session is on AT MOST ONE plan: connecting elsewhere MOVES it, and `movedFrom: {id, name} | null` says which plan it left (null = no plan, or already this one). Binds only your OWN session, resolved from the session id this server forwards; afterwards every plan WRITE tool acts on this plan and takes no plan id. `listenerHealth` reports the event bridge's state — `null` (no session), or `{attached, state: \"unattached\"|\"credential_mismatch\"|\"healthy\", nextStep}` naming a concrete fix per unhealthy state (same shape `plan_list`/`plan_get` report). Once healthy, every comment, answer, problem added and block/unblock on this plan's cards reaches the session unpolled, relayed by the plugin's event bridge as a line like `[DX-8 \"Title\" repo:board] newms87 answered \"<statement>\": chose \"Pause E2E\"`. Pass `title` (call `get_session({session_id:\"self\"})` first and forward its `title` verbatim) so the dashboard shows the same name Claude does — this server cannot read it itself.", {
974
941
  plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
975
942
  title: z
976
943
  .string()
@@ -994,7 +961,7 @@ async (args) => {
994
961
  }),
995
962
  });
996
963
  });
997
- strictTool("plan_add_record", "Add a goal, rule or caveat to your connected plan (see `kind`'s own values below for what each means — never progress/status/session notes, those are card comments). `body` is one plain statement, at most 250 characters (400 if longer); detail goes in markdown `context`. Allocates a permanent reference (`G-1`, `R-4`, `CAV-12`). Takes no plan id; not connected → `plan_connect` first. Returns the created record plus `records_count` (that kind's live count, not the whole list) — read the list with `plan_get({fields:[\"records:<kind>\"]})`.", {
964
+ strictTool("plan_add_record", "Add a goal, rule or caveat to your connected plan (see `kind`'s own values below for what each means — never progress/status/session notes, those are card comments). `body` is one plain statement, at most 250 characters (400 if longer); detail goes in markdown `context`. Allocates a permanent reference (`G-1`, `R-4`, `CAV-12`). Takes no plan id; not connected → `plan_connect` first. Returns the created record plus `records_count` (that kind's live count, not the whole list) — read the list with `plan_get({fields: {records: {kind: \"<kind>\"}}})`.", {
998
965
  kind: z.enum(["goal", "rule", "caveat"]).describe("goal = outcome, rule = constraint, caveat = architecture trade-off."),
999
966
  body: z.string().min(1).describe("One plain statement, at most 250 characters. Details go in `context`."),
1000
967
  context: z.string().optional().describe("Markdown detail behind the statement: evidence, history, examples."),
@@ -1016,7 +983,7 @@ strictTool("plan_delete_record", 'Soft-delete a goal/rule/caveat; its reference
1016
983
  record_id: z.number().int().positive().describe("The record id to delete."),
1017
984
  content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
1018
985
  }, async (args) => jsonResult(await planDeleteRecord(client, args)));
1019
- strictTool("plan_add_note", "Write a milestone note to a plan's timeline — a MILESTONE, not a log: use for a card (or group) finishing, an important decision, or a meaningful goal/rule/caveat/architecture change; routine progress stays a card comment. Terse: `title` at most 60 characters, `body` at most 250 (400 if too long, naming the limit). Links resolve on read into their target (a card's title, a record's ref+body, a section's title); an unknown card, unparseable/foreign record ref, or unknown/foreign section id is refused 400 naming which one. A card link needn't be a plan member; a record/section link must belong to THIS plan. `author` is stamped server-side. Takes an explicit `plan_id` (like `plan_remove_card`/`plan_rename`) so a dispatched worker with no plan connection can still write. Unknown plan → 404. Returns the new note plus `notes_count` (the plan's total live count, not the latest page) — read the timeline with `plan_get({fields:[\"notes\"]})`.", {
986
+ strictTool("plan_add_note", "Write a milestone note to a plan's timeline — a MILESTONE, not a log: use for a card (or group) finishing, an important decision, or a meaningful goal/rule/caveat/architecture change; routine progress stays a card comment. Terse: `title` at most 60 characters, `body` at most 250 (400 if too long, naming the limit). Links resolve on read into their target (a card's title, a record's ref+body, a section's title); an unknown card, unparseable/foreign record ref, or unknown/foreign section id is refused 400 naming which one. A card link needn't be a plan member; a record/section link must belong to THIS plan. `author` is stamped server-side. Takes an explicit `plan_id` (like `plan_remove_card`/`plan_rename`) so a dispatched worker with no plan connection can still write. Unknown plan → 404. Returns the new note plus `notes_count` (the plan's total live count, not the latest page) — read the timeline with `plan_get({fields: {notes: true}})`.", {
1020
987
  plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
1021
988
  title: z.string().min(1).describe("At most 60 characters."),
1022
989
  body: z.string().min(1).describe("The wrap-up, at most 250 characters."),
@@ -1042,7 +1009,7 @@ strictTool("plan_delete_note", 'Soft-delete a plan note. `content_hash` must be
1042
1009
  note_id: z.number().int().positive().describe("The note id to delete."),
1043
1010
  content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
1044
1011
  }, async (args) => jsonResult(await planDeleteNote(client, args)));
1045
- strictTool("plan_add_card", "Add an existing card, from ANY board, to the plan this session is connected to. Idempotent (re-adding is a no-op; a card may sit in several plans). Adds MEMBERSHIP only, never edits the card. Takes no plan id — resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns `{card_id, member: true, cards_count}` (the plan's total member-card count, not the full list — a plan can hold hundreds; read it with `plan_get({fields:[\"cards\"]})`, paged).", {
1012
+ strictTool("plan_add_card", "Add an existing card, from ANY board, to the plan this session is connected to. Idempotent (re-adding is a no-op; a card may sit in several plans). Adds MEMBERSHIP only, never edits the card. Takes no plan id — resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns `{card_id, member: true, cards_count}` (the plan's total member-card count, not the full list — a plan can hold hundreds; read it with `plan_get({fields: {cards: true}})`, cursor-paged).", {
1046
1013
  card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
1047
1014
  }, async (args) => jsonResult(await planAddCard(client, args)));
1048
1015
  strictTool("plan_remove_card", "Remove a card from a plan — sibling of `plan_add_card`, any board. Idempotent (removing a non-member is a no-op). Removes MEMBERSHIP only — never touches the card itself or its membership in other plans. Unlike `plan_add_card`, takes an EXPLICIT `plan_id` — you may remove from any plan you can name. Unknown plan → 404. Returns `{card_id, member: false, cards_count}` (see `plan_add_card`).", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.156",
3
+ "version": "0.1.157",
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",