Skip to main content

Altiplano

Altiplano

CI codecov Python PyPI version License

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(): includes parent_project_id for subprojects.
  • create_project(title, parent_project_id?, description?): pass parent_project_id to create a subproject.
Tasks
  • list_tasks(project_id, filter?, sort_by?, page=1, per_page=50): Vikunja applies filter and sort_by before pagination.
  • search_tasks(query?, filter?, sort_by?, page=1, per_page=50): searches all visible projects and includes project_id in each result. Vikunja does not combine text search with filter.
  • 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 for due_date, start_date, or end_date to clear it.
  • move_task(task_id, project_id): moves labels, assignees, comments, relations, and dates with the task. The destination project assigns a new local identifier.
  • duplicate_task(task_id): copies the task, labels, assignees, attachments, and reminders into the same project. The copy receives a copiedfrom relation 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 as create_task, title included, 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. Omit limit, or pass 0, 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_count remains 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". Without view_id, Altiplano uses the first kanban view in the project's view order.
  • bucket_configuration_mode="manual" accepts explicit moves. In filter mode, 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 done to true through update_task moves 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") makes other_task_id a child of task_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_color is 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_comment replaces the complete text. Get comment_id from list_comments.

Assignees

search_users(query), list_assignees(task_id), add_assignee(task_id, user_id), remove_assignee(task_id, user_id).

Use search_users to find the user_id required by the assignment tools.

Credentials

Altiplano checks these sources in order:

  1. VIKUNJA_URL and VIKUNJA_API_TOKEN environment variables.
  2. A file containing KEY=VALUE pairs, 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_URL should end in /api/v1 or /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/v2 when 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: 0 Unset, 1 Low, 2 Medium, 3 High, 4 Urgent, and 5 DO NOW.
  • Dates are ISO 8601 datetimes. start_date and end_date define the work window; due_date is the deadline.
  • An empty string clears a date by writing Vikunja's zero time, 0001-01-01T00:00:00Z.
  • percent_done is a fraction. A quarter complete is 0.25. Vikunja does not clamp the value, so 50 remains 50.

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 by repeat_after.
  • 1: repeat monthly and ignore repeat_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_tasks may 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_assignees returns 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

altiplano-1.1.0.tar.gz (999.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

altiplano-1.1.0-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

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

Hashes for altiplano-1.1.0.tar.gz
Algorithm Hash digest
SHA256 6be75896c7b91c5c50eccd5a179710f1722e0f4000a2876e892d5ee6ba07425f
MD5 e807b9eff6903682576181d4570dc18e
BLAKE2b-256 c4838fe38fa44956024f41687672ffa6f4a56478003b1e60eeed8e2a386c0136

See more details on using hashes here.

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

Hashes for altiplano-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f96a2d1cf2fde349a791405ec85f3a64fef819dfc6e598da7841a08c5d844140
MD5 00807d097c2b8cba91d6c2dfa0b099ad
BLAKE2b-256 714514ed4ad35872e6f7626c2da2566b8fa157c125382d949e6e5b9910f78a45

See more details on using hashes here.

Release history Release notifications | RSS feed

2.0.1

2 files

2.0.0

2 files

1.2.0

2 files

This release

1.1.0 This release

2 files

1.0.0

2 files

0.14.1

2 files

0.10.0

2 files

0.8.6

2 files

0.8.5

2 files

0.8.1

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page