Altiplano
A small, dependable MCP server for Vikunja.
Named after the Andean altiplano, the high plateau that is the Vicuña's native habitat.
Requires Python 3.10 or later.
Tools
Projects
list_projects(): includesparent_project_idfor subprojects.create_project(title, parent_project_id?, description?): passparent_project_idto create a subproject.
Tasks
list_tasks(project_id, filter?, sort_by?, page=1, per_page=50): Vikunja appliesfilterandsort_bybefore pagination.search_tasks(query?, filter?, sort_by?, page=1, per_page=50): searches all visible projects and includesproject_idin each result. Vikunja does not combine text search withfilter.get_task(task_id): returns full task detail. On v2, the description is Markdown.create_task(project_id, title, description?, priority?, due_date?, start_date?, end_date?, percent_done?, is_favorite?, repeat_after?, repeat_mode?)update_task(task_id, title?, description?, done?, priority?, due_date?, start_date?, end_date?, percent_done?, is_favorite?, repeat_after?, repeat_mode?): changes only the supplied fields and requires at least one. Pass an empty string fordue_date,start_date, orend_dateto clear it.move_task(task_id, project_id): moves labels, assignees, comments, relations, and dates with the task. The destination project assigns a new localidentifier.duplicate_task(task_id): copies the task, labels, assignees, attachments, and reminders into the same project. The copy receives acopiedfromrelation to the original.bulk_create_tasks(project_id, tasks): creates a batch of tasks in one request, atomically and in the given order. Requires/api/v2. Each entry takes the same fields ascreate_task,titleincluded, anything else is refused. Vikunja caps a batch at 100 and names the entry that fails. Returns one summary per created task.bulk_update_tasks(task_ids, done?, priority?): requires at least one field. The request fails as a unit if the token lacks write access to any affected project.set_reminders(task_id, reminders): replaces all reminders with the supplied ISO 8601 datetimes. Pass an empty list to clear them.delete_task(task_id): soft-deletes the task and removes its comments, labels, and assignees. Vikunja retains the task for 30 days and provides no restore endpoint. Treat deletion as irreversible.
Kanban
list_kanban_views(project_id): includes the default and done bucket IDs.list_buckets(project_id, view_id?): returns columns in board order and marks the default and done columns.create_bucket(project_id, title, view_id?, limit?): adds a column at the right. Omitlimit, or pass0, for no limit.delete_bucket(project_id, bucket_id, view_id?): moves the column's tasks to the default column. Vikunja will not remove the last column.list_bucket_tasks(project_id, view_id?, filter?): returns columns and their tasks.task_countremains the full count when Vikunja caps the returned task list.list_task_buckets(task_id): returns one bucket for each kanban view.move_task_to_bucket(task_id, bucket_id, view_id?): reads the project ID from the task.
Bucket behaviour:
- Bucket operations require a view with
view_kind="kanban". Withoutview_id, Altiplano uses the first kanban view in the project's view order. bucket_configuration_mode="manual"accepts explicit moves. Infiltermode, filters determine the column.- A move into a full bucket fails.
- Moving a task into the done column closes it; moving it out reopens it.
- A repeating task moved into the done column reopens in the default column.
- Setting
doneto true throughupdate_taskmoves the task into the done column.
Relations
add_relation(task_id, other_task_id, relation_kind="related")remove_relation(task_id, other_task_id, relation_kind="related"): use the same kind that created the relation.
get_task returns related_tasks, grouped by kind. Supported kinds are subtask, parenttask, related, duplicateof, duplicates, blocking, blocked, precedes, follows, copiedfrom, and copiedto.
add_relation(task_id, other_task_id, "subtask")makesother_task_ida child oftask_id.
Labels
list_labels(), create_label(title, hex_color?, description?), delete_label(label_id), add_label(task_id, label_id), remove_label(task_id, label_id).
hex_coloris six hexadecimal digits without#. Deleting a label removes it from every task.
Comments
list_comments(task_id), add_comment(task_id, comment), update_comment(task_id, comment_id, comment), delete_comment(task_id, comment_id).
update_commentreplaces the complete text. Getcomment_idfromlist_comments.
Assignees
search_users(query), list_assignees(task_id), add_assignee(task_id, user_id), remove_assignee(task_id, user_id).
Use
search_usersto find theuser_idrequired by the assignment tools.
Credentials
Altiplano checks these sources in order:
VIKUNJA_URLandVIKUNJA_API_TOKENenvironment variables.- A file containing
KEY=VALUEpairs, defaulting to~/.config/altiplano/env.
Set ALTIPLANO_CONFIG before starting Altiplano to use another file. Use an absolute path; ~ is not expanded in custom paths.
VIKUNJA_URL must include the API prefix, such as https://todo.example.com/api/v2.
Vikunja 2.4.0 introduced
/api/v2.VIKUNJA_URLshould end in/api/v1or/api/v2. Altiplano strips trailing slashes and enables v2 only for a URL ending in/api/v2; every other URL keeps its configured path and uses v1 request verbs. Use/api/v2when the server supports it.
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
Permissions broader than 600 produce a warning (on POSIX) but do not prevent startup. An unreadable file is ignored after a warning.
MCP server entry (without credentials):
{
"altiplano": {
"command": "uvx",
"args": ["--refresh-package", "altiplano", "altiplano@latest"]
}
}
--refresh-package altiplano checks PyPI for a current release. If an older version still starts, close the MCP client and run:
uv cache clean altiplano
Task behaviour
Task updates
Both API versions preserve fields omitted from an update.
On v1, POST /tasks/{id} replaces the complete task. Altiplano reads the current task, merges the changes, and writes it back for update_task, set_reminders, and move_task. Each call costs one extra request.
On v2, those calls use PATCH. A description change uses a read and full replacement because PATCH does not apply Markdown conversion.
When a v2 read includes an ETag, Altiplano sends it in If-Match during the replacement. Vikunja returns HTTP 412 if the task changed between the read and write. Read the task again before retrying. V1 and v2 responses without an ETag have no concurrency guard.
Dates, priority, and progress
- Priorities use Vikunja's scale:
0Unset,1Low,2Medium,3High,4Urgent, and5DO NOW. - Dates are ISO 8601 datetimes.
start_dateandend_datedefine the work window;due_dateis the deadline. - An empty string clears a date by writing Vikunja's zero time,
0001-01-01T00:00:00Z. percent_doneis a fraction. A quarter complete is0.25. Vikunja does not clamp the value, so50remains50.
Repeating tasks
repeat_after is measured in seconds. Completing a repeating task reopens it and advances its due date and reminders.
repeat_mode values:
0: advance the existing dates byrepeat_after.1: repeat monthly and ignorerepeat_after.2: calculate the next occurrence from the completion date.
Vikunja's v1 specification lists 3 in the repeat_mode description; its generated enum defines the final mode as 2.
A repeating task with no dates reopens immediately and cannot remain closed. Set a due_date when enabling repetition.
Identifiers and errors
The Vikunja UI displays a project-local identifier, such as #50. API calls use the global numeric id.
Failed requests include Vikunja's detail, message, or title field when present. Altiplano also includes a non-zero numeric error code when Vikunja provides one. Redirects are errors and include the destination.
Requests time out after 30 seconds and are not retried.
Markdown
Vikunja stores task descriptions and comments as HTML. Altiplano requests Markdown conversion for full task reads, task creates, project creates, comment reads and writes, and full task replacements.
The v1 API has no Markdown conversion. Markdown sent through v1 is stored literally.
V2 partial updates through update_task, move_task, and set_reminders return the stored HTML description. Call get_task to retrieve Markdown. An update_task call that changes description uses a full replacement.
Vikunja resolves @mentions during Markdown conversion and notifies the named user.
Compatibility
Tested with Vikunja 2.5.0 against /api/v1 and /api/v2.
Identified issues:
- On Vikunja 2.5.0, the v2 grouped-bucket route used by
list_bucket_tasksmay return HTTP 401 when the token works elsewhere. Try a new full-permission token or/api/v1. The same diagnosis is returned for every v2 HTTP 401 from that route, so verify the token itself too. - On Vikunja 2.3.0,
list_assigneesreturns HTTP 500. The endpoint worked on 2.5.0.
Contributing
Enable the pre-commit hook once per clone:
git config core.hooksPath hooks
The hook runs Ruff 0.16.4 over src and tests, then pytest with a 90 percent coverage minimum. CI runs Ruff in one job and pytest on Python 3.10 and 3.13.
Run
uv run altiplano # development checkout
uvx --from /your/local/path altiplano # local package path
uvx --refresh-package altiplano altiplano@latest # current PyPI release
Layout
src/altiplano/
app.py MCP instance imported by the tool modules
config.py Credential resolution and credential-file parsing
api.py API-version handling, requests, and response shaping
tools/ One module for each tool group
server.py Tool registration and the main entry point
Register a tool group by adding its module and importing it from server.py. Add its tools to the routing-table test and the smoke test's exact list.
Pull requests are always welcome!
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-1.1.0.tar.gz.
File metadata
- Download URL: altiplano-1.1.0.tar.gz
- Upload date:
- Size: 999.6 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 |
6be75896c7b91c5c50eccd5a179710f1722e0f4000a2876e892d5ee6ba07425f
|
|
| MD5 |
e807b9eff6903682576181d4570dc18e
|
|
| BLAKE2b-256 |
c4838fe38fa44956024f41687672ffa6f4a56478003b1e60eeed8e2a386c0136
|
File details
Details for the file altiplano-1.1.0-py3-none-any.whl.
File metadata
- Download URL: altiplano-1.1.0-py3-none-any.whl
- Upload date:
- Size: 25.9 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 |
f96a2d1cf2fde349a791405ec85f3a64fef819dfc6e598da7841a08c5d844140
|
|
| MD5 |
00807d097c2b8cba91d6c2dfa0b099ad
|
|
| BLAKE2b-256 |
714514ed4ad35872e6f7626c2da2566b8fa157c125382d949e6e5b9910f78a45
|