Skip to content

Fix: Planka v2 (2.x) API route compatibility - #10

Open
cameronsjo wants to merge 18 commits into
bradrisse:mainfrom
cameronsjo:planka-v2.1-compat
Open

Fix: Planka v2 (2.x) API route compatibility#10
cameronsjo wants to merge 18 commits into
bradrisse:mainfrom
cameronsjo:planka-v2.1-compat

Conversation

@cameronsjo

Copy link
Copy Markdown

What

Updates the Planka API calls to be compatible with Planka v2.x (verified against 2.1.1). Several operations targeted pre-v2 routes and return 404/parse errors on current Planka. Card create/move/list/delete already matched and are unchanged.

Route changes

Operation Before After
Create comment POST /api/cards/:id/comment-actions POST /api/cards/:id/comments
List comments GET /api/cards/:id/actions (filter type==commentCard) GET /api/cards/:id/comments
Update / delete comment PATCH/DELETE /api/comment-actions/:id PATCH/DELETE /api/comments/:id
Add board member POST /api/boards/:id/memberships POST /api/boards/:id/board-memberships
List board members GET /api/boards/:id/memberships board detail included.boardMemberships (no list route in v2)
Add label to card POST /api/cards/:id/labels POST /api/cards/:id/card-labels
Remove label from card DELETE /api/cards/:id/labels/:labelId DELETE /api/cards/:id/card-labels/labelId::labelId
Create task POST /api/cards/:id/tasks POST /api/task-lists/:taskListId/tasks (auto-ensures a task list on the card)

Comments also became first-class in v2: the response carries text at the top level rather than a commentCard action with data.text. The comment schema and the card-details tool's heuristic were updated accordingly.

Verification

Each changed route was exercised live as a board member against Planka 2.1.1 — all returned 2xx:

comments: create / list / update / delete
card-labels: add / remove
task-lists: create   tasks: create
board-memberships: create

npm run build is clean (no tsc errors).

Notes

  • tasks are now nested under task lists in Planka v2; createTask(cardId, ...) keeps its signature and transparently reuses the card's first task list (creating one named "Tasks" if none exists).
  • The labelId::labelId form in the card-label delete route is Planka's literal path-param syntax, so the request path is /api/cards/:id/card-labels/labelId:<labelId>.

🤖 Generated with Claude Code

cameronsjo and others added 18 commits June 20, 2026 11:14
kanban-mcp targeted an older Planka. Updated to v2 routes, all verified live:
- comments: /comment-actions -> /comments (+ first-class {text}, not commentCard data.text); GET /actions -> /comments; PATCH/DELETE /comment-actions/:id -> /comments/:id
- board memberships: POST /boards/:id/memberships -> /board-memberships; list now reads board detail included.boardMemberships (no list route in v2)
- card labels: POST /cards/:id/labels -> /card-labels; DELETE -> /card-labels/labelId::id
- tasks: now nested under task lists (ensureTaskListId -> POST /task-lists/:id/tasks)
- card-details tool: read comment.text (v2) not comment.data.text
Two read/aggregate paths broke against Planka 2.1.1's null fields:

- get_board_summary threw "Cannot read properties of null (reading 'toLowerCase')". Planka 2.1.1 auto-creates archive/trash system lists with name: null, which flow unvalidated through getLists() into the list-name comparisons. Guard each with optional chaining. The identical pattern in workflow-actions (start_working/move_to_testing/move_to_done) had the same latent crash on those system lists, so guard it there too.

- get_projects failed zod validation (items[].background "Required"). Planka 2.1.1 omits the project background field entirely when none is set, so make PlankaProjectSchema.background optional as well as nullable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single Jest config ran .jest/setEnvVars.js for every test, which
process.exit(1)'d when localhost:3333 was unreachable — no test could run
without live Planka. Split it so unit tests run anywhere (CI included):

- jest.config.js is now the unit default: testMatch tests/unit, setup
  .jest/unit.setup.js (dummy env only, no connectivity check, no exit).
- jest.integration.config.js keeps the live-Planka precheck, now in
  .jest/integration.setup.js — a failed check throws a readable error
  instead of process.exit(1) killing the reporter. Env-overridable URL/creds.
- Move tests/integration.test.ts -> tests/integration/ (import depth fixed).
- package.json: test -> unit (CI-safe default), plus test:unit,
  test:integration, test:watch, test:coverage (closes Developer-Guide drift).
  Coverage scoped to common/operations/tools.
- tests/unit/spike.test.ts derisks the ts-jest + ESM + global-fetch-spy path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 tests, no live Planka needed (global fetch is spied via jest.spyOn):

- request-shapes.test.ts — the regression lock for commit 764821a: asserts
  exact path/method/body for comments (/api/cards/:id/comments,
  /api/comments/:id), card-labels (literal labelId: prefix on DELETE),
  board-memberships (nested create, board-detail list, flat by-id), and
  tasks (ensureTaskListId reuse-or-create, then /api/task-lists/:id/tasks).
- utils.test.ts — buildUrl, validate*Name, plankaRequest base-URL
  normalization, /api/ prefixing, Bearer auth, FormData content-type removal,
  non-ok error throw.
- errors.test.ts — createPlankaError 401/403/404/409/422/429/default,
  isPlankaError, 429 resetAt with and without reset_at.
- board-summary.test.ts — getNextActionSuggestion branches (now exported).
- tests/unit/helpers.ts — mockFetch(responder) routing /api/access-tokens so
  auth resolves; route assertions compare URL pathname (host-independent).

