Altiplano

Altiplano
A small, dependable MCP server for Vikunja. Named after the Andean altiplano, the high plateau that is the Vicuña's native habitat.
Altiplano runs locally through uvx, or as a stand-alone HTTP service that several people share on one endpoint, each acting as their own Vikunja user. Both modes expose the same tools and the same guidance.
Choose how to use Altiplano
Local, through | Shared, over HTTP | |
Where Altiplano runs | On your computer, launched by your MCP client. | On a host running Altiplano as a stand-alone service. |
How your MCP client connects | Runs | Connects to the service URL with a valid bearer token. |
Requirements on your computer |
| An MCP client supporting Streamable HTTP and a configured |
Where the Vikunja credentials live | On each computer running Altiplano. | On the service host, one token per client. |
Setup |
Connecting to an existing HTTP service needs its URL and an Altiplano client token. You do not need to install Altiplano,
uv, or Python on the client.
The configuration examples below use an mcpServers block. Adapt the surrounding structure to your MCP client's configuration format.
Use locally with uvx
Your MCP client launches Altiplano as a local subprocess and communicates with it over stdio. Each client manages its own Altiplano process.
1. Install uv
uv provides the uvx command, which runs Altiplano from PyPI without a repository checkout.
2. Configure Vikunja credentials
Create an API token in Vikunja under Settings → API Tokens, reachable from the menu under your username. Give it the scopes covering the tools you intend to call. See Vikunja's API documentation.
mkdir -p ~/.config/altiplanoCreate ~/.config/altiplano/env containing:
VIKUNJA_URL=https://altiplano.example.com/api/v2
VIKUNJA_API_TOKEN=tk_replace_meRestrict the file's permissions:
chmod 600 ~/.config/altiplano/envAltiplano checks these sources in order:
The
VIKUNJA_URLandVIKUNJA_API_TOKENenvironment variables.A file containing
KEY=VALUEpairs, defaulting to~/.config/altiplano/env.
Set ALTIPLANO_CONFIG before starting Altiplano to read a different file. Use absolute paths; ~ is not expanded in custom paths.
VIKUNJA_URLmust end in/api/v1or/api/v2. That suffix selects the version, for examplehttps://altiplano.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.
Permissions broader than
600produce a warning on POSIX systems and startup continues. An unreadable file is ignored after a warning.
3. Configure your MCP client
Add a local server entry:
{
"mcpServers": {
"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.
4. Verify with one call
Restart or reconnect your MCP client, then call list_projects(). Any list, an empty one included, confirms that Altiplano reaches Vikunja with the configured credentials.
Altiplano speaks MCP over stdio. Running
uvx altiplanoin a terminal prints nothing and waits for a client on stdin and stdout.
Use over HTTP
altiplano-http serves the same tools over Streamable HTTP from one always-on host. Each client presents its own bearer token, which Altiplano mints, stores as a SHA-256 hash, and revokes one at a time.
The service must already be running and reachable from the computer running your MCP client. Adding its URL to your client configuration connects to the service. It does not start it. DEPLOYMENT.md covers standing one up, and Docker is the shortest path there.
One endpoint serves several people, each as their own Vikunja user. There is no shared Vikunja API token: the host holds one per registered client, and a request is made with the token belonging to the client that sent it. Two people on one service reach their own projects and their own tasks, with Vikunja applying its own permissions to each.
Registering a client is a manual step for whoever operates the host. An unrecognised client token is refused with
401. A registered client whose record holds no Vikunja token is refused with403.
Connect to an existing service
Obtain the MCP endpoint URL and a client token from whoever operates the host server. If that is you, Register clients has the steps.
Use one client token per client. Sharing one works, at two costs: the clients all act as the same Vikunja user, and revoking it cuts off every one of them.
claude mcp add --transport http altiplano \
https://altiplano.example.com/mcp \
--header "Authorization: Bearer altp_replace_me"The equivalent in a client's own configuration:
{
"mcpServers": {
"altiplano": {
"type": "http",
"url": "https://altiplano.example.com/mcp",
"headers": {
"Authorization": "Bearer altp_replace_me"
}
}
}
}type: Not part of the MCP protocol, which covers the wire format and leaves the configuration shape to each client. They disagree: some require
type, some spell the valuestreamable-httpand othershttp, some call the keytransport, and some infer the transport fromurlalone. Use the form your client documents.url: Replace it with the real endpoint, including its port and path where required. The example assumes HTTPS is configured for the service.
headers: The bearer token here is an Altiplano client token, issued by
altiplano-clientkey, and it says which client is calling. Your Vikunja API token is a separate thing: it stays on the service host, registered against your client, and it is the identity your requests act as. Give the operator a token from your own Vikunja account.
A client that only launches subprocesses cannot reach an HTTP URL at all. Keep the stdio entry on those machines.
Restart or reconnect your MCP client, then call list_projects(). A successful response confirms the connection, the client token, and access to Vikunja.
Keep the client token private. It grants access to the service and every tool it exposes. Ask the operator to revoke and replace a lost or exposed token. A revocation applies to the next request.
The supported path is a client that sends the header you configure. Altiplano answers an unauthenticated request with a bare
WWW-Authenticate: Bearerchallenge and publishes no OAuth metadata. A client may still probe the well-known metadata URLs on its own initiative and will get a404. A client that can only obtain credentials through an OAuth flow is not supported here.
Tools
list_projects(include_archived=False): includesparent_project_idfor sub-projects andis_archived. Vikunja omits archived projects from this endpoint;include_archived=Trueadds them back alongside the active ones.create_project(title, parent_project_id?, description?): passparent_project_idto create a sub-project.update_project(project_id, title?, description?, parent_project_id?, is_archived?, hex_color?): changes only the supplied fields and requires at least one.is_archivedarchives and unarchives, and Vikunja has no separate archive endpoint.hex_coloris six hexadecimal digits without#.delete_project(project_id): deletes the project, its sub-projects, and every task in all of them, along with each task's comments, labels, and assignees. Vikunja retains them for 30 days and provides no restore endpoint. Treat deletion as irreversible, and preferupdate_project(project_id, is_archived=True)to set a project aside.
Archiving hides a project from
list_projectsunlessinclude_archived=True. Vikunja also refuses every other edit to an archived project, and to the tasks in it, with a412. Unarchive it first.
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.
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.update_bucket(project_id, bucket_id, title?, limit?, view_id?): renames a column or changes its limit, and requires at least one field. Neither API version offers a partial update here. The call reads the column and writes it back whole. Column order is not writable.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_board(project_id, view_id?, filter?): returns columns and their tasks.task_countremains the full count when Vikunja caps the returned task list. Takes the same arguments aslist_bucketsand adds the tasks and their counts.list_task_placements(task_id): returns the column holding the task, one entry 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.
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.
list_labels(), create_label(title, hex_color?, description?), update_label(label_id, 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#.update_labelchanges only the supplied fields and requires at least one; every task with the label shows the change. Deleting a label removes it from every task.
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.
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 four 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 holds the full version, with 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.mdimports it, for Claude Code.DEPLOYMENT.mdcovers running the HTTP transport as a service on a host: Docker with aDockerfileand a compose file, a two-commanduvpath with no clone, every environment variable the transport reads, minting client tokens, encrypting the connection, four checks that say whether it works, and an FAQ of the ways it fails.
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, and50remains50.
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_boardmay 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. Verify the token itself too.list_bucketsreads a different route and is unaffected.On Vikunja 2.3.0,
list_assigneesreturns HTTP 500. The endpoint worked on 2.5.0.
Contributing
Pull requests are always welcome. CONTRIBUTING.md covers the development setup, the commands, the pre-commit hook, the source layout, and what a pull request needs. Taking part means agreeing to the code of conduct.
Licence
MIT.
Support
RTFM, then RTFS. If you are still stuck, or just need an additional feature, file an issue.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/aichholzer/altiplano'
If you have feedback or need assistance with the MCP directory API, please join our Discord server