@omnicoreos/planka-mcp 0.2.0 → 0.3.0

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.
Files changed (195) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +26 -0
  3. package/.claude-plugin/plugin.json +45 -0
  4. package/.codex-plugin/mcp.json +16 -0
  5. package/.codex-plugin/plugin.json +27 -0
  6. package/.mcp.json +17 -0
  7. package/CHANGELOG.md +510 -0
  8. package/README.es.md +294 -55
  9. package/README.md +293 -55
  10. package/dist/cli/init.d.ts +101 -0
  11. package/dist/cli/init.d.ts.map +1 -0
  12. package/dist/cli/init.js +481 -0
  13. package/dist/cli/init.js.map +1 -0
  14. package/dist/client.d.ts +32 -4
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +106 -32
  17. package/dist/client.js.map +1 -1
  18. package/dist/config/policy.d.ts +82 -0
  19. package/dist/config/policy.d.ts.map +1 -0
  20. package/dist/config/policy.js +226 -0
  21. package/dist/config/policy.js.map +1 -0
  22. package/dist/errors.d.ts +5 -1
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +62 -5
  25. package/dist/errors.js.map +1 -1
  26. package/dist/identity.generated.d.ts +2 -1
  27. package/dist/identity.generated.d.ts.map +1 -1
  28. package/dist/identity.generated.js +2 -1
  29. package/dist/identity.generated.js.map +1 -1
  30. package/dist/index.js +85 -10
  31. package/dist/index.js.map +1 -1
  32. package/dist/instructions.d.ts +21 -0
  33. package/dist/instructions.d.ts.map +1 -0
  34. package/dist/instructions.js +37 -0
  35. package/dist/instructions.js.map +1 -0
  36. package/dist/operations/actions.d.ts +654 -0
  37. package/dist/operations/actions.d.ts.map +1 -0
  38. package/dist/operations/actions.js +154 -0
  39. package/dist/operations/actions.js.map +1 -0
  40. package/dist/operations/archive.d.ts +28 -0
  41. package/dist/operations/archive.d.ts.map +1 -0
  42. package/dist/operations/archive.js +74 -0
  43. package/dist/operations/archive.js.map +1 -0
  44. package/dist/operations/attachments.d.ts +1 -1
  45. package/dist/operations/attachments.d.ts.map +1 -1
  46. package/dist/operations/attachments.js +3 -1
  47. package/dist/operations/attachments.js.map +1 -1
  48. package/dist/operations/board-id.d.ts +1 -1
  49. package/dist/operations/board-id.d.ts.map +1 -1
  50. package/dist/operations/board-id.js +13 -7
  51. package/dist/operations/board-id.js.map +1 -1
  52. package/dist/operations/boards.d.ts +96 -19
  53. package/dist/operations/boards.d.ts.map +1 -1
  54. package/dist/operations/boards.js +377 -93
  55. package/dist/operations/boards.js.map +1 -1
  56. package/dist/operations/card-brief.d.ts +91 -0
  57. package/dist/operations/card-brief.d.ts.map +1 -0
  58. package/dist/operations/card-brief.js +79 -0
  59. package/dist/operations/card-brief.js.map +1 -0
  60. package/dist/operations/cards.d.ts +34 -9
  61. package/dist/operations/cards.d.ts.map +1 -1
  62. package/dist/operations/cards.js +60 -14
  63. package/dist/operations/cards.js.map +1 -1
  64. package/dist/operations/comments.d.ts +61 -4
  65. package/dist/operations/comments.d.ts.map +1 -1
  66. package/dist/operations/comments.js +91 -8
  67. package/dist/operations/comments.js.map +1 -1
  68. package/dist/operations/duplicate.d.ts +16 -0
  69. package/dist/operations/duplicate.d.ts.map +1 -0
  70. package/dist/operations/duplicate.js +43 -0
  71. package/dist/operations/duplicate.js.map +1 -0
  72. package/dist/operations/labels.d.ts +1 -1
  73. package/dist/operations/labels.d.ts.map +1 -1
  74. package/dist/operations/labels.js +7 -4
  75. package/dist/operations/labels.js.map +1 -1
  76. package/dist/operations/lists.d.ts +63 -1
  77. package/dist/operations/lists.d.ts.map +1 -1
  78. package/dist/operations/lists.js +97 -2
  79. package/dist/operations/lists.js.map +1 -1
  80. package/dist/operations/members.d.ts +39 -0
  81. package/dist/operations/members.d.ts.map +1 -0
  82. package/dist/operations/members.js +107 -0
  83. package/dist/operations/members.js.map +1 -0
  84. package/dist/operations/projects.d.ts +16 -0
  85. package/dist/operations/projects.d.ts.map +1 -1
  86. package/dist/operations/projects.js +54 -9
  87. package/dist/operations/projects.js.map +1 -1
  88. package/dist/operations/tasks.d.ts +1 -1
  89. package/dist/operations/tasks.d.ts.map +1 -1
  90. package/dist/operations/tasks.js +5 -3
  91. package/dist/operations/tasks.js.map +1 -1
  92. package/dist/operations/users.d.ts +123 -0
  93. package/dist/operations/users.d.ts.map +1 -0
  94. package/dist/operations/users.js +180 -0
  95. package/dist/operations/users.js.map +1 -0
  96. package/dist/operations/verify.d.ts +84 -0
  97. package/dist/operations/verify.d.ts.map +1 -0
  98. package/dist/operations/verify.js +124 -0
  99. package/dist/operations/verify.js.map +1 -0
  100. package/dist/prompts.d.ts +48 -0
  101. package/dist/prompts.d.ts.map +1 -0
  102. package/dist/prompts.js +155 -0
  103. package/dist/prompts.js.map +1 -0
  104. package/dist/resources.d.ts +38 -0
  105. package/dist/resources.d.ts.map +1 -0
  106. package/dist/resources.js +127 -0
  107. package/dist/resources.js.map +1 -0
  108. package/dist/schemas/entities.d.ts +115 -24
  109. package/dist/schemas/entities.d.ts.map +1 -1
  110. package/dist/schemas/entities.js +48 -0
  111. package/dist/schemas/entities.js.map +1 -1
  112. package/dist/schemas/requests.d.ts +121 -46
  113. package/dist/schemas/requests.d.ts.map +1 -1
  114. package/dist/schemas/requests.js +57 -12
  115. package/dist/schemas/requests.js.map +1 -1
  116. package/dist/schemas/responses.d.ts +541 -186
  117. package/dist/schemas/responses.d.ts.map +1 -1
  118. package/dist/schemas/responses.js +13 -2
  119. package/dist/schemas/responses.js.map +1 -1
  120. package/dist/tools/activity.d.ts +150 -0
  121. package/dist/tools/activity.d.ts.map +1 -0
  122. package/dist/tools/activity.js +198 -0
  123. package/dist/tools/activity.js.map +1 -0
  124. package/dist/tools/annotations.d.ts +52 -0
  125. package/dist/tools/annotations.d.ts.map +1 -0
  126. package/dist/tools/annotations.js +214 -0
  127. package/dist/tools/annotations.js.map +1 -0
  128. package/dist/tools/attachments.d.ts +28 -4
  129. package/dist/tools/attachments.d.ts.map +1 -1
  130. package/dist/tools/attachments.js +53 -34
  131. package/dist/tools/attachments.js.map +1 -1
  132. package/dist/tools/card-ops.d.ts +232 -0
  133. package/dist/tools/card-ops.d.ts.map +1 -0
  134. package/dist/tools/card-ops.js +333 -0
  135. package/dist/tools/card-ops.js.map +1 -0
  136. package/dist/tools/cards.d.ts +90 -8
  137. package/dist/tools/cards.d.ts.map +1 -1
  138. package/dist/tools/cards.js +411 -128
  139. package/dist/tools/cards.js.map +1 -1
  140. package/dist/tools/comments.d.ts +226 -22
  141. package/dist/tools/comments.d.ts.map +1 -1
  142. package/dist/tools/comments.js +163 -134
  143. package/dist/tools/comments.js.map +1 -1
  144. package/dist/tools/dispatch.d.ts +47 -0
  145. package/dist/tools/dispatch.d.ts.map +1 -0
  146. package/dist/tools/dispatch.js +63 -0
  147. package/dist/tools/dispatch.js.map +1 -0
  148. package/dist/tools/guard.d.ts +9 -0
  149. package/dist/tools/guard.d.ts.map +1 -0
  150. package/dist/tools/guard.js +20 -0
  151. package/dist/tools/guard.js.map +1 -0
  152. package/dist/tools/index.d.ts +748 -450
  153. package/dist/tools/index.d.ts.map +1 -1
  154. package/dist/tools/index.js +136 -17
  155. package/dist/tools/index.js.map +1 -1
  156. package/dist/tools/labels.d.ts +213 -18
  157. package/dist/tools/labels.d.ts.map +1 -1
  158. package/dist/tools/labels.js +218 -203
  159. package/dist/tools/labels.js.map +1 -1
  160. package/dist/tools/lists.d.ts +222 -15
  161. package/dist/tools/lists.d.ts.map +1 -1
  162. package/dist/tools/lists.js +175 -156
  163. package/dist/tools/lists.js.map +1 -1
  164. package/dist/tools/members.d.ts +128 -0
  165. package/dist/tools/members.d.ts.map +1 -0
  166. package/dist/tools/members.js +150 -0
  167. package/dist/tools/members.js.map +1 -0
  168. package/dist/tools/navigation.d.ts +22 -2
  169. package/dist/tools/navigation.d.ts.map +1 -1
  170. package/dist/tools/navigation.js +60 -15
  171. package/dist/tools/navigation.js.map +1 -1
  172. package/dist/tools/queries.d.ts +196 -166
  173. package/dist/tools/queries.d.ts.map +1 -1
  174. package/dist/tools/queries.js +125 -155
  175. package/dist/tools/queries.js.map +1 -1
  176. package/dist/tools/tasks.d.ts +26 -6
  177. package/dist/tools/tasks.d.ts.map +1 -1
  178. package/dist/tools/tasks.js +110 -55
  179. package/dist/tools/tasks.js.map +1 -1
  180. package/dist/tools/users.d.ts +130 -0
  181. package/dist/tools/users.d.ts.map +1 -0
  182. package/dist/tools/users.js +165 -0
  183. package/dist/tools/users.js.map +1 -0
  184. package/docs/planka-2x-gotchas.md +121 -5
  185. package/docs/tools.md +771 -187
  186. package/docs/troubleshooting.md +137 -5
  187. package/hooks/hooks.json +15 -0
  188. package/hooks/preflight.mjs +100 -0
  189. package/package.json +6 -1
  190. package/scripts/setup.sh +8 -26
  191. package/scripts/sync-identity.mjs +55 -1
  192. package/server.json +87 -6
  193. package/tests/smoke/planka-smoke.mjs +512 -72
  194. package/workflow/skills/planka-close-card/SKILL.md +18 -5
  195. package/workflow/skills/planka-orchestrator/SKILL.md +36 -7
package/docs/tools.md CHANGED
@@ -1,14 +1,41 @@
1
1
  # Tools reference
2
2
 
3
- The server exposes exactly 24 tools. Tool results are MCP text content containing
4
- formatted JSON. On an expected Planka error, the result has `isError: true` and a
5
- human-readable message instead of a false success.
3
+ The server exposes exactly 41 tools: 34 in active use plus 7 deprecated ones kept
4
+ as aliases (see [Deprecated aliases](#deprecated-aliases); `PLANKA_HIDE_DEPRECATED`
5
+ takes them out of `tools/list`). Tool results are MCP text content containing
6
+ JSON (minified — the indentation was pure token cost). On an expected Planka
7
+ error, the result has `isError: true` and a human-readable message instead of a
8
+ false success.
9
+
10
+ ## Every write is verified
11
+
12
+ A `200` from Planka proves the request was accepted, not that the change is in
13
+ the database. So every write re-reads the entity and reports what LANDED, never
14
+ what was asked for. The response fields:
15
+
16
+ | Field | Meaning |
17
+ |---|---|
18
+ | `verified: true` | A re-read confirmed the change. This is the only real success. |
19
+ | `verified: false` + `isError` | The re-read contradicted the write, or the write could not be checked. Do not assume the write failed: read before retrying. |
20
+ | `verificationError` | The re-read itself failed. The write may or may not have landed. |
21
+ | `verificationUnavailable` | Verification was structurally impossible (a task or attachment whose card is unknown). The message names the parameter — usually `cardId` — that would have made the check possible. |
22
+
23
+ **`success` mirrors `verified`, and nothing else.** "I could not check" is not a
24
+ success: a result that carried `success: true` next to `verified: false` and
25
+ `"delete not confirmed"` was read by models as done. Every unverified write now
26
+ answers `success: false` with `isError`, and says what to pass to get it
27
+ verified.
28
+ | `partialFailure` | A follow-up step failed (`planka_create_card` only). The card id is always present anyway. |
29
+ | `notLanded` | The fields, or the list, that the re-read says did not change. |
30
+
31
+ Verification costs one extra read per write — a card read for cards, tasks,
32
+ comments and attachments, and a board read for labels and lists.
6
33
 
7
34
  ## Before your first call
8
35
 
9
36
  - All Planka IDs are strings, even when they contain only digits.
10
- - Discover IDs with `planka_get_structure`, `planka_list_lists`, or
11
- `planka_get_board`.
37
+ - Discover IDs with `planka_board_summary` (columns and labels of one board) or
38
+ `planka_get_structure` (projects and boards).
12
39
  - Reading a whole board is the expensive call. When you only need one column,
13
40
  one label, or the shape of the board, use a [scoped read](#scoped-reads).
14
41
  - Dates use ISO 8601, for example `2026-09-01T17:00:00.000Z`.
@@ -17,6 +44,59 @@ human-readable message instead of a false success.
17
44
  - Destructive tools really delete data. Ask for confirmation in your agent prompt
18
45
  when that matters.
19
46
 
47
+ ## Server instructions and annotations
48
+
49
+ Two things travel with `tools/list` and the `initialize` handshake, so a client
50
+ gets them without installing anything.
51
+
52
+ **Server instructions.** The `initialize` result carries an `instructions`
53
+ string (~1.8 kB). Claude Code injects it into the session system prompt and
54
+ Codex CLI reads it alongside the tool list. It holds the guidance that is shared
55
+ by many tools and therefore does not belong in any single description: open a
56
+ session with `planka_board_summary` and never `planka_get_board`, IDs are
57
+ strings of digits taken from `planka_get_structure` or `planka_board_summary`,
58
+ read every comment on a card before acting on it, reads return digests with
59
+ `total`/`returned`/`hasMore`, writes report `verified` and `finalLabelIds`, and
60
+ Planka answers `404` where it means `403`. The MCP spec asks that instructions
61
+ not duplicate tool descriptions, which is why the descriptions below are short.
62
+ Source: `src/instructions.ts`, budget enforced at 1500-2500 characters by
63
+ `tests/unit/instructions.test.ts`.
64
+
65
+ **Tool annotations.** Every tool publishes a `title` and all four behavioural
66
+ hints, spelled out rather than left to the spec defaults — which are pessimistic
67
+ (`readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`,
68
+ `openWorldHint: true`) and would make `planka_get_comments` look as dangerous as
69
+ `planka_delete_card`. `openWorldHint` is `false` everywhere: the domain is one
70
+ configured Planka instance, not the open web.
71
+
72
+ | Group | `readOnlyHint` | `destructiveHint` | `idempotentHint` |
73
+ |---|---|---|---|
74
+ | Reads (`get_*`, `list_*`, `find_cards`, `board_summary`) | `true` | `false` | `true` |
75
+ | Additive writes (`create_card`, `create_tasks`, `create_comment`, `create_label`, `create_list`, `add_attachment`) | `false` | `false` | `false` |
76
+ | Converging writes (`move_card`, `update_task`, `update_label`, `update_list`, `update_comment`) | `false` | `false` | `true` |
77
+ | Data-losing writes (`update_card`, `set_card_labels`, `set_card_members`, `archive_card`, `move_list_cards`, every `delete_*`) | `false` | `true` | `true` |
78
+ | Deprecated multi-action aliases (`manage_labels`, `manage_lists`, `manage_comment`) | `false` | `true` | `false` |
79
+
80
+ **Anthropic `_meta`.** Claude Code does not gate permissions on the standard
81
+ hints; it reads two `_meta` keys instead, and the server ships both:
82
+
83
+ - `anthropic/requiresUserInteraction: true` forces the permission prompt even
84
+ under an `allow` rule — set on eleven tools: `planka_delete_card`,
85
+ `planka_delete_task`, `planka_delete_attachment`, `planka_delete_label`,
86
+ `planka_delete_list`, `planka_delete_comment`, `planka_archive_card`,
87
+ `planka_move_list_cards`, and the deprecated `planka_manage_comment`,
88
+ `planka_manage_lists` and `planka_manage_labels`. The three aliases are marked
89
+ whole because one of their actions deletes: a list takes its cards with it, a
90
+ label disappears from every card that carried it. `archive_card` and
91
+ `move_list_cards` are there because both move cards a human will look for
92
+ where they used to be.
93
+ - `anthropic/maxResultSizeChars: 4500000` on `planka_view_attachment`, so a
94
+ base64 image up to 3 MB is not clipped by the default per-tool ceiling.
95
+
96
+ The map lives in `src/tools/annotations.ts`. A tool with no entry there fails
97
+ `tests/unit/annotations.test.ts`, and the same file pins the order of
98
+ `tools/list` so clients that cache it see a stable payload.
99
+
20
100
  ## Navigation
21
101
 
22
102
  ### `planka_get_structure`
@@ -36,58 +116,99 @@ Or restrict it to one project:
36
116
  }
37
117
  ```
38
118
 
119
+ | Field | Default | Notes |
120
+ |---|---|---|
121
+ | `projectId` | none | An id that is not visible errors and lists the ones that are |
122
+ | `withLists` | `true` | Include each board's columns |
123
+
124
+ Planka has no per-board list endpoint: the columns only exist inside a full
125
+ board read, so `withLists: true` costs **one board read per board**.
126
+ `withLists: false` answers projects and boards from the single
127
+ `GET /api/projects` — 203 chars and 1 request against 696 chars and 4 requests
128
+ on the reference instance. Use `planka_board_summary` when you want the columns
129
+ of one board: it brings their card counts too.
130
+
39
131
  ### `planka_get_board`
40
132
 
41
- Reads one board with its labels, lists, cards, and optional checklist counts.
133
+ > **DEPRECATED — use `planka_board_summary` + `planka_find_cards`.**
134
+ > It still works, and it is still the most expensive read of the set.
135
+
136
+ Reads one board with its labels, lists and cards.
42
137
 
43
138
  ```json
44
139
  {
45
140
  "boardId": "2234567890123456789",
46
- "includeTaskCounts": true
141
+ "includeTaskCounts": true,
142
+ "limit": 50
47
143
  }
48
144
  ```
49
145
 
50
- Set `includeTaskCounts` to `false` for a smaller response. Labels include their
51
- raw `color` string; reads intentionally accept colors added by newer Planka versions.
52
-
53
- ## Scoped reads
54
-
55
- `planka_get_board` returns the whole board: every card, every description, every
56
- label link. That is the right call when you really need all of it, and an
57
- expensive one when you do not. The four tools in this section answer narrower
58
- questions and return proportionally less text.
59
-
60
- Measured against one real board holding 100 cards, in characters of tool
61
- response:
62
-
63
- | Call | Response size | Compared to `planka_get_board` |
146
+ | Field | Default | Notes |
64
147
  |---|---|---|
65
- | `planka_get_board` | 44,449 | baseline |
66
- | `planka_list_lists` | 1,147 | 39x smaller |
67
- | `planka_board_summary` | 2,737 | 16x smaller |
68
- | `planka_find_cards` filtered by label | 2,421 | 18x smaller |
69
- | `planka_find_cards` filtered by text | 795 | 56x smaller |
148
+ | `includeTaskCounts` | `true` | Checklist counts per card |
149
+ | `limit` | `50` | Cards across the WHOLE board, maximum `200` |
150
+
151
+ It hands out cards in board order until `limit` runs out and then reports
152
+ `totalCards`, `returnedCards` and `truncated: true` — it no longer grows without
153
+ a ceiling. Every response carries `excludesArchived: true`: the board read has
154
+ no cards from the archive and trash lists (see
155
+ [planka-2x-gotchas.md](planka-2x-gotchas.md)).
70
156
 
71
- The gap widens as the board grows, because only `planka_get_board` grows with
72
- the number of cards.
157
+ ## Scoped reads
73
158
 
74
- Which one to reach for:
159
+ Three tools, one principle: **a small digest by default, the detail through
160
+ parameters**.
75
161
 
76
162
  | Question | Tool |
77
163
  |---|---|
78
- | What columns does this board have, and how full is each one? | `planka_list_lists` |
79
- | What is on this board and what should I pick up first? | `planka_board_summary` |
80
- | What is in this one column? | `planka_list_cards` |
81
- | Which cards carry this label, or mention this text? | `planka_find_cards` |
82
- | I need every card with every field, in one object | `planka_get_board` |
83
-
84
- All four report how much they clipped (`total`, `returned`, `hasMore`,
85
- `nextOffset`), so a partial answer is never mistaken for a complete one.
164
+ | What is on this board — columns, counts, labels? | `planka_board_summary` |
165
+ | What is in this one column? | `planka_find_cards` with `listId` |
166
+ | Which cards carry this label, mention this text, belong to this person? | `planka_find_cards` |
167
+ | I need every field of every card, in one object | `planka_get_board` (deprecated) |
168
+
169
+ `planka_list_lists` and `planka_list_cards` are **deprecated aliases** kept for
170
+ existing callers: `planka_list_lists` is `planka_board_summary` with
171
+ `cardsFrom: []`, and `planka_list_cards` is `planka_find_cards` with `listId`.
172
+ Same implementation, same response — and together they were the two most
173
+ expensive schemas in `tools/list`, which is why `PLANKA_HIDE_DEPRECATED` now
174
+ drops them too.
175
+
176
+ ### Measured, against the real reference board
177
+
178
+ 185 cards, 10 columns, 23 labels. `chars` is the tool response as serialized;
179
+ `reqs` is how many requests to Planka it took.
180
+
181
+ | Call | before | after | Δ chars |
182
+ |---|---|---|---|
183
+ | `board_summary` — session opener | 2,045 chars / 1 req | 2,037 / 1 | −0 % |
184
+ | `board_summary` columns only (= `list_lists`) | 782 / 1 | 1,947 / 1 | +149 % |
185
+ | One column of 15 cards (default `limit` 25) | 2,572 / 2 | 3,366 / 2 | +31 % |
186
+ | One column of 159 cards (default `limit` 25) | 4,272 / **5** | 5,834 / **2** | +37 % |
187
+ | One column of 159 cards, `limit: 200` | 27,044 / **5** | 36,230 / **2** | +34 % |
188
+ | `find_cards` by label (29 hits, `limit` 25) | 6,085 / 1 | 6,758 / 1 | +11 % |
189
+ | `find_cards` by text | 1,724 / 1 | 1,986 / 1 | +15 % |
190
+ | `get_board` | 56,112 / 1 | **17,215** / 1 | **−69 %** |
191
+ | `get_structure` with columns | 696 / 4 | 696 / 4 | = |
192
+ | `get_structure` `withLists: false` | — | **203 / 1** | — |
193
+
194
+ Read it honestly: the card digest got **richer**, not smaller. Each card now
195
+ also carries `commentsTotal`, `updatedAt` and its members — around 40 chars each
196
+ — which is what stops the follow-up call that used to be needed to find out
197
+ whether a card had comments or had moved since the last session. What got
198
+ cheaper is everything else: the whole-board dump (−69 %), the structure without
199
+ columns (−71 %), and the number of requests behind a column read (5 → 2, and
200
+ 1 with `withLabels: false`).
201
+
202
+ `planka_list_lists` grew because it is now `planka_board_summary`'s payload: it
203
+ also returns the board's labels **with their IDs**, which is exactly what
204
+ `planka_set_card_labels` needs and what used to send callers to the deprecated
205
+ `planka_get_board`.
86
206
 
87
- ### `planka_list_lists`
207
+ ### `planka_board_summary`
88
208
 
89
- Lists the columns of a board with the number of cards in each one, and no cards
90
- at all. Use it to see the shape of a board before deciding which column to open.
209
+ One call that describes a board well enough to start working: every column with
210
+ its card count, every label with its ID, and — when asked — the cards of the
211
+ column(s) named in `cardsFrom`.
91
212
 
92
213
  ```json
93
214
  {
@@ -95,140 +216,118 @@ at all. Use it to see the shape of a board before deciding which column to open.
95
216
  }
96
217
  ```
97
218
 
98
- `planka_get_structure` returns the same column names but not the counts.
99
- Archive and trash lists are excluded, because they are not board columns.
100
-
101
- ### `planka_board_summary`
102
-
103
- One call that describes a board well enough to start working: every column with
104
- its card count, every label, and the cards sitting in the column or columns that
105
- are waiting on a decision. Cards already carrying the unblocking label are sorted
106
- first and marked `highlighted: true`.
219
+ That answers columns and labels and **no cards at all**. Ask for cards by naming
220
+ the columns:
107
221
 
108
222
  ```json
109
223
  {
110
224
  "boardId": "2234567890123456789",
111
225
  "cardsFrom": [
112
- "blocked"
226
+ "blocked",
227
+ "needs review"
113
228
  ],
114
- "highlightLabel": "ready",
229
+ "highlightLabel": "approved",
115
230
  "limit": 50
116
231
  }
117
232
  ```
118
233
 
119
- Every field except `boardId` is optional, and the defaults are opinionated:
120
-
121
- | Field | Default | What the default does |
234
+ | Field | Default | What it does |
122
235
  |---|---|---|
123
- | `cardsFrom` | `["decision", "decide", "blocked", "bloquead"]` | Picks the columns whose names contain one of these fragments |
124
- | `highlightLabel` | `"decidido"` | Marks cards carrying a label with that exact name |
236
+ | `cardsFrom` | `PLANKA_SUMMARY_DECISION_LISTS`, else none | List IDs, or column-name fragments matched case- and accent-insensitively. `[]` means columns only |
237
+ | `highlightLabel` | `PLANKA_SUMMARY_HIGHLIGHT_LABEL`, else none | Cards carrying that label sort first and get `highlighted: true` |
125
238
  | `limit` | `50` | Caps returned cards; maximum `200` |
126
239
 
127
- Those defaults come from the board layout this server was written against, which
128
- is why one of them is Spanish. They are matching hints, not a requirement: pass
129
- your own values and the tool behaves exactly the same on an English board.
240
+ **No board vocabulary is hardcoded in the server any more.** The defaults come
241
+ from the environment, so a deployment describes its own board:
130
242
 
131
- ```json
132
- {
133
- "boardId": "2234567890123456789",
134
- "cardsFrom": [
135
- "blocked",
136
- "needs review"
137
- ],
138
- "highlightLabel": "approved"
139
- }
243
+ ```bash
244
+ PLANKA_SUMMARY_DECISION_LISTS="decision,probalo,miralo"
245
+ PLANKA_SUMMARY_HIGHLIGHT_LABEL="decidido"
140
246
  ```
141
247
 
142
- `cardsFrom` entries match either an exact list ID or a fragment of a column
143
- name, ignoring case and accents. `highlightLabel` must match a label name
144
- exactly, again ignoring case and accents. The response echoes the columns it
145
- actually selected in `cardsFrom` and the label it used in `highlightLabel`, so a
146
- hint that matched nothing is visible instead of silent.
248
+ A `cardsFrom` entry that matches no column, or a `highlightLabel` that is not on
249
+ the board, comes back in `warnings` **naming the columns or labels that do
250
+ exist** — never as a silently empty result. The response also carries
251
+ `excludesArchived: true`.
147
252
 
148
- Card payload is deliberately minimal: `id`, `name`, `list`, `labels`. Follow up
149
- with `planka_get_card` for one card rather than `planka_get_board` for all of
150
- them.
253
+ ### `planka_find_cards`
151
254
 
152
- ### `planka_list_cards`
255
+ The one read over a board's cards. At least one filter is required
256
+ (`listId`, `listName`, `query`, `labelName`, `labelIds` or `userIds`); an
257
+ unfiltered search would just be a slower `planka_get_board`.
153
258
 
154
- Reads the cards of one column: `id`, `name`, labels, checklist counts, and due
155
- date. Descriptions are off by default because they are the expensive field.
259
+ One column, the cheap path:
156
260
 
157
261
  ```json
158
262
  {
159
- "listId": "3234567890123456789",
160
- "limit": 25,
161
- "offset": 0
263
+ "listId": "3234567890123456789"
162
264
  }
163
265
  ```
164
266
 
165
- Filter the column by label name, or ask for description previews:
267
+ `GET /api/lists/:id` returns **every card of that column in one response**, with
268
+ no cursor to follow: `total` is the real size of the column and the answer
269
+ carries `truncated: false`. `boardId` is not needed — the list names its own
270
+ board.
271
+
272
+ Search a board:
166
273
 
167
274
  ```json
168
275
  {
169
- "listId": "3234567890123456789",
170
- "labelFilter": "ready",
171
- "withDescription": true,
172
- "limit": 50
276
+ "boardId": "2234567890123456789",
277
+ "query": "export timeout",
278
+ "labelName": "ready",
279
+ "descriptionChars": 200
173
280
  }
174
281
  ```
175
282
 
176
283
  | Field | Default | Notes |
177
284
  |---|---|---|
178
- | `withLabels` | `true` | Resolving label names costs one extra board read |
179
- | `withDescription` | `false` | Adds a 200-character preview per card |
180
- | `labelFilter` | none | Exact label name, case- and accent-insensitive; implies `withLabels` |
285
+ | `boardId` | `PLANKA_DEFAULT_BOARD_ID` | Ignored when `listId` is given |
286
+ | `listId` | none | One column, one request, never truncated |
287
+ | `listName` | none | Column-name fragment; costs a board read |
288
+ | `query` | none | Name **and** description. Several words are ANDed; a leading `/` makes the rest a case-insensitive regular expression |
289
+ | `labelName` | none | Exact label name, case- and accent-insensitive. `labelFilter` is still accepted as an alias, but is no longer announced in the schema |
290
+ | `labelIds` | none | Label IDs, OR between them |
291
+ | `userIds` | none | Members **or** task assignees, OR between them |
292
+ | `withLabels` | `true` | `false` skips the extra board read on the `listId` path |
293
+ | `detail` | `"digest"` | `"full"` returns every field of the card, description included |
294
+ | `descriptionChars` | `0` | Per-match description preview, in characters. `withDescription: true` is still accepted (it means 200 characters) but is no longer announced in the schema |
181
295
  | `limit` | `25` | Maximum `200` |
182
296
  | `offset` | `0` | Pass the previous response's `nextOffset` to page |
183
297
 
184
- `limit` and `offset` clip the response, not the request: Planka's own list
185
- endpoint accepts neither, so the server resolves the whole column through
186
- Planka's cursor and pages the output. That is what makes `total` honest. If
187
- pagination stops before the column is exhausted, the response carries
188
- `truncated: true` and a note saying so.
189
-
190
- Get list IDs from `planka_list_lists` or `planka_get_structure`.
191
-
192
- ### `planka_find_cards`
193
-
194
- Searches a board's cards by label, by text, or by column, and returns only the
195
- matches.
196
-
197
- Sweep every card carrying a label:
198
-
199
- ```json
200
- {
201
- "boardId": "2234567890123456789",
202
- "labelName": "ready"
203
- }
204
- ```
298
+ The digest of a card is `id`, `name`, `labels`, `labelIds`, `members`, `tasks`
299
+ (`"2/5"`), `dueDate`, `commentsTotal` and `updatedAt`. Empty values are left out
300
+ entirely, and the column is **not repeated on every card**: when the search is
301
+ scoped to one column the response carries `list: {id, name}` once, and only a
302
+ result spanning several columns puts `listId` and `listName` on each match.
205
303
 
206
- Check whether a card about something already exists, before creating a
207
- duplicate:
304
+ **The two digests share one vocabulary.** `labelIds` is in the digest, not only
305
+ in `detail: "full"`, because `planka_set_card_labels` takes label *ids* and only
306
+ ids: publishing the names alone sent the caller back to `planka_board_summary`
307
+ for a value `find_cards` already had. Where a field exists in both
308
+ `planka_find_cards` and `planka_get_card` it means the same thing and carries the
309
+ same name:
208
310
 
209
- ```json
210
- {
211
- "boardId": "2234567890123456789",
212
- "query": "export timeout",
213
- "withDescription": true
214
- }
215
- ```
311
+ | Concept | `planka_find_cards` (digest) | `planka_get_card` (digest) |
312
+ |---|---|---|
313
+ | column | `listId` (multi-column results) + `listName` | `card.listId` + `card.listName` (`null` plus `listNameUnavailable` when the board could not be read) |
314
+ | labels | `labels` (names) + `labelIds` | `labels: [{id, name}]` |
315
+ | members | `members` (names) | `members: [{id, name}]` |
316
+ | tasks | `tasks: "2/5"` | `tasks: {done, total, lists}` |
317
+ | description | `description`, only when `descriptionChars > 0` | `descriptionPreview` + `descriptionChars` + `descriptionTruncated` |
216
318
 
217
- Filters combine, and at least one of `labelName`, `query`, or `listId` is
218
- required; an unfiltered search would just be a slower `planka_get_board`.
319
+ The response says where it came from and what it could not see:
219
320
 
220
- | Field | Default | Notes |
221
- |---|---|---|
222
- | `labelName` | none | Exact label name, case- and accent-insensitive |
223
- | `query` | none | Substring matched against card name **and** description, case- and accent-insensitive |
224
- | `listId` | none | Restricts the search to one column |
225
- | `withDescription` | `false` | Adds a 200-character preview per match |
226
- | `limit` | `25` | Maximum `200` |
227
- | `offset` | `0` | Pass the previous response's `nextOffset` to page |
321
+ | Field | Meaning |
322
+ |---|---|
323
+ | `source: "list"` | Read through the list show: one request, whole column |
324
+ | `truncated: false` | Guaranteed on the list path |
325
+ | `source: "board"` | Filtered over one board read |
326
+ | `excludesArchived: true` | The board read has no archive/trash cards |
327
+ | `labelsResolved: false` | Label names were not fetched, so `labels` is absent |
328
+ | `warnings` | A label, column or filter that matched nothing, with what does exist |
228
329
 
229
- If the board has no label with the requested name, the response still succeeds
230
- and carries a `warning` naming the labels that do exist. The same happens for a
231
- `listId` that is not on the board.
330
+ Get list IDs from `planka_board_summary` or `planka_get_structure`.
232
331
 
233
332
  ## Cards
234
333
 
@@ -254,12 +353,21 @@ same tool.
254
353
  }
255
354
  ```
256
355
 
257
- Only `listId` and `name` are required. The result reports `tasksCreated`,
258
- `labelsAttached`, and any individual `labelErrors`.
356
+ Only `listId` and `name` are required.
357
+
358
+ The result reports `tasksRequested` / `tasksCreated` and `labelsRequested` /
359
+ `labelsAttached` **counted from a re-read of the card**, plus `finalLabelIds`.
360
+ If a follow-up step fails the card is still created: `card.id` always comes
361
+ back, together with `partialFailure` (`tasks`, `tasksMissing`, `labelIds`) and
362
+ `isError`. Never retry a `planka_create_card` that errored without reading the
363
+ id it returned — that is how boards grow duplicates. Every label failing is an
364
+ error, not a `success: true` with zero labels.
259
365
 
260
366
  ### `planka_get_card`
261
367
 
262
- Reads full card details, including tasks, comments, labels, and attachments.
368
+ Returns a **digest** by default, and the full record only when asked for. On a
369
+ real card of the OmnicoreOS board the digest is **1.3 KB where the old
370
+ everything-always answer was 13.2 KB** — a 90 % cut with nothing important lost.
263
371
 
264
372
  ```json
265
373
  {
@@ -267,6 +375,38 @@ Reads full card details, including tasks, comments, labels, and attachments.
267
375
  }
268
376
  ```
269
377
 
378
+ The digest carries: `id`, `name`, `listId` **and `listName`**, `boardId`,
379
+ `labels` (ids **with names**), `members`, `tasks` as `{done, total, lists:[…]}`
380
+ — one entry per checklist, never flattened —, `attachmentsCount`,
381
+ `commentsTotal` (Planka's own counter), `dueDate`, `isDueCompleted`, `isClosed`,
382
+ `updatedAt` and `descriptionPreview` (first 200 chars, with `descriptionChars`
383
+ and `descriptionTruncated`).
384
+
385
+ | Parameter | Default | What it does |
386
+ |---|---|---|
387
+ | `detail` | `"digest"` | `"full"` adds the whole description, every task item, the attachment list and the custom field values |
388
+ | `withComments` | `0` | A number returns the N most recent comments; `"all"` pages through every one of them |
389
+ | `descriptionChars` | `200` | Size of the description preview (max 2000). Ignored when `detail` is `"full"` |
390
+
391
+ ```json
392
+ {
393
+ "cardId": "5234567890123456789",
394
+ "detail": "full",
395
+ "withComments": 10
396
+ }
397
+ ```
398
+
399
+ Comments are **off by default**: they are the biggest and most variable part of
400
+ a card, and most reads do not need them. When they are requested they come back
401
+ oldest first — reading order — with `commentsReturned`, `commentsHasMore`,
402
+ `nextBeforeId` and, for `"all"`, `commentsTruncated` when the card has more than
403
+ the page ceiling.
404
+
405
+ `listName` and the label names come from one board read, cached for a minute.
406
+ If that read fails the names are simply absent; the card is never lost with
407
+ them. If the comments endpoint fails, the tool ERRORS: it never reports an empty
408
+ comment list it could not actually read.
409
+
270
410
  ### `planka_update_card`
271
411
 
272
412
  Updates only the fields present in the request.
@@ -283,6 +423,10 @@ Updates only the fields present in the request.
283
423
 
284
424
  Use `null` to clear `description` or `dueDate`.
285
425
 
426
+ Passing only `cardId` is rejected (`nothing to do`): an empty PATCH used to
427
+ answer `success: true` without changing anything. The result re-reads the card
428
+ and lists in `notLanded` any field that did not actually change.
429
+
286
430
  ### `planka_move_card`
287
431
 
288
432
  Moves a card to a list and optionally assigns its numeric position.
@@ -291,15 +435,22 @@ Moves a card to a list and optionally assigns its numeric position.
291
435
  {
292
436
  "cardId": "5234567890123456789",
293
437
  "listId": "6234567890123456789",
438
+ "boardId": "2234567890123456789",
294
439
  "position": 65536
295
440
  }
296
441
  ```
