Task.md Utilities MCP Server
by DrSkippy
README.md
# Task.md Utilities (HENDRICKSON KANBAN)
A REST API, MCP server, and interactive CLI for managing tasks organized in lanes, where each task is a markdown file. The CLI includes a full-screen Textual TUI (kanban board with keyboard navigation) as well as non-interactive subcommands for scripting.
## About Tasks.md
This project builds on and extends the [Tasks.md](https://github.com/BaldissaraMatheus/Tasks.md) project. For comprehensive information about the Tasks.md system, including task visualization, board views, file format specifications, and VSCode integration, visit the [Tasks.md repository](https://github.com/BaldissaraMatheus/Tasks.md).
This package adds a REST API, a network-capable interactive CLI, and an MCP server for programmatic task management.
## Overview
Tasks are stored as markdown files organized in lane directories:
- Task title = filename (without `.md`)
- Tags: `[tag:tagname]` — one per line at the top of the file
- Due dates: `[due:YYYY-MM-DD]`
- Tasks can be split into subtasks using the `[[split]]` marker
## Architecture
```
┌──────────────────┐ HTTP ┌───────────────────┐
│ tasks CLI / TUI │ ────────▶│ Flask REST API │ :2999 (container) / :3101 (host)
│ (bin/tasks) │ │ (task_api/) │──┐
└──────────────────┘ └───────────────────┘ │ ┌──────────────┐ ┌───────────────────┐
├──▶│ task_lib/ │──▶│ Markdown files │
┌──────────────────┐ MCP ┌───────────────────┐ │ │ (in-process) │ │ /data/tasks/ │
│ AI assistant │ ────────▶│ MCP server │──┘ └──────────────┘ └───────────────────┘
│ (Claude etc.) │ │ (mcp_task_service)│ :3003 ▲
└──────────────────┘ └───────────────────┘ │ same folder
Tasks.md web app ──────────┘
```
The REST API and the MCP server each import `task_lib` and read and write the
card files directly; neither calls the other. The CLI and TUI only talk to the
REST API. The Tasks.md web app reads the same folder.
## Project Structure
```
task.md-utilities/
├── bin/
│ ├── tasks # CLI — TUI (no args) or subcommands (scripting)
│ └── tag-utility.py # One-time tag format migration utility
├── docs/
│ └── screenshot.svg # TUI kanban board screenshot
├── task_lib/
│ ├── api_client.py # Shared HTTP helpers (used by CLI and TUI)
│ ├── cli_config.py # CLI settings (~/.config/tasks/config.yaml)
│ ├── config.py # Configuration (YAML)
│ ├── lane_order.py # Per-machine lane (column) order for CLI and TUI
│ ├── errors.py # Typed errors: not_found, exists, invalid, conflict
│ ├── sections.py # Partial edits: append, upsert section, check items
│ ├── task.py # Task model and file I/O
│ └── task_manager.py # Lane, task and trash operations
├── task_tui/ # Textual TUI package
│ ├── app.py # KanbanApp — main board, keybindings, workers
│ ├── api.py # Sync API wrappers for use in workers
│ ├── screens.py # Detail, form, confirm, filter, move screens
│ └── widgets.py # LaneColumn, TaskItem, FunctionKeyBar
├── task_api/ # Flask REST API service
│ ├── app.py
│ ├── config.py
│ ├── models.py # Pydantic request/response schemas
│ ├── routes/ # tasks, trash, lanes, operations, health
│ ├── gunicorn.conf.py
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── README.md
├── mcp_task_service/ # FastMCP server for AI assistants
│ ├── server.py
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── README.md
├── tests/ # pytest suite (conftest.py isolates $HOME)
├── config.yaml # Task data location (baked into Docker images)
├── tasks.config.example.yaml # Every CLI setting with its default
├── docker-compose.yml # Deploys both services together
├── Makefile # build / push / deploy / test / lint
└── .mcp.json # Connects Claude Code in this repo to the MCP server
```
## Deployment
Both services are deployed as Docker containers. Build from the **repo root** (build context must include both `task_lib/` and the service directory).
### Build images
```bash
make push # build and push both images (make build to only build)
make release # run the tests first, then build and push
make deploy # recreate the containers in /opt/stacks/mcp-task
# Single service, or another registry/tag
make push-mcp
make push-api REGISTRY=registry.example:5000 TAG=0.3.0
```
`make help` lists every target.
### Deploy
The running services are the Dockge stack in `/opt/stacks/mcp-task`, whose
compose file matches the root `docker-compose.yml`. After `make push`:
```bash
make deploy # docker compose pull && up -d --force-recreate in /opt/stacks/mcp-task
```
Restarting the MCP server drops connected MCP clients; reconnect them (in
Claude Code: `/mcp reconnect`). To run the stack from the repo instead, use
`docker compose up -d` in the repo root.
The compose file starts both services on a shared network:
| Service | Internal port | Host port |
|---------|--------------|-----------|
| REST API | 2999 | 3101 |
| MCP server | 3003 | 3003 |
Both containers mount the task data directory:
```yaml
volumes:
- /mnt/raid1/lib/tasks.md/tasks:/data/tasks
```
### Configuration
`config.yaml` (committed to the repo) sets the task data location inside the container. It is copied into the image at build time — no volume mount needed:
```yaml
base_dir: /data/tasks
```
To change the data path, edit `config.yaml` and rebuild the images.
### Running services individually
Each service has its own `docker-compose.yml` for standalone use:
```bash
cd task_api && docker-compose up -d # REST API only
cd mcp_task_service && docker-compose up -d # MCP only
```
## CLI (`bin/tasks`)
The `tasks` binary has two modes:
- **No arguments** — launches the full-screen interactive TUI (kanban board)
- **With a subcommand** — runs non-interactively for scripting
### Installation
```bash
poetry install
poetry run bin/tasks --help
```
`poetry install` does not put `tasks` on your `PATH`; the examples below write
`tasks` for `poetry run bin/tasks`. To type `tasks` directly, add an alias:
```bash
alias tasks='poetry -C ~/Working/task.md-utilities run ~/Working/task.md-utilities/bin/tasks'
```
### Configuration
The CLI reads its settings from a YAML file, the first of:
1. `--config PATH` flag
2. `TASKS_CONFIG` environment variable
3. `~/.config/tasks/config.yaml`
Every key is optional and a missing default file is fine. Copy
[`tasks.config.example.yaml`](tasks.config.example.yaml), which lists every key
with its default: `api_url`, request `timeout`, the board's `app_title`,
`default_sort` for `tasks show`, the `empty_cell` placeholder, and the table
`styles`. Unknown keys and badly typed values are reported as errors.
The board's column order is per machine: Shift+←/→ in the board or
`tasks lanes order` saves it to `~/.local/state/tasks/lane_order.yaml`
(`$XDG_STATE_HOME`, or the `lane_order_file` config key). `tasks lanes list`
and `tasks show` follow it too. The server, the MCP tools and other machines
still list lanes alphabetically.
The API URL comes from the first of:
1. `--api-url URL` flag
2. `TASKS_API_URL` environment variable
3. `api_url` in the config file
4. Default: `http://localhost:3101`
```yaml
api_url: http://your-server:3101
```
---
### Interactive TUI
```bash
tasks
```
Launches a full-screen kanban board. Lanes are displayed as side-by-side columns; tasks are listed under each lane.

#### Keyboard navigation
| Key | Action |
|-----|--------|
| `←` / `→` | Move between lanes |
| `Shift+←` / `Shift+→` | Move the focused lane (column) left / right; the order is saved |
| `↑` / `↓` | Move between tasks within a lane |
| `Enter` | Open task detail view |
| `Esc` | Go back / close dialog |
| `F1` | Show keyboard shortcut help |
| `F2` | New task (in the focused lane) |
| `F3` | Edit selected task |
| `F4` | Delete selected task (confirm prompt) |
| `F5` | Filter tasks (by lane, tag, or title substring) |
| `F6` | Move selected task to another lane |
| `F7` | Open / close task detail view |
| `F8` | Refresh board from the API |
| `F9` | Quit |
Active filters are shown in a status bar at the top; press `F5` again to change or clear them.
---
### Subcommands
```
tasks [--api-url URL] [--config PATH] COMMAND
Commands:
show List tasks (--lane, --tag, --string, --query, --due-before,
--due-after, --sort lane|title|due|modified)
find Search titles and bodies, showing the matching text
get Show a single task in full
add Create a new task (lane must exist unless --create-lane)
update Update task fields
append Append text to the end of a task
section Replace (or add) one "## Heading" section of a task
check Tick "- [ ]" checklist items (--uncheck to untick)
delete Move a task to Trash
move Change a task's lane (--create-lane for a new lane)
trash Manage the Trash
list List trashed cards, newest first
restore Restore a card into a lane
empty Permanently delete Trash (--dry-run, --yes)
empty-trash Same as `trash empty`
split Split tasks containing [[split]] marker
lanes Manage lanes
list List lanes with task counts
order Show or set the column order (--reset)
add Create a new lane
stats Show statistics summary
```
Adding, renaming, moving and restoring never overwrite a card: reusing an
existing title fails, and so does naming a lane that doesn't exist (unless you
pass `--create-lane`). `split` also checks first: a card whose `1-<title>`,
`2-<title>`, … names are already taken is left unsplit and reported.
### Examples
```bash
# List all tasks
tasks show
# Filter by lane, tag, or title substring (all case-insensitive)
tasks show --lane "In Progress"
tasks show --tag urgent
tasks show --string "login"
# Show full task detail
tasks get "Implement login"
# Create a task (prompts for missing fields)
tasks add --title "Fix bug" --content "Reproduce and fix" --lane Backlog --tags "bug,urgent"
# Update fields (only provided fields change)
tasks update "Fix bug" --tags "bug,urgent,p1" --due-date 2026-06-01
# Search titles and bodies; sort by due date
tasks find "books db"
tasks show --sort due --due-before 2026-06-30
# Edit part of a card without resending the whole body
tasks section "Fix bug" Progress "Reproduced on staging"
tasks append --no-blank-line "Fix bug" -- "- [ ] write regression test"
tasks check "Fix bug" "regression test"
echo "Patch the null check" | tasks section "Fix bug" "Next action" -
# Move between lanes
tasks move "Fix bug" "In Progress"
# Delete (moves to Trash, prompts for confirmation)
tasks delete "Fix bug"
# Trash: list, restore, preview, then permanently delete (prompts for confirmation)
tasks trash list
tasks trash restore "Fix bug" Backlog
tasks trash empty --dry-run
tasks trash empty
# Lane management
tasks lanes list
tasks lanes add "Sprint 3"
# Column order: named lanes first, the rest keep their order after them
tasks lanes order # show it
tasks lanes order Doing Backlog # set it
tasks lanes order --reset # back to alphabetical
# Split a task (divides on [[split]] marker)
tasks split
# Statistics
tasks stats
```
## MCP Server
The MCP server exposes kanban operations to AI assistants (Claude, Cursor, etc.) via the [Model Context Protocol](https://modelcontextprotocol.io/).
### Connecting a client
Add to your Claude Desktop / Claude Code config (`~/.claude/claude_desktop_config.json` or `.mcp.json`):
```json
{
"mcpServers": {
"task-manager": {
"type": "http",
"url": "http://localhost:3003/mcp"
}
}
}
```
### Available tools
Every tool returns `{"ok": true, ...}` or `{"ok": false, "error": {"code", "message"}}`.
| Tool | Description |
|------|-------------|
| `info` | Server version, data folder, counts |
| `list_lanes` | Lanes with task counts, plus `trash_count` |
| `list_tasks` | Task summaries; lane/tag/due filters, sort, limit, optional bodies |
| `find_tasks` | Search titles and bodies |
| `get_task` | One task in full |
| `get_statistics` | Lane, tag, due-date, overdue and due-this-week counts |
| `add_task` | Create a task (refuses existing titles and unknown lanes) |
| `update_task` | Update task fields |
| `append_to_task` / `upsert_section` / `check_items` | Edit part of a card |
| `move_task_to_lane` | Move a task to another lane |
| `delete_task` | Move a task to Trash |
| `list_trash` / `restore_task` | See and undo deletions |
| `empty_trash` | Permanently delete Trash contents (`dry_run` supported) |
| `add_lane` | Create a new lane |
| `split_tasks` | Split tasks with `[[split]]` marker; skips (and reports) cards whose subtask titles are taken |
Title, lane and tag matching is **case-insensitive** across all tools. Writes
accept `expected_version` (from any task result) to guard against concurrent edits,
and return the card's summary, or the full card with `include_content=true`.
See [`mcp_task_service/README.md`](mcp_task_service/README.md) for full details.
## REST API
The REST API is documented in [`task_api/README.md`](task_api/README.md).
Quick reference:
```bash
# Health check
curl http://localhost:3101/health
# List tasks
curl "http://localhost:3101/tasks"
curl "http://localhost:3101/tasks?lane=Backlog&tag=urgent"
curl "http://localhost:3101/tasks?search=login" # title substring
curl "http://localhost:3101/tasks?query=books%20db&sort=due" # title or body
# Create a task
curl -X POST http://localhost:3101/tasks \
-H 'Content-Type: application/json' \
-d '{"title":"My Task","content":"Details","lane":"Backlog","tags":["urgent"]}'
# Move a task
curl -X POST "http://localhost:3101/tasks/My%20Task/move" \
-H 'Content-Type: application/json' \
-d '{"lane":"In Progress"}'
# Trash: list, preview emptying
curl http://localhost:3101/trash
curl -X DELETE "http://localhost:3101/operations/trash?dry_run=true"
# Statistics
curl http://localhost:3101/operations/statistics
```
## Task File Format
```markdown
[tag:frontend]
[tag:urgent]
[due:2026-06-01]
Task body text goes here.
```
Only lines that contain nothing but `[tag:…]` tokens (or a single `[due:…]`) are
metadata; a tag mentioned inside a sentence is ordinary text.
## Data Directory Structure
```
/data/tasks/
├── Backlog/
│ └── Implement login.md
├── In Progress/
│ └── Fix auth bug.md
├── Done/
│ └── Setup CI.md
└── Trash/
└── old-task~20261014-120102.md # timestamp suffix keeps same-named cards apart
```
## Development
### Set up
```bash
poetry install --extras dev # adds black, mypy and type stubs
```
### Run tests and checks
```bash
make test # pytest with coverage for task_lib, task_api, mcp_task_service
make lint # black --check and mypy on the modules kept clean so far
```
`make test` doesn't measure `task_tui`; for that, run
`poetry run pytest --cov=task_tui --cov-report=term-missing tests/`. Much of the
older code doesn't pass `black` or `mypy` yet, so `make lint` covers only the
files that do.
`tests/conftest.py` points `Path.home()` at an empty temporary directory and
clears `TASKS_CONFIG`, `TASKS_API_URL` and `XDG_STATE_HOME`, so your own
`~/.config/tasks/config.yaml` and saved lane order never affect a test run.
### Run the services locally
The committed `config.yaml` points at `/data/tasks`, the path inside the
containers. Locally, point `TASK_CONFIG_PATH` at a config for a scratch folder:
```bash
mkdir -p /tmp/tasks-dev && echo "base_dir: /tmp/tasks-dev" > /tmp/tasks-dev.yaml
TASK_CONFIG_PATH=/tmp/tasks-dev.yaml poetry run gunicorn --config task_api/gunicorn.conf.py "task_api.app:create_app()"
TASK_CONFIG_PATH=/tmp/tasks-dev.yaml poetry run python mcp_task_service/server.py
```
## Utilities
### Tag Format Migration
`bin/tag-utility.py` converts old-format tags (`tags: tag1, tag2`) to the current format (`[tag:tagname]`). Edit the hardcoded directory path in the script before running. Supports dry-run mode.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues