@omnicoreos/planka-mcp 0.2.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 (126) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/CONTRIBUTING.md +37 -0
  3. package/CREDITS.md +39 -0
  4. package/LICENSE +22 -0
  5. package/README.es.md +213 -0
  6. package/README.md +213 -0
  7. package/dist/client.d.ts +96 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +281 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/errors.d.ts +85 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +226 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/identity.generated.d.ts +3 -0
  16. package/dist/identity.generated.d.ts.map +1 -0
  17. package/dist/identity.generated.js +4 -0
  18. package/dist/identity.generated.js.map +1 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +93 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/operations/attachments.d.ts +34 -0
  24. package/dist/operations/attachments.d.ts.map +1 -0
  25. package/dist/operations/attachments.js +89 -0
  26. package/dist/operations/attachments.js.map +1 -0
  27. package/dist/operations/board-id.d.ts +9 -0
  28. package/dist/operations/board-id.d.ts.map +1 -0
  29. package/dist/operations/board-id.js +86 -0
  30. package/dist/operations/board-id.js.map +1 -0
  31. package/dist/operations/boards.d.ts +143 -0
  32. package/dist/operations/boards.d.ts.map +1 -0
  33. package/dist/operations/boards.js +391 -0
  34. package/dist/operations/boards.js.map +1 -0
  35. package/dist/operations/cards.d.ts +36 -0
  36. package/dist/operations/cards.d.ts.map +1 -0
  37. package/dist/operations/cards.js +81 -0
  38. package/dist/operations/cards.js.map +1 -0
  39. package/dist/operations/comments.d.ts +28 -0
  40. package/dist/operations/comments.d.ts.map +1 -0
  41. package/dist/operations/comments.js +53 -0
  42. package/dist/operations/comments.js.map +1 -0
  43. package/dist/operations/labels.d.ts +60 -0
  44. package/dist/operations/labels.d.ts.map +1 -0
  45. package/dist/operations/labels.js +146 -0
  46. package/dist/operations/labels.js.map +1 -0
  47. package/dist/operations/lists.d.ts +15 -0
  48. package/dist/operations/lists.d.ts.map +1 -0
  49. package/dist/operations/lists.js +40 -0
  50. package/dist/operations/lists.js.map +1 -0
  51. package/dist/operations/projects.d.ts +24 -0
  52. package/dist/operations/projects.d.ts.map +1 -0
  53. package/dist/operations/projects.js +61 -0
  54. package/dist/operations/projects.js.map +1 -0
  55. package/dist/operations/tasks.d.ts +30 -0
  56. package/dist/operations/tasks.d.ts.map +1 -0
  57. package/dist/operations/tasks.js +111 -0
  58. package/dist/operations/tasks.js.map +1 -0
  59. package/dist/schemas/entities.d.ts +389 -0
  60. package/dist/schemas/entities.d.ts.map +1 -0
  61. package/dist/schemas/entities.js +202 -0
  62. package/dist/schemas/entities.js.map +1 -0
  63. package/dist/schemas/requests.d.ts +539 -0
  64. package/dist/schemas/requests.d.ts.map +1 -0
  65. package/dist/schemas/requests.js +187 -0
  66. package/dist/schemas/requests.js.map +1 -0
  67. package/dist/schemas/responses.d.ts +2626 -0
  68. package/dist/schemas/responses.d.ts.map +1 -0
  69. package/dist/schemas/responses.js +75 -0
  70. package/dist/schemas/responses.js.map +1 -0
  71. package/dist/tools/attachments.d.ts +270 -0
  72. package/dist/tools/attachments.d.ts.map +1 -0
  73. package/dist/tools/attachments.js +247 -0
  74. package/dist/tools/attachments.js.map +1 -0
  75. package/dist/tools/cards.d.ts +401 -0
  76. package/dist/tools/cards.d.ts.map +1 -0
  77. package/dist/tools/cards.js +368 -0
  78. package/dist/tools/cards.js.map +1 -0
  79. package/dist/tools/comments.d.ts +217 -0
  80. package/dist/tools/comments.d.ts.map +1 -0
  81. package/dist/tools/comments.js +208 -0
  82. package/dist/tools/comments.js.map +1 -0
  83. package/dist/tools/index.d.ts +1293 -0
  84. package/dist/tools/index.d.ts.map +1 -0
  85. package/dist/tools/index.js +62 -0
  86. package/dist/tools/index.js.map +1 -0
  87. package/dist/tools/labels.d.ts +214 -0
  88. package/dist/tools/labels.d.ts.map +1 -0
  89. package/dist/tools/labels.js +302 -0
  90. package/dist/tools/labels.js.map +1 -0
  91. package/dist/tools/lists.d.ts +117 -0
  92. package/dist/tools/lists.d.ts.map +1 -0
  93. package/dist/tools/lists.js +178 -0
  94. package/dist/tools/lists.js.map +1 -0
  95. package/dist/tools/navigation.d.ts +106 -0
  96. package/dist/tools/navigation.d.ts.map +1 -0
  97. package/dist/tools/navigation.js +151 -0
  98. package/dist/tools/navigation.js.map +1 -0
  99. package/dist/tools/queries.d.ts +380 -0
  100. package/dist/tools/queries.d.ts.map +1 -0
  101. package/dist/tools/queries.js +256 -0
  102. package/dist/tools/queries.js.map +1 -0
  103. package/dist/tools/tasks.d.ts +223 -0
  104. package/dist/tools/tasks.d.ts.map +1 -0
  105. package/dist/tools/tasks.js +169 -0
  106. package/dist/tools/tasks.js.map +1 -0
  107. package/docs/planka-2x-gotchas.md +192 -0
  108. package/docs/tools.md +659 -0
  109. package/docs/troubleshooting.md +138 -0
  110. package/package.json +71 -0
  111. package/project.identity.json +7 -0
  112. package/scripts/bootstrap-board.mjs +160 -0
  113. package/scripts/lib/planka-api.mjs +244 -0
  114. package/scripts/lib/workflow-template.mjs +26 -0
  115. package/scripts/setup.mjs +236 -0
  116. package/scripts/setup.sh +48 -0
  117. package/scripts/sync-identity.mjs +57 -0
  118. package/server.json +43 -0
  119. package/tests/smoke/planka-smoke.mjs +920 -0
  120. package/workflow/README.md +232 -0
  121. package/workflow/board-template.md +115 -0
  122. package/workflow/skills/planka-close-card/SKILL.md +81 -0
  123. package/workflow/skills/planka-orchestrator/SKILL.md +126 -0
  124. package/workflow/worktrees/README.md +104 -0
  125. package/workflow/worktrees/wt.conf.example +24 -0
  126. package/workflow/worktrees/wt.sh +345 -0
