@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
@@ -8,7 +8,7 @@ the fix.
8
8
  **Cause:** the old MCP reads `included.comments` from the card response. Planka 2.x
9
9
  does not populate it, so HTTP succeeds with zero items.
10
10
 
11
- **Fix:** make sure Claude Code runs this fork, restart Claude Code, and call
11
+ **Fix:** make sure Claude Code runs planka-mcp, restart Claude Code, and call
12
12
  `planka_get_comments` again. Run the opt-in smoke test if the result is still
13
13
  suspicious; it compares MCP output to `GET /api/cards/:id/comments`.
14
14
 
@@ -16,7 +16,7 @@ suspicious; it compares MCP output to `GET /api/cards/:id/comments`.
16
16
 
17
17
  **Cause:** the old tool schema knows `addLabelIds` but silently drops `labelIds`.
18
18
 
19
- **Fix:** use this fork. It accepts both names and re-reads the card. Treat any result
19
+ **Fix:** use planka-mcp. It accepts both names and re-reads the card. Treat any result
20
20
  with a non-empty `failed` array as an error.
21
21
 
22
22
  ## “Removing a label returns 404, or the label remains”
@@ -24,7 +24,7 @@ with a non-empty `failed` array as an error.
24
24
  **Cause:** Planka 2.x deletes by `labelId:<labelId>` criteria, not by the ID of the
25
25
  card-label junction record.
26
26
 
27
- **Fix:** upgrade to this fork and pass the board label ID in `removeLabelIds`:
27
+ **Fix:** upgrade to planka-mcp and pass the board label ID in `removeLabelIds`:
28
28
 
