Fix: Planka v2 (2.x) API route compatibility - #10
Open
cameronsjo wants to merge 18 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
POST /api/cards/:id/comment-actionsPOST /api/cards/:id/commentsGET /api/cards/:id/actions(filtertype==commentCard)GET /api/cards/:id/commentsPATCH/DELETE /api/comment-actions/:idPATCH/DELETE /api/comments/:idPOST /api/boards/:id/membershipsPOST /api/boards/:id/board-membershipsGET /api/boards/:id/membershipsincluded.boardMemberships(no list route in v2)POST /api/cards/:id/labelsPOST /api/cards/:id/card-labelsDELETE /api/cards/:id/labels/:labelIdDELETE /api/cards/:id/card-labels/labelId::labelIdPOST /api/cards/:id/tasksPOST /api/task-lists/:taskListId/tasks(auto-ensures a task list on the card)Comments also became first-class in v2: the response carries
textat the top level rather than acommentCardaction withdata.text. The comment schema and thecard-detailstool's heuristic were updated accordingly.Verification
Each changed route was exercised live as a board member against Planka 2.1.1 — all returned 2xx:
npm run buildis clean (notscerrors).Notes
tasksare 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).labelId::labelIdform 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