package/docs/tools.md ADDED
@@ -0,0 +1,659 @@
1
+ # Tools reference
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.
6
+
7
+ ## Before your first call
8
+
9
+ - 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`.
12
+ - Reading a whole board is the expensive call. When you only need one column,
13
+ one label, or the shape of the board, use a [scoped read](#scoped-reads).
14
+ - Dates use ISO 8601, for example `2026-09-01T17:00:00.000Z`.
15
+ - Omit optional fields you do not want to change. Use `null` only where the schema
16
+ explicitly allows it.
17
+ - Destructive tools really delete data. Ask for confirmation in your agent prompt
18
+ when that matters.
19
+
20
+ ## Navigation
21
+
22
+ ### `planka_get_structure`
23
+
24
+ Lists the project → board → list hierarchy visible to the agent user. Pass an
25
+ empty object to inspect everything:
26
+
27
+ ```json
28
+ {}
29
+ ```
30
+
31
+ Or restrict it to one project:
32
+
33
+ ```json
34
+ {
35
+ "projectId": "1234567890123456789"
36
+ }
37
+ ```
38
+
39
+ ### `planka_get_board`
40
+
41
+ Reads one board with its labels, lists, cards, and optional checklist counts.
42
+
43
+ ```json
44
+ {
45
+ "boardId": "2234567890123456789",
46
+ "includeTaskCounts": true
47
+ }
48
+ ```
49
+
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` |
64
+ |---|---|---|
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 |
70
+
71
+ The gap widens as the board grows, because only `planka_get_board` grows with
72
+ the number of cards.
73
+
74
+ Which one to reach for:
75
+
76
+ | Question | Tool |
77
+ |---|---|
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.
86
+
87
+ ### `planka_list_lists`
88
+
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.
91
+
92
+ ```json
93
+ {
94
+ "boardId": "2234567890123456789"
95
+ }
96
+ ```
97
+
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`.
107
+
108
+ ```json
109
+ {
110
+ "boardId": "2234567890123456789",
111
+ "cardsFrom": [
112
+ "blocked"
113
+ ],
114
+ "highlightLabel": "ready",
115
+ "limit": 50
116
+ }
117
+ ```
118
+
119
+ Every field except `boardId` is optional, and the defaults are opinionated:
120
+
121
+ | Field | Default | What the default does |
122
+ |---|---|---|
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 |
125
+ | `limit` | `50` | Caps returned cards; maximum `200` |
126
+
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.
130
+
131
+ ```json
132
+ {
133
+ "boardId": "2234567890123456789",
134
+ "cardsFrom": [
135
+ "blocked",
136
+ "needs review"
137
+ ],
138
+ "highlightLabel": "approved"
139
+ }
140
+ ```
141
+
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.
147
+
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.
151
+
152
+ ### `planka_list_cards`
153
+
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.
156
+
157
+ ```json
158
+ {
159
+ "listId": "3234567890123456789",
160
+ "limit": 25,
161
+ "offset": 0
162
+ }
163
+ ```
164
+
165
+ Filter the column by label name, or ask for description previews:
166
+
167
+ ```json
168
+ {
169
+ "listId": "3234567890123456789",
170
+ "labelFilter": "ready",
171
+ "withDescription": true,
172
+ "limit": 50
173
+ }
174
+ ```
175
+
176
+ | Field | Default | Notes |
177
+ |---|---|---|
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` |
181
+ | `limit` | `25` | Maximum `200` |
182
+ | `offset` | `0` | Pass the previous response's `nextOffset` to page |
183
+
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
+ ```
205
+
206
+ Check whether a card about something already exists, before creating a
207
+ duplicate:
208
+
209
+ ```json
210
+ {
211
+ "boardId": "2234567890123456789",
212
+ "query": "export timeout",
213
+ "withDescription": true
214
+ }
215
+ ```
216
+
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`.
219
+
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 |
228
+
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.
232
+
233
+ ## Cards
234
+
235
+ ### `planka_create_card`
236
+
237
+ Creates a card. Tasks and labels are optional follow-up writes performed by the
238
+ same tool.
239
+
240
+ ```json
241
+ {
242
+ "listId": "3234567890123456789",
243
+ "name": "Verify the export flow",
244
+ "description": "Reproduce the timeout and record the expected result.",
245
+ "tasks": [
246
+ "Create a large sample",
247
+ "Run the export",
248
+ "Record the duration"
249
+ ],
250
+ "dueDate": "2026-09-01T17:00:00.000Z",
251
+ "labelIds": [
252
+ "4234567890123456789"
253
+ ]
254
+ }
255
+ ```
256
+
257
+ Only `listId` and `name` are required. The result reports `tasksCreated`,
258
+ `labelsAttached`, and any individual `labelErrors`.
259
+
260
+ ### `planka_get_card`
261
+
262
+ Reads full card details, including tasks, comments, labels, and attachments.
263
+
264
+ ```json
265
+ {
266
+ "cardId": "5234567890123456789"
267
+ }
268
+ ```
269
+
270
+ ### `planka_update_card`
271
+
272
+ Updates only the fields present in the request.
273
+
274
+ ```json
275
+ {
276
+ "cardId": "5234567890123456789",
277
+ "name": "Verify and document the export flow",
278
+ "description": "Timeout reproduced. Add the accepted duration to the docs.",
279
+ "dueDate": null,
280
+ "isCompleted": false
281
+ }
282
+ ```
283
+
284
+ Use `null` to clear `description` or `dueDate`.
285
+
286
+ ### `planka_move_card`
287
+
288
+ Moves a card to a list and optionally assigns its numeric position.
289
+
290
+ ```json
291
+ {
292
+ "cardId": "5234567890123456789",
293
+ "listId": "6234567890123456789",
294
+ "position": 65536
295
+ }
296
+ ```
297
+
298
+ If `position` is omitted, Planka decides the final position.
299
+
300
+ ### `planka_delete_card`
301
+
302
+ Permanently deletes a card.
303
+
304
+ ```json
305
+ {
306
+ "cardId": "5234567890123456789"
307
+ }
308
+ ```
309
+
310
+ ## Checklist tasks
311
+
312
+ ### `planka_create_tasks`
313
+
314
+ Adds one or more checklist tasks to a card.
315
+
316
+ ```json
317
+ {
318
+ "cardId": "5234567890123456789",
319
+ "tasks": [
320
+ "Add a regression test",
321
+ "Update the operator notes"
322
+ ]
323
+ }
324
+ ```
325
+
326
+ ### `planka_update_task`
327
+
328
+ Renames a task, changes completion, or does both.
329
+
330
+ ```json
331
+ {
332
+ "taskId": "7234567890123456789",
333
+ "name": "Add and run a regression test",
334
+ "isCompleted": true
335
+ }
336
+ ```
337
+
338
+ ### `planka_delete_task`
339
+
340
+ Deletes one checklist task.
341
+
342
+ ```json
343
+ {
344
+ "taskId": "7234567890123456789"
345
+ }
346
+ ```
347
+
348
+ ## Labels
349
+
350
+ ### `planka_manage_labels`
351
+
352
+ Creates, updates, or deletes a board label. The `action` determines which IDs
353
+ are required.
354
+
355
+ Create:
356
+
357
+ ```json
358
+ {
359
+ "action": "create",
360
+ "boardId": "2234567890123456789",
361
+ "name": "needs-decision",
362
+ "color": "berry-red"
363
+ }
364
+ ```
365
+
366
+ Update:
367
+
368
+ ```json
369
+ {
370
+ "action": "update",
371
+ "labelId": "4234567890123456789",
372
+ "name": "decision-needed",
373
+ "color": "pumpkin-orange"
374
+ }
375
+ ```
376
+
377
+ Delete:
378
+
379
+ ```json
380
+ {
381
+ "action": "delete",
382
+ "labelId": "4234567890123456789"
383
+ }
384
+ ```
385
+
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.
389
+
390
+ ### `planka_set_card_labels`
391
+
392
+ Adds and removes labels, then re-reads the card and reports the verified final
393
+ state. `labelIds` is a supported alias for `addLabelIds`; if both are provided,
394
+ their values are merged.
395
+
396
+ ```json
397
+ {
398
+ "cardId": "5234567890123456789",
399
+ "addLabelIds": [
400
+ "4234567890123456789"
401
+ ],
402
+ "labelIds": [
403
+ "8234567890123456789"
404
+ ],
405
+ "removeLabelIds": [
406
+ "9234567890123456789"
407
+ ]
408
+ }
409
+ ```
410
+
411
+ The response distinguishes requested, added, removed, already present, already
412
+ absent, failed, and final label IDs. This makes silent no-op writes visible.
413
+
414
+ ## Comments
415
+
416
+ ### `planka_add_comment`
417
+
418
+ Adds a Markdown-capable comment through Planka 2.x's dedicated card action
419
+ endpoint.
420
+
421
+ ```json
422
+ {
423
+ "cardId": "5234567890123456789",
424
+ "text": "Verification complete: unit tests and the manual export both passed."
425
+ }
426
+ ```
427
+
428
+ ### `planka_get_comments`
429
+
430
+ Reads the dedicated comment collection for one card.
431
+
432
+ ```json
433
+ {
434
+ "cardId": "5234567890123456789"
435
+ }
436
+ ```
437
+
438
+ This does not rely on `included.comments`, which is empty in affected Planka 2.x
439
+ responses even when the card has comments.
440
+
441
+ ### `planka_manage_comment`
442
+
443
+ Edits or deletes an existing comment. Get comment IDs from
444
+ `planka_get_comments`.
445
+
446
+ Update:
447
+
448
+ ```json
449
+ {
450
+ "action": "update",
451
+ "commentId": "1034567890123456789",
452
+ "text": "Verification complete: unit tests and the manual export both passed."
453
+ }
454
+ ```
455
+
456
+ Delete:
457
+
458
+ ```json
459
+ {
460
+ "action": "delete",
461
+ "commentId": "1034567890123456789"
462
+ }
463
+ ```
464
+
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.
468
+
469
+ ## Lists
470
+
471
+ ### `planka_manage_lists`
472
+
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:
477
+
478
+ ```json
479
+ {
480
+ "action": "create",
481
+ "boardId": "2234567890123456789",
482
+ "name": "Pending",
483
+ "position": 65536,
484
+ "type": "active"
485
+ }
486
+ ```
487
+
488
+ Update:
489
+
490
+ ```json
491
+ {
492
+ "action": "update",
493
+ "listId": "3234567890123456789",
494
+ "name": "Ready",
495
+ "position": 131072,
496
+ "type": "active"
497
+ }
498
+ ```
499
+
500
+ Delete:
501
+
502
+ ```json
503
+ {
504
+ "action": "delete",
505
+ "listId": "3234567890123456789"
506
+ }
507
+ ```
508
+
509
+ Only `active` and `closed` are accepted for list writes. Internal Planka list
510
+ types such as archive and trash remain readable.
511
+
512
+ ## Attachments
513
+
514
+ Attachments carry the evidence a card needs to be understood later: a
515
+ screenshot of the bug, a before/after pair, a failing console. Prefer a code
516
+ block in a comment for stack traces, diffs, and logs; an image of text is
517
+ harder to read and impossible to search.
518
+
519
+ ### `planka_add_attachment`
520
+
521
+ Uploads a local file to a card, then re-reads the card's attachments to confirm
522
+ it landed. The response carries `verified`, and the result is flagged as an
523
+ error when the re-read does not find the upload.
524
+
525
+ ```json
526
+ {
527
+ "cardId": "5234567890123456789",
528
+ "filePath": "/home/user/screenshots/export-timeout.png",
529
+ "name": "before-export-timeout.png"
530
+ }
531
+ ```
532
+
533
+ `filePath` must be an absolute path on the machine running the MCP server.
534
+ `name` is optional and defaults to the file's basename; descriptive names such
535
+ as `before-...`, `after-...`, or `console-error-...` are worth the extra field.
536
+ The content type is derived from the extension, with `application/octet-stream`
537
+ as the fallback. A missing file returns `File not found: <path>` instead of an
538
+ opaque failure.
539
+
540
+ ### `planka_get_attachments`
541
+
542
+ Lists what a card holds: `id`, `name`, `type`, `mimeType`, `size`, download
543
+ `url`, and `createdAt`.
544
+
545
+ ```json
546
+ {
547
+ "cardId": "5234567890123456789"
548
+ }
549
+ ```
550
+
551
+ ### `planka_view_attachment`
552
+
553
+ Returns an attachment's content rather than its metadata.
554
+
555
+ ```json
556
+ {
557
+ "cardId": "5234567890123456789",
558
+ "attachmentId": "1134567890123456789"
559
+ }
560
+ ```
561
+
562
+ `attachmentId` is optional when the card has exactly one attachment. With more
563
+ than one and no ID, the tool refuses and lists the available IDs and names
564
+ instead of guessing.
565
+
566
+ What comes back depends on the content type:
567
+
568
+ | Content type | Result |
569
+ |---|---|
570
+ | `image/*`, except `image/svg+xml` | MCP image content, so a model with vision actually sees it. Limited to 3 MB; larger images return the download URL instead |
571
+ | `text/*`, `application/json`, `image/svg+xml` | Inline text, clipped at 50 KB and marked `[truncated to 50KB]` when clipped |
572
+ | Anything else | Metadata and the download URL, with a note that it cannot be rendered inline |
573
+
574
+ ### `planka_delete_attachment`
575
+
576
+ Permanently deletes one attachment. It is addressed by attachment ID alone, not
577
+ by card.
578
+
579
+ ```json
580
+ {
581
+ "attachmentId": "1134567890123456789"
582
+ }
583
+ ```
584
+
585
+ ## When arguments are wrong
586
+
587
+ Invalid input is reported as an error result naming the tool, the field, what
588
+ the schema expected, and what actually arrived. Three cases are distinguished
589
+ on purpose, because the right correction differs.
590
+
591
+ A required field that never arrived is caught before the handler runs:
592
+
593
+ ```text
594
+ Invalid arguments for planka_create_tasks: missing required field(s): tasks.
595
+ - tasks (array) — Task names to create
596
+ Required fields for planka_create_tasks: cardId, tasks
597
+ Arguments received: {"cardId":"5234567890123456789"}
598
+ ```
599
+
600
+ A field that arrived with the wrong shape or value reports the received value
601
+ alongside the field's declared type and description:
602
+
603
+ ```text
604
+ Invalid arguments for planka_find_cards:
605
+ - (whole arguments object): give at least one filter: labelName, query or listId (an unfiltered search would return the whole board) (received: {"boardId":"2234567890123456789"})
606
+ Required fields for planka_find_cards: boardId
607
+ Arguments received: {"boardId":"2234567890123456789"}
608
+ ```
609
+
610
+ A response from Planka that does not match the expected shape is labelled as
611
+ such, so it is not mistaken for a bad argument:
612
+
613
+ ```text
614
+ PLANKA returned an unexpected response shape while running planka_get_board.
615
+ This is an API/server mismatch, not a problem with the arguments:
616
+ - item.name: Expected string, received null
617
+ ```
618
+
619
+ Long values are clipped in these messages. An unknown tool name is answered with
620
+ the list of tools the server actually exposes.
621
+
622
+ ## Raw verification
623
+
624
+ Unit tests validate schemas, request construction, and error formatting without a
625
+ network: run them with `npm test`.
626
+
627
+ 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
629
+ 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.
631
+
632
+ ```bash
633
+ export PLANKA_BASE_URL="https://planka.example.com"
634
+ export PLANKA_AGENT_EMAIL="agent@example.com"
635
+ export PLANKA_AGENT_PASSWORD="<YOUR_PASSWORD>"
636
+ export PLANKA_SMOKE_BOARD_ID="2234567890123456789"
637
+ npm run test:smoke
638
+ ```
639
+
640
+ `npm run test:smoke` builds first, so it cannot silently test a stale `dist/`.
641
+
642
+ | Variable | Required | What it does |
643
+ |---|---|---|
644
+ | `PLANKA_BASE_URL` | yes | Planka origin, the same one the MCP client uses |
645
+ | `PLANKA_AGENT_EMAIL` | yes | Agent account email or username |
646
+ | `PLANKA_AGENT_PASSWORD` | yes | Agent account password |
647
+ | `PLANKA_SMOKE_BOARD_ID` | yes | Board to run against. Use a writable, non-critical board |
648
+ | `PLANKA_SMOKE_LIST_ID` | no | Column where the scratch card is created. Without it, the test picks a scratch-looking column, falling back to the first one |
649
+ | `PLANKA_SMOKE_FAIL_AFTER` | no | Injects a failure after check `<n>`, to prove that cleanup still runs. Use it to verify the safety net, not the tools |
650
+ | `PLANKA_MCP_ENTRY` | no | Server entry point. Defaults to `dist/index.js` |
651
+ | `VERBOSE` | no | Set to `1` to print each check's payload |
652
+
653
+ Everything the test creates is prefixed and deleted in a `finally`, and on
654
+ `SIGINT`/`SIGTERM` too. Cleanup goes over raw HTTP on purpose: when a signal kills
655
+ the test process, the MCP server child dies with it, so the safety net cannot
656
+ depend on the thing under test.
657
+
658
+ Exit codes: `0` all checks passed, `1` a check failed, `2` a configuration
659
+ problem.