297
442
 
298
- If `position` is omitted, Planka decides the final position.
443
+ If `position` is omitted, the card goes to the end of the list. `boardId` is
444
+ only for moving a card to another board, and the `listId` must belong to it.
445
+
446
+ The card is re-read: if it did not end up in the requested list the result is
447
+ `verified: false` + `isError`, with the list it is actually in.
299
448
 
300
449
  ### `planka_delete_card`
301
450
 
302
- Permanently deletes a card.
451
+ Permanently deletes a card, with its tasks, comments and attachments. The
452
+ deletion is confirmed by re-reading the card: a 404 is the proof, and anything
453
+ else comes back as `verified: false` + `isError`.
303
454
 
304
455
  ```json
305
456
  {
@@ -323,69 +474,101 @@ Adds one or more checklist tasks to a card.
323
474
  }
324
475
  ```
325
476
 
477
+ The result reports `tasksCreated` counted from a re-read of the card, not from
478
+ the request.
479
+
326
480
  ### `planka_update_task`
327
481
 
328
- Renames a task, changes completion, or does both.
482
+ Renames a task, changes completion, or does both. Passing only `taskId` is
483
+ rejected (`nothing to do`).
329
484
 
330
485
  ```json
331
486
  {
332
487
  "taskId": "7234567890123456789",
488
+ "cardId": "5234567890123456789",
333
489
  "name": "Add and run a regression test",
334
490
  "isCompleted": true
335
491
  }
336
492
  ```
337
493
 
494
+ `cardId` is optional but strongly recommended: Planka has no GET for a single
495
+ task, so the card is the only place the change can be verified. Without it the
496
+ result carries `verificationUnavailable` instead of a proven success.
497
+
338
498
  ### `planka_delete_task`
339
499
 
340
- Deletes one checklist task.
500
+ Deletes one checklist task. Same rule: pass `cardId` to have the deletion
501
+ confirmed against the card.
341
502
 
342
503
  ```json
343
504
  {
344
- "taskId": "7234567890123456789"
505
+ "taskId": "7234567890123456789",
506
+ "cardId": "5234567890123456789"
345
507
  }
346
508
  ```
347
509
 
348
510
  ## Labels
349
511
 
350
- ### `planka_manage_labels`
351
-
352
- Creates, updates, or deletes a board label. The `action` determines which IDs
353
- are required.
512
+ ### `planka_create_label`
354
513
 
355
- Create:
514
+ Creates a board label.
356
515
 
357
516
  ```json
358
517
  {
359
- "action": "create",
360
518
  "boardId": "2234567890123456789",
361
519
  "name": "needs-decision",
362
- "color": "berry-red"
520
+ "color": "berry-red",
521
+ "position": 65536
363
522
  }
364
523
  ```
365
524
 
366
- Update:
525
+ `position` is optional; Planka appends the label when it is omitted.
526
+
527
+ `boardId`, `name` and `color` are `required` in the JSON Schema, so a client
528
+ rejects an incomplete call before it leaves. With `PLANKA_DEFAULT_BOARD_ID` set,
529
+ `boardId` drops out of `required` and is filled in.
530
+
531
+ `color` is a real JSON Schema `enum` of the 43 Planka 2.x colors **here and only
532
+ here** — repeating the enum on `planka_update_label` and `planka_manage_labels`
533
+ cost ~650 characters of every session for the same list. Those two take the same
534
+ values, validated server-side with a message that spells them out, and the
535
+ `planka://labels/colors` resource serves the list on demand.
536
+ A bad color is rejected before the call leaves the client. Unknown colors are still accepted
537
+ on reads, so one new upstream color cannot break a board response. Run
538
+ `planka_board_summary` to see the colors already in use.
539
+
540
+ The write is verified against a re-read of the board's labels.
541
+
542
+ ### `planka_update_label`
543
+
544
+ Renames a label or changes its color.
367
545
 
368
546
  ```json
369
547
  {
370
- "action": "update",
371
548
  "labelId": "4234567890123456789",
372
549
  "name": "decision-needed",
373
550
  "color": "pumpkin-orange"
374
551
  }
375
552
  ```
376
553
 
377
- Delete:
554
+ A call carrying only `labelId` is rejected (`nothing to do`) instead of sending
555
+ an empty `PATCH` that Planka would answer `200` to. The response reports the
556
+ board's copy of the label, not the `PATCH` echo.
557
+
558
+ ### `planka_delete_label`
559
+
560
+ Deletes a board label.
378
561
 
379
562
  ```json
380
563
  {
381
- "action": "delete",
382
564
  "labelId": "4234567890123456789"
383
565
  }
384
566
  ```
385
567
 
386
- Writes validate colors against the Planka 2.x palette. Run
387
- `planka_get_board` to see colors already in use. Unknown colors are accepted on
388
- reads so one new upstream color cannot break the whole board response.
568
+ > **DESTRUCTIVE.** The label disappears from every card that carried it.
569
+
570
+ Confirmed by re-reading the board: a label still there afterwards comes back as
571
+ `verified: false` + `isError`.
389
572
 
390
573
  ### `planka_set_card_labels`
391
574
 
@@ -413,10 +596,11 @@ absent, failed, and final label IDs. This makes silent no-op writes visible.
413
596
 
414
597
  ## Comments
415
598
 
416
- ### `planka_add_comment`
599
+ ### `planka_create_comment`
417
600
 
418
601
  Adds a Markdown-capable comment through Planka 2.x's dedicated card action
