Altiplano
A small, dependable MCP server for Vikunja. Named after the Andean altiplano, the high plateau that is the Vicuña's native habitat.
Filtering and sorting are passed straight to the Vikunja API (server-side), so there is no client-side filtering engine and no paginate-then-filter pitfall.
Tools
Projects:
list_projects(includesparent_project_id, shows sub-project nesting)create_project(title, parent_project_id?, description?): passparent_project_idfor a sub-project
Tasks:
list_tasks(project_id, filter, sort_by, page, per_page)get_task(task_id)create_task(project_id, title, description?, priority?, due_date?, start_date?, end_date?)update_task(task_id, title?, description?, done?, priority?, start_date?, end_date?): on v1, see the warning below about omitted fieldsset_reminders(task_id, reminders): replaces the task's reminders with the given ISO 8601 datetimes; empty list clearsdelete_task(task_id): soft-deletes the task along with its comments, labels and assignees. Vikunja keeps it for 30 days but exposes no way to restore it, so treat this as irreversible
Labels:
list_labelsadd_label(task_id, label_id)remove_label(task_id, label_id)
Comments:
list_comments(task_id)add_comment(task_id, comment)update_comment(task_id, comment_id, comment): replaces the comment text; getcomment_idfromlist_commentsdelete_comment(task_id, comment_id)
Assignees:
search_users(query): find auser_idto assignlist_assignees(task_id)add_assignee(task_id, user_id)remove_assignee(task_id, user_id)
Credentials (no secrets in mcp.json)
The server resolves two values, in order:
- Environment variables
VIKUNJA_URLandVIKUNJA_API_TOKEN. - A per-device file of
KEY=VALUElines, default~/.config/altiplano/env(override the path withALTIPLANO_CONFIG).
VIKUNJA_URL is the base API URL including the version prefix (e.g. https://todo.example.com/api/v2).
Recommended so the your mcp.json carries no secrets:
- Drop a per-device file and lock it down:
mkdir -p ~/.config/altiplano printf 'VIKUNJA_URL=https://todo.example.com/api/v2\nVIKUNJA_API_TOKEN=tk_xxx\n' > ~/.config/altiplano/env chmod 600 ~/.config/altiplano/env
- Or inject via the launcher's environment (e.g. a systemd unit
EnvironmentFile=pointing at achmod 600file), which the server inherits. - For stronger setups, source the token from a secret manager/keychain at launch and export it into the environment.
Then mcp.json only needs the command, no env block, no plain-text secrets:
{
"altiplano": {
"command": "uvx",
"args": ["--refresh-package", "altiplano", "altiplano@latest"]
}
}
uvxcaches aggressively. It fetches a version on first run and reuses it afterwards, and it separately caches the list of versions it knows about. Soaltiplano@lateston its own can keep launching an older build for a while after a release, and restarting your client does not help.
--refresh-package altiplanorevalidates against PyPI on each start, which is what makes@latestactually mean latest. It costs roughly a tenth of a second.If you are already stuck on an older build, quit your MCP client first, then run
uv cache clean altiplano. It has to be quit: while the client is running it holds that cache, and the command will sit and wait for it.
Choosing the API version
Vikunja 2.4.0 added a v2 API alongside v1, and this MCP deals with both. The version comes from the URL you configure, so there is nothing else to set:
VIKUNJA_URL ends in |
you get |
|---|---|
/api/v2 |
the v2 API |
/api/v1 |
the v1 API |
Prefer /api/v2 if your server has it. Stay on /api/v1 only for older servers; every tool works the same either way, with some exceptions.
On v1, updating a task discards the fields you omit
v1's update endpoint is a replace, not a partial update. Whatever you leave out is reset to its zero value:
update_task(task_id=42, priority=4) # on v1, this also blanks the description
update_task(task_id=42, done=True) # and this discards description, priority and dates
set_reminders(task_id=42, reminders=[]) # same, via the same endpoint
This is Vikunja's behaviour. On v1, you must pass every field you want to keep.
v2 is unaffected. It uses PATCH and omitted fields survive.
Markdown descriptions and comments (v2 only)
Vikunja stores task descriptions and comments as HTML. On v2 this server asks it to convert, so you write and read Markdown:
create_task(project_id=12, title="Ship it", description="**bold** and a [link](https://example.com)")
Vikunja stores that as <p><strong>bold</strong> and a <a href="...">link</a></p>, and get_task hands it back as the Markdown you wrote. The same applies to update_task, create_project, add_comment and update_comment.
On v1 there is no conversion and the fields are HTML, so Markdown you send is stored verbatim and renders as literal asterisks.
Two things worth mentioning:
- v2 only converts on create and on full replace, never on a partial update. So changing a description reads the task first and writes it back whole, which costs one extra request and could lose a concurrent edit by something else. Updates that do not touch the description stay a single partial update.
- Vikunja resolves
@mentionsduring conversion, so writing@someonein a description notifies them.
Run
uv run altiplano # dev, from this directory
uvx --from /your/local/path altiplano # local path
uvx altiplano@latest # from PyPI, refreshing the cache
Notes
- Vikunja priority scale: 0 Unset, 1 Low, 2 Medium, 3 High, 4 Urgent, 5 DO NOW.
- Dates are ISO 8601 datetimes.
start_date/end_datemark the window you plan to work on a task (start work / finish work);due_dateis the deadline. - The UI shows tasks by their project-local
identifier(e.g.#50), which is not the globalidthe API uses. - Verified end to end against Vikunja v2.5.0 on both
/api/v1and/api/v2. list_assigneesneeds a server whereGET /tasks/{id}/assigneesworks. It answers 500 on v2.3.0, which was a server-side bug, and works on v2.5.0. Every other tool works on both.
Contributing
Enable the pre-commit hook once per clone:
git config core.hooksPath hooks
Git hook configuration lives in
.git/config, which is not version controlled, so I can't do it for you. The hook itself is committed inhooks/, and git will not look there until you point it.The same checks run on every pull request. The suite must pass and coverage must stay above 90 percent.
Coverage is also reported to Codecov on each pull request, as a comment and as inline annotations. That reporting is purely informational and never fails a build.
Licence
MIT.
Support
RTFM, then RTFC... If you are still stuck or just need an additional feature, file an issue.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file altiplano-0.8.6.tar.gz.
File metadata
- Download URL: altiplano-0.8.6.tar.gz
- Upload date:
- Size: 975.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
576e1a3071994d925be4338c7225d04f7309e6973f02cc500214cbe9cc966357
|
|
| MD5 |
017c860701b4ae7e96239502c88a67b4
|
|
| BLAKE2b-256 |
d4c3d7957fa8f2b243d51b92eba2675dc6c40f06aeca4f777f01f574465ad03a
|
File details
Details for the file altiplano-0.8.6-py3-none-any.whl.
File metadata
- Download URL: altiplano-0.8.6-py3-none-any.whl
- Upload date:
- Size: 11.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b76ad43b83c776c40f226b57dd494df0e5bc33d85cfcd378b2e7e17ed6d0522e
|
|
| MD5 |
5862caeb08274a9079ef6fa6b3e534d3
|
|
| BLAKE2b-256 |
60b0f57711435dbffc239db30ff70c4f1905be43175ccac9c0fed84807a1387d
|