vikunja-mcp
# vikunja-mcp
MCP server for [Vikunja](https://vikunja.io), built on **API v2**.
Exposes Vikunja tasks to any MCP client (Claude Code, Claude Desktop, …) as tools.
The one deliberate deviation from the API: **tasks are never deleted**. Removing a
task attaches a configurable label instead, and listings hide labelled tasks.
## Tools
| Tool | What it does |
| --- | --- |
| `list_tasks` | List tasks globally or in one project, with Vikunja filter syntax, search, sorting, pagination. Hides removed tasks by default. |
| `get_task` | Read a single task including labels and assignees. |
| `create_task` | Create a task; optionally attaches labels (creating missing ones). |
| `update_task` | Partial update via JSON Merge Patch; `clear_fields` empties a field. |
| `remove_task` | Soft delete — attaches the removal label. Never calls `DELETE`. |
| `restore_task` | Detaches the removal label. |
| `add_task_labels` | Attach labels to a task, creating any that do not exist. |
| `list_projects` | Projects with their numeric ids (needed for filters). |
| `get_project` | Read a single project by id. |
| `create_project` | Create a project, optionally nested under a parent. |
| `list_labels` | Labels with their numeric ids (needed for filters). |
## Removal by label
`remove_task` never issues `DELETE`. It:
1. resolves the label named by `VIKUNJA_REMOVED_LABEL` (default `removed_by_label`),
creating it when `VIKUNJA_AUTO_CREATE_REMOVED_LABEL=true`;
2. attaches it to the task via `POST /tasks/{id}/labels`.
`list_tasks` then appends `labels not in <label_id>` to the filter query. Because
Vikunja drops rows whose filtered field is null, `filter_include_nulls` is set to
`true` alongside that clause unless you pass it explicitly — otherwise tasks with
no labels at all would disappear from the listing. Results are additionally
filtered client-side, so a removed task never reaches the model.
Set `include_removed=true` on `list_tasks` to see them.
## Configuration
All settings come from environment variables (or a local `.env`); see
[`.env.example`](.env.example).
| Variable | Default | Meaning |
| --- | --- | --- |
| `VIKUNJA_URL` | *required* | Instance URL. `https://host`, `.../api`, `.../api/v1` and `.../api/v2` are all normalised to `/api/v2`. |
| `VIKUNJA_TOKEN` | *required* | API token (`tk_...`) or JWT, sent as `Authorization: Bearer`. |
| `VIKUNJA_REMOVED_LABEL` | `removed_by_label` | Label used instead of deleting. |
| `VIKUNJA_AUTO_CREATE_REMOVED_LABEL` | `true` | Create that label on first use. |
| `VIKUNJA_DEFAULT_PROJECT_ID` | – | Project used by `create_task` when `project_id` is omitted. |
| `VIKUNJA_DESCRIPTION_FORMAT` | `markdown` | `markdown` or `html` for rich-text fields. |
| `VIKUNJA_TIMEOUT` | `30` | HTTP timeout, seconds. |
| `VIKUNJA_VERIFY_SSL` | `true` | TLS verification. |
| `VIKUNJA_MCP_TRANSPORT` | `stdio` | `stdio`, `http`, `sse` or `streamable-http`. |
| `VIKUNJA_MCP_HOST` | `127.0.0.1` | Bind address for HTTP transports. |
| `VIKUNJA_MCP_PORT` | `8000` | Port for HTTP transports. |
Create the token in Vikunja under **Settings → API Tokens**. It needs read/write
scopes on tasks, labels and projects.
## Running
### Locally
```bash
make install
```
```bash
cp .env.example .env # then fill in VIKUNJA_URL and VIKUNJA_TOKEN
```
```bash
make run
```
`make run` serves stdio and waits for JSON-RPC on stdin, which is how MCP
clients launch it. For HTTP use `make run-http` (override with `HOST=` and
`PORT=`). `make help` lists every target.
### Docker
```bash
make docker-build
```
stdio (how MCP clients usually launch it):
```bash
docker run --rm -i -e VIKUNJA_URL -e VIKUNJA_TOKEN vikunja-mcp
```
HTTP:
```bash
docker run --rm -p 8000:8000 -e VIKUNJA_MCP_TRANSPORT=http -e VIKUNJA_URL -e VIKUNJA_TOKEN vikunja-mcp
```
### Registering with an MCP client
```json
{
"mcpServers": {
"vikunja": {
"command": "docker",
"args": ["run", "--rm", "-i", "-e", "VIKUNJA_URL", "-e", "VIKUNJA_TOKEN", "vikunja-mcp"],
"env": {
"VIKUNJA_URL": "https://try.vikunja.io",
"VIKUNJA_TOKEN": "tk_..."
}
}
}
}
```
## Development
```bash
uv run pre-commit install
```
```bash
make test
```
```bash
make lint
```
Tests mock the Vikunja API with `respx` and drive the server through FastMCP's
in-memory client, so no live instance is required.
## Layout
| File | Role |
| --- | --- |
| [`config.py`](src/vikunja_mcp/config.py) | Settings and URL normalisation |
| [`models.py`](src/vikunja_mcp/models.py) | Pydantic models for the v2 payloads |
| [`client.py`](src/vikunja_mcp/client.py) | Async HTTP client, RFC 9457 error mapping |
| [`service.py`](src/vikunja_mcp/service.py) | Removal-by-label logic, label resolution |
| [`server.py`](src/vikunja_mcp/server.py) | FastMCP tool definitions |
TDQS
Scored across 11 tools
Each tool targets a distinct resource and action: tasks, projects, and labels are cleanly separated, and remove_task/restore_task are explicitly paired as opposites. There is no meaningful overlap between any two tools.
All tools follow a consistent lower_snake_case verb_noun pattern, such as list_tasks, get_task, update_task, and create_project. The naming is uniform and predictable across the whole set.
Eleven tools is a well-scoped set for a task/project management server, covering tasks, projects, and labels without excessive fragmentation. Each tool earns its place in the API surface.
Task lifecycle coverage is strong with list/get/create/update/remove/restore and label manipulation. The main gap is project coverage, which supports list/get/create but lacks update/delete or archive operations, though these are less central to task-focused workflows.