.gitignore: coverage/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docker-compose.yml: pin Planka image :latest -> :2.1.1 for reproducible
  integration runs (the version 764821a's route work targets).
- scripts/wait-for-planka.sh: poll /api/users until 401 (API up).
- scripts/accept-planka-terms.sh: walk Planka 2.1.x's terms-acceptance flow
  (login -> pendingToken -> GET /api/terms signature -> POST accept-terms),
  exchanging the pending token for a real access token. A clean 2.1.x gates
  the FIRST login behind this; without it the server's authenticateAgent
  cannot log in. Run once before the integration suite (no-op if accepted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commit 764821a fixed comments/labels/boardMemberships/tasks routes but left
the resource create-chain incompatible with Planka 2.1.x, which now requires a
`type` on create and nests tasks under task lists. Verified against a live
Planka 2.1.1: the integration suite now passes 36/36 (was 10/36).

- operations/lists.ts: send required `type` (default "active"); expose an
  optional active|closed enum on CreateListSchema.
- operations/cards.ts: send required `type` (default "project"); expose an
  optional project|story enum on CreateCardSchema.
- common/types.ts: PlankaTaskSchema required `cardId`, but v2.1 tasks carry
  `taskListId` (not cardId) — made cardId optional, added taskListId and
  linkedCardId. This unblocked updateTask, which parses with this schema.
- tests/integration/integration.test.ts: align stale assertions to the v2
  comment shape (text is top-level, not data.text) and send project `type`.

createProject has no MCP path (index.ts only reads projects), so only the
test helper needed the project `type`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
createList/createCard now send the required Planka v2.1 `type` (default
active/project, override-able). CI runs only the unit suite, so without these
a regression to the type default would pass CI and only surface in the
live integration run. Four assertions: default + explicit type for each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Biome 2.5.0 as formatter + linter, tuned to 2-space / double-quote / semicolons.
files.includes restricts it to code written this pass (tests/unit, .jest, the
jest configs, biome.json); the inherited fork (operations/, common/, index.ts,
tests/integration/) keeps its mixed style until a deliberate full-repo reformat
(tracked follow-up) — so this commit changes none of it.

Scripts: lint (biome check), format (biome format --write), check (biome check
--write). `npm run lint` checks 11 files clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ci.yml (push to main + every PR): Node 22, npm ci, build (typecheck),
  Biome lint, unit tests with coverage. No Planka needed; mirrors local.
- integration.yml (workflow_dispatch + weekly): write a CI .env, npm run up
  a Planka 2.1.1 container, wait for health, run the terms-acceptance
  provisioning, then test:integration; tears down on always(). Kept off the
  PR path so CI stays deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…racy

- package.json version 0.0.6 -> 0.1.0 to match common/version.ts (the value the
  User-Agent already reports).
- Drop node-fetch + @types/node-fetch: dead runtime deps. Only the (now removed)
  Jest setup imported them; all source uses the global fetch (Node 18+).
- wiki/Developer-Guide.md: the referenced test:coverage/test:watch/
  test:integration scripts now exist; correct the "all tests" wording (npm test
  is unit-only) and add the Planka 2.1.x terms-acceptance provisioning step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the session's deliverables (8 commits), verification evidence (unit
55/55, live integration 36/36, build/lint clean), live-verification findings,
and the security + code-review reviewer findings. Top item: a CRITICAL tracked
.env with SECRET_KEY/passwords (needs owner decision — rotation + history),
flagged for follow-up, NOT actioned here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-review flagged the optional `cardId` + "for v1 cases" comment as a
backwards-compat shim, which the rules prohibit. This branch targets Planka
2.1.x only, where tasks carry `taskListId` (and optionally `linkedCardId`), not
`cardId`. Removed `cardId`; zod strips the unknown key, so updateTask still
parses. Re-verified: unit 55/55, live integration 36/36.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…follow-ups)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First write-path exercise against Planka 2.1.1. card_manager create fails on the running server because every live kanban-mcp process predates the 17:00 dist rebuild that added the required `type` field — stale in-memory code, not a missing fix. A server reconnect picks up the already-built fix. Feeds PR bradrisse#10 / planka-v2.1-compat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Planka entity IDs are numeric snowflakes. Client-supplied IDs were z.string()
free text interpolated straight into API paths, so a crafted id (e.g.
"1/../../users") could coerce a tool into reaching a different resource on the
same Planka host with the agent's privileges (a prompt-injection vector).
Added a reusable `plankaId = z.string().regex(/^\d+$/)` and applied it to all 18
ID fields in the tool input schemas — rejected at the boundary before any URL
is built. Trusted internal calls (Planka-sourced IDs) are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auth resilience (common/utils.ts):
- authenticateAgent now detects Planka 2.1.x's 403 accept-terms response and
  throws an actionable error (run scripts/accept-planka-terms.sh / accept in
  the UI) instead of an opaque permission failure.
- plankaRequest clears the cached agentToken and retries once on a 401, so a
  long-lived server self-heals from an expired/revoked token instead of failing
  every request until restart. Unit test added for the retry.

Error honesty: getUserIdByEmail/Username (utils.ts), getComments (comments.ts),
getLabels (labels.ts), getBoardMemberships (boardMemberships.ts) no longer
catch-all to []/null. Request errors propagate; []/null is returned only for a
genuinely empty/not-found result — a real failure is no longer masked as empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Values confirmed demo/test by owner, so no rotation. Untrack .env so future
real secrets aren't committed (the file stays on disk for `npm run up`), add it
to .gitignore, and ship a placeholder .env.example documenting the vars compose
and the MCP server need (with `openssl rand -hex 64` for SECRET_KEY).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant