Skip to main content
Glama
andrejberg
by andrejberg
README.md
# ya-planka-mcp

A Model Context Protocol (MCP) server that enables AI clients to read and manage your Planka boards using the Planka REST API.

[![Tests](https://github.com/andrejberg/ya-planka-mcp/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/andrejberg/ya-planka-mcp/actions/workflows/test.yml)

## Getting Started

New here? Four steps to a running, registered server — minimal detail, each
one links to the full section below if you need it.

1. **Clone the repo:**
   ```bash
   git clone https://github.com/andrejberg/ya-planka-mcp
   cd ya-planka-mcp
   ```
2. **Install dependencies:** `uv sync` (see [Installation](#installation) for the `pip` alternative).
3. **Get an API key and fill in `.env`:** log in to Planka as an **admin**,
   generate an API key, then:
   ```bash
   cp .env.example .env
   # edit .env: set PLANKA_BASE_URL and PLANKA_API_KEY
   ```
   Full walkthrough: [Obtaining API Credentials](#obtaining-api-credentials).
4. **Register the server with your MCP client** — Claude Desktop, Claude Code,
   or any generic client: see [MCP Client Configuration](#mcp-client-configuration).

That's the whole path. For dependency details, alternate install methods,
credential fallbacks, troubleshooting, and everything else, keep scrolling.

## Overview

ya-planka-mcp provides a lightweight bridge between MCP clients and your self‑hosted Planka instance. It exposes projects, boards, lists, cards, tasks, and labels through MCP tools, allowing agents to use planka for the full project management and execution live cycle.

## Features

- List projects, boards, lists, labels, and members — full dump or scoped to one board
- Search and retrieve cards with multiple detail levels
- Read card checklists with real task IDs and accurate progress counts
- Create, update, delete, and bulk-move cards
- Create, update, and delete checklist tasks and task lists
- Manage labels (create idempotently, assign, remove) and card members
- Read and write card comments
- Scaffold complete boards (canonical kanban lists + lifecycle labels) in one call
- Composite workflow tools: next workable card, mechanical readiness checks, and a
  computed one-call board status report
- Token-optimized: detail levels, scoped responses, slimmed schemas — measured by
  a committed audit script
- Works with any MCP‑compatible client

Example use cases:
- "Show all 'In Progress' cards across my workspace."
- "Create a new card in `<Board> / TODO` with subtasks…"
- "Find the 'Login bug' card and list all tasks."
- "Mark the 'Write tests' task as complete."
- "Add the 'Urgent' label to the 'Deploy' card."

## Prerequisites

- Python 3.10+
- [`uv`](https://docs.astral.sh/uv/) (recommended) or `pip` + `venv`
- Access to a Planka instance
- A Planka account you're comfortable letting the MCP server act as (see
  **Obtaining API Credentials** below — the account's role determines what
  the server can do; see **Roles & Permissions**)

## Installation

### 1. Clone the repository

```bash
git clone https://github.com/andrejberg/ya-planka-mcp
cd ya-planka-mcp
```

### 2. Install dependencies

With `uv` (recommended — creates `.venv/` and resolves the lockfile):
```bash
uv sync
```

This installs runtime dependencies only. If you also want to run the test
suite (see **Running Tests** below), install the `test` extra instead:
```bash
uv sync --extra test
```

Or with `pip` in a manually created virtualenv (the last two lines below —
installing `requirements.txt` then an editable install — are exactly what CI
runs in `.github/workflows/test.yml`, just without a venv there):
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install -e .
```

Note the venv directory name differs between the two paths: `uv sync` creates
`.venv/`; the `pip` walkthrough above creates `venv/`. This matters for the
`run_server.sh` helper script and the MCP client config below — pick one path
and stay consistent with it.

### 3. Create your `.env` file

```bash
cp .env.example .env
```

Then fill in the variables below.

#### Environment variables

| Variable | Required? | Purpose |
|---|---|---|
| `PLANKA_BASE_URL` | Yes | Base URL of your Planka instance (e.g. `https://planka.example.com`, no trailing slash) |
| `PLANKA_API_KEY` | Option 1 | **Recommended.** The admin-generated API key from Planka's user administration screen — see **Obtaining API Credentials**. Checked second, only if `PLANKA_API_TOKEN` is unset. |
| `PLANKA_API_TOKEN` | Option 2 | JWT access token from the legacy `/api/access-tokens` exchange — see **Fallback: JWT access-token exchange**. Checked first (takes priority over `PLANKA_API_KEY` if both are set). |
| `PLANKA_EMAIL` + `PLANKA_PASSWORD` | Option 3 | Login credentials. The server exchanges them for a token automatically at startup. Checked last, only if neither token variable is set. |

Precedence is fixed in `src/planka_mcp/api_client.py::initialize_auth`:
`PLANKA_API_TOKEN` > `PLANKA_API_KEY` > `PLANKA_EMAIL`+`PLANKA_PASSWORD` — this
only matters if you set more than one. For a fresh setup, set just
`PLANKA_API_KEY` (the admin-generated key); see **Obtaining API Credentials**
below for how to get it.

`.env.example` also documents role-scoped variables
(`PLANKA_API_TOKEN_BOARD_MANAGER`, `PLANKA_API_TOKEN_BOARD_MEMBER`,
`PLANKA_SMOKE_LIST_ID`, `PLANKA_SMOKE_LABEL_ID`) — these are only read by the
opt-in test suite (see **Opt-in permission smoke** under Running Tests), not
by the server itself.

## Obtaining API Credentials

This server was developed and tested against **Planka Community v2.0.3**. In
that version, generating an API key requires **admin** access — there is no
self-service "generate my key" option for a regular board member.

1. **Log in to your Planka instance as an admin user.**
2. **Open User Administration** (the admin-only user management screen).
3. **Generate an API key** for the account you want the MCP server to act
   as — this can be the admin account itself or any other account. The key
   carries exactly that account's Planka permissions; see **Roles &
   Permissions** below for what a given account can and can't do through
   this server before picking which one to use.
4. **Copy the generated key** into `.env`:
   ```bash
   PLANKA_API_KEY=your-generated-api-key
   ```

That's the whole flow. If your Planka instance predates v2.0.3 or otherwise
has no admin-generated API key screen, use one of the fallback methods
below instead.

#### Fallback: JWT access-token exchange

Older Planka versions (and any instance without an admin-generated API key
screen) expose a JWT token via the `/api/access-tokens` endpoint — the same
exchange the Planka web client performs internally on login:

```bash
curl -X POST https://your-planka-instance.com/api/access-tokens \
  -H "Content-Type: application/json" \
  -d '{
    "emailOrUsername": "your-email@example.com",
    "password": "your-password"
  }'
```

**Response:**
```json
{
  "item": {
    "accessToken": "..."
  }
}
```

Copy the `accessToken` value into `.env` as `PLANKA_API_TOKEN`.

**Note**: JWT tokens may expire. If you get authentication errors later,
repeat the exchange to mint a new one.

#### Fallback: Email/Password

If you'd rather not mint a token up front, set `PLANKA_EMAIL` and
`PLANKA_PASSWORD` directly — the server performs the same JWT exchange
automatically at startup:

```bash
PLANKA_EMAIL=your-email@example.com
PLANKA_PASSWORD=your-password
```

This is only used if `PLANKA_API_TOKEN` and `PLANKA_API_KEY` are both unset.

## MCP Client Configuration

The server is a stdio MCP server; the actual entry point every config below
runs is `mcp_server.py` (`main.py` exists only as a legacy wrapper that
subprocesses into `mcp_server.py`). Replace `/path/to/ya-planka-mcp` with
your clone's absolute path.

### Claude Desktop

Edit `claude_desktop_config.json` (macOS:
`~/Library/Application Support/Claude/claude_desktop_config.json`; Windows:
`%APPDATA%\Claude\claude_desktop_config.json`).

If you installed with `uv sync` (step 2 above):
```json
{
  "mcpServers": {
    "ya-planka-mcp": {
      "command": "uv",
      "args": ["--directory", "/path/to/ya-planka-mcp", "run", "python", "mcp_server.py"],
      "env": {
        "PLANKA_BASE_URL": "https://your.domain",
        "PLANKA_API_KEY": "<token>"
      }
    }
  }
}
```

If you installed with `pip` into `venv/` (step 2 above), point directly at
that interpreter — use absolute paths for both `command` and the script
argument, since the client's working directory when it spawns the process is
not guaranteed to be this repo:
```json
{
  "mcpServers": {
    "ya-planka-mcp": {
      "command": "/path/to/ya-planka-mcp/venv/bin/python",
      "args": ["/path/to/ya-planka-mcp/mcp_server.py"],
      "env": {
        "PLANKA_BASE_URL": "https://your.domain",
        "PLANKA_API_KEY": "<token>"
      }
    }
  }
}
```

### Claude Code

Either add a project-scoped `.mcp.json` at the root of the project you're
working in (not inside `ya-planka-mcp` itself):
```json
{
  "mcpServers": {
    "ya-planka-mcp": {
      "type": "stdio",
      "command": "uv",
      "args": ["--directory", "/path/to/ya-planka-mcp", "run", "python", "mcp_server.py"],
      "env": {
        "PLANKA_BASE_URL": "https://your.domain",
        "PLANKA_API_KEY": "<token>"
      }
    }
  }
}
```

or register it with the CLI:
```bash
claude mcp add ya-planka-mcp \
  --env PLANKA_BASE_URL=https://your.domain \
  --env PLANKA_API_KEY=<token> \
  -- uv --directory /path/to/ya-planka-mcp run python mcp_server.py
```

`claude mcp add` defaults to `--scope local` (visible only to you, in this
project); pass `--scope project` to commit `.mcp.json` for teammates, or
`--scope user` to make the server available across all your projects.

### Generic MCP client

Any client that can spawn a stdio subprocess needs only the same three
pieces of information as the configs above:
- **command/args**: `uv --directory /path/to/ya-planka-mcp run python mcp_server.py`
  (or the direct venv-interpreter form if you installed with `pip`)
- **env**: `PLANKA_BASE_URL` and one auth variable (see Environment variables above)

`run_server.sh` is an alternative launcher some clients may prefer over a raw
`uv`/python command — it activates `./venv` (not `.venv`) if present and runs
`mcp_server.py`, or falls back to the system `python3` running `main.py`
otherwise. It only auto-detects a venv literally named `venv/`, so it is
compatible with the `pip` install path above but not with `uv sync`'s
`.venv/`; the explicit `uv --directory ...` / venv-interpreter forms above
work regardless of install method and are recommended.

## Tools & Capabilities

29 tools, grouped by kind:

### Read

| Tool | Purpose |
|------|---------|
| `planka_get_workspace` | Workspace structure (projects, boards, lists, labels, users). Pass `board_name`/`project_name` to scope the response — a scoped call is a fraction of the full dump |
| `planka_get_me` | Authenticated user identity (id, username, role) |
| `planka_get_labels` | All labels on a board (fast targeted refresh) |
| `planka_list_cards` | Filter and list cards with detail levels (`preview`/`summary`/`detailed`) and pagination |
| `planka_find_and_get_card` | Search and fetch a specific card |
| `planka_get_card` | Fetch a card with full checklist detail (real task IDs, progress counts) |
| `planka_get_comments` | List a card's comments, newest first, with optional prefix filter (e.g. `Blocked:`) |

### Write

| Tool | Purpose |
|------|---------|
| `planka_create_board` | Scaffold a board with canonical kanban lists and lifecycle labels in one call |
| `planka_create_list` / `planka_update_list` | Create, rename, and reorder board lists |
| `planka_create_card` / `planka_update_card` / `planka_delete_card` | Card CRUD |
| `planka_move_cards` | Bulk-move 1–50 cards to a list (by id or name), order-preserving, per-card results |
| `planka_create_task_list` / `planka_add_task` / `planka_update_task` / `planka_delete_task` | Checklist management |
| `planka_create_label` / `planka_update_label` / `planka_delete_label` | Label CRUD (create is idempotent by name) |
| `planka_add_card_label` / `planka_remove_card_label` | Assign/remove board labels on cards |
| `planka_add_card_member` / `planka_remove_card_member` | Assign/remove card members |
| `planka_add_comment` | Write a comment to a card |

### Composite workflow

| Tool | Purpose |
|------|---------|
| `planka_next_card` | Top card of a list (default TODO) with everything needed to execute it — description, tasks with ids, labels, members, latest comments — in one round-trip |
| `planka_card_ready_check` | Mechanical readiness validation (labels, description sections, checklist shape) |
| `planka_board_status` | One-call board report: per-list counts, in-progress ages, blocked cards with latest `Blocked:` comment, unsatisfied gates, done-window points |

### Token efficiency

Responses support `response_format` (`markdown`/`json`) and, where reads can be
large, `detail_level` and scope filters. `scripts/token_audit.py` measures the
per-session tool-definition cost and live response sizes against your instance:

```bash
uv pip install tiktoken   # optional, for exact o200k_base counts
.venv/bin/python scripts/token_audit.py [--board <name>] [--skip-live]
```

## Usage Examples

Ask your assistant:

- "List all my boards."
- "Search for cards mentioning 'invoice'."
- "Create a card named 'App release checklist' with these subtasks…"
- "Move the 'Integrate payment API' card to 'Done'."
- "Mark the 'Write tests' task on the 'Backend sprint' card as complete."
- "Add the 'Urgent' label to the 'Deploy hotfix' card."

## Roles & Permissions

The MCP server does **no role handling of its own**. It acts as whatever
Planka user the configured API key (or email/password) belongs to — whatever
that account can do in Planka, the server can do through its tools; whatever
it can't, the server can't either. You control what the agent is capable of
entirely by which account's key you put in `.env`, and by adjusting that
account's rights inside Planka itself.

Planka distinguishes two kinds of board access, and this section uses
Planka's own terms throughout:

- **Board manager** — a user in the board's managers list. Only accounts
  with at least **Project Owner** or **Admin** role can be added as a board
  manager. Board managers get full card/task access plus board-level
  management (settings, members, labels).
- **Board member** — a user added to a board with either **edit** or
  **view-only** rights. Edit rights allow card/task mutations; view-only is
  read-only. Board members — even with edit rights — cannot manage the
  board's members list or most board-level settings.

### Role capability matrix

| Capability | Board manager token | Board member token | Evidence |
|---|---|---|---|
| List cards on a board list (`planka_list_cards`) | Works | Not exercised by the board member live smoke (it never calls this tool) | `tests/test_live_smoke.py::test_non_owner_live_smoke_permissions` |
| Create a card, read fresh card detail, delete the card | Works | Works | `test_non_owner_live_smoke_permissions`; `test_board_member_live_smoke_permissions` |
| Create a task list, add/update/delete a task | Works | Not exercised by the board member live smoke | `test_non_owner_live_smoke_permissions` |
| Add / remove a card label | Works | Not exercised by the board member live smoke | `test_non_owner_live_smoke_permissions` |
| Read `cardMemberships` on a card detail | Works (implied) | Works — asserted explicitly (`cardMemberships` key must be present) | `test_board_member_live_smoke_permissions` (docstring: "PERM-01 evidence") |
| Assign / remove **another** board member as a card member | Works | Not exercised — the board member smoke only self-assigns, consistent with board member tokens being unable to enumerate other users (see next row) | `test_non_owner_live_smoke_permissions` (`include_member_mutations=True`) |
| Assign / remove **yourself** as a card member | Works (not the focus of this test) | Works — self-assign then self-remove is the entire point of the test | `test_board_member_live_smoke_permissions` |
| `planka_get_workspace` users list | Populated | **Empty.** Planka returns `403` on `/api/users` for board member tokens; the server catches it and degrades gracefully (returns `{}` plus `_users_hidden_due_to_permissions: true` in JSON) instead of raising | Source: `fetch_workspace_data` in `src/planka_mcp/handlers/workspace.py` (tagged `PERM-03`); unit tests `test_fetch_workspace_data_users_403_returns_empty_map` and `test_get_workspace_permission_hint_when_users_empty` in `tests/test_workspace.py` (mocked 403, not part of the live smoke suite) |

Rows marked "not exercised" are not asserted anywhere in this repo's tests —
they are not claimed to fail, only that the board member live smoke does not
cover them. Only claim behavior beyond this table after adding a test that
proves it.

### Gotchas

- **A 403 on `/api/users` is invisible by design.** If `planka_get_workspace`
  comes back with no users, check the account's board role (`planka_get_me`)
  before assuming a bug — a board member token gets an empty users map on
  purpose, not an error.
- **Non-admin tokens silently see less, not an error.** This is a Planka
  platform characteristic (not asserted by this repo's own test suite): a
  Planka user — and therefore a token scoped to that user — only sees
  projects/boards/lists/cards it is a member of. A wrong or under-scoped token
  does not raise; `planka_get_workspace` and `planka_list_cards` just return
  fewer boards/cards than you expect. If something you know exists is
  missing, check board membership for that token's user first.

### Security recommendations

- The MCP server accesses only what the authenticated Planka user can access.
- API token recommended over email/password.
- Use HTTPS when exposing Planka externally.
- Consider using a dedicated Planka service user with restricted permissions.

## Troubleshooting & FAQ

**401 Unauthorized**
Check token validity and `.env` configuration.

**Client cannot connect to server**
Verify the correct Python/uv path, firewall rules, and execution permissions.

**No boards or cards returned**
Confirm the Planka user has workspace access.

**Task creation fails**
Ensure you are passing a valid `task_list_id`. Use `planka_get_card` to retrieve real task list IDs from the card's checklist.

## Development

This project uses an editable install so the `src/` directory is automatically on the Python path.

```bash
# Install in editable mode
uv sync
# or: pip install -e .
```

## Running Tests

Test dependencies (`pytest` and friends) live behind the `test` extra, not
the base install — run this once before the commands below:
```bash
uv sync --extra test
```

```bash
# Run all offline tests
uv run pytest --cov=src/planka_mcp --cov-report=term-missing

# Run a specific test file
uv run pytest tests/test_cards.py -v
```

### Opt-in live smoke test

The repo includes an opt-in live smoke path that creates a card, task, and label on a real Planka board, then cleans up. It is skipped unless you opt in.

Required environment variables:
- `PLANKA_RUN_LIVE_SMOKE=1`
- `PLANKA_BASE_URL`
- One auth method: `PLANKA_API_KEY`, `PLANKA_API_TOKEN`, or `PLANKA_EMAIL` + `PLANKA_PASSWORD`

The smoke test targets any writable list on your Planka instance — no disposable list or label IDs are required. Point it at a safe test board (e.g. "Test Project").

pytest does **not** auto-load `.env` — source it into the shell first:

```bash
set -a; source .env; set +a
uv run pytest -m live tests/test_live_smoke.py -q --no-cov
```

Safety contract:
- Creates a disposable card and checklist task, then removes them
- Label add/remove uses a reusable board label
- Cleanup runs in `finally` even on failure
- Fresh-state assertions after task mutation use a new API client (not cached reads)

### Card state and cache behavior

`planka_get_card` caches per-card detail payloads for up to 60 seconds. Card mutation tools
that can affect detail output invalidate that card cache: comments, task lists/tasks, labels,
members, custom field values, card updates, moves, and deletes.

`planka_list_cards` reads the board view live for every call. Preview and summary output use
state present in that board response, including `commentsTotal` when comment bodies are not
loaded. Detailed list output fetches fresh detail for each paginated card before rendering so
tasks and comments reflect live Planka state instead of a stale detail cache.

### Opt-in permission smoke (role tokens)

For board-permission verification, run the gated permission smoke with role-scoped tokens.
Tests are role-dependent:
- `test_non_owner_live_smoke_permissions` uses `PLANKA_API_TOKEN_BOARD_MANAGER`
- `test_board_member_live_smoke_permissions` uses `PLANKA_API_TOKEN_BOARD_MEMBER`

Required environment variables:
- `PLANKA_RUN_PERMISSION_SMOKE=1`
- `PLANKA_BASE_URL` (or `PLANKA_PERM_BASE_URL`)
- `PLANKA_API_TOKEN_BOARD_MANAGER` for board manager-role smoke
- `PLANKA_API_TOKEN_BOARD_MEMBER` for board member-role smoke
- Optional explicit disposable targets: `PLANKA_SMOKE_LIST_ID` and `PLANKA_SMOKE_LABEL_ID` (or `PLANKA_PERM_SMOKE_*`)

```bash
set -a; source .env; set +a
PLANKA_RUN_PERMISSION_SMOKE=1 uv run pytest -m live tests/test_live_smoke.py -q --no-cov
```

### Test with MCP Inspector

```bash
npx @modelcontextprotocol/inspector uv run python mcp_server.py
```

## Contributing

**Dev environment setup**

```bash
git clone https://github.com/andrejberg/ya-planka-mcp
cd ya-planka-mcp
uv sync                 # editable install, src/ on the Python path
cp .env.example .env    # fill in PLANKA_BASE_URL + a token before touching live tests
```

**Before opening a PR**

1. Run the offline suite with coverage — CI (`.github/workflows/test.yml`) runs
   `pytest --cov=src/planka_mcp --cov-report=xml --cov-fail-under=80` on every push/PR
   to `main`; match that locally:
   ```bash
   uv run pytest --cov=src/planka_mcp --cov-report=term-missing --cov-fail-under=80
   ```
2. If the change touches role/permission behavior (anything in
   `src/planka_mcp/handlers/workspace.py`, card membership, or label handling), run
   the opt-in permission smoke locally against a real Planka instance with both
   role tokens before describing the change as verified — see "Opt-in permission
   smoke (role tokens)" above:
   ```bash
   set -a; source .env; set +a
   PLANKA_RUN_PERMISSION_SMOKE=1 uv run pytest -m live tests/test_live_smoke.py -q --no-cov
   ```
   This is not part of CI (it requires live credentials) — it is a pre-PR check
   for the author, not a merge gate.
3. Keep the Roles & Permissions matrix above honest: if a PR changes what a
   board manager or board member token can do, update the matrix and its test in the
   same PR — don't let the docs drift from `tests/test_live_smoke.py`.
4. New tools/handlers follow the existing layout: Pydantic models in
   `src/planka_mcp/models.py`, handler functions in `src/planka_mcp/handlers/`,
   matching tests in `tests/`.

**PR expectations**

- Offline tests pass and coverage stays at or above 80% (CI enforces this).
- No secrets (`.env`, tokens) committed.
- README/docs updated when behavior, tools, or setup steps change.

## Acknowledgements

- Planka project: https://github.com/plankanban/planka
- Model Context Protocol: https://modelcontextprotocol.io/

## Credits & Attribution

This project is a fork of [another-planka-mcp](https://github.com/roelven/another-planka-mcp) by Roel van der Ven, adapted and substantially extended as **ya-planka-mcp**. The original implementation provided the foundation for Planka MCP integration; this fork introduces significant enhancements, new features, and production-oriented improvements.

## License

MIT License. See `LICENSE`.

TDQS

C2.8/5.0

Scored across 38 tools

Disambiguation4/5

Most tools have clearly distinct purposes, such as creating, updating, deleting, or retrieving specific entities. However, slight overlap exists between 'planka_find_and_get_card' and 'planka_get_card', and 'planka_get_labels' may be redundant with workspace data, but descriptions clarify intent.

Naming Consistency4/5

The vast majority follow a consistent 'planka_verb_noun' pattern (e.g., planka_create_card, planka_delete_label). A few outliers like 'planka_board_status' and 'planka_card_ready_check' break the pattern, and 'planka_next_card' uses an adjective as verb, but overall naming is predictable.

Tool Count4/5

38 tools is on the higher end but appropriate for a full-featured project management server covering boards, lists, cards, labels, custom fields, tasks, comments, and members. Each tool serves a specific action, and while a few could be merged, the count is reasonable for the domain.

Completeness3/5

The tool set covers most core operations (CRUD for cards, labels, custom fields, tasks, comments), but lacks board update/delete, list delete, and comment delete. The presence of specialized tools like 'planka_board_status' and 'planka_card_ready_check' indicates attention to workflows, yet gaps remain.

Maintenance

ActivityStale
ResponsivenessNo issues