@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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +26 -0
- package/.claude-plugin/plugin.json +45 -0
- package/.codex-plugin/mcp.json +16 -0
- package/.codex-plugin/plugin.json +27 -0
- package/.mcp.json +17 -0
- package/CHANGELOG.md +510 -0
- package/README.es.md +294 -55
- package/README.md +293 -55
- package/dist/cli/init.d.ts +101 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +481 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/client.d.ts +32 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +106 -32
- package/dist/client.js.map +1 -1
- package/dist/config/policy.d.ts +82 -0
- package/dist/config/policy.d.ts.map +1 -0
- package/dist/config/policy.js +226 -0
- package/dist/config/policy.js.map +1 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +62 -5
- package/dist/errors.js.map +1 -1
- package/dist/identity.generated.d.ts +2 -1
- package/dist/identity.generated.d.ts.map +1 -1
- package/dist/identity.generated.js +2 -1
- package/dist/identity.generated.js.map +1 -1
- package/dist/index.js +85 -10
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +21 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +37 -0
- package/dist/instructions.js.map +1 -0
- package/dist/operations/actions.d.ts +654 -0
- package/dist/operations/actions.d.ts.map +1 -0
- package/dist/operations/actions.js +154 -0
- package/dist/operations/actions.js.map +1 -0
- package/dist/operations/archive.d.ts +28 -0
- package/dist/operations/archive.d.ts.map +1 -0
- package/dist/operations/archive.js +74 -0
- package/dist/operations/archive.js.map +1 -0
- package/dist/operations/attachments.d.ts +1 -1
- package/dist/operations/attachments.d.ts.map +1 -1
- package/dist/operations/attachments.js +3 -1
- package/dist/operations/attachments.js.map +1 -1
- package/dist/operations/board-id.d.ts +1 -1
- package/dist/operations/board-id.d.ts.map +1 -1
- package/dist/operations/board-id.js +13 -7
- package/dist/operations/board-id.js.map +1 -1
- package/dist/operations/boards.d.ts +96 -19
- package/dist/operations/boards.d.ts.map +1 -1
- package/dist/operations/boards.js +377 -93
- package/dist/operations/boards.js.map +1 -1
- package/dist/operations/card-brief.d.ts +91 -0
- package/dist/operations/card-brief.d.ts.map +1 -0
- package/dist/operations/card-brief.js +79 -0
- package/dist/operations/card-brief.js.map +1 -0
- package/dist/operations/cards.d.ts +34 -9
- package/dist/operations/cards.d.ts.map +1 -1
- package/dist/operations/cards.js +60 -14
- package/dist/operations/cards.js.map +1 -1
- package/dist/operations/comments.d.ts +61 -4
- package/dist/operations/comments.d.ts.map +1 -1
- package/dist/operations/comments.js +91 -8
- package/dist/operations/comments.js.map +1 -1
- package/dist/operations/duplicate.d.ts +16 -0
- package/dist/operations/duplicate.d.ts.map +1 -0
- package/dist/operations/duplicate.js +43 -0
- package/dist/operations/duplicate.js.map +1 -0
- package/dist/operations/labels.d.ts +1 -1
- package/dist/operations/labels.d.ts.map +1 -1
- package/dist/operations/labels.js +7 -4
- package/dist/operations/labels.js.map +1 -1
- package/dist/operations/lists.d.ts +63 -1
- package/dist/operations/lists.d.ts.map +1 -1
- package/dist/operations/lists.js +97 -2
- package/dist/operations/lists.js.map +1 -1
- package/dist/operations/members.d.ts +39 -0
- package/dist/operations/members.d.ts.map +1 -0
- package/dist/operations/members.js +107 -0
- package/dist/operations/members.js.map +1 -0
- package/dist/operations/projects.d.ts +16 -0
- package/dist/operations/projects.d.ts.map +1 -1
- package/dist/operations/projects.js +54 -9
- package/dist/operations/projects.js.map +1 -1
- package/dist/operations/tasks.d.ts +1 -1
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js +5 -3
- package/dist/operations/tasks.js.map +1 -1
- package/dist/operations/users.d.ts +123 -0
- package/dist/operations/users.d.ts.map +1 -0
- package/dist/operations/users.js +180 -0
- package/dist/operations/users.js.map +1 -0
- package/dist/operations/verify.d.ts +84 -0
- package/dist/operations/verify.d.ts.map +1 -0
- package/dist/operations/verify.js +124 -0
- package/dist/operations/verify.js.map +1 -0
- package/dist/prompts.d.ts +48 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +155 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +38 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +127 -0
- package/dist/resources.js.map +1 -0
- package/dist/schemas/entities.d.ts +115 -24
- package/dist/schemas/entities.d.ts.map +1 -1
- package/dist/schemas/entities.js +48 -0
- package/dist/schemas/entities.js.map +1 -1
- package/dist/schemas/requests.d.ts +121 -46
- package/dist/schemas/requests.d.ts.map +1 -1
- package/dist/schemas/requests.js +57 -12
- package/dist/schemas/requests.js.map +1 -1
- package/dist/schemas/responses.d.ts +541 -186
- package/dist/schemas/responses.d.ts.map +1 -1
- package/dist/schemas/responses.js +13 -2
- package/dist/schemas/responses.js.map +1 -1
- package/dist/tools/activity.d.ts +150 -0
- package/dist/tools/activity.d.ts.map +1 -0
- package/dist/tools/activity.js +198 -0
- package/dist/tools/activity.js.map +1 -0
- package/dist/tools/annotations.d.ts +52 -0
- package/dist/tools/annotations.d.ts.map +1 -0
- package/dist/tools/annotations.js +214 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/attachments.d.ts +28 -4
- package/dist/tools/attachments.d.ts.map +1 -1
- package/dist/tools/attachments.js +53 -34
- package/dist/tools/attachments.js.map +1 -1
- package/dist/tools/card-ops.d.ts +232 -0
- package/dist/tools/card-ops.d.ts.map +1 -0
- package/dist/tools/card-ops.js +333 -0
- package/dist/tools/card-ops.js.map +1 -0
- package/dist/tools/cards.d.ts +90 -8
- package/dist/tools/cards.d.ts.map +1 -1
- package/dist/tools/cards.js +411 -128
- package/dist/tools/cards.js.map +1 -1
- package/dist/tools/comments.d.ts +226 -22
- package/dist/tools/comments.d.ts.map +1 -1
- package/dist/tools/comments.js +163 -134
- package/dist/tools/comments.js.map +1 -1
- package/dist/tools/dispatch.d.ts +47 -0
- package/dist/tools/dispatch.d.ts.map +1 -0
- package/dist/tools/dispatch.js +63 -0
- package/dist/tools/dispatch.js.map +1 -0
- package/dist/tools/guard.d.ts +9 -0
- package/dist/tools/guard.d.ts.map +1 -0
- package/dist/tools/guard.js +20 -0
- package/dist/tools/guard.js.map +1 -0
- package/dist/tools/index.d.ts +748 -450
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +136 -17
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/labels.d.ts +213 -18
- package/dist/tools/labels.d.ts.map +1 -1
- package/dist/tools/labels.js +218 -203
- package/dist/tools/labels.js.map +1 -1
- package/dist/tools/lists.d.ts +222 -15
- package/dist/tools/lists.d.ts.map +1 -1
- package/dist/tools/lists.js +175 -156
- package/dist/tools/lists.js.map +1 -1
- package/dist/tools/members.d.ts +128 -0
- package/dist/tools/members.d.ts.map +1 -0
- package/dist/tools/members.js +150 -0
- package/dist/tools/members.js.map +1 -0
- package/dist/tools/navigation.d.ts +22 -2
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/navigation.js +60 -15
- package/dist/tools/navigation.js.map +1 -1
- package/dist/tools/queries.d.ts +196 -166
- package/dist/tools/queries.d.ts.map +1 -1
- package/dist/tools/queries.js +125 -155
- package/dist/tools/queries.js.map +1 -1
- package/dist/tools/tasks.d.ts +26 -6
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +110 -55
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/users.d.ts +130 -0
- package/dist/tools/users.d.ts.map +1 -0
- package/dist/tools/users.js +165 -0
- package/dist/tools/users.js.map +1 -0
- package/docs/planka-2x-gotchas.md +121 -5
- package/docs/tools.md +771 -187
- package/docs/troubleshooting.md +137 -5
- package/hooks/hooks.json +15 -0
- package/hooks/preflight.mjs +100 -0
- package/package.json +6 -1
- package/scripts/setup.sh +8 -26
- package/scripts/sync-identity.mjs +55 -1
- package/server.json +87 -6
- package/tests/smoke/planka-smoke.mjs +512 -72
- package/workflow/skills/planka-close-card/SKILL.md +18 -5
- package/workflow/skills/planka-orchestrator/SKILL.md +36 -7
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/hooks/hooks.json
ADDED
|
@@ -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.
|
|
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/
|
|
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.
|
|
20
|
+
"version": "0.3.0",
|
|
10
21
|
"packages": [
|
|
11
22
|
{
|
|
12
23
|
"registryType": "npm",
|
|
13
24
|
"identifier": "@omnicoreos/planka-mcp",
|
|
14
|
-
"version": "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
|
|
29
|
-
"isRequired":
|
|
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
|
|
36
|
-
"isRequired":
|
|
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
|
}
|