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.
Install
1. Install uv
uv provides uvx, which runs Altiplano without a checkout. See installation guide.
2. Vikunja API token
In Vikunja, open Settings from the menu under your username, then API Tokens. See Vikunja's API documentation.
Give the token scopes covering the tools you intend to call.
3. Store the credentials
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
VIKUNJA_URLmust end in/api/v1or/api/v2, the suffix selects the version (eg.:https://todo.example.com/api/v2)
Vikunja 2.4.0 introduced
/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.
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 a different file. Use absolute paths; ~ is not expanded in custom paths.
Permissions broader than
600produce a warning (on POSIX) but do not prevent startup. An unreadable file is ignored after a warning.
4. Add the MCP server entry
In your client's MCP configuration:
{
"altiplano": {
"command": "uvx",
"args": ["--refresh-package", "altiplano", "altiplano@latest"]
}
}
--refresh-package altiplanochecks PyPI for a current release; if an older version still starts, close the client and runuv cache clean altiplano.
5. Verify with one call
Restart the client so it launches the server, then call list_projects(). Any list, an empty one included, means the install works.
Altiplano speaks MCP over stdio, so
uvx altiplanoprints nothing and waits for a client. That silence is the expected behaviour.
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_taskreturnsrelated_tasks, grouped by kind. Supported kinds aresubtask,parenttask,related,duplicateof,duplicates,blocking,blocked,precedes,follows,copiedfrom, andcopiedto.
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.
Guidance
Altiplano documents its own use in three places.
- The handshake sends usage rules: resolve ids by name, which calls cannot be undone, how to close a task. Clients apply them on connect.
- The
altiplano_guideprompt has the full version, adding cross-tool sequencing and the v1 and v2 differences. Clients list it asUsing Altiplano. AGENTS.mdcovers working on this repository, and installing Altiplano for someone else.CLAUDE.md, because Claude Code needs its own file.
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 and prompt modules
config.py Credential resolution and credential-file parsing
api.py API-version handling, requests, and response shaping
prompts.py The usage guidance, served as a prompt
tools/ One module for each tool group
server.py 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.2.0.tar.gz.
File metadata
- Download URL: altiplano-1.2.0.tar.gz
- Upload date:
- Size: 1.0 MB
- 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 |
0c75df9e27502870f895e7de26cf4a19e649005efa03af0f781d3d31d15e4166
|
|
| MD5 |
2d37f654a6f134442ef31096e81488b6
|
|
| BLAKE2b-256 |
22dd95b4346fde2feb99e438a9395075bfb719567d8ffffa81a4a830f66ddc2b
|
File details
Details for the file altiplano-1.2.0-py3-none-any.whl.
File metadata
- Download URL: altiplano-1.2.0-py3-none-any.whl
- Upload date:
- Size: 30.4 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 |
ed5c2d9091bf4cd9567978122a8db6f60fd842f001b888efe38d6447bd502e01
|
|
| MD5 |
c0a300644e9ca079601364a1d6a74b0e
|
|
| BLAKE2b-256 |
6ddc91321b7f3888f39c2aa66babd249022618981cc4844e03cd5399497e6d89
|