<!--
SPDX-FileCopyrightText: Amolith <amolith@secluded.site>

SPDX-License-Identifier: CC0-1.0
-->

# lunatask-mcp-server

_Interact with [Lunatask](https://lunatask.app) using `$PREFERRED_LLM`_

[![scratchanitch.dev badge](https://img.shields.io/badge/scratchanitch-dev-FFC4B5)](https://scratchanitch.dev)
[![Go Report Card](https://goreportcard.com/badge/git.sr.ht/~amolith/lunatask-mcp-server)](https://goreportcard.com/report/git.sr.ht/~amolith/lunatask-mcp-server)
[![REUSE status](https://api.reuse.software/badge/git.sr.ht/~amolith/lunatask-mcp-server)](https://api.reuse.software/info/git.sr.ht/~amolith/lunatask-mcp-server)
[![Liberapay donation status](https://img.shields.io/liberapay/receives/Amolith.svg?logo=liberapay)](https://liberapay.com/Amolith/)

**Note:** lots of this was written by LLMs and I haven't reviewed it thoroughly
enough to be comfortable making it more public than unlisted. Please refrain
from sharing it around and do let me know if there are any issues.

---

## MCP server setup

Ensure you have [the Go toolchain] installed.

[the Go toolchain]: https://go.dev/doc/install

```sh
git clone https://git.sr.ht/~amolith/lunatask-mcp-server
cd lunatask-mcp-server
# specify GOOS and GOARCH if cross-compiling
CGO_ENABLED=0 go build -o lunatask-mcp-server .
./lunatask-mcp-server
# it'll generate config.toml with default values
# fill them out with your preferred editor
./lunatask-mcp-server
```

If you have [just], build with `just build` (supports `GOOS` and `GOARCH`). If
you also have [upx], compress the resulting binary with `just pack`. You can run
one after the other with `just build pack`.

[just]: https://github.com/casey/just
[upx]: https://github.com/upx/upx

Point [Home Assistant's MCP integration] at the `/sse` endpoint.

[Home Assistant's MCP integration]: https://www.home-assistant.io/integrations/mcp/

## Available Tools

This MCP server provides several tools for interacting with Lunatask:

### `get_timestamp`

Parses natural language dates into RFC3339 timestamps.

**Parameters:**
- `natural_language_date` (string, required): A natural language description of a date/time (e.g., "today at 3pm", "next Monday", "in 2 hours")

### `list_areas_and_goals`

Lists all configured areas and their goals in Lunatask.

**Returns:**
- Text formatted as a list with area names and IDs, and their associated goal names and IDs

### `create_task`

Creates a new task in Lunatask.

**Parameters:**
- `area_id` (string, required): UUID of the area to create the task in
- `name` (string, required): Name of the task (max 100 characters)
- `goal_id` (string, optional): UUID of the goal to associate the task with
- `note` (string, optional): Additional notes for the task in Markdown format
- `status` (string, optional): Task status - one of "later", "next", "started", "waiting", or "completed"
- `motivation` (string, optional): Motivation level - one of "must", "should", or "want"
- `estimate` (integer, optional): Estimated time to complete task in minutes (0-720)
- `priority` (string, optional): Task priority - one of "lowest", "low", "neutral", "high", or "highest"
- `eisenhower` (string, optional): Eisenhower matrix categorization - one of "uncategorised", "both urgent and important", "urgent, but not important", "important, but not urgent", or "neither urgent nor important"
- `scheduled_on` (string, optional): RFC3339 formatted timestamp for when the task is scheduled

### `update_task`

Updates an existing task in Lunatask.

**Parameters:**
- `task_id` (string, required): UUID of the task to update
- `area_id` (string, optional): UUID of the area to move the task to
- `name` (string, optional): New name for the task
- `goal_id` (string, optional): UUID of the goal to associate the task with
- `note` (string, optional): Additional notes for the task in Markdown format
- `status` (string, optional): Task status - one of "later", "next", "started", "waiting", or "completed"
- `motivation` (string, optional): Motivation level - one of "must", "should", or "want"
- `estimate` (integer, optional): Estimated time to complete task in minutes (0-720)
- `priority` (string, optional): Task priority - one of "lowest", "low", "neutral", "high", or "highest"
- `eisenhower` (string, optional): Eisenhower matrix categorization - one of "uncategorised", "both urgent and important", "urgent, but not important", "important, but not urgent", or "neither urgent nor important"
- `scheduled_on` (string, optional): RFC3339 formatted timestamp for when the task is scheduled

### `delete_task`

Deletes a task from Lunatask.

**Parameters:**
- `task_id` (string, required): UUID of the task to delete

### `list_habits_and_activities`

Lists all configured habits in Lunatask.

**Returns:**
- Text formatted as a list with habit names and IDs

### `track_habit_activity`

Tracks an activity for a habit in Lunatask.

**Parameters:**
- `habit_id` (string, required): UUID of the habit to track
- `performed_on` (string, required): RFC3339 formatted timestamp of when the habit was performed

## Collaboration

Patch requests are in [amolith/llm-projects] on [pr.pico.sh]. You don't need a
new account to contribute, you don't need to fork this repo, you don't need to
fiddle with `git send-email`, you don't need to faff with your email client to
get `git request-pull` working...

You just need:

- Git
- SSH
- An SSH key

```sh
# Clone this repo, make your changes, and commit them
# Create a new patch request with
git format-patch origin/main --stdout | ssh pr.pico.sh pr create amolith/llm-projects
# After potential feedback, submit a revision to an existing patch request with
git format-patch origin/main --stdout | ssh pr.pico.sh pr add {prID}
# List patch requests
ssh pr.pico.sh pr ls amolith/llm-projects
```

See "How do Patch Requests work?" on [pr.pico.sh]'s home page for a more
complete example workflow.

[amolith/llm-projects]: https://pr.pico.sh/r/amolith/llm-projects
[pr.pico.sh]: https://pr.pico.sh

## Models and prompts

I'm currently using `google/gemini-2.5-flash` through [OpenRouter] with the
following system prompt:

[OpenRouter]: https://openrouter.ai/

> You are a calm and thoughtful voice assistant for Home Assistant. You can both
> interact with Home Assistant and additional systems plugged into it, like
> Lunatask. Lunatask is an ADHD-friendly all-in-one encrypted to-do list, habit
> tracker, journaling, life-tracking and notes app.
>
> \<lunatask_instructions\>
>
> When interacting with Lunatask, write task names in sentence case. When asked
> to mark something complete or when the user says they've done somethizng and
> you have no other context, check their habits. If one matches, track it.
>
> For every request to create or change a task:
>
> 1. List the areas and goals and consider which one area is most relevant.
>    Consider whether the task belongs within any of that area's goals.
> 2. Get the task timestamp.
> 3. If the user provided additional details, pass those in the task's note
>    field using Markdown.
> 4. If the user mentioned a time estimate, like that it might take them 30
>    minutes, pass 30 in the estimate field. If they said 2 hours, 120, and so
>    on.
> 5. Try and interpret the text from the STT engine. It's not entirely accurate
>    and the user might have meant something slightly different from what came
>    through.
> 6. Only include parameters if the user indicates or hints at them. Avoid
>    scheduling tasks unless the user explicitly says something like "in three
>    days" or "next monday" or "in 8 hours" or "schedule it for {day/time}" or
>    "remind me on {day} to {task}" or even "remind me at {time} on {day} to
>    {thing}" and other variations.
> 7. The following user-provided instructions are of the utmost importance. Keep
>    them in mind throughout.
>
> \<user_provided_instructions\>
>
> - My Work area uses the Kanban workflow, so Work tasks must _always_ include a
>   status value of either `later`, `next`, `started`, `waiting`, or
>   `completed`. Default to `later` unless I specify otherwise.
> - My Personal area uses the Now/Later workflow, so tasks created there _must_
>   include a status value of either "later" or "started". Default to "later"
>   unless I specify otherwise.
> - My Someday area uses a simple priority list. Do not include a status and
>   instead only provide a priority if I specify one.
> - My Acquisitions area currently follows the Eisenhower workflow; do not
>   include a status, do not include a date unless I specify, and do include the
>   eisenhower parameter. Its values may be _one_ of `uncategorised`, `both
urgent and important`, `urgent, but not important`, `important, but not
urgent`, or `neither urgent nor important`. If I don’t specify a value, ask
>   me. Never set it to `uncategorised` unless I explicitly say to “clear” it or
>   “unset” it or “set it to uncategorised” or something like that.
> - If I don't provide an estimate for any task regardless of area, please
>   consider whether it might take 10, 25, 30, 45, 60, or more minutes. Always
>   include an estimate for Work, Personal, and Somday areas. Never for
>   Acquisitions.
> - Interpret verbal dictation, like converting "something dash something" to
>   "something-something".
> - Again I don't want any of my tasks scheduled. You should only need to call
>   the timestamp tool when marking habits complete.
>
> \</user_provided_instructions\>
>
> Only once you've gathered the requisite area ID and other information should
> you attempt to create or update a task.
>
> \</lunatask_instructions\>

## TODO

- _Optionally_ include a tool that calls a configurable LLM for task time
  estimations so the main LLM doesn't have to guess.