29
29
  ```json
30
30
  {
@@ -38,7 +38,7 @@ card-label junction record.
38
38
  **Cause:** the old response schema has a closed color enum. One newer Planka color
39
39
  causes Zod to reject the entire board.
40
40
 
41
- **Fix:** use this fork. Read paths accept backend color strings. Write paths still
41
+ **Fix:** use planka-mcp. Read paths accept backend color strings. Write paths still
42
42
  validate known colors.
43
43
 
44
44
  ## “My credential is expired,” but the password works in the browser
@@ -80,7 +80,7 @@ with a body containing `userId` and `role: "editor"`.
80
80
  **Cause:** Planka 2.x requires `type` as well as `name` and `position`. Older MCP
81
81
  builds omit it.
82
82
 
83
- **Fix:** use this fork. `planka_manage_lists` sends `type: "active"` by default and
83
+ **Fix:** use planka-mcp. `planka_create_list` sends `type: "active"` by default and
84
84
  also accepts `closed`.
85
85
 
86
86
  ## “The MCP tools do not appear in Claude Code”
@@ -136,3 +136,135 @@ an invalid JSON token response even though both can look like HTML to a client.
136
136
 
137
137
  **Fix:** restore the Planka service, verify the final HTTPS URL in a browser, and
138
138
  rerun setup. The setup script does not write configuration after failed validation.
139
+
140
+ ## “Conflicting Planka credentials”
141
+
142
+ `PLANKA_API_KEY` is set **and** so is `PLANKA_AGENT_EMAIL` / `PLANKA_AGENT_PASSWORD`.
143
+ This is refused on purpose rather than guessed: Planka reads `Authorization`
144
+ first and ignores `x-api-key` whenever both headers arrive, so a stale password
145
+ would keep working and the key you thought was in use would be dead weight.
146
+
147
+ Pick one. Keep `PLANKA_API_KEY` and delete the email/password pair from the
148
+ client configuration — that is the whole point of moving to a key.
149
+
150
+ ## “Missing Planka credentials”
151
+
152
+ The server has a `PLANKA_BASE_URL` but nothing to authenticate with. Set either
153
+ `PLANKA_API_KEY` (see *Authentication* in the README) **or** both
154
+ `PLANKA_AGENT_EMAIL` and `PLANKA_AGENT_PASSWORD`.
155
+
156
+ If you meant to use a key: it can only be issued by a Planka **admin**, with
157
+ `POST /api/users/<USER_ID>/api-key`, and the plaintext value appears exactly
158
+ once in `included.apiKey` of that response. There is no way to read it back
159
+ afterwards — Planka stores only the hash and the prefix. Issue a new one.
160
+
161
+ ## “boardId X is not in PLANKA_ALLOWED_BOARD_IDS”
162
+
163
+ The board allowlist is doing its job. Either the agent picked the wrong id, or
164
+ the board legitimately belongs in scope and the operator should add it to
165
+ `PLANKA_ALLOWED_BOARD_IDS` (ids, comma-separated — names are not accepted, since
166
+ any editor can rename a board).
167
+
168
+ Remember the allowlist also filters `planka_get_structure`: if a board is missing
169
+ from the structure, it is either outside the allowlist or outside the Planka
170
+ user's board memberships. The second one is checked by Planka and answers 404.
171
+
172
+ ## “boardId is required … PLANKA_DEFAULT_BOARD_ID is not set”
173
+
174
+ A tool was called without `boardId` and there is no default configured. Either
175
+ pass the id, or set `PLANKA_DEFAULT_BOARD_ID` — with it, `boardId` disappears
176
+ from every tool's `required` list and the default is used whenever the argument
177
+ is omitted.
178
+
179
+ ## “Tool … is disabled / this server runs in read-only mode”
180
+
181
+ `PLANKA_DISABLED_TOOLS` or `PLANKA_READ_ONLY=true` switched that tool off. The
182
+ tool is also absent from `tools/list`, so an agent hitting this error is usually
183
+ working from a cached tool list or from a skill that names the tool by hand:
184
+ restarting the client resyncs the list.
185
+
186
+ Do not retry the call — it will be refused identically. Either the operator
187
+ re-enables the tool, or the change gets reported to a human instead of attempted.
188
+
189
+ ## “list … is protected by PLANKA_PROTECTED_LIST_IDS”
190
+
191
+ A card was about to be created in, or moved into, a column the operator marked as
192
+ protected — or the column itself was about to be edited or deleted. This is a
193
+ client-side guard-rail, not a Planka permission (Planka has no per-list rights):
194
+ it exists so an agent does not close its own work. A human can still do it in the
195
+ Planka UI.
196
+
197
+ ## “No project with id … is visible to this account”
198
+
199
+ `planka_get_structure` was given a `projectId` that this account cannot see. The
200
+ error lists the projects that *are* visible. Two causes, in order of likelihood:
201
+ the id belongs to a board rather than a project, or the Planka user has no board
202
+ membership inside that project. `PLANKA_ALLOWED_PROJECT_IDS` produces a different
203
+ message that names the variable.
204
+
205
+ ## “`planka_get_card` no longer returns the comments”
206
+
207
+ Not a bug: comments are **off by default** since the card read became a digest.
208
+ They were the biggest and least predictable part of the answer, and most reads do
209
+ not need them.
210
+
211
+ Ask for them: `withComments: 10` for the ten most recent, `withComments: "all"`
212
+ to page through every one. The digest still tells you how many exist —
213
+ `commentsTotal`, Planka's own counter — and the placeholder in the `comments`
214
+ field says exactly this.
215
+
216
+ Same story for the description: the digest carries `descriptionPreview` (200
217
+ chars, with `descriptionChars` and `descriptionTruncated`). `detail: "full"`
218
+ returns the whole record — description, task items, attachments, custom fields.
219
+
220
+ ## “The comment count does not match what I see in Planka”
221
+
222
+ `planka_get_comments` returns **one page**. Planka serves comments 50 at a time
223
+ and ignores `?limit`/`?page`, so `commentCount` is what that response carries,
224
+ never the card's total.
225
+
226
+ - the card's real total: `commentsTotal` from `planka_get_card`;
227
+ - more comments to read: `hasMore: true` and a `nextBeforeId` to pass back as
228
+ `beforeId`;
229
+ - everything at once: `all: true`, which reports `truncated: true` if the card
230
+ has more than 10 pages (500 comments).
231
+
232
+ ## “Forbidden: the agent user is probably not a member of this board”
233
+
234
+ A 403 from Planka on a card, list, or label that reads fine in the browser. The
235
+ agent user is a different user: either it is not a member of that board at all,
236
+ or it is a **viewer** where an **editor** is needed.
237
+
238
+ Fix it in Planka: open the board, add the agent user to the board members, and
239
+ give it the editor role. Note that Planka answers **404** for this same cause
240
+ often enough that a “not found” on an ID that worked a moment ago should be read
241
+ as a permission problem first.
242
+
243
+ ## “Resource not found”, on an ID that exists
244
+
245
+ The 404 now says which kind of ID the path expected (`labelId`, `listId`,
246
+ `cardId`…). Three causes, in order:
247
+
248
+ 1. **the wrong kind of ID** — they are all strings of digits, so a `labelId`
249
+ passed as a `listId` looks perfectly valid until the request goes out. Get the
250
+ right one from `planka_board_summary` (labels and lists, with their IDs);
251
+ 2. **no permission** — see the entry above; Planka disguises 403 as 404;
252
+ 3. the entity really is gone.
253
+
254
+ ## “Returned HTML instead of JSON: this route does not exist in Planka 2.x”
255
+
256
+ Every unknown `GET /api/...` falls through to the single-page app, which answers
257
+ **200 with HTML**. The client detects it instead of parsing garbage.
258
+
259
+ If it happened on a **write**, the message says so: the write may have landed.
260
+ Re-read the entity before retrying, or you risk a duplicate. If it happened on a
261
+ read, either the route genuinely does not exist in Planka 2.x (see
262
+ `docs/planka-2x-gotchas.md`), or `PLANKA_BASE_URL` points at a proxy or an
263
+ `http://` URL that redirects.
264
+
265
+ ## “Network error … could not reach Planka at `<url>`”
266
+
267
+ The base URL is now part of the message, so the first check is whether that URL
268
+ is the one you meant. Then: DNS, VPN/Tailscale, and whether the instance is up.
269
+ A **POST** that fails this way may still have been received — re-read before
270
+ retrying an upload.
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/preflight.mjs\"",
9
+ "timeout": 10
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart preflight for the Planka plugin.
4
+ *
5
+ * Cheap on purpose: it answers "is this server going to be able to start?"
6
+ * without touching the network and without spawning the server, so it never
7
+ * adds latency to a session. It checks the two things that actually break the
8
+ * install — a Node too old for the package, and missing/contradictory
9
+ * credentials — and stays silent when everything is in place.
10
+ *
11
+ * Set PLANKA_MCP_PREFLIGHT=full to also spawn the server binary and wait for
12
+ * its stdio banner (adds an npx download on a cold cache; opt-in only).
13
+ *
14
+ * It NEVER exits non-zero: a broken preflight must not block a session.
15
+ */
16
+ import { spawn } from "node:child_process";
17
+
18
+ const option = (key) => {
19
+ const raw =
20
+ process.env[`CLAUDE_PLUGIN_OPTION_${key.toUpperCase()}`] ??
21
+ process.env[key.toUpperCase()];
22
+ const value = typeof raw === "string" ? raw.trim() : "";
23
+ return value || undefined;
24
+ };
25
+
26
+ const notes = [];
27
+
28
+ const major = Number(process.versions.node.split(".")[0]);
29
+ if (Number.isFinite(major) && major < 18) {
30
+ notes.push(
31
+ `Node ${process.versions.node} is too old for @omnicoreos/planka-mcp (needs >= 18).`
32
+ );
33
+ }
34
+
35
+ const baseUrl = option("PLANKA_BASE_URL");
36
+ const apiKey = option("PLANKA_API_KEY");
37
+ const email = option("PLANKA_AGENT_EMAIL");
38
+ const password = option("PLANKA_AGENT_PASSWORD");
39
+
40
+ if (!baseUrl) {
41
+ notes.push(
42
+ "PLANKA_BASE_URL is not configured: run /plugin and set the plugin's Planka base URL, or export PLANKA_BASE_URL."
43
+ );
44
+ } else if (baseUrl.startsWith("http://")) {
45
+ notes.push(
46
+ `PLANKA_BASE_URL is http:// (${baseUrl}). A redirect to https:// drops POST bodies — configure the final https:// URL.`
47
+ );
48
+ }
49
+
50
+ if (!apiKey && !(email && password)) {
51
+ notes.push(
52
+ "No Planka credentials: set the plugin's API key (recommended), or export PLANKA_AGENT_EMAIL + PLANKA_AGENT_PASSWORD."
53
+ );
54
+ } else if (apiKey && (email || password)) {
55
+ notes.push(
56
+ "Both PLANKA_API_KEY and email/password are set. The server refuses that combination — keep the API key and unset the pair."
57
+ );
58
+ }
59
+
60
+ async function spawnCheck() {
61
+ return new Promise((resolve) => {
62
+ const child = spawn(
63
+ process.env.PLANKA_MCP_COMMAND || "npx",
64
+ process.env.PLANKA_MCP_COMMAND ? [] : ["-y", "@omnicoreos/planka-mcp"],
65
+ { stdio: ["ignore", "ignore", "pipe"] }
66
+ );
67
+ const timer = setTimeout(() => {
68
+ child.kill();
69
+ resolve("timed out before printing its startup banner");
70
+ }, 20000);
71
+ let stderr = "";
72
+ child.stderr.on("data", (chunk) => {
73
+ stderr += String(chunk);
74
+ if (stderr.includes("PLANKA MCP server started")) {
75
+ clearTimeout(timer);
76
+ child.kill();
77
+ resolve(null);
78
+ }
79
+ });
80
+ child.on("error", (error) => {
81
+ clearTimeout(timer);
82
+ resolve(`could not be spawned: ${error.message}`);
83
+ });
84
+ child.on("exit", () => {
85
+ clearTimeout(timer);
86
+ resolve(stderr.includes("PLANKA MCP server started") ? null : "exited without starting");
87
+ });
88
+ });
89
+ }
90
+
91
+ if (process.env.PLANKA_MCP_PREFLIGHT === "full") {
92
+ const failure = await spawnCheck();
93
+ if (failure) notes.push(`The Planka MCP server ${failure}.`);
94
+ }
95
+
96
+ if (notes.length > 0) {
97
+ console.log(
98
+ `Planka MCP preflight found ${notes.length} problem(s):\n- ${notes.join("\n- ")}`
99
+ );
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnicoreos/planka-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A Planka 2.x MCP server with verified writes and an optional agent workflow",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,8 +12,13 @@
12
12
  "access": "public"
13
13
  },
14
14
  "files": [
15
+ ".agents",
16
+ ".claude-plugin",
17
+ ".codex-plugin",
18
+ ".mcp.json",
15
19
  "dist",
16
20
  "docs",
21
+ "hooks",
17
22
  "scripts",
18
23
  "tests/smoke",
19
24
  "workflow",
package/scripts/setup.sh CHANGED
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env bash
2
+ #
3
+ # Thin alias for `planka-mcp init --client claude`, kept because the README has
4
+ # been pointing at it since 0.1. Everything it used to do by hand now lives in
5
+ # the published binary, so a clone and an npx install take the same path.
6
+ #
7
+ # The interactive board picker and the full write smoke still live in
8
+ # scripts/setup.mjs: run `node scripts/setup.mjs` for those.
2
9
 
3
10
  set -euo pipefail
4
11
 
@@ -15,34 +22,9 @@ if [[ "$NODE_MAJOR" -lt 18 ]]; then
15
22
  exit 1
16
23
  fi
17
24
 
18
- echo "Planka MCP setup"
19
- echo
20
-
21
- DEFAULT_URL="${PLANKA_BASE_URL:-https://planka.example.com}"
22
- read -r -p "Planka base URL [$DEFAULT_URL]: " INPUT_URL
23
- export PLANKA_BASE_URL="${INPUT_URL:-$DEFAULT_URL}"
24
-
25
- DEFAULT_EMAIL="${PLANKA_AGENT_EMAIL:-}"
26
- if [[ -n "$DEFAULT_EMAIL" ]]; then
27
- read -r -p "Agent email or username [$DEFAULT_EMAIL]: " INPUT_EMAIL
28
- export PLANKA_AGENT_EMAIL="${INPUT_EMAIL:-$DEFAULT_EMAIL}"
29
- else
30
- read -r -p "Agent email or username: " PLANKA_AGENT_EMAIL
31
- export PLANKA_AGENT_EMAIL
32
- fi
33
-
34
- if [[ -z "${PLANKA_AGENT_PASSWORD:-}" ]]; then
35
- read -r -s -p "Agent password: " PLANKA_AGENT_PASSWORD
36
- echo
37
- export PLANKA_AGENT_PASSWORD
38
- else
39
- echo "Using PLANKA_AGENT_PASSWORD from the environment."
40
- fi
41
-
42
25
  if [[ ! -f "$ROOT_DIR/dist/index.js" ]]; then
43
- echo
44
26
  echo "Building the MCP server..."
45
27
  (cd "$ROOT_DIR" && npm ci && npm run build)
46
28
  fi
47
29
 
48
- exec node "$ROOT_DIR/scripts/setup.mjs"
30
+ exec node "$ROOT_DIR/dist/index.js" init --client claude "$@"
@@ -26,12 +26,66 @@ serverJson.version = packageJson.version;
26
26
  serverJson.packages[0].identifier = identity.packageName;
27
27
  serverJson.packages[0].version = packageJson.version;
28
28
 
29
- const generatedIdentity = `// Generated by scripts/sync-identity.mjs. Edit project.identity.json instead.\nexport const SERVER_NAME = ${JSON.stringify(identity.mcpServerName)};\nexport const SERVER_VERSION = ${JSON.stringify(packageJson.version)};\n`;
29
+ const generatedIdentity = `// Generated by scripts/sync-identity.mjs. Edit project.identity.json instead.\nexport const SERVER_NAME = ${JSON.stringify(identity.mcpServerName)};\nexport const PACKAGE_NAME = ${JSON.stringify(identity.packageName)};\nexport const SERVER_VERSION = ${JSON.stringify(packageJson.version)};\n`;
30
+
31
+ // The plugin manifests pin the published package by exact version. A pin that
32
+ // drifts installs the OLD server from a NEW plugin, silently — so the pin is
33
+ // generated here and `npm run check:identity` fails the build when it is stale.
34
+ const pinnedArgs = ["-y", `${identity.packageName}@${packageJson.version}`];
35
+
36
+ const mcpJson = await readJson(".mcp.json");
37
+ mcpJson.mcpServers.planka.args = pinnedArgs;
38
+
39
+ const codexMcpJson = await readJson(".codex-plugin/mcp.json");
40
+ codexMcpJson.mcp_servers.planka.args = pinnedArgs;
41
+
42
+ const claudePlugin = await readJson(".claude-plugin/plugin.json");
43
+ claudePlugin.version = packageJson.version;
44
+ claudePlugin.homepage = `${identity.repositoryUrl}#readme`;
45
+ claudePlugin.repository = identity.repositoryUrl;
46
+
47
+ const codexPlugin = await readJson(".codex-plugin/plugin.json");
48
+ codexPlugin.version = packageJson.version;
49
+ codexPlugin.homepage = `${identity.repositoryUrl}#readme`;
50
+ codexPlugin.repository = identity.repositoryUrl;
51
+
52
+ const marketplace = await readJson(".claude-plugin/marketplace.json");
53
+ marketplace.plugins[0].version = packageJson.version;
54
+ marketplace.plugins[0].homepage = `${identity.repositoryUrl}#readme`;
55
+ marketplace.plugins[0].repository = identity.repositoryUrl;
56
+
57
+ // The two skills the plugin installs carry their own `metadata.version`. They
58
+ // are not JSON, so they get a targeted rewrite of that one line: left out of
59
+ // this map they went stale on every release and `check:identity` never saw it.
60
+ const skillFiles = [
61
+ "workflow/skills/planka-close-card/SKILL.md",
62
+ "workflow/skills/planka-orchestrator/SKILL.md",
63
+ ];
64
+
65
+ const skillOutputs = await Promise.all(
66
+ skillFiles.map(async (name) => {
67
+ const current = await readFile(resolve(root, name), "utf8");
68
+ const updated = current.replace(
69
+ /^(\s*version:\s*).*$/m,
70
+ `$1${packageJson.version}`
71
+ );
72
+ if (updated === current && !current.includes(`version: ${packageJson.version}`)) {
73
+ throw new Error(`${name} has no metadata.version line to synchronize`);
74
+ }
75
+ return [name, updated];
76
+ })
77
+ );
30
78
 
31
79
  const outputs = new Map([
32
80
  ["package.json", `${JSON.stringify(packageJson, null, 2)}\n`],
33
81
  ["server.json", `${JSON.stringify(serverJson, null, 2)}\n`],
34
82
  ["src/identity.generated.ts", generatedIdentity],
83
+ [".mcp.json", `${JSON.stringify(mcpJson, null, 2)}\n`],
84
+ [".codex-plugin/mcp.json", `${JSON.stringify(codexMcpJson, null, 2)}\n`],
85
+ [".claude-plugin/plugin.json", `${JSON.stringify(claudePlugin, null, 2)}\n`],
86
+ [".codex-plugin/plugin.json", `${JSON.stringify(codexPlugin, null, 2)}\n`],
87
+ [".claude-plugin/marketplace.json", `${JSON.stringify(marketplace, null, 2)}\n`],
88
+ ...skillOutputs,
35
89
  ]);
36
90
 
37
91
  let stale = false;
package/server.json CHANGED
@@ -1,17 +1,28 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.omnicoreos/planka-mcp",
4
+ "title": "Planka MCP",
4
5
  "description": "Planka 2.x MCP server with verified card, task, comment, label, and list operations.",
6
+ "websiteUrl": "https://github.com/omnicoreos/planka-mcp",
7
+ "icons": [
8
+ {
9
+ "src": "https://avatars.githubusercontent.com/u/265942793?v=4",
10
+ "mimeType": "image/png",
11
+ "sizes": [
12
+ "460x460"
13
+ ]
14
+ }
15
+ ],
5
16
  "repository": {
6
17
  "url": "https://github.com/omnicoreos/planka-mcp",
7
18
  "source": "github"
8
19
  },
9
- "version": "0.2.0",
20
+ "version": "0.3.0",
10
21
  "packages": [
11
22
  {
12
23
  "registryType": "npm",
13
24
  "identifier": "@omnicoreos/planka-mcp",
14
- "version": "0.2.0",
25
+ "version": "0.3.0",
15
26
  "transport": {
16
27
  "type": "stdio"
17
28
  },
@@ -23,19 +34,89 @@
23
34
  "format": "string",
24
35
  "isSecret": false
25
36
  },
37
+ {
38
+ "name": "PLANKA_API_KEY",
39
+ "description": "Planka API key (X-Api-Key). Recommended: replaces the email/password login",
40
+ "isRequired": false,
41
+ "format": "string",
42
+ "isSecret": true
43
+ },
26
44
  {
27
45
  "name": "PLANKA_AGENT_EMAIL",
28
- "description": "Email address or username for the Planka agent user",
29
- "isRequired": true,
46
+ "description": "Email or username of the Planka agent user. Only needed when PLANKA_API_KEY is not set",
47
+ "isRequired": false,
30
48
  "format": "string",
31
49
  "isSecret": false
32
50
  },
33
51
  {
34
52
  "name": "PLANKA_AGENT_PASSWORD",
35
- "description": "Password for the Planka agent user",
36
- "isRequired": true,
53
+ "description": "Password of the Planka agent user. Only needed when PLANKA_API_KEY is not set",
54
+ "isRequired": false,
37
55
  "format": "string",
38
56
  "isSecret": true
57
+ },
58
+ {
59
+ "name": "PLANKA_DEFAULT_BOARD_ID",
60
+ "description": "Board id used when a tool call omits boardId",
61
+ "isRequired": false,
62
+ "format": "string",
63
+ "isSecret": false
64
+ },
65
+ {
66
+ "name": "PLANKA_ALLOWED_BOARD_IDS",
67
+ "description": "Comma-separated board ids this server may see and touch. Everything else is hidden",
68
+ "isRequired": false,
69
+ "format": "string",
70
+ "isSecret": false
71
+ },
72
+ {
73
+ "name": "PLANKA_ALLOWED_PROJECT_IDS",
74
+ "description": "Comma-separated project ids this server may see. Everything else is hidden",
75
+ "isRequired": false,
76
+ "format": "string",
77
+ "isSecret": false
78
+ },
79
+ {
80
+ "name": "PLANKA_READ_ONLY",
81
+ "description": "Set to true to hide and refuse every write tool",
82
+ "isRequired": false,
83
+ "format": "string",
84
+ "isSecret": false
85
+ },
86
+ {
87
+ "name": "PLANKA_DISABLED_TOOLS",
88
+ "description": "Comma-separated tool names to switch off, for example planka_get_board",
89
+ "isRequired": false,
90
+ "format": "string",
91
+ "isSecret": false
92
+ },
93
+ {
94
+ "name": "PLANKA_PROTECTED_LIST_IDS",
95
+ "description": "Comma-separated list ids that must not receive cards nor be edited or deleted",
96
+ "isRequired": false,
97
+ "format": "string",
98
+ "isSecret": false
99
+ },
100
+ {
101
+ "name": "PLANKA_HIDE_DEPRECATED",
102
+ "description": "Set to true to drop the deprecated tool aliases from tools/list. They stay callable",
103
+ "isRequired": false,
104
+ "format": "string",
105
+ "isSecret": false
106
+ },
107
+ {
108
+ "name": "PLANKA_SUMMARY_HIGHLIGHT_LABEL",
109
+ "description": "Label planka_board_summary flags as unblocked/ready. Unset: no highlighting",
110
+ "isRequired": false,
111
+ "format": "string",
112
+ "isSecret": false
113
+ },
114
+ {
115
+ "name": "PLANKA_SUMMARY_DECISION_LISTS",
116
+ "description": "Comma-separated columns planka_board_summary returns cards from when the call does not say",
117
+ "isRequired": false,
118
+ "format": "string",
119
+ "isSecret": false
39
120
  }
40
121
  ]
41
122
  }