Skip to main content
Glama
README.md
# 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

A4.2/5.0

Scored across 11 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues