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.

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_URL must end in /api/v1 or /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/v2 when the server supports it.

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 a different file. Use absolute paths; ~ is not expanded in custom paths.

Permissions broader than 600 produce 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 altiplano checks PyPI for a current release; if an older version still starts, close the client and run uv 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 altiplano prints nothing and waits for a client. That silence is the expected behaviour.

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.

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_guide prompt has the full version, adding cross-tool sequencing and the v1 and v2 differences. Clients list it as Using Altiplano.
  • AGENTS.md covers 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: 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 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

altiplano-1.2.0.tar.gz (1.0 MB view details)

Uploaded Source

Built Distribution

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

altiplano-1.2.0-py3-none-any.whl (30.4 kB view details)

Uploaded Python 3

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

Hashes for altiplano-1.2.0.tar.gz
Algorithm Hash digest
SHA256 0c75df9e27502870f895e7de26cf4a19e649005efa03af0f781d3d31d15e4166
MD5 2d37f654a6f134442ef31096e81488b6
BLAKE2b-256 22dd95b4346fde2feb99e438a9395075bfb719567d8ffffa81a4a830f66ddc2b

See more details on using hashes here.

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

Hashes for altiplano-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ed5c2d9091bf4cd9567978122a8db6f60fd842f001b888efe38d6447bd502e01
MD5 c0a300644e9ca079601364a1d6a74b0e
BLAKE2b-256 6ddc91321b7f3888f39c2aa66babd249022618981cc4844e03cd5399497e6d89

See more details on using hashes here.

Release history Release notifications | RSS feed

2.0.1

2 files

2.0.0

2 files

This release

1.2.0 This release

2 files

1.1.0

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