@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/CHANGELOG.md CHANGED
@@ -2,6 +2,516 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.3.0] - 2026-09-03
8
+
9
+ ### Highlights
10
+
11
+ - **Every write is verified.** The server writes, re-reads the entity and
12
+ reports what the re-read says — never what was asked for. `success` mirrors
13
+ `verified` and nothing else: "I could not check" now answers `success: false`
14
+ with `isError` and names the parameter that would make the check possible.
15
+ - **API key authentication and a real access policy.** `PLANKA_API_KEY` replaces
16
+ the login (no token refresh, no sign-in rate limit), and seven environment
17
+ variables scope what the server may see and touch: default board, board and
18
+ project allowlists, read-only mode, disabled tools, protected columns, hidden
19
+ deprecated tools.
20
+ - **`instructions` and annotations on the handshake.** The shared guidance is
21
+ stated once instead of 41 times, every tool publishes a title and all four MCP
22
+ behaviour hints explicitly, and the eleven tools that lose data carry Claude
23
+ Code's `requiresUserInteraction` prompt.
24
+ - **Reads return digests, and say what they clipped.** Descriptions are off
25
+ unless asked for, results are capped, and every scoped read reports
26
+ `total`/`returned`/`hasMore`/`nextOffset`, so a clipped answer is never
27
+ mistaken for a complete one. `planka_get_board` went 56 k → 17 k characters.
28
+ - **Eight new tools**: `planka_board_summary`, `planka_find_cards`,
29
+ `planka_get_activity`, `planka_list_users`, `planka_set_card_members`,
30
+ `planka_duplicate_card`, `planka_archive_card`, `planka_move_list_cards`.
31
+ - **The three `manage_*` tools are split into one tool per action** (32 → 41
32
+ tools) so `required` finally means something, with the old names kept as
33
+ deprecated aliases that route to the replacement — and that now inherit the
34
+ replacement's access policy.
35
+ - **Ships as a Claude Code and Codex plugin**, with MCP resources and prompts,
36
+ a `planka-mcp init --client` installer, and a preflight hook.
37
+ - **Breaking changes**: `success` is no longer `true` on an unverified write;
38
+ `planka_list_cards` and `planka_list_lists` are deprecated (still callable);
39
+ `withDescription` and `labelFilter` are still accepted but no longer announced
40
+ in `inputSchema`; `planka_find_cards` digests now carry `labelIds`;
41
+ `planka_get_card` returns `listName: null` plus `listNameUnavailable` instead
42
+ of omitting the field.
43
+
44
+ ### Fixed — adversarial review before release
45
+
46
+ - **`PLANKA_DEFAULT_BOARD_ID` no longer breaks the tool the instructions tell
47
+ you to call first.** The runtime presence check validated against the raw
48
+ registry schema while `tools/list` advertised the policy-applied one, so
49
+ `planka_board_summary {}` was refused for a missing `boardId` the listing said
50
+ was optional — on the five tools that declare it (`board_summary`,
51
+ `list_lists`, `get_board`, `create_list`, `create_label`). Both sides now go
52
+ through `effectiveInputSchema(tool, policy)`.
53
+ - **Deprecated aliases no longer bypass `PLANKA_DISABLED_TOOLS`.**
54
+ `planka_manage_lists {action:"delete"}` reached a disabled
55
+ `planka_delete_list`, and `planka_add_comment` a disabled
56
+ `planka_create_comment`. Both now consult the target's policy before
57
+ dispatching.
58
+ - **No more `{"success":true,"verified":false,"message":"… not confirmed"}`.**
59
+ That was the default branch of `update_task` and `delete_task` without
60
+ `cardId`, of `delete_attachment` when Planka did not echo the card, and of the
61
+ deletes whose response body does not parse. Unverified writes answer
62
+ `success: false` + `isError` and say what to pass; `update_task` without
63
+ `cardId` also labels its fields as `taskFromWriteEcho`.
64
+ - **The test guarding `WRITE_TOOL_NAMES` could not fail.** It compared a set
65
+ against its own complement. It now asserts, tool by tool, that membership
66
+ matches the `readOnlyHint` the tool advertises — the only net that stops the
67
+ next write tool from staying visible under `PLANKA_READ_ONLY=true`.
68
+ - **`PLANKA_PROTECTED_LIST_IDS` protects the cards, not just the column.**
69
+ `move_card`, `archive_card` and `delete_card` refused to write *into* a
70
+ protected column but happily emptied it one card at a time. The origin column
71
+ is now checked too — with no extra read on deployments that set no protected
72
+ list.
73
+ - **`planka_add_attachment` goes through `verifiedWrite`.** A failed re-read
74
+ used to return an error without the attachment id, and the natural retry
75
+ uploaded the file twice.
76
+ - **Four hand-written `verified: true` are gone.** `set_card_labels` and
77
+ `set_card_members` report through `writeResult` with a flag derived from their
78
+ own re-read; the two "nothing to do" states say which read backs the claim.
79
+ - **`planka_create_card` verifies what it wrote** (`name` and `listId`, not just
80
+ the id it re-read by), and a label that failed to attach comes back as
81
+ `{labelId, reason}` instead of a bare id — a 403 on the board and a wrong
82
+ labelId are otherwise indistinguishable.
83
+ - **`planka_find_cards` digests carry `labelIds`**, so
84
+ `find_cards → set_card_labels` no longer needs a `planka_board_summary` in
85
+ between, and multi-column results carry `listId` next to `listName`.
86
+ - **A 404 on the board keeps its diagnosis** when `/api/projects` is unreadable
87
+ too: the original error, with its hint, is re-thrown instead of replaced by a
88
+ bare "Board not found".
89
+ - **`planka_create_list` and `planka_create_label` check the board allowlist
90
+ explicitly**, instead of relying on a lower layer to do it.
91
+ - `planka_get_card` returns `listName: null` with `listNameUnavailable` when the
92
+ board could not be read; a 404 on `/api/users/me` no longer claims `me` should
93
+ be a `userId`; the HTML-instead-of-JSON error only blames an HTTP→HTTPS
94
+ redirect when the URL was `http://` or the request was actually redirected;
95
+ `planka_set_card_members` with names and no board says the problem was
96
+ resolving a **name**; the `planka://labels/colors` resource points at
97
+ `planka_create_label` / `planka_update_label` instead of the deprecated
98
+ `planka_manage_labels`.
99
+
100
+ ### Changed — `tools/list` cost
101
+
102
+ - `planka_list_cards` and `planka_list_lists` joined `DEPRECATED_TOOL_NAMES`,
103
+ the 43-colour enum is announced once (on `planka_create_label`) instead of
104
+ three times, `withDescription` and `labelFilter` are no longer announced, and
105
+ the `boardId` description was cut. Measured over JSON-RPC against the built
106
+ server: `tools/list` **34 477 → 32 840** characters, and **29 821 → 26 034**
107
+ with `PLANKA_HIDE_DEPRECATED=true` (−22 % against the pre-release baseline).
108
+ Nothing lost capability: every trimmed parameter is still accepted.
109
+
110
+ ### Documentation
111
+
112
+ - `server.json` declares all thirteen environment variables the code reads;
113
+ `PLANKA_MCP_PREFLIGHT` and `PLANKA_MCP_COMMAND` are documented in both READMEs;
114
+ `docs/tools.md` lists the eleven `requiresUserInteraction` tools, the complete
115
+ data-losing set, `position` on `planka_create_label`, and the shared digest
116
+ vocabulary; `scripts/sync-identity.mjs` now also stamps the two `SKILL.md`
117
+ versions, so `check:identity` catches them when they go stale.
118
+
119
+ ### Added
120
+
121
+ - **The three `manage_*` tools are split, one tool per action: 32 → 41.**
122
+ `planka_create_label` / `planka_update_label` / `planka_delete_label`,
123
+ `planka_create_list` / `planka_update_list` / `planka_delete_list`, and
124
+ `planka_create_comment` / `planka_update_comment` / `planka_delete_comment`.
125
+ Each declares a real `required` in its JSON Schema, so the client refuses an
126
+ incomplete call before it leaves and the server's generic presence check does
127
+ the rest. The `action` enum could not express "boardId is required when
128
+ action=create", which is why every `manage_*` carried 20-40 lines of
129
+ hand-written guards — and a forgotten branch is exactly how an `update`
130
+ carrying only an id shipped an empty `PATCH` that Planka answered `200` to.
131
+ All nine reuse the existing operations and `verifiedWrite`/`verifiedDelete`,
132
+ and answer with the same digest (`{id, name, …, verified}`) as before.
133
+ `planka_delete_list` says in its own description that it deletes every card in
134
+ the column.
135
+ - `PLANKA_HIDE_DEPRECATED=true` drops the deprecated tools from `tools/list`:
136
+ `planka_manage_labels`, `planka_manage_lists`, `planka_manage_comment`,
137
+ `planka_add_comment`, `planka_get_board` and — added at the end of this
138
+ release — `planka_list_cards` and `planka_list_lists`. Measured on `tools/list`
139
+ at 0.3.0: **32 840 → 26 034 characters** (−6 806, −20.7 %). They remain
140
+ callable, so a model working from a cached list or from a skill that names one
141
+ still gets the real behaviour instead of "unknown tool". Default `false`.
142
+ - 35 unit tests for the split (`tests/unit/split-manage.test.ts`): the happy path
143
+ of each new tool against a mocked re-read, the `nothing to do` guard, a delete
144
+ that did not delete, `required` enforcement per tool, every alias dispatching,
145
+ and the `PLANKA_HIDE_DEPRECATED` filter. Suite: 189 → 224 tests.
146
+ - **The repository is now a plugin, for Claude Code and for Codex.**
147
+ `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` install the MCP
148
+ server plus the two `workflow/skills/` in one step, and the repo is its own
149
+ marketplace (`.claude-plugin/marketplace.json`,
150
+ `.agents/plugins/marketplace.json`):
151
+ `/plugin marketplace add omnicoreos/planka-mcp` → `/plugin install planka@planka-mcp`.
152
+ `userConfig` asks for the base URL and the API key when the plugin is enabled
153
+ and stores the key in the OS keychain — the manifest itself holds no secret,
154
+ only `${user_config.*}` substitutions. Claude Code reads `.mcp.json`
155
+ (`mcpServers`), Codex reads `.codex-plugin/mcp.json` (`mcp_servers`); the two
156
+ files exist because the wrapper key differs and one file cannot be both
157
+ without Codex reading a server literally named `mcpServers`. Validated with
158
+ `claude plugin validate --strict`.
159
+ - **A `SessionStart` preflight** (`hooks/preflight.mjs`): catches a Node older
160
+ than 18, a missing credential, an API key set together with email+password,
161
+ and an `http://` base URL that a redirect would turn into a dropped POST body.
162
+ Offline, silent when everything is fine, and it never exits non-zero — a
163
+ broken preflight must not block a session. `PLANKA_MCP_PREFLIGHT=full` also
164
+ spawns the server and waits for its banner.
165
+ - **MCP resources** (`src/resources.ts`): `planka://workflow/readme`,
166
+ `planka://workflow/board-template`, `planka://workflow/skills/orchestrator`,
167
+ `planka://workflow/skills/close-card`, `planka://gotchas/planka-2x` and
168
+ `planka://labels/colors`. The first five are read from the tarball at runtime
169
+ (resolved from `import.meta.url`, with a note pointing at the repository if an
170
+ install trimmed them); the last is generated from `LabelColorSchema`, so the
171
+ 43 colors can never drift from what the tools validate. A resource costs
172
+ nothing until it is read, which is where the 25 000 chars of `workflow/` and
173
+ `docs/` belong — not in `instructions`, which every session pays for.
174
+ - **MCP prompts** (`src/prompts.ts`): `planka-open-session`,
175
+ `planka-close-card` and `planka-board-triage`, surfaced by Claude Code as
176
+ `/mcp__planka__…`. Each orders the calls and says what to look at in the
177
+ answers, without restating what tool descriptions already say. Arguments are
178
+ optional except `cardId`, and a missing required argument is refused by name.
179
+ - **`planka-mcp init --client claude|codex|cursor|vscode|print`**: one setup
180
+ command for every client. Emits `claude mcp add`, a `[mcp_servers.planka]`
181
+ TOML block, a merged `~/.cursor/mcp.json` plus an "Add to Cursor" deeplink,
182
+ and a `.vscode/mcp.json` whose key is a `promptString` input rather than a
183
+ literal. Every emitter merges non-destructively — an existing `planka` entry
184
+ is reported and left alone — files written into `$HOME` get mode `0600`, and
185
+ no printed snippet or deeplink ever carries a real credential. Before writing
186
+ it runs one authenticated `GET /api/users/me`, which is the check that catches
187
+ a base URL pointing at the SPA instead of the API. `--dry-run` writes nothing;
188
+ `--client print` needs no credentials at all.
189
+ - README and README.es: an **Install** table with a one-liner per client
190
+ (Claude Code plugin and server, Codex, Cursor badge, VS Code badge, `init`)
191
+ and a **Resources and prompts** section.
192
+
193
+ - **`planka_get_card` is a digest with detail on demand.** By default it returns
194
+ counts and names instead of the whole record: `listName` next to `listId`,
195
+ label **names** with their ids, `members`, tasks as `{done, total, lists:[…]}`
196
+ with one entry per checklist, `attachmentsCount`, Planka's own `commentsTotal`,
197
+ `dueDate`/`isDueCompleted`/`isClosed`/`updatedAt`, and a 200-char
198
+ `descriptionPreview` (with `descriptionChars`, `descriptionTruncated`).
199
+ New parameters: `detail: "digest" | "full"`, `withComments` (a number or
200
+ `"all"`), `descriptionChars`. Measured on a real card with 6 comments and 5
201
+ attachments: **1275 chars against 13 187** for the old always-everything
202
+ answer — 90 % less, with the column name and the label names it never had.
203
+ - `planka_get_comments` paginates honestly: `limit` (default 20, max 50),
204
+ `beforeId` as an in/out cursor, `hasMore`, `nextBeforeId`, `order`
205
+ (`"asc"` — oldest first — by default) and `all: true` with a 10-page ceiling
206
+ that reports `truncated`. `getComments()` / `getCommentsPage()` in
207
+ `src/operations/comments.ts` are the operations behind it.
208
+ - `getBoardNames()`: the one board read that resolves a card's `listId` and
209
+ `labelId`s to names, memoized for 60 s. Best effort by design — a board that
210
+ cannot be read costs the names, never the card.
211
+ - 23 unit tests: digest vs full, `withComments` 0/N/`"all"` against a mocked
212
+ two-page endpoint, checklists that stay apart, members, the description
213
+ preview, the comment cursor and its ceiling, and one per error branch.
214
+ Suite: 109 → 132 tests.
215
+ - **Eight new tools, 24 → 32.** All of them answer with a digest and every write
216
+ re-reads before reporting:
217
+ - `planka_card_history` and `planka_board_activity` read Planka's activity log
218
+ (`/api/cards/:id/actions`, `/api/boards/:id/actions`) and turn each event
219
+ into one human line — "moved from X to Y", "added member Z", "completed task
220
+ T". `board_activity` walks pages backwards to a `since` date (5-page cap,
221
+ reported as `truncated`) and groups the result by card. Both say, when they
222
+ return nothing, that Planka logs only creation, moves, member changes and
223
+ task completion — never comments, renames or description edits.
224
+ - `planka_set_card_members` assigns and unassigns people, accepting user IDs
225
+ or names, and reports `finalMemberIds` read back from the card. The mirror
226
+ of `planka_set_card_labels`, argument for argument.
227
+ - `planka_list_users` lists who can be assigned, falling back to the board's
228
+ own members when `GET /api/users` is refused (it is admin-only) and saying
229
+ so in `source`. Emails only with `detail: "full"`.
230
+ - `planka_whoami` names the account, its board role, the Planka version and
231
+ the local access policy — the first call to make when a `404` makes no
232
+ sense. Warns when the instance is not a Planka 2.x.
233
+ - `planka_duplicate_card` copies a card with its tasks, labels and members
234
+ (attachments are not copied) and reports the counts from re-reading the copy.
235
+ - `planka_archive_card` archives into the board's hidden `archive` list and
236
+ restores with `restoreToListId`. Verified by the card's column: `isClosed`
237
+ does NOT change on archive, so it proves nothing.
238
+ - `planka_move_list_cards` empties one column into another, counting both
239
+ columns before and after. It uses Planka's bulk endpoint only for the case
240
+ that endpoint accepts (a closed column into the archive) and moves card by
241
+ card otherwise, preserving each card's position; `strategy` reports which.
242
+ - `src/operations/card-brief.ts`: a permissive card read used for verification.
243
+ An archived card comes back with `position: null`, which the strict card
244
+ schema rejects — the archive tool could not have verified its own write
245
+ without it. It also exposes `cardMemberships`, which no other read did.
246
+ - `countListCards()` and `moveListCards()` in `src/operations/lists.ts`, plus
247
+ `src/operations/{actions,members,users,archive,duplicate}.ts`.
248
+ - The four new write tools are in `WRITE_TOOL_NAMES`, so `PLANKA_READ_ONLY`
249
+ hides them, and `planka_archive_card` and `planka_move_list_cards` carry
250
+ `anthropic/requiresUserInteraction`: both change what a whole column looks
251
+ like.
252
+ - 32 unit tests for the new surface (fetch mocked): one summary per event type,
253
+ the `since` sweep and its cursor, the 403 fallback of the user roster, the
254
+ archive-list resolution, and the two failures that must not read as success —
255
+ a membership that never landed and a bulk move whose counts do not add up.
256
+ Suite: 109 → 141 tests.
257
+ - Smoke: the new tools are exercised end to end and cross-checked against the
258
+ raw API — history after a real move and a real assignment, a duplicate, an
259
+ archive/restore round trip, and a bulk move between the smoke's temporary
260
+ column and its working column. Every card it creates is deleted in the
261
+ `finally`.
262
+ - Server instructions gained one line: `planka_board_activity` is the call to
263
+ make when resuming a board someone else left.
264
+
265
+ - `src/operations/verify.ts`: `verifiedWrite`, `verifiedDelete` and `goneOn404`,
266
+ the single implementation of "write, re-read, report what landed". Until now
267
+ the pattern lived hand-copied in two places and was missing from eleven writes.
268
+ - Every write reports `verified: true|false`, and `isError` when the re-read
269
+ contradicts it or could not be made. New response fields: `verificationError`,
270
+ `verificationUnavailable`, `partialFailure`, `notLanded`.
271
+ - `planka_move_card` accepts `boardId`, so a card can finally be moved between
272
+ boards (the operation supported it; the tool did not expose it).
273
+ - `planka_update_task`, `planka_delete_task` and `planka_delete_attachment`
274
+ accept an optional `cardId`, which is what makes their verification possible
275
+ (Planka 2.x has no GET for a single task).
276
+ - `getCardLight()`: a card read without its comments, used for verification and
277
+ by `planka_set_card_labels`, which was pulling every comment of a card twice
278
+ only to compare label ids.
279
+ - 19 unit tests for the false greens: comments endpoint failing, partial task
280
+ creation, deletes that do not delete, no-op PATCHes, a move that did not move,
281
+ and labels that never attached. Suite: 45 → 64 tests.
282
+ - Smoke: a REAL move between two columns (temporary column created, card moved
283
+ there and back, both directions cross-checked against the raw API) and
284
+ `verified: true` assertions on every write.
285
+
286
+ - **Board reads redesigned around one principle: a small digest by default, the
287
+ detail through parameters.** `planka_find_cards` is now the single read over
288
+ cards and takes `listId`, `listName`, `query`, `labelName` (alias
289
+ `labelFilter`), `labelIds`, `userIds`, `withLabels`, `detail`
290
+ (`"digest" | "full"`), `withDescription`, `descriptionChars`, `limit` and
291
+ `offset`.
292
+ - `planka_find_cards` with a `listId` reads the column through
293
+ `GET /api/lists/:id`: **one request, no cursor**, so `total` is the real size
294
+ of the column and the answer carries `truncated: false`. A 159-card column
295
+ went from five requests to two (one with `withLabels: false`).
296
+ - The card digest now carries `members`, `commentsTotal` and `updatedAt`, and
297
+ `detail: "full"` returns every field Planka gave, description included.
298
+ - `planka_board_summary` absorbs `planka_list_lists`: `cardsFrom: []` answers
299
+ columns and labels with no cards. It also reports `totalCards`.
300
+ - `warnings` on `planka_board_summary` and `planka_find_cards`: a `cardsFrom`
301
+ entry, a `listName` or a `highlightLabel` that matches nothing comes back
302
+ naming what does exist, instead of an empty result that reads like "there is
303
+ nothing there".
304
+ - `PLANKA_SUMMARY_DECISION_LISTS` and `PLANKA_SUMMARY_HIGHLIGHT_LABEL`: the
305
+ board vocabulary that used to be hardcoded in Spanish inside the server now
306
+ comes from the environment, and defaults to nothing.
307
+ - `planka_get_structure` takes `withLists` (default `true`). With `false` the
308
+ whole structure is ONE request instead of one board read per board — 203
309
+ characters against 696 on the reference instance.
310
+ - `planka_get_board` takes `limit` (default 50) and reports `totalCards`,
311
+ `returnedCards` and `truncated`.
312
+ - `excludesArchived: true` on every read derived from the board show, and a new
313
+ gotcha documenting why: `GET /api/boards/:id` carries no archive and no trash
314
+ cards, and there is no way to ask for them.
315
+ - `query` semantics are now Planka's own: several words are ANDed, and a leading
316
+ `/` makes the rest a case-insensitive regular expression. A broken regex is
317
+ reported instead of matching nothing.
318
+ - 18 unit tests for the redesigned reads (list path, digest vs full, aliases,
319
+ warnings, derived page size, `excludesArchived`, `withLists`). Suite: 116 →
320
+ 134 tests.
321
+
322
+ ### Deprecated
323
+
324
+ - **`planka_get_board`.** Its description now starts with
325
+ `DEPRECATED: use planka_board_summary + planka_find_cards`. It stays
326
+ functional, and it is capped: 56,112 → 17,215 characters on the reference
327
+ board.
328
+ - `planka_list_lists` and `planka_list_cards` are kept as **aliases** —
329
+ respectively `planka_board_summary` with `cardsFrom: []` and
330
+ `planka_find_cards` with `listId`. Same implementation, same response shape;
331
+ the tool count and every tool name are unchanged.
332
+
333
+ ### Changed
334
+
335
+ - **The old names survive as deprecated aliases.** `planka_manage_labels`,
336
+ `planka_manage_lists`, `planka_manage_comment` and `planka_add_comment` keep
337
+ working unchanged; each is a ten-line dispatcher that routes `action` to the
338
+ tool that replaced it and borrows that tool's `required` (so
339
+ `manage_labels action:update` with no `labelId` is refused with a message
340
+ naming `planka_update_label`). Their descriptions open with `DEPRECATED:`.
341
+ - The nine new tools are in `WRITE_TOOL_NAMES` (18 → 27), so `PLANKA_READ_ONLY`
342
+ hides them, and in `src/tools/annotations.ts`; the three `delete_*` carry
343
+ `anthropic/requiresUserInteraction`.
344
+ - The smoke suite drives labels, lists and comments through the new tools and
345
+ keeps one check per alias, plus one that the alias borrows its target's
346
+ `required`.
347
+ - `scripts/setup.sh` is now an alias for `init --client claude`. The guided
348
+ installer that also picks or creates a board and runs the full write smoke
349
+ stays available as `node scripts/setup.mjs`.
350
+ - `workflow/skills/*/SKILL.md`: frontmatter trimmed to the portable
351
+ agentskills.io subset (`version:` moved under `metadata:`), and the bodies
352
+ updated to the current read surface — `board_summary` as the only opening
353
+ read, `find_cards` with a `listId` for a whole column, `get_card` as a digest
354
+ with `withComments`, `board_activity` when resuming someone else's work,
355
+ `verified` on every write, and `get_board`/`list_lists`/`list_cards` named as
356
+ deprecated aliases.
357
+ - `scripts/sync-identity.mjs` also generates the version pin in `.mcp.json`,
358
+ `.codex-plugin/mcp.json` and both plugin manifests, and exports
359
+ `PACKAGE_NAME`. A pin that drifts would install the old server from a new
360
+ plugin, silently; `npm run check:identity` now fails the build instead.
361
+ - `package.json` ships `.claude-plugin`, `.codex-plugin`, `.agents`,
362
+ `.mcp.json` and `hooks` in the tarball.
363
+ - 36 unit tests for the new surface (resources list/read and mime types,
364
+ prompts, the `init` emitters and merges, and the plugin manifests, which
365
+ nothing else type-checks).
366
+
367
+ - **`planka_get_card` no longer returns comments by default.** They were the
368
+ biggest and least predictable part of the answer and most reads do not need
369
+ them; `withComments` brings them back, and the digest always says how many
370
+ exist (`commentsTotal`). Comments come back **oldest first**, the order the
371
+ thread was written.
372
+ - `commentCount` in `planka_get_comments` is documented and reported as what
373
+ *that response* carries, never as the card's total — Planka serves comments 50
374
+ at a time and ignores `?limit`/`?page`, so the old field was a page size
375
+ wearing a total's name.
376
+ - Errors say what to do next: a **403** names the likely cause (the agent user is
377
+ not a board member, or is a viewer where an editor is needed) and warns that
378
+ Planka answers 404 for it too; a **404** keeps Planka's own message and names
379
+ the kind of ID the path expected (`labelId`, `listId`…); a **401** that
380
+ survives the automatic retry points at `PLANKA_API_KEY` /
381
+ `PLANKA_AGENT_PASSWORD` instead of at the session; **HTML instead of JSON**
382
+ names the SPA catch-all and, on a write, warns that it MAY have landed;
383
+ **network failures** carry the base URL.
384
+ - `planka_view_attachment` on a card with no attachments says there is nothing
385
+ to view, instead of asking for an `attachmentId` and listing "none".
386
+ - `CardIncludedSchema` covers what Planka 2.2 really sends (`cardMemberships`,
387
+ `users`, `customFields`, `customFieldValues`) and the card schema tolerates
388
+ `commentsTotal`, `isDueCompleted`, `isClosed` and `stopwatch`. Enums stay open
389
+ strings on read paths.
390
+ - **Read response shapes changed** (the tool names did not): `planka_list_cards`
391
+ answers `matches` where it used to answer `cards`, `warning` (a string) became
392
+ `warnings` (an array) on `planka_find_cards`, the per-card `list` is now
393
+ `listName` and appears only when the result spans several columns, and
394
+ `planka_board_summary` no longer defaults to Spanish column and label names.
395
+ - **A `200` is no longer a success.** `planka_create_card`, `planka_update_card`,
396
+ `planka_move_card`, `planka_delete_card`, `planka_create_tasks`,
397
+ `planka_update_task`, `planka_delete_task`, `planka_manage_labels`,
398
+ `planka_add_comment`, `planka_manage_comment`, `planka_manage_lists` and
399
+ `planka_delete_attachment` now re-read before reporting.
400
+ - `planka_create_card` counts the tasks and labels **that are on the card**, not
401
+ the ones requested, always returns `card.id` (even when a follow-up step
402
+ fails, so a retry does not duplicate the card), and errors when every
403
+ requested label failed to attach.
404
+ - `planka_update_card`, `planka_update_task`, `planka_manage_labels update` and
405
+ `planka_manage_lists update` reject a request carrying only an id
406
+ (`nothing to do`) instead of sending an empty PATCH and reporting success.
407
+ - `planka_manage_lists` says in its description and in the docs that deleting a
408
+ list deletes every card in it.
409
+ - `PlankaClient.delete()` returns the response body instead of `void`: Planka
410
+ 2.x echoes the deleted item, and its parent id is what makes the confirmation
411
+ read possible.
412
+ - Label `color` is a real JSON Schema `enum` of the 43 colors instead of a
413
+ sentence in the description, so the client rejects a bad color before the
414
+ call. (Measured: 599 chars against 580 — this is a correctness win, not a
415
+ token saving.)
416
+ - Tool payloads are serialized without indentation (`JSON.stringify(x)`),
417
+ 27–46 % fewer characters on the read-heavy tools at zero information cost.
418
+ - Tool descriptions are shorter. The prose that was repeated across many of them
419
+ — that IDs are strings, that `planka_get_board` is the expensive call, the
420
+ meaning of `total`/`returned`/`hasMore` — now lives in the server
421
+ instructions, as the spec asks. The sum of all descriptions went from 3658 to
422
+ 2093 characters; no `inputSchema` changed.
423
+ - `getToolDefinitions()` now propagates `title`, `annotations` and `_meta`. It
424
+ previously mapped only name, description and schema, which would have dropped
425
+ them from `tools/list` without any error.
426
+
427
+ ### Fixed
428
+
429
+ - `planka_move_list_cards` answered 403 "Not enough rights" on any ordinary
430
+ column-to-column move. `POST /api/lists/:id/move-cards` is not the general
431
+ bulk move its name suggests: Planka 2.2.1 refuses anything but a `closed`
432
+ source into an `archive` destination, and the refusal reads like a permission
433
+ problem. The tool now falls back to one PATCH per card, keeps each card's
434
+ position, and reports which path it took plus any card that refused to move.
435
+ - Restoring a card out of the archive sent only `listId`, and Planka answered
436
+ *"Position must be present"* — an archived card has `position: null`. The
437
+ restore path now always sends a position. Caught against a real Planka 2.2.1,
438
+ not in a mock.
439
+ - `getCardsForList` derives Planka's page size from the first response instead
440
+ of assuming 50. An instance answering 20 per page used to stop on page one and
441
+ report a third of a column as the whole of it, with `truncated: false`.
442
+ - The card schema accepts `commentsTotal`, `isClosed` and `isDueCompleted`,
443
+ including the `null` Planka actually sends — reading them as booleans would
444
+ have made every board read fail.
445
+ - `planka_find_cards` no longer returns `listId` **and** `list` on every match:
446
+ a search scoped to one column names it once, at the root.
447
+ - `planka_get_card` no longer swallows a failing comments endpoint into
448
+ `comments: []`. A 403, a timeout or an HTML answer now surfaces as an error:
449
+ "this card has no comments" and "I could not read them" are different facts,
450
+ and on a board whose memory lives in the comments the difference is expensive.
451
+ - Server instructions in the `initialize` handshake
452
+ (`InitializeResult.instructions`, `src/instructions.ts`). Claude Code injects
453
+ them into the session system prompt and Codex CLI reads them alongside the
454
+ tool list, so the guidance shared by many tools is stated once: open with
455
+ `planka_board_summary` and never `planka_get_board`, IDs are strings of digits
456
+ that come from `planka_get_structure` or `planka_board_summary`, read every
457
+ comment on a card before acting, reads return digests with
458
+ `total`/`returned`/`hasMore`, writes report `verified` and `finalLabelIds`,
459
+ and Planka answers `404` where it means `403`. Budgeted at 1500-2500
460
+ characters, with the first 512 self-contained for clients that truncate.
461
+ - Tool annotations on all twenty-four tools: a display `title` plus
462
+ `readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint`, every
463
+ field written out rather than inherited. The spec defaults are pessimistic, so
464
+ without them a conformant client had to treat `planka_get_comments` as being
465
+ as destructive as `planka_delete_card`.
466
+ - The two Anthropic `_meta` keys Claude Code actually acts on:
467
+ `anthropic/requiresUserInteraction` on `planka_delete_card`,
468
+ `planka_delete_task`, `planka_delete_attachment`, `planka_manage_comment`,
469
+ `planka_manage_lists` and `planka_manage_labels`, which forces the permission
470
+ prompt even under an `allow` rule; and `anthropic/maxResultSizeChars` on
471
+ `planka_view_attachment`, so a 3 MB base64 image is not clipped.
472
+ - Tests that keep all of the above honest: every registered tool must have an
473
+ annotations entry (a new tool without one fails the suite), the tools marked
474
+ as needing confirmation must carry the `_meta` flag, the instructions must fit
475
+ their budget, and `tools/list` must return a fixed, snapshotted tool order.
476
+ - **API key authentication.** `PLANKA_API_KEY` sends `X-Api-Key` on every
477
+ request — JSON calls, multipart uploads and binary attachment downloads alike
478
+ — with no login round-trip and no token refresh. It also sidesteps Planka's
479
+ sign-in rate limit of ten logins per identity per minute, which several agents
480
+ starting at once will otherwise hit. Configuration is now API key XOR
481
+ email/password: setting both is refused rather than guessed, because Planka
482
+ reads `Authorization` first and silently ignores `x-api-key` when both arrive.
483
+ - **Access policy driven by environment variables**, in `src/config/policy.ts`:
484
+ `PLANKA_DEFAULT_BOARD_ID`, `PLANKA_ALLOWED_BOARD_IDS`,
485
+ `PLANKA_ALLOWED_PROJECT_IDS`, `PLANKA_READ_ONLY`, `PLANKA_DISABLED_TOOLS` and
486
+ `PLANKA_PROTECTED_LIST_IDS`. Allowlists are by id, never by name: a name is one
487
+ edit away from being someone else's.
488
+ - **`boardId` is now optional** on every tool that takes one, whenever
489
+ `PLANKA_DEFAULT_BOARD_ID` is configured. It is removed from the tool's
490
+ `required` list and the description says where the default comes from, so an
491
+ agent stops calling `planka_get_structure` just to recover an id that never
492
+ changes.
493
+ - **`PLANKA_READ_ONLY` and `PLANKA_DISABLED_TOOLS` hide tools and refuse them.**
494
+ Hidden tools disappear from `tools/list`, and calling one anyway returns an
495
+ error that names the variable responsible and says not to retry — hiding alone
496
+ is not enough for a client working from a cached list.
497
+ - **Allowlist filtering in `planka_get_structure`**, so an agent never sees the
498
+ boards and projects it may not touch, and a `projectId` that does not exist now
499
+ answers with the visible projects instead of an empty array.
500
+ - `server.json`: the new environment variables, plus `title`, `websiteUrl` and
501
+ `icons` for the MCP Registry listing. `PLANKA_AGENT_EMAIL` and
502
+ `PLANKA_AGENT_PASSWORD` are no longer marked required.
503
+ - README (English and Spanish): *Authentication* — including the four-step recipe
504
+ for a scoped `boardUser` with an API key — and *Scoping the server*, with the
505
+ three-layer model and the variable matrix. New entries in
506
+ `docs/troubleshooting.md` for every new error message.
507
+ - Binary attachment downloads now retry once on a 401 with a fresh token, the way
508
+ every other request already did. Before, an expired token during a download
509
+ surfaced as a bare authentication failure.
510
+ - A `200` carrying the SPA's HTML instead of JSON — what Planka answers for any
511
+ path it does not route, and what an `http://` base URL redirected to `https://`
512
+ ends up producing — is now reported as such on every request, not only during
513
+ login.
514
+
5
515
  ## [0.2.0] - 2026-08-24
6
516
 
7
517
  ### Added