419
- endpoint.
602
+ endpoint. (Formerly `planka_add_comment`, which still works — see
603
+ [Deprecated aliases](#deprecated-aliases).)
420
604
 
421
605
  ```json
422
606
  {
@@ -425,59 +609,81 @@ endpoint.
425
609
  }
426
610
  ```
427
611
 
612
+ The comment is re-read from the card before the tool reports success.
613
+
428
614
  ### `planka_get_comments`
429
615
 
430
- Reads the dedicated comment collection for one card.
616
+ Reads the dedicated comment collection for one card, **oldest first**, one page
617
+ at a time. Planka serves comments 50 at a time and ignores `?limit`/`?page`; the
618
+ only cursor is `beforeId`, and this tool exposes it honestly.
431
619
 
432
620
  ```json
433
621
  {
434
- "cardId": "5234567890123456789"
622
+ "cardId": "5234567890123456789",
623
+ "limit": 20,
624
+ "order": "asc"
435
625
  }
436
626
  ```
437
627
 
628
+ | Parameter | Default | What it does |
629
+ |---|---|---|
630
+ | `limit` | `20` | Comments to return, max 50 (Planka's page) |
631
+ | `beforeId` | — | Cursor: returns comments **older** than that comment id |
632
+ | `all` | `false` | Walks every page, up to 10 of them (500 comments) |
633
+ | `order` | `"asc"` | `"asc"` oldest first (reading order), `"desc"` newest first |
634
+
635
+ The answer carries `commentCount` (**what this response holds — never the card's
636
+ total**), `hasMore`, `nextBeforeId` and, with `all`, `truncated` plus the reason.
637
+ Continue by passing the `nextBeforeId` you were handed:
638
+
639
+ ```json
640
+ { "cardId": "5234567890123456789", "beforeId": "1034567890123456789" }
641
+ ```
642
+
643
+ The card's real total comes from `planka_get_card` (`commentsTotal`), which is
644
+ Planka's own counter.
645
+
438
646
  This does not rely on `included.comments`, which is empty in affected Planka 2.x
439
647
  responses even when the card has comments.
440
648
 
441
- ### `planka_manage_comment`
442
-
443
- Edits or deletes an existing comment. Get comment IDs from
444
- `planka_get_comments`.
649
+ ### `planka_update_comment`
445
650
 
446
- Update:
651
+ Rewrites a comment. Get comment IDs from `planka_get_comments`.
447
652
 
448
653
  ```json
449
654
  {
450
- "action": "update",
451
655
  "commentId": "1034567890123456789",
452
656
  "text": "Verification complete: unit tests and the manual export both passed."
453
657
  }
454
658
  ```
455
659
 
456
- Delete:
660
+ `commentId` and `text` are both `required`. Planka only lets the authenticated
661
+ user modify comments that user wrote, so an agent can correct its own notes but
662
+ not someone else's. The card's comments are re-read before the tool reports: an
663
+ edit that did not stick comes back as `verified: false` + `isError`.
664
+
665
+ ### `planka_delete_comment`
666
+
667
+ Deletes a comment.
457
668
 
458
669
  ```json
459
670
  {
460
- "action": "delete",
461
671
  "commentId": "1034567890123456789"
462
672
  }
463
673
  ```
464
674
 
465
- `text` is required for `update` and ignored for `delete`. Planka only lets the
466
- authenticated user modify comments that user wrote, so an agent can correct its
467
- own notes but not someone else's.
675
+ Confirmed by re-reading the card's comments; a comment still present afterwards
676
+ comes back as `verified: false` + `isError`.
468
677
 
469
678
  ## Lists
470
679
 
471
- ### `planka_manage_lists`
680
+ ### `planka_create_list`
472
681
 
473
- Creates, updates, or deletes a board list. Creation sends an explicit Planka 2.x
474
- list `type`; it defaults to `active`.
475
-
476
- Create:
682
+ Creates a board list (column). Creation sends an explicit Planka 2.x list
683
+ `type`; it defaults to `active`.
477
684
 
478
685
  ```json
479
686
  {
480
- "action": "create",
481
687
  "boardId": "2234567890123456789",
482
688
  "name": "Pending",
483
689
  "position": 65536,
@@ -485,11 +691,16 @@ Create:
485
691
  }
486
692
  ```
487
693
 
488
- Update:
694
+ `boardId` and `name` are `required` (`boardId` drops out with
695
+ `PLANKA_DEFAULT_BOARD_ID`). Only `active` and `closed` are accepted for list
696
+ writes; internal Planka list types such as archive and trash remain readable.
697
+
698
+ ### `planka_update_list`
699
+
700
+ Renames, repositions or reclassifies a column.
489
701
 
490
702
  ```json
491
703
  {
492
- "action": "update",
493
704
  "listId": "3234567890123456789",
494
705
  "name": "Ready",
495
706
  "position": 131072,
@@ -497,17 +708,26 @@ Update:
497
708
  }
498
709
  ```
499
710
 
500
- Delete:
711
+ A call carrying only `listId` is rejected (`nothing to do`). The response reports
712
+ the board's copy of the list.
713
+
714
+ ### `planka_delete_list`
715
+
716
+ Deletes a column.
501
717
 
502
718
  ```json
503
719
  {
504
- "action": "delete",
505
720
  "listId": "3234567890123456789"
506
721
  }
507
722
  ```
508
723
 
509
- Only `active` and `closed` are accepted for list writes. Internal Planka list
510
- types such as archive and trash remain readable.
724
+ > **DESTRUCTIVE — it takes the cards with it.** Deleting a list deletes every
725
+ > card inside it, and Planka does not ask. Move the cards out first with
726
+ > `planka_move_list_cards` or `planka_move_card`, or delete a column you created
727
+ > yourself. This cannot be undone.
728
+
729
+ Confirmed by re-reading the board: a list still there afterwards comes back as
730
+ `verified: false` + `isError`.
511
731
 
512
732
  ## Attachments
513
733
 
@@ -573,15 +793,379 @@ What comes back depends on the content type:
573
793
 
574
794
  ### `planka_delete_attachment`
575
795
 
576
- Permanently deletes one attachment. It is addressed by attachment ID alone, not
577
- by card.
796
+ Permanently deletes one attachment.
578
797
 
579
798
  ```json
580
799
  {
581
- "attachmentId": "1134567890123456789"
800
+ "attachmentId": "1134567890123456789",
801
+ "cardId": "5234567890123456789"
802
+ }
803
+ ```
804
+
805
+ `cardId` is optional: Planka normally echoes the deleted attachment (which
806
+ carries its card), and that is what makes the deletion verifiable. Pass it when
807
+ you have it; if neither is available the result says
808
+ `verificationUnavailable` rather than claiming success.
809
+
810
+ ## Activity
811
+
812
+ Planka records **six** kinds of event, and only six
813
+ (`Action.Types` in `server/api/models/Action.js`):
814
+
815
+ `createCard` · `moveCard` · `addMemberToCard` · `removeMemberFromCard` ·
816
+ `completeTask` · `uncompleteTask`
817
+
818
+ There is no event for a comment, a rename or a description edit. A card with an
819
+ empty history was not necessarily untouched — it was only never moved, assigned
820
+ or checked off. Both tools say so in a `note` when they return nothing.
821
+
822
+ Both endpoints return at most **50 events** (a Planka constant, not a query
823
+ parameter) and page backwards with a cursor.
824
+
825
+ ### `planka_card_history`
826
+
827
+ One card's log, newest first, as one human line per event.
828
+
829
+ ```json
830
+ {
831
+ "cardId": "5234567890123456789",
832
+ "limit": 20,
833
+ "beforeId": "7234567890123456789"
834
+ }
835
+ ```
836
+
837
+ `limit` defaults to 20 and is capped at 50. Response:
838
+
839
+ ```json
840
+ {
841
+ "cardId": "5234567890123456789",
842
+ "cardName": "Retire the nginx failover",
843
+ "returned": 2,
844
+ "events": [
845
+ {
846
+ "id": "1855662785539605720",
847
+ "type": "moveCard",
848
+ "at": "2026-09-03T02:50:42.309Z",
849
+ "user": "Claude",
850
+ "summary": "moved the card from \"In progress\" to \"Review\""
851
+ },
852
+ {
853
+ "id": "1855530665752134803",
854
+ "type": "createCard",
855
+ "at": "2026-09-02T22:28:12.404Z",
856
+ "user": "Claude",
857
+ "summary": "created the card in \"Backlog\""
858
+ }
859
+ ],
860
+ "hasMore": false
861
+ }
862
+ ```
863
+
864
+ `hasMore` with `nextBeforeId` means there is older activity: call again passing
865
+ `beforeId: nextBeforeId`. List, user and task names come from the event itself —
866
+ no extra read is made to resolve them.
867
+
868
+ ### `planka_board_activity`
869
+
870
+ What moved on a whole board, grouped by card. The call to make when resuming a
871
+ session someone else (or a past you) left behind.
872
+
873
+ ```json
874
+ {
875
+ "boardId": "2234567890123456789",
876
+ "since": "2026-09-01T00:00:00.000Z",
877
+ "limit": 50,
878
+ "types": ["moveCard", "createCard"]
879
+ }
880
+ ```
881
+
882
+ | Field | Meaning |
883
+ |---|---|
884
+ | `since` | ISO 8601. The sweep walks pages backwards and stops at it. |
885
+ | `limit` | Maximum events returned. Default 50, capped at 250. |
886
+ | `types` | Keep only these event types. |
887
+
888
+ The sweep is capped at **5 pages** (250 events). Response:
889
+
890
+ ```json
891
+ {
892
+ "boardId": "2234567890123456789",
893
+ "since": "2026-09-01T00:00:00.000Z",
894
+ "pagesFetched": 1,
895
+ "scanned": 50,
896
+ "returned": 5,
897
+ "cardsTouched": 1,
898
+ "truncated": true,
899
+ "nextBeforeId": "1855558191031518415",
900
+ "cards": [
901
+ {
902
+ "cardId": "5234567890123456789",
903
+ "cardName": "Retire the nginx failover",
904
+ "events": [
905
+ {
906
+ "id": "1855662785539605720",
907
+ "type": "moveCard",
908
+ "at": "2026-09-03T02:50:42.309Z",
909
+ "user": "Claude",
910
+ "summary": "moved the card from \"In progress\" to \"Review\""
911
+ }
912
+ ]
913
+ }
914
+ ]
915
+ }
916
+ ```
917
+
918
+ `truncated: true` means older activity exists that this answer does not carry —
919
+ either `limit` or the page cap stopped the sweep. It is `false` only when the
920
+ log was exhausted or `since` was reached.
921
+
922
+ ## People
923
+
924
+ ### `planka_list_users`
925
+
926
+ Who can be assigned, with the IDs `planka_set_card_members` needs.
927
+
928
+ ```json
929
+ {
930
+ "boardId": "2234567890123456789",
931
+ "detail": "digest"
932
+ }
933
+ ```
934
+
935
+ `GET /api/users` is admin-only in Planka. When this account is a plain
936
+ `boardUser` the call is refused and the tool falls back to the **board's own
937
+ members**, which is the assignable set anyway. The fallback is never silent:
938
+
939
+ | Field | Meaning |
940
+ |---|---|
941
+ | `source: "instance"` | The instance roster. |
942
+ | `source: "board"` + `note` | The instance roster was refused; these are the board's members, each with `boardRole` and `canComment`. |
943
+
944
+ `boardId` is only used by the fallback. `detail: "full"` adds email addresses,
945
+ which the digest deliberately omits.
946
+
947
+ ```json
948
+ {
949
+ "source": "instance",
950
+ "total": 4,
951
+ "users": [
952
+ { "id": "1830226958957413619", "name": "Fede", "username": "fede", "role": "boardUser" }
953
+ ]
954
+ }
955
+ ```
956
+
957
+ ### `planka_whoami`
958
+
959
+ The first call to make when something answers `404` that should not: it names
960
+ the account, what it may do on the board, and what the local policy switched
961
+ off — the three usual causes of a confusing refusal.
962
+
963
+ ```json
964
+ {
965
+ "user": { "id": "1829420929365574677", "name": "Claude", "username": "claude", "role": "admin" },
966
+ "plankaVersion": "2.2.1",
967
+ "authMode": "password",
968
+ "boardMembership": { "boardId": "2234567890123456789", "role": "editor", "canComment": null },
969
+ "policy": {
970
+ "defaultBoardId": null,
971
+ "allowedBoardIds": null,
972
+ "allowedProjectIds": null,
973
+ "readOnly": false,
974
+ "disabledTools": [],
975
+ "protectedListIds": []
976
+ }
582
977
  }
583
978
  ```
584
979
 
980
+ `plankaVersion` comes from `/api/bootstrap`, the only endpoint in 2.x that
981
+ carries it. A version that does not start with `2.` adds a `versionWarning`:
982
+ this server targets 2.x and the paths it uses may not exist elsewhere.
983
+ `boardMembership` is read from `/api/projects` (~3 kB), not from the board show
984
+ (~900 kB) — both carry the memberships.
985
+
986
+ ### `planka_set_card_members`
987
+
988
+ Assigns and unassigns people on a card, then re-reads the card and reports the
989
+ verified final state. The mirror of `planka_set_card_labels`, argument for
990
+ argument.
991
+
992
+ ```json
993
+ {
994
+ "cardId": "5234567890123456789",
995
+ "add": ["1830226958957413619"],
996
+ "remove": ["1830228857995658485"],
997
+ "addUserNames": ["fede"],
998
+ "removeUserNames": ["Pat"],
999
+ "boardId": "2234567890123456789"
1000
+ }
1001
+ ```
1002
+
1003
+ Names are matched exactly and case-insensitively, on `username` first and then
1004
+ on `name`. An ambiguous name is **not** guessed — assigning the wrong person is
1005
+ worse than failing — and comes back in `failed`. Removals are applied before
1006
+ additions, so one call can hand a card from one person to another.
1007
+
1008
+ The response distinguishes `added`, `removed`, `alreadyPresent`, `notPresent`,
1009
+ `failed` and `finalMemberIds`. A `404` on an addition almost always means the
1010
+ user is not a member of that board; the reason says so.
1011
+
1012
+ ## Card operations
1013
+
1014
+ ### `planka_duplicate_card`
1015
+
1016
+ Copies a card through Planka's own `duplicate` endpoint, which carries over the
1017
+ card's tasks, labels, members and custom field values. **Attachments are not
1018
+ copied.**
1019
+
1020
+ ```json
1021
+ {
1022
+ "cardId": "5234567890123456789",
1023
+ "listId": "3234567890123456789",
1024
+ "name": "Retire the nginx failover (round 2)",
1025
+ "position": 65536
1026
+ }
1027
+ ```
1028
+
1029
+ Without `listId` the copy lands in the original's column; without `name` Planka
1030
+ appends its own suffix. `boardId` copies across boards and then `listId` is
1031
+ required — a card cannot land on a board without a column. The counts in
1032
+ `copied` come from re-reading the new card, not from the write's response.
1033
+
1034
+ ```json
1035
+ {
1036
+ "success": true,
1037
+ "verified": true,
1038
+ "card": { "id": "6234567890123456789", "name": "…", "listId": "3234567890123456789" },
1039
+ "sourceCardId": "5234567890123456789",
1040
+ "copied": { "tasks": 2, "labels": 1, "members": 1, "attachments": 0 }
1041
+ }
1042
+ ```
1043
+
1044
+ ### `planka_archive_card`
1045
+
1046
+ Planka has no "archive" verb. Every board owns two invisible lists besides its
1047
+ columns — one of type `archive` and one of type `trash` — with no name, no
1048
+ position, and no `GET` of their own (`GET /api/lists/:id` answers 404 for
1049
+ them). Archiving is a move into the first of those, and this tool resolves it
1050
+ from the board.
1051
+
1052
+ ```json
1053
+ { "cardId": "5234567890123456789" }
1054
+ ```
1055
+
1056
+ To bring a card back, name the column it should land in:
1057
+
1058
+ ```json
1059
+ { "cardId": "5234567890123456789", "restoreToListId": "3234567890123456789" }
1060
+ ```
1061
+
1062
+ Two behaviours worth knowing, both verified against Planka 2.2.1:
1063
+
1064
+ - `isClosed` does **not** change when a card is archived, so it is useless as
1065
+ proof. The only honest verification is the card's `listId`.
1066
+ - An archived card has `position: null`, and Planka rejects a `PATCH` that only
1067
+ carries `listId` with *"Position must be present"*. The restore path always
1068
+ sends a position (`position` is accepted; it defaults to the end).
1069
+
1070
+ The archive response reports `previousListId`, which is the argument to pass to
1071
+ `restoreToListId` later. Archiving an already-archived card does not move it
1072
+ again: it answers `alreadyArchived: true` and names the column it came from.
1073
+
1074
+ ### `planka_move_list_cards`
1075
+
1076
+ Moves **every** card of one column into another.
1077
+
1078
+ ```json
1079
+ {
1080
+ "fromListId": "3234567890123456789",
1081
+ "toListId": "4234567890123456789",
1082
+ "boardId": "2234567890123456789"
1083
+ }
1084
+ ```
1085
+
1086
+ Planka has a bulk endpoint, `POST /api/lists/:id/move-cards`, and it is far
1087
+ narrower than its name: `server/api/controllers/lists/move-cards.js` in 2.2.1
1088
+ refuses everything except archiving a whole closed column.
1089
+
1090
+ ```js
1091
+ if (list.type !== List.Types.CLOSED) throw NOT_ENOUGH_RIGHTS; // 403
1092
+ if (nextList.type !== List.Types.ARCHIVE) throw LIST_NOT_FOUND; // 404
1093
+ ```
1094
+
1095
+ So an ordinary column-to-column call comes back **403 "Not enough rights"**,
1096
+ which reads like a permission problem and is not one. This tool uses the bulk
1097
+ call for the single case it accepts and moves card by card everywhere else, one
1098
+ `PATCH` each, keeping each card's position so the relative order survives.
1099
+ `strategy` says which path ran: `"bulk"` or `"perCard"`.
1100
+
1101
+ Both columns are counted before and after, and success is only claimed when the
1102
+ numbers add up:
1103
+
1104
+ ```json
1105
+ {
1106
+ "success": true,
1107
+ "verified": true,
1108
+ "fromListId": "3234567890123456789",
1109
+ "toListId": "4234567890123456789",
1110
+ "strategy": "perCard",
1111
+ "moved": 3,
1112
+ "fromRemaining": 0,
1113
+ "toTotal": 8,
1114
+ "expectedTotal": 8
1115
+ }
1116
+ ```
1117
+
1118
+ | Field | Meaning |
1119
+ |---|---|
1120
+ | `strategy` | `bulk` (closed → archive, one call) or `perCard` (one PATCH per card) |
1121
+ | `moved` | Cards that actually left the source column |
1122
+ | `failed` | Cards whose PATCH was rejected, with the reason. Present only on a partial move |
1123
+ | `toCountUnavailable` | The destination is the archive, which Planka will not show; only the source could be counted |
1124
+
1125
+ `verified: false` with `isError` means the counts disagree — read both columns
1126
+ before retrying. An already empty source is not an error (`moved: 0`). A source
1127
+ column Planka refuses to show (`archive`, `trash`, or an id that does not exist)
1128
+ is rejected with a message that says so. `PLANKA_PROTECTED_LIST_IDS` guards both
1129
+ ends, so a protected column can be neither filled nor drained.
1130
+
1131
+ ## Deprecated aliases
1132
+
1133
+ Seven names are kept alive only so callers written against them keep working.
1134
+ They add no behaviour: each routes its arguments to the tool that replaced it.
1135
+
1136
+ | Deprecated | Use instead |
1137
+ |---|---|
1138
+ | `planka_list_cards` | `planka_find_cards` with `listId` |
1139
+ | `planka_list_lists` | `planka_board_summary` with `cardsFrom: []` |
1140
+ | `planka_manage_labels` | `planka_create_label` · `planka_update_label` · `planka_delete_label` |
1141
+ | `planka_manage_lists` | `planka_create_list` · `planka_update_list` · `planka_delete_list` |
1142
+ | `planka_manage_comment` | `planka_update_comment` · `planka_delete_comment` (and `planka_create_comment`) |
1143
+ | `planka_add_comment` | `planka_create_comment` |
1144
+ | `planka_get_board` | `planka_board_summary` + `planka_find_cards` |
1145
+
1146
+ **Why they were split.** A single tool with an `action` enum cannot express
1147
+ "`boardId` is required when `action` is `create`" in JSON Schema. So each
1148
+ `manage_*` tool carried 20-40 lines of hand-written "which field does this
1149
+ action need?" checks — and a forgotten branch is exactly how `update` with only
1150
+ an id shipped an empty `PATCH` that Planka answered `200` to. One tool per
1151
+ action puts the answer back in `required`, where the client enforces it before
1152
+ the call is made and the server's generic presence check covers the rest.
1153
+
1154
+ The aliases borrow their target's `required` at dispatch time, so
1155
+ `planka_manage_labels` with `action: "update"` and no `labelId` is refused with a
1156
+ message naming `planka_update_label` rather than sending an empty write.
1157
+
1158
+ They also borrow their target's **access policy**: `planka_manage_lists` with
1159
+ `action: "delete"` is refused when `planka_delete_list` is in
1160
+ `PLANKA_DISABLED_TOOLS`, and `planka_add_comment` is refused when
1161
+ `planka_create_comment` is. An alias is a second name for a tool, never a way
1162
+ around the configuration that switched it off.
1163
+
1164
+ **Hiding them.** `PLANKA_HIDE_DEPRECATED=true` drops all seven from `tools/list`,
1165
+ which is worth ~6.8 kB (32,840 → 26,034 characters, measured on 0.3.0).
1166
+ They stay callable: a model working from a cached tool list, or from a skill that
1167
+ names one, gets the real behaviour instead of "unknown tool".
1168
+
585
1169
  ## When arguments are wrong
586
1170
 
587
1171
  Invalid input is reported as an error result naming the tool, the field, what
@@ -625,9 +1209,9 @@ Unit tests validate schemas, request construction, and error formatting without
625
1209
  network: run them with `npm test`.
626
1210
 
627
1211
  The opt-in smoke test is the other half. It starts the built MCP server over stdio,
628
- drives all 24 tools through real MCP calls, and independently queries the raw
1212
+ drives all 41 tools through real MCP calls, and independently queries the raw
629
1213
  Planka HTTP API after every mutation, so a tool that reports success on something
630
- that never landed is caught here. It runs 64 named checks.
1214
+ that never landed is caught here. It runs over 90 named checks.
631
1215
 
632
1216
  ```bash
633
1217
  export PLANKA_BASE_URL="https://planka.